#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));
}
}