packages feed

scripths-0.5.0.0: test/Test/Compiled.hs

module Test.Compiled (compiledTests) where

import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))

import Data.Text (Text)
import qualified Data.Text as T
import ScriptHs.Compiled (
    CellChunk (..),
    CompileIssue (..),
    checkCompilable,
    isValidModuleName,
    linePragmaTag,
    parseLinePragmaTag,
    renderCompiledModule,
 )
import ScriptHs.Parser (
    CompileDirective (..),
    ScriptFile (scriptCompile, scriptLines),
    Line (..),
    parseScript,
    parseScriptNumbered,
 )

numbered :: Text -> [(Int, Line)]
numbered = snd . parseScriptNumbered

compiledTests :: TestTree
compiledTests =
    testGroup
        "Compiled"
        [ directiveTests
        , numberedTests
        , checkTests
        , nameTests
        , tagTests
        , renderTests
        ]

directiveTests :: TestTree
directiveTests =
    testGroup
        "compile directive"
        [ testCase "absent by default" $
            scriptCompile (parseScript "x = 1\n") @?= Nothing
        , testCase "bare directive" $
            scriptCompile (parseScript "-- compile\nx = 1\n")
                @?= Just CompileDefault
        , testCase "named directive" $
            scriptCompile (parseScript "-- compile: Training\nx = 1\n")
                @?= Just (CompileNamed "Training")
        , testCase "dotted name" $
            scriptCompile (parseScript "-- compile: Training.Core\nx = 1\n")
                @?= Just (CompileNamed "Training.Core")
        , testCase "directive removed from script lines" $
            scriptLines (parseScript "-- compile\nx = 1\n")
                @?= [HaskellLine "x = 1"]
        , testCase "directive after cabal lines" $ do
            let sf = parseScript "-- cabal: build-depends: text\n-- compile\nx = 1\n"
            scriptCompile sf @?= Just CompileDefault
        , testCase "whitespace tolerated" $
            scriptCompile (parseScript "--  compile:  Training \nx = 1\n")
                @?= Just (CompileNamed "Training")
        , testCase "unrelated comment is not a directive" $
            scriptCompile (parseScript "-- compiled fast\nx = 1\n") @?= Nothing
        ]

numberedTests :: TestTree
numberedTests =
    testGroup
        "parseScriptNumbered"
        [ testCase "plain lines numbered from 1" $
            numbered "x = 1\ny = 2\n"
                @?= [(1, HaskellLine "x = 1"), (2, HaskellLine "y = 2")]
        , testCase "directive lines keep following numbering" $
            numbered "-- cabal: build-depends: text\n-- compile\nx = 1\n"
                @?= [(3, HaskellLine "x = 1")]
        , testCase "agrees with parseScript lines" $ do
            let src = "-- compile\nimport Data.List\n\nf :: Int -> Int\nf x = x\n"
            map snd (numbered src) @?= scriptLines (parseScript src)
        ]

checkTests :: TestTree
checkTests =
    testGroup
        "checkCompilable"
        [ testCase "declarations pass" $
            checkCompilable (numbered "f :: Int -> Int\nf x = x + 1\n") @?= []
        , testCase "imports and pragmas pass" $
            checkCompilable
                (numbered "{-# LANGUAGE LambdaCase #-}\nimport Data.List\n")
                @?= []
        , testCase "data declaration passes" $
            checkCompilable (numbered "data T = A | B\n") @?= []
        , testCase "bare expression rejected with its line" $ do
            let issues = checkCompilable (numbered "f x = x\n\nprint 3\n")
            map ciLine issues @?= [3]
        , testCase "monadic bind rejected" $ do
            let issues = checkCompilable (numbered "x <- readLn\n")
            map ciLine issues @?= [1]
        , testCase ":set -X allowed, other ghci commands rejected" $ do
            checkCompilable (numbered ":set -XLambdaCase\nf x = x\n") @?= []
            map ciLine (checkCompilable (numbered ":type f\nf x = x\n")) @?= [1]
        , testCase "TH splice allowed" $
            checkCompilable (numbered "$(pure [])\n") @?= []
        ]

