packages feed

sydtest-mutation-driver-0.1.0.0: src/Test/Syd/Mutation/Driver/SuitePkg.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

-- | Expand a list of 'SuitePkgSpec's (parsed from @--suite-pkg@ flags)
-- into a @Map Text SuiteConfig@ by walking each spec's
-- @<built-test-pkg-root>/test@ directory.
--
-- The 'walkSuitePkgs' function replaces the bash @suitesJsonScript@
-- the Nix harness used to inline: for each test-package it listed the
-- files in @<root>/test@, took the basename as the suite key, and
-- built a JSON object @{ "<name>": {exe, resourceDir}, ... }@ that
-- the driver then deserialised from YAML.
module Test.Syd.Mutation.Driver.SuitePkg
  ( walkSuitePkgs,
    SuitePkgWalkError (..),
  )
where

import Control.Exception (Exception, throwIO)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import qualified Data.Text as T
import Path
import Path.IO (doesDirExist, listDirRel)
import Test.Syd.Mutation.Driver.OptParse (SuiteConfig (..), SuitePkgSpec (..))

-- | Thrown by 'walkSuitePkgs' when the walk cannot produce a usable
-- suite map.
data SuitePkgWalkError
  = -- | The @--suite-pkg@ flags expanded to zero installed
    -- test-suite executables across all listed packages.  The Nix
    -- harness used to detect this case explicitly so the user gets a
    -- friendly "you forgot to declare a test-suite" message instead
    -- of an opaque "no suites configured" further down.
    NoSuitesDeclared
  | -- | Two test-packages contributed an executable with the same
    -- basename.  This would silently overwrite the first in a 'Map';
    -- the bash version had the same hazard but it's worth a typed
    -- error here so a future caller does not silently shadow a suite.
    DuplicateSuiteName !Text
  deriving (Show)

instance Exception SuitePkgWalkError

-- | Walk each spec's @<root>/test@ directory and accumulate one
-- 'SuiteConfig' per installed test executable, keyed by the
-- executable's basename.
--
-- A spec whose @<root>/test@ directory does not exist contributes no
-- suites; that mirrors the bash @for exe in "$pkgTestDir"/*@ which
-- silently produced no iterations.
--
-- Throws 'NoSuitesDeclared' when every spec produced zero suites, and
-- 'DuplicateSuiteName' when two specs produced a suite with the same
-- key.
walkSuitePkgs :: [SuitePkgSpec] -> IO (Map.Map Text SuiteConfig)
walkSuitePkgs specs = do
  perSpec <- mapM walkOne specs
  let combined = foldr mergeNoOverwrite (Right Map.empty) perSpec
  case combined of
    Left dup -> throwIO (DuplicateSuiteName dup)
    Right m
      | Map.null m -> throwIO NoSuitesDeclared
      | otherwise -> pure m
  where
    walkOne :: SuitePkgSpec -> IO (Map.Map Text SuiteConfig)
    walkOne SuitePkgSpec {suitePkgSpecBuiltTestPkgRoot, suitePkgSpecResourceDir} = do
      let testDir = suitePkgSpecBuiltTestPkgRoot </> [reldir|test|]
      exists <- doesDirExist testDir
      if not exists
        then pure Map.empty
        else do
          (_, files) <- listDirRel testDir
          pure $
            Map.fromList
              [ ( T.pack (fromRelFile relFile),
                  SuiteConfig
                    { suiteConfigExe = testDir </> relFile,
                      suiteConfigResourceDir = Just suitePkgSpecResourceDir
                    }
                )
              | relFile <- files
              ]

    mergeNoOverwrite ::
      Map.Map Text SuiteConfig ->
      Either Text (Map.Map Text SuiteConfig) ->
      Either Text (Map.Map Text SuiteConfig)
    mergeNoOverwrite _ (Left dup) = Left dup
    mergeNoOverwrite m (Right acc) =
      case Map.lookupMin (Map.intersection m acc) of
        Just (k, _) -> Left k
        Nothing -> Right (Map.union m acc)