packages feed

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

module Unit.Tricorder.SessionSpec (test_Session) where

import Atelier.Effects.FileSystem (runFileSystemState)
import Atelier.Effects.Log (Message (..), Severity (..), runLogNoOp, runLogWriter)
import Data.Aeson (object, (.=))
import Distribution.PackageDescription.Parsec (parseGenericPackageDescriptionMaybe)
import Effectful (runPureEff)
import Effectful.Reader.Static (runReader)
import Effectful.State.Static.Shared (evalState)
import Effectful.Writer.Static.Shared (execWriter)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertFailure, testCase, (@?), (@?=))

import Atelier.Effects.Input qualified as Input
import Data.Aeson qualified as Aeson
import Data.Text qualified as T

import Tricorder.Config.LoadedConfig (LoadedConfig (..))
import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session.CabalFile (CabalFile (..))
import Tricorder.Session.IdleTimeout (IdleTimeout (..))
import Unit.Tricorder.Session.Helpers (libWithPreludeCabal, preludeOnlyLibCabal)

import Tricorder.Config qualified
import Tricorder.Session qualified as Session


test_Session :: TestTree
test_Session =
    testGroup
        "Session"
        [ testLoadSession
        , testIdleTimeout
        , testMissingTargetPlaceholder
        ]


testLoadSession :: TestTree
testLoadSession =
    testGroup
        "load"
        [ testGroup
            "when every resolved target exposes a custom Prelude module"
            [ testCase "logs one message" do
                let msgs = captureSessionLogs [preludeOnlyCF]
                length msgs @?= 1
            , testCase "emits a WARN" do
                let msgs = (\m -> (m.severity, m.text)) <$> captureSessionLogs [preludeOnlyCF]
                case msgs of
                    [(sev, msg)] -> do
                        sev @?= WARN
                        "Every resolved target exposes a custom Prelude module" `T.isPrefixOf` msg
                            @? "does not start with expected string"
                    xs ->
                        assertFailure $ "Too many logged messages: " <> show (length xs)
            ]
        , testGroup
            "when not every resolved target exposes a custom Prelude module"
            [ testCase "does not emit a WARN" do
                -- libWithPreludeCabal has both a lib (custom Prelude) and an exe (no Prelude)
                let msgs = captureSessionLogs [mixedCF]
                any (\m -> m.severity == WARN) msgs @?= False
            ]
        , testCase "does not emit a WARN when there are no resolved targets" do
            any (\m -> m.severity == WARN) (captureSessionLogs []) @?= False
        ]
  where
    preludeOnlyCF =
        CabalFile "/p.cabal"
            $ fromMaybe (error "preludeOnlyLibCabal failed to parse")
            $ parseGenericPackageDescriptionMaybe (preludeOnlyLibCabal "p")
    mixedCF =
        CabalFile "/mixed.cabal"
            $ fromMaybe (error "libWithPreludeCabal failed to parse")
            $ parseGenericPackageDescriptionMaybe (libWithPreludeCabal "mixed")
    captureSessionLogs = captureSessionLogs' $ Aeson.Object mempty
    captureSessionLogs' cfg cabalFiles =
        filter (\m -> m.severity /= DEBUG)
            . runPureEff
            . execWriter @[Message]
            . runLogWriter
            . evalState @(Map FilePath ByteString) mempty
            . runFileSystemState
            . Input.withConst cabalFiles
            . runReader (ProjectRoot "/")
            . Input.withConst (LoadedConfig $ toStrict $ Aeson.encode cfg)
            . Tricorder.Config.asInput
            $ Session.load


testMissingTargetPlaceholder :: TestTree
testMissingTargetPlaceholder =
    testGroup
        "loadSession missing {target} placeholder"
        [ testGroup
            "test.command_template"
            [ testCase "emits a WARN when it has no {target} placeholder" do
                let cfg = sessionCfg ["test" .= object ["command_template" .= ("cabal repl test:foo" :: Text)]]
                any (\m -> m.severity == WARN) (captureLogsFor cfg) @?= True
            , testCase "does not emit a WARN when it has the {target} placeholder" do
                let cfg = sessionCfg ["test" .= object ["command_template" .= ("cabal repl {target}" :: Text)]]
                any (\m -> m.severity == WARN) (captureLogsFor cfg) @?= False
            ]
        , testGroup
            "eval.command_template"
            [ testCase "emits a WARN when it has no {target} placeholder" do
                let cfg = sessionCfg ["eval" .= object ["command_template" .= ("cabal repl Tricorder.Foo" :: Text)]]
                any (\m -> m.severity == WARN) (captureLogsFor cfg) @?= True
            , testCase "does not emit a WARN when it has the {target} placeholder" do
                let cfg = sessionCfg ["eval" .= object ["command_template" .= ("cabal repl {target}" :: Text)]]
                any (\m -> m.severity == WARN) (captureLogsFor cfg) @?= False
            ]
        , testGroup
            "build.command_template"
            [ testCase "does not emit a WARN when it has no {targets} placeholder" do
                let cfg = sessionCfg ["build" .= object ["command_template" .= ("cabal repl lib:foo" :: Text)]]
                any (\m -> m.severity == WARN) (captureLogsFor cfg) @?= False
            ]
        ]
  where
    sessionCfg session = object ["session" .= object session]
    captureLogsFor cfg =
        runPureEff
            . execWriter @[Message]
            . runLogWriter
            . evalState @(Map FilePath ByteString) mempty
            . runFileSystemState
            . Input.withConst ([] :: [CabalFile])
            . runReader (ProjectRoot "/")
            . Input.withConst (LoadedConfig $ toStrict $ Aeson.encode cfg)
            . Tricorder.Config.asInput
            $ Session.load


testIdleTimeout :: TestTree
testIdleTimeout =
    testGroup
        "loadSession idleTimeout"
        [ testCase "defaults to 300 seconds when unset" do
            (.idleTimeout) <$> loadSessionWith (Aeson.Object mempty) @?= Right (IdleTimeout 300)
        , testCase "reads idle_timeout_seconds from the session config" do
            let cfg = object ["session" .= object ["idle_timeout_seconds" .= (5 :: Int)]]
            (.idleTimeout) <$> (loadSessionWith cfg) @?= Right (IdleTimeout 5)
        ]
  where
    loadSessionWith cfg =
        runPureEff
            . runLogNoOp
            . evalState @(Map FilePath ByteString) mempty
            . runFileSystemState
            . Input.withConst ([] :: [CabalFile])
            . runReader (ProjectRoot "/")
            . Input.withConst (LoadedConfig $ toStrict $ Aeson.encode cfg)
            . Tricorder.Config.asInput
            $ Session.load