packages feed

tricorder-0.2.1.0: test/Unit/Tricorder/Session/CabalFileSpec.hs

module Unit.Tricorder.Session.CabalFileSpec (spec_CabalFile) where

import Atelier.Effects.Env (runEnvConst)
import Atelier.Effects.FileSystem (runFileSystemState)
import Effectful (runPureEff)
import Effectful.Reader.Static (runReader)
import Effectful.State.Static.Shared (evalState)
import Test.Hspec (Spec, describe, it, shouldBe)

import Data.Map.Strict qualified as Map

import Tricorder.Runtime (ProjectRoot (..))
import Tricorder.Session.CabalFile (discoverCabalFiles)
import Unit.Tricorder.Session.Helpers (cabalFixture, libTestCabal, multiPackageFs)


spec_CabalFile :: Spec
spec_CabalFile = do
    describe "discoverCabalFiles" testDiscoverCabalFiles


-- | Pins the discovery contract: a @cabal.project@ (or @.local@/@.freeze@
-- variant) selects per-package @.cabal@ files from its @packages:@ stanza;
-- otherwise the @.cabal@ files in the project root are used. Falls back
-- further to @$HOME/.cabal/config@'s @packages:@ stanza if none of the
-- project-root files exist.
testDiscoverCabalFiles :: Spec
testDiscoverCabalFiles = do
    describe "when there is no cabal.project" do
        it "finds the .cabal files in the project root" do
            let actual =
                    runDiscovery (Map.singleton "/myapp.cabal" cabalFixture) []
                        $ discoverCabalFiles
            actual `shouldBe` ["/myapp.cabal"]

        it "returns no files when the root has no cabal file" do
            let actual = runDiscovery mempty [] discoverCabalFiles
            actual `shouldBe` []

    describe "when there is a multi-package cabal.project" do
        it "resolves each listed package to its .cabal (regression: was root-only)" do
            let actual = runDiscovery multiPackageFs [] discoverCabalFiles
            actual `shouldBe` ["/pkg-a/pkg-a.cabal", "/pkg-b/pkg-b.cabal"]

    describe "priority among cabal.project.local, cabal.project.freeze, and cabal.project" do
        it "prefers cabal.project.local over cabal.project" do
            let fs =
                    Map.fromList
                        [ ("/cabal.project.local", "packages: pkg-a\n")
                        , ("/cabal.project", "packages: pkg-b\n")
                        ]
                        `Map.union` multiPackageCabalFiles
                actual = runDiscovery fs [] discoverCabalFiles
            actual `shouldBe` ["/pkg-a/pkg-a.cabal"]

        it "prefers cabal.project.freeze over cabal.project" do
            let fs =
                    Map.fromList
                        [ ("/cabal.project.freeze", "packages: pkg-a\n")
                        , ("/cabal.project", "packages: pkg-b\n")
                        ]
                        `Map.union` multiPackageCabalFiles
                actual = runDiscovery fs [] discoverCabalFiles
            actual `shouldBe` ["/pkg-a/pkg-a.cabal"]

        describe "when a higher-priority file lists no packages" do
            it "falls through to the next file in priority order" do
                let fs =
                        Map.fromList
                            [ ("/cabal.project.local", "tests: True\n")
                            , ("/cabal.project", "packages: pkg-b\n")
                            ]
                            `Map.union` multiPackageCabalFiles
                    actual = runDiscovery fs [] discoverCabalFiles
                actual `shouldBe` ["/pkg-b/pkg-b.cabal"]

    describe "packages: entry resolution" do
        it "uses a direct .cabal path entry verbatim, without scanning a directory" do
            let fs = Map.singleton "/cabal.project" "packages: sub/foo.cabal\n"
                actual = runDiscovery fs [] discoverCabalFiles
            actual `shouldBe` ["/sub/foo.cabal"]

        it "skips glob entries under packages: (not expanded)" do
            let fs = Map.singleton "/cabal.project" "packages: */*.cabal\n"
                actual = runDiscovery fs [] discoverCabalFiles
            actual `shouldBe` []

    describe "$HOME/.cabal/config fallback" do
        describe "when no cabal.project files exist" do
            it "uses $HOME/.cabal/config as a last-resort packages source" do
                let fs =
                        Map.singleton "/home/user/.cabal/config" "packages: pkg-a\n"
                            `Map.union` multiPackageCabalFiles
                    actual = runDiscovery fs [("HOME", "/home/user")] discoverCabalFiles
                actual `shouldBe` ["/pkg-a/pkg-a.cabal"]

        describe "when $HOME/.cabal/config exists but lists no packages" do
            it "falls back to scanning the project root" do
                let fs =
                        Map.fromList
                            [ ("/home/user/.cabal/config", "")
                            , ("/myapp.cabal", cabalFixture)
                            ]
                    actual = runDiscovery fs [("HOME", "/home/user")] discoverCabalFiles
                actual `shouldBe` ["/myapp.cabal"]
  where
    pr = ProjectRoot "/"
    multiPackageCabalFiles =
        Map.fromList
            [ ("/pkg-a/pkg-a.cabal", libTestCabal "pkg-a")
            , ("/pkg-b/pkg-b.cabal", libTestCabal "pkg-b")
            ]
    runDiscovery fs env =
        runPureEff
            . runEnvConst env
            . evalState fs
            . runFileSystemState
            . runReader pr