#nullable enable namespace MBN_STOCK_WEBVIEW.DbWriteSmoke; internal enum DevelopmentDatabaseSmokeStage { None, Preflight, Create, CreateReadback, Save, SaveReadback, Cleanup, VerifyAbsent } internal sealed record DevelopmentDatabaseSmokeResult( string ScenarioName, bool Passed, DevelopmentDatabaseSmokeStage FailureStage, string FailureClass, bool OutcomeUnknown, bool CleanupAttempted, bool CleanupVerified, bool ManualCleanupMayBeRequired); /// /// Runs one collision-isolated development write scenario. Every delegate is /// invoked at most once. A mutation with an unknown outcome quarantines the /// scenario immediately: no cleanup mutation or later scenario may be issued. /// internal sealed class DevelopmentDatabaseSmokeScenario { private readonly Func _preflight; private readonly Func _create; private readonly Func _verifyCreate; private readonly Func? _save; private readonly Func? _verifySave; private readonly Func _cleanup; private readonly Func _verifyAbsent; private readonly Func _isOutcomeUnknown; internal DevelopmentDatabaseSmokeScenario( string name, Func preflight, Func create, Func verifyCreate, Func? save, Func? verifySave, Func cleanup, Func verifyAbsent, Func isOutcomeUnknown) { ArgumentException.ThrowIfNullOrWhiteSpace(name); if ((save is null) != (verifySave is null)) { throw new ArgumentException( "Save and save-readback delegates must either both be present or both be absent.", nameof(save)); } Name = name; _preflight = preflight ?? throw new ArgumentNullException(nameof(preflight)); _create = create ?? throw new ArgumentNullException(nameof(create)); _verifyCreate = verifyCreate ?? throw new ArgumentNullException(nameof(verifyCreate)); _save = save; _verifySave = verifySave; _cleanup = cleanup ?? throw new ArgumentNullException(nameof(cleanup)); _verifyAbsent = verifyAbsent ?? throw new ArgumentNullException(nameof(verifyAbsent)); _isOutcomeUnknown = isOutcomeUnknown ?? throw new ArgumentNullException(nameof(isOutcomeUnknown)); } internal string Name { get; } internal async Task RunAsync( CancellationToken cancellationToken = default) { var currentStage = DevelopmentDatabaseSmokeStage.Preflight; Exception? primaryFailure = null; var failureStage = DevelopmentDatabaseSmokeStage.None; var outcomeUnknown = false; var failureClass = "none"; var createKnownCommitted = false; var cleanupAttempted = false; var cleanupVerified = false; try { try { await _preflight(cancellationToken).ConfigureAwait(false); currentStage = DevelopmentDatabaseSmokeStage.Create; await _create(cancellationToken).ConfigureAwait(false); createKnownCommitted = true; currentStage = DevelopmentDatabaseSmokeStage.CreateReadback; await _verifyCreate(cancellationToken).ConfigureAwait(false); if (_save is not null) { currentStage = DevelopmentDatabaseSmokeStage.Save; await _save(cancellationToken).ConfigureAwait(false); currentStage = DevelopmentDatabaseSmokeStage.SaveReadback; await _verifySave!(cancellationToken).ConfigureAwait(false); } } catch (Exception exception) { primaryFailure = exception; failureStage = currentStage; failureClass = RedactedDatabaseFailureClassifier.Classify(exception); outcomeUnknown = _isOutcomeUnknown(exception); } } finally { // A known successful create is cleaned up even when a later read-only // verification fails. An unknown mutation outcome is quarantined and // never followed by another mutation. if (createKnownCommitted && !outcomeUnknown) { cleanupAttempted = true; try { currentStage = DevelopmentDatabaseSmokeStage.Cleanup; await _cleanup(CancellationToken.None).ConfigureAwait(false); currentStage = DevelopmentDatabaseSmokeStage.VerifyAbsent; await _verifyAbsent(CancellationToken.None).ConfigureAwait(false); cleanupVerified = true; } catch (Exception exception) { primaryFailure = exception; failureStage = currentStage; failureClass = RedactedDatabaseFailureClassifier.Classify(exception); outcomeUnknown = _isOutcomeUnknown(exception); } } } if (primaryFailure is null && cleanupVerified) { return new DevelopmentDatabaseSmokeResult( Name, Passed: true, DevelopmentDatabaseSmokeStage.None, "none", OutcomeUnknown: false, CleanupAttempted: true, CleanupVerified: true, ManualCleanupMayBeRequired: false); } return new DevelopmentDatabaseSmokeResult( Name, Passed: false, failureStage, failureClass, outcomeUnknown, cleanupAttempted, cleanupVerified, ManualCleanupMayBeRequired: outcomeUnknown && (createKnownCommitted || failureStage == DevelopmentDatabaseSmokeStage.Create)); } }