packages feed

quick-process-0.0.3: src/System/Process/Quick/CallSpec/Verify.hs

module System.Process.Quick.CallSpec.Verify where

import Control.Monad.Writer.Strict hiding (lift)
import Data.Map qualified as M
import Data.Multimap.Table (row, rowKeys, rowKeysSet)
import Data.Set (findMin)
import Data.Text.Lazy qualified as LT
import System.Process.Quick.CallSpec.Verify.ImportOverlook
import Language.Haskell.TH.Syntax
import System.Process.Quick.CallSpec
import System.Process.Quick.CallSpec.Verify.Sandbox
import System.Process.Quick.CallSpec.Verify.TrailingHelp ( verifyTrailingHelp )
import System.Process.Quick.CallSpec.Verify.Type
import System.Process.Quick.Predicate (ArgCollector, RefinedInArgLocator(..), RefinedOutArgLocator(..))
import System.Process.Quick.Predicate.InDir ()
import System.Process.Quick.Predicate.InFile ()
import System.Process.Quick.Prelude hiding (Type, lift)
import System.Process.Quick.Prelude qualified as P
import System.Process.Quick.Util ( M )
import Text.Pretty.Simple


verifyWithActiveMethods ::
  forall w m cs. (M m, CallSpec cs, WriterT [FilePath] (CsPerfT m) ~ w) =>
  ArgCollector w ->
  ArgCollector w ->
  Set VerificationMethod ->
  Proxy cs ->
  Int ->
  CsPerfT m [CsViolationWithCtx]
verifyWithActiveMethods inArgLocators outArgLocators activeVerMethods pcs iterations =
  catMaybes <$> mapM go  (filter (`member` activeVerMethods) (verificationMethods pcs))
  where
    go :: VerificationMethod -> CsPerfT m (Maybe CsViolationWithCtx)
    go = \case
      TrailingHelpValidate ->
        measureX pcs TrailingHelpValidate #csTotalTime $
          P.lift (verifyTrailingHelp pcs iterations)
      SandboxValidate ->
        measureX pcs SandboxValidate #csTotalTime $
          validateInSandbox inArgLocators outArgLocators (generate (arbitrary @cs)) iterations

-- |Compose a list of monadic actions into one action.  Composes using
-- ('>=>') - that is, the output of each action is fed to the input of
-- the one after it in the list.
concatM :: (Monad m) => [a -> m a] -> (a -> m a)
concatM fs = foldr (>=>) return fs

