packages feed

pandoc-3.14: test/Tests/Data.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{- |
   Module      : Tests.Data
   Copyright   : © 2026 John MacFarlane
   License     : GNU GPL, version 2 or above

   Maintainer  : John MacFarlane <jgm@berkeley.edu>
   Stability   : alpha
   Portability : portable

Tests for data file lookup, including lookup in @PANDOC_DATA_DIRS@.
-}
module Tests.Data (tests) where

import Codec.Archive.Zip (Archive, filesInArchive,
#ifdef EMBED_DATA_FILES
                          findEntryByPath, fromEntry,
#endif
                          toArchive)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.List (intercalate)
import qualified Data.Text as T
import System.FilePath ((</>), searchPathSeparator)
import System.Info (os)
import Test.Tasty
import Test.Tasty.HUnit
import Text.Pandoc.Class (PandocPure, FileInfo(..), FileTree,
                          findFileWithDataFallback, getDataDirs,
                          getPandocDataDirs, insertInFileTree,
                          modifyPureState, PureState(..), runIOorExplode,
                          runPure, setUserDataDir)
import Text.Pandoc.Data (readDataFile, readDefaultDataFile)
import Data.Time.Clock.POSIX (posixSecondsToUTCTime)

-- | An absolute path that is valid on the current platform.
absPath :: FilePath -> FilePath
absPath d = if os == "mingw32" then "C:\\" </> d else "/" </> d

userDir, dirA, dirB :: FilePath
userDir = absPath "user"
dirA = absPath "dirA"
dirB = absPath "dirB"

pandocDataDirs :: [FilePath] -> T.Text
pandocDataDirs = T.pack . intercalate [searchPathSeparator]

-- | Run a 'PandocPure' action with the given user data directory,
-- value of @PANDOC_DATA_DIRS@, and files.
runWith :: Maybe FilePath -> Maybe T.Text -> FileTree -> [(FilePath, B.ByteString)]
        -> PandocPure a -> Either String a
runWith mbUserDir mbDataDirs defaultFiles files action =
  either (Left . show) Right $ runPure $ do
    modifyPureState $ \st ->
      st{ stEnv = maybe [] (\x -> [("PANDOC_DATA_DIRS", x)]) mbDataDirs
        , stFiles = foldr (\(fp, bs) -> insertInFileTree fp (fileInfo bs))
                          defaultFiles files }
    setUserDataDir mbUserDir
    action

fileInfo :: B.ByteString -> FileInfo
fileInfo bs = FileInfo{ infoFileMTime = posixSecondsToUTCTime 0
                      , infoFileContents = bs }

-- | Default data files that are needed by the tests, placed where
-- 'PandocPure' looks for them if data files are not embedded.
getDefaultFiles :: IO (FileTree, B.ByteString, Archive)
getDefaultFiles = runIOorExplode $ do
  abbrevs <- readDefaultDataFile "abbreviations"
  refdocx <- toArchive . BL.fromStrict <$>
               readDefaultDataFile "reference.docx"
  parts <- mapM (\p -> (p,) <$> readDefaultDataFile ("docx" </> p))
                (filesInArchive refdocx)
  let tree = foldr (\(fp, bs) -> insertInFileTree ("data" </> fp) (fileInfo bs))
                   mempty
                   (("abbreviations", abbrevs) :
                     [("docx" </> p, bs) | (p, bs) <- parts])
  return (tree, abbrevs, refdocx)

#ifdef EMBED_DATA_FILES
-- | Contents of a file in a docx archive.
docxPart :: FilePath -> B.ByteString -> Maybe BL.ByteString
docxPart p = fmap fromEntry . findEntryByPath p . toArchive . BL.fromStrict
#endif

