using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness;
///
/// An in-memory ConfigDb that behaves like SQL Server where the InMemory provider does not (lmxopcua#868 third
/// review): a FailureReason longer than the column refuses the whole SaveChanges with a
/// over SQL error 2628. It can also reject (,
/// error 547) or fail transiently (, deadlock 1205) every update of
/// an existing NodeDeploymentState row — each a real SqlException inside EF's
/// , the shape SQL Server produces (fourth review M1).
///
public sealed class ConstraintEnforcingDbFactory(string dbName) : IDbContextFactory
{
private readonly Interceptor _interceptor = new();
/// Every update of an existing node row is rejected as a constraint violation.
public bool PoisonNodeStateUpdates { get => _interceptor.Poison; set => _interceptor.Poison = value; }
/// Every update of an existing node row fails as if the connection dropped.
public bool FailNodeStateUpdatesTransiently { get => _interceptor.Transient; set => _interceptor.Transient = value; }
private int _failNextContexts;
/// The next context creations throw (an unreachable server).
public void FailNextContexts(int count) => Volatile.Write(ref _failNextContexts, count);
///
public OtOpcUaConfigDbContext CreateDbContext() =>
Volatile.Read(ref _failNextContexts) > 0 && Interlocked.Decrement(ref _failNextContexts) >= 0
? throw new InvalidOperationException("ConfigDb unreachable (simulated)")
: Create();
private OtOpcUaConfigDbContext Create() => new(new DbContextOptionsBuilder()
.UseInMemoryDatabase(dbName)
.AddInterceptors(_interceptor)
.Options);
private sealed class Interceptor : SaveChangesInterceptor
{
public volatile bool Poison;
public volatile bool Transient;
public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result)
{
Check(eventData.Context!);
return result;
}
public override ValueTask> SavingChangesAsync(
DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default)
{
Check(eventData.Context!);
return ValueTask.FromResult(result);
}
private void Check(DbContext ctx)
{
foreach (var e in ctx.ChangeTracker.Entries())
{
var reason = e.Entity switch
{
NodeDeploymentState n => n.FailureReason,
Deployment d => d.FailureReason,
_ => null,
};
if (reason is { Length: > FailureReasons.MaxLength })
throw new DbUpdateException("An error occurred while saving the entity changes.",
SqlExceptionFactory.Create(2628, "String or binary data would be truncated in column 'FailureReason'."));
if (e.Entity is NodeDeploymentState && e.State == EntityState.Modified)
{
if (Poison)
throw new DbUpdateException("An error occurred while saving the entity changes.",
SqlExceptionFactory.Create(547, "The UPDATE statement conflicted with a CHECK constraint."));
if (Transient)
throw new DbUpdateException("An error occurred while saving the entity changes.",
SqlExceptionFactory.Create(1205, "Transaction was deadlocked on lock resources and has been chosen as the deadlock victim."));
}
}
}
}
}