From 5ba0adef02c8f13aa0a43ac0a2f101180f18add8 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Sun, 27 Sep 2026 14:47:49 -0500 Subject: [PATCH 1/2] ci: don't run the full test suite for pure export additions to PiHoleShell.psm1 Every feature PR adds its new functions' names to PiHoleShell.psm1's Export-ModuleMember list, which previously always forced the full Pester suite to run on a PR build - the one file nearly every PR touches was exactly the one that defeated the changed-file scoping added in an earlier PR. A pure addition to the export list can't break any existing, already-passing test, so only fall back to the full suite when an existing export was actually removed. Comparing whole diff lines doesn't work here since several exports share one line - adding a new function to an existing category's line shows as that whole line being removed and re-added, even though nothing already there was dropped. Instead, this extracts the quoted names from just the removed lines and just the added lines and compares those sets: a name that reappears in the added lines was only reformatted, not actually removed. Verified against real PR diffs from this session (via git worktree, without touching the live pipeline): a pure-addition change (adding Config's new functions to the Config category's existing line) correctly skips the fallback, and a synthetic removal correctly still triggers it. Co-Authored-By: Claude Sonnet 5 --- azuredevops-pihole-pester-tests.yml | 30 +++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/azuredevops-pihole-pester-tests.yml b/azuredevops-pihole-pester-tests.yml index 8c59fba..c954956 100644 --- a/azuredevops-pihole-pester-tests.yml +++ b/azuredevops-pihole-pester-tests.yml @@ -82,8 +82,34 @@ steps: foreach ($file in $changedFiles) { if ($runFullSuite) { break } - if ($file -match '^PiHoleShell/Private/' -or - $file -eq 'PiHoleShell/PiHoleShell.psm1' -or + if ($file -eq 'PiHoleShell/PiHoleShell.psm1') { + # A pure addition to the export list can't break any existing, already- + # passing test, so only fall back to the full suite when an existing + # export was actually removed. Comparing whole diff lines doesn't work + # here since several exports share one line - adding a new function to + # an existing category's line shows as that whole line being removed + # and re-added, even though nothing already there was dropped. Instead, + # extract the quoted names from just the removed lines and just the + # added lines and compare those sets: a name that reappears in the + # added lines was only reformatted, not actually removed. + $manifestDiff = git diff "origin/$targetBranch...HEAD" -- $file + if ($LASTEXITCODE -ne 0) { throw "git diff failed for '$file'" } + + $removedLines = ($manifestDiff | Where-Object { $_ -match '^-' -and $_ -notmatch '^---' }) -join "`n" + $addedLines = ($manifestDiff | Where-Object { $_ -match '^\+' -and $_ -notmatch '^\+\+\+' }) -join "`n" + $removedNames = [regex]::Matches($removedLines, "'([A-Za-z0-9-]+)'") | ForEach-Object { $_.Groups[1].Value } + $addedNames = [regex]::Matches($addedLines, "'([A-Za-z0-9-]+)'") | ForEach-Object { $_.Groups[1].Value } + + $trulyRemoved = $removedNames | Where-Object { $_ -notin $addedNames } + if ($trulyRemoved) { + Write-Host "Export(s) removed from $file ($($trulyRemoved -join ', ')) - running the full suite" + $runFullSuite = $true + } + else { + Write-Host "Only additions in $file - no full-suite fallback needed" + } + } + elseif ($file -match '^PiHoleShell/Private/' -or $file -eq 'PiHoleShell/PiHoleShell.psd1' -or $file -eq 'PiHoleShell/Public/Authentication/Request-PiHoleAuth.ps1' -or $file -eq 'tests/Initialize-PiHoleTestData.ps1' -or From d513bc1229c771633e62b8e23f8ae005b7d374d0 Mon Sep 17 00:00:00 2001 From: Mike Madeja Date: Mon, 28 Sep 2026 20:11:45 -0500 Subject: [PATCH 2/2] test: fix flaky Update-PiHoleClient tests missing a create-settle delay This test file's Update-PiHoleClient tests were failing intermittently in CI (3 times in a row across recent builds) with either a 400 on the Update-PiHoleClient call itself or a cascading "already exists" on the next test's New-PiHoleClient call. Every It block calls New-PiHoleClient immediately followed by Update-PiHoleClient with zero delay between them - the real Pi Zero W test server needs a moment to make a just-created client queryable/updatable again, the same known hardware limitation already worked around in tools/Update-ExampleOutput.ps1 for Lists/Domains/Groups, but this test file never got the same fix. Verified with a clean 7/7 pass against the real server after adding a short Start-Sleep between each New-PiHoleClient and the Update-PiHoleClient call that depends on it. Co-Authored-By: Claude Sonnet 5 --- .../Update-PiHoleClient.Integration.Tests.ps1 | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 b/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 index d3461de..b07af5d 100644 --- a/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 +++ b/tests/ClientManagement/Update-PiHoleClient.Integration.Tests.ps1 @@ -38,6 +38,11 @@ Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' { It 'updates only the comment, preserving Group' -Skip:(-not $script:ConfigAvailable) { New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'original comment' | Out-Null + # The test server takes a moment to make a just-added client queryable/updatable again - + # a real hardware limitation of this Pi Zero W, not a module bug (same settle-time + # reasoning as tools/Update-ExampleOutput.ps1 uses for Lists/Domains/Groups). + Start-Sleep -Seconds 2 + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'updated comment' $result | Format-List | Out-String | Write-Host @@ -51,6 +56,8 @@ Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' { It 'updates only Group, preserving the current comment' -Skip:(-not $script:ConfigAvailable) { New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'keep this comment' | Out-Null + Start-Sleep -Seconds 2 + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group $script:TestGroupName $result | Format-List | Out-String | Write-Host @@ -64,6 +71,8 @@ Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' { It 'errors when -Group names a group that does not exist' -Skip:(-not $script:ConfigAvailable) { New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + Start-Sleep -Seconds 2 + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Group 'DefinitelyNotARealGroup' -ErrorVariable errOut -ErrorAction SilentlyContinue Write-Host "Error ($($errOut.Count) entries, showing last): [$($errOut[-1])]" @@ -75,6 +84,8 @@ Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' { It 'returns the raw API response when RawOutput is set' -Skip:(-not $script:ConfigAvailable) { New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + Start-Sleep -Seconds 2 + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'raw output test' -RawOutput $true $result | Format-List | Out-String | Write-Host @@ -86,6 +97,8 @@ Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' { It 'errors when neither Comment nor Group is specified' -Skip:(-not $script:ConfigAvailable) { New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + Start-Sleep -Seconds 2 + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -ErrorVariable errOut -ErrorAction SilentlyContinue $errOut | Should -Not -BeNullOrEmpty @@ -102,6 +115,8 @@ Describe 'Update-PiHoleClient (Integration)' -Tag 'Integration' { It 'errors when given a bad password' -Skip:(-not $script:ConfigAvailable) { New-PiHoleClient -PiHoleServer $script:PiHoleServer -Password $script:PiHoleToken -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient | Out-Null + Start-Sleep -Seconds 2 + $result = Update-PiHoleClient -PiHoleServer $script:PiHoleServer -Password 'definitely-not-the-real-token' -IgnoreSsl $script:PiHoleIgnoreSsl -Client $script:TestClient -Comment 'irrelevant' -ErrorVariable errOut -ErrorAction SilentlyContinue $errOut | Should -Not -BeNullOrEmpty