packages feed

sydtest-mutation-driver 0.2.0.0 → 0.3.0.0

raw patch · 5 files changed

+86/−23 lines, 5 files

Files

CHANGELOG.md view
@@ -1,5 +1,17 @@ # Changelog +## [0.3.0.0] - 2026-10-01++### Changed++* A mutation that only overran its budget is given another go, up to three+  attempts.+* A run with a failed control exits non-zero, so its verdict is not cached+  and the run can simply be retried.+* `assert-score` fails on a mutation that ran out of time, and so does a+  `--fail-fast` run.++ ## [0.2.0.0] - 2026-09-11  ### Fixed
src/Test/Syd/Mutation/Driver.hs view
@@ -33,12 +33,12 @@   ( AugmentedManifest (..),     AugmentedMutationGroup (..),     AugmentedMutationRecord (..),-    ControlTally (..),-    MutationRunReport (..),-    MutationTally (..),+    RunSoundness (..),     filterAugmentedManifestByIds,     readAndUnionBaselineDirs,     readAndUnionCoverageDirs,+    runFoundFailure,+    runSoundness,     writeAugmentedManifestFile,   ) import Test.Syd.Mutation.Driver.AssertScore (runAssertScore)@@ -94,9 +94,8 @@   -- 'SuiteConfig' map, not just the exes), so no parent-side 'cd' is needed   -- here.  It prints the report to stdout and writes report.json +   -- report.txt + per-suite *.log files to the out dir.  It returns the run-  -- report; under --fail-fast we then exit non-zero ourselves, preserving-  -- the historical behaviour of the @run@ subcommand (a downstream-  -- @assert-score@ step is the gate when --fail-fast is off).+  -- report; we then decide the exit code from it below (a downstream+  -- @assert-score@ step is the gate for survivors when --fail-fast is off).   report <-     runMutationMode       mutationDriverSettingFailFast@@ -107,14 +106,24 @@       mutationDriverSettingMutationJobs       suites   hFlush stdout-  when-    ( mutationDriverSettingFailFast-        && ( mutationTallySurvived (mutationRunReportMutations report) > 0-               || mutationTallyUncovered (mutationRunReportMutations report) > 0-               || controlTallyFailed (mutationRunReportControls report) > 0-           )-    )-    $ exitWith (ExitFailure 1)+  -- A failed control fails this run even without --fail-fast, which a+  -- survivor deliberately does not.+  --+  -- The difference is whether the verdict can be reproduced. Survivors can+  -- be, so writing them to a report that a later assert-score step reads is+  -- what we want: the expensive run is done once and the cheap gate reports+  -- it as often as asked. A failed control cannot be, because the thing that+  -- fails a control is a suite nobody can rely on: flaky, or too slow for its+  -- budget on the machine it happened to land on. Succeeding here would store+  -- that verdict in a build artefact, and every later attempt would read the+  -- stored copy rather than run anything, so the run could not be retried+  -- without hunting down and deleting the artefact. Failing leaves nothing+  -- behind to read, and the next attempt runs the mutations again.+  case runSoundness report of+    RunUnsound -> exitWith (ExitFailure 1)+    RunSound -> pure ()+  when (mutationDriverSettingFailFast && runFoundFailure report) $+    exitWith (ExitFailure 1)  -- | Assemble the augmented manifest the mutation phase reads from pre-computed -- per-package coverage directories, instead of running the coverage phase.
src/Test/Syd/Mutation/Driver/AssertScore.hs view
@@ -6,8 +6,8 @@ -- | Implementation of the @assert-score@ subcommand: read @report.json@ -- from a report directory, print a one-line pass\/fail header followed -- by the rendered report body, and exit 0 on success, 1 on a failed--- assertion (any survived, or any uncovered when the uncovered assertion--- is enabled).+-- assertion (any survived, any timed out, or any uncovered when the+-- uncovered assertion is enabled). module Test.Syd.Mutation.Driver.AssertScore   ( runAssertScore,     assertScoreResult,@@ -43,8 +43,8 @@ -- | The decision 'assert-score' renders from a 'MutationRunReport'. data AssertScoreResult = AssertScoreResult   { -- | True when the assertion is violated: at least one survivor, at least-    -- one failed control, or (when uncovered is also asserted) at least one-    -- uncovered mutation.+    -- one mutation that ran out of time, at least one failed control, or+    -- (when uncovered is also asserted) at least one uncovered mutation.     assertScoreFailed :: !Bool,     -- | Pass/fail header line (e.g. @PASS: All 17 mutation(s) accounted for.@).     assertScoreHeader :: ![Chunk]@@ -65,11 +65,17 @@       -- transient noise.  Treat it as an assertion failure like a survivor.       failed =         mutationTallySurvived > 0+          || mutationTallyTimedOut > 0           || (assertNoneUncovered && mutationTallyUncovered > 0)           || controlTallyFailed > 0+      -- Timed-out mutations used to be inside the killed count, which is why+      -- this sum did not name them.  Now that they are their own category it+      -- has to add them back, or the count a FAIL is measured against leaves+      -- out part of what was run.       total =         mutationTallyKilled           + mutationTallySurvived+          + mutationTallyTimedOut           + mutationTallyUncovered       -- A count is good (green) when it's zero, bad (red) when it isn't.       -- The header colour reflects the overall verdict, but each@@ -88,6 +94,14 @@               countChunk mutationTallyUncovered,               chunk " uncovered"             ]+              -- Named only when one ran out of time, so the usual+              -- survivor/uncovered header reads as it always has.  Without+              -- it a run that failed on nothing else reads "0 surviving, 0+              -- uncovered" and leaves the reader to guess what failed.+              ++ ( if mutationTallyTimedOut > 0+                     then [chunk ", ", countChunk mutationTallyTimedOut, chunk " timed out"]+                     else []+                 )               -- Only mention controls when one actually failed, so the common               -- survivor/uncovered FAIL header reads exactly as before.               ++ ( if controlTallyFailed > 0
src/Test/Syd/Mutation/Driver/Mutate.hs view
@@ -272,10 +272,10 @@           Nothing -> pure (MutationUncovered (UncoveredMutation record))           Just suiteNames -> do             -- Run one child per covering suite.  The mutation is killed-            -- if any child exits non-zero; timed out (counted as killed)-            -- if any child exceeded its budget without any other child-            -- killing it first; otherwise survived.-            outcomes <- mapM (runOneSuite record mid) suiteNames+            -- if any child exits non-zero; timed out if any child exceeded+            -- its budget without any other child killing it first;+            -- otherwise survived.+            outcomes <- runSuitesRetryingOverruns record mid suiteNames attemptsOnOverrun             -- A control (no-op) mutation is expected to survive.  Reinterpret             -- its raw outcome: survival is the control passing, a kill or             -- timeout is the control failing (the suite is unsound).@@ -283,6 +283,34 @@               if isControlOperator (augmentedMutationRecordOperator record)                 then asControlResult outcomes (classifyOutcomes record outcomes)                 else classifyOutcomes record outcomes++    -- How many times a mutation may overrun its budget before the overrun is+    -- taken at face value.+    attemptsOnOverrun :: Word+    attemptsOnOverrun = 3++    -- Run every covering suite, repeating only while the run tells us+    -- nothing.+    --+    -- A kill and a survival are both observations and stand on the first run.+    -- Repeating either would bias the score towards whichever outcome we+    -- repeated into, which is the trap sydtest's own 'flaky' warns about from+    -- the other side: retrying a survivor until something fails scores the+    -- mutation as caught on the strength of an accidental failure.+    --+    -- An overrun is not an observation. The budget is ten times a baseline+    -- measured on an idle machine, so a loaded one exceeds it with nothing+    -- wrong, and re-running replaces a non-observation with a real one+    -- without favouring either outcome. That is worth doing here rather than+    -- leaving to whoever re-runs the check, because the run this belongs to+    -- costs the better part of an hour.+    runSuitesRetryingOverruns record mid suiteNames attemptsLeft = do+      outcomes <- mapM (runOneSuite record mid) suiteNames+      case classifyOutcomes record outcomes of+        MutationTimedOut _+          | attemptsLeft > 1 ->+              runSuitesRetryingOverruns record mid suiteNames (attemptsLeft - 1)+        _ -> pure outcomes      -- Map a control mutation's raw classification onto the control-specific     -- results.  An uncovered control never reaches here (it short-circuits
sydtest-mutation-driver.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           sydtest-mutation-driver-version:        0.2.0.0+version:        0.3.0.0 synopsis:       Out-of-process mutation testing driver for sydtest. description:    Standalone driver executable that orchestrates the coverage and mutation phases of sydtest's mutation testing infrastructure. Spawns instrumented sydtest test suite executables as children. category:       Testing