The psake/Invoke-Build task drift guard compares nothing against nothing
Nobody has claimed this yet.
- Dominant language
- PowerShell
- Stars
- 145
- Forks
- 27
- Avg merge
- 10h 16m
- Merged PRs (30d)
- 34
Description
tests/IBTasks.tests.ps1, the It 'Contains all the tasks that were in the Psake file', is the only guard that psake and Invoke-Build still define the same tasks. It cannot fail. Two independent defects cancel each other out, and fixing either one alone turns it red.
What happens
Defect one: $IBTasksResult is $null inside the It that reads it.
$IBTasksResult = $null # Describe body: runs at discovery only
It 'Parseable by invoke-build' {
$IBTasksResult = Start-Job { ... } | Wait-Job | Receive-Job # assigned here
...
}
It 'Contains all the tasks that were in the Psake file' {
$IBTaskNames = $IBTasksResult.all.name # ...and $null here
Each It block runs in its own scope, so the assignment in the first block is not visible in the second, and the $IBTasksResult = $null at the Describe level runs during discovery rather than during the run. $IBTaskNames is therefore $null.
Defect two: $psakeTaskNames is a collection of $null.
$psakeTaskNames = Start-Job -ScriptBlock {
Invoke-PSake -docs -buildfile $using:psakeFilePath | Where-Object name -notmatch '^(default|\?)$' | ForEach-Object name
} | Wait-Job | Receive-Job
Invoke-PSake -docs formats a table to the output stream. What crosses the job boundary is therefore format records, not task objects, and a format record has no Name property. Measured on this repository at 484f24f:
Deserialized.Microsoft.PowerShell.Commands.Internal.Format.FormatStartData 1
Deserialized.Microsoft.PowerShell.Commands.Internal.Format.GroupStartData 1
Deserialized.Microsoft.PowerShell.Commands.Internal.Format.FormatEntryData 17
Deserialized.Microsoft.PowerShell.Commands.Internal.Format.GroupEndData 1
Deserialized.Microsoft.PowerShell.Commands.Internal.Format.FormatEndData 1
$psakeTaskNames.Count = 21
non-null entries = 0
Where-Object name -notmatch ... also passes all 21 through, because $null -notmatch anything is true.
The two cancel. The loop becomes foreach ($taskItem in @($null, $null, ...)), the test is $null -notin $null, which evaluates to False, so the throw never fires. The closing assertion is @($null, $null, ...) | Should -Not -BeNullOrEmpty, which passes because the array has 21 elements even though every one of them is $null.
Measured: the guard does not fire on the divergence it exists to catch
Renaming a single task in the built IB.tasks.ps1 so it no longer matches psakeFile.ps1 — exactly the drift the test was written for:
-Task Sign SignModule, SignCatalog
+Task RenamedSignTask SignModule, SignCatalog
Describing Invoke-Build Tasks
[+] IB.tasks.ps1 exists
[+] Parseable by invoke-build
[+] Contains all the tasks that were in the Psake file <-- should be red
Tests Passed: 8, Failed: 0
PowerShell 7.6.5, Pester 6.1.0, psake 5.0.4, Windows 11.
Why it matters
Divergence between the two task files is not hypothetical; it is this repository's most productive defect class, and it reaches Invoke-Build consumers only.
- #178:
IB.tasks.ps1readTest.CodeCoverage.OutputFormatwhile the defaults defineOutputFileFormat, so no Invoke-Build consumer had a working code coverage format. - #193:
IB.tasks.ps1never passedSkipValidation, so$PSBPreference.Sign.SkipCertificateValidationwas dead for every Invoke-Build consumer while it worked for psake consumers.
Both were found by hand and then covered by the other Describe blocks in this same file — the settings-path comparison and the signing-settings comparison — which do work. The task-name comparison, the oldest of the three and the one everything else was layered on top of, has never worked. A task added to psakeFile.ps1 and forgotten in IB.tasks.ps1 would ship silently today.
Options
- Fix both defects and keep the test. Move the whole thing into one
It(or hoist the Invoke-Build run into aBeforeAllthat assigns to a$script:variable), and get real task names out of psake withGet-PSakeScriptTasks -buildFile $psakeFilePathinstead ofInvoke-PSake -docs, which returns objects rather than a formatted table. Both halves must change together; either alone makes the test fail for the wrong reason. - Replace it with a static comparison, like its two neighbours. The
Settings referenced by the task filesandSigning settings referenced by the task filesblocks in this same file parse the two task files with a regular expression and compare, with no job, no psake invocation, and no format-record hazard. Task names could be compared the same way, which would also make the test fast and immune to psake's output format changing again. - Delete it. It has never tested anything, and the two working
Describeblocks below it cover the settings-level drift that has actually bitten. This is the cheapest option and the one that loses real coverage, since neither of those blocks can see a task that exists in one file and not the other.
(2) looks like the best value: it keeps the coverage, matches the pattern the rest of the file already uses, and removes the job entirely.
Related: #178, #193.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in tests/IBTasks.tests.ps1 at the It blocks named 'Parseable by invoke-build' and 'Contains all the tasks that were in the Psake file', then run the Pester suite to reproduce the false pass. Compare the task-name guard with the neighboring settings and signing comparisons. Done means the unchanged files pass and a deliberate task-name divergence fails.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- powershell
- Domain
- build-system, testing-qa
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100