packages feed

sydtest-mutation-0.1.0.0: test/Test/Syd/PhaseBoundarySpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}

-- | Smoke tests around bug #1 (the @<<loop>>@ that surfaces in
-- 'runCoverageMode' on large projects).
--
-- These tests exercise the read-then-tear-down and read-merge-write
-- patterns that the coverage harness uses internally.  They do NOT
-- reliably reproduce the original @<<loop>>@ — that requires the full
-- nix-ci scenario (~1500 tests, tmp-postgres, 8 concurrent jobs).  They
-- serve as a regression guard against gross mistakes in those code
-- paths: if the read/merge/write round-trip stops preserving data, or
-- if 'mapConcurrently' over many per-child temp dirs starts looping,
-- these tests will catch it.
--
-- Determinism note: 'mapConcurrently' is non-deterministic in
-- scheduling, but the assertions only check structural invariants
-- (counts, no failure to decode), so flakiness is not expected.
module Test.Syd.PhaseBoundarySpec (spec) where

import Control.Concurrent.Async (mapConcurrently)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import Path
import Path.IO (withSystemTempDir)
import Test.Syd
import Test.Syd.Mutation.AugmentedManifest
import Test.Syd.Mutation.Runtime (MutationId (..))
import Test.Syd.Mutation.TestBaselineMap
  ( TestBaselineMap (..),
    readTestBaselineMapFile,
    writeTestBaselineMapFile,
  )
import Test.Syd.Mutation.TestCoverageMap
  ( TestCoverageMap (..),
    readTestCoverageMapFile,
    writeTestCoverageMapFile,
  )
import Test.Syd.Mutation.TestId (TestId (..))

spec :: Spec
spec = describe "phase boundary smoke (bug #1)" $ do
  it "concurrent per-child read-then-teardown of coverage maps preserves data" $ do
    -- Mirrors the runCoverageChild pattern: per-thread temp dir, write
    -- result, read it back, then let the temp dir get torn down.
    let n = 500 :: Int
        readOne (i :: Int) = withSystemTempDir "phase-boundary-cov" $ \dir -> do
          let path = fromAbsFile (dir </> [relfile|coverage.json|])
              tid = TestId ((T.pack ('t' : show i), 0) :| [])
              cm = TestCoverageMap (Map.singleton tid (Set.singleton (MutationId ["M", "Op", show i, "1", "5", "r", "1"])))
          writeTestCoverageMapFile path cm
          eRead <- readTestCoverageMapFile path
          case eRead of
            Left err -> expectationFailure ("decode failed: " ++ err)
            Right (TestCoverageMap m) -> Map.size m `shouldBe` 1
    _ <- mapConcurrently readOne [1 .. n]
    pure ()

  it "concurrent per-child read-then-teardown of baseline maps preserves data" $ do
    let n = 500 :: Int
        readOne (i :: Int) = withSystemTempDir "phase-boundary-base" $ \dir -> do
          let path = fromAbsFile (dir </> [relfile|baseline.json|])
              tid = TestId ((T.pack ('t' : show i), 0) :| [])
              bm = TestBaselineMap (Map.singleton tid (fromIntegral i))
          writeTestBaselineMapFile path bm
          eRead <- readTestBaselineMapFile path
          case eRead of
            Left err -> expectationFailure ("decode failed: " ++ err)
            Right (TestBaselineMap m) -> Map.size m `shouldBe` 1
    _ <- mapConcurrently readOne [1 .. n]
    pure ()

  it "many sequential read-merge-write rounds against the same file preserve data" $
    -- Mirrors the multi-suite augmented-manifest flow where one file is
    -- repeatedly read, merged, and written back in the same process.
    withSystemTempDir "phase-boundary-multi" $ \dir -> do
      let mkRec :: Int -> Int -> AugmentedMutationRecord
          mkRec i suite =
            AugmentedMutationRecord
              { augmentedMutationRecordId = MutationId ["M", "Op", show i, "1", "5", "r", "1"],
                augmentedMutationRecordOperator = "Op",
                augmentedMutationRecordOriginal = "+",
                augmentedMutationRecordReplacement = "-",
                augmentedMutationRecordModule = "M",
                augmentedMutationRecordLine = fromIntegral i,
                augmentedMutationRecordEndLine = fromIntegral i,
                augmentedMutationRecordColStart = 1,
                augmentedMutationRecordColEnd = 5,
                augmentedMutationRecordSourceFile = Just $(mkRelFile "src/M.hs"),
                augmentedMutationRecordSourceLines = [],
                augmentedMutationRecordMutatedLines = [],
                augmentedMutationRecordContextBefore = [],
                augmentedMutationRecordContextAfter = [],
                augmentedMutationRecordCoveringTests =
                  Map.singleton (T.pack ("suite-" ++ show suite)) [],
                augmentedMutationRecordTimeoutMicros = 30000000,
                augmentedMutationRecordBinding = Nothing,
                augmentedMutationRecordMitigation = Nothing
              }
          newSuite :: Int -> Int -> AugmentedManifest
          newSuite suite recs =
            -- One singleton group per record keeps the merge semantics
            -- (combine by id) easy to reason about.
            AugmentedManifest [AugmentedMutationGroup [mkRec i suite] | i <- [1 .. recs]]
          recsPerSuite = 200
          totalSuites = 50
      writeAugmentedManifestFile dir (newSuite 0 recsPerSuite)
      let loop s
            | s > totalSuites = pure ()
            | otherwise = do
                mPrev <- readAugmentedManifestFileIfExists dir
                case mPrev of
                  Nothing -> expectationFailure "expected manifest"
                  Just prev -> do
                    let merged = mergeAugmentedManifests prev (newSuite s recsPerSuite)
                    writeAugmentedManifestFile dir merged
                    loop (s + 1)
      loop 1
      AugmentedManifest finalGroups <- readAugmentedManifestFile dir
      let finalRecords = [r | AugmentedMutationGroup rs <- finalGroups, r <- rs]
      length finalRecords `shouldBe` recsPerSuite
      -- All suites should appear in the merged covering_tests maps.
      let allSuites = Map.unionsWith (++) (map augmentedMutationRecordCoveringTests finalRecords)
      length (Map.keys allSuites) `shouldBe` totalSuites + 1