1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
|
-----------------------------------------------------------------------------
--
-- Module : Main
-- Copyright : (c) Phil Freeman 2013
-- License : MIT
--
-- Maintainer : Phil Freeman <paf31@cantab.net>
-- Stability : experimental
-- Portability :
--
-- |
--
-----------------------------------------------------------------------------
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
-- 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
module Main (main) where
import qualified Language.PureScript as P
import qualified Language.PureScript.CodeGen.JS as J
import qualified Language.PureScript.CoreFn as CF
import Data.Char (isSpace)
import Data.Maybe (mapMaybe, fromMaybe)
import Data.List (isSuffixOf, sort, stripPrefix)
import Data.Traversable (traverse)
import Data.Time.Clock (UTCTime())
import qualified Data.Map as M
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import Control.Applicative
import Control.Arrow ((>>>))
import Control.Monad.Reader
import Control.Monad.Writer
import Control.Monad.Trans.Maybe
import Control.Monad.Trans.Except
import Control.Monad.Error.Class
import System.Exit
import System.Process
import System.FilePath
import System.Directory
import qualified System.FilePath.Glob as Glob
import Text.Parsec (ParseError)
modulesDir :: FilePath
modulesDir = ".test_modules" </> "node_modules"
makeActions :: M.Map P.ModuleName FilePath -> P.MakeActions P.Make
makeActions foreigns = (P.buildMakeActions modulesDir (error "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 (error "getOutputTimestamp: read timestamp") else Nothing)
readInput :: [FilePath] -> IO [(FilePath, String)]
readInput inputFiles = forM inputFiles $ \inputFile -> do
text <- readFile inputFile
return (inputFile, text)
type TestM = WriterT [(FilePath, String)] IO
runTest :: P.Make a -> IO (Either P.MultipleErrors a)
runTest = fmap (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 =
readFile
>>> 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
findNodeProcess :: IO (Maybe String)
findNodeProcess = runMaybeT . msum $ map (MaybeT . findExecutable) names
where
names = ["nodejs", "node"]
main :: IO ()
main = do
fetchSupportCode
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,) <$> readFile 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 exitSuccess
else do
putStrLn "Failures:"
forM_ failures $ \(fp, err) ->
let fp' = fromMaybe fp $ stripPrefix (failing ++ [pathSeparator]) fp
in putStrLn $ fp' ++ ": " ++ err
exitFailure
fetchSupportCode :: IO ()
fetchSupportCode = do
setCurrentDirectory "tests/support"
callProcess "npm" ["install"]
-- Sometimes we run as a root (e.g. in simple docker containers)
-- And we are non-interactive: https://github.com/bower/bower/issues/1162
callProcess "node_modules/.bin/bower" ["--allow-root", "install", "--config.interactive=false"]
callProcess "node" ["setup.js"]
setCurrentDirectory "../.."
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"
]
|