Skip to content

Commit 76ab43d

Browse files
committed
Tests: (JSON-first disposal, concurrency, ADO wrappers, math invariants, redactor order, JSON meta) in KeelMatrix.QueryWatch.Tests
1 parent be4080f commit 76ab43d

File tree

10 files changed

+277
-36
lines changed

10 files changed

+277
-36
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using System.Data;
3+
using System.Data.Common;
4+
using FluentAssertions;
5+
using KeelMatrix.QueryWatch.Ado;
6+
using Xunit;
7+
8+
namespace KeelMatrix.QueryWatch.Tests {
9+
public class AdoWrapperTests {
10+
[Fact]
11+
public void QueryWatchCommand_ExecuteNonQuery_Records_One_Event_With_Text() {
12+
using var session = KeelMatrix.QueryWatch.QueryWatcher.Start();
13+
var inner = new FakeDbCommand { CommandText = "SELECT 1" };
14+
using var cmd = new QueryWatchCommand(inner, session);
15+
16+
var result = cmd.ExecuteNonQuery();
17+
result.Should().Be(1);
18+
19+
var report = session.Stop();
20+
report.TotalQueries.Should().Be(1);
21+
report.Events[0].CommandText.Should().Be("SELECT 1");
22+
}
23+
24+
[Fact]
25+
public void QueryWatchConnection_CreateDbCommand_Wraps_Inner_Command() {
26+
using var session = KeelMatrix.QueryWatch.QueryWatcher.Start();
27+
using var innerConn = new FakeDbConnection();
28+
using var wrapped = new QueryWatchConnection(innerConn, session);
29+
30+
using var cmd = wrapped.CreateCommand();
31+
cmd.Should().BeOfType<QueryWatchCommand>();
32+
33+
cmd.CommandText = "UPDATE X";
34+
cmd.ExecuteNonQuery();
35+
36+
var report = session.Stop();
37+
report.TotalQueries.Should().Be(1);
38+
report.Events[0].CommandText.Should().Be("UPDATE X");
39+
}
40+
41+
private sealed class FakeDbConnection : DbConnection {
42+
public override string ConnectionString { get; set; } = "";
43+
public override string Database => "FakeDb";
44+
public override string DataSource => "Fake";
45+
public override string ServerVersion => "1.0";
46+
public override ConnectionState State => ConnectionState.Open;
47+
public override void ChangeDatabase(string databaseName) { }
48+
public override void Close() { }
49+
public override void Open() { }
50+
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => throw new NotSupportedException();
51+
protected override DbCommand CreateDbCommand() => new FakeDbCommand();
52+
}
53+
54+
private sealed class FakeDbCommand : DbCommand {
55+
private string _commandText = string.Empty;
56+
public override string CommandText { get => _commandText; set => _commandText = value ?? string.Empty; }
57+
public override int CommandTimeout { get; set; }
58+
public override CommandType CommandType { get; set; } = CommandType.Text;
59+
public override UpdateRowSource UpdatedRowSource { get; set; } = UpdateRowSource.None;
60+
61+
protected override DbConnection? DbConnection { get; set; }
62+
protected override DbParameterCollection DbParameterCollection { get; } = new FakeDbParameterCollection();
63+
protected override DbTransaction? DbTransaction { get; set; }
64+
public override bool DesignTimeVisible { get; set; }
65+
66+
public override void Cancel() { }
67+
public override int ExecuteNonQuery() => 1;
68+
public override object? ExecuteScalar() => 42;
69+
public override void Prepare() { }
70+
71+
protected override DbParameter CreateDbParameter() => new FakeDbParameter();
72+
protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => throw new NotSupportedException();
73+
74+
private sealed class FakeDbParameterCollection : DbParameterCollection {
75+
private readonly System.Collections.ArrayList _list = new();
76+
public override int Add(object value) { _list.Add(value); return _list.Count - 1; }
77+
public override void AddRange(Array values) { foreach (var v in values) _list.Add(v); }
78+
public override void Clear() => _list.Clear();
79+
public override bool Contains(object value) => _list.Contains(value);
80+
public override bool Contains(string value) => IndexOf(value) >= 0;
81+
public override void CopyTo(Array array, int index) => _list.CopyTo(array, index);
82+
public override int Count => _list.Count;
83+
public override System.Collections.IEnumerator GetEnumerator() => _list.GetEnumerator();
84+
protected override DbParameter GetParameter(int index) => (DbParameter)_list[index]!;
85+
protected override DbParameter GetParameter(string parameterName) => throw new NotSupportedException();
86+
public override int IndexOf(object value) => _list.IndexOf(value);
87+
public override int IndexOf(string parameterName) => -1;
88+
public override void Insert(int index, object value) => _list.Insert(index, value);
89+
public override bool IsFixedSize => false;
90+
public override bool IsReadOnly => false;
91+
public override bool IsSynchronized => false;
92+
public override void Remove(object value) => _list.Remove(value);
93+
public override void RemoveAt(int index) => _list.RemoveAt(index);
94+
public override void RemoveAt(string parameterName) => throw new NotSupportedException();
95+
protected override void SetParameter(int index, DbParameter value) => _list[index] = value;
96+
protected override void SetParameter(string parameterName, DbParameter value) => throw new NotSupportedException();
97+
public override object SyncRoot => this;
98+
}
99+
100+
private sealed class FakeDbParameter : DbParameter {
101+
public override DbType DbType { get; set; }
102+
public override ParameterDirection Direction { get; set; } = ParameterDirection.Input;
103+
public override bool IsNullable { get; set; }
104+
public override string ParameterName { get; set; } = "";
105+
public override string SourceColumn { get; set; } = "";
106+
public override object? Value { get; set; }
107+
public override bool SourceColumnNullMapping { get; set; }
108+
public override int Size { get; set; }
109+
public override void ResetDbType() { }
110+
}
111+
}
112+
}
113+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
using FluentAssertions;
3+
using KeelMatrix.QueryWatch.Reporting;
4+
using Xunit;
5+
6+
namespace KeelMatrix.QueryWatch.Tests {
7+
public class QueryWatchJsonMetaTests {
8+
[Fact]
9+
public void ToSummary_Includes_Library_Meta() {
10+
using var session = KeelMatrix.QueryWatch.QueryWatcher.Start();
11+
session.Record("x", TimeSpan.FromMilliseconds(1));
12+
var report = session.Stop();
13+
14+
var s = QueryWatchJson.ToSummary(report, sampleTop: 1);
15+
s.Meta.Should().ContainKey("library").WhoseValue.Should().Be("KeelMatrix.QueryWatch");
16+
}
17+
}
18+
}

tests/KeelMatrix.QueryWatch.Tests/QueryWatchJsonTests.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,13 @@
77
using KeelMatrix.QueryWatch.Reporting;
88
using Xunit;
99

10-
namespace KeelMatrix.QueryWatch.Tests
11-
{
12-
public class QueryWatchJsonTests
13-
{
10+
namespace KeelMatrix.QueryWatch.Tests {
11+
public class QueryWatchJsonTests {
1412
// TODO: REMOVE LATER. These tests lock down the JSON "shape" that CI relies on.
1513
// We purposely avoid overfitting to timestamps and only assert stable fields and sampling behavior.
1614

1715
[Fact]
18-
public void ToSummary_Respects_SampleTop_And_Sorts_Descending()
19-
{
16+
public void ToSummary_Respects_SampleTop_And_Sorts_Descending() {
2017
using var session = QueryWatcher.Start();
2118
session.Record("fast", TimeSpan.FromMilliseconds(5));
2219
session.Record("slow", TimeSpan.FromMilliseconds(12));
@@ -33,8 +30,7 @@ public void ToSummary_Respects_SampleTop_And_Sorts_Descending()
3330
}
3431

3532
[Fact]
36-
public void ExportToFile_Writes_File_With_Meta_SampleTop_And_Creates_Directory()
37-
{
33+
public void ExportToFile_Writes_File_With_Meta_SampleTop_And_Creates_Directory() {
3834
using var session = QueryWatcher.Start();
3935
session.Record("a", TimeSpan.FromMilliseconds(3));
4036
session.Record("b", TimeSpan.FromMilliseconds(9));
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using FluentAssertions;
3+
using Xunit;
4+
5+
namespace KeelMatrix.QueryWatch.Tests {
6+
public class QueryWatchRedactorOrderTests {
7+
private sealed class ReplaceFooWithBar : KeelMatrix.QueryWatch.IQueryTextRedactor {
8+
public string Redact(string input) => (input ?? string.Empty).Replace("foo", "bar");
9+
}
10+
11+
private sealed class ReplaceBarWithBaz : KeelMatrix.QueryWatch.IQueryTextRedactor {
12+
public string Redact(string input) => (input ?? string.Empty).Replace("bar", "baz");
13+
}
14+
15+
[Fact]
16+
public void Redactors_Are_Applied_In_Order() {
17+
var options = new KeelMatrix.QueryWatch.QueryWatchOptions { CaptureSqlText = true };
18+
options.Redactors.Add(new ReplaceFooWithBar());
19+
options.Redactors.Add(new ReplaceBarWithBaz());
20+
21+
using var session = KeelMatrix.QueryWatch.QueryWatcher.Start(options);
22+
session.Record("select * from foo", TimeSpan.FromMilliseconds(1));
23+
var report = session.Stop();
24+
25+
report.Events[0].CommandText.Should().Contain("baz");
26+
report.Events[0].CommandText.Should().NotContain("foo");
27+
report.Events[0].CommandText.Should().NotContain("bar");
28+
}
29+
}
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using FluentAssertions;
3+
using Xunit;
4+
5+
namespace KeelMatrix.QueryWatch.Tests {
6+
public class QueryWatchReportMathTests {
7+
[Fact]
8+
public void AverageDuration_Is_Calculated_From_TotalMs_Over_Count() {
9+
using var session = KeelMatrix.QueryWatch.QueryWatcher.Start();
10+
session.Record("a", TimeSpan.FromMilliseconds(1));
11+
session.Record("b", TimeSpan.FromMilliseconds(2));
12+
var r = session.Stop();
13+
14+
r.AverageDuration.TotalMilliseconds.Should().BeApproximately(1.5, 0.1);
15+
r.TotalDuration.TotalMilliseconds.Should().BeApproximately(3.0, 0.1);
16+
}
17+
18+
[Fact]
19+
public void AverageDuration_Is_Zero_For_No_Events() {
20+
using var session = KeelMatrix.QueryWatch.QueryWatcher.Start();
21+
var r = session.Stop();
22+
r.TotalQueries.Should().Be(0);
23+
r.AverageDuration.Should().Be(TimeSpan.Zero);
24+
r.TotalDuration.Should().Be(TimeSpan.Zero);
25+
}
26+
}
27+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Text.Json;
5+
using FluentAssertions;
6+
using KeelMatrix.QueryWatch.Reporting;
7+
using KeelMatrix.QueryWatch.Testing;
8+
using Xunit;
9+
10+
namespace KeelMatrix.QueryWatch.Tests {
11+
public class QueryWatchScopeJsonFirstTests {
12+
[Fact]
13+
public void Dispose_Writes_JSON_Even_When_Thresholds_Violated() {
14+
var root = Path.Combine(Path.GetTempPath(), "QueryWatchTests", Guid.NewGuid().ToString("N"));
15+
var path = Path.Combine(root, "artifacts", "qwatch.report.json");
16+
17+
Action act = () => {
18+
using var scope = QueryWatchScope.Start(
19+
maxQueries: 1,
20+
exportJsonPath: path,
21+
sampleTop: 2);
22+
23+
scope.Session.Record("A", TimeSpan.FromMilliseconds(3));
24+
scope.Session.Record("B", TimeSpan.FromMilliseconds(4));
25+
};
26+
27+
act.Should().Throw<KeelMatrix.QueryWatch.QueryWatchViolationException>();
28+
29+
File.Exists(path).Should().BeTrue("JSON must be exported before asserting budgets");
30+
var json = File.ReadAllText(path);
31+
var summary = JsonSerializer.Deserialize<QueryWatchJson.Summary>(json);
32+
summary.Should().NotBeNull();
33+
summary!.Schema.Should().Be(QueryWatchJson.SchemaVersion);
34+
summary.Meta.Should().ContainKey("sampleTop").WhoseValue.Should().Be("2");
35+
summary.Events.Count.Should().BeLessOrEqualTo(2);
36+
}
37+
}
38+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using Xunit;
6+
7+
namespace KeelMatrix.QueryWatch.Tests {
8+
public class QueryWatchSessionConcurrencyTests {
9+
[Fact]
10+
public async Task Record_Is_ThreadSafe_For_Many_Writers() {
11+
using var session = KeelMatrix.QueryWatch.QueryWatcher.Start();
12+
var tasks = Enumerable.Range(0, 100)
13+
.Select(i => Task.Run(() => session.Record("Q" + i, TimeSpan.FromMilliseconds(1))))
14+
.ToArray();
15+
await Task.WhenAll(tasks);
16+
17+
var report = session.Stop();
18+
report.TotalQueries.Should().Be(100);
19+
report.AverageDuration.Should().BeCloseTo(TimeSpan.FromMilliseconds(1), precision: TimeSpan.FromMilliseconds(1));
20+
}
21+
22+
[Fact]
23+
public void Dispose_Then_Record_Should_Throw_ObjectDisposed() {
24+
var session = KeelMatrix.QueryWatch.QueryWatcher.Start();
25+
session.Dispose();
26+
27+
Action act = () => session.Record("SELECT 1", TimeSpan.FromMilliseconds(1));
28+
act.Should().Throw<ObjectDisposedException>();
29+
}
30+
}
31+
}

tests/KeelMatrix.QueryWatch.Tests/ReportOptionViolationsTests.cs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
using KeelMatrix.QueryWatch;
44
using Xunit;
55

6-
namespace KeelMatrix.QueryWatch.Tests
7-
{
8-
public class ReportOptionViolationsTests
9-
{
6+
namespace KeelMatrix.QueryWatch.Tests {
7+
public class ReportOptionViolationsTests {
108
[Fact]
11-
public void ThrowIfViolations_Respects_MaxQueries()
12-
{
9+
public void ThrowIfViolations_Respects_MaxQueries() {
1310
var options = new QueryWatchOptions { MaxQueries = 1 };
1411
using var session = QueryWatcher.Start(options);
1512
session.Record("SELECT 1", TimeSpan.FromMilliseconds(1));
@@ -23,8 +20,7 @@ public void ThrowIfViolations_Respects_MaxQueries()
2320
}
2421

2522
[Fact]
26-
public void ThrowIfViolations_Respects_MaxAverageDuration()
27-
{
23+
public void ThrowIfViolations_Respects_MaxAverageDuration() {
2824
var options = new QueryWatchOptions { MaxAverageDuration = TimeSpan.FromMilliseconds(5) };
2925
using var session = QueryWatcher.Start(options);
3026
// Avg = (8 + 4) / 2 = 6 ms > 5 ms
@@ -39,8 +35,7 @@ public void ThrowIfViolations_Respects_MaxAverageDuration()
3935
}
4036

4137
[Fact]
42-
public void ThrowIfViolations_Respects_MaxTotalDuration()
43-
{
38+
public void ThrowIfViolations_Respects_MaxTotalDuration() {
4439
var options = new QueryWatchOptions { MaxTotalDuration = TimeSpan.FromMilliseconds(5) };
4540
using var session = QueryWatcher.Start(options);
4641
session.Record("SELECT 1", TimeSpan.FromMilliseconds(4));
@@ -54,10 +49,8 @@ public void ThrowIfViolations_Respects_MaxTotalDuration()
5449
}
5550

5651
[Fact]
57-
public void ThrowIfViolations_NoViolations_DoesNotThrow()
58-
{
59-
var options = new QueryWatchOptions
60-
{
52+
public void ThrowIfViolations_NoViolations_DoesNotThrow() {
53+
var options = new QueryWatchOptions {
6154
MaxQueries = 5,
6255
MaxAverageDuration = TimeSpan.FromMilliseconds(10),
6356
MaxTotalDuration = TimeSpan.FromMilliseconds(100)
@@ -73,8 +66,7 @@ public void ThrowIfViolations_NoViolations_DoesNotThrow()
7366
}
7467

7568
[Fact]
76-
public void Helper_Asserts_DoNotThrow_When_UnderLimits()
77-
{
69+
public void Helper_Asserts_DoNotThrow_When_UnderLimits() {
7870
using var session = QueryWatcher.Start();
7971
session.Record("SELECT 1", TimeSpan.FromMilliseconds(2));
8072
session.Record("SELECT 2", TimeSpan.FromMilliseconds(2));
@@ -86,8 +78,7 @@ public void Helper_Asserts_DoNotThrow_When_UnderLimits()
8678
}
8779

8880
[Fact]
89-
public void Helper_Asserts_Throw_When_OverLimits()
90-
{
81+
public void Helper_Asserts_Throw_When_OverLimits() {
9182
using var session = QueryWatcher.Start();
9283
session.Record("SELECT 1", TimeSpan.FromMilliseconds(10));
9384
session.Record("SELECT 2", TimeSpan.FromMilliseconds(10));

tests/KeelMatrix.QueryWatch.Tests/SessionRecordGuardTests.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@
33
using KeelMatrix.QueryWatch;
44
using Xunit;
55

6-
namespace KeelMatrix.QueryWatch.Tests
7-
{
8-
public class SessionRecordGuardTests
9-
{
6+
namespace KeelMatrix.QueryWatch.Tests {
7+
public class SessionRecordGuardTests {
108
[Fact]
11-
public void Record_After_Stop_Throws()
12-
{
9+
public void Record_After_Stop_Throws() {
1310
using var session = QueryWatcher.Start();
1411
var report = session.Stop();
1512
Action act = () => session.Record("SELECT 1", TimeSpan.FromMilliseconds(1));
@@ -19,8 +16,7 @@ public void Record_After_Stop_Throws()
1916
}
2017

2118
[Fact]
22-
public void Record_When_CaptureSqlText_False_Stores_Empty_Text()
23-
{
19+
public void Record_When_CaptureSqlText_False_Stores_Empty_Text() {
2420
var options = new QueryWatchOptions { CaptureSqlText = false };
2521
using var session = QueryWatcher.Start(options);
2622
session.Record("SELECT secret FROM t", TimeSpan.FromMilliseconds(2));

0 commit comments

Comments
 (0)