diff options
Diffstat (limited to 'source/Felix/Test/Unit/Provers.hs')
| -rw-r--r-- | source/Felix/Test/Unit/Provers.hs | 1159 |
1 files changed, 1159 insertions, 0 deletions
diff --git a/source/Felix/Test/Unit/Provers.hs b/source/Felix/Test/Unit/Provers.hs new file mode 100644 index 0000000..f1e3cc1 --- /dev/null +++ b/source/Felix/Test/Unit/Provers.hs @@ -0,0 +1,1159 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Felix.Test.Unit.Provers (unitTests) where + +import Base hiding (Empty) +import Felix.Checking.Backend.Problem +import Felix.Checking.Core +import Felix.Provers + +import Control.Concurrent + ( newEmptyMVar + , putMVar + , takeMVar + , threadDelay + ) +import Control.Exception (bracket) +import Control.Exception qualified as Exception +import Control.Monad (when) +import Data.IORef + ( atomicModifyIORef' + , newIORef + , readIORef + , writeIORef + ) +import Data.Set qualified as Set +import Data.Text qualified as Text +import Data.Text.IO qualified as Text +import Data.Vector qualified as Vector +import Felix.Report.Location (Location(..)) +import System.Directory qualified as Directory +import System.Exit (ExitCode(..)) +import System.FilePath.Posix ((</>)) +import System.Posix.Signals + ( nullSignal + , sigTERM + , signalProcess + ) +import System.Posix.Types (ProcessID) +import System.Timeout qualified as Timeout +import Test.Tasty +import Test.Tasty.HUnit +import Text.Read (readMaybe) +import Text.Megaparsec (parseMaybe) +import UnliftIO.Async (cancel, mapConcurrently, withAsync) +import UnliftIO.Async qualified as Async + +unitTests :: TestTree +unitTests = + testGroup "Provers" + [ vampireStatusParserTests + , vampireClassifierTests + , jobsSelectionTests + , slowAtpReportTests + , vampireExecutorTests + , vampireProcessTests + ] + +jobsSelectionTests :: TestTree +jobsSelectionTests = + testGroup "effective jobs" + [ testCase "uses a positive override exactly" do + detectorCalled <- newIORef False + selected <- selectEffectiveJobs + (effectiveJobs 3) + (writeIORef detectorCalled True >> pure 99) + selected `shouldBe` positiveJobs 3 + readIORef detectorCalled >>= (`shouldBe` False) + , testCase "rounds automatic jobs to one third of detected processors" do + for_ + [(8, 3), (16, 5), (24, 8), (32, 11)] + \(detected, expected) -> do + selected <- selectEffectiveJobs Nothing (pure detected) + selected `shouldBe` positiveJobs expected + , testCase "falls back to one after bad detection" do + nonPositive <- selectEffectiveJobs Nothing (pure 0) + nonPositive `shouldBe` positiveJobs 1 + failed <- selectEffectiveJobs Nothing + (Exception.throwIO (userError "processor detection failed")) + failed `shouldBe` positiveJobs 1 + ] + +slowAtpReportTests :: TestTree +slowAtpReportTests = + testGroup "slow ATP report" + [ testCase "applies the threshold and retains the twelve slowest" do + prepared <- preparedTypedTask 0 + let requestId = + preparedVerificationRequestId + (preparedTypedProverRequest prepared) + task nanoseconds position = + SlowAtpTask + { slowAtpDuration = + atpDurationFromNanoseconds nanoseconds + , slowAtpOutcome = SlowAtpAccepted + , slowAtpPosition = workPosition 1 position + , slowAtpLocation = testLocation + , slowAtpRequestId = requestId + } + report = slowAtpReportFromTasks + ( task 4999999999 0 + : [ task (5000000000 + fromIntegral position) position + | position <- [0..13] + ] + ) + slowAtpQualifyingTaskCount report `shouldBe` 14 + length (slowAtpTasks report) `shouldBe` 12 + slowAtpOmittedTaskCount report `shouldBe` 2 + atpDurationNanoseconds + (slowAtpDuration + (fromMaybe + (error "slow report unexpectedly empty") + (listToMaybe (slowAtpTasks report)))) + `shouldBe` 5000000013 + , testCase "keeps earlier source positions on equal durations" do + prepared <- preparedTypedTask 0 + let requestId = + preparedVerificationRequestId + (preparedTypedProverRequest prepared) + task position = + SlowAtpTask + { slowAtpDuration = + atpDurationFromNanoseconds 5000000000 + , slowAtpOutcome = SlowAtpAccepted + , slowAtpPosition = workPosition 1 position + , slowAtpLocation = testLocation + , slowAtpRequestId = requestId + } + report = slowAtpReportFromTasks (task <$> [1..13]) + (workPositionLocalRequestOrdinal . slowAtpPosition + <$> slowAtpTasks report) + `shouldBe` [1..12] + ] + +vampireExecutorTests :: TestTree +vampireExecutorTests = + testGroup "bounded Vampire executor" + [ testCase "records completed qualifying tasks with runtime context" do + prepared <- preparedTypedTask 0 + clock <- scriptedClock [10, 5000000010] + let position = workPosition 2 3 + withFakeVampire + [ "cat >/dev/null" + , "printf '%s\n' '% SZS status Theorem for fake'" + ] + \vampireCommand -> + withVampireExecutorUsingClock + clock + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + handle <- submitVampireRequest + owner + position + testLocation + (preparedTypedProverRequest prepared) + awaitVampireRequest handle + >>= assertAcceptedRequest prepared + report <- vampireExecutorSlowAtpReport executor + slowAtpQualifyingTaskCount report `shouldBe` 1 + case slowAtpTasks report of + [task] -> do + atpDurationNanoseconds + (slowAtpDuration task) + `shouldBe` 5000000000 + slowAtpOutcome task `shouldBe` SlowAtpAccepted + slowAtpPosition task `shouldBe` position + slowAtpLocation task `shouldBe` testLocation + slowAtpRequestId task `shouldBe` + preparedVerificationRequestId + (preparedTypedProverRequest prepared) + tasks -> + assertFailure + ("unexpected slow-task report: " + <> show tasks) + , testCase "does not report a cancelled partial task" do + prepared <- preparedTypedTask 0 + clock <- scriptedClock [0, 6000000000] + withProcessGroupFake + defaultTimeLimit + [] + \pidFile vampireCommand -> + withVampireExecutorUsingClock + clock + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + handle <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + processIds <- waitForProcessIds pidFile + cancelVampireRequest handle + report <- vampireExecutorSlowAtpReport executor + slowAtpQualifyingTaskCount report `shouldBe` 0 + assertBool "cancelled report is empty" + (null (slowAtpTasks report)) + assertProcessesGone processIds + , testCase "opaque handles complete out of submission order" do + prepared <- preparedTypedTask 0 + firstStarted <- newEmptyMVar + releaseFirst <- newEmptyMVar + withFakeVampire + [ "cat >/dev/null" + , "printf '%s\n' '% SZS status Theorem for fake'" + ] + \vampireCommand -> + withVampireExecutor + (positiveJobs 2) + vampireCommand + (\position _request -> + when + (workPositionLocalRequestOrdinal position == 1) + (putMVar firstStarted () >> takeMVar releaseFirst)) + \executor -> withVampireRequestOwner executor \owner -> do + first <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + takeMVar firstStarted + second <- submitVampireRequest + owner + (workPosition 1 2) + testLocation + (preparedTypedProverRequest prepared) + secondCompletion <- awaitVampireRequest second + assertAcceptedRequest prepared secondCompletion + putMVar releaseFirst () + firstCompletion <- awaitVampireRequest first + assertAcceptedRequest prepared firstCompletion + , testCase "validates request identity before a rejection" do + submitted <- preparedTypedTask 0 + mismatched <- preparedTypedTask 1 + withFakeVampire + [ "cat >/dev/null" + , "printf '%s\n' '% SZS status CounterSatisfiable for fake'" + ] + \vampireCommand -> + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + handle <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest submitted) + outcome <- Exception.try + (awaitPreparedVampireRequest + (preparedTypedProverRequest mismatched) + handle) + case outcome of + Left (failure :: VampireExecutorFault) -> + assertBool + "request mismatch is an integrity fault" + ("wrong request id" + `Text.isInfixOf` + Text.pack (show failure)) + Right answer -> + assertFailure + ("mismatched rejection was accepted: " + <> show answer) + , testCase "runs requests through the bounded worker pool" do + prepared <- preparedTypedTask 0 + withFakeVampire + [ "previous=''" + , "found=0" + , "for argument in \"$@\"; do" + , " if [ \"$previous\" = '--cores' ]; then" + , " [ \"$argument\" = '2' ] || exit 17" + , " found=1" + , " fi" + , " previous=$argument" + , "done" + , "[ \"$found\" = '1' ] || exit 18" + , "cat >/dev/null" + , "sleep 0.2" + , "printf '%s\n' '% SZS status Theorem for fake'" + ] + \vampireCommand -> + withVampireExecutor + (positiveJobs 2) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + answers <- mapConcurrently + (\ordinal -> + runPreparedTypedProverWithExecutor + owner + (workPosition 1 ordinal) + testLocation + prepared) + [1..4] + traverse_ assertProved answers + , testCase "propagates observer failure to the submitter" do + prepared <- preparedTypedTask 0 + withFakeVampire + [ "cat >/dev/null" + , "printf '%s\n' '% SZS status Theorem for fake'" + ] + \vampireCommand -> + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\_position _request -> + Exception.throwIO + (userError "observer failed")) + \executor -> withVampireRequestOwner executor \owner -> do + result <- Exception.try + (runPreparedTypedProverWithExecutor + owner + (workPosition 1 1) + testLocation + prepared) + case result of + Left (failure :: VampireExecutorFault) -> + assertBool + "global executor fault" + ("observer failed" + `Text.isInfixOf` + Text.pack (show failure)) + Right answer -> + assertFailure + ("observer failure was lost: " + <> show answer) + , testCase "cancels queued and running jobs independently" do + prepared <- preparedTypedTask 0 + withProcessGroupFake + defaultTimeLimit + [] + \pidFile vampireCommand -> + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> + withAsync + (runPreparedTypedProverWithExecutor + owner + (workPosition 1 1) + testLocation + prepared) + \running -> do + processIds <- waitForProcessIds pidFile + queuedSubmitted <- newEmptyMVar + withAsync + (do + handle <- submitVampireRequest + owner + (workPosition 2 1) + testLocation + (preparedTypedProverRequest prepared) + putMVar queuedSubmitted () + awaitPreparedVampireRequest + (preparedTypedProverRequest prepared) + handle) + \queued -> do + takeMVar queuedSubmitted + cancel queued + cancel running + assertProcessesGone processIds + , testCase "explicit cancellation completes queued and running handles" do + prepared <- preparedTypedTask 0 + withProcessGroupFake + defaultTimeLimit + [] + \pidFile vampireCommand -> + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + running <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + processIds <- waitForProcessIds pidFile + queued <- submitVampireRequest + owner + (workPosition 1 2) + testLocation + (preparedTypedProverRequest prepared) + cancelVampireRequest queued + awaitVampireRequest queued + >>= assertCancelled prepared + cancelVampireRequest running + awaitVampireRequest running + >>= assertCancelled prepared + assertProcessesGone processIds + , testCase "structured shutdown wakes waiter and full-queue submitter" do + prepared <- preparedTypedTask 0 + withProcessGroupFake + defaultTimeLimit + [] + \pidFile vampireCommand -> do + (waiter, blockedSubmit, processIds) <- + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> + withVampireRequestOwner executor \owner -> do + running <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + processIds <- waitForProcessIds pidFile + _queuedOne <- submitVampireRequest + owner + (workPosition 1 2) + testLocation + (preparedTypedProverRequest prepared) + _queuedTwo <- submitVampireRequest + owner + (workPosition 1 3) + testLocation + (preparedTypedProverRequest prepared) + waiter <- Async.async + (awaitVampireRequest running) + submitStarted <- newEmptyMVar + blockedSubmit <- Async.async do + putMVar submitStarted () + submitVampireRequest + owner + (workPosition 1 4) + testLocation + (preparedTypedProverRequest prepared) + takeMVar submitStarted + pure (waiter, blockedSubmit, processIds) + Async.waitCatch waiter >>= \case + Right completion -> + assertCancelled prepared completion + Left failure -> + assertFailure + ("shutdown waiter failed: " <> show failure) + Async.waitCatch blockedSubmit >>= \case + Left failure -> + assertBool + "backpressured submit observes owner shutdown" + ("VampireRequestOwnerClosed" + `Text.isInfixOf` + Text.pack (show failure)) + Right _handle -> + assertFailure + "backpressured submit survived structured shutdown" + assertProcessesGone processIds + , testCase "worker fault wakes a waiter and a full-queue submitter" do + prepared <- preparedTypedTask 0 + observerEntered <- newEmptyMVar + failObserver <- newEmptyMVar + withFakeVampire + [ "cat >/dev/null" + , "printf '%s\n' '% SZS status Theorem for fake'" + ] + \vampireCommand -> + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\position _request -> + when + (workPositionLocalRequestOrdinal position == 1) + (putMVar observerEntered () + >> takeMVar failObserver + >> Exception.throwIO + (userError "fatal observer fault"))) + \executor -> withVampireRequestOwner executor \owner -> do + first <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + takeMVar observerEntered + _second <- submitVampireRequest + owner + (workPosition 1 2) + testLocation + (preparedTypedProverRequest prepared) + _third <- submitVampireRequest + owner + (workPosition 1 3) + testLocation + (preparedTypedProverRequest prepared) + withAsync + (submitVampireRequest + owner + (workPosition 1 4) + testLocation + (preparedTypedProverRequest prepared)) + \blockedSubmit -> do + putMVar failObserver () + awaitFault (awaitVampireRequest first) + Async.waitCatch blockedSubmit >>= \case + Left failure -> + assertExecutorFault failure + Right _handle -> + assertFailure + "full-queue submission survived executor fault" + , testCase "declared launch failure remains request-local" do + prepared <- preparedTypedTask 0 + let missing = vampire + "/definitely/missing/felix-vampire" + defaultTimeLimit + defaultMemoryLimit + withVampireExecutor + (positiveJobs 1) + missing + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + handle <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + completion <- awaitVampireRequest handle + assertCompletionRequest prepared completion + case vampireCompletionTerminal completion of + VampireProcessFailed ProverLaunchFailed{} -> pure () + terminal -> + assertFailure + ("expected a local launch failure, got " + <> show terminal) + , testCase "protocol failure is distinct from ATP rejection" do + prepared <- preparedTypedTask 0 + withFakeVampire + [ "cat >/dev/null" + , "printf '%s\n' 'completed without an SZS status'" + ] + \vampireCommand -> + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + handle <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + completion <- awaitVampireRequest handle + assertCompletionRequest prepared completion + case vampireCompletionTerminal completion of + VampireProtocolFailed{} -> pure () + terminal -> + assertFailure + ("expected a protocol terminal, got " + <> show terminal) + , testCase "completed rejection diagnostics are compact" do + prepared <- preparedTypedTask 0 + let headMarker :: Text + headMarker = "HEAD-MARKER" + tailMarker :: Text + tailMarker = "TAIL-MARKER" + status :: Text + status = "% SZS status CounterSatisfiable for fake" + originalByteCount = + Text.length headMarker + + 1048576 + + Text.length tailMarker + 1 + + Text.length status + 1 + withFakeVampire + [ "printf '%s' 'HEAD-MARKER'" + , "head -c 1048576 /dev/zero" + , "printf '%s\n' 'TAIL-MARKER'" + , "printf '%s\n' '% SZS status CounterSatisfiable for fake'" + ] + \vampireCommand -> + withVampireExecutor + (positiveJobs 1) + vampireCommand + (\_position _request -> pure ()) + \executor -> withVampireRequestOwner executor \owner -> do + handle <- submitVampireRequest + owner + (workPosition 1 1) + testLocation + (preparedTypedProverRequest prepared) + completion <- awaitVampireRequest handle + case renderVampireTerminalDiagnostic + (vampireCompletionTerminal completion) of + Just diagnostic -> do + assertBool + "retained diagnostic is bounded" + (Text.length diagnostic < 70000) + assertBool + "truncation is reported" + ("retained first and last 16 KiB" + `Text.isInfixOf` diagnostic) + assertBool + "original byte count is reported" + (("of " + <> Text.pack + (show originalByteCount) + <> " bytes)") + `Text.isInfixOf` diagnostic) + assertBool + "diagnostic head is retained" + (headMarker `Text.isInfixOf` diagnostic) + assertBool + "diagnostic tail is retained" + (tailMarker `Text.isInfixOf` diagnostic) + Nothing -> + assertFailure "expected a rejected terminal" + ] + +positiveJobs :: Int -> EffectiveJobs +positiveJobs amount = + fromMaybe + (error "test requested a non-positive job count") + (effectiveJobs amount) + +scriptedClock :: [Word64] -> IO (IO Word64) +scriptedClock ticks = do + remaining <- newIORef ticks + pure + (atomicModifyIORef' remaining \case + next : rest -> (rest, next) + [] -> error "test monotonic clock exhausted") + +testLocation :: Location +testLocation = Location maxBound + +vampireStatusParserTests :: TestTree +vampireStatusParserTests = + testGroup "Vampire status parser" + [ testCase "parses canonical status lines" do + parseMaybe + vampireStatusParser + "% SZS status ContradictoryAxioms for 2260" + `shouldBe` Just StatusContradictoryAxioms + , testCase "parses worker-prefixed status lines" do + parseMaybe + vampireStatusParser + "% (2581105)SZS status Timeout for " + `shouldBe` Just StatusTimeout + , testCase "parses ResourceOut status" do + parseMaybe + vampireStatusParser + "% SZS status ResourceOut for 2260" + `shouldBe` Just StatusResourceOut + , testCase "retains unsupported status values" do + parseMaybe + vampireStatusParser + "% SZS status AlienResult for 2260" + `shouldBe` Just (UnsupportedStatus "AlienResult") + ] + +vampireClassifierTests :: TestTree +vampireClassifierTests = + testGroup "Vampire completed transcript classifier" + [ testCase "maps each terminal status in both task modes" do + classify DirectTask [StatusTheorem] + `shouldBe` Right Proved + classify DirectTask [StatusCounterSatisfiable] + `shouldBe` Right Counterexample + classify DirectTask [StatusContradictoryAxioms] + `shouldBe` Right ContradictoryInput + classify IndirectTask [StatusContradictoryAxioms] + `shouldBe` Right Proved + , testCase "maps every resource status to indeterminate" do + for_ indeterminateStatuses \status -> + classify DirectTask [status] + `shouldBe` Right Indeterminate + , testCase "lets a unique terminal outcome override resource statuses" do + for_ indeterminateStatuses \status -> + classify DirectTask [status, StatusTheorem] + `shouldBe` Right Proved + , testCase "accepts duplicate and equivalent terminal statuses" do + classify DirectTask [StatusTheorem, StatusTheorem] + `shouldBe` Right Proved + classify + IndirectTask + [StatusTheorem, StatusContradictoryAxioms] + `shouldBe` Right Proved + , testCase "rejects every pair of different terminal outcomes" do + for_ conflictingTerminalCases + \(mode, statuses, outcomes) -> + classify mode statuses + `shouldBe` + Left (ConflictingTerminalOutcomes outcomes) + , testCase "is independent of status order" do + for_ orderCases \(mode, statuses) -> + classify mode statuses + `shouldBe` classify mode (reverse statuses) + , testCase "rejects unsupported status values" do + classify + DirectTask + [StatusTheorem, UnsupportedStatus "AlienResult"] + `shouldBe` + Left + (UnsupportedVampireStatuses + (Set.singleton "AlienResult")) + , testCase "rejects a successful exit without an outcome" do + classify DirectTask [] + `shouldBe` Left MissingVampireOutcome + , testCase "rejects every status after a nonzero exit" do + classifyVampireProtocol + DirectTask + (ExitFailure 7) + [StatusTheorem] + `shouldBe` + Left (UnsuccessfulVampireExit (ExitFailure 7)) + ] + +vampireProcessTests :: TestTree +vampireProcessTests = + testGroup "Vampire process boundary" + [ testCase "classifies statuses from both completed streams" do + answer <- runFakeVampire + [ "printf '%s\\n' '% SZS status Timeout for fake'" + , "printf '%s\\n' '% SZS status Theorem for fake' >&2" + , "exit 0" + ] + assertProved answer + , testCase "rejects a split-stream terminal conflict" do + answer <- runFakeVampire + [ "printf '%s\\n' '% SZS status Theorem for fake'" + , "printf '%s\\n' '% SZS status CounterSatisfiable for fake' >&2" + , "exit 0" + ] + assertProtocolError "ConflictingTerminalOutcomes" answer + , testCase "rejects a theorem from a nonzero exit" do + answer <- runFakeVampire + [ "printf '%s\\n' '% SZS status Theorem for fake'" + , "exit 7" + ] + assertProtocolError "ExitFailure 7" answer + , testCase "rejects malformed UTF-8 output" do + answer <- runFakeVampire + [ "printf '\\377'" + , "exit 0" + ] + case answer of + Left + (ProverOutputMalformedUtf8 + _ + ProverOutputStdout + _) -> + pure () + result -> + assertFailure + ("expected malformed stdout, got " <> show result) + , testCase "returns a broken stdin pipe" do + prepared <- preparedTypedTask 20000 + result <- withFakeVampire + [ "exec 0<&-" + , "sleep 1" + ] + \vampireCommand -> + runPreparedTypedProver vampireCommand prepared + case result of + Left (ProverCommunicationFailed _ ProverStdin _) -> + pure () + processResult -> + assertFailure + ("expected a communication failure, got " + <> show processResult) + , testCase "drains output while feeding prover input" do + prepared <- preparedTypedTask 20000 + guardedAnswer <- Timeout.timeout + 30000000 + (withFakeVampire + [ "head -c 1048576 /dev/zero &" + , "head -c 1048576 /dev/zero >&2 &" + , "wait" + , "printf '\\n'" + , "printf '\\n' >&2" + , "cat >/dev/null" + , "printf '%s\\n' '% SZS status Theorem for fake'" + , "exit 0" + ] + \vampireCommand -> do + runPreparedTypedProver vampireCommand prepared) + case guardedAnswer of + Nothing -> + assertFailure "prover communication did not finish" + Just answer -> + assertProved answer + , testCase "reports signal termination separately" do + prepared <- preparedTypedTask 0 + result <- withFakeVampire + [ "kill -TERM $$" + ] + \vampireCommand -> + runPreparedTypedProver vampireCommand prepared + case result of + Left + (ProverTerminatedBySignal + _ + signalNumber + _) -> + assertEqual + "termination signal" + (fromIntegral sigTERM) + signalNumber + processResult -> + assertFailure + ("expected signal termination, got " + <> show processResult) + , testCase "deadline terminates and reaps the process group" do + prepared <- preparedTypedTask 0 + withProcessGroupFake + (Seconds 0) + [] + \pidFile vampireCommand -> do + result <- + runPreparedTypedProver vampireCommand prepared + assertTimedOut result + processIds <- readProcessIds pidFile + assertProcessesGone processIds + , testCase "output exhaustion terminates the process group" do + prepared <- preparedTypedTask 0 + withProcessGroupFake + defaultTimeLimit + [ "head -c 33554432 /dev/zero" + ] + \pidFile vampireCommand -> do + result <- + runPreparedTypedProver vampireCommand prepared + case result of + Left + (ProverOutputLimitExceeded + _ + ProverOutputStdout + _) -> + pure () + processResult -> + assertFailure + ("expected stdout limit exhaustion, got " + <> show processResult) + processIds <- readProcessIds pidFile + assertProcessesGone processIds + , testCase "cancellation terminates and reaps the process group" do + prepared <- preparedTypedTask 0 + withProcessGroupFake + defaultTimeLimit + [] + \pidFile vampireCommand -> + withAsync + (runPreparedTypedProver vampireCommand prepared) + \worker -> do + processIds <- waitForProcessIds pidFile + cancel worker + assertProcessesGone processIds + ] + +classify + :: VampireTaskMode + -> [VampireStatus] + -> Either VampireProtocolError CanonicalAtpOutcome +classify mode = + classifyVampireProtocol mode ExitSuccess + +indeterminateStatuses :: [VampireStatus] +indeterminateStatuses = + [ StatusTimeout + , StatusResourceOut + , StatusGaveUp + , StatusUnknown + ] + +conflictingTerminalCases + :: [(VampireTaskMode, [VampireStatus], Set CanonicalAtpOutcome)] +conflictingTerminalCases = + [ ( DirectTask + , [StatusTheorem, StatusCounterSatisfiable] + , Set.fromList [Proved, Counterexample] + ) + , ( DirectTask + , [StatusTheorem, StatusContradictoryAxioms] + , Set.fromList [Proved, ContradictoryInput] + ) + , ( DirectTask + , [StatusCounterSatisfiable, StatusContradictoryAxioms] + , Set.fromList [Counterexample, ContradictoryInput] + ) + , ( IndirectTask + , [StatusTheorem, StatusCounterSatisfiable] + , Set.fromList [Proved, Counterexample] + ) + , ( IndirectTask + , [StatusCounterSatisfiable, StatusContradictoryAxioms] + , Set.fromList [Proved, Counterexample] + ) + ] + +orderCases :: [(VampireTaskMode, [VampireStatus])] +orderCases = + [ (DirectTask, StatusTheorem : indeterminateStatuses) + , (DirectTask, [StatusTheorem, StatusCounterSatisfiable]) + , (IndirectTask, [StatusTheorem, StatusContradictoryAxioms]) + , (DirectTask, [UnsupportedStatus "B", UnsupportedStatus "A"]) + ] + +assertProved + :: Either ProverProcessError ProverAnswer + -> Assertion +assertProved = \case + Right Yes -> + pure () + answer -> + assertFailure ("expected a proof, got " <> show answer) + +assertAcceptedRequest + :: PreparedTypedProverTask ref local origin global + -> VampireCompletion + -> Assertion +assertAcceptedRequest prepared completion = do + assertCompletionRequest prepared completion + case vampireCompletionTerminal completion of + VampireAccepted -> pure () + terminal -> + assertFailure ("expected an accepted terminal, got " <> show terminal) + +assertCompletionRequest + :: PreparedTypedProverTask ref local origin global + -> VampireCompletion + -> Assertion +assertCompletionRequest prepared completion = + vampireCompletionRequestId completion + `shouldBe` + preparedVerificationRequestId + (preparedTypedProverRequest prepared) + +assertCancelled + :: PreparedTypedProverTask ref local origin global + -> VampireCompletion + -> Assertion +assertCancelled prepared completion = do + assertCompletionRequest prepared completion + vampireCompletionTerminal completion `shouldBe` VampireCancelled + +awaitFault :: IO value -> Assertion +awaitFault action = do + result <- Exception.try action + case result of + Left failure -> assertExecutorFault failure + Right _value -> assertFailure "expected a global executor fault" + +assertExecutorFault :: Exception.SomeException -> Assertion +assertExecutorFault failure = + case Exception.fromException failure :: Maybe VampireExecutorFault of + Just _fault -> pure () + Nothing -> + assertFailure + ("expected VampireExecutorFault, got " <> show failure) + +assertProtocolError + :: Text + -> Either ProverProcessError ProverAnswer + -> Assertion +assertProtocolError expected = \case + Right (Error _label diagnostic) -> + assertBool + ( "expected protocol error containing " + <> show expected + <> ", got " + <> show diagnostic + ) + (expected `Text.isInfixOf` diagnostic) + answer -> + assertFailure ("expected a protocol error, got " <> show answer) + +assertTimedOut + :: Either ProverProcessError a + -> Assertion +assertTimedOut = \case + Left ProverTimedOut{} -> + pure () + result -> + assertFailure ("expected prover timeout, got " <> showResult result) + where + showResult = \case + Left err -> + show err + Right _ -> + "successful process result" + +withProcessGroupFake + :: TimeLimit + -> [String] + -> (FilePath -> Vampire -> IO a) + -> IO a +withProcessGroupFake timeLimit body action = + withFakeVampireIn + (\temp -> + let pidFile = temp </> "process-ids" + in [ "trap '' TERM" + , "sleep 60 &" + , "printf '%s %s\\n' \"$$\" \"$!\" > " <> pidFile + ] + <> body + <> ["wait"]) + timeLimit + \temp -> + action (temp </> "process-ids") + +readProcessIds :: FilePath -> IO [ProcessID] +readProcessIds path = do + contents <- Text.readFile path + case traverse + (readMaybe . Text.unpack) + (Text.words contents) of + Just processIds@[_leader, _descendant] -> + pure processIds + _ -> + assertFailure + ("expected leader and descendant process ids, got " + <> show contents) + +waitForProcessIds :: FilePath -> IO [ProcessID] +waitForProcessIds path = do + guarded <- Timeout.timeout 10000000 loop + case guarded of + Just processIds -> + pure processIds + Nothing -> + assertFailure "fake prover did not publish its process ids" + where + loop = do + exists <- Directory.doesFileExist path + if exists + then readProcessIds path + else do + threadDelay 10000 + loop + +assertProcessesGone :: [ProcessID] -> Assertion +assertProcessesGone processIds = do + guarded <- Timeout.timeout 10000000 loop + case guarded of + Just () -> + pure () + Nothing -> + assertFailure + ("supervisor left processes running: " + <> show processIds) + where + loop = do + alive <- traverse processIsAlive processIds + if or alive + then do + threadDelay 10000 + loop + else pure () + +processIsAlive :: ProcessID -> IO Bool +processIsAlive processId = + (signalProcess nullSignal processId >> pure True) + `Exception.catch` \(err :: Exception.IOException) -> + if isDoesNotExistError err + then pure False + else throwIO err + +runFakeVampire + :: [String] + -> IO (Either ProverProcessError ProverAnswer) +runFakeVampire scriptLines = + withFakeVampire + (["cat >/dev/null"] <> scriptLines) + \vampireCommand -> do + prepared <- preparedTypedTask 0 + runPreparedTypedProver vampireCommand prepared + +withFakeVampire + :: [String] + -> (Vampire -> IO a) + -> IO a +withFakeVampire scriptLines action = + withFakeVampireIn + (const scriptLines) + defaultTimeLimit + (const action) + +withFakeVampireIn + :: (FilePath -> [String]) + -> TimeLimit + -> (FilePath -> Vampire -> IO a) + -> IO a +withFakeVampireIn makeScript timeLimit action = + withTemporaryDirectory "felix-fake-vampire" \temp -> do + let executablePath = temp </> "vampire" + writeFile executablePath + (unlines + ( [ "#!/bin/sh" + ] + <> makeScript temp + )) + permissions <- Directory.getPermissions executablePath + Directory.setPermissions executablePath + (Directory.setOwnerExecutable True permissions) + action + temp + (vampire + executablePath + timeLimit + defaultMemoryLimit) + +withTemporaryDirectory :: String -> (FilePath -> IO a) -> IO a +withTemporaryDirectory template = + bracket create Directory.removePathForcibly + where + create = do + systemTemp <- Directory.getTemporaryDirectory + (path, handle) <- openTempFile systemTemp template + hClose handle + Directory.removeFile path + Directory.createDirectory path + pure path + +shouldBe :: (Eq a, Show a, HasCallStack) => a -> a -> Assertion +shouldBe = + flip (assertEqual "") + +preparedTypedTask + :: Int + -> IO (PreparedTypedProverTask Int Void Void Void) +preparedTypedTask factCount = do + checked <- expectRight + (checkScopedCanonicalCore + (const Nothing) + [] + propositionTerm) + proposition <- expectRight + (supportedProposition Vector.empty checked) + capability <- expectRight + (classifySupportedProposition (const Nothing) proposition) + let facts = + Vector.generate + factCount + (\reference -> + typedBackendFact reference proposition capability) + problem <- expectRight + (planTypedProblem + (const Nothing) + facts + proposition + [] + [] + FirstOrderLocals + ExplicitHigherOrderJustification) + expectRight (prepareTypedProverTask DirectTask problem) + where + propositionTerm = + CEq TySet + (CIntrinsic Empty) + (CIntrinsic Empty) + +expectRight :: Show error => Either error value -> IO value +expectRight = \case + Left failure -> + assertFailure (show failure) >> fail "unreachable" + Right value -> + pure value |
