forked from purescript/purescript
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTestCompiler.hs
More file actions
309 lines (266 loc) · 11.6 KB
/
TestCompiler.hs
File metadata and controls
309 lines (266 loc) · 11.6 KB
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DoAndIfThenElse #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
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.Function (on)
import Data.List (sort, stripPrefix, intercalate, groupBy, sortBy, minimumBy)
import Data.Maybe (mapMaybe)
import Data.Time.Clock (UTCTime())
import qualified Data.Text as T
import Data.Tuple (swap)
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
import System.IO.UTF8
import System.IO.Silently
import qualified System.FilePath.Glob as Glob
import TestUtils
import Test.Hspec
main :: IO ()
main = hspec spec
spec :: Spec
spec = do
(supportExterns, supportForeigns, passingTestCases, warningTestCases, failingTestCases) <- runIO $ do
cwd <- getCurrentDirectory
let passing = cwd </> "examples" </> "passing"
let warning = cwd </> "examples" </> "warning"
let failing = cwd </> "examples" </> "failing"
let supportDir = cwd </> "tests" </> "support" </> "bower_components"
let supportFiles ext = Glob.globDir1 (Glob.compile ("purescript-*/src/**/*." ++ ext)) supportDir
passingFiles <- getTestFiles passing <$> testGlob passing
warningFiles <- getTestFiles warning <$> testGlob warning
failingFiles <- getTestFiles failing <$> testGlob failing
supportPurs <- supportFiles "purs"
supportPursFiles <- readInput supportPurs
supportExterns <- runExceptT $ do
modules <- ExceptT . return $ P.parseModulesFromFiles id supportPursFiles
foreigns <- inferForeignModules modules
externs <- ExceptT . fmap fst . runTest $ P.make (makeActions foreigns) (map snd modules)
return (zip (map snd modules) externs, foreigns)
case supportExterns of
Left errs -> fail (P.prettyPrintMultipleErrors P.defaultPPEOptions errs)
Right (externs, foreigns) -> return (externs, foreigns, passingFiles, warningFiles, failingFiles)
outputFile <- runIO $ do
tmp <- getTemporaryDirectory
createDirectoryIfMissing False (tmp </> logpath)
openFile (tmp </> logpath </> logfile) WriteMode
context "Passing examples" $
forM_ passingTestCases $ \testPurs ->
it ("'" <> takeFileName (getTestMain testPurs) <> "' should compile and run without error") $
assertCompiles supportExterns supportForeigns testPurs outputFile
context "Warning examples" $
forM_ warningTestCases $ \testPurs -> do
let mainPath = getTestMain testPurs
expectedWarnings <- runIO $ getShouldWarnWith mainPath
it ("'" <> takeFileName mainPath <> "' should compile with warning(s) '" <> intercalate "', '" expectedWarnings <> "'") $
assertCompilesWithWarnings supportExterns supportForeigns testPurs expectedWarnings
context "Failing examples" $
forM_ failingTestCases $ \testPurs -> do
let mainPath = getTestMain testPurs
expectedFailures <- runIO $ getShouldFailWith mainPath
it ("'" <> takeFileName mainPath <> "' should fail with '" <> intercalate "', '" expectedFailures <> "'") $
assertDoesNotCompile supportExterns supportForeigns testPurs expectedFailures
where
-- A glob for all purs and js files within a test directory
testGlob :: FilePath -> IO [FilePath]
testGlob = Glob.globDir1 (Glob.compile "**/*.purs")
-- Groups the test files so that a top-level file can have dependencies in a
-- subdirectory of the same name. The inner tuple contains a list of the
-- .purs files and the .js files for the test case.
getTestFiles :: FilePath -> [FilePath] -> [[FilePath]]
getTestFiles baseDir
= map (filter ((== ".purs") . takeExtensions) . map (baseDir </>))
. groupBy ((==) `on` extractPrefix)
. sortBy (compare `on` extractPrefix)
. map (makeRelative baseDir)
-- Takes the test entry point from a group of purs files - this is determined
-- by the file with the shortest path name, as everything but the main file
-- will be under a subdirectory.
getTestMain :: [FilePath] -> FilePath
getTestMain = minimumBy (compare `on` length)
-- Extracts the filename part of a .purs file, or if the file is in a
-- subdirectory, the first part of that directory path.
extractPrefix :: FilePath -> FilePath
extractPrefix fp =
let dir = takeDirectory fp
ext = reverse ".purs"
in if dir == "."
then maybe fp reverse $ stripPrefix ext $ reverse fp
else dir
-- Scans a file for @shouldFailWith directives in the comments, used to
-- determine expected failures
getShouldFailWith :: FilePath -> IO [String]
getShouldFailWith = extractPragma "shouldFailWith"
-- Scans a file for @shouldWarnWith directives in the comments, used to
-- determine expected warnings
getShouldWarnWith :: FilePath -> IO [String]
getShouldWarnWith = extractPragma "shouldWarnWith"
extractPragma :: String -> FilePath -> IO [String]
extractPragma pragma = fmap go . readUTF8File
where
go = lines >>> mapMaybe (stripPrefix ("-- @" ++ pragma ++ " ")) >>> map trim
inferForeignModules
:: MonadIO m
=> [(FilePath, P.Module)]
-> m (M.Map P.ModuleName FilePath)
inferForeignModules = P.inferForeignModules . fromList
where
fromList :: [(FilePath, P.Module)] -> M.Map P.ModuleName (Either P.RebuildPolicy FilePath)
fromList = M.fromList . map ((P.getModuleName *** Right) . swap)
trim :: String -> String
trim = dropWhile isSpace >>> reverse >>> dropWhile isSpace >>> reverse
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 (T.unpack (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 </> T.unpack (P.runModuleName mn)
exists <- liftIO $ doesDirectoryExist filePath
return (if exists then Just (P.internalError "getOutputTimestamp: read timestamp") else Nothing)
readInput :: [FilePath] -> IO [(FilePath, T.Text)]
readInput inputFiles = forM inputFiles $ \inputFile -> do
text <- readUTF8FileT inputFile
return (inputFile, text)
runTest :: P.Make a -> IO (Either P.MultipleErrors a, P.MultipleErrors)
runTest = P.runMake P.defaultOptions
compile
:: [(P.Module, P.ExternsFile)]
-> M.Map P.ModuleName FilePath
-> [FilePath]
-> ([P.Module] -> IO ())
-> IO (Either P.MultipleErrors [P.ExternsFile], P.MultipleErrors)
compile supportExterns supportForeigns inputFiles check = silence $ runTest $ do
fs <- liftIO $ readInput inputFiles
ms <- P.parseModulesFromFiles id fs
foreigns <- inferForeignModules ms
liftIO (check (map snd ms))
let actions = makeActions (foreigns `M.union` supportForeigns)
case ms of
[singleModule] -> pure <$> P.rebuildModule actions (map snd supportExterns) (snd singleModule)
_ -> P.make actions (map fst supportExterns ++ map snd ms)
assert
:: [(P.Module, P.ExternsFile)]
-> M.Map P.ModuleName FilePath
-> [FilePath]
-> ([P.Module] -> IO ())
-> (Either P.MultipleErrors P.MultipleErrors -> IO (Maybe String))
-> Expectation
assert supportExterns supportForeigns inputFiles check f = do
(e, w) <- compile supportExterns supportForeigns inputFiles check
maybeErr <- f (const w <$> e)
maybe (return ()) expectationFailure maybeErr
checkMain :: [P.Module] -> IO ()
checkMain ms =
unless (any ((== P.moduleNameFromString "Main") . P.getModuleName) ms)
(fail "Main module missing")
checkShouldFailWith :: [String] -> P.MultipleErrors -> Maybe String
checkShouldFailWith expected errs =
let actual = map P.errorCode $ P.runMultipleErrors errs
in if sort expected == sort (map T.unpack actual)
then Nothing
else Just $ "Expected these errors: " ++ show expected ++ ", but got these: " ++ show actual
assertCompiles
:: [(P.Module, P.ExternsFile)]
-> M.Map P.ModuleName FilePath
-> [FilePath]
-> Handle
-> Expectation
assertCompiles supportExterns supportForeigns inputFiles outputFile =
assert supportExterns supportForeigns inputFiles checkMain $ \e ->
case e of
Left errs -> return . Just . P.prettyPrintMultipleErrors P.defaultPPEOptions $ errs
Right _ -> do
process <- findNodeProcess
let entryPoint = modulesDir </> "index.js"
writeFile entryPoint "require('Main').main()"
result <- traverse (\node -> readProcessWithExitCode node [entryPoint] "") process
hPutStrLn outputFile $ "\n" <> takeFileName (last inputFiles) <> ":"
case result of
Just (ExitSuccess, out, err)
| not (null err) -> return $ Just $ "Test wrote to stderr:\n\n" <> err
| not (null out) && trim (last (lines out)) == "Done" -> do
hPutStr outputFile out
return Nothing
| otherwise -> return $ Just $ "Test did not finish with 'Done':\n\n" <> out
Just (ExitFailure _, _, err) -> return $ Just err
Nothing -> return $ Just "Couldn't find node.js executable"
assertCompilesWithWarnings
:: [(P.Module, P.ExternsFile)]
-> M.Map P.ModuleName FilePath
-> [FilePath]
-> [String]
-> Expectation
assertCompilesWithWarnings supportExterns supportForeigns inputFiles shouldWarnWith =
assert supportExterns supportForeigns inputFiles checkMain $ \e ->
case e of
Left errs ->
return . Just . P.prettyPrintMultipleErrors P.defaultPPEOptions $ errs
Right warnings ->
return
. fmap (printAllWarnings warnings)
$ checkShouldFailWith shouldWarnWith warnings
where
printAllWarnings warnings =
(<> "\n\n" <> P.prettyPrintMultipleErrors P.defaultPPEOptions warnings)
assertDoesNotCompile
:: [(P.Module, P.ExternsFile)]
-> M.Map P.ModuleName FilePath
-> [FilePath]
-> [String]
-> Expectation
assertDoesNotCompile supportExterns supportForeigns inputFiles shouldFailWith =
assert supportExterns supportForeigns inputFiles noPreCheck $ \e ->
case e of
Left 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
noPreCheck = const (return ())
logpath :: FilePath
logpath = "purescript-output"
logfile :: FilePath
logfile = "psc-tests.out"