hledger-flow-0.18.0: test/ImportHelpers/Integration.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module ImportHelpers.Integration (tests) where
import qualified Data.List as List
import Data.Either (isLeft)
import qualified Data.Text.IO as T
import Hledger.Flow.Import.ImportHelpers (withStagedOutput)
import Hledger.Flow.Import.Inventory
import TestHelpers (withTestDir, touchAll)
import Hledger.Flow.PathHelpers (AbsFile)
import Path
import Path.IO
import qualified System.FilePath
import System.IO.Error (tryIOError)
import Test.HUnit
touchFile :: AbsFile -> IO ()
touchFile f = do
createDirIfMissing True (parent f)
T.writeFile (toFilePath f) "x"
testInventory :: Test
testInventory = TestLabel "active inventory discovery and scope" $ TestCase $ withTestDir $ \base -> do
let root = base </> [reldir|import|]
account = root </> [reldir|owner/bank/checking|]
inputs = [ "owner/bank/checking/1-in/2018/old.csv",
"owner/bank/checking/1-in/2019/march.bank.txt",
"owner/bank/checking/1-in/2020/new.csv",
"2-preprocessed/3-journal/1-in/1-in/2019/reserved.csv" ]
others = [ "owner/bank/checking/2-preprocessed/2019/march.bank.csv",
"owner/bank/checking/3-journal/2019/march.bank.journal",
"owner/_manual_/2019/post-import.journal",
"owner/bank/checking/1-in/20a0/bad.csv",
"owner/bank/checking/1-in/20190/bad.csv",
"owner/bank/checking/1-in/2019/nested/bad.csv" ]
hidden = [ "owner/bank/checking/1-in/2019/.hidden.csv",
".hidden/bank/account/1-in/2019/bad.csv",
"owner/bank/checking/.trash/1-in/2019/bad.csv",
"owner/bank/checking/1-in/__pycache__/2019/bad.csv" ]
absolute = (toFilePath root System.FilePath.</>)
touchAll (map absolute (inputs ++ others ++ hidden))
inventory <- findImportFiles root
expected <- mapM (parseAbsFile . absolute) (inputs ++ others)
assertEqual "exclude hidden files, hidden directories and Python cache" (List.sort expected) inventory
sources <- mapM (parseAbsFile . absolute) inputs
assertEqual "only direct children of a valid input year" (List.sort sources)
(stageFiles root [reldir|1-in|] inventory)
selected <- mapM (parseAbsFile . absolute) (take 2 (drop 1 inputs))
assertEqual "account scope and inclusive start year" selected
(filter (inImportScope account 2019) (stageFiles root [reldir|1-in|] inventory))
let source = account </> [relfile|1-in/2019/march.bank.txt|]
generatedOutput [reldir|2-preprocessed|] ".csv" source >>= assertEqual "keep earlier filename extensions"
(account </> [relfile|2-preprocessed/2019/march.bank.csv|])
generatedOutput [reldir|3-journal|] ".journal" source >>= assertEqual "journal destination"
(account </> [relfile|3-journal/2019/march.bank.journal|])
assertEqual "discover generated journals separately"
[account </> [relfile|3-journal/2019/march.bank.journal|]]
(stageFiles root [reldir|3-journal|] inventory)
testStagedOutput :: Test
testStagedOutput = TestCase $ withSystemTempDir "hlflow-output" $ \dir -> do
let target = dir </> [relfile|march.bank.csv|]
failAfterWriting staged = do
assertEqual "hook output keeps its filename" (filename target) (filename staged)
assertBool "hook output has a temporary parent" (parent staged /= dir)
T.writeFile (toFilePath staged) "partial"
ioError (userError "failed hook")
checkFailure = do
result <- tryIOError (withStagedOutput target failAfterWriting)
assertBool "propagate hook failure" (isLeft result)
(dirs, _) <- listDir dir
assertEqual "clean up staging directories" [] dirs
checkFailure
doesFileExist target >>= assertEqual "failed first run publishes no file" False
T.writeFile (toFilePath target) "previous"
checkFailure
T.readFile (toFilePath target) >>= assertEqual "preserve previous output" "previous"
withStagedOutput target $ \staged -> T.writeFile (toFilePath staged) "complete"
T.readFile (toFilePath target) >>= assertEqual "publish successful retry" "complete"
tests :: Test
tests = TestList [testInventory, testStagedOutput, testTrashLock]
testTrashLock :: Test
testTrashLock = TestCase $ withSystemTempDir "hlflow-trash" $ \root -> do
let source = root </> [relfile|import/owner/bank/account/3-journal/2020/march.journal|]
lock = root </> [reldir|import/owner/bank/account/.trash/.move-lock|]
target = root </> [relfile|import/owner/bank/account/.trash/2020/3-journal/march.journal|]
touchFile source
createDirIfMissing True lock
result <- tryIOError (moveToTrash source)
assertBool "A concurrent trash move must not race for a destination" (isLeft result)
T.readFile (toFilePath source) >>= assertEqual "Busy trash leaves source intact" "x"
doesFileExist target >>= assertBool "Busy trash writes no destination" . not
removeDir lock
actual <- moveToTrash source
assertEqual "Retry after the other move completes succeeds" target actual