scripths-0.5.0.0: test/Test/Parser.hs
module Test.Parser (parseTests) where
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (
assertBool,
assertFailure,
testCase,
(@?=),
)
import qualified Data.Text as T
import ScriptHs.Parser (
CabalMeta (
metaDeps,
metaExts,
metaGhcOptions,
metaPackages,
metaSourceRepos,
metaUnknownKeys
),
Line (..),
ScriptFile (scriptLines, scriptMeta),
SourceRepoPin (..),
parseScript,
)
parseTests :: TestTree
parseTests =
testGroup
"Parse"
[ testGroup
"Line classification"
[ testCase "blank line" $ do
let sf = parseScript "\n"
scriptLines sf @?= [Blank]
, testCase "import" $ do
let sf = parseScript "import Data.Text (Text)\n"
case scriptLines sf of
[Import t] -> assertBool "import text" (T.isPrefixOf "import " t)
other -> assertFailure $ "expected Import, got: " ++ show other
, testCase "qualified import" $ do
let sf = parseScript "import qualified Data.Map as Map\n"
case scriptLines sf of
[Import t] -> assertBool "qualified" (T.isInfixOf "qualified" t)
other -> assertFailure $ "expected Import, got: " ++ show other
, testCase "ghci command :set" $ do
let sf = parseScript ":set -XOverloadedStrings\n"
case scriptLines sf of
[GhciCommand t] -> t @?= ":set -XOverloadedStrings"
other -> assertFailure $ "expected GhciCommand, got: " ++ show other
, testCase "ghci command :def!" $ do
let sf = parseScript ":def! declareColumns \\s -> return s\n"
case scriptLines sf of
[GhciCommand t] -> assertBool ":def!" (T.isPrefixOf ":def!" t)
other -> assertFailure $ "expected GhciCommand, got: " ++ show other
, testCase "pragma" $ do
let sf = parseScript "{-# LANGUAGE TemplateHaskell #-}\n"
case scriptLines sf of
[Pragma t] -> assertBool "pragma" (T.isPrefixOf "{-#" t)
other -> assertFailure $ "expected Pragma, got: " ++ show other
, testCase "a leading -- scripths: version tag is dropped, not treated as code" $ do
let sf = parseScript "-- scripths: 0.4.1.0\nimport Data.Text (Text)\n"
case scriptLines sf of
[Import _] -> pure ()
other -> assertFailure $ "expected the tag dropped, got: " ++ show other
, testCase "a -- scripths: comment in the body is kept, not silently dropped" $ do
let sf = parseScript "x = 1\n-- scripths: just a note\ny = 2\n"
kept = any (lineHasText "-- scripths: just a note") (scriptLines sf)
assertBool "mid-body comment retained" kept
, testCase "a leading -- scripths:mime line is not a tag (kept)" $ do
let sf = parseScript "-- scripths:mime text/plain\nimport Data.Text (Text)\n"
kept = any (lineHasText "-- scripths:mime text/plain") (scriptLines sf)
assertBool "mime line retained" kept
, testCase "leading tag then a -- cabal: line: tag dropped, dep still parsed" $ do
let sf = parseScript "-- scripths: 1.2.3\n-- cabal: build-depends: text\nimport X\n"
(metaDeps . scriptMeta) sf @?= ["text"]
scriptLines sf @?= [Import "import X"]
, testCase
"a tag AFTER a shebang is not dropped (only the first non-blank is checked)"
$ do
-- Pins current behaviour: dropLeadingVersionTag inspects the first
-- non-blank line (the shebang), so the tag survives as a comment line.
let sf = parseScript "#!/usr/bin/env scripths\n-- scripths: 1.2.3\nimport X\n"
assertBool "tag line retained" $
any (lineHasText "-- scripths: 1.2.3") (scriptLines sf)
, testCase "haskell line" $ do
let sf = parseScript "print (5 + 5)\n"
case scriptLines sf of
[HaskellLine t] -> t @?= "print (5 + 5)"
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "IO bind line" $ do
let sf = parseScript "x <- getLine\n"
case scriptLines sf of
[HaskellLine t] -> assertBool "has <-" (T.isInfixOf "<-" t)
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "TH splice line" $ do
let sf = parseScript "$(declareColumns iris)\n"
case scriptLines sf of
[HaskellLine t] -> assertBool "has $(" (T.isPrefixOf "_ = ();" t)
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
]
, testGroup
"Cabal metadata"
[ testCase "build-depends" $ do
let sf = parseScript "-- cabal: build-depends: base, text, containers\n"
(metaDeps . scriptMeta) sf @?= ["base", "text", "containers"]
, testCase "default-extensions" $ do
let sf =
parseScript "-- cabal: default-extensions: TemplateHaskell, TypeApplications\n"
(metaExts . scriptMeta) sf @?= ["TemplateHaskell", "TypeApplications"]
, testCase "ghc-options" $ do
let sf = parseScript "-- cabal: ghc-options: -threaded, -O2\n"
(metaGhcOptions . scriptMeta) sf @?= ["-threaded", "-O2"]
, testCase "packages directive lists extra local package dirs" $ do
let sf = parseScript "-- cabal: packages: ../th, ../persistent\n"
(metaPackages . scriptMeta) sf @?= ["../th", "../persistent"]
, testCase "metadata stripped from lines" $ do
let input =
T.unlines
[ "-- cabal: build-depends: base"
, "import Data.Text"
]
let sf = parseScript input
(length . scriptLines) sf @?= 1
case scriptLines sf of
[Import _] -> pure ()
other -> assertFailure $ "expected [Import], got: " ++ show other
, testCase "multiple metadata lines merge" $ do
let input =
T.unlines
[ "-- cabal: build-depends: base, text"
, "-- cabal: build-depends: containers"
, "-- cabal: default-extensions: GADTs"
]
let sf = parseScript input
(metaDeps . scriptMeta) sf @?= ["base", "text", "containers"]
(metaExts . scriptMeta) sf @?= ["GADTs"]
, testCase "unknown cabal key is recorded (and warned), not a dep" $ do
let sf = parseScript "-- cabal: foo: bar, baz\n"
(metaDeps . scriptMeta) sf @?= []
(metaExts . scriptMeta) sf @?= []
(metaUnknownKeys . scriptMeta) sf @?= ["foo"]
, testCase "source-repository-package directive parses a git pin" $ do
let sf =
parseScript
"-- cabal: source-repository-package: https://x/repo abc123 sub\n"
(metaSourceRepos . scriptMeta) sf
@?= [SourceRepoPin "https://x/repo" "abc123" (Just "sub")]
]
, testGroup
"Multi-line scripts"
[ testCase "interleaved imports and expressions" $ do
let input =
T.unlines
[ "import Data.Text (Text)"
, ""
, "x <- getLine"
, ""
, "import Data.Map (Map)"
, ""
, "print x"
]
let sf = parseScript input
let ls = scriptLines sf
length ls @?= 7 -- 3 code + 4 blanks (trailing newline)
case filter notBlank ls of
[Import _, HaskellLine _, Import _, HaskellLine _] -> pure ()
other -> assertFailure $ "unexpected structure: " ++ show other
, testCase "empty input" $ do
let sf = parseScript ""
scriptLines sf @?= []
metaDeps (scriptMeta sf) @?= []
, testCase "no trailing newline" $ do
let sf = parseScript "print 42"
case scriptLines sf of
[HaskellLine t] -> t @?= "print 42"
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
]
, testGroup
"Edge cases"
[ testCase "comment that looks like cabal but isn't" $ do
let sf = parseScript "-- cabal is great\n"
case scriptLines sf of
[HaskellLine _] -> pure ()
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "regular comment" $ do
let sf = parseScript "-- this is a comment\n"
case scriptLines sf of
[HaskellLine t] -> assertBool "comment" (T.isPrefixOf "--" t)
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "indented import is haskell line" $ do
let sf = parseScript " import Data.Text\n"
case scriptLines sf of
[HaskellLine _] -> pure ()
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
]
, testGroup
"Let stripping (don't break)"
[ testCase "bare binding unchanged" $ do
let sf = parseScript "x = 5\n"
case scriptLines sf of
[HaskellLine t] -> t @?= "x = 5"
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "let-in expression preserved" $ do
let sf = parseScript "let x = 1 in x + 1\n"
case scriptLines sf of
[HaskellLine t] -> assertBool "has let-in" ("let " `T.isPrefixOf` t && " in " `T.isInfixOf` t)
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "indented let preserved (inside do/where)" $ do
let sf = parseScript " let x = 5\n"
case scriptLines sf of
[HaskellLine t] -> assertBool "still has let" ("let" `T.isInfixOf` t)
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
]
, testGroup
"Let stripping (new behavior)"
[ testCase "top-level let stripped" $ do
let sf = parseScript "let x = 5\n"
case scriptLines sf of
[HaskellLine t] -> t @?= "x = 5"
other -> assertFailure $ "expected HaskellLine \"x = 5\", got: " ++ show other
, testCase "top-level let with complex binding stripped" $ do
let sf = parseScript "let foo = bar 1 2\n"
case scriptLines sf of
[HaskellLine t] -> t @?= "foo = bar 1 2"
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "multiple let lines each stripped" $ do
let input = T.unlines ["let x = 1", "let y = 2"]
let sf = parseScript input
let code = filter notBlank (scriptLines sf)
case code of
[HaskellLine a, HaskellLine b] -> do
a @?= "x = 1"
b @?= "y = 2"
other -> assertFailure $ "expected two stripped lets, got: " ++ show other
, testCase "let-in NOT stripped" $ do
let sf = parseScript "let x = 1 in x + 1\n"
case scriptLines sf of
[HaskellLine t] -> assertBool "preserved" ("let " `T.isPrefixOf` t)
other -> assertFailure $ "expected HaskellLine, got: " ++ show other
, testCase "indented let NOT stripped" $ do
let sf = parseScript " let x = 5\n"
case scriptLines sf of
[HaskellLine t] -> assertBool "still has let" ("let " `T.isInfixOf` t)
other -> assertFailure $ "expected HaskellLine with let, got: " ++ show other
]
]
notBlank :: Line -> Bool
notBlank Blank = False
notBlank _ = True
-- | Does a parsed line carry text containing the needle?
lineHasText :: T.Text -> Line -> Bool
lineHasText s ln = case ln of
HaskellLine t -> s `T.isInfixOf` t
GhciCommand t -> s `T.isInfixOf` t
Pragma t -> s `T.isInfixOf` t
Import t -> s `T.isInfixOf` t
Blank -> False