170 lines
6.3 KiB
C#
170 lines
6.3 KiB
C#
#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);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal sealed class DevelopmentDatabaseSmokeScenario
|
|
{
|
|
private readonly Func<CancellationToken, Task> _preflight;
|
|
private readonly Func<CancellationToken, Task> _create;
|
|
private readonly Func<CancellationToken, Task> _verifyCreate;
|
|
private readonly Func<CancellationToken, Task>? _save;
|
|
private readonly Func<CancellationToken, Task>? _verifySave;
|
|
private readonly Func<CancellationToken, Task> _cleanup;
|
|
private readonly Func<CancellationToken, Task> _verifyAbsent;
|
|
private readonly Func<Exception, bool> _isOutcomeUnknown;
|
|
|
|
internal DevelopmentDatabaseSmokeScenario(
|
|
string name,
|
|
Func<CancellationToken, Task> preflight,
|
|
Func<CancellationToken, Task> create,
|
|
Func<CancellationToken, Task> verifyCreate,
|
|
Func<CancellationToken, Task>? save,
|
|
Func<CancellationToken, Task>? verifySave,
|
|
Func<CancellationToken, Task> cleanup,
|
|
Func<CancellationToken, Task> verifyAbsent,
|
|
Func<Exception, bool> 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<DevelopmentDatabaseSmokeResult> 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));
|
|
}
|
|
}
|