Skip to content

Commit 3b8b148

Browse files
committed
Testing: watch buggify on by default in the repo's own harness backends (BG-2)
The library default stays clean (buggify-off is the emulator's shipped behavior, since it is public and that decision belongs to each downstream consumer). One level up, where we own the tests, the harness backends turn on seeded watch chaos by default so every watch-arming suite exercises the weak watch contract (a watch may fire spuriously, and a net-reverted change may never fire) instead of the emulator's deterministic-but-unrealistic clean watches. Each store gets a per-test-name seed, so a run is reproducible and a failure replays. - FakeDbStore.Buggify.EnableChaos(name, ...): the one-line, per-suite opt-in the design promised for SDK-level consumers. Derives a process-stable seed from the name (FNV-1a, not the per-run-randomized string.GetHashCode) and installs a chaos profile. - Harness backends flipped chaos-on: FakeDbTestBackend (the dual-backend layer-suite backend) and the Docker-free conformance heads (Transaction, Smoke, RangeQuery, Retryable). Chaos is a no-op for any suite that arms no watches (nothing to fire, no checks to defer), so this is behaviorally inert everywhere except the transaction conformance watch facts. - The scenario family (generator/trace/replay/dual-live/fuzz) is left buggify-OFF: it is a trace-comparison / exact-semantics engine, and the real-vs-FakeDb dual-live and FdbLite-vs-FakeDb emulator differentials must stay head-for-head faithful (BG-4). Injected glitches are designed divergence; a buggify-on differential would only re-prove the injection the injection facts already prove directly. - The watch conformance facts that assert exact fire/pending timing opt out with one line - RequireCleanWatches() - which documents "this test needs clean watches" at the site. Two facts actually failed under their seed (spurious-fired a should-be-pending watch); the rest assert exact timing too and are annotated for robustness against a future seed/rate change. Migration outcome: full FakeDb suite green (0 failed), the scenario dual-live/trace heads green with buggify off.
1 parent 6d94d14 commit 3b8b148

8 files changed

Lines changed: 120 additions & 11 deletions

File tree

FoundationDB.FakeDb.Tests/Conformance/RangeQueryConformanceFacts.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1668,14 +1668,14 @@ public class RangeQueryFakeDbFacts : RangeQueryConformanceFacts
16681668
protected override Task<IFdbDatabase> OpenTestDatabaseAsync(bool readOnly = false)
16691669
{
16701670
// mirror FdbTest.OpenTestDatabaseAsync: the real-cluster head seeds a 15s default timeout via FdbConnectionOptions.DefaultTimeout
1671-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(FdbPath.Root, readOnly);
1671+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(FdbPath.Root, readOnly);
16721672
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
16731673
return Task.FromResult<IFdbDatabase>(db);
16741674
}
16751675

16761676
protected override Task<IFdbDatabase> OpenTestPartitionAsync(string? testMethod = null)
16771677
{
1678-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
1678+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
16791679
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
16801680
return Task.FromResult<IFdbDatabase>(db);
16811681
}

FoundationDB.FakeDb.Tests/Conformance/RetryableConformanceFacts.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,14 +458,14 @@ public class RetryableFakeDbFacts : RetryableConformanceFacts
458458
protected override Task<IFdbDatabase> OpenTestDatabaseAsync(bool readOnly = false)
459459
{
460460
// mirror FdbTest.OpenTestDatabaseAsync: the real-cluster head seeds a 15s default timeout via FdbConnectionOptions.DefaultTimeout
461-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(FdbPath.Root, readOnly);
461+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(FdbPath.Root, readOnly);
462462
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
463463
return Task.FromResult<IFdbDatabase>(db);
464464
}
465465

466466
protected override Task<IFdbDatabase> OpenTestPartitionAsync(string? testMethod = null)
467467
{
468-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
468+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
469469
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
470470
return Task.FromResult<IFdbDatabase>(db);
471471
}

FoundationDB.FakeDb.Tests/Conformance/SmokeConformanceFacts.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,14 @@ public class SmokeFakeDbFacts : SmokeConformanceFacts
115115

116116
protected override Task<IFdbDatabase> OpenTestDatabaseAsync(bool readOnly = false)
117117
{
118-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(FdbPath.Root, readOnly);
118+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(FdbPath.Root, readOnly);
119119
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
120120
return Task.FromResult<IFdbDatabase>(db);
121121
}
122122

123123
protected override Task<IFdbDatabase> OpenTestPartitionAsync(string? testMethod = null)
124124
{
125-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
125+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
126126
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
127127
return Task.FromResult<IFdbDatabase>(db);
128128
}

FoundationDB.FakeDb.Tests/Conformance/TransactionConformanceFacts.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ namespace FoundationDB.Client.Tests
4545
public abstract class TransactionConformanceFacts : FdbTest
4646
{
4747

48+
/// <summary>Opts this test out of watch buggify so it sees clean, deterministic watch semantics (no-op on a real cluster; on the
49+
/// FakeDb head it disables the seeded chaos the harness turns on by default). Call at the top of a test that asserts exact watch timing.</summary>
50+
protected virtual void RequireCleanWatches() { }
51+
4852
[Test]
4953
public async Task Test_Can_Create_And_Dispose_Transactions()
5054
{
@@ -3447,6 +3451,8 @@ await AssertThrowsFdbErrorAsync(
34473451
[CoversCells("watches/no-fire-single-commit-aba")]
34483452
public async Task Test_Watch_Registration_Compares_Endpoint_Values()
34493453
{
3454+
RequireCleanWatches(); // clean watches: this test asserts exact fire/pending semantics
3455+
34503456
// the pre-arm window is judged by VALUES at the endpoints, not by version history: a key that changed
34513457
// and reverted across two commits between the watching transaction's read version and its arming
34523458
// commit registers as still-pending (oracle-pinned; contrast with the POST-arm two-commit case, where
@@ -3507,6 +3513,7 @@ public async Task Test_Same_Key_Watches_Fire_Together()
35073513
[Test]
35083514
public async Task Test_Can_Setup_And_Cancel_Watches()
35093515
{
3516+
RequireCleanWatches(); // clean watches: this test asserts exact fire/pending semantics
35103517
using var db = await OpenTestPartitionAsync();
35113518
await CleanLocation(db);
35123519

@@ -3597,6 +3604,7 @@ public async Task Test_Cannot_Use_Transaction_CancellationToken_With_Watch()
35973604
[Test]
35983605
public async Task Test_Setting_Key_To_Same_Value_Should_Not_Trigger_Watch()
35993606
{
3607+
RequireCleanWatches(); // clean watches: this test asserts exact fire/pending semantics
36003608
using var db = await OpenTestPartitionAsync();
36013609
await CleanLocation(db);
36023610

@@ -3658,6 +3666,8 @@ await db.WriteAsync(async tr =>
36583666
[Test]
36593667
public async Task Test_Watched_Key_Changed_By_Same_Transaction_Before_Commit_Should_Trigger_Watch()
36603668
{
3669+
RequireCleanWatches(); // clean watches: this test asserts exact fire/pending semantics
3670+
36613671
// Steps:
36623672
// - T1: set a watch on a key, but does not commit yet
36633673
// - T1: change the value of the watched key
@@ -3707,6 +3717,8 @@ await db.WriteAsync(async tr =>
37073717
[Test]
37083718
public async Task Test_Concurrent_Change_To_Watched_Key_Before_Commit_Should_Still_Trigger_Watch()
37093719
{
3720+
RequireCleanWatches(); // clean watches: this test asserts exact fire/pending semantics
3721+
37103722
// Steps:
37113723
// - T1: set a watch on a key, but do not commit yet
37123724
// - T2: update the watched key and commit before T1
@@ -3765,6 +3777,7 @@ public async Task Test_Can_Cancel_Awaited_Watch_With_CancellationToken()
37653777
{
37663778
// Test that calling watch.WaitAsync(CancellationToken) will throw if the token is triggered before the watch fires
37673779

3780+
RequireCleanWatches(); // clean watches: this test asserts the watch stays pending until cancelled
37683781
using var db = await OpenTestPartitionAsync();
37693782
await CleanLocation(db);
37703783

@@ -3816,6 +3829,7 @@ public async Task Test_Can_Cancel_Awaited_Watch_After_Timeout()
38163829
{
38173830
// Test that calling watch.WaitAsync(TimeSpan, CancellationToken) will throw if the timeout expires before the watch fires
38183831

3832+
RequireCleanWatches(); // clean watches: this test asserts the watch stays pending until the timeout
38193833
using var db = await OpenTestPartitionAsync();
38203834
await CleanLocation(db);
38213835

FoundationDB.FakeDb.Tests/Conformance/TransactionConformanceHeads.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,27 @@ namespace FoundationDB.Client.Tests
3333
public class TransactionFakeDbFacts : TransactionConformanceFacts
3434
{
3535

36-
/// <summary>Store shared by all databases opened during a single test, reset between tests.</summary>
36+
/// <summary>Store shared by all databases opened during a single test, reset between tests. Watch buggify is on by default (see <see cref="TestBuggify"/>); a test needing clean watches calls <see cref="TransactionConformanceFacts.RequireCleanWatches"/>.</summary>
3737
private FakeDbStore? Store { get; set; }
3838

3939
protected override bool UseRealServer => false;
4040

4141
[TearDown]
4242
public void ResetFakeDbStore() => this.Store = null;
4343

44+
protected override void RequireCleanWatches() => (this.Store ??= TestBuggify.ChaosStore()).Buggify.Disable();
45+
4446
protected override Task<IFdbDatabase> OpenTestDatabaseAsync(bool readOnly = false)
4547
{
4648
// mirror FdbTest.OpenTestDatabaseAsync: the real-cluster head seeds a 15s default timeout via FdbConnectionOptions.DefaultTimeout
47-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(FdbPath.Root, readOnly);
49+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(FdbPath.Root, readOnly);
4850
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
4951
return Task.FromResult<IFdbDatabase>(db);
5052
}
5153

5254
protected override Task<IFdbDatabase> OpenTestPartitionAsync(string? testMethod = null)
5355
{
54-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
56+
var db = (this.Store ??= TestBuggify.ChaosStore()).OpenDatabase(GetTestPartitionPath(testMethod), readOnly: false);
5557
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
5658
return Task.FromResult<IFdbDatabase>(db);
5759
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#region Copyright (c) 2023-2026 SnowBank SAS, (c) 2005-2023 Doxense SAS
2+
// All rights reserved.
3+
//
4+
// Redistribution and use in source and binary forms, with or without
5+
// modification, are permitted provided that the following conditions are met:
6+
// * Redistributions of source code must retain the above copyright
7+
// notice, this list of conditions and the following disclaimer.
8+
// * Redistributions in binary form must reproduce the above copyright
9+
// notice, this list of conditions and the following disclaimer in the
10+
// documentation and/or other materials provided with the distribution.
11+
// * Neither the name of SnowBank nor the
12+
// names of its contributors may be used to endorse or promote products
13+
// derived from this software without specific prior written permission.
14+
//
15+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16+
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17+
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
// DISCLAIMED. IN NO EVENT SHALL SNOWBANK SAS BE LIABLE FOR ANY
19+
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20+
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21+
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22+
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24+
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
#endregion
26+
27+
namespace FoundationDB.Client.Tests
28+
{
29+
using FoundationDB.Testing;
30+
31+
/// <summary>Repo test-harness policy for FakeDb watch buggify: the repo's own harness backends are watch-realistic BY DEFAULT.</summary>
32+
/// <remarks>
33+
/// <para>The library default is buggify-off (that decision belongs to each downstream consumer, since the emulator is public), but
34+
/// one level up - where we own the tests - the harness backends enable seeded chaos by default so every watch-arming suite exercises
35+
/// the weak watch contract (a watch may fire spuriously, and a net-reverted change may never fire). Each test gets a stable,
36+
/// per-test-name seed, so a run is reproducible and a failure replays.</para>
37+
/// <para>A test that asserts exact, clean watch semantics disables it with one line: <c>store.Buggify.Disable()</c> (or, on a
38+
/// conformance head, the fixture's <c>RequireCleanWatches()</c> hook), which documents "this test needs clean watches" at the site.</para>
39+
/// <para>Chaos is a no-op for a suite that arms no watches (nothing to fire, no checks to defer), so enabling it on a non-watch
40+
/// conformance head is harmless.</para>
41+
/// </remarks>
42+
internal static class TestBuggify
43+
{
44+
45+
/// <summary>Creates a fresh FakeDb store with watch chaos enabled, seeded from the currently-running test's name.</summary>
46+
public static FakeDbStore ChaosStore()
47+
{
48+
var store = new FakeDbStore();
49+
store.Buggify.EnableChaos(NUnit.Framework.TestContext.CurrentContext.Test.FullName);
50+
return store;
51+
}
52+
53+
}
54+
55+
}

FoundationDB.FakeDb/FakeDbDatabase.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4272,6 +4272,34 @@ internal FakeDbBuggify(FakeDbStore store)
42724272
/// is unaffected: it only fires when the test explicitly calls it, so there is nothing to disable.</remarks>
42734273
public void Disable() => this.Chaos = null;
42744274

4275+
/// <summary>Enables seeded chaos with a stable seed derived from <paramref name="name"/> - the one-line opt-in for a whole suite: buggify every watch-arming test with a profile that is distinct per name yet reproducible across runs.</summary>
4276+
/// <param name="name">A stable, distinct identifier (typically the test or suite name) that fixes the injection profile.</param>
4277+
/// <param name="spuriousFireRate">Per-commit probability of a fan-out spurious fire on one armed key.</param>
4278+
/// <param name="deferredCheckRate">Per-check probability that a watch check is deferred (skipped) this commit.</param>
4279+
/// <returns>The installed chaos profile (for further tuning, e.g. clearing one of the rates).</returns>
4280+
/// <remarks>Recommended for any suite whose code arms watches: it forces that code through the weak watch contract (spurious
4281+
/// and reverted-miss fires) without giving up reproducibility. Chaos never produces a contract-illegal outcome, so it is safe
4282+
/// to leave on for suites that do not assert exact watch timing.</remarks>
4283+
public FakeDbBuggifyChaos EnableChaos(string name, double spuriousFireRate = 0.25, double deferredCheckRate = 0.25)
4284+
{
4285+
Contract.NotNull(name);
4286+
var chaos = new FakeDbBuggifyChaos(StableSeed(name)) { SpuriousFireRate = spuriousFireRate, DeferredCheckRate = deferredCheckRate };
4287+
this.Chaos = chaos;
4288+
return chaos;
4289+
4290+
// FNV-1a over the name: a process-stable hash (unlike string.GetHashCode, which is randomized per run since .NET Core)
4291+
static int StableSeed(string s)
4292+
{
4293+
uint h = 2166136261u;
4294+
foreach (var c in s)
4295+
{
4296+
h = (h ^ (byte) c) * 16777619u;
4297+
h = (h ^ (byte) (c >> 8)) * 16777619u;
4298+
}
4299+
return unchecked((int) h);
4300+
}
4301+
}
4302+
42754303
/// <summary>Injects an immediate spurious fire of every watch registered on <paramref name="key"/>, then unregisters them (the FDBV-026 per-key fan-out shape).</summary>
42764304
/// <param name="key">The (fully-encoded) watched key, as registered by <c>tr.Watch(...)</c>.</param>
42774305
/// <returns>The number of watches fired (0 when no watch was armed on the key: a test can assert the injection landed).</returns>

FoundationDB.Layers.Tests/FakeDbTestBackend.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public Task<IFdbDatabase> OpenAsync(FdbPath path, bool readOnly = false)
4646
{
4747
try
4848
{
49-
var db = (this.Store ??= new FakeDbStore()).OpenDatabase(path, readOnly);
49+
var db = (this.Store ??= NewStore()).OpenDatabase(path, readOnly);
5050
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
5151
return Task.FromResult<IFdbDatabase>(db);
5252
}
@@ -55,13 +55,23 @@ public Task<IFdbDatabase> OpenAsync(FdbPath path, bool readOnly = false)
5555
// disposing a database opened from the store disposes the store with it: a test that opens
5656
// several partitions in sequence (bench loops) gets a fresh, empty store per iteration,
5757
// which matches the isolated-partition semantics the suites rely on
58-
this.Store = new FakeDbStore();
58+
this.Store = NewStore();
5959
var db = this.Store.OpenDatabase(path, readOnly);
6060
db.Options.WithDefaultTimeout(TimeSpan.FromSeconds(15));
6161
return Task.FromResult<IFdbDatabase>(db);
6262
}
6363
}
6464

65+
/// <summary>Creates a fresh store with watch buggify enabled by default (seeded per running test): the repo's own harness is
66+
/// watch-realistic so any layer suite that arms watches exercises the weak watch contract. A layer test needing clean, exact
67+
/// watch semantics disables it with one line - <c>store.Buggify.Disable()</c>. Chaos is a no-op for a suite that arms no watches.</summary>
68+
private static FakeDbStore NewStore()
69+
{
70+
var store = new FakeDbStore();
71+
store.Buggify.EnableChaos(NUnit.Framework.TestContext.CurrentContext.Test.FullName);
72+
return store;
73+
}
74+
6575
}
6676

6777
}

0 commit comments

Comments
 (0)