Skip to content

Commit 5a483b5

Browse files
authored
Enhance progress tracking and testing for Nova workflows (#241)
* #240 fix: Enhance progress tracking in Nova workflows and tests - Added detailed progress assertions in Invoke-NovaBuildWorkflow, Invoke-NovaPackageUploadWorkflow, Invoke-NovaPackageWorkflow, Invoke-NovaTestWorkflow, Invoke-NovaPublishWorkflow, Invoke-NovaReleaseWorkflow, Invoke-NovaModuleInitializationWorkflow, and Invoke-NovaModuleSelfUpdateWorkflow tests to ensure accurate reporting of workflow states. - Implemented a new test suite to verify that all workflows writing progress have corresponding test files, ensuring consistency and coverage. - Updated mock functions to capture and validate messages related to workflow actions, improving test reliability and output verification. * #240 fix: add tests for Get-NovaPesterExecution and Get-NovaPublishWorkflowPropertyValue functions
1 parent b3c01e7 commit 5a483b5

15 files changed

Lines changed: 1004 additions & 132 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
2020

2121
### Fixed
2222

23+
- `Publish-NovaModule` now keeps its completion summary available after local publish import or CI session refresh steps reload the module, so local publish flows no longer fail with missing private release-helper functions at the end of a successful publish.
24+
- `Test-NovaBuild` and `% nova test` now keep the Nova progress display visibly active during long Pester runs instead of appearing stuck on one step while tests continue in the background.
25+
- During the long Pester phase, Nova now uses the discovered test count plus completed test results to drive the progress bar, so the visible percentage tracks real test completion instead of elapsed time.
26+
- The progress text now stays simple while the bar itself reflects the real test-driven completion state.
27+
- The configured Pester output still flows live so `project.json` settings such as `Pester.Output.Verbosity = "Detailed"` remain visible.
28+
- Progress-enabled private workflows now have stronger mirrored progress-contract tests, and the repository adds a guardrail test so future `Write-Progress` workflows cannot land without matching test ownership.
29+
2330
### Security
2431

2532
## [3.1.0] - 2026-05-24

RELEASE_NOTE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang
1919

2020
### Fixed
2121

22+
- `Publish-NovaModule` no longer loses its completion summary after local publish import or CI session refresh steps reload the module.
23+
- `Test-NovaBuild` and `% nova test` now keep the Nova progress display visibly active during long Pester runs instead of appearing stuck on one step while tests continue.
24+
- During the long Pester phase, Nova now drives the progress bar from discovered and completed Pester tests instead of elapsed time, keeps the progress text simple, and still shows configured Pester output such as `Pester.Output.Verbosity = "Detailed"`.
25+
2226
### Security
2327

2428
## [3.1.0] - 2026-05-24

src/private/quality/InvokeNovaTestWorkflow.ps1

Lines changed: 312 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,22 @@ function Invoke-NovaTestWorkflowExecution {
7171

7272
$WorkflowContext.PesterConfig.TestResult.OutputPath = $WorkflowContext.TestResultPath
7373
$coverageTargetAssertion = Get-NovaCoverageTargetAssertionScriptBlock -WorkflowContext $WorkflowContext
74-
$testResult = Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Running Pester tests' -PercentComplete 70 -Action {
75-
Invoke-NovaPesterWithSuppressedProgress -Configuration $WorkflowContext.PesterConfig
74+
$testProgressContext = [pscustomobject]@{
75+
Activity = $Activity
76+
StartPercentComplete = 70
77+
EndPercentComplete = 94
7678
}
79+
$testResult = Invoke-NovaPesterWithSuppressedProgress -Configuration $WorkflowContext.PesterConfig -ProgressContext $testProgressContext
7780

78-
Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Writing the test result report' -PercentComplete 85 -Action {
81+
Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Writing the test result report' -PercentComplete 96 -Action {
7982
& $WorkflowContext.TestResultArtifactWriter.ScriptBlock -TestResult $testResult -OutputPath $WorkflowContext.TestResultPath -ReportWriter $WorkflowContext.TestResultReportWriter.ScriptBlock
8083
}
8184

8285
if ($testResult.Result -ne 'Passed') {
8386
Stop-NovaOperation -Message (Get-NovaTestWorkflowFailureMessage -WorkflowContext $WorkflowContext) -ErrorId 'Nova.Workflow.TestRunFailed' -Category InvalidOperation -TargetObject $WorkflowContext.TestResultPath
8487
}
8588

86-
Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Checking the configured code coverage target' -PercentComplete 95 -Action {
89+
Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Checking the configured code coverage target' -PercentComplete 99 -Action {
8790
& $coverageTargetAssertion -WorkflowContext $WorkflowContext -TestResult $testResult
8891
}
8992

