packages feed

tricorder-0.6.0.0: test/Unit/Tricorder/Daemon/Ghci/SessionSpec.hs

module Unit.Tricorder.Daemon.Ghci.SessionSpec (test_Session) where

import Atelier.Effects.PubSub.Pub (Pub)
import Control.Exception (ErrorCall (..))
import Effectful (IOE, runEff)
import Effectful.Exception (try)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))

import Atelier.Effects.PubSub.Pub qualified as Pub
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set

import Tricorder.Build (BuildProgress, Diagnostic (..), Severity (..))
import Tricorder.Daemon.Ghci.Controls (Controls (..))
import Tricorder.Daemon.Ghci.LoadResult (LoadResult (..))
import Tricorder.Daemon.Ghci.Session (Session)
import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session.Command.RenderedCommand (RenderedCommand (..))
import Tricorder.Session.Stage (Stage (..))

import Tricorder.Daemon.Ghci.Session qualified as Session


test_Session :: TestTree
test_Session =
    testGroup
        "Session"
        [ testGroup "runScripted" testScripted
        ]


--------------------------------------------------------------------------------
-- Scripted interpreter tests
--------------------------------------------------------------------------------

testScripted :: [TestTree]
testScripted =
    [ testGroup
        "withGhci"
        [ testGroup
            "initial load"
            [ testCase "returns scripted messages" do
                LoadResult {diagnostics = msgs} <-
                    runScripted [simpleResult [errMsg]]
                        $ Session.with Pub.publish cmd (ProjectRoot "/") \initial _ -> pure initial
                msgs @?= [errMsg]
            , testCase "returns empty list when scripted result has no messages" do
                LoadResult {diagnostics = msgs} <-
                    runScripted [simpleResult []]
                        $ Session.with Pub.publish cmd (ProjectRoot "/") \initial _ -> pure initial
                msgs @?= []
            , testCase "throws when scripted result is Left" do
                result <-
                    runScripted [Left (toException boom)]
                        $ try @ErrorCall
                        $ Session.with Pub.publish cmd (ProjectRoot "/") \initial _ -> pure initial
                result @?= Left boom
            ]
        , testGroup
            "reloading"
            [ testCase "returns scripted messages" do
                LoadResult {diagnostics = msgs} <-
                    runScripted [simpleResult [warnMsg], simpleResult [errMsg]]
                        $ Session.with Pub.publish cmd (ProjectRoot "/") \_ controls -> controls.reload
                msgs @?= [errMsg]
            , testCase "throws when scripted result is Left" do
                result <-
                    runScripted [Left (toException boom)]
                        $ try @ErrorCall
                        $ Session.with Pub.publish cmd (ProjectRoot "/") \_ controls -> controls.reload
                result @?= Left boom
            ]
        ]
    , testGroup
        "sequencing"
        [ testCase "consumes results in order across mixed operations" do
            (a, b) <- runScripted [simpleResult [errMsg], simpleResult [warnMsg]] do
                Session.with Pub.publish cmd (ProjectRoot "/") \LoadResult {diagnostics = a} controls -> do
                    LoadResult {diagnostics = b} <- controls.reload
                    pure (a, b)
            a @?= [errMsg]
            b @?= [warnMsg]
        , testCase "recover scenario: error then success" do
            result <- runScripted [Left (toException boom), simpleResult []] do
                r1 <- try @ErrorCall $ Session.with Pub.publish cmd (ProjectRoot "/") \i _ -> pure i
                LoadResult {diagnostics = r2} <- Session.with Pub.publish cmd (ProjectRoot "/") \i _ -> pure i
                pure (r1, r2)
            assertBool "expected Left" $ isLeft (fst result)
            snd result @?= []
        ]
    ]


--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------

cmd :: RenderedCommand 'Build
cmd = RenderedCommand "cabal repl lib:foo"


boom :: ErrorCall
boom = ErrorCall "simulated GHCi crash"


errMsg :: Diagnostic
errMsg =
    Diagnostic
        { severity = SError
        , file = "src/Foo.hs"
        , line = 1
        , col = 1
        , endLine = 1
        , endCol = 5
        , title = "Variable not in scope: foo"
        , text = "Variable not in scope: foo"
        }


warnMsg :: Diagnostic
warnMsg =
    Diagnostic
        { severity = SWarning
        , file = "src/Bar.hs"
        , line = 10
        , col = 3
        , endLine = 10
        , endCol = 8
        , title = "Unused import"
        , text = "Unused import"
        }


-- | Convenience constructor: a scripted result with no compiled-file info.
simpleResult :: [Diagnostic] -> Either SomeException LoadResult
simpleResult msgs =
    Right
        LoadResult
            { moduleCount = 0
            , compiledFiles = Set.empty
            , loadedModules = Map.empty
            , targetNames = []
            , diagnostics = msgs
            }


runScripted
    :: [Either SomeException LoadResult]
    -> Eff '[Session, Pub BuildProgress, IOE] a
    -> IO a
runScripted results = runEff . Pub.runNoOp . Session.runScripted results