sydtest-mutation-runtime-0.1.0.0: src/Test/Syd/Mutation/TestBaselineMap.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Test.Syd.Mutation.TestBaselineMap
( TestBaselineMap (..),
readTestBaselineMapFile,
writeTestBaselineMapFile,
writeTestBaselineMapDir,
readTestBaselineMapDirIfExists,
)
where
import Autodocodec
import qualified Data.Aeson as Aeson
import qualified Data.ByteString as SB
import qualified Data.ByteString.Lazy as LB
import Data.GenValidity
import Data.GenValidity.Containers ()
import qualified Data.Map.Strict as Map
import GHC.Generics (Generic)
import Path
import Path.IO (doesFileExist, ensureDir)
import Test.Syd.Mutation.TestId (TestId)
-- | Per-test monotonic-clock baselines (microseconds) collected during the
-- coverage phase. Used to derive per-mutation timeouts in the mutation
-- phase.
newtype TestBaselineMap = TestBaselineMap (Map.Map TestId Word)
deriving (Show, Eq, Generic)
deriving (Aeson.ToJSON, Aeson.FromJSON) via (Autodocodec TestBaselineMap)
instance Validity TestBaselineMap
instance GenValid TestBaselineMap where
genValid = genValidStructurally
shrinkValid = shrinkValidStructurally
-- Encoded as a JSON array of {test_id, elapsed_micros} objects because
-- TestId is not a valid JSON object key.
instance HasCodec TestBaselineMap where
codec =
dimapCodec
(TestBaselineMap . Map.fromList)
(\(TestBaselineMap m) -> Map.toList m)
( listCodec $
object "TestBaselineEntry" $
(,)
<$> requiredField' "test_id" .= fst
<*> requiredField' "elapsed_micros" .= snd
)
-- | Merge by taking the maximum elapsed time seen for each test. Coverage
-- children are run with @settingThreads = Synchronous@ but a test may be
-- exercised across multiple suites, and the safer baseline is the slowest.
instance Semigroup TestBaselineMap where
TestBaselineMap a <> TestBaselineMap b = TestBaselineMap (Map.unionWith max a b)
instance Monoid TestBaselineMap where
mempty = TestBaselineMap Map.empty
writeTestBaselineMapFile :: FilePath -> TestBaselineMap -> IO ()
writeTestBaselineMapFile path m =
LB.writeFile path (encodeJSONViaCodec m)
-- | Read the baseline map strictly (via 'SB.readFile' +
-- 'eitherDecodeJSONViaCodec') so the file handle is closed before this
-- function returns. Defensive against a suspected (but unproven) contributor
-- to 'BlockedIndefinitelyOnMVar' loops at the coverage/mutation phase
-- boundary on large projects. Returns the aeson error message on parse
-- failure.
readTestBaselineMapFile :: FilePath -> IO (Either String TestBaselineMap)
readTestBaselineMapFile path =
eitherDecodeJSONViaCodec . LB.fromStrict <$> SB.readFile path
-- | The name under which a merged 'TestBaselineMap' is stored inside an
-- augmented-manifest directory, alongside @manifest-augmented.json@. The
-- coverage phase writes it and the mutation child reads it to order covering
-- tests cheapest-first.
baselineMapRelFile :: Path Rel File
baselineMapRelFile = [relfile|baseline.json|]
-- | Write a 'TestBaselineMap' to @<dir>/baseline.json@, creating @dir@ if
-- needed.
writeTestBaselineMapDir :: Path Abs Dir -> TestBaselineMap -> IO ()
writeTestBaselineMapDir dir m = do
ensureDir dir
writeTestBaselineMapFile (fromAbsFile (dir </> baselineMapRelFile)) m
-- | Read @<dir>/baseline.json@, returning 'Nothing' when the file is absent or
-- unreadable. Ordering is a best-effort hint, so a missing or corrupt baseline
-- is not fatal: the caller simply does not reorder.
readTestBaselineMapDirIfExists :: Path Abs Dir -> IO (Maybe TestBaselineMap)
readTestBaselineMapDirIfExists dir = do
let path = dir </> baselineMapRelFile
exists <- doesFileExist path
if exists
then either (const Nothing) Just <$> readTestBaselineMapFile (fromAbsFile path)
else pure Nothing