nameTests :: TestTree
nameTests =
    testGroup
        "isValidModuleName"
        [ testCase "simple" $ isValidModuleName "Training" @?= True
        , testCase "dotted" $ isValidModuleName "Training.Core" @?= True
        , testCase "lowercase rejected" $ isValidModuleName "training" @?= False
        , testCase "empty segment rejected" $ isValidModuleName "A..B" @?= False
        , testCase "empty rejected" $ isValidModuleName "" @?= False
        , testCase "spaces rejected" $ isValidModuleName "A B" @?= False
        ]

tagTests :: TestTree
tagTests =
    testGroup
        "line pragma tags"
        [ testCase "round trip" $
            parseLinePragmaTag (linePragmaTag 42) @?= Just 42
        , testCase "non-tag rejected" $
            parseLinePragmaTag "Training" @?= Nothing
        ]

renderTests :: TestTree
renderTests =
    testGroup
        "renderCompiledModule"
        [ testCase "module header and decl with LINE pragma" $ do
            let src = renderCompiledModule "Training" [] [] [chunk 7 "f :: Int -> Int\nf x = x + 1\n"]
            assertContains src "module Training where"
            assertContains src ("{-# LINE 1 \"" <> linePragmaTag 7 <> "\" #-}")
            assertContains src "f x = x + 1"
        , testCase "default extensions become pragmas" $ do
            let src = renderCompiledModule "M" ["LambdaCase"] [] [chunk 1 "f x = x\n"]
            assertContains src "{-# LANGUAGE LambdaCase #-}"
        , testCase "imports hoisted with their own pragma and line" $ do
            let src =
                    renderCompiledModule
                        "M"
                        []
                        []
                        [chunk 3 "f x = x\nimport Data.List (sort)\ng y = sort y\n"]
            assertContains src ("{-# LINE 2 \"" <> linePragmaTag 3 <> "\" #-}")
            assertContains src "import Data.List (sort)"
            -- imports appear before declarations
            assertBool "import before decl" $
                textIndex "import Data.List" src < textIndex "f x = x" src
        , testCase "extra imports rendered before cell imports" $ do
            let src = renderCompiledModule "M" [] ["import Helpers"] [chunk 1 "f x = x\n"]
            assertContains src "import Helpers"
        , testCase ":set -X contributes a pragma" $ do
            let src = renderCompiledModule "M" [] [] [chunk 1 ":set -XGADTs\nf x = x\n"]
            assertContains src "{-# LANGUAGE GADTs #-}"
        , testCase "pragmas deduped" $ do
            let src =
                    renderCompiledModule
                        "M"
                        ["GADTs"]
                        []
                        [chunk 1 "{-# LANGUAGE GADTs #-}\nf x = x\n"]
            T.count "LANGUAGE GADTs" src @?= 1
        , testCase "unused-import warnings suppressed" $ do
            let src = renderCompiledModule "M" [] [] [chunk 1 "f x = x\n"]
            assertContains src "{-# OPTIONS_GHC -Wno-unused-imports #-}"
        , testCase "two chunks keep their own tags" $ do
            let src =
                    renderCompiledModule
                        "M"
                        []
                        []
                        [chunk 1 "f x = x\n", chunk 2 "g y = y\n"]
            assertContains src (linePragmaTag 1)
            assertContains src (linePragmaTag 2)
        ]
  where
    chunk cid src = CellChunk{ccTag = linePragmaTag cid, ccLines = numbered src}
    assertContains hay needle =
        assertBool (T.unpack ("missing: " <> needle <> "\nin:\n" <> hay)) $
            needle `T.isInfixOf` hay
    textIndex needle hay = T.length (fst (T.breakOn needle hay))