git.iko.soy/rowtype-yoga/purescm
on bytevector help sign in
Setup snapshots based on purs-backend-es
commit210d73bbdd2db5226207a9e06324e713add08de1 authorJordan Martinez <jordanalex.martinez@protonmail.com> dateTue 14 Mar 2023, 07:22 (4 years ago) parent19c8ef7
11 files changed+416−4
.gitignore+6 README.md+6 package.json+6 spago.dhall+1−1 test-snapshots/snapshots-input/Snapshot.Literals.purs+31 test-snapshots/snapshots-output/Snapshot.Literals.ss+1 test-snapshots/spago.dhall+7 test.dhall+44 test/Main.purs+176−3 test/Utils.js+6 test/Utils.purs+132
.gitignore+6
@@ -15,3 +15,9 @@
1515/*.purs*
1616/**/.DS_Store
1717/examples/**/out
18+/test-snapshots/.spago
19+/test-snapshots/output
20+/test-snapshots/.purs*
21+/test-snapshots/.psc*
22+/test-snapshots/generated-docs
23+/test-snapshots/test-out/
README.md+6
@@ -1 +1,7 @@
11# purs-backend-chez
2+
3+## Snapshots
4+
5+To run snapshots, run `npm run test`
6+To add a new snapshot, create a file called `Snapshot.X.purs` where `X` indicates what is being tested.
7+If a snapshot needs a dependency, install it by running `spago -x test-snapshots/spago.dhall install <packages...>`. The snapshots have their own `spago.dhall` file so as not to pollute the `purs-backend-chez` binary with unneeded dependencies.
package.jsonnew file+6
@@ -0,0 +1,6 @@
1+{
2+ "private": true,
3+ "scripts": {
4+ "test": "spago -x test.dhall test"
5+ }
6+}
spago.dhall+1−1
@@ -21,5 +21,5 @@
2121 , "tuples"
2222 ]
2323, packages = ./packages.dhall
24−, sources = [ "src/**/*.purs", "test/**/*.purs" ]
24+, sources = [ "src/**/*.purs" ]
2525}
test-snapshots/snapshots-input/Snapshot.Literals.pursnew file+31
@@ -0,0 +1,31 @@
1+module Snapshot.Literals where
2+
3+boolean1 :: Boolean
4+boolean1 = true
5+
6+boolean2 :: Boolean
7+boolean2 = false
8+
9+int :: Int
10+int = 1
11+
12+number :: Number
13+number = 2.0
14+
15+char :: Char
16+char = 'a'
17+
18+string :: String
19+string = "string"
20+
21+array :: Array Int
22+array = []
23+
24+array2 :: Array Int
25+array2 = [ 1, 2, 3 ]
26+
27+record :: {}
28+record = {}
29+
30+record2 :: { foo :: String }
31+record2 = { foo: "bar" }
test-snapshots/snapshots-output/Snapshot.Literals.ssnew file+1
@@ -0,0 +1 @@
1+(library (Snapshot.Literals lib) (export array array2 boolean1 boolean2 char int number record record2 string) (import (prefix (chezscheme) scm:)) (scm:define string "string") (scm:define record2 (scm:letrec* (($record (scm:make-hashtable scm:string-hash scm:string=?))) (scm:hashtable-set! $record "foo" "bar") $record)) (scm:define record (scm:letrec* (($record (scm:make-hashtable scm:string-hash scm:string=?))) $record)) (scm:define number 2.0) (scm:define int 1) (scm:define char #\a) (scm:define boolean2 #f) (scm:define boolean1 #t) (scm:define array2 (scm:vector 1 2 3)) (scm:define array (scm:vector)))
test-snapshots/spago.dhallnew file+7
@@ -0,0 +1,7 @@
1+{ name = "snapshots"
2+, dependencies =
3+ [ "prelude"
4+ ]
5+, packages = ../packages.dhall
6+, sources = [ "snapshots-input/**/*.purs" ]
7+}
test.dhallnew file+44
@@ -0,0 +1,44 @@
1+{ name = "purs-backend-chez-tests"
2+, dependencies =
3+ [ "aff"
4+ , "ansi"
5+ , "argonaut"
6+ , "argparse-basic"
7+ , "arrays"
8+ , "backend-optimizer"
9+ , "bifunctors"
10+ , "console"
11+ , "control"
12+ , "debug"
13+ , "dodo-printer"
14+ , "effect"
15+ , "either"
16+ , "filterable"
17+ , "foldable-traversable"
18+ , "lazy"
19+ , "lists"
20+ , "maybe"
21+ , "newtype"
22+ , "node-buffer"
23+ , "node-child-process"
24+ , "node-fs"
25+ , "node-fs-aff"
26+ , "node-glob-basic"
27+ , "node-path"
28+ , "node-process"
29+ , "node-streams"
30+ , "ordered-collections"
31+ , "parallel"
32+ , "partial"
33+ , "posix-types"
34+ , "prelude"
35+ , "profunctor"
36+ , "refs"
37+ , "safe-coerce"
38+ , "strings"
39+ , "transformers"
40+ , "tuples"
41+ ]
42+, packages = ./packages.dhall
43+, sources = [ "src/**/*.purs", "test/**/*.purs" ]
44+}
test/Main.purs+176−3
@@ -2,10 +2,183 @@ module Test.Main where
22
33import Prelude
44
5+import Ansi.Codes (Color(..))
6+import Ansi.Output (foreground, withGraphics)
7+import ArgParse.Basic (ArgParser)
8+import ArgParse.Basic as ArgParser
9+import Data.Array (findMap)
10+import Data.Array as Array
11+import Data.Array.NonEmpty (NonEmptyArray)
12+import Data.Either (Either(..))
13+import Data.Foldable (for_)
14+import Data.Foldable as Foldable
15+import Data.Map as Map
16+import Data.Maybe (Maybe(..))
17+import Data.Monoid (power)
18+import Data.Newtype (unwrap)
19+import Data.Set as Set
20+import Data.String (Pattern(..))
21+import Data.String as String
22+import Data.String.CodeUnits as SCU
23+import Data.TraversableWithIndex (forWithIndex)
24+import Data.Tuple (Tuple(..))
25+import Dodo (plainText)
26+import Dodo as Dodo
527import Effect (Effect)
6−import Effect.Class.Console (log)
28+import Effect.Aff (Aff, Error, attempt, launchAff_)
29+import Effect.Aff as Error
30+import Effect.Class (liftEffect)
31+import Effect.Class.Console as Console
32+import Effect.Ref as Ref
33+import Node.Encoding (Encoding(..))
34+import Node.FS.Aff as FS
35+import Node.Glob.Basic (expandGlobs)
36+import Node.Path as Path
37+import Node.Process as Process
38+import PureScript.Backend.Chez.Convert (codegenModule)
39+import PureScript.Backend.Chez.Syntax as S
40+import PureScript.Backend.Optimizer.Builder (buildModules)
41+import PureScript.Backend.Optimizer.Convert (BackendModule)
42+import PureScript.Backend.Optimizer.CoreFn (Comment(..), Module(..), ModuleName(..))
43+import PureScript.Backend.Optimizer.Directives (parseDirectiveFile)
44+import PureScript.Backend.Optimizer.Directives.Defaults (defaultDirectives)
45+import PureScript.Backend.Optimizer.Semantics.Foreign (coreForeignSemantics)
46+import Test.Utils (bufferToUTF8, coreFnModulesFromOutput, execWithStdin, mkdirp, spawnFromParent)
47+
48+type TestArgs =
49+ { accept :: Boolean
50+ , filter :: NonEmptyArray String
51+ }
52+
53+argParser :: ArgParser TestArgs
54+argParser =
55+ ArgParser.fromRecord
56+ { accept:
57+ ArgParser.flag [ "--accept", "-a" ]
58+ "Accepts snapshot output"
59+ # ArgParser.boolean
60+ # ArgParser.default false
61+ , filter:
62+ ArgParser.argument [ "--filter", "-f" ]
63+ "Filter tests matching a prefix"
64+ # ArgParser.unfolded1
65+ # ArgParser.default (pure "Snapshot.*")
66+ }
767
868main :: Effect Unit
969main = do
10− log "🍝"
11− log "You should add some tests."
70+ cliArgs <- Array.drop 2 <$> Process.argv
71+ case ArgParser.parseArgs "test" "" argParser cliArgs of
72+ Left err ->
73+ Console.error $ ArgParser.printArgError err
74+ Right args ->
75+ launchAff_ $ runSnapshotTests args
76+
77+runSnapshotTests :: TestArgs -> Aff Unit
78+runSnapshotTests { accept, filter } = do
79+ liftEffect $ Process.chdir $ Path.concat [ "test-snapshots" ]
80+ spawnFromParent "spago" [ "build", "-u", "-g corefn" ]
81+ snapshotDir <- liftEffect Process.cwd
82+ snapshotPaths <- expandGlobs (Path.concat [ snapshotDir, "snapshots-input" ])
83+ [ "Snapshot.*.purs" ]
84+ outputRef <- liftEffect $ Ref.new Map.empty
85+ let snapshotsOut = Path.concat [ snapshotDir, "snapshots-output" ]
86+ let testOut = Path.concat [ snapshotDir, "test-out" ]
87+ mkdirp snapshotsOut
88+ mkdirp testOut
89+ coreFnModulesFromOutput "output" filter >>= case _ of
90+ Left errors -> do
91+ for_ errors \(Tuple filePath err) -> do
92+ Console.error $ filePath <> " " <> err
93+ liftEffect $ Process.exit 1
94+ Right coreFnModules -> do
95+ let { directives } = parseDirectiveFile defaultDirectives
96+ -- No runtime .ss files needed yet
97+ -- copyFile (Path.concat [ "..", "..", "runtime.js" ]) (Path.concat [ testOut, "runtime.js" ])
98+ coreFnModules # buildModules
99+ { directives
100+ , foreignSemantics: coreForeignSemantics -- no chez scheme specific foreign semantics yet
101+ , onCodegenModule: \_ (Module { name: ModuleName name, path }) backend -> do
102+ let
103+ formatted =
104+ Dodo.print plainText Dodo.twoSpaces $ S.printChezExpr $ codegenModule
105+ backend
106+ let testFileDir = Path.concat [ testOut, name ]
107+ let testFilePath = Path.concat [ testFileDir, "output.ss" ]
108+ mkdirp testFileDir
109+ FS.writeTextFile UTF8 testFilePath formatted
110+ -- Not worried about FFI yet
111+ -- unless (Set.isEmpty backend.foreign) do
112+ -- let
113+ -- foreignSiblingPath =
114+ -- fromMaybe path (String.stripSuffix (Pattern (Path.extname path)) path) <> ".ss"
115+ -- let foreignOutputPath = Path.concat [ testFileDir, "foreign.ss" ]
116+ -- copyFile foreignSiblingPath foreignOutputPath
117+ when (Set.member (Path.concat [ snapshotDir, path ]) snapshotPaths) do
118+ void $ liftEffect $ Ref.modify (Map.insert name (Tuple formatted (hasFails backend)))
119+ outputRef
120+ , onPrepareModule: \build coreFnMod@(Module { name }) -> do
121+ let total = show build.moduleCount
122+ let index = show (build.moduleIndex + 1)
123+ let padding = power " " (SCU.length total - SCU.length index)
124+ Console.log $ "[" <> padding <> index <> " of " <> total <> "] Building " <> unwrap name
125+ pure coreFnMod
126+ }
127+ outputModules <- liftEffect $ Ref.read outputRef
128+ results <- forWithIndex outputModules \name (Tuple output _failsWith) -> do
129+ let
130+ snapshotFilePath = Path.concat [ snapshotsOut, name <> ".ss" ]
131+ -- Not doing acceptance tests yet
132+ -- runAcceptedTest = do
133+ -- result <- attempt $ foldMap liftEffect =<< loadModuleMain =<< liftEffect
134+ -- (Path.resolve [ testOut, name ] "output.ss")
135+ -- case result of
136+ -- Left err | matchesFail err failsWith -> do
137+ -- Console.log $ withGraphics (foreground Red) "✗" <> " " <> name <> " failed."
138+ -- Console.log $ Error.message err
139+ -- pure false
140+ -- Right _ | isJust failsWith -> do
141+ -- Console.log $ withGraphics (foreground Red) "✗" <> " " <> name <>
142+ -- " succeeded when it should have failed."
143+ -- pure false
144+ -- _ ->
145+ -- pure true
146+ attempt (FS.readTextFile UTF8 snapshotFilePath) >>= case _ of
147+ Left _ -> do
148+ Console.log $ withGraphics (foreground Yellow) "✓" <> " " <> name <> " saved."
149+ FS.writeTextFile UTF8 snapshotFilePath output
150+ pure true
151+ Right prevOutput
152+ | output == prevOutput ->
153+ -- runAcceptedTest
154+ pure true
155+ | accept -> do
156+ Console.log $ withGraphics (foreground Yellow) "✓" <> " " <> name <> " accepted."
157+ FS.writeTextFile UTF8 snapshotFilePath output
158+ -- runAcceptedTest
159+ pure true
160+ | otherwise -> do
161+ Console.log $ withGraphics (foreground Red) "✗" <> " " <> name <> " failed."
162+ diff <- bufferToUTF8 <<< _.stdout =<< execWithStdin
163+ ("diff " <> snapshotFilePath <> " -")
164+ output
165+ Console.log diff
166+ pure false
167+ unless (Foldable.and results) do
168+ liftEffect $ Process.exit 1
169+
170+hasFails :: BackendModule -> Maybe String
171+hasFails = findMap go <<< _.comments
172+ where
173+ go = case _ of
174+ LineComment comm ->
175+ String.stripPrefix (Pattern "@fails ") (String.trim comm)
176+ _ ->
177+ Nothing
178+
179+matchesFail :: Error -> Maybe String -> Boolean
180+matchesFail err = case _ of
181+ Just msg ->
182+ not $ String.contains (Pattern msg) $ Error.message err
183+ Nothing ->
184+ true
test/Utils.jsnew file+6
@@ -0,0 +1,6 @@
1+export const loadModuleMainImpl = onError => onSuccessMain => onSuccess => path => () => {
2+ import(path).then(
3+ mod => mod.main ? onSuccessMain(mod.main)() : onSuccess(),
4+ err => onError(err)()
5+ );
6+}
test/Utils.pursnew file+132
@@ -0,0 +1,132 @@
1+module Test.Utils where
2+
3+import Prelude
4+
5+import Control.Monad.Except (ExceptT(..), lift, runExceptT)
6+import Control.Parallel (parTraverse)
7+import Data.Argonaut as Json
8+import Data.Array.NonEmpty (NonEmptyArray)
9+import Data.Array.NonEmpty as NonEmptyArray
10+import Data.Bifunctor (lmap)
11+import Data.Compactable (separate)
12+import Data.Either (Either(..))
13+import Data.Foldable (foldl)
14+import Data.Lazy as Lazy
15+import Data.List (List)
16+import Data.Maybe (Maybe(..), maybe)
17+import Data.Posix.Signal (Signal(..))
18+import Data.Set as Set
19+import Data.Set.NonEmpty as NonEmptySet
20+import Data.Tuple (Tuple(..))
21+import Effect (Effect)
22+import Effect.Aff (Aff, Error, effectCanceler, error, makeAff, throwError)
23+import Effect.Class (liftEffect)
24+import Node.Buffer (Buffer, freeze)
25+import Node.Buffer.Immutable as ImmutableBuffer
26+import Node.ChildProcess (ExecResult, Exit(..), defaultExecOptions, defaultSpawnOptions, inherit)
27+import Node.ChildProcess as ChildProcess
28+import Node.Encoding (Encoding(..))
29+import Node.FS.Aff as FS
30+import Node.FS.Perms (mkPerms)
31+import Node.FS.Perms as Perms
32+import Node.FS.Stats as Stats
33+import Node.FS.Stream (createReadStream, createWriteStream)
34+import Node.Glob.Basic (expandGlobs)
35+import Node.Path (FilePath)
36+import Node.Process as Process
37+import Node.Stream as Stream
38+import PureScript.Backend.Optimizer.CoreFn (Ann, Module, ModuleName(..))
39+import PureScript.Backend.Optimizer.CoreFn.Json (decodeModule)
40+import PureScript.Backend.Optimizer.CoreFn.Sort (emptyPull, pullResult, resumePull, sortModules)
41+
42+spawnFromParent :: String -> Array String -> Aff Unit
43+spawnFromParent command args = makeAff \k -> do
44+ childProc <- ChildProcess.spawn command args defaultSpawnOptions { stdio = inherit }
45+ ChildProcess.onExit childProc case _ of
46+ Normally code
47+ | code > 0 -> Process.exit code
48+ | otherwise -> k (Right unit)
49+ BySignal _ ->
50+ Process.exit 1
51+ pure $ effectCanceler do
52+ ChildProcess.kill SIGABRT childProc
53+
54+execWithStdin :: String -> String -> Aff ExecResult
55+execWithStdin command input = makeAff \k -> do
56+ childProc <- ChildProcess.exec command defaultExecOptions (k <<< pure)
57+ _ <- Stream.writeString (ChildProcess.stdin childProc) UTF8 input mempty
58+ Stream.end (ChildProcess.stdin childProc) mempty
59+ pure $ effectCanceler $ ChildProcess.kill SIGABRT childProc
60+
61+bufferToUTF8 :: Buffer -> Aff String
62+bufferToUTF8 = liftEffect <<< map (ImmutableBuffer.toString UTF8) <<< freeze
63+
64+mkdirp :: FilePath -> Aff Unit
65+mkdirp path = FS.mkdir' path { recursive: true, mode: mkPerms Perms.all Perms.all Perms.all }
66+
67+foreign import loadModuleMainImpl
68+ :: (Error -> Effect Unit)
69+ -> (Effect Unit -> Effect Unit)
70+ -> Effect Unit
71+ -> FilePath
72+ -> Effect Unit
73+
74+-- | Note: this code needs to be updated to run Chez Scheme scripts
75+loadModuleMain :: FilePath -> Aff (Maybe (Effect Unit))
76+loadModuleMain path = makeAff \k -> do
77+ loadModuleMainImpl (k <<< Left) (k <<< Right <<< Just) (k (Right Nothing)) path
78+ mempty
79+
80+copyFile :: FilePath -> FilePath -> Aff Unit
81+copyFile from to = do
82+ stats <- FS.stat from
83+ unless (Stats.isFile stats) do
84+ throwError $ error $ "Not a file: " <> from
85+ makeAff \k -> do
86+ src <- createReadStream from
87+ dst <- createWriteStream to
88+ res <- Stream.pipe src dst
89+ Stream.onError src (k <<< Left)
90+ Stream.onError dst (k <<< Left)
91+ Stream.onError res (k <<< Left)
92+ Stream.onFinish res (k (Right unit))
93+ pure $ effectCanceler do
94+ Stream.destroy res
95+ Stream.destroy dst
96+ Stream.destroy src
97+
98+coreFnModulesFromOutput
99+ :: String
100+ -> NonEmptyArray String
101+ -> Aff (Either (NonEmptyArray (Tuple FilePath String)) (List (Module Ann)))
102+coreFnModulesFromOutput path globs = runExceptT do
103+ paths <- Set.toUnfoldable <$> lift
104+ (expandGlobs path ((_ <> "/corefn.json") <$> NonEmptyArray.toArray globs))
105+ case NonEmptyArray.toArray globs of
106+ [ "**" ] ->
107+ sortModules <$> modulesFromPaths paths
108+ _ ->
109+ go <<< foldl resumePull emptyPull =<< modulesFromPaths paths
110+ where
111+ modulesFromPaths paths = ExceptT do
112+ { left, right } <- separate <$> parTraverse readCoreFnModule paths
113+ pure $ maybe (Right right) Left $ NonEmptyArray.fromArray left
114+
115+ pathFromModuleName (ModuleName mn) =
116+ path <> "/" <> mn <> "/corefn.json"
117+
118+ go pull = case pullResult pull of
119+ Left needed ->
120+ go <<< foldl resumePull pull =<< modulesFromPaths
121+ (pathFromModuleName <$> NonEmptySet.toUnfoldable needed)
122+ Right modules ->
123+ pure $ Lazy.force modules
124+
125+readCoreFnModule :: String -> Aff (Either (Tuple FilePath String) (Module Ann))
126+readCoreFnModule filePath = do
127+ contents <- FS.readTextFile UTF8 filePath
128+ case lmap Json.printJsonDecodeError <<< decodeModule =<< Json.jsonParser contents of
129+ Left err -> do
130+ pure $ Left $ Tuple filePath err
131+ Right mod ->
132+ pure $ Right mod