@@ -119,16 +122,316 @@ function Get-NovaTestWorkflowBuildStatus {
119122
function Invoke-NovaPesterWithSuppressedProgress {
120123
[CmdletBinding()]
121124
param(
122-
[Parameter(Mandatory)][object]$Configuration
125+
[Parameter(Mandatory)][object]$Configuration,
126+
[Parameter(Mandatory)][pscustomobject]$ProgressContext
123127
)
124128

125-
$previousProgressPreference = $global:ProgressPreference
126-
$global:ProgressPreference = 'SilentlyContinue'
129+
$heartbeatMilliseconds = Get-NovaPropertyValue -InputObject $ProgressContext -Name 'HeartbeatMilliseconds'
130+
if ($null -eq $heartbeatMilliseconds) {
131+
$heartbeatMilliseconds = 2000
132+
}
133+
134+
$execution = Get-NovaPesterExecution -Configuration $Configuration
127135
try {
128-
return Invoke-NovaPester -Configuration $Configuration
136+
Write-NovaTestWorkflowPesterProgress -Execution $execution -ProgressContext $ProgressContext
137+
while (-not (Wait-NovaPesterExecution -Execution $execution -TimeoutMilliseconds $HeartbeatMilliseconds)) {
138+
Write-NovaPesterExecutionOutput -Execution $execution
139+
Write-NovaTestWorkflowPesterProgress -Execution $execution -ProgressContext $ProgressContext
140+
}
141+
142+
Write-NovaPesterExecutionOutput -Execution $execution
143+
Write-NovaTestWorkflowPesterProgress -Execution $execution -ProgressContext $ProgressContext
144+
return Receive-NovaPesterExecutionResult -Execution $execution
129145
} finally {
130-
$global:ProgressPreference = $previousProgressPreference
146+
Complete-NovaPesterExecution -Execution $execution
147+
}
148+
}
149+
150+
function Write-NovaPesterExecutionOutput {
151+
[CmdletBinding()]
152+
param(
153+
[Parameter(Mandatory)][pscustomobject]$Execution
154+
)
155+
156+
$informationRecords = @(Get-NovaPesterExecutionInformationRecordBuffer -Execution $Execution)
157+
$nextInformationRecordIndex = Get-NovaPropertyValue -InputObject $Execution -Name 'NextInformationRecordIndex'
158+
if ($null -eq $nextInformationRecordIndex) {
159+
$nextInformationRecordIndex = 0
131160
}
161+
162+
while ($nextInformationRecordIndex -lt $informationRecords.Count) {
163+
$record = $informationRecords[$nextInformationRecordIndex]
164+
Invoke-NovaPesterExecutionProgressStateUpdate -Execution $Execution -Record $record
165+
Write-NovaPesterInformationRecord -Record $record
166+
$nextInformationRecordIndex += 1
167+
}
168+
169+
$Execution.NextInformationRecordIndex = $nextInformationRecordIndex
170+
}
171+
172+
function Get-NovaPesterExecutionInformationRecordBuffer {
173+
[CmdletBinding()]
174+
param(
175+
[Parameter(Mandatory)][pscustomobject]$Execution
176+
)
177+
178+
$powershell = Get-NovaPropertyValue -InputObject $Execution -Name 'PowerShell'
179+
if ($null -eq $powershell) {
180+
return @()
181+
}
182+
183+
return @(Get-NovaPropertyValue -InputObject $powershell.Streams -Name 'Information')
184+
}
185+
186+
function Write-NovaPesterInformationRecord {
187+
[CmdletBinding()]
188+
param(
189+
[Parameter(Mandatory)][object]$Record
190+
)
191+
192+
$messageData = Get-NovaPropertyValue -InputObject $Record -Name 'MessageData'
193+
$tags = @(Get-NovaPropertyValue -InputObject $Record -Name 'Tags')
194+
if (($tags -contains 'PSHOST') -and (Test-NovaPesterHostInformationMessage -MessageData $messageData)) {
195+
Write-NovaPesterHostInformationMessage -MessageData $messageData
196+
return
197+
}
198+
199+
Write-Information -MessageData $messageData -Tags $tags -InformationAction Continue
200+
}
201+
202+
function Invoke-NovaPesterExecutionProgressStateUpdate {
203+
[CmdletBinding()]
204+
param(
205+
[Parameter(Mandatory)][pscustomobject]$Execution,
206+
[Parameter(Mandatory)][object]$Record
207+
)
208+
209+
$messageText = Get-NovaPesterInformationMessageText -Record $Record
210+
$discoveredTestCount = Get-NovaPesterDiscoveredTestCount -MessageText $messageText
211+
if ($null -ne $discoveredTestCount) {
212+
$Execution.TotalTestCount = $discoveredTestCount
213+
return
214+
}
215+
216+
if (Test-NovaPesterTestCompletionMessage -Record $Record -MessageText $messageText) {
217+
$Execution.CompletedTestCount = (Get-NovaPropertyValue -InputObject $Execution -Name 'CompletedTestCount') + 1
218+
}
219+
}
220+
221+
function Get-NovaPesterInformationMessageText {
222+
[CmdletBinding()]
223+
param(
224+
[Parameter(Mandatory)][object]$Record
225+
)
226+
227+
$messageData = Get-NovaPropertyValue -InputObject $Record -Name 'MessageData'
228+
if (Test-NovaPesterHostInformationMessage -MessageData $messageData) {
229+
return [string](Get-NovaPropertyValue -InputObject $messageData -Name 'Message')
230+
}
231+
232+
return [string]$messageData
233+
}
234+
235+
function Get-NovaPesterDiscoveredTestCount {
236+
[CmdletBinding()]
237+
param(
238+
[AllowNull()][string]$MessageText
239+
)
240+
241+
$match = [System.Text.RegularExpressions.Regex]::Match([string]$MessageText, 'Discovery found (?<Count>\d+) tests? in')
242+
if (-not $match.Success) {
243+
return $null
244+
}
245+
246+
return [int]$match.Groups['Count'].Value
247+
}
248+
249+
function Test-NovaPesterTestCompletionMessage {
250+
[CmdletBinding()]
251+
param(
252+
[Parameter(Mandatory)][object]$Record,
253+
[AllowNull()][string]$MessageText
254+
)
255+
256+
$messageData = Get-NovaPropertyValue -InputObject $Record -Name 'MessageData'
257+
if (-not (Test-NovaPesterHostInformationMessage -MessageData $messageData)) {
258+
return $false
259+
}
260+
261+
if ($true -ne [bool](Get-NovaPropertyValue -InputObject $messageData -Name 'NoNewLine')) {
262+
return $false
263+
}
264+
265+
return [string]$MessageText -match '^\s+\[(\+|-|!|\?)\]\s+'
266+
}
267+
268+
function Test-NovaPesterHostInformationMessage {
269+
[CmdletBinding()]
270+
param(
271+
[AllowNull()][object]$MessageData
272+
)
273+
274+
return $null -ne $MessageData -and $MessageData.PSObject.Properties.Name -contains 'Message'
275+
}
276+
277+
function Write-NovaPesterHostInformationMessage {
278+
[CmdletBinding()]
279+
param(
280+
[Parameter(Mandatory)][object]$MessageData
281+
)
282+
283+
$writeHostParameters = @{
284+
Object = $MessageData.Message
285+
}
286+
287+
if ($true -eq [bool](Get-NovaPropertyValue -InputObject $MessageData -Name 'NoNewLine')) {
288+
$writeHostParameters.NoNewline = $true
289+
}
290+
291+
foreach ($colorName in 'ForegroundColor', 'BackgroundColor') {
292+
$colorValue = Get-NovaPropertyValue -InputObject $MessageData -Name $colorName
293+
if ($null -ne $colorValue) {
294+
$writeHostParameters[$colorName] = $colorValue
295+
}
296+
}
297+
298+
Write-Host @writeHostParameters
299+
}
300+
301+
function Get-NovaPesterExecution {
302+
[CmdletBinding()]
303+
param(
304+
[Parameter(Mandatory)][object]$Configuration
305+
)
306+
307+
$powershell = [powershell]::Create()
308+
$command = @'
309+
param($Configuration)
310+
Import-Module Pester -ErrorAction Stop
311+
$previousProgressPreference = $global:ProgressPreference
312+
$global:ProgressPreference = 'SilentlyContinue'
313+
try {
314+
Invoke-Pester -Configuration $Configuration
315+
} finally {
316+
$global:ProgressPreference = $previousProgressPreference
317+
}
318+
'@
319+
$null = $powershell.AddScript($command).AddArgument($Configuration)
320+
321+
return [pscustomobject]@{
322+
PowerShell = $powershell
323+
AsyncResult = $powershell.BeginInvoke()
324+
CompletedTestCount = 0
325+
NextInformationRecordIndex = 0
326+
TotalTestCount = $null
327+
LastProgressStatus = $null
328+
LastProgressPercentComplete = $null
329+
}
330+
}
331+
332+
function Wait-NovaPesterExecution {
333+
[CmdletBinding()]
334+
param(
335+
[Parameter(Mandatory)][pscustomobject]$Execution,
336+
[Parameter(Mandatory)][int]$TimeoutMilliseconds
337+
)
338+
339+
return $Execution.AsyncResult.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)
340+
}
341+
342+
function Receive-NovaPesterExecutionResult {
343+
[CmdletBinding()]
344+
param(
345+
[Parameter(Mandatory)][pscustomobject]$Execution
346+
)
347+
348+
$output = $Execution.PowerShell.EndInvoke($Execution.AsyncResult)
349+
return @($output | Select-Object -Last 1)[0]
350+
}
351+
352+
function Complete-NovaPesterExecution {
353+
[CmdletBinding()]
354+
param(
355+
[AllowNull()][pscustomobject]$Execution
356+
)
357+
358+
if ($null -eq $Execution) {
359+
return
360+
}
361+
362+
$powershell = Get-NovaPropertyValue -InputObject $Execution -Name 'PowerShell'
363+
if ($null -eq $powershell) {
364+
return
365+
}
366+
367+
$powershell.Dispose()
368+
}
369+
370+
function Write-NovaTestWorkflowPesterProgress {
371+
[CmdletBinding()]
372+
param(
373+
[Parameter(Mandatory)][pscustomobject]$Execution,
374+
[Parameter(Mandatory)][pscustomobject]$ProgressContext
375+
)
376+
377+
$activity = Get-NovaPropertyValue -InputObject $ProgressContext -Name 'Activity'
378+
$startPercentComplete = Get-NovaPropertyValue -InputObject $ProgressContext -Name 'StartPercentComplete'
379+
$endPercentComplete = Get-NovaPropertyValue -InputObject $ProgressContext -Name 'EndPercentComplete'
380+
$totalTestCount = Get-NovaPropertyValue -InputObject $Execution -Name 'TotalTestCount'
381+
$completedTestCount = Get-NovaPropertyValue -InputObject $Execution -Name 'CompletedTestCount'
382+
$status = Get-NovaTestWorkflowPesterStatus -TotalTestCount $totalTestCount
383+
$percentComplete = Get-NovaTestWorkflowPesterPercentComplete -StartPercentComplete $startPercentComplete -EndPercentComplete $endPercentComplete -CompletedTestCount $completedTestCount -TotalTestCount $totalTestCount
384+
385+
if (($Execution.LastProgressStatus -eq $status) -and ($Execution.LastProgressPercentComplete -eq $percentComplete)) {
386+
return
387+
}
388+
389+
Write-Progress -Activity $activity -Status $status -PercentComplete $percentComplete
390+
$Execution.LastProgressStatus = $status
391+
$Execution.LastProgressPercentComplete = $percentComplete
392+
}
393+
394+
function Get-NovaTestWorkflowPesterStatus {
395+
[CmdletBinding()]
396+
param(
397+
[AllowNull()][object]$TotalTestCount
398+
)
399+
400+
if ($null -eq $TotalTestCount) {
401+
return 'Discovering Pester tests'
402+
}
403+
404+
return 'Running Pester tests'
405+
}
406+
407+
function Get-NovaTestWorkflowPesterPercentComplete {
408+
[CmdletBinding()]
409+
param(
410+
[Parameter(Mandatory)][int]$StartPercentComplete,
411+
[Parameter(Mandatory)][int]$EndPercentComplete,
412+
[Parameter(Mandatory)][int]$CompletedTestCount,
413+
[AllowNull()][object]$TotalTestCount
414+
)
415+
416+
if ($null -eq $TotalTestCount) {
417+
return $StartPercentComplete
418+
}
419+
420+
if ($TotalTestCount -le 0) {
421+
return $EndPercentComplete
422+
}
423+
424+
$progressRange = $EndPercentComplete - $StartPercentComplete
425+
$percentComplete = $StartPercentComplete + [int][math]::Floor(($CompletedTestCount / $TotalTestCount) * $progressRange)
426+
if ($percentComplete -gt $EndPercentComplete) {
427+
return $EndPercentComplete
428+
}
429+
430+
if ($percentComplete -lt $StartPercentComplete) {
431+
return $StartPercentComplete
432+
}
433+
434+
return $percentComplete
132435
}
133436

134437
function Get-NovaTestWorkflowFailureMessage {

0 commit comments

Comments
 (0)