packages feed

scripths-0.5.0.0: test/Test/Repl.hs

module Test.Repl (replTests) where

import qualified Data.Text as T
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))

import ScriptHs.Repl (
    autoPrintDirective,
    cdDirective,
    compileCdSetup,
    compileCdTo,
    replEvalExpr,
    scripthsInternalQualifiers,
    scrubInternalNames,
 )

replTests :: TestTree
replTests =
    testGroup
        "Repl"
        [ compileCdToTests
        , cdDirectiveTests
        , preludeAgnosticTests
        , scrubTests
        ]

cdDirectiveTests :: TestTree
cdDirectiveTests =
    testGroup
        "cdDirective"
        [ testCase "runtime chdir via qualified System.Directory, show-escaped path" $ do
            let txt = cdDirective "/data/my project"
            assertBool "imports Directory qualified" $
                T.isInfixOf "import qualified System.Directory as ScripthsInternalDir" txt
            assertBool "calls setCurrentDirectory" $
                T.isInfixOf "ScripthsInternalDir.setCurrentDirectory" txt
            assertBool "path is show-escaped (quoted)" $
                T.isInfixOf "\"/data/my project\"" txt
            assertBool "no bare Prelude" (not (T.isInfixOf "Prelude" txt))
        ]

compileCdToTests :: TestTree
compileCdToTests =
    testGroup
        "compileCdTo"
        [ testCase "renders a compile-time setCurrentDirectory splice" $ do
            let txt = compileCdTo "/some/dir"
            assertBool "splice block" (T.isInfixOf ":{" txt)
            assertBool "chdir call" (T.isInfixOf "setCurrentDirectory" txt)
            assertBool "the target dir" (T.isInfixOf "/some/dir" txt)
        , testCase "sequences via qualified >>/pure, not bare Prelude" $ do
            let txt = compileCdTo "/some/dir"
            assertBool "qualified (>>)" (T.isInfixOf "ScripthsInternalMonad.>>" txt)
            assertBool "qualified pure" (T.isInfixOf "ScripthsInternalApplicative.pure" txt)
            assertBool "no bare Prelude" (not (T.isInfixOf "Prelude." txt))
        ]

preludeAgnosticTests :: TestTree
preludeAgnosticTests =
    testGroup
        "prelude-agnostic injection"
        -- Every name scripths injects into the repl is qualified against a base
        -- module other than Prelude, so a notebook under NoImplicitPrelude / a
        -- custom prelude still runs (and the implicit Prelude is not suppressed).
        [ testCase "autoPrintDirective qualifies through System.IO, not Prelude" $ do
            assertBool "imports System.IO qualified" $
                T.isInfixOf
                    "import qualified System.IO as ScripthsInternalIO"
                    autoPrintDirective
            assertBool "sets the auto-print" $
                T.isInfixOf "-interactive-print scripthsAutoPrint" autoPrintDirective
            assertBool "never mentions Prelude" $
                not (T.isInfixOf "Prelude" autoPrintDirective)
        , testCase "compileCdSetup imports the sequencing modules qualified" $ do
            assertBool "Control.Monad" $
                T.isInfixOf
                    "import qualified Control.Monad as ScripthsInternalMonad"
                    compileCdSetup
            assertBool "Control.Applicative" $
                T.isInfixOf
                    "import qualified Control.Applicative as ScripthsInternalApplicative"
                    compileCdSetup
        , testCase "the -e terminator is prelude-free and space-free" $ do
            assertBool
                "uses qualified hFlush"
                (T.isInfixOf "ScripthsInternalIO.hFlush" replEvalExpr)
            assertBool "no Prelude" (not (T.isInfixOf "Prelude" replEvalExpr))
            assertBool "no return" (not (T.isInfixOf "return" replEvalExpr))
            assertBool "no spaces" (not (T.isInfixOf " " replEvalExpr))
        , testCase "the full injected preamble names no Prelude dependency" $ do
            -- The whole script.ghci prefix, assembled, must not mention Prelude.
            let injected =
                    autoPrintDirective
                        <> cdDirective "/some/dir"
                        <> compileCdSetup
                        <> compileCdTo "/some/dir"
            assertBool "no Prelude anywhere" (not (T.isInfixOf "Prelude" injected))
        ]

scrubTests :: TestTree
scrubTests =
    testGroup
        "scrubInternalNames"
        -- Captured GHCi diagnostics must read like vanilla GHCi, never exposing
        -- the synthetic preamble's identifiers.
        [ testCase "rewrites scripthsAutoPrint to print" $ do
            let s =
                    scrubInternalNames
                        "arising from a use of scripthsAutoPrint\nscripthsAutoPrint it"
            assertBool "no scripthsAutoPrint" (not (T.isInfixOf "scripthsAutoPrint" s))
            assertBool "reads as print it" (T.isInfixOf "print it" s)
        , testCase "strips an internal qualifier (the IsString leak)" $
            scrubInternalNames "No instance for (ScripthsInternalStr.IsString Int)"
                @?= "No instance for (IsString Int)"
        , testCase "removes every internal qualifier" $ do
            let sample = T.unwords [q <> ".foo" | q <- scripthsInternalQualifiers]
            assertBool
                "no ScripthsInternal remains"
                (not (T.isInfixOf "ScripthsInternal" (scrubInternalNames sample)))
        , testCase "rewrites the ScripthsAutoPrint class to Show" $ do
            -- The common non-Showable-cell error is `No instance for (ScripthsAutoPrint …)`.
            let s =
                    scrubInternalNames
                        "No instance for (ScripthsAutoPrint Foo) arising from a use of scripthsAutoPrint"
            assertBool "class gone" (not (T.isInfixOf "ScripthsAutoPrint" s))
            assertBool "reads as Show" (T.isInfixOf "No instance for (Show Foo)" s)
        , testCase "a bare alias (no trailing dot) becomes its real module" $
            scrubInternalNames "Could not load module 'ScripthsInternalDir'"
                @?= "Could not load module 'System.Directory'"
        , testCase "leaves ordinary output untouched" $
            scrubInternalNames "Variable not in scope: frobnicate"
                @?= "Variable not in scope: frobnicate"
        , testCase "preserves multibyte Unicode while scrubbing (guards the UTF-8 path)" $ do
            -- GHC diagnostics carry Unicode (curly quotes, the • bullet); scrubbing an
            -- internal name beside them must not mangle the surrounding text.
            let s =
                    scrubInternalNames
                        "Variable not in scope: \8216caf\233\8217 \8226 ScripthsInternalDir.x"
            assertBool "keeps the curly-quoted café" (T.isInfixOf "\8216caf\233\8217" s)
            assertBool "keeps the bullet" (T.isInfixOf "\8226" s)
            assertBool "still scrubbed" (not (T.isInfixOf "ScripthsInternal" s))
        , testCase "scrubbing is idempotent (scrub . scrub == scrub)" $ do
            let sample =
                    T.unwords
                        [ "ScripthsInternalStr.IsString"
                        , "bare ScripthsInternalDir"
                        , "scripthsAutoPrint"
                        , "ScripthsAutoPrint"
                        ]
                once = scrubInternalNames sample
            scrubInternalNames once @?= once
        ]