src/PSMutation.Killers.ps1
|
# Which tests to run FIRST for a mutant (#254): the tests that already killed a mutant near it. # # A killed mutant runs its covering files until the first test fails, and most of a run's time is # spent there -- measured on this repo's self-run, one source file took 74% of all worker time # because its covering suite takes 26s and a kill stops only at the first failure. One named test # takes 0.22s. And mutants in one function are mostly killed by the same few tests: over 1584 # kills, trying the tests that had already killed a mutant in the same function, then the same # file, would have caught at least 61% with three names tried -- at least, because the report # records only the first killer of each. # # Learned within the run, from mutants already finished, so a first run gets it too and no # report has to be kept between runs. # # A name is TRUSTED only once it has passed on its own against unmutated source. A test that # depends on an earlier test's state fails alone whatever the code, and trusted unproven it would # kill every mutant it was tried on -- a perfect score that nothing flags. The runner proves each # name once, on an idle worker, before any mutant is run against it; a name that fails alone, or # matches no test, is never tried. # # Pure: the runner owns the jobs and hands each outcome here. function New-PSMutationKillerMemory { # An empty memory for one run. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure: returns a new object, changes no state.')] [OutputType([pscustomobject])] [CmdletBinding()] param() return [pscustomobject]@{ # "file`nfunction" -> the names that killed a mutant there, oldest first. ByFunction = @{} # file -> the same, for the whole file. ByFile = @{} Proven = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) # name -> seconds its proof took, which sizes the deadline of a first pass that runs it. Seconds = [System.Collections.Generic.Dictionary[string, double]]::new([System.StringComparer]::Ordinal) # Every name ever queued for proof, so a name is proven once however often it kills. Queued = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) # Waiting for proof: { Name; Suite }, in the order they were learned. Pending = [System.Collections.Generic.List[object]]::new() } } function Add-PSMutationKiller { # Record the tests that killed one mutant, and queue for proof each name never queued before. [OutputType([void])] [CmdletBinding()] param( [Parameter(Mandatory)] $Memory, [Parameter(Mandatory)] [string]$File, [Parameter(Mandatory)] [AllowEmptyString()] [string]$Function, # The killers by the UNEXPANDED path a Pester filter matches, not the report's names. [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]]$Name, # The covering files these names were found in, which a proof runs. [Parameter(Mandatory)] [string[]]$Suite ) $fnKey = "$File`n$Function" foreach ($n in $Name) { if (-not $Memory.ByFunction.ContainsKey($fnKey)) { $Memory.ByFunction[$fnKey] = [System.Collections.Generic.List[string]]::new() } if (-not $Memory.ByFile.ContainsKey($File)) { $Memory.ByFile[$File] = [System.Collections.Generic.List[string]]::new() } $Memory.ByFunction[$fnKey].Add($n) $Memory.ByFile[$File].Add($n) if ($Memory.Queued.Add($n)) { $Memory.Pending.Add([pscustomobject]@{ Name = $n; Suite = $Suite }) } } } function Get-PSMutationFirstTest { # The proven names to try first for a mutant: the latest killers in its function, then in its # file, each once, at most Limit. # # Latest first because a function's mutants are numbered in source order, so the one finished # just before sits next to this one. Three is where the measured hit rate flattens -- 61% at 3, # 64% at 5, 65% at 10 -- and every name costs a survivor a little more. [OutputType([string[]])] [CmdletBinding()] param( [Parameter(Mandatory)] $Memory, [Parameter(Mandatory)] [string]$File, [Parameter(Mandatory)] [AllowEmptyString()] [string]$Function, [int]$Limit = 3 ) $picked = [System.Collections.Generic.List[string]]::new() $lists = @($Memory.ByFunction["$File`n$Function"], $Memory.ByFile[$File]) foreach ($list in $lists) { # A foreach STATEMENT over a reversed copy: an absent list iterates zero times. foreach ($n in [System.Linq.Enumerable]::Reverse([string[]]@($list | Where-Object { $_ }))) { if ($picked.Count -eq $Limit) { return [string[]]$picked } if ($Memory.Proven.Contains($n) -and -not $picked.Contains($n)) { $picked.Add($n) } } } return [string[]]$picked } function Get-PSMutationProofBatch { # Take the waiting names that share the first one's covering files, to prove in one run: # { Suite; Name }. $null when nothing waits. [OutputType([pscustomobject])] [CmdletBinding()] param([Parameter(Mandatory)] $Memory) if ($Memory.Pending.Count -eq 0) { return $null } $suite = $Memory.Pending[0].Suite $key = $suite -join "`n" $batch = @($Memory.Pending | Where-Object { ($_.Suite -join "`n") -eq $key }) foreach ($b in $batch) { [void]$Memory.Pending.Remove($b) } return [pscustomobject]@{ Suite = $suite; Name = [string[]]@($batch | ForEach-Object { $_.Name }) } } function Complete-PSMutationProof { # Record what one proof run said: a name is proven when a test by it passed and none by it # failed -- a -ForEach test is one name with several cases. One that failed, or matched no test, stays unproven for the rest of the run -- it was # queued once and is not queued again -- and so is never tried first. [OutputType([void])] [CmdletBinding()] param( [Parameter(Mandatory)] $Memory, [Parameter(Mandatory)] [string[]]$Name, [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]]$PassedPath, [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]]$FailedPath, # How long the proof took, kept against every name it proved. [Parameter(Mandatory)] [double]$Seconds ) foreach ($n in $Name) { if (($PassedPath -ccontains $n) -and -not ($FailedPath -ccontains $n)) { [void]$Memory.Proven.Add($n) $Memory.Seconds[$n] = $Seconds } } } function Get-PSMutationFirstPassTimeout { # The deadline of a first pass over these names, in whole seconds: the rule every deadline # follows -- the slowest name's proof time, times the factor and the workers sharing the # machine, never under the floor -- and never over the whole run's own deadline, Cap. [OutputType([int])] [CmdletBinding()] param( [Parameter(Mandatory)] $Memory, [Parameter(Mandatory)] [string[]]$Name, # { Factor; Floor }: the run's timeoutFactor and timeoutFloorSeconds. [Parameter(Mandatory)] $Rule, [Parameter(Mandatory)] [int]$Workers, [Parameter(Mandatory)] [int]$Cap ) $slowest = ($Name | ForEach-Object { $Memory.Seconds[$_] } | Measure-Object -Maximum).Maximum $sized = [math]::Max([double]$Rule.Floor, $slowest * $Rule.Factor * $Workers) return [int][math]::Min($Cap, [math]::Ceiling($sized)) } |