{-# LANGUAGE DataKinds #-} {-# LANGUAGE DoAndIfThenElse #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE FlexibleInstances #-} module TestCompiler where -- Failing tests can specify the kind of error that should be thrown with a -- @shouldFailWith declaration. For example: -- -- "-- @shouldFailWith TypesDoNotUnify" -- -- will cause the test to fail unless that module fails to compile with exactly -- one TypesDoNotUnify error. -- -- If a module is expected to produce multiple type errors, then use multiple -- @shouldFailWith lines; for example: -- -- -- @shouldFailWith TypesDoNotUnify -- -- @shouldFailWith TypesDoNotUnify -- -- @shouldFailWith TransitiveExportError import Prelude () import Prelude.Compat import qualified Language.PureScript as P import Data.Char (isSpace) import Data.Maybe (mapMaybe, fromMaybe) import Data.List (isSuffixOf, sort, stripPrefix) import Data.Time.Clock (UTCTime()) import qualified Data.Map as M import Control.Monad import Control.Arrow ((>>>)) import Control.Monad.Reader import Control.Monad.Writer.Strict import Control.Monad.Trans.Except import System.Exit import System.Process hiding (cwd) import System.FilePath import System.Directory import System.IO.UTF8 import qualified System.FilePath.Glob as Glob import TestUtils main :: IO () main = do cwd <- getCurrentDirectory let supportDir = cwd "tests" "support" "flattened" let supportFiles ext = Glob.globDir1 (Glob.compile ("*." ++ ext)) supportDir supportPurs <- supportFiles "purs" supportJS <- supportFiles "js" foreignFiles <- forM supportJS (\f -> (f,) <$> readUTF8File f) Right (foreigns, _) <- runExceptT $ runWriterT $ P.parseForeignModulesFromFiles foreignFiles let passing = cwd "examples" "passing" passingTestCases <- sort . filter (".purs" `isSuffixOf`) <$> getDirectoryContents passing let failing = cwd "examples" "failing" failingTestCases <- sort . filter (".purs" `isSuffixOf`) <$> getDirectoryContents failing failures <- execWriterT $ do forM_ passingTestCases $ \inputFile -> assertCompiles (supportPurs ++ [passing inputFile]) foreigns forM_ failingTestCases $ \inputFile -> assertDoesNotCompile (supportPurs ++ [failing inputFile]) foreigns if null failures then pure () else do putStrLn "Failures:" forM_ failures $ \(fp, err) -> let fp' = fromMaybe fp $ stripPrefix (failing ++ [pathSeparator]) fp in putStrLn $ fp' ++ ": " ++ err exitFailure modulesDir :: FilePath modulesDir = ".test_modules" "node_modules" makeActions :: M.Map P.ModuleName FilePath -> P.MakeActions P.Make makeActions foreigns = (P.buildMakeActions modulesDir (P.internalError "makeActions: input file map was read.") foreigns False) { P.getInputTimestamp = getInputTimestamp , P.getOutputTimestamp = getOutputTimestamp } where getInputTimestamp :: P.ModuleName -> P.Make (Either P.RebuildPolicy (Maybe UTCTime)) getInputTimestamp mn | isSupportModule (P.runModuleName mn) = return (Left P.RebuildNever) | otherwise = return (Left P.RebuildAlways) where isSupportModule = flip elem supportModules getOutputTimestamp :: P.ModuleName -> P.Make (Maybe UTCTime) getOutputTimestamp mn = do let filePath = modulesDir P.runModuleName mn exists <- liftIO $ doesDirectoryExist filePath return (if exists then Just (P.internalError "getOutputTimestamp: read timestamp") else Nothing) readInput :: [FilePath] -> IO [(FilePath, String)] readInput inputFiles = forM inputFiles $ \inputFile -> do text <- readUTF8File inputFile return (inputFile, text) type TestM = WriterT [(FilePath, String)] IO runTest :: P.Make a -> IO (Either P.MultipleErrors a) runTest = fmap fst . P.runMake P.defaultOptions compile :: [FilePath] -> M.Map P.ModuleName FilePath -> IO (Either P.MultipleErrors P.Environment) compile inputFiles foreigns = runTest $ do fs <- liftIO $ readInput inputFiles ms <- P.parseModulesFromFiles id fs P.make (makeActions foreigns) (map snd ms) assert :: [FilePath] -> M.Map P.ModuleName FilePath -> (Either P.MultipleErrors P.Environment -> IO (Maybe String)) -> TestM () assert inputFiles foreigns f = do e <- liftIO $ compile inputFiles foreigns maybeErr <- liftIO $ f e case maybeErr of Just err -> tell [(last inputFiles, err)] Nothing -> return () assertCompiles :: [FilePath] -> M.Map P.ModuleName FilePath -> TestM () assertCompiles inputFiles foreigns = do liftIO . putStrLn $ "Assert " ++ last inputFiles ++ " compiles successfully" assert inputFiles foreigns $ \e -> case e of Left errs -> return . Just . P.prettyPrintMultipleErrors False $ errs Right _ -> do process <- findNodeProcess let entryPoint = modulesDir "index.js" writeFile entryPoint "require('Main').main()" result <- traverse (\node -> readProcessWithExitCode node [entryPoint] "") process case result of Just (ExitSuccess, out, _) -> putStrLn out >> return Nothing Just (ExitFailure _, _, err) -> return $ Just err Nothing -> return $ Just "Couldn't find node.js executable" assertDoesNotCompile :: [FilePath] -> M.Map P.ModuleName FilePath -> TestM () assertDoesNotCompile inputFiles foreigns = do let testFile = last inputFiles liftIO . putStrLn $ "Assert " ++ testFile ++ " does not compile" shouldFailWith <- getShouldFailWith testFile assert inputFiles foreigns $ \e -> case e of Left errs -> do putStrLn (P.prettyPrintMultipleErrors False errs) return $ if null shouldFailWith then Just $ "shouldFailWith declaration is missing (errors were: " ++ show (map P.errorCode (P.runMultipleErrors errs)) ++ ")" else checkShouldFailWith shouldFailWith errs Right _ -> return $ Just "Should not have compiled" where getShouldFailWith = readUTF8File >>> liftIO >>> fmap ( lines >>> mapMaybe (stripPrefix "-- @shouldFailWith ") >>> map trim ) checkShouldFailWith expected errs = let actual = map P.errorCode $ P.runMultipleErrors errs in if sort expected == sort actual then Nothing else Just $ "Expected these errors: " ++ show expected ++ ", but got these: " ++ show actual trim = dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse supportModules :: [String] supportModules = [ "Control.Monad.Eff.Class" , "Control.Monad.Eff.Console" , "Control.Monad.Eff" , "Control.Monad.Eff.Unsafe" , "Control.Monad.ST" , "Data.Function" , "Prelude" , "Test.Assert" ]