polysemy-process-0.10.0.0: test/Polysemy/Process/Test/ProcessTest.hs
{-# options_ghc -fplugin=Polysemy.Plugin #-}
module Polysemy.Process.Test.ProcessTest where
import qualified Data.ByteString as ByteString
import qualified Polysemy.Conc.Effect.Race as Conc (timeout)
import Polysemy.Conc.Effect.Race (Race)
import Polysemy.Conc.Effect.Scoped (Scoped)
import Polysemy.Conc.Interpreter.Race (interpretRace)
import qualified Polysemy.Conc.Race as Race
import Polysemy.Resume (resumeEither, resumeHoistAs, resumeHoistError, resuming, runStop, type (!!), (<!))
import Polysemy.Test (TestError (TestError), UnitTest, assertJust, assertLeft, evalLeft, runTestAuto, unitTest, (===))
import Polysemy.Time (MilliSeconds (MilliSeconds), Seconds (Seconds))
import System.Exit (ExitCode (ExitSuccess))
import qualified System.Process.Typed as Process
import System.Process.Typed (ProcessConfig)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.ExpectedFailure (ignoreTest)
import qualified Polysemy.Process.Data.ProcessError as ProcessError
import Polysemy.Process.Data.ProcessError (ProcessError)
import Polysemy.Process.Data.ProcessKill (ProcessKill (KillNever))
import Polysemy.Process.Data.ProcessOptions (ProcessOptions (kill))
import Polysemy.Process.Data.ProcessOutputParseResult (ProcessOutputParseResult (Done, Partial))
import Polysemy.Process.Data.SystemProcessError (SystemProcessScopeError (StartFailed))
import qualified Polysemy.Process.Effect.Process as Process
import Polysemy.Process.Effect.Process (Process, withProcessOneshot, withProcess_)
import qualified Polysemy.Process.Effect.SystemProcess as SystemProcess
import Polysemy.Process.Effect.SystemProcess (withSystemProcess_)
import Polysemy.Process.Interpreter.Process (interpretProcessNative_)
import Polysemy.Process.Interpreter.ProcessIO (ProcessIO, interpretProcessByteString, interpretProcessTextLines)
import Polysemy.Process.Interpreter.ProcessOneshot (interpretProcessOneshotNative)
import Polysemy.Process.Interpreter.ProcessOutput (parseMany)
import Polysemy.Process.Interpreter.SystemProcess (SysProcConf, interpretSystemProcessNative_)
config :: ProcessConfig () () ()
config =
Process.proc "cat" []
messageLines :: [Text]
messageLines =
replicate 4 "line"
message :: Text
message =
unlines messageLines
test_process :: UnitTest
test_process =
runTestAuto $ interpretRace $ asyncToIOFinal $ interpretProcessByteString $ interpretProcessNative_ def config do
response <- resumeHoistError @ProcessError @(Scoped _ _) show do
withProcess_ do
Process.send (encodeUtf8 message)
Race.timeout_ (throw "timed out") (Seconds 5) Process.recv
message === decodeUtf8 response
test_processLines :: UnitTest
test_processLines =
runTestAuto $ interpretRace $ asyncToIOFinal $ interpretProcessTextLines $ interpretProcessNative_ def config do
response <- resumeHoistError @ProcessError @(Scoped _ _) show do
withProcess_ do
Process.send message
Race.timeout_ (throw "timed out") (Seconds 5) (replicateM 4 Process.recv)
messageLines === response
test_processKillNever :: UnitTest
test_processKillNever =
runTestAuto $ interpretRace $ asyncToIOFinal $ interpretProcessTextLines $ interpretProcessNative_ def { kill = KillNever } config do
result <- resumeHoistError @ProcessError @(Scoped _ _) show do
Conc.timeout unit (MilliSeconds 100) do
withProcess_ do
Process.send message
Process.recv
-- This does not succeed. It should be 'Left', but apparently the `timeout` causes the `SystemProcess` scope to stop
-- the process and makes the right side terminate regularly.
assertLeft () result
test_processIncremental :: UnitTest
test_processIncremental =
runTestAuto do
(Nothing, ([Right "aa", Right "bb"], "")) === first void (parseMany parse Nothing "aabb")
let (c1, r1) = parseMany parse Nothing "aabbc"
([Right "aa", Right "bb"], "") === r1
case ($ "c") <$> c1 of
Just (Done a "") -> "cc" === a
a -> fail ("not Done: " <> show a)
let (c2, r2) = parseMany parse Nothing "a"
([], "") === r2
case ($ "a") <$> c2 of
Just (Done a "") -> "aa" === a
a -> fail ("not Done: " <> show a)
(Nothing, ([Right "aa"], "")) === first void (parseMany parse c2 "a")
where
parse b
| ByteString.length b == 1 =
Partial (parse . (b <>))
| otherwise =
Done (ByteString.take 2 b) (ByteString.drop 2 b)
interpretOneshot ::
Members [Error TestError, Resource, Race, Async, Embed IO] r =>
(Text -> SysProcConf) ->
InterpretersFor (Scoped Text (Process Text Text !! ProcessError) : ProcessIO Text Text) r
interpretOneshot conf =
interpretProcessTextLines .
interpretProcessOneshotNative def (pure . Right . conf) .
resumeHoistError (TestError . show @Text @SystemProcessScopeError) .
insertAt @1
test_processOneshot :: UnitTest
test_processOneshot =
runTestAuto $ interpretRace $ asyncToIOFinal $ interpretOneshot conf do
num <- runStop @Int $ withProcessOneshot message do
Race.timeout_ (throw "timed out") (Seconds 5) do
for_ @[] [1..5] \ i ->
resumeHoistAs i Process.recv
unit
assertLeft 5 num
where
conf msg =
Process.proc "echo" ["-n", toString msg]
test_exit :: UnitTest
test_exit =
runTestAuto $ interpretRace $ asyncToIOFinal $ interpretProcessByteString $ interpretProcessNative_ def conf do
response <- resuming @_ @(Scoped _ _) (pure . Just) $ withProcess_ do
Race.timeout_ (throw (TestError "timed out")) (Seconds 5) do
void Process.recv
void Process.recv
pure Nothing
assertJust (ProcessError.Exit ExitSuccess) response
where
conf =
Process.proc "echo" ["-n", "text"]
test_startFailed :: UnitTest
test_startFailed =
runTestAuto $ interpretRace $ asyncToIOFinal $ interpretSystemProcessNative_ conf do
result <- resumeEither $ withSystemProcess_ do
Nothing <! (Just <$> SystemProcess.wait)
evalLeft result >>= \case
StartFailed _ ->
unit
where
conf =
Process.proc "fnord-detector" []
test_processAll :: TestTree
test_processAll =
testGroup "process" [
unitTest "read raw chunks" test_process,
unitTest "read lines" test_processLines,
ignoreTest (unitTest "don't kill the process at the end of the scope" test_processKillNever),
unitTest "expect termination" test_processOneshot,
unitTest "daemon exit code" test_exit,
unitTest "system process start error" test_startFailed
]