packages feed

vcs-ignore-0.1.0.0: test/Data/VCS/Ignore/Git/Internal/PatternSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}

module Data.VCS.Ignore.Git.Internal.PatternSpec where

import Control.Exception (
    IOException,
    bracket_,
    try,
 )
import qualified Data.Text as T
import Data.VCS.Ignore.Git.Internal.Pattern
import Data.VCS.Ignore.Types (PathKind (..))
import System.Directory (
    createDirectory,
    createFileLink,
    getPermissions,
    readable,
    setPermissions,
 )
import System.FilePath ((</>))
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec

spec :: Spec
spec = do
    describe "evaluatePatternGroups" $ do
        it "uses the last matching pattern across ordered groups" $ do
            let groups =
                    [ group "/" "*.log\n!keep.log\n"
                    , group "/nested/" "keep.log\n!keep.log\n"
                    ]
            evaluatePatternGroups RegularFile groups "drop.log" `shouldBe` True
            evaluatePatternGroups RegularFile groups "keep.log" `shouldBe` False
            evaluatePatternGroups RegularFile groups "nested/keep.log" `shouldBe` False

        it "handles Git line parsing edge cases" $ do
            let groups =
                    [ group
                        "/"
                        "# comment\r\n\\#literal\r\n\\!important\r\ntrailing   \r\nescaped\\ \r\n"
                    ]
            evaluatePatternGroups RegularFile groups "#literal" `shouldBe` True
            evaluatePatternGroups RegularFile groups "!important" `shouldBe` True
            evaluatePatternGroups RegularFile groups "trailing" `shouldBe` True
            evaluatePatternGroups RegularFile groups "trailing   " `shouldBe` False
            evaluatePatternGroups RegularFile groups "escaped " `shouldBe` True

        it "applies directory-only patterns only to real directories" $ do
            let groups = [group "/" "build/\n"]
            evaluatePatternGroups Directory groups "build" `shouldBe` True
            evaluatePatternGroups RegularFile groups "build" `shouldBe` False
            evaluatePatternGroups SymbolicLink groups "build" `shouldBe` False
            evaluatePatternGroups Other groups "build" `shouldBe` False

        it "respects anchoring, nested scopes, and recursive wildcards" $ do
            let groups =
                    [ group "/" "/root.txt\na/**/result.txt\ntree/**\n**/marker\n"
                    , group "/nested/" "/local.txt\n"
                    ]
            evaluatePatternGroups RegularFile groups "root.txt" `shouldBe` True
            evaluatePatternGroups RegularFile groups "deep/root.txt" `shouldBe` False
            evaluatePatternGroups RegularFile groups "a/result.txt" `shouldBe` True
            evaluatePatternGroups RegularFile groups "a/b/c/result.txt" `shouldBe` True
            evaluatePatternGroups RegularFile groups "tree/one/two/file" `shouldBe` True
            evaluatePatternGroups RegularFile groups "marker" `shouldBe` True
            evaluatePatternGroups RegularFile groups "deep/marker" `shouldBe` True
            evaluatePatternGroups RegularFile groups "nested/local.txt" `shouldBe` True
            evaluatePatternGroups RegularFile groups "nested/deep/local.txt" `shouldBe` False

        it "does not extend an ordinary pattern to every descendant" $ do
            let groups = [group "/" "foo/*\n!foo/bar/\n"]
            evaluatePatternGroups Directory groups "foo/bar" `shouldBe` False
            evaluatePatternGroups RegularFile groups "foo/bar/file.txt" `shouldBe` False

    describe "loadPatternsFile" $ do
        it "loads a regular ignore file"
            . withSystemTempDirectory "vcs-ignore-patterns"
            $ \directory -> do
                let path = directory </> ".gitignore"
                writeFile path "*.tmp\n"
                patterns <- loadPatternsFile False path
                evaluatePatternGroups RegularFile [PatternGroup "/" patterns] "file.tmp"
                    `shouldBe` True

        it "treats missing, non-regular, and non-followed symbolic-link sources as empty"
            . withSystemTempDirectory "vcs-ignore-patterns"
            $ \directory -> do
                loadPatternsFile False (directory </> "missing") `shouldReturn` []
                let sourceDirectory = directory </> "directory-source"
                createDirectory sourceDirectory
                loadPatternsFile False sourceDirectory `shouldReturn` []
                let target = directory </> "rules"
                    link = directory </> ".gitignore"
                writeFile target "*.tmp\n"
                createFileLink target link
                loadPatternsFile False link `shouldReturn` []

        it "propagates errors from an existing unreadable source when supported"
            . withSystemTempDirectory "vcs-ignore-patterns"
            $ \directory -> do
                let source = directory </> ".gitignore"
                writeFile source "*.tmp\n"
                permissions <- getPermissions source
                result <-
                    bracket_
                        (setPermissions source permissions{readable = False})
                        (setPermissions source permissions)
                        (try @IOException $ loadPatternsFile True source)
                case result of
                    Left _ -> pure ()
                    Right _ -> pendingWith "unreadable file permissions are not enforced"

group :: FilePath -> String -> PatternGroup
group prefix contents = PatternGroup prefix (parsePatterns $ T.pack contents)