packages feed

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

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

import System.FilePath (isAbsolute, (</>))
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase, (@?=))

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.PackageId (splitPackageId)
import Tricorder.SourceLookup.Tarball
    ( cabalPackagesDirs
    , extractModule
    , matchesModule
    , tarballPath
    )


test_Tarball :: TestTree
test_Tarball =
    testGroup
        "Tarball"
        [ testGroup
            "splitPackageId"
            [ testCase "splits a simple package id" do
                splitPackageId "aeson-2.2.5.0" @?= ("aeson", "2.2.5.0")
            , testCase "keeps hyphens inside the package name" do
                splitPackageId "list-t-1.0.5.7" @?= ("list-t", "1.0.5.7")
            ]
        , testGroup
            "tarballPath"
            [ testCase "derives the cache path for a package id" do
                tarballPath "/c" "hackage.haskell.org" "aeson-2.2.5.0"
                    @?= "/c" </> "hackage.haskell.org/aeson/2.2.5.0/aeson-2.2.5.0.tar.gz"
            ]
        , testGroup
            "cabalPackagesDirs"
            [ testCase "honors CABAL_DIR" do
                cabalPackagesDirs [("CABAL_DIR", "/cd")] @?= ["/cd/packages"]
            , testCase "includes the legacy ~/.cabal location alongside the XDG one" do
                cabalPackagesDirs [("HOME", "/h")]
                    @?= ["/h/.cache/cabal/packages", "/h/.cabal/packages"]
            , testCase "yields no candidates (never a relative path) when HOME is unset" do
                cabalPackagesDirs [] @?= []
            , testCase "only ever produces absolute candidates" do
                all isAbsolute (cabalPackagesDirs [("HOME", "/h"), ("XDG_CACHE_HOME", "/x")])
                    @?= True
            ]
        , testGroup
            "matchesModule"
            [ testCase "matches a src/ layout entry" do
                matchesModule "Data.Aeson" "aeson-2.2.5.0/src/Data/Aeson.hs" @?= True
            , testCase "matches a lib/ layout entry" do
                matchesModule "Data.Aeson" "aeson-2.2.5.0/lib/Data/Aeson.hs" @?= True
            , testCase "matches a flat layout entry" do
                matchesModule "Data.Aeson" "aeson-2.2.5.0/Data/Aeson.hs" @?= True
            , testCase "does not match a different module under the same prefix" do
                matchesModule "Data.Aeson" "aeson-2.2.5.0/src/Data/Aeson/Types.hs" @?= False
            , testCase "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" @?= False
            , testCase "matches a preprocessed .hsc entry" do
                matchesModule "System.Posix.Files" "unix-2.8.5.0/System/Posix/Files.hsc" @?= True
            , testCase "matches a literate .lhs entry" do
                matchesModule "Data.Ratio" "base-4.19.0.0/src/Data/Ratio.lhs" @?= True
            ]
        , testGroup
            "extractModule"
            [ testCase "extracts a member by module name" do
                extractModule "Data.Aeson" fixtureTarball @?= Just "module Data.Aeson where\n"
            , testCase "returns Nothing when the module is absent" do
                extractModule "Data.Missing" fixtureTarball @?= Nothing
            , testCase "prefers a library source path over a same-named test path" do
                extractModule "Data.Aeson" dupModuleTarball
                    @?= 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))