formatPerfReportLine :: (TypeRep, CsPerf) -> Doc
formatPerfReportLine (typR, csp) =
  hsep [ fill 29 $ pretty typR
       , fill 15 . pretty . getSum $ csTotalTime csp
       -- , pretty $ (getSum (csExeTime csp <> csGenerationTime csp) `div` iterations
       , fill 15 . pretty . getSum $ csGenerationTime csp
       , fill 15 . pretty . getSum $ csExeTime csp
       ]

reportFor :: MonadIO m => VerificationMethod -> CsPerfT m Doc
reportFor vm = do
  perfStats <- get
  pure $ tab (linebreak <+> "*** Method: " <+> pretty vm <> linebreak <>
       hsep [ fill 29 "Call Spec"
            , fill 15 "Total"
            , fill 15 "Generation"
            , fill 15 "Execution"
            ] $$
        "=======================================================================" $$
        (vcat . fmap formatPerfReportLine . reverse . sortWith (^. _2) . M.toList $ row vm perfStats)
      ) <> linebreak


consumeViolations :: MonadIO m => Int -> [CsViolationWithCtx] -> CsPerfT m ()
consumeViolations iterations = \case
  [] -> do
    perfStats <- get
    reports <- mapM reportFor $ rowKeys perfStats
    printDoc $ "All of" <+> pretty (M.size $ row (findMin $ rowKeysSet perfStats) perfStats)
      <+> "CallSpecs are valid due "
      <+> pretty iterations <+> "tests executed for each" <> fold reports

  vis -> do
    let dashes = "-------------------------------------------------------------"
    -- good case for hetftio ??
    printDoc $ "Error: quick-process found" <+> doc (length vis) <+> "failed call specs:"
      $$ (vcat $ zipWith (\i v -> tab ("-- [" <> doc i <> "]" <+> dashes $$ printViolation v))
                 [1::Int ..] (sortByProgamName vis))
      $$ "-------" <> dashes $$ "End of quick-process violation report" <> linebreak
    exitFailure
  where
    sortByProgamName = sortWith (\(CsViolationWithCtx x _) -> programName $ pure x)
    printViolation (CsViolationWithCtx cs v) =
      let pn = (text . toLazy . toText . programName $ pure cs) in
        case v of
          HelpKeyIgnored -> pn <> ": help key ignored"
          ProgramNotFound report' pathCopy ->
            "[" <> pn <> "] is not found on PATH:" $$ tab (vsep pathCopy)
             $$ "Report:" $$ tab report'
          HelpKeyNotSupported report' ->
            "--help key is not supported by [" <> pn <> "]"
            $$ "Report:" $$ tab report'
          HelpKeyExitNonZero rep ->
            pn <> " - non zero exit code:" $$ tab rep
          ExceptionThrown e ->
            "Launch of " <> pn <> " - thrown exception:" $$ tab (text $ show e)
            $$ "With arguments: " <> tab (programArgs cs)
          UnexpectedCallEffect uce -> do
            pn <> ": has unsafisfied effects:" $$ (vsep . fmap text . LT.lines $ pShow uce)
            $$ "With arguments: " <> tab (programArgs cs)

discoverAndVerifyCallSpecs :: Set VerificationMethod -> Int -> Q Exp
discoverAndVerifyCallSpecs activeVerMethods iterations = do
  startedAt <- runIO currentTime
  inArgLocators <- extractInstanceType <$> reifyInstances ''RefinedInArgLocator [VarT (mkName "b")]
  when (inArgLocators == []) $ putStrLn "Discovered 0 InArg locators!!!"
  outArgLocators <- extractInstanceType <$> reifyInstances ''RefinedOutArgLocator [VarT (mkName "c")]
  when (outArgLocators == []) $ putStrLn "Discovered 0 OutArg locators!!!"
  ts <- extractInstanceType <$> reifyInstances ''CallSpec [VarT (mkName "a")]
  when (ts == []) $ fail "Discovered 0 types with CallSpec instance!!!"
  overlookedCss <- verifyFoundCsCoverCompiledOnes ts
  when (overlookedCss /= mempty) . fail . toString . displayT . renderOneLine $
    "Overlooked CallSpecs: " <> pretty overlookedCss
  !r <- [| void $ runStateT (
             fmap concat
               (sequence $(ListE <$> (mapM (genCsVerification
                                            inArgLocators outArgLocators) ts))) >>=
             consumeViolations $(lift iterations))
             mempty
        |]
  endedAt <- runIO currentTime
  putStrLn $ "discoverAndVerifyCallSpecs generation took " <> show (endedAt `diffUTCTime` startedAt)
  pure r
  where
    getLocator n t = AppE (VarE n) (SigE (ConE 'Proxy) (AppT (ConT ''Proxy) t))

    pipeLocators :: Name -> [Type] -> Q Exp
    pipeLocators locName ts =
      [| concatM $(pure . ListE $ getLocator locName <$> ts) |]

    genCsVerification :: [Type] -> [Type] -> Type -> Q Exp
    genCsVerification inArL outArL t =
      [| verifyWithActiveMethods
           $(pipeLocators 'locateRefinedInArg inArL)
           $(pipeLocators 'locateRefinedOutArg outArL)
           $(lift activeVerMethods)
           $(pure $ SigE (ConE 'Proxy) (AppT (ConT ''Proxy) t))
           $(lift iterations)
       |]
    extractInstanceType :: [Dec] -> [Type]
    extractInstanceType = mapMaybe $ \case
      InstanceD _ _ (AppT _ t) _ ->
        Just t
      _ -> Nothing