Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,11 @@ public void Add(Metadata m, object evt)

public class LiteDbFactory
{
private static int _id = 0;

public static LiteDatabase Get()
{
string fileName = $"./test_{Interlocked.Increment(ref _id)}.db";
if(File.Exists(fileName))
File.Delete(fileName);
return new LiteDatabase(fileName);
}
/// <summary>
/// A fresh, empty, IN-MEMORY LiteDB per call. It was a file, ./test_{n}.db, "fresh" by deleting that file — but LiteDB
/// also writes a journal, ./test_{n}-log.db, which survived and was REPLAYED on the next open: a later process's
/// HowToSubscribeRealModel counted earlier runs' reservations ("found 2", "found 6", 10 — growing run on run, and
/// passing only where the journal had been checkpointed away). Memory leaves nothing behind for the next run.
/// </summary>
public static LiteDatabase Get() => new LiteDatabase(new MemoryStream());
}
29 changes: 23 additions & 6 deletions src/MicroPlumberd.Tests/HowTo/PlumberMethodsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,16 @@ public async Task HowToSubscribeRealModel()
await plumber.SubscribeEventHandler(dbModel);

var suffixOfStreamWhereOurEventWillBeAppend = Guid.NewGuid();
var ourLovelyEvent = new TicketReserved() { MovieName = "Golden Eye", RoomName = "101"};
// THIS row's own reservation, by its own name. The "found 2/6" reds were a LiteDB journal replayed from earlier runs
// (fixed in LiteDbFactory); the class-fixture store contributes nothing TODAY (the absolute-count mutant survives) —
// but the model subscribes from the beginning, so the next row in this class that reserves under a shared name would
// bring an absolute count's order-dependence back. Count ours.
var movie = $"Golden Eye {Guid.NewGuid():N}";
var ourLovelyEvent = new TicketReserved() { MovieName = movie, RoomName = "101"};

await plumber.AppendEvent(ourLovelyEvent, suffixOfStreamWhereOurEventWillBeAppend);
await Task.Delay(1000);

dbModel.Reservations.Query().Count().Should().Be(1);
(await OwnReservationsFolded(dbModel.Reservations, movie)).Should().Be(1, "our event is folded exactly once");

}
[Fact]
Expand All @@ -102,12 +106,12 @@ public async Task HowToSubscribeRealModelWithHost()
.StartAsync();

var suffixOfStreamWhereOurEventWillBeAppend = Guid.NewGuid();
var ourLovelyEvent = new TicketReserved() { MovieName = "Golden Eye", RoomName = "101" };
var movie = $"Golden Eye {Guid.NewGuid():N}"; // this row's own reservation (see HowToSubscribeRealModel)
var ourLovelyEvent = new TicketReserved() { MovieName = movie, RoomName = "101" };

await sp.GetRequiredService<IPlumber>().AppendEvent(ourLovelyEvent, suffixOfStreamWhereOurEventWillBeAppend);
await Task.Delay(1000);

sp.GetRequiredService<LiteDatabase>().Reservations().Query().Count().Should().Be(1);
(await OwnReservationsFolded(sp.GetRequiredService<LiteDatabase>().Reservations(), movie)).Should().Be(1, "our event is folded exactly once");

}

Expand Down Expand Up @@ -265,6 +269,19 @@ await plumber.Rehydrate(model, $"ReservationStream-{Guid.NewGuid()}",
}


/// <summary>
/// Waits — bounded, for the condition the assertion needs: THIS row's reservation folded — instead of a fixed delay, then
/// gives a short settle so a duplicate delivery of the same event would still be counted. Returns how many are folded.
/// </summary>
private static async Task<int> OwnReservationsFolded(LiteDB.ILiteCollection<Reservation> reservations, string movie)
{
var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(15);
while (reservations.Count(x => x.MovieName == movie) == 0 && DateTime.UtcNow < deadline)
await Task.Delay(50);
await Task.Delay(250);
return reservations.Count(x => x.MovieName == movie);
}

public void Dispose()
{
_host?.Dispose();
Expand Down
48 changes: 48 additions & 0 deletions src/MicroPlumberd.Tests/Unit/LiteDbFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using FluentAssertions;
using LiteDB;
using MicroPlumberd.Tests.App.CinemaDomain;
using MicroPlumberd.Tests.App.Infrastructure;

namespace MicroPlumberd.Tests.Unit;

/// <summary>
/// HowToSubscribeRealModel counted reservations of EARLIER test runs: LiteDbFactory "freshened" ./test_{n}.db by deleting
/// it, but LiteDB's journal ./test_{n}-log.db survived and was replayed on the next open ("found 2", "found 6", 10 — growing
/// run on run). Two rows, because a file factory shows differently on the two kinds of bin (rev-reach):
/// - CLEAN bin (CI, always): the file factory CREATES test_1.db — the no-file row is red; StartsEmpty is green (nothing to replay).
/// - DIRTY bin (a dev's second run): it re-opens existing names — no new path, but the files are MODIFIED (the no-file row,
/// by length + write time, is red). StartsEmpty is red only where a NOT-yet-checkpointed journal was left behind (as the
/// HowTo rows left one: "found 11"); in this class's own runs it stayed green. The no-file row is the guard on both bins.
/// A path-set-only form of the no-file row was blind on the dirty bin (names are reused); this one sees creation AND change.
/// </summary>
public class LiteDbFactoryTests
{
[Fact]
public void AnAssertionDb_CreatesOrTouchesNoFile_ForALaterRunToReplay()
{
var dir = Directory.GetCurrentDirectory();
var before = Snapshot(dir);

using (var db = LiteDbFactory.Get())
db.Reservations().Insert(new Reservation { MovieName = "written", RoomName = "1" });

var after = Snapshot(dir);
var created = after.Keys.Except(before.Keys).ToList();
var modified = after.Where(f => before.TryGetValue(f.Key, out var was) && was != f.Value).Select(f => f.Key).ToList();
created.Concat(modified).Should().BeEmpty(
"an assertion db must leave nothing on disk — a file, or its journal, is replayed into the next run's model");
}

private static Dictionary<string, (long Length, DateTime Written)> Snapshot(string dir) =>
Directory.GetFiles(dir, "*.db").ToDictionary(f => f, f => { var i = new FileInfo(f); return (i.Length, i.LastWriteTimeUtc); });

[Fact]
public void EachAssertionDb_StartsEmpty()
{
using (var first = LiteDbFactory.Get())
first.Reservations().Insert(new Reservation { MovieName = "written", RoomName = "1" });

using var next = LiteDbFactory.Get();
next.Reservations().Count().Should().Be(0);
}
}
Loading