src/BaselineFile.ps1
|
# The baseline FILE: reading it, refusing it, and writing it back. # # Apart from Policy.ps1, which decides what a baseline MEANS, because this half touches a disk # and that half must stay pure. Apart from Measure-PSComplexity.ps1, which owns the scan and the # two public commands, for a reason that is measured rather than aesthetic: a covering suite is # re-run once per mutant of the file it covers, and Measure.Tests.ps1 takes 18 seconds because it # measures real source. These two functions need no measurement at all -- unit records are # ordinary objects and a baseline is a small JSON file -- so leaving their 28 mutants in that # file cost about eight minutes of every gate run for nothing. # # MetricVersion arrives as a parameter rather than being read from Measure-PSComplexity.ps1's # module state. A $script: variable is read only by the file that writes it, and reaching across # would also make the dependency circular: the gate calls in here, so here cannot call back. function Get-PSCxBaselineState { # The recorded scores to hold units to, having refused every way the file could fail to # describe this run. An empty map when no baseline was asked for, which is what makes every # unit face the real ceilings. # # The I/O and the refusals live here rather than in the gate for the reason every decision in # this module does: Test-PSComplexity is a thin predicate, and a baseline that half-applies is # the failure mode worth keeping out of it. [OutputType([hashtable])] [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyString()] [string]$Path, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Unit, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Accept, [Parameter(Mandatory)] [int]$MaxCyclomatic, [Parameter(Mandatory)] [int]$MaxCognitive, [Parameter(Mandatory)] [int]$MetricVersion ) if (-not $Path) { return @{} } $document = Read-PSCxDocument -Path $Path $documentFault = Get-PSCxBaselineDocumentFault -Document $document ` -MetricVersion $MetricVersion -SchemaVersion (Get-PSCxBaselineSchemaVersion) if ($documentFault) { throw "Cannot use the baseline $Path : $documentFault." } $entries = @($document.units) # Thrown, not returned as a failing gate. A baseline entry that no longer describes the run # is a fault in the policy rather than a complaint about the code, and reporting it as a # failure would send somebody to refactor a unit that is fine. $faults = @($entries | Get-PSCxBaselineFault -Unit $Unit -Accept $Accept ` -MaxCyclomatic $MaxCyclomatic -MaxCognitive $MaxCognitive) if ($faults.Count -gt 0) { throw ("The baseline $Path does not describe this run: " + ($faults -join '; ') + '. A ratchet that has stopped matching is a suppression list, so it fails here rather than ageing quietly.') } return Get-PSCxBaselineMap -Entry $entries } function Write-PSCxBaselineFile { # Record every breaching, unaccepted unit at the score it has now -- refusing, first, to # record any of them worse than the file already does. # # That refusal is the whole ratchet. Without it, re-running with -UpdateBaseline absorbs # whatever regression the gate had just caught, and the file becomes a suppression list that # updates itself. # No SupportsShouldProcess. Its only caller is Test-PSComplexity, which has no -WhatIf to # forward, so the guard could never return false -- a branch that cannot fire looks exactly # like one that keeps passing, and it would sit here at 100% coverage saying nothing. [OutputType([void])] [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Unit, [Parameter(Mandatory)] [AllowEmptyCollection()] [object[]]$Accept, [Parameter(Mandatory)] [int]$MaxCyclomatic, [Parameter(Mandatory)] [int]$MaxCognitive, [Parameter(Mandatory)] [int]$MetricVersion ) # Absent is the ordinary first run, not an error: seeding a baseline is the reason this # switch exists. $existing = if (Test-Path -LiteralPath $Path -PathType Leaf) { Read-PSCxDocument -Path $Path } else { $null } # Only compare against a file whose numbers still mean the same thing. When the metric # version has moved, the recorded scores are not smaller or larger -- they are answers to a # different question, and a ratchet cannot be enforced across that. Regenerating wholesale is # the fix, and the diff is where it gets reviewed. $comparable = $null -ne $existing -and -not (Get-PSCxBaselineDocumentFault -Document $existing ` -MetricVersion $MetricVersion -SchemaVersion (Get-PSCxBaselineSchemaVersion)) if ($comparable) { $raised = @(Get-PSCxBaselineRaise -Unit $Unit -Map (Get-PSCxBaselineMap -Entry @($existing.units))) if ($raised.Count -gt 0) { throw ("Refusing to update $Path : " + (($raised | ForEach-Object { "$($_.File) $($_.Unit) is now cyclomatic $($_.Cyclomatic), cognitive $($_.Cognitive)" }) -join '; ') + ' -- worse than recorded. A baseline only ever ratchets down; fix the regression, or accept the unit with an argument for it.') } } Save-PSCxDocument -Path $Path -Document (Get-PSCxBaselineDocument -Unit $Unit -Accept $Accept ` -MaxCyclomatic $MaxCyclomatic -MaxCognitive $MaxCognitive ` -MetricVersion $MetricVersion -SchemaVersion (Get-PSCxBaselineSchemaVersion) ` -GeneratedAt (Get-Date)) } |