using System.Data;
using System.Data.Common;
namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Browser.Tests;
///
/// Records how many command executions are ever in flight at once, and how long each one is artificially
/// held open. Shared by and the commands it creates.
///
///
/// This exists because "the session serializes on a gate" is otherwise unfalsifiable from the outside:
/// against a real SQLite connection, overlapping calls mostly still *work*, so a test that only asserted
/// correct results would pass with the gate deleted. Measuring the overlap directly is what makes the
/// gate's removal visible.
///
public sealed class ConcurrencyProbe
{
private int _inFlight;
private int _peak;
/// How long each command execution is held before it reaches the real provider.
public TimeSpan Delay { get; init; } = TimeSpan.FromMilliseconds(120);
/// The greatest number of executions observed in flight simultaneously.
public int Peak => Volatile.Read(ref _peak);
/// Marks the start of one command execution.
public void Enter()
{
var current = Interlocked.Increment(ref _inFlight);
int observed;
while (current > (observed = Volatile.Read(ref _peak)))
{
if (Interlocked.CompareExchange(ref _peak, current, observed) == observed) break;
}
}
/// Marks the end of one command execution.
public void Exit() => Interlocked.Decrement(ref _inFlight);
}
///
/// A pass-through that hands out commands instrumented with a
/// . Everything else delegates to the wrapped connection, so the code under
/// test runs against a real SQLite catalog.
///
/// The real connection to delegate to. Not owned — the caller disposes it.
/// The probe that observes command overlap.
public sealed class ConcurrencyProbingDbConnection(DbConnection inner, ConcurrencyProbe probe) : DbConnection
{
///
[System.Diagnostics.CodeAnalysis.AllowNull]
public override string ConnectionString
{
get => inner.ConnectionString;
set => inner.ConnectionString = value!;
}
///
public override string Database => inner.Database;
///
public override string DataSource => inner.DataSource;
///
public override string ServerVersion => inner.ServerVersion;
///
public override ConnectionState State => inner.State;
///
public override void ChangeDatabase(string databaseName) => inner.ChangeDatabase(databaseName);
///
public override void Close() => inner.Close();
///
public override void Open() => inner.Open();
///
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) =>
inner.BeginTransaction(isolationLevel);
///
protected override DbCommand CreateDbCommand() =>
new ConcurrencyProbingDbCommand(inner.CreateCommand(), this, probe);
}
///
/// A pass-through that reports every execution to a
/// and holds it open for the probe's delay, so overlapping executions are observable.
///
/// The real command to delegate to.
/// The connection that created this command.
/// The probe that observes command overlap.
public sealed class ConcurrencyProbingDbCommand(DbCommand inner, DbConnection owner, ConcurrencyProbe probe)
: DbCommand
{
///
[System.Diagnostics.CodeAnalysis.AllowNull]
public override string CommandText
{
get => inner.CommandText;
set => inner.CommandText = value!;
}
///
public override int CommandTimeout
{
get => inner.CommandTimeout;
set => inner.CommandTimeout = value;
}
///
public override CommandType CommandType
{
get => inner.CommandType;
set => inner.CommandType = value;
}
///
public override bool DesignTimeVisible
{
get => inner.DesignTimeVisible;
set => inner.DesignTimeVisible = value;
}
///
public override UpdateRowSource UpdatedRowSource
{
get => inner.UpdatedRowSource;
set => inner.UpdatedRowSource = value;
}
///
protected override DbConnection? DbConnection
{
get => owner;
set { /* The session never reassigns the connection; the wrapped command keeps its own. */ }
}
///
protected override DbParameterCollection DbParameterCollection => inner.Parameters;
///
protected override DbTransaction? DbTransaction
{
get => inner.Transaction;
set => inner.Transaction = value;
}
///
public override void Cancel() => inner.Cancel();
///
public override int ExecuteNonQuery() => inner.ExecuteNonQuery();
///
public override object? ExecuteScalar() => inner.ExecuteScalar();
///
public override void Prepare() => inner.Prepare();
///
protected override DbParameter CreateDbParameter() => inner.CreateParameter();
///
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
{
probe.Enter();
try
{
Thread.Sleep(probe.Delay);
return inner.ExecuteReader(behavior);
}
finally
{
probe.Exit();
}
}
///
protected override async Task ExecuteDbDataReaderAsync(
CommandBehavior behavior, CancellationToken cancellationToken)
{
probe.Enter();
try
{
await Task.Delay(probe.Delay, cancellationToken).ConfigureAwait(false);
return await inner.ExecuteReaderAsync(behavior, cancellationToken).ConfigureAwait(false);
}
finally
{
probe.Exit();
}
}
///
protected override void Dispose(bool disposing)
{
if (disposing) inner.Dispose();
base.Dispose(disposing);
}
}