tests :: [TestTree]
tests =
  [ testGroup "getPandocDataDirs"
    [ testCase "PANDOC_DATA_DIRS unset" $
        runWith Nothing Nothing mempty [] getPandocDataDirs @?= Right []
    , testCase "ignoring relative and empty entries" $
        runWith Nothing (Just (pandocDataDirs [dirA, "relative", "", dirB]))
          mempty [] getPandocDataDirs
          @?= Right [dirA, dirB]
    ]
  , testGroup "getDataDirs"
    [ testCase "user data dir comes first" $
        runWith (Just userDir) (Just (pandocDataDirs [dirA, dirB])) mempty []
          (getDataDirs "templates/default.html5")
          @?= Right [userDir, dirA, dirB]
    , testCase "no user data dir" $
        runWith Nothing (Just (pandocDataDirs [dirA])) mempty []
          (getDataDirs "abbreviations")
          @?= Right [dirA]
    , testCase "absolute path" $
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty []
          (getDataDirs (absPath "abbreviations"))
          @?= Right []
    , testCase "path relative to parent directory" $
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty []
          (getDataDirs (".." </> "abbreviations"))
          @?= Right []
    ]
  , withResource getDefaultFiles (const (return ())) $ \getDefaults ->
    testGroup "readDataFile"
    [ testCase "user data dir takes precedence" $ do
        (defs, _, _) <- getDefaults
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs
          [ (userDir </> "abbreviations", "user")
          , (dirA </> "abbreviations", "dirA") ]
          (readDataFile "abbreviations")
          @?= Right "user"
    , testCase "earlier PANDOC_DATA_DIRS entries take precedence" $ do
        (defs, _, _) <- getDefaults
        runWith (Just userDir) (Just (pandocDataDirs [dirA, dirB])) defs
          [ (dirA </> "abbreviations", "dirA")
          , (dirB </> "abbreviations", "dirB") ]
          (readDataFile "abbreviations")
          @?= Right "dirA"
    , testCase "later PANDOC_DATA_DIRS entries are used" $ do
        (defs, _, _) <- getDefaults
        runWith Nothing (Just (pandocDataDirs [dirA, dirB])) defs
          [ (dirB </> "abbreviations", "dirB") ]
          (readDataFile "abbreviations")
          @?= Right "dirB"
#ifdef EMBED_DATA_FILES
    , testCase "falls back to default data files" $ do
        (defs, abbrevs, _) <- getDefaults
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs []
          (readDataFile "abbreviations")
          @?= Right abbrevs
    , testCase "readDefaultDataFile ignores data dirs" $ do
        (defs, abbrevs, _) <- getDefaults
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs
          [ (userDir </> "abbreviations", "user")
          , (dirA </> "abbreviations", "dirA") ]
          (readDefaultDataFile "abbreviations")
          @?= Right abbrevs
    , testCase "reference.docx from PANDOC_DATA_DIRS" $ do
        (defs, _, _) <- getDefaults
        runWith Nothing (Just (pandocDataDirs [dirA])) defs
          [ (dirA </> "reference.docx", "dirA") ]
          (readDataFile "reference.docx")
          @?= Right "dirA"
    , testCase "reference.docx component from PANDOC_DATA_DIRS" $ do
        (defs, _, _) <- getDefaults
        let styles = "word" </> "styles.xml"
        runWith Nothing (Just (pandocDataDirs [dirA])) defs
          [ (dirA </> "docx" </> styles, "dirA") ]
          (docxPart styles <$> readDataFile "reference.docx")
          @?= Right (Just "dirA")
    , testCase "readDefaultDataFile ignores reference.docx components" $ do
        (defs, _, refdocx) <- getDefaults
        let styles = "word" </> "styles.xml"
        let expected = fromEntry <$> findEntryByPath styles refdocx
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) defs
          [ (userDir </> "docx" </> styles, "user")
          , (dirA </> "docx" </> styles, "dirA") ]
          (docxPart styles <$> readDefaultDataFile "reference.docx")
          @?= Right expected
#endif
    ]
  , testGroup "findFileWithDataFallback"
    [ testCase "file in working directory takes precedence" $
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty
          [ ("f.lua", "")
          , (dirA </> "filters" </> "f.lua", "") ]
          (findFileWithDataFallback "filters" "f.lua")
          @?= Right (Just "f.lua")
    , testCase "user data dir takes precedence" $
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty
          [ (userDir </> "filters" </> "f.lua", "")
          , (dirA </> "filters" </> "f.lua", "") ]
          (findFileWithDataFallback "filters" "f.lua")
          @?= Right (Just (userDir </> "filters" </> "f.lua"))
    , testCase "file in PANDOC_DATA_DIRS" $
        runWith (Just userDir) (Just (pandocDataDirs [dirA, dirB])) mempty
          [ (dirB </> "filters" </> "f.lua", "") ]
          (findFileWithDataFallback "filters" "f.lua")
          @?= Right (Just (dirB </> "filters" </> "f.lua"))
    , testCase "file not found" $
        runWith (Just userDir) (Just (pandocDataDirs [dirA])) mempty []
          (findFileWithDataFallback "filters" "f.lua")
          @?= Right Nothing
    ]
  ]