packages feed

purescript-0.8.1.0: tests/TestCompiler.hs

{-# 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"
  ]