diff options
| author | adelon <22380201+adelon@users.noreply.github.com> | 2026-08-06 17:54:00 +0200 |
|---|---|---|
| committer | adelon <22380201+adelon@users.noreply.github.com> | 2026-08-06 17:54:00 +0200 |
| commit | 82328890108bae64b372b8d58620ebc62699de76 (patch) | |
| tree | 575404c6b425c19259c0ded296f1c8ffb7ff0e2b /source/Felix/Test/Unit/OutputPlan.hs | |
| parent | 1a25421c2a168d420581358c8733fcd8f36f379b (diff) | |
Diffstat (limited to 'source/Felix/Test/Unit/OutputPlan.hs')
| -rw-r--r-- | source/Felix/Test/Unit/OutputPlan.hs | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/source/Felix/Test/Unit/OutputPlan.hs b/source/Felix/Test/Unit/OutputPlan.hs new file mode 100644 index 0000000..803b7da --- /dev/null +++ b/source/Felix/Test/Unit/OutputPlan.hs @@ -0,0 +1,236 @@ +{-# LANGUAGE NoImplicitPrelude #-} + +module Felix.Test.Unit.OutputPlan (unitTests) where + +import Base +import Felix.OutputPlan qualified as Output +import Felix.Source +import Felix.Store qualified as Store + +import Control.Exception qualified as Exception +import System.Directory qualified as Directory +import System.Environment qualified as Environment +import System.FilePath.Posix qualified as Posix +import System.IO.Temp qualified as Temp +import Test.Tasty +import Test.Tasty.HUnit + + +unitTests :: TestTree +unitTests = + testGroup "Verification output preflight" + [ testCase "accepts absent and empty dump destinations" + acceptsAbsentAndEmptyDumpDestinations + , testCase "rejects a nonempty dump before store startup" + rejectsNonemptyDumpBeforeStoreStartup + , testCase "rejects persistent and fresh store collisions" + rejectsStoreCollisions + , testCase "reserves the store rollback journal" + reservesRollbackJournal + , testCase "rejects dump and HTML route collisions" + rejectsDumpHtmlCollisions + ] + +acceptsAbsentAndEmptyDumpDestinations :: Assertion +acceptsAbsentAndEmptyDumpDestinations = + Temp.withSystemTempDirectory "felix-output-dump" \root -> do + let storeParent = root Posix.</> "store" + storeFile = storeParent Posix.</> "store.sqlite" + dump = root Posix.</> "dump" + Directory.createDirectory storeParent + plan <- expectRightIO + (Store.planStore + (Store.ExplicitStore storeFile)) + Store.withStoreLease plan \lease -> do + absent <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + (Just dump) + Nothing + plannedAbsent <- expectOutputRight absent + assertEqual "absent dump path" + (Just dump) + (Output.dumpOutputPath + <$> Output.verificationDumpOutput plannedAbsent) + + Directory.createDirectory dump + emptyResult <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + (Just dump) + Nothing + void (expectOutputRight emptyResult) + +rejectsNonemptyDumpBeforeStoreStartup :: Assertion +rejectsNonemptyDumpBeforeStoreStartup = + Temp.withSystemTempDirectory "felix-output-before-store" \root -> do + let cacheRoot = root Posix.</> "cache" + dump = root Posix.</> "dump" + Directory.createDirectory cacheRoot + Directory.createDirectory dump + writeFile (dump Posix.</> "old.p") "stale" + withEnvironment "XDG_CACHE_HOME" cacheRoot do + plan <- expectRightIO + (Store.planStore Store.DefaultStore) + Store.withStoreLease plan \lease -> do + result <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + (Just dump) + Nothing + case result of + Left Output.DumpDestinationNotEmpty{} -> + pure () + Left other -> + assertFailure + ("unexpected nonempty result: " <> show other) + Right _ -> + assertFailure "nonempty dump was accepted" + assertBool "preflight did not create the default store" + . not + =<< Directory.doesPathExist + (cacheRoot Posix.</> "felix") + +rejectsStoreCollisions :: Assertion +rejectsStoreCollisions = + Temp.withSystemTempDirectory "felix-output-store-collision" \root -> do + let dump = root Posix.</> "dump" + persistentStore = dump Posix.</> "store.sqlite" + Directory.createDirectory dump + persistentPlan <- expectRightIO + (Store.planStore + (Store.ExplicitStore persistentStore)) + Store.withStoreLease persistentPlan \lease -> do + result <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + (Just dump) + Nothing + expectCollision result + + freshPlan <- expectRightIO + (Store.planStore Store.FreshTemporaryStore) + Store.withStoreLease freshPlan \lease -> do + let freshParent = + Posix.takeDirectory + (Store.storePathFilePath + (Store.storeLeasePath lease)) + result <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + (Just freshParent) + Nothing + expectCollision result + + relative <- expectRight (safeRelativePath "page.html") + let htmlRoot = root Posix.</> "html" + htmlStore = htmlRoot Posix.</> "page.html" + Directory.createDirectory htmlRoot + htmlPlan <- expectRightIO + (Store.planStore + (Store.ExplicitStore htmlStore)) + Store.withStoreLease htmlPlan \lease -> do + result <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + Nothing + (Just (htmlRoot, [relative])) + expectCollision result + +rejectsDumpHtmlCollisions :: Assertion +rejectsDumpHtmlCollisions = + Temp.withSystemTempDirectory "felix-output-cross-collision" \root -> do + let storeParent = root Posix.</> "store" + storeFile = storeParent Posix.</> "store.sqlite" + htmlRoot = root Posix.</> "html" + Directory.createDirectory storeParent + Directory.createDirectory htmlRoot + relative <- expectRight (safeRelativePath "nested/page.html") + plan <- expectRightIO + (Store.planStore + (Store.ExplicitStore storeFile)) + Store.withStoreLease plan \lease -> do + result <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + (Just htmlRoot) + (Just (htmlRoot, [relative])) + expectCollision result + +reservesRollbackJournal :: Assertion +reservesRollbackJournal = + Temp.withSystemTempDirectory "felix-output-journal" \root -> do + let storeParent = root Posix.</> "store" + storeFile = storeParent Posix.</> "store.sqlite" + journal = storeFile <> "-journal" + Directory.createDirectory storeParent + plan <- expectRightIO + (Store.planStore + (Store.ExplicitStore storeFile)) + Store.withStoreLease plan \lease -> do + result <- Output.planVerificationOutputs + (Store.storeLeasePath lease) + (Just journal) + Nothing + case result of + Left + (Output.CollidingOutputNamespaces + (Output.OutputNamespaceCollision + Output.StoreJournalOutputNamespace + reserved + Output.DumpOutputNamespace + requested :| [])) -> do + assertEqual "reserved journal" journal reserved + assertEqual "requested dump" journal requested + Left other -> + assertFailure + ("unexpected journal collision: " <> show other) + Right _ -> + assertFailure "store rollback journal was not reserved" + +expectCollision + :: Either Output.OutputPlanError Output.VerificationOutputPlan + -> Assertion +expectCollision = \case + Left Output.CollidingOutputNamespaces{} -> + pure () + Left other -> + assertFailure + ("unexpected output-plan result: " <> show other) + Right _ -> + assertFailure "colliding output namespaces were accepted" + +expectOutputRight + :: Either Output.OutputPlanError Output.VerificationOutputPlan + -> IO Output.VerificationOutputPlan +expectOutputRight = \case + Left failure -> + assertFailure (show failure) >> fail "unreachable" + Right plan -> + pure plan + +expectRight :: Show failure => Either failure value -> IO value +expectRight = \case + Left failure -> + assertFailure (show failure) >> fail "unreachable" + Right value -> + pure value + +expectRightIO + :: Show failure + => IO (Either failure value) + -> IO value +expectRightIO action = + expectRight =<< action + +withEnvironment + :: String + -> String + -> IO value + -> IO value +withEnvironment name value action = + Exception.bracket + (Environment.lookupEnv name) + restore + \_previous -> do + Environment.setEnv name value + action + where + restore = \case + Nothing -> + Environment.unsetEnv name + Just previous -> + Environment.setEnv name previous |
