packages feed

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

module System.Process.Quick.CallSpec.Verify.Sandbox where

import Data.Conduit (runConduitRes, (.|))
import Data.Conduit.Find as F
import Data.Conduit.List qualified as DCL
import Data.Multimap.Table qualified as T
import System.Directory
import System.FilePath (takeDirectory, takeExtension)
import System.IO.Temp (withSystemTempDirectory)
import System.Process
import System.Process.Quick.CallEffect
import System.Process.Quick.CallSpec
import System.Process.Quick.CallSpec.Verify.Type
import System.Process.Quick.Predicate
import System.Process.Quick.Predicate.InDir ()
import System.Process.Quick.Predicate.InFile ()
import System.Process.Quick.Prelude hiding (Type, lift)
import System.Process.Quick.Util


callProcessAndReport :: (CallSpec cs, M m) => cs -> m CsExecReport
callProcessAndReport cs = do
  proLaunchDir <- liftIO getCurrentDirectory
  startedAt <- currentTime
  liftIO (readProcessWithExitCode (programName (pure cs)) (programArgs cs) "") >>= \(ec, out, err) -> do
    endedAt <- currentTime
    pure CsExecReport
      { exitCode = ec
      , stdErr = err
      , stdOut = out
      , execTime = endedAt `diffUTCTime` startedAt
      , processDir = proLaunchDir
      }

normalizeOutcomeChecks :: CallSpec cs => cs -> [OutcomeChecker]
normalizeOutcomeChecks cs =
  case filter ecP origOutcomeChecks of
    [] -> ExitCodeEqualTo ExitSuccess : origOutcomeChecks
    [ExitCodeEqualTo ExitSuccess] ->
      $(tr "!ExitCodeEqualTo ExitSuccess check is redundant/cs")
        origOutcomeChecks
    [ExitCodeEqualTo _] -> origOutcomeChecks
    _ ->
      $(tr "!Multiple ExitCodeEqualTo checks/cs")
        origOutcomeChecks
  where
    origOutcomeChecks = outcomeCheckers cs
    ecP = \case ExitCodeEqualTo _ -> True ; _ -> False


measureX :: forall m cs a. (Typeable cs, M m) =>
  Proxy cs -> VerificationMethod -> Lens' CsPerf (Sum NominalDiffTime) -> CsPerfT m a -> CsPerfT m a
measureX pcs vm l a = do
  started <- currentTime
  !r <- a
  ended <- currentTime
  modify' $ T.alter (merge ended started) vm (typeRep pcs)
  pure r
  where
    merge e s = pure . (l .~ (Sum $ e `diffUTCTime` s)) . fromMaybe mempty

validateInSandbox ::
  forall w m cs. (M m, CallSpec cs, WriterT [FilePath] (CsPerfT m) ~ w) =>
  ArgCollector w ->
  ArgCollector w ->
  (IO cs) ->
  Int ->
  CsPerfT m (Maybe CsViolationWithCtx)
validateInSandbox inArgLocators outArgLocators mkCs !iterations
  | iterations <= 0 = pure Nothing
  | otherwise =
      withSystemTempDirectory "quick-process" go >>= \case
        Nothing -> validateInSandbox inArgLocators outArgLocators mkCs $ iterations - 1
        Just e -> pure $ Just e
  where
    validateInit cs projectDir (CsBox h:initCss)
      = validateInSandboxOne projectDir inArgLocators outArgLocators (pure h) >>= \case
          Nothing -> validateInit cs projectDir initCss
          Just e -> pure $ Just e
    validateInit cs projectDir []
      = validateInSandboxOne projectDir inArgLocators outArgLocators (pure cs)
    doIn projectDir () = do
      cs <- liftIO mkCs
      validateInit cs projectDir =<< initCallSpecs cs
    go tdp = do
      projectDir <- liftIO getCurrentDirectory
      bracket
        (liftIO $ setCurrentDirectory tdp)
        (\() -> liftIO $ setCurrentDirectory projectDir)
        (doIn projectDir)


validateInSandboxOne ::
  forall w m cs. (M m, CallSpec cs, WriterT [FilePath] (CsPerfT m) ~ w) =>
  FilePath ->
  ArgCollector w ->
  ArgCollector w ->
  (IO cs) ->
  CsPerfT m (Maybe CsViolationWithCtx)
validateInSandboxOne projectDir inArgLocators outArgLocators mkCs = doIn
  where
    checkFilesExist cs outFiles = do
      filterM (pure . not <=< doesFileExist) outFiles >>= \case
        [] -> pure Nothing
        ne -> pure . Just . CsViolationWithCtx cs $
          UnexpectedCallEffect
          [ FsEffect . FsAnd $ fmap (FsNot . flip FsPathPredicate [FsExists]) ne
          ]

    findOriginFor inFile = do
      xs :: [FilePath] <- runConduitRes $
        F.find projectDir (do ignoreVcs
                              glob $ "*" <> takeExtension inFile
                              regular
                              not_ F.executable) .| DCL.consume
      case xs of
        [] -> pure Nothing
        neXs -> Just <$> generate (elements neXs)

    genInputFile inFile = (fromMaybe "/etc/hosts" <$> findOriginFor inFile) >>=
      \origin -> createDirectoryIfMissing True (takeDirectory inFile) >>
                 copyFile origin inFile
                 -- putStrLn ("File "  <> show origin <> " => " <> show inFile)

    doIn = do
      let pcs = Proxy @cs
      cs <- measureX pcs SandboxValidate #csGenerationTime (liftIO mkCs)
      inFiles <- execWriterT (gmapM inArgLocators cs)
      -- absolute path is an issue for generator
      -- though process in docker is run under root - high chance to pass ;)
      -- quick hack is to use  odd size in Gen to avoid absolute path it Sandbox mode
      mapM_ (liftIO1 genInputFile) inFiles
      let nocs = normalizeOutcomeChecks cs
      tryIO (measureX pcs SandboxValidate #csExeTime $ callProcessAndReport cs) >>= \case
        Left e -> throw . CsViolationWithCtx cs . ExceptionThrown $ SomeException e
        Right csr -> mapM (check csr) nocs >>= pure . concat >>=
          \case
            [] -> do
              outFiles <- execWriterT (gmapM outArgLocators cs)
              liftIO (checkFilesExist cs outFiles)
            cfs -> pure . Just . CsViolationWithCtx cs $ UnexpectedCallEffect cfs