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