diff --git a/CLAUDE.md b/CLAUDE.md index 0391d65..9ed6360 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,6 +132,30 @@ Critical path for MVP: Stages 1–5, 7–8, 10–11 xunit.v3 is built against Microsoft.Testing.Platform 1.x; anything 2.x makes every test run die at startup with a `TypeLoadException`. See the comment in `Directory.Packages.props`. +### Cross-store compliance suites + +Event sourcing behavior that Polecat and Marten both have belongs in +`JasperFx.Events.ComplianceTests` (source-only package; the suites compile into `Polecat.Tests` so +JasperFx's aggregate source generator binds Polecat's session types). Enroll a suite with an empty +subclass in `Compliance/polecat_event_store_compliance.cs`, implement whatever seam it needs on +`PolecatComplianceFixture`, and delete the Polecat-local copy. Ten suites are enrolled across three +waves (#393, #400, #399). + +To iterate on a suite before the JasperFx release, build against a working copy instead of the +package: + +``` +dotnet build src/Polecat.Tests/Polecat.Tests.csproj \ + -p:ComplianceSourceDir=$HOME/code/jasperfx/src/JasperFx.Events.ComplianceTests +``` + +**Two markers, opposite meanings.** `ported:` on a test file means it *duplicates* a test that also +exists in another store and should eventually move into a shared suite — one file carries it today, +`Linq/additional_linq_operator_tests.cs`, blocked because there is no document-db compliance library. +A bare `marten#NNNN` citation means the opposite: Polecat implemented the same feature and the issue +number is provenance. Do not treat a `marten#NNNN` count as a porting backlog; it grows with every +parity feature and never converges. + ### Writing tests that survive being run in parallel processes The suite is a candidate for being run across several worker processes at once (Bobcat's supervisor diff --git a/Directory.Packages.props b/Directory.Packages.props index d4a4fe7..6a417de 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -61,15 +61,27 @@ Also carries the jasperfx#594 patch half (a timed-out blue/green side-effect gate re-reads progression and succeeds when the replay had already reached the mark, plus a configurable DaemonSettings.SideEffectGateTimeout), jasperfx#595 (BatchingChannel could - deliver its trailing batch twice on shutdown) and #597. Same lockstep rule as above. --> - - + deliver its trailing batch twice on shutdown) and #597. Same lockstep rule as above. + JasperFx 2.38.0: (a) jasperfx#613 — compliance wave 3 (polecat#399): + JasperFx.Events.ComplianceTests gains ActivityCorrelationCompliance and + StringIdentitySingleStreamCompliance, plus the seam those needed + (ComplianceStoreConfig.StreamIdentity/.EnableCorrelationTracking and the + CorrelationIdFor/CausationIdFor/SetCorrelationId fixture members). That is the half this + bump consumes. (b) jasperfx#616 — the ProjectionScenario test harness lifted out of + Marten into a new JasperFx.Events.TestSupport namespace, which is what polecat#404 will + consume to delete Polecat's seven-file twin; nothing in this bump depends on it, and the + namespaces do not collide. (c) jasperfx#611 (EventProjection published types discovered + semantically rather than syntactically), jasperfx#615 (the blue/green side-effect gate + warm-up moved off the agent start path, #598/#610) and jasperfx#617 (container-scoped + projections usable by live aggregation, marten#5095). Same lockstep rule as above. --> + + - + - + @@ -160,16 +172,20 @@ Weasel 9.23.1 (weasel#420): the weasel#416 identifier validation finally reaches the SQL Server migrator, which validated NOTHING before this — the directly relevant half for Polecat. Companion providers (Oracle, MySQL, Sqlite) fixed in the same pass, and - PostgreSQL's copy folded onto the shared Weasel.Core helper (weasel#422). --> - - - + PostgreSQL's copy folded onto the shared Weasel.Core helper (weasel#422). + Weasel 9.23.2: weasel#425 — carries the JasperFx 2.38.0 floor, which is the reason to + take it; Polecat pins the family in lockstep rather than letting it resolve + transitively. Also weasel#423, where Sqlite and MySql finally implement the non-generic + Weasel.Core.ICommandBuilder — no SQL Server surface touched. --> + + + - + diff --git a/docs/projection-sg-audit-108.md b/docs/projection-sg-audit-108.md index 4481e98..3080417 100644 --- a/docs/projection-sg-audit-108.md +++ b/docs/projection-sg-audit-108.md @@ -17,6 +17,12 @@ ## How to read this report +- Types marked *(shared: SomethingCompliance)* are declared in + `JasperFx.Events.ComplianceTests` rather than in a Polecat test file. That + package is source-only, so they compile inside `Polecat.Tests` and the source + generator treats them exactly like local types. They moved there in compliance + waves 1–3 (#393, #400, #399); everything else cites the file it lives in. + - **✅ Generated**: SG emits a `[GeneratedEvolver]` partial / standalone evolver / `EventProjection` partial override; runtime registration finds the dispatcher; the post-#276 fail-fast does not fire. @@ -59,16 +65,16 @@ methods and emits a standalone `TEvolver` class. | 9 | `Report` *(Events/project_latest_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._Report_System_GuidEvolver.g.cs` | ✅ | | 10 | `StringReport` *(Events/project_latest_tests.cs)* | `string` | Apply + Create | ✓ | `..._StringReport_stringEvolver.g.cs` | ✅ | | 11 | `ScenarioQuestParty` *(Events/projection_scenario_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._ScenarioQuestParty_System_GuidEvolver.g.cs` | ✅ | -| 12 | `MutableIEventEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, IEvent)` | ✗ | `..._MutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | -| 13 | `MutableObjectEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, TEvent)` | ✗ | `..._MutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | -| 14 | `ImmutableIEventEvolveAggregate` *(record; Events/self_aggregating_evolve_method.cs)* | `Guid` | `Evolve(snapshot, IEvent)` returning new record | ✗ | `..._ImmutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 12 | `MutableIEventEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, IEvent)` | ✗ | `..._MutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 13 | `MutableObjectEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, TEvent)` | ✗ | `..._MutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 14 | `ImmutableIEventEvolveAggregate` *(record; shared: SelfAggregatingEvolveCompliance)* | `Guid` | `Evolve(snapshot, IEvent)` returning new record | ✗ | `..._ImmutableIEventEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | | 15 | `ImmutableObjectEvolveAggregate` *(record; same file)* | `Guid` | `Evolve(snapshot, TEvent)` returning new record | ✗ | `..._ImmutableObjectEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | -| 16 | `AsyncEvolveAggregate` *(Events/self_aggregating_evolve_method.cs)* | `Guid` | `EvolveAsync` | ✗ | `..._AsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | +| 16 | `AsyncEvolveAggregate` *(shared: SelfAggregatingEvolveCompliance)* | `Guid` | `EvolveAsync` | ✗ | `..._AsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | | 17 | `ImmutableAsyncEvolveAggregate` *(record; same file)* | `Guid` | `EvolveAsync` returning new record | ✗ | `..._ImmutableAsyncEvolveAggregate_System_GuidEvolveEvolver.g.cs` | ✅ | | 18 | `CompositeQuestParty` *(Projections/composite_projection_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._CompositeQuestParty_System_GuidEvolver.g.cs` | ✅ | | 19 | `QuestStats` *(Projections/composite_projection_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._QuestStats_System_GuidEvolver.g.cs` | ✅ | | 20 | `QuestParty` *(Projections/inline_projection_tests.cs)* | `Guid` | Apply + Create + ShouldDelete | ✓ | `..._QuestParty_System_GuidEvolver.g.cs` | ✅ | -| 21 | `SelfAggregatingStringQuest` *(Projections/single_stream_projection_with_string_identity_tests.cs)* | `string` | Apply + Create | ✓ | `..._SelfAggregatingStringQuest_stringEvolver.g.cs` | ✅ | +| 21 | `SelfAggregatingStringQuest` *(shared: StringIdentitySingleStreamCompliance)* | `string` | Apply + Create | ✓ | `..._SelfAggregatingStringQuest_stringEvolver.g.cs` | ✅ | | 22 | `SnapshotParty` *(Projections/snapshot_registration_tests.cs)* | `Guid` | Apply + Create | ✓ | `..._SnapshotParty_System_GuidEvolver.g.cs` | ✅ | | 23 | `SnapshotPartyByString` *(Projections/snapshot_registration_tests.cs)* | `string` | Apply + Create | ✓ | `..._SnapshotPartyByString_stringEvolver.g.cs` | ✅ | | 24 | `Payment` *(Projections/using_guid_based_strong_typed_id_for_aggregate_identity.cs)* | `PaymentId` *(wraps Guid)* | Apply + Create | ✓ | `..._Payment_Polecat_Tests_Projections_PaymentIdEvolver.g.cs` | ✅ | @@ -90,13 +96,13 @@ override) — **`partial` required on the class** for these shapes. |---|---|---|---|---|---|---| | 1 | `QuestLogProjection` *(Projections/event_projection_tests.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` | ✓ | `..._QuestLogProjection.EventProjection.g.cs` | ✅ | | 2 | `MultiEventQuestLogProjection` *(Projections/event_projection_tests.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` × 2 | ✓ | `..._MultiEventQuestLogProjection.EventProjection.g.cs` | ✅ | -| 3 | `SimpleEnrichmentProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._SimpleEnrichmentProjection.EventProjection.g.cs` | ✅ (EnrichEventsAsync is a side-channel; dispatch still through SG-emitted `Project`) | -| 4 | `EnrichmentCallOrderProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._EnrichmentCallOrderProjection.EventProjection.g.cs` | ✅ | -| 5 | `DbLookupEnrichmentProjection` *(Projections/event_projection_enrichment_tests.cs)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._DbLookupEnrichmentProjection.EventProjection.g.cs` | ✅ | -| 6 | `AuditRecordProjection` *(Projections/event_projection_should_register_document_types.cs)* | `EventProjection` | overrides `ApplyAsync` directly | ✓ | `..._AuditRecordProjection.TypeRegistration.g.cs` *(TypeRegistration-only)* | ✅ — dispatch via `ApplyAsync` override; SG emits only the `RegisterPublishedType` constructor so `Schema.For` is discoverable (per [marten#4166](https://github.com/JasperFx/marten/issues/4166)) | -| 7 | `AuditRecordCreatorProjection` *(Projections/event_projection_should_register_document_types.cs)* | `EventProjection` | conventional `Create(AuditableEvent)` returning the doc type | ✓ | `..._AuditRecordCreatorProjection.EventProjection.g.cs` | ✅ | +| 3 | `SimpleEnrichmentProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._SimpleEnrichmentProjection.EventProjection.g.cs` | ✅ (EnrichEventsAsync is a side-channel; dispatch still through SG-emitted `Project`) | +| 4 | `EnrichmentCallOrderProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._EnrichmentCallOrderProjection.EventProjection.g.cs` | ✅ | +| 5 | `DbLookupEnrichmentProjection` *(shared: EventProjectionEnrichmentCompliance)* | `EventProjection` | `Project` + `EnrichEventsAsync` override | ✓ | `..._DbLookupEnrichmentProjection.EventProjection.g.cs` | ✅ | +| 6 | `AuditRecordProjection` *(shared: EventProjectionRegistrationCompliance)* | `EventProjection` | overrides `ApplyAsync` directly | ✓ | `..._AuditRecordProjection.TypeRegistration.g.cs` *(TypeRegistration-only)* | ✅ — dispatch via `ApplyAsync` override; SG emits only the `RegisterPublishedType` constructor so `Schema.For` is discoverable (per [marten#4166](https://github.com/JasperFx/marten/issues/4166)) | +| 7 | `AuditRecordCreatorProjection` *(shared: EventProjectionRegistrationCompliance)* | `EventProjection` | conventional `Create(AuditableEvent)` returning the doc type | ✓ | `..._AuditRecordCreatorProjection.EventProjection.g.cs` | ✅ | | 8 | `ImportSqlProjection` *(Projections/using_event_projection_for_flat_tables.cs)* | `EventProjection` | conventional `Project(TEvent, IDocumentSession)` × 2 | ✓ | `..._ImportSqlProjection.EventProjection.g.cs` | ✅ | -| 9 | `StringQuestPartyProjection` *(Projections/single_stream_projection_with_string_identity_tests.cs)* | `SingleStreamProjection` | conventional Apply/Create + ShouldDelete on the **projection** (delegating to doc would be the alternative) | ✓ | `..._StringQuestPartyProjection.Evolver.g.cs` *(PartialProjection)* | ✅ | +| 9 | `StringQuestPartyProjection` *(shared: StringIdentitySingleStreamCompliance)* | `SingleStreamProjection` | conventional Apply/Create + ShouldDelete on the **projection** (delegating to doc would be the alternative) | ✓ | `..._StringQuestPartyProjection.Evolver.g.cs` *(PartialProjection)* | ✅ | | 10 | `CustomerSummaryProjection` *(Projections/multi_stream_projection_tests.cs)* | `MultiStreamProjection` | conventional Apply on the projection | ✓ | `..._CustomerSummaryProjection.Evolver.g.cs` | ✅ | | 11 | `MonthlyAccountActivityProjection` *(Projections/time_based_multi_stream_projection_tests.cs)* | `MultiStreamProjection` | conventional Apply on the projection | ✓ | `..._MonthlyAccountActivityProjection.Evolver.g.cs` | ✅ | | 12 | `CompositeOrderProjection` *(Projections/composite_try_find_upstream_cache_tests.cs)* | `SingleStreamProjection` | **overrides `Evolve` directly** | ✓ | — | ⚠ Deliberate bypass — override wins per #276 doctrine. Composite-projection test exercises upstream-cache lookup, not dispatch correctness. | @@ -151,15 +157,15 @@ through the projection and asserts state evolves correctly: |---|---| | Self-aggregating sync Apply + Create | `fetch_for_writing_tests`, `Bug_4197_fetch_for_writing_natural_key` | | Self-aggregating Apply + Create + **ShouldDelete** | `aggregate_stream_to_last_known_tests` (DeletableAggregate), `inline_projection_tests` (QuestParty) | -| Self-aggregating sync `Evolve(snapshot, IEvent)` (mutable + immutable record) | `self_aggregating_evolve_method` (×4 variants) | -| Self-aggregating `EvolveAsync` | `self_aggregating_evolve_method` (×2 variants) | -| String identity | `single_stream_projection_with_string_identity_tests`, `project_latest_tests` (StringReport), `always_enforce_consistency_with_string_stream_id` | +| Self-aggregating sync `Evolve(snapshot, IEvent)` (mutable + immutable record) | `SelfAggregatingEvolveCompliance` (×4 variants) | +| Self-aggregating `EvolveAsync` | `SelfAggregatingEvolveCompliance` (×2 variants) | +| String identity | `StringIdentitySingleStreamCompliance`, `project_latest_tests` (StringReport), `always_enforce_consistency_with_string_stream_id` | | Strong-typed-id identity (wrapper struct) | `using_guid_based_strong_typed_id_for_aggregate_identity`, `using_string_based_strong_typed_id_for_aggregate_identity` | | `EventProjection` with conventional `Project(TEvent, IDocumentSession)` | `event_projection_tests` | -| `EventProjection` with `Create(TEvent)` returning a doc | `event_projection_should_register_document_types` (AuditRecordCreatorProjection) | -| `EventProjection` with `ApplyAsync` override | `event_projection_should_register_document_types` (AuditRecordProjection) | -| `EventProjection` with `EnrichEventsAsync` override | `event_projection_enrichment_tests` (×3 variants) | -| `SingleStreamProjection` subclass with Apply on the projection | `single_stream_projection_with_string_identity_tests` (StringQuestPartyProjection) | +| `EventProjection` with `Create(TEvent)` returning a doc | `EventProjectionRegistrationCompliance` (AuditRecordCreatorProjection) | +| `EventProjection` with `ApplyAsync` override | `EventProjectionRegistrationCompliance` (AuditRecordProjection) | +| `EventProjection` with `EnrichEventsAsync` override | `EventProjectionEnrichmentCompliance` (×3 variants) | +| `SingleStreamProjection` subclass with Apply on the projection | `StringIdentitySingleStreamCompliance` (StringQuestPartyProjection) | | `MultiStreamProjection` subclass with Apply on the projection | `multi_stream_projection_tests`, `time_based_multi_stream_projection_tests` | | `PolecatCompositeProjection` with `Snapshot` | `composite_projection_tests` | | `PolecatCompositeProjection` with `TryFindUpstreamCache` between stages | `composite_try_find_upstream_cache_tests` | diff --git a/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs b/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs index f730b27..78d5853 100644 --- a/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs +++ b/src/Polecat.Tests/Compliance/ComplianceQuerySessionAlias.cs @@ -8,3 +8,9 @@ // they cannot reach the pair their suite class is generic over. global using ComplianceOperations = Polecat.IDocumentSession; global using ComplianceEventProjection = Polecat.Projections.EventProjection; + +// The string-identity suite's custom projection needs Polecat's own SingleStreamProjection base, and +// that one is generic over the identity type as well as the document, so this alias names a closed +// generic rather than an open one. +global using ComplianceStringPartyProjectionBase = + Polecat.Projections.SingleStreamProjection; diff --git a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs index 3a07392..388b016 100644 --- a/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs +++ b/src/Polecat.Tests/Compliance/PolecatComplianceFixture.cs @@ -37,6 +37,17 @@ protected override async Task BuildStoreAsync(ComplianceStoreConfig config) options.DaemonSettings.MaxConcurrentRebuildsPerDatabase = config.MaxConcurrentRebuildsPerDatabase; } + if (config.StreamIdentity.HasValue) + { + options.Events.StreamIdentity = config.StreamIdentity.Value; + } + + if (config.EnableCorrelationTracking) + { + options.Events.EnableCorrelationId = true; + options.Events.EnableCausationId = true; + } + config.ApplyTo(new PolecatComplianceRegistrar(options)); _store = new DocumentStore(options); @@ -81,6 +92,13 @@ public override Task SaveChangesAsync(IDocumentSession session, CancellationToke public override IEventStoreOperations EventsFor(IDocumentSession session) => session.Events; + public override string? CorrelationIdFor(IDocumentSession session) => session.CorrelationId; + + public override string? CausationIdFor(IDocumentSession session) => session.CausationId; + + public override void SetCorrelationId(IDocumentSession session, string? correlationId) + => session.CorrelationId = correlationId; + public override IEventStore EventStore => _store; public override IEnumerable AllAggregateTypes() => _store.Options.Projections.AllAggregateTypes(); diff --git a/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs b/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs index f4d505d..e3693f7 100644 --- a/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs +++ b/src/Polecat.Tests/Compliance/polecat_event_store_compliance.cs @@ -33,3 +33,9 @@ public class event_projection_enrichment_compliance public class rebuild_concurrency_cap_compliance : RebuildConcurrencyCapCompliance; + +public class activity_correlation_compliance + : ActivityCorrelationCompliance; + +public class string_identity_single_stream_compliance + : StringIdentitySingleStreamCompliance; diff --git a/src/Polecat.Tests/Events/activity_correlation_tests.cs b/src/Polecat.Tests/Events/activity_correlation_tests.cs deleted file mode 100644 index 7b40128..0000000 --- a/src/Polecat.Tests/Events/activity_correlation_tests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Diagnostics; -using Polecat.Tests.Harness; - -namespace Polecat.Tests.Events; - -/// -/// #239: a session auto-seeds CorrelationId from Activity.Current.RootId and CausationId from -/// Activity.Current.ParentId on open, so distributed-tracing context flows onto events with zero -/// app code (mirrors Marten). An explicit caller value still wins. -/// -public class activity_correlation_tests : OneOffConfigurationsContext -{ - private async Task ConfigureAndApply(Action configure) - { - ConfigureStore(configure); - await theDatabase.ApplyAllConfiguredChangesToDatabaseAsync(); - } - - private static (Activity parent, Activity child) StartActivityScope() - { - Activity.DefaultIdFormat = ActivityIdFormat.W3C; - var parent = new Activity("parent").Start(); - var child = new Activity("child").Start(); // child of parent → has a non-null ParentId - return (parent, child); - } - - [Fact] - public async Task session_seeds_correlation_and_causation_from_activity() - { - await ConfigureAndApply(_ => { }); - - var (parent, child) = StartActivityScope(); - try - { - await using var session = theStore.LightweightSession(); - session.CorrelationId.ShouldBe(child.RootId); - session.CausationId.ShouldBe(child.ParentId); - } - finally - { - child.Stop(); - parent.Stop(); - } - } - - [Fact] - public async Task events_appended_in_activity_scope_carry_root_and_parent_ids() - { - await ConfigureAndApply(opts => - { - opts.Events.EnableCorrelationId = true; - opts.Events.EnableCausationId = true; - }); - - var (parent, child) = StartActivityScope(); - var streamId = Guid.NewGuid(); - try - { - await using var session = theStore.LightweightSession(); - session.Events.StartStream(streamId, new QuestStarted("Traced Quest")); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - } - finally - { - child.Stop(); - parent.Stop(); - } - - await using var query = theStore.QuerySession(); - var events = await query.Events.FetchStreamAsync(streamId, token: TestContext.Current.CancellationToken); - - events.Count.ShouldBe(1); - events[0].CorrelationId.ShouldBe(child.RootId); - events[0].CausationId.ShouldBe(child.ParentId); - } - - [Fact] - public async Task explicit_caller_value_wins_over_activity() - { - await ConfigureAndApply(_ => { }); - - var (parent, child) = StartActivityScope(); - try - { - await using var session = theStore.LightweightSession(); - session.CorrelationId = "explicit-corr"; - session.CorrelationId.ShouldBe("explicit-corr"); - session.CorrelationId.ShouldNotBe(child.RootId); - } - finally - { - child.Stop(); - parent.Stop(); - } - } - - [Fact] - public async Task no_activity_leaves_correlation_null() - { - await ConfigureAndApply(_ => { }); - - // Ensure no ambient activity for this test. - var saved = Activity.Current; - Activity.Current = null; - try - { - await using var session = theStore.LightweightSession(); - session.CorrelationId.ShouldBeNull(); - session.CausationId.ShouldBeNull(); - } - finally - { - Activity.Current = saved; - } - } -} diff --git a/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs b/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs index 0d6894f..a62f4c6 100644 --- a/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs +++ b/src/Polecat.Tests/Linq/additional_linq_operator_tests.cs @@ -4,9 +4,24 @@ namespace Polecat.Tests.Linq; /// -/// Tests for LINQ operators ported from Marten: negation, modulo, Last/LastOrDefault, -/// .Equals(), nullable types, and HasValue. +/// ported: Marten — LINQ operators duplicated from Marten's suite: negation, modulo, +/// Last/LastOrDefault, .Equals(), nullable types, and HasValue. /// +/// +/// The last remaining duplication of a Marten test after compliance wave 3 (#399), and the one +/// the shared library cannot absorb: LINQ is document-database behavior, and +/// JasperFx.Events.ComplianceTests spans only the event sourcing surface. Polecat still +/// re-declares IDocumentStore/IDocumentSession/IQuerySession in its own +/// namespace, so there is nothing for a document-db compliance suite to be generic over yet; +/// Weasel.Storage is the intended convergence layer. Tracked in marten#5118. +/// +/// +/// Marker convention: ported: means this file duplicates a test that also exists in +/// another store and should eventually move into a shared suite. A bare marten#NNNN +/// citation elsewhere in this suite means the opposite — Polecat implemented the same +/// feature and the issue number is provenance worth keeping. +/// +/// public class additional_linq_operator_tests : OneOffConfigurationsContext { private async Task StoreSeedDataAsync() diff --git a/src/Polecat.Tests/Polecat.Tests.csproj b/src/Polecat.Tests/Polecat.Tests.csproj index 1714f2f..5cecd77 100644 --- a/src/Polecat.Tests/Polecat.Tests.csproj +++ b/src/Polecat.Tests/Polecat.Tests.csproj @@ -30,7 +30,14 @@ - + + + diff --git a/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs b/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs index 6a71f30..32a0b74 100644 --- a/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs +++ b/src/Polecat.Tests/Projections/aggregateto_linq_operator_tests.cs @@ -8,8 +8,27 @@ namespace Polecat.Tests.Projections; // #364 follow-up (Marten parity): AggregateToAsync() folds every event matched by an event query // into a single aggregate, regardless of stream, optionally starting from supplied state. The // aggregate's identity is stamped from the last queried event's stream. Reuses the self-aggregating -// QuestParty (inline_projection_tests.cs) and SelfAggregatingStringQuest -// (single_stream_projection_with_string_identity_tests.cs) declared in this namespace. +// QuestParty (inline_projection_tests.cs) declared in this namespace. + +/// +/// String-keyed self-aggregating type for the key-identity case below. +/// +/// +/// Lived in single_stream_projection_with_string_identity_tests.cs until that file was retired +/// into JasperFx.Events.ComplianceTests (#399). The shared library's equivalent folds the +/// compliance suite's own events, so this test keeps a local type that folds Polecat's. +/// +public partial class AggregateToStringQuest +{ + public string Id { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public List Members { get; set; } = new(); + + public static AggregateToStringQuest Create(QuestStarted e) => new() { Name = e.Name }; + + public void Apply(MembersJoined e) => Members.AddRange(e.Members); +} + public class aggregateto_linq_operator_tests : OneOffConfigurationsContext { private readonly MembersJoined _joined1 = new(1, "Emond's Field", ["Rand", "Matrim", "Perrin", "Thom"]); @@ -115,7 +134,7 @@ public async Task gets_the_key_set() var quest = await session.Events.QueryAllRawEvents() .Where(x => x.StreamKey == key) - .AggregateToAsync(token: TestContext.Current.CancellationToken); + .AggregateToAsync(token: TestContext.Current.CancellationToken); quest.ShouldNotBeNull(); quest.Id.ShouldBe(key); diff --git a/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs b/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs deleted file mode 100644 index 0aa10bf..0000000 --- a/src/Polecat.Tests/Projections/single_stream_projection_with_string_identity_tests.cs +++ /dev/null @@ -1,248 +0,0 @@ -using JasperFx.Events; -using JasperFx.Events.Projections; -using Polecat.Projections; -using Polecat.Tests.Harness; - -namespace Polecat.Tests.Projections; - -/// -/// Aggregate document with a string identity, for use with string-keyed streams. -/// -public partial class StringQuestParty -{ - public string Id { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public List Members { get; set; } = new(); - public string? Location { get; set; } - public List MonstersSlain { get; set; } = new(); -} - -/// -/// Self-aggregating document type with string identity, for use with -/// the Snapshot<T, TId> API (conventional Apply/Create methods on the type itself). -/// -public partial class SelfAggregatingStringQuest -{ - public string Id { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public List Members { get; set; } = new(); - - public static SelfAggregatingStringQuest Create(QuestStarted e) - { - return new SelfAggregatingStringQuest { Name = e.Name }; - } - - public void Apply(MembersJoined e) - { - Members.AddRange(e.Members); - } -} - -/// -/// Custom SingleStreamProjection with TId = string, demonstrating the new -/// two-type-parameter signature that mirrors Marten's SingleStreamProjection<TDoc, TId>. -/// -public partial class StringQuestPartyProjection : SingleStreamProjection -{ - public StringQuestPartyProjection() - { - } - - public static StringQuestParty Create(IEvent @event) - { - return new StringQuestParty - { - Id = @event.StreamKey!, - Name = @event.Data.Name - }; - } - - public void Apply(MembersJoined e, StringQuestParty party) - { - party.Members.AddRange(e.Members); - party.Location = e.Location; - } - - public void Apply(MembersDeparted e, StringQuestParty party) - { - foreach (var m in e.Members) party.Members.Remove(m); - } - - public void Apply(ArrivedAtLocation e, StringQuestParty party) - { - party.Location = e.Location; - } - - public void Apply(MonsterSlain e, StringQuestParty party) - { - party.MonstersSlain.Add(e.Name); - } - - public bool ShouldDelete(QuestEnded e) - { - return true; - } -} - -[Collection("integration")] -public class single_stream_projection_with_string_identity_tests : IntegrationContext -{ - public single_stream_projection_with_string_identity_tests(DefaultStoreFixture fixture) : base(fixture) - { - } - - private async Task CreateStoreWithStringProjection() - { - await StoreOptions(opts => - { - opts.DatabaseSchemaName = "string_proj"; - opts.Events.StreamIdentity = StreamIdentity.AsString; - opts.Projections.Add(JasperFx.Events.Projections.ProjectionLifecycle.Inline); - }); - return theStore; - } - - [Fact] - public async Task custom_projection_creates_aggregate_on_start_stream() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Destroy the Ring")); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Id.ShouldBe(streamKey); - party.Name.ShouldBe("Destroy the Ring"); - } - - [Fact] - public async Task custom_projection_applies_multiple_events() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Fellowship"), - new MembersJoined(1, "Rivendell", ["Aragorn", "Legolas", "Gimli"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Name.ShouldBe("Fellowship"); - party.Members.ShouldBe(["Aragorn", "Legolas", "Gimli"]); - party.Location.ShouldBe("Rivendell"); - } - - [Fact] - public async Task custom_projection_updates_on_append() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Adventure"), - new MembersJoined(1, "Start", ["Hero"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var session2 = store.LightweightSession(); - session2.Events.Append(streamKey, - new ArrivedAtLocation("Dungeon", 2), - new MonsterSlain("Goblin", 50)); - await session2.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Location.ShouldBe("Dungeon"); - party.MonstersSlain.ShouldContain("Goblin"); - } - - [Fact] - public async Task custom_projection_should_delete_removes_aggregate() - { - var store = await CreateStoreWithStringProjection(); - - var streamKey = "quest-" + Guid.NewGuid(); - await using var session = store.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Doomed Quest"), - new MembersJoined(1, "Start", ["Hero"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var session2 = store.LightweightSession(); - session2.Events.Append(streamKey, - new QuestEnded("Doomed Quest")); - await session2.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - party.ShouldBeNull(); - } - - [Fact] - public async Task multiple_string_keyed_streams_projected_independently() - { - var store = await CreateStoreWithStringProjection(); - - var key1 = "quest-" + Guid.NewGuid(); - var key2 = "quest-" + Guid.NewGuid(); - - await using var session = store.LightweightSession(); - session.Events.StartStream(key1, - new QuestStarted("Quest 1"), - new MembersJoined(1, "Town A", ["Alpha"])); - session.Events.StartStream(key2, - new QuestStarted("Quest 2"), - new MembersJoined(1, "Town B", ["Beta", "Gamma"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = store.QuerySession(); - var party1 = await query.LoadAsync(key1, TestContext.Current.CancellationToken); - var party2 = await query.LoadAsync(key2, TestContext.Current.CancellationToken); - - party1.ShouldNotBeNull(); - party1.Name.ShouldBe("Quest 1"); - party1.Members.ShouldBe(["Alpha"]); - - party2.ShouldNotBeNull(); - party2.Name.ShouldBe("Quest 2"); - party2.Members.ShouldBe(["Beta", "Gamma"]); - } - - [Fact] - public async Task self_aggregating_projection_with_string_identity() - { - // Use SingleStreamProjection with a self-aggregating type - await StoreOptions(opts => - { - opts.DatabaseSchemaName = "string_snap"; - opts.Events.StreamIdentity = StreamIdentity.AsString; - opts.Projections.Add>(ProjectionLifecycle.Inline); - }); - - var streamKey = "snap-" + Guid.NewGuid(); - await using var session = theStore.LightweightSession(); - session.Events.StartStream(streamKey, - new QuestStarted("Snapshot Quest"), - new MembersJoined(1, "Castle", ["Knight"])); - await session.SaveChangesAsync(TestContext.Current.CancellationToken); - - await using var query = theStore.QuerySession(); - var party = await query.LoadAsync(streamKey, TestContext.Current.CancellationToken); - - party.ShouldNotBeNull(); - party.Name.ShouldBe("Snapshot Quest"); - party.Members.ShouldBe(["Knight"]); - } -}