keiro-dsl-0.7.0.0: test/conformance-scalar-expressions/Generated/AggregateScalarExpressions/ScalarAccount/BehaviorContract.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedLabels #-}
{-# OPTIONS_GHC -Wno-missing-signatures -Wno-name-shadowing #-}
-- @generated by keiro-dsl; do not edit. Regenerated from the .keiro spec.
module Generated.AggregateScalarExpressions.ScalarAccount.BehaviorContract where
import Generated.AggregateScalarExpressions.ScalarAccount.Codec (encodeScalarAccountEvent, parseScalarAccountEvent, scalarAccountCodec)
import Generated.AggregateScalarExpressions.ScalarAccount.Domain
import Generated.AggregateScalarExpressions.ScalarAccount.Transducer (scalarAccountTransducer)
import Data.Aeson (ToJSON (..), object, (.=))
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty)
import Data.List.NonEmpty qualified as NonEmpty
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Keiki.Core qualified as K (EdgeMode (..), EdgeRef (..), RegFile, ReplayAttribution (..), ReplayEventSpan (..), ReplaySuccess (..), StepFailure (..), StepSuccess (..), applyEventsDetailedEither, stepDetailedEither, (!))
import Keiro.Codec qualified as Codec (Codec (eventType), EventType (..))
newtype BehaviorKey = BehaviorKey { unBehaviorKey :: Text }
deriving stock (Eq, Ord, Show)
data ObligationKind = LiveTransition | RequiredRejection | ReplayTransition
deriving stock (Eq, Ord, Show)
data EvidenceLevel = GeneratedAuthoritative | HoleWitnessed | LegacyRuntimeWitness
deriving stock (Eq, Ord, Show)
data GuardCoverage = GuardTotal | GuardPartial | GuardUnknown | GuardNotApplicable
deriving stock (Eq, Ord, Show)
data BehaviorRequirement = BehaviorRequirement
{ requirementKey :: !BehaviorKey
, requirementKind :: !ObligationKind
, requirementEvidence :: !EvidenceLevel
, requirementGuardCoverage :: !GuardCoverage
, requirementSource :: !ScalarAccountVertex
, requirementCommandName :: !Text
, requirementExpectedEdge :: !(Maybe (K.EdgeRef ScalarAccountVertex))
, requirementTarget :: !(Maybe ScalarAccountVertex)
, requirementEventKinds :: ![Text]
, requirementLine :: !Int
}
deriving stock (Eq, Show)
data RejectionClass = RejectNoOutgoingEdges | RejectNoMatchingEdge
deriving stock (Eq, Show)
data LiveExpectation
= Emits (NonEmpty ScalarAccountEvent)
| Rejects RejectionClass
| NoOp
deriving stock (Eq, Show)
data BehaviorWitness
= Pending BehaviorKey
| LiveWitness
{ witnessKey :: BehaviorKey
, witnessHistory :: [ScalarAccountEvent]
, witnessCommand :: ScalarAccountCommand
, witnessExpected :: LiveExpectation
}
| ReplayWitness
{ witnessKey :: BehaviorKey
, witnessHistoryPrefix :: [ScalarAccountEvent]
, witnessObservedChunk :: [ScalarAccountEvent]
}
deriving stock (Eq, Show)
data BehaviorFailure = BehaviorFailure
{ failureKey :: !BehaviorKey
, failureCode :: !Text
, failureDetail :: !Text
}
deriving stock (Eq, Show)
instance ToJSON BehaviorFailure where
toJSON failure = object
[ "key" .= unBehaviorKey (failureKey failure)
, "code" .= failureCode failure
, "detail" .= failureDetail failure
]
data BehaviorConformanceReport = BehaviorConformanceReport
{ reportRequired :: ![BehaviorKey]
, reportFilled :: ![BehaviorKey]
, reportPending :: ![BehaviorKey]
, reportMissing :: ![BehaviorKey]
, reportDuplicate :: ![BehaviorKey]
, reportStale :: ![BehaviorKey]
, reportFailed :: ![BehaviorFailure]
, reportVerified :: ![BehaviorKey]
, reportUnverified :: ![BehaviorKey]
}
deriving stock (Eq, Show)
instance ToJSON BehaviorConformanceReport where
toJSON report = object
[ "schema" .= ("keiro/behavior-conformance/1" :: Text)
, "required" .= keyTexts (reportRequired report)
, "filled" .= keyTexts (reportFilled report)
, "pending" .= keyTexts (reportPending report)
, "missing" .= keyTexts (reportMissing report)
, "duplicate" .= keyTexts (reportDuplicate report)
, "stale" .= keyTexts (reportStale report)
, "failed" .= reportFailed report
, "verified" .= keyTexts (reportVerified report)
, "unverified" .= keyTexts (reportUnverified report)
]
behaviorRequirements :: [BehaviorRequirement]
behaviorRequirements =
[ BehaviorRequirement (BehaviorKey "behavior-v1-1de91883b9e3f8bc") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountOpen "Close" Nothing Nothing [] 32
, BehaviorRequirement (BehaviorKey "behavior-v1-5c58e0aafeb5f400") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountReviewed "Adjust" Nothing Nothing [] 32
, BehaviorRequirement (BehaviorKey "behavior-v1-78f2c698e2ec1dc0") LiveTransition GeneratedAuthoritative GuardUnknown ScalarAccountOpen "Adjust" (Just (K.EdgeRef ScalarAccountOpen 0)) (Just ScalarAccountReviewed) ["Adjusted"] 42
, BehaviorRequirement (BehaviorKey "behavior-v1-a19a8e94d935493f") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountClosed "Adjust" Nothing Nothing [] 32
, BehaviorRequirement (BehaviorKey "behavior-v1-b7db52f28632d8da") LiveTransition HoleWitnessed GuardTotal ScalarAccountReviewed "Close" (Just (K.EdgeRef ScalarAccountReviewed 0)) (Just ScalarAccountClosed) ["ClosedEvent"] 62
, BehaviorRequirement (BehaviorKey "behavior-v1-dbf483c868dd6d34") RequiredRejection GeneratedAuthoritative GuardNotApplicable ScalarAccountClosed "Close" Nothing Nothing [] 32
]
behaviorCoverageReport :: [BehaviorWitness] -> BehaviorConformanceReport
behaviorCoverageReport witnesses =
BehaviorConformanceReport
{ reportRequired = sortedKeys (Map.keys requiredByKey)
, reportFilled = sortedKeys [key | (key, [witness]) <- Map.toList witnessGroups, Map.member key requiredByKey, not (isPending witness)]
, reportPending = sortedKeys [key | (key, rows) <- Map.toList witnessGroups, Map.member key requiredByKey, any isPending rows]
, reportMissing = sortedKeys [key | key <- Map.keys requiredByKey, Map.notMember key witnessGroups]
, reportDuplicate = sortedKeys [key | (key, rows) <- Map.toList witnessGroups, length rows > 1]
, reportStale = sortedKeys [key | key <- Map.keys witnessGroups, Map.notMember key requiredByKey]
, reportFailed = sortOn (unBehaviorKey . failureKey) failures
, reportVerified = sortedKeys [requirementKey requirement | (requirement, Right ()) <- executions, proofStrength requirement]
, reportUnverified = sortedKeys [requirementKey requirement | (requirement, Right ()) <- executions, not (proofStrength requirement)]
}
where
requiredByKey = Map.fromList [(requirementKey requirement, requirement) | requirement <- behaviorRequirements]
witnessGroups = Map.fromListWith (flip (<>)) [(behaviorWitnessKey witness, [witness]) | witness <- witnesses]
executions =
[ (requirement, runWitness requirement witness)
| (key, [witness]) <- Map.toList witnessGroups
, not (isPending witness)
, Just requirement <- [Map.lookup key requiredByKey]
]
failures = [failure | (_, Left failure) <- executions]
behaviorConformancePassed :: BehaviorConformanceReport -> Bool
behaviorConformancePassed = behaviorConformancePassedWith False
behaviorConformancePassedWith :: Bool -> BehaviorConformanceReport -> Bool
behaviorConformancePassedWith failOnUnverified report =
null (reportPending report)
&& null (reportMissing report)
&& null (reportDuplicate report)
&& null (reportStale report)
&& null (reportFailed report)
&& (not failOnUnverified || null (reportUnverified report))
renderBehaviorConformanceText :: BehaviorConformanceReport -> Text
renderBehaviorConformanceText report = T.unlines
[ "behavior conformance: ScalarAccount"
, "schema: keiro/behavior-conformance/1"
, countLine "required" (reportRequired report)
, countLine "filled" (reportFilled report)
, countLine "pending" (reportPending report)
, countLine "missing" (reportMissing report)
, countLine "duplicate" (reportDuplicate report)
, countLine "stale" (reportStale report)
, "failed: " <> tshow (length (reportFailed report))
, countLine "verified" (reportVerified report)
, countLine "unverified" (reportUnverified report)
] <> T.unlines ["FAIL " <> unBehaviorKey (failureKey failure) <> " [" <> failureCode failure <> "] " <> failureDetail failure | failure <- reportFailed report]
runWitness :: BehaviorRequirement -> BehaviorWitness -> Either BehaviorFailure ()
runWitness requirement witness = case witness of
Pending _ -> failure requirement "pending" "witness is still Pending"
LiveWitness _ history command expectation -> runLive requirement history command expectation
ReplayWitness _ prefix chunk -> runReplay requirement prefix chunk
runLive :: BehaviorRequirement -> [ScalarAccountEvent] -> ScalarAccountCommand -> LiveExpectation -> Either BehaviorFailure ()
runLive requirement history command expectation = do
settled <- settleHistory requirement "history" history
ensure requirement (K.replaySuccessState settled == requirementSource requirement) "history-wrong-source" "history does not settle at the required source vertex"
ensure requirement (commandKind command == requirementCommandName requirement) "command-mismatch" "witness command constructor does not match the required state/command cell"
case requirementKind requirement of
ReplayTransition -> failure requirement "witness-kind" "a replay-only requirement needs ReplayWitness"
RequiredRejection -> runRejection requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation
LiveTransition -> runAcceptance requirement (K.replaySuccessState settled, K.replaySuccessRegs settled) command expectation
runRejection requirement seed command expectation = case expectation of
Emits _ -> failure requirement "expectation-kind" "a rejection requirement cannot expect emitted events"
NoOp -> failure requirement "expectation-kind" "a rejection requirement cannot expect an accepted no-op"
Rejects expectedClass -> case K.stepDetailedEither scalarAccountTransducer seed command of
Left K.NoOutgoingEdges {} -> ensure requirement (expectedClass == RejectNoOutgoingEdges) "rejection-class" "expected NoMatchingEdge but runtime returned NoOutgoingEdges"
Left K.NoMatchingEdge {} -> ensure requirement (expectedClass == RejectNoMatchingEdge) "rejection-class" "expected NoOutgoingEdges but runtime returned NoMatchingEdge"
Left K.AmbiguousEdges {} -> failure requirement "ambiguous-edges" "AmbiguousEdges can never satisfy a rejection witness"
Right _ -> failure requirement "unexpected-acceptance" "runtime accepted a command required to reject"
runAcceptance requirement seed command expectation = case expectation of
Rejects _ -> failure requirement "expectation-kind" "a live-transition requirement needs Emits or NoOp"
NoOp -> case K.stepDetailedEither scalarAccountTransducer seed command of
Left stepFailure -> failure requirement "unexpected-rejection" (tshow stepFailure)
Right success -> do
checkAcceptedEnvelope requirement success
ensure requirement (null (K.stepSuccessOutputs success)) "noop-emitted" "NoOp emitted one or more events"
ensure requirement (K.stepSuccessState success == fst seed) "noop-vertex-change" "NoOp changed the control vertex"
ensure requirement (regsEqual (K.stepSuccessRegs success) (snd seed)) "noop-register-change" "NoOp changed one or more registers"
Emits expectedEvents -> case K.stepDetailedEither scalarAccountTransducer seed command of
Left stepFailure -> failure requirement "unexpected-rejection" (tshow stepFailure)
Right success -> do
checkAcceptedEnvelope requirement success
let expected = NonEmpty.toList expectedEvents
actual = K.stepSuccessOutputs success
ensure requirement (actual == expected) "event-value-mismatch" "runtime event values differ from the exact witness expectation"
ensure requirement (map eventKind actual == requirementEventKinds requirement) "event-envelope-mismatch" "runtime event kinds differ from the declared ordered envelope"
decoded <- either (failure requirement "emitted-codec-decode") Right (decodeEvents actual)
replayed <- case K.applyEventsDetailedEither scalarAccountTransducer seed decoded of
Left replayFailure -> failure requirement "emitted-replay-failed" (tshow replayFailure)
Right replaySuccess -> Right replaySuccess
ensure requirement (K.replaySuccessState replayed == K.stepSuccessState success) "forward-replay-vertex" "decoded emissions replay to a different vertex"
ensure requirement (regsEqual (K.replaySuccessRegs replayed) (K.stepSuccessRegs success)) "forward-replay-registers" "decoded emissions replay to different registers"
checkSingleAttribution requirement K.Live (length decoded) (K.replaySuccessTrace replayed)
checkAcceptedEnvelope requirement success = do
ensure requirement (K.stepSuccessMode success == K.Live) "forward-mode" "forward execution selected a non-live edge"
ensure requirement (Just (K.stepSuccessEdge success) == requirementExpectedEdge requirement) "edge-attribution" "runtime selected a different guarded sibling"
ensure requirement (Just (K.stepSuccessState success) == requirementTarget requirement) "target-mismatch" "runtime reached a different target vertex"
runReplay :: BehaviorRequirement -> [ScalarAccountEvent] -> [ScalarAccountEvent] -> Either BehaviorFailure ()
runReplay requirement prefix chunk = case requirementKind requirement of
ReplayTransition -> do
settled <- settleHistory requirement "history-prefix" prefix
ensure requirement (K.replaySuccessState settled == requirementSource requirement) "history-wrong-source" "history prefix does not settle at the replay edge source"
ensure requirement (not (null chunk)) "empty-replay-chunk" "a replay-only edge has no observable empty chunk"
decoded <- either (failure requirement "replay-chunk-codec-decode") Right (decodeEvents chunk)
replayed <- case K.applyEventsDetailedEither scalarAccountTransducer (K.replaySuccessState settled, K.replaySuccessRegs settled) decoded of
Left replayFailure -> failure requirement "replay-chunk-failed" (tshow replayFailure)
Right replaySuccess -> Right replaySuccess
ensure requirement (Just (K.replaySuccessState replayed) == requirementTarget requirement) "target-mismatch" "replay chunk reached a different target vertex"
checkSingleAttribution requirement K.ReplayOnly (length decoded) (K.replaySuccessTrace replayed)
_ -> failure requirement "witness-kind" "ReplayWitness supplied for a non-replay requirement"
checkSingleAttribution requirement expectedMode eventCount trace = case trace of
[attribution] -> do
ensure requirement (Just (K.replayAttributionEdge attribution) == requirementExpectedEdge requirement) "replay-edge-attribution" "replay selected a different edge"
ensure requirement (K.replayAttributionMode attribution == expectedMode) "replay-mode-attribution" "replay selected the wrong live/replay-only phase"
ensure requirement (K.replayAttributionSource attribution == requirementSource requirement) "replay-source-attribution" "replay attribution starts at the wrong source"
ensure requirement (Just (K.replayAttributionTarget attribution) == requirementTarget requirement) "replay-target-attribution" "replay attribution ends at the wrong target"
ensure requirement (K.replayAttributionSpan attribution == K.ReplayEventSpan 0 eventCount) "replay-span-attribution" "replay attribution did not consume the exact chunk"
_ -> failure requirement "replay-trace-cardinality" "expected exactly one completed-edge attribution"
settleHistory requirement label history = do
decoded <- either (failure requirement (label <> "-codec-decode")) Right (decodeEvents history)
case K.applyEventsDetailedEither scalarAccountTransducer (ScalarAccountOpen, initialScalarAccountRegs) decoded of
Left replayFailure -> failure requirement (label <> "-replay-failed") (tshow replayFailure)
Right replaySuccess -> Right replaySuccess
decodeEvents :: [ScalarAccountEvent] -> Either Text [ScalarAccountEvent]
decodeEvents = traverse (\event -> parseScalarAccountEvent (Codec.eventType scalarAccountCodec event) (encodeScalarAccountEvent event))
commandKind command = case command of
Adjust _ -> "Adjust"
Close _ -> "Close"
eventKind event = case Codec.eventType scalarAccountCodec event of Codec.EventType tag -> tag
regsEqual :: K.RegFile ScalarAccountRegs -> K.RegFile ScalarAccountRegs -> Bool
regsEqual left right = (left K.! #balance) == (right K.! #balance) && (left K.! #reserved) == (right K.! #reserved) && (left K.! #capacity) == (right K.! #capacity) && (left K.! #machine) == (right K.! #machine) && (left K.! #label) == (right K.! #label) && (left K.! #active) == (right K.! #active) && (left K.! #mode) == (right K.! #mode) && (left K.! #requestId) == (right K.! #requestId) && (left K.! #openedAt) == (right K.! #openedAt) && (left K.! #limits) == (right K.! #limits)
proofStrength requirement =
requirementEvidence requirement == GeneratedAuthoritative
&& requirementGuardCoverage requirement `elem` [GuardTotal, GuardNotApplicable]
behaviorWitnessKey witness = case witness of
Pending key -> key
LiveWitness { witnessKey = key } -> key
ReplayWitness { witnessKey = key } -> key
isPending Pending {} = True
isPending _ = False
ensure requirement condition code detail = if condition then Right () else failure requirement code detail
failure requirement code detail = Left (BehaviorFailure (requirementKey requirement) code detail)
sortedKeys = sortOn unBehaviorKey
keyTexts = map unBehaviorKey
countLine label values = label <> ": " <> tshow (length values)
tshow :: Show value => value -> Text
tshow = T.pack . show