module TestPsci.TestEnv where import Prelude import Control.Exception.Lifted (bracket_) import Control.Monad (void, when) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.RWS.Strict (evalRWST, asks, local, RWST) import Data.Foldable (traverse_) import Data.List (isSuffixOf) import Data.Text qualified as T import Language.PureScript qualified as P import Language.PureScript.CST qualified as CST import Language.PureScript.Interactive (Command(..), PSCiConfig(..), PSCiState, handleCommand, indexFile, initialPSCiState, loadAllModules, make, modulesDir, parseCommand, readNodeProcessWithExitCode, runMake, updateLoadedExterns) import System.Directory (getCurrentDirectory, doesPathExist, removeFile) import System.Exit import System.FilePath ((), pathSeparator) import System.FilePath.Glob qualified as Glob import Test.Hspec (shouldBe, Expectation) -- | A monad transformer for handle PSCi actions in tests type TestPSCi a = RWST PSCiConfig () PSCiState IO a -- | Initialise PSCi state and config for tests initTestPSCiEnv :: IO (PSCiState, PSCiConfig) initTestPSCiEnv = do -- Load test support packages cwd <- getCurrentDirectory let supportDir = cwd "tests" "support" psciFiles <- Glob.globDir1 (Glob.compile "**/*.purs") (supportDir "psci") libraries <- Glob.globDir1 (Glob.compile "purescript-*/src/**/*.purs") (supportDir "bower_components") let pursFiles = psciFiles ++ libraries modulesOrError <- loadAllModules pursFiles case modulesOrError of Left err -> print err >> exitFailure Right modules -> do -- Make modules makeResultOrError <- runMake . make $ fmap CST.pureResult <$> modules case makeResultOrError of Left errs -> putStrLn (P.prettyPrintMultipleErrors P.defaultPPEOptions errs) >> exitFailure Right (externs, _) -> return (updateLoadedExterns (const (zip (map snd modules) externs)) initialPSCiState, PSCiConfig pursFiles) -- | Execute a TestPSCi, returning IO execTestPSCi :: TestPSCi a -> IO a execTestPSCi i = do (s, c) <- initTestPSCiEnv -- init state and config fst <$> evalRWST i c s -- | Evaluate JS to which a PSCi input is compiled. The actual JS input is not -- needed as an argument, as it is already written in the file during the -- command evaluation. jsEval :: TestPSCi String jsEval = liftIO $ do writeFile indexFile "import('./$PSCI/index.js').then(({ $main }) => $main());" result <- readNodeProcessWithExitCode Nothing [indexFile] "" case result of Right (ExitSuccess, out, _) -> return out Right (ExitFailure _, _, err) -> putStrLn err >> exitFailure Left err -> putStrLn err >> exitFailure -- | Run a PSCi command and evaluate its outputs: -- * jsOutputEval is used to evaluate compiled JS output by PSCi -- * printedOutputEval is used to evaluate text printed directly by PSCi itself runAndEval :: String -> TestPSCi () -> (String -> TestPSCi ()) -> TestPSCi () runAndEval comm jsOutputEval textOutputEval = case parseCommand comm of Left errStr -> liftIO $ putStrLn errStr >> exitFailure Right commands -> -- The JS result is ignored, as it's already written in a JS source file. -- For the detail, please refer to Interactive.hs traverse_ (handleCommand (const jsOutputEval) (return ()) textOutputEval) commands -- | Run a PSCi command, evaluate compiled JS, and ignore evaluation output and printed output run :: String -> TestPSCi () run comm = runAndEval comm (void jsEval) ignorePrinted where ignorePrinted _ = return () -- | A lifted evaluation of Hspec 'shouldBe' for the TestPSCi equalsTo :: (Eq a, Show a) => a -> a -> TestPSCi () equalsTo x y = liftIO $ x `shouldBe` y -- | An assertion to check command evaluated javascript output against a given string evaluatesTo :: String -> String -> TestPSCi () evaluatesTo command expected = runAndEval command evalJsAndCompare ignorePrinted where evalJsAndCompare = do actual <- jsEval actual `equalsTo` (expected ++ "\n") ignorePrinted _ = return () -- | An assertion to check command PSCi printed output against a given string prints :: String -> String -> TestPSCi () prints command expected = printed command (`shouldBe` expected) printed :: String -> (String -> Expectation) -> TestPSCi () printed command f = runAndEval command (void jsEval) (liftIO . f) simulateModuleEdit :: P.ModuleName -> FilePath -> TestPSCi a -> TestPSCi a simulateModuleEdit mn newPath action = do ms <- asks psciFileGlobs case replacePath ms of Nothing -> fail $ "Did not find " ++ inputPath ++ " in psciFileGlobs" Just xs' -> local (\c -> c { psciFileGlobs = xs' }) temporarily <* rebuild where outputPath = modulesDir T.unpack (P.runModuleName mn) "index.js" inputPath = T.unpack (T.replace "." slash (P.runModuleName mn)) ++ ".purs" slash = T.singleton pathSeparator replacePath :: [String] -> Maybe [String] replacePath (x:xs) | inputPath `isSuffixOf` x = Just (newPath : xs) | otherwise = fmap (x:) (replacePath xs) replacePath [] = Nothing -- Simply adding the file to `PSCiConfig.fileGlobs` isn't sufficient; running -- ":reload" might not rebuild because the compiled JS artifact has a more -- recent timestamp than the "new" source file `newPath`. temporarily = bracket_ enableRebuild enableRebuild action enableRebuild = liftIO $ do { b <- doesPathExist outputPath; when b (removeFile outputPath) } rebuild = handleCommand discard (return ()) discard ReloadState discard _ = return ()