packages feed

tricorder-0.2.0.0: test/Unit/Tricorder/SourceLookup/TarballSpec.hs

module Unit.Tricorder.SourceLookup.TarballSpec (spec_Tarball) where

import System.FilePath (isAbsolute, (</>))
import Test.Hspec

import Codec.Archive.Tar qualified as Tar
import Codec.Archive.Tar.Entry qualified as Tar
import Codec.Compression.GZip qualified as GZip
import Data.ByteString.Lazy qualified as BSL

import Tricorder.SourceLookup.Tarball
    ( cabalPackagesDirs
    , extractModule
    , matchesModule
    , splitPackageId
    , tarballPath
    )


spec_Tarball :: Spec
spec_Tarball = do
    describe "splitPackageId" do
        it "splits a simple package id" do
            splitPackageId "aeson-2.2.5.0" `shouldBe` ("aeson", "2.2.5.0")

        it "keeps hyphens inside the package name" do
            splitPackageId "list-t-1.0.5.7" `shouldBe` ("list-t", "1.0.5.7")

    describe "tarballPath" do
        it "derives the cache path for a package id" do
            tarballPath "/c" "hackage.haskell.org" "aeson-2.2.5.0"
                `shouldBe` "/c" </> "hackage.haskell.org/aeson/2.2.5.0/aeson-2.2.5.0.tar.gz"

    describe "cabalPackagesDirs" do
        it "honors CABAL_DIR" do
            cabalPackagesDirs [("CABAL_DIR", "/cd")] `shouldBe` ["/cd/packages"]

        it "includes the legacy ~/.cabal location alongside the XDG one" do
            cabalPackagesDirs [("HOME", "/h")]
                `shouldBe` ["/h/.cache/cabal/packages", "/h/.cabal/packages"]

        it "yields no candidates (never a relative path) when HOME is unset" do
            cabalPackagesDirs [] `shouldBe` []

        it "only ever produces absolute candidates" do
            all isAbsolute (cabalPackagesDirs [("HOME", "/h"), ("XDG_CACHE_HOME", "/x")])
                `shouldBe` True

    describe "matchesModule" do
        it "matches a src/ layout entry" do
            matchesModule "Data.Aeson" "aeson-2.2.5.0/src/Data/Aeson.hs" `shouldBe` True

        it "matches a lib/ layout entry" do
            matchesModule "Data.Aeson" "aeson-2.2.5.0/lib/Data/Aeson.hs" `shouldBe` True

        it "matches a flat layout entry" do
            matchesModule "Data.Aeson" "aeson-2.2.5.0/Data/Aeson.hs" `shouldBe` True

        it "does not match a different module under the same prefix" do
            matchesModule "Data.Aeson" "aeson-2.2.5.0/src/Data/Aeson/Types.hs" `shouldBe` False

        it "does not match a deeper module whose final component coincides" do
            -- Module `Lens` must not resolve to the file for `Control.Lens`.
            matchesModule "Lens" "pkg-1.0/Control/Lens.hs" `shouldBe` False

        it "matches a preprocessed .hsc entry" do
            matchesModule "System.Posix.Files" "unix-2.8.5.0/System/Posix/Files.hsc" `shouldBe` True

        it "matches a literate .lhs entry" do
            matchesModule "Data.Ratio" "base-4.19.0.0/src/Data/Ratio.lhs" `shouldBe` True

    describe "extractModule" do
        it "extracts a member by module name" do
            extractModule "Data.Aeson" fixtureTarball `shouldBe` Just "module Data.Aeson where\n"

        it "returns Nothing when the module is absent" do
            extractModule "Data.Missing" fixtureTarball `shouldBe` Nothing

        it "prefers a library source path over a same-named test path" do
            extractModule "Data.Aeson" dupModuleTarball
                `shouldBe` Just "module Data.Aeson (lib) where\n"


-- | A gzipped tar with a single source member.
fixtureTarball :: LByteString
fixtureTarball = mkTarball [("aeson-2.2.5.0/src/Data/Aeson.hs", "module Data.Aeson where\n")]


-- | A gzipped tar in which the same module appears under both a test tree and
-- the library tree, with the test copy listed first.
dupModuleTarball :: LByteString
dupModuleTarball =
    mkTarball
        [ ("aeson-2.2.5.0/tests/Data/Aeson.hs", "module Data.Aeson (test) where\n")
        , ("aeson-2.2.5.0/src/Data/Aeson.hs", "module Data.Aeson (lib) where\n")
        ]


-- | Build a gzipped tar from @(path, contents)@ pairs, in the given order.
mkTarball :: [(FilePath, Text)] -> LByteString
mkTarball entries =
    GZip.compress (Tar.write [fileEntry path content | (path, content) <- entries])
  where
    fileEntry path content =
        Tar.fileEntry
            (either (error . toText) id (Tar.toTarPath False path))
            (BSL.fromStrict (encodeUtf8 content))