public static IEnumerable <object[]> GetTestCases() { var assembly = typeof(SpecRunner).Assembly; var specs = assembly.GetManifestResourceNames().Where(x => x.EndsWith("-spec.json")); foreach (var spec in specs) { JsonDocument doc; using (var stream = assembly.GetManifestResourceStream(spec)) { doc = JsonDocument.Parse(stream !); } var projection = doc.RootElement.GetProperty("projection").GetString(); Assert.False(string.IsNullOrWhiteSpace(projection)); var projectionSourceName = assembly.GetManifestResourceNames() .SingleOrDefault(x => x.EndsWith($"{projection}.js")); Assert.NotNull(projectionSourceName); string source; using (var stream = assembly.GetManifestResourceStream(projectionSourceName !)) { Assert.NotNull(stream); using (var sr = new StreamReader(stream !)) { source = sr.ReadToEnd(); } } List <InputEventSequence> sequences = new(); foreach (var inputEvent in doc.RootElement.GetProperty("input").EnumerateArray()) { var stream = inputEvent.GetProperty("streamId").GetString(); Assert.NotNull(stream); var sequence = new InputEventSequence(stream !); sequences.Add(sequence); foreach (var e in inputEvent.GetProperty("events").EnumerateArray()) { var et = e.GetProperty("eventType").GetString(); Assert.NotNull(et); bool skip = false; if (e.TryGetProperty("skip", out var skipElement)) { skip = skipElement.GetBoolean(); } var initializedPartitions = new List <string>(); if (e.TryGetProperty("initializedPartitions", out var partitions)) { foreach (var element in partitions.EnumerateArray()) { initializedPartitions.Add(element.GetString() !); } } var expectedStates = new Dictionary <string, string?>(); int stateCount = 0; if (e.TryGetProperty("states", out var states)) { foreach (var state in states.EnumerateArray()) { stateCount++; var expectedStateNode = state.GetProperty("state"); var expectedState = expectedStateNode.ValueKind == JsonValueKind.Null ? null : expectedStateNode.GetRawText(); if (!expectedStates.TryAdd(state.GetProperty("partition").GetString() !, expectedState)) { throw new InvalidOperationException("Duplicate state"); } } } if (stateCount > 2) { throw new InvalidOperationException("Cannot specify more than 2 states"); } sequence.Events.Add(new InputEvent(et !, e.GetProperty("data").GetRawText(), e.TryGetProperty("metadata", out var metadata) ? metadata.GetRawText() : null, initializedPartitions, expectedStates, skip, e.TryGetProperty("eventId", out var idElement) && idElement.TryGetGuid(out var id) ? id: Guid.NewGuid())); } } var output = doc.RootElement.GetProperty("output"); var sdb = new SourceDefinitionBuilder(); var config = output.GetProperty("config"); foreach (var item in config.EnumerateObject()) { switch (item.Name) { case "definesStateTransform": if (item.Value.GetBoolean()) { sdb.SetDefinesStateTransform(); } break; case "handlesDeletedNotifications": sdb.SetHandlesStreamDeletedNotifications(item.Value.GetBoolean()); break; case "producesResults": if (item.Value.GetBoolean()) { sdb.SetOutputState(); } break; case "definesFold": if (item.Value.GetBoolean()) { sdb.SetDefinesFold(); } break; case "resultStreamName": sdb.SetResultStreamNameOption(item.Value.GetString()); break; case "partitionResultStreamNamePattern": sdb.SetPartitionResultStreamNamePatternOption(item.Value.GetString()); break; case "$includeLinks": sdb.SetIncludeLinks(item.Value.GetBoolean()); break; case "reorderEvents": sdb.SetReorderEvents(item.Value.GetBoolean()); break; case "processingLag": sdb.SetProcessingLag(item.Value.GetInt32()); break; case "biState": sdb.SetIsBiState(item.Value.GetBoolean()); break; case "categories": foreach (var c in item.Value.EnumerateArray()) { sdb.FromCategory(c.GetString()); } break; case "partitioned": if (item.Value.GetBoolean()) { sdb.SetByCustomPartitions(); } break; case "events": foreach (var e in item.Value.EnumerateArray()) { sdb.IncludeEvent(e.GetString()); } break; case "allEvents": if (item.Value.GetBoolean()) { sdb.AllEvents(); } else { sdb.NotAllEvents(); } break; case "allStreams": if (item.Value.GetBoolean()) { sdb.FromAll(); } break; default: throw new Exception($"unexpected property in expected config {item.Name}"); } } #nullable disable List <OutputEvent> expectedEmittedEvents = new(); if (output.TryGetProperty("emitted", out var expectedEmittedElement)) { foreach (var element in expectedEmittedElement.EnumerateObject()) { var stream = element.Name; foreach (var eventElement in element.Value.EnumerateArray()) { if (eventElement.ValueKind == JsonValueKind.String) { expectedEmittedEvents.Add( new OutputEvent(stream, "$>", eventElement.GetString(), null)); } } } } JintProjectionStateHandler runner = null; IQuerySources definition = null; IQuerySources expectedDefinition = sdb.Build(); yield return(WithOutput($"{projection} compiles", o => { runner = new JintProjectionStateHandler(source, true, TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(100)); })); yield return(For($"{projection} getDefinition", () => { definition = runner.GetSourceDefinition(); })); yield return(For($"{projection} qs.AllStreams", () => Assert.Equal(expectedDefinition.AllStreams, definition.AllStreams))); yield return(For($"{projection} qs.Categories", () => Assert.Equal(expectedDefinition.Categories, definition.Categories))); yield return(For($"{projection} qs.Streams", () => Assert.Equal(expectedDefinition.Streams, definition.Streams))); yield return(For($"{projection} qs.AllEvents", () => Assert.Equal(expectedDefinition.AllEvents, definition.AllEvents))); yield return(For($"{projection} qs.Events", () => Assert.Equal(expectedDefinition.Events, definition.Events))); yield return(For($"{projection} qs.ByStreams", () => Assert.Equal(expectedDefinition.ByStreams, definition.ByStreams))); yield return(For($"{projection} qs.ByCustomPartitions", () => Assert.Equal(expectedDefinition.ByCustomPartitions, definition.ByCustomPartitions))); yield return(For($"{projection} qs.DefinesStateTransform", () => Assert.Equal(expectedDefinition.DefinesStateTransform, definition.DefinesStateTransform))); yield return(For($"{projection} qs.DefinesFold", () => Assert.Equal(expectedDefinition.DefinesFold, definition.DefinesFold))); yield return(For($"{projection} qs.HandlesDeletedNotifications", () => Assert.Equal(expectedDefinition.HandlesDeletedNotifications, definition.HandlesDeletedNotifications))); yield return(For($"{projection} qs.ProducesResults", () => Assert.Equal(expectedDefinition.ProducesResults, definition.ProducesResults))); yield return(For($"{projection} qs.IsBiState", () => Assert.Equal(expectedDefinition.IsBiState, definition.IsBiState))); yield return(For($"{projection} qs.IncludeLinksOption", () => Assert.Equal(expectedDefinition.IncludeLinksOption, definition.IncludeLinksOption))); yield return(For($"{projection} qs.ResultStreamNameOption", () => Assert.Equal(expectedDefinition.ResultStreamNameOption, definition.ResultStreamNameOption))); yield return(For($"{projection} qs.PartitionResultStreamNamePatternOption", () => Assert.Equal(expectedDefinition.PartitionResultStreamNamePatternOption, definition.PartitionResultStreamNamePatternOption))); yield return(For($"{projection} qs.ReorderEventsOption", () => Assert.Equal(expectedDefinition.ReorderEventsOption, definition.ReorderEventsOption))); yield return(For($"{projection} qs.ProcessingLagOption", () => Assert.Equal(expectedDefinition.ProcessingLagOption, definition.ProcessingLagOption))); yield return(For($"{projection} qs.LimitingCommitPosition", () => Assert.Equal(expectedDefinition.LimitingCommitPosition, definition.LimitingCommitPosition))); var partitionedState = new Dictionary <string, string>(); var sharedStateInitialized = false; var revision = new Dictionary <string, long>(); List <EmittedEventEnvelope> actualEmittedEvents = new(); for (int i = 0; i < sequences.Count; i++) { var sequence = sequences[i]; if (!revision.TryGetValue(sequence.Stream, out _)) { revision[sequence.Stream] = 0; } for (int j = 0; j < sequences[i].Events.Count; j++) { var logPosition = i * 100 + j; var flags = PrepareFlags.IsJson | PrepareFlags.Data; if (j == 0) { flags |= PrepareFlags.TransactionBegin; } if (j == sequence.Events.Count - 1) { flags |= PrepareFlags.TransactionEnd; } /*Sequence: * Get partition if bycustom partition or by stream * if the partition is null or an empty string skip this event (NB: need to indicate that an event should be skipped) * load the state if it doesn't exist or tell the projection to init state * init shared if it doesn't exist * if init was state was called, call created * if processing a delete, call partition deleted (NB: bistate partitions do not support deletes) * process the event * save any shared state if it isn't null * save any state * save emitted events to verify later */ var @event = sequence.Events[j]; var body = JObject.Parse(@event.Body).ToString(Formatting.Indented); var metadata = Array.Empty <byte>(); if (@event.Metadata != null) { metadata = Utf8NoBom.GetBytes(JObject.Parse(@event.Metadata).ToString(Formatting.Indented)); } var er = new EventRecord( revision[sequence.Stream], logPosition, Guid.NewGuid(), @event.EventId, i, j, sequence.Stream, i, DateTime.Now, flags, @event.EventType, Utf8NoBom.GetBytes(body), metadata); var e = new ResolvedEvent(EventStore.Core.Data.ResolvedEvent.ForUnresolvedEvent(er, logPosition), Array.Empty <byte>()); if (@event.Skip) { yield return(For($"{projection} skips {er.EventNumber}@{sequence.Stream}", () => Assert.Null(runner.GetStatePartition(CheckpointTag.Empty, "", e)))); } else { var expectedPartition = ""; if (expectedDefinition.DefinesFold) { if (@event.ExpectedStates.Any()) { expectedPartition = @event.ExpectedStates .SingleOrDefault(x => string.Empty != x.Key).Key ?? string.Empty; if (expectedPartition != string.Empty) { yield return(For( $"{projection} {er.EventNumber}@{sequence.Stream} returns expected partition", () => Assert.Equal(expectedPartition, runner.GetStatePartition(CheckpointTag.Empty, "", e)))); foreach (var initializedState in @event.InitializedPartitions) { yield return(For( $"should not have already initialized \"{initializedState}\" at {er.EventNumber}@{sequence.Stream}", () => Assert.DoesNotContain(initializedState, (IReadOnlyDictionary <string, string>)partitionedState))); } } if (expectedDefinition.IsBiState && !sharedStateInitialized) { sharedStateInitialized = true; yield return(For("initializes shared state without error", () => { runner.InitializeShared(); })); } if ([email protected](expectedPartition)) { yield return(For( $"can load current state at {er.EventNumber}@{sequence.Stream}", () => { Assert.True( partitionedState.TryGetValue(expectedPartition, out var value), $"did not find expected state for partition{expectedPartition}"); runner.Load(value); }));