diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml
index 7be7317..09600ec 100644
--- a/.github/workflows/codecoverage.yml
+++ b/.github/workflows/codecoverage.yml
@@ -27,23 +27,23 @@ jobs:
- name: Run Unit Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "Shared.Tests\Shared.Tests.csproj" \
- /p:CollectCoverage=true \
- /p:Exclude="[xunit*]*" \
- /p:ExcludeByAttribute="Obsolete" \
- /p:ExcludeByAttribute="GeneratedCodeAttribute" \
- /p:ExcludeByAttribute="CompilerGeneratedAttribute" \
- /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" \
- /p:CoverletOutput="../lcov1.info" /maxcpucount:1 \
- /p:CoverletOutputFormat="lcov"
-
- dotnet test "Shared.EventStore.Tests\Shared.EventStore.Tests.csproj" \
- /p:CollectCoverage=true /p:Exclude="[xunit*]*" \
- /p:ExcludeByAttribute="Obsolete" \
- /p:ExcludeByAttribute="GeneratedCodeAttribute" \
- /p:ExcludeByAttribute="CompilerGeneratedAttribute" \
- /p:ExcludeByAttribute="ExcludeFromCodeCoverageAttribute" \
- /p:CoverletOutput="../lcov2.info" /maxcpucount:1 /p:CoverletOutputFormat="lcov"
+ dotnet test "Shared.Tests/Shared.Tests.csproj" \
+ --results-directory "coverage/Shared.Tests" \
+ --coverlet --coverlet-output-format lcov --coverlet-file-prefix Shared.Tests \
+ --coverlet-exclude "[xunit*]*" \
+ --coverlet-exclude-by-attribute Obsolete \
+ --coverlet-exclude-by-attribute GeneratedCodeAttribute \
+ --coverlet-exclude-by-attribute CompilerGeneratedAttribute \
+ --coverlet-exclude-by-attribute ExcludeFromCodeCoverageAttribute
+
+ dotnet test "Shared.EventStore.Tests/Shared.EventStore.Tests.csproj" \
+ --results-directory "coverage/Shared.EventStore.Tests" \
+ --coverlet --coverlet-output-format lcov --coverlet-file-prefix Shared.EventStore.Tests \
+ --coverlet-exclude "[xunit*]*" \
+ --coverlet-exclude-by-attribute Obsolete \
+ --coverlet-exclude-by-attribute GeneratedCodeAttribute \
+ --coverlet-exclude-by-attribute CompilerGeneratedAttribute \
+ --coverlet-exclude-by-attribute ExcludeFromCodeCoverageAttribute
- name: Install LCOV merger
run: npm install -g lcov-result-merger
@@ -51,7 +51,7 @@ jobs:
- name: Merge LCOV reports
run: |
mkdir -p coverage
- lcov-result-merger "*.info" > lcov.info
+ lcov-result-merger "coverage/**/*.lcov" > lcov.info
- name: Upload merged coverage to Codacy
uses: codacy/codacy-coverage-reporter-action@v1
diff --git a/.github/workflows/nightlybuild.yml b/.github/workflows/nightlybuild.yml
index dc25577..df94184 100644
--- a/.github/workflows/nightlybuild.yml
+++ b/.github/workflows/nightlybuild.yml
@@ -37,18 +37,18 @@ jobs:
- name: Run Unit Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "Shared.Tests\Shared.Tests.csproj"
+ dotnet test "Shared.Tests/Shared.Tests.csproj"
- name: Run Event Store Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "Shared.EventStore.Tests\Shared.EventStore.Tests.csproj"
- dotnet test "Shared.EventStoreContext.Tests\Shared.EventStoreContext.Tests.csproj"
+ dotnet test "Shared.EventStore.Tests/Shared.EventStore.Tests.csproj"
+ dotnet test "Shared.EventStoreContext.Tests/Shared.EventStoreContext.Tests.csproj"
- name: Run Integration Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "Shared.IntegrationTesting.Tests\Shared.IntegrationTesting.Tests.csproj"
+ dotnet test "Shared.IntegrationTesting.Tests/Shared.IntegrationTesting.Tests.csproj"
- uses: actions/upload-artifact@v4.4.3
with:
diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml
index a17c807..8afcd34 100644
--- a/.github/workflows/pullrequest.yml
+++ b/.github/workflows/pullrequest.yml
@@ -30,18 +30,18 @@ jobs:
- name: Run Unit Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "Shared.Tests\Shared.Tests.csproj"
+ dotnet test "Shared.Tests/Shared.Tests.csproj"
- name: Run Event Store Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "Shared.EventStore.Tests\Shared.EventStore.Tests.csproj"
- dotnet test "Shared.EventStoreContext.Tests\Shared.EventStoreContext.Tests.csproj"
+ dotnet test "Shared.EventStore.Tests/Shared.EventStore.Tests.csproj"
+ dotnet test "Shared.EventStoreContext.Tests/Shared.EventStoreContext.Tests.csproj"
- name: Run Integration Tests
run: |
echo "ASPNETCORE_ENVIRONMENT are > ${ASPNETCORE_ENVIRONMENT}"
- dotnet test "Shared.IntegrationTesting.Tests\Shared.IntegrationTesting.Tests.csproj"
+ dotnet test "Shared.IntegrationTesting.Tests/Shared.IntegrationTesting.Tests.csproj"
- uses: actions/upload-artifact@v4.4.3
with:
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 210567d..8f90f5a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,40 +4,38 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
+
+
-
-
-
+
-
+
-
\ No newline at end of file
+
diff --git a/Driver/Driver.csproj b/Driver/Driver.csproj
index e6e0035..7852aa6 100644
--- a/Driver/Driver.csproj
+++ b/Driver/Driver.csproj
@@ -11,6 +11,7 @@
+
diff --git a/README.md b/README.md
index 57f7627..e3ffc07 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ Other packages can be installed independently depending on the functionality req
Run the test suite with:
```bash
-dotnet test Shared.sln
+dotnet test --solution Shared.sln
```
Docker-backed integration tests require a compatible Linux Docker engine.
diff --git a/Shared.EventStore.Tests/AssemblyInfo.cs b/Shared.EventStore.Tests/AssemblyInfo.cs
index 2171200..61e212b 100644
--- a/Shared.EventStore.Tests/AssemblyInfo.cs
+++ b/Shared.EventStore.Tests/AssemblyInfo.cs
@@ -1,3 +1,4 @@
-using Xunit;
+using Xunit.Sdk;
+using Xunit.v3;
-[assembly: CollectionBehavior(DisableTestParallelization = true)]
+[assembly: Parallelization(Mode = ParallelMode.None)]
diff --git a/Shared.EventStore.Tests/Shared.EventStore.Tests.csproj b/Shared.EventStore.Tests/Shared.EventStore.Tests.csproj
index 32e8910..a13e49e 100644
--- a/Shared.EventStore.Tests/Shared.EventStore.Tests.csproj
+++ b/Shared.EventStore.Tests/Shared.EventStore.Tests.csproj
@@ -13,16 +13,16 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
+ all
+
+
all
diff --git a/Shared.EventStore/Shared.EventStore.csproj b/Shared.EventStore/Shared.EventStore.csproj
index b7c39b9..9fe6060 100644
--- a/Shared.EventStore/Shared.EventStore.csproj
+++ b/Shared.EventStore/Shared.EventStore.csproj
@@ -16,6 +16,7 @@
+
diff --git a/Shared.EventStoreContext.Tests/EventStoreContextRetryIntegrationTests.cs b/Shared.EventStoreContext.Tests/EventStoreContextRetryIntegrationTests.cs
index 0b68b4b..36b0b5e 100644
--- a/Shared.EventStoreContext.Tests/EventStoreContextRetryIntegrationTests.cs
+++ b/Shared.EventStoreContext.Tests/EventStoreContextRetryIntegrationTests.cs
@@ -60,10 +60,10 @@ public async Task EventStoreContext_InsertEvents_RetriesAfterDockerOutage()
Task restartTask = Task.Run(async () =>
{
await Task.Delay(2000);
- await this.EventStoreDockerHelper.UnpauseEventStoreContainer();
+ await this.EventStoreDockerHelper.UnpauseKurrentDbContainer();
});
- await this.EventStoreDockerHelper.PauseEventStoreContainer();
+ await this.EventStoreDockerHelper.PauseKurrentDbContainer();
try
{
diff --git a/Shared.EventStoreContext.Tests/EventStoreDockerHelper.cs b/Shared.EventStoreContext.Tests/EventStoreDockerHelper.cs
index e2acc3c..5f96799 100644
--- a/Shared.EventStoreContext.Tests/EventStoreDockerHelper.cs
+++ b/Shared.EventStoreContext.Tests/EventStoreDockerHelper.cs
@@ -18,7 +18,7 @@ public async Task StartContainers(
{
this.SetHostTraceFolder(testName);
this.ScenarioName = testName;
- await this.StartContainersForScenarioRun(testName, DockerServices.EventStore, cancellationToken);
+ await this.StartContainersForScenarioRun(testName, DockerServices.KurrentDb, cancellationToken);
}
public override async Task StopContainersForScenarioRun(DockerServices sharedDockerServices)
@@ -66,34 +66,34 @@ public override async Task StartContainersForScenarioRun(
ContainerBuilder SetupSecureEventStoreContainerLocal()
{
- this.IsSecureEventStore = true;
+ this.IsSecureKurrentDb = true;
- this.EventStoreContainerName = "UnitTestEventStore_Secure";
+ this.KurrentDbContainerName = "UnitTestKurrentDb_Secure";
- return this.SetupEventStoreContainer().WithReuse(true);
+ return this.SetupKurrentDbContainer().WithReuse(true);
}
ContainerBuilder SetupInsecureEventStoreContainerLocal()
{
- this.IsSecureEventStore = false;
+ this.IsSecureKurrentDb = false;
- this.EventStoreContainerName = "UnitTestEventStore_Insecure";
+ this.KurrentDbContainerName = "UnitTestKurrentDb_Insecure";
- return this.SetupEventStoreContainer().WithReuse(true);
+ return this.SetupKurrentDbContainer().WithReuse(true);
}
await this.StartContainer2(SetupSecureEventStoreContainerLocal,
new List {
networkService
},
- DockerServices.EventStore,
+ DockerServices.KurrentDb,
cancellationToken);
await this.StartContainer2(SetupInsecureEventStoreContainerLocal,
new List {
networkService
},
- DockerServices.EventStore,
+ DockerServices.KurrentDb,
cancellationToken);
}
@@ -101,8 +101,8 @@ public KurrentDBClientSettings CreateEventStoreClientSettings(Boolean secureEven
{
String connectionString = secureEventStore switch
{
- true => $"esdb://{userName}:{password}@127.0.0.1:{this.EventStoreSecureHttpPort}?tls=true&tlsVerifyCert=false",
- _ => $"esdb://{userName}:{password}@127.0.0.1:{this.EventStoreHttpPort}?tls=false"
+ true => $"esdb://{userName}:{password}@127.0.0.1:{this.KurrentDbSecureHttpPort}?tls=true&tlsVerifyCert=false",
+ _ => $"esdb://{userName}:{password}@127.0.0.1:{this.KurrentDbHttpPort}?tls=false"
};
KurrentDBClientSettings settings = KurrentDBClientSettings.Create(connectionString);
@@ -127,24 +127,24 @@ public KurrentDBClientSettings CreateEventStoreClientSettings(Boolean secureEven
return settings;
}
- public Task PauseEventStoreContainer()
+ public Task PauseKurrentDbContainer()
{
- return this.RunDockerCommand($"pause {this.EventStoreContainerName}");
+ return this.RunDockerCommand($"pause {this.KurrentDbContainerName}");
}
- public Task UnpauseEventStoreContainer()
+ public Task UnpauseKurrentDbContainer()
{
- return this.RunDockerCommand($"unpause {this.EventStoreContainerName}");
+ return this.RunDockerCommand($"unpause {this.KurrentDbContainerName}");
}
public Task StopEventStoreContainer()
{
- return this.RunDockerCommand($"stop -t 0 {this.EventStoreContainerName}");
+ return this.RunDockerCommand($"stop -t 0 {this.KurrentDbContainerName}");
}
- public Task StartEventStoreContainer()
+ public Task StartKurrentDbContainer()
{
- return this.RunDockerCommand($"start {this.EventStoreContainerName}");
+ return this.RunDockerCommand($"start {this.KurrentDbContainerName}");
}
private async Task RunDockerCommand(String arguments)
diff --git a/Shared.EventStoreContext.Tests/Shared.EventStoreContext.Tests.csproj b/Shared.EventStoreContext.Tests/Shared.EventStoreContext.Tests.csproj
index 90aa53c..668eb02 100644
--- a/Shared.EventStoreContext.Tests/Shared.EventStoreContext.Tests.csproj
+++ b/Shared.EventStoreContext.Tests/Shared.EventStoreContext.Tests.csproj
@@ -2,6 +2,8 @@
net10.0
+ true
+ Exe
enable
enable
Full
@@ -16,11 +18,8 @@
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/Shared.IntegrationTesting.Tests/AssemblyInfo.cs b/Shared.IntegrationTesting.Tests/AssemblyInfo.cs
index 1f8a70e..ea172eb 100644
--- a/Shared.IntegrationTesting.Tests/AssemblyInfo.cs
+++ b/Shared.IntegrationTesting.Tests/AssemblyInfo.cs
@@ -16,5 +16,6 @@
// The following GUID is for the ID of the typelib if this project is exposed to COM.
[assembly: Guid("36b1817d-245b-424f-8a32-a924b23f98ba")]
+// Scenarios remain parallelizable because shared SQL connections use per-scenario database names.
[assembly: LevelOfParallelism(2)]
-[assembly: Parallelizable(ParallelScope.Fixtures)]
\ No newline at end of file
+[assembly: Parallelizable(ParallelScope.Fixtures)]
diff --git a/Shared.IntegrationTesting.Tests/DockerHelperStartupPlanTests.cs b/Shared.IntegrationTesting.Tests/DockerHelperStartupPlanTests.cs
index 81f8576..b55cd1c 100644
--- a/Shared.IntegrationTesting.Tests/DockerHelperStartupPlanTests.cs
+++ b/Shared.IntegrationTesting.Tests/DockerHelperStartupPlanTests.cs
@@ -1,18 +1,19 @@
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Containers;
using EventStore.Client;
+using KurrentDB.Client;
using NLog;
using NUnit.Framework;
-using Shared.Logger;
using Shared.IntegrationTesting;
using Shared.IntegrationTesting.TestContainers;
+using Shared.Logger;
using Shouldly;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
namespace Shared.IntegrationTesting.Tests;
@@ -20,14 +21,43 @@ public class DockerHelperStartupPlanTests
{
private sealed class InspectableDockerHelper : TestDockerHelper
{
+ public string GetConnectionStringForTesting(string databaseName)
+ {
+ return this.SetConnectionString(databaseName);
+ }
+
+ public static string GetScenarioDatabaseNameForTesting(
+ string databaseName,
+ Guid testId,
+ bool sharedSqlServer)
+ {
+ return BaseDockerHelper.GetScenarioDatabaseName(databaseName, testId, sharedSqlServer);
+ }
+
+ public static DockerServices GetServicesForStartupForTesting(
+ DockerServices requiredServices,
+ bool hasSharedSqlServer)
+ {
+ return BaseDockerHelper.GetServicesForStartup(requiredServices, hasSharedSqlServer);
+ }
+
+ public static string GetHostTraceFolderPathForTesting(
+ string scenarioName,
+ DateTimeOffset runStartedAt,
+ bool isCI,
+ bool isWindows)
+ {
+ return BuildHostTraceFolderPath(scenarioName, runStartedAt, isCI, isWindows);
+ }
+
public IReadOnlyList> GetStartupGroupsForTesting()
{
return base.GetStartupGroups();
}
- public HttpClient GetEventStoreHealthCheckClientForTesting()
+ public HttpClient GetKurrentDbHealthCheckClientForTesting()
{
- return BaseDockerHelper.EventStoreHealthCheckClient;
+ return BaseDockerHelper.KurrentDbHealthCheckClient;
}
public Task CaptureContainerLogsForTesting(IContainer container)
@@ -73,7 +103,7 @@ protected override List GetRequiredProjections()
}
protected override Task CreateContinuousProjection(
- EventStoreProjectionManagementClient projectionClient,
+ KurrentDBProjectionManagementClient projectionClient,
string projectionName,
string projection,
CancellationToken cancellationToken)
@@ -91,7 +121,7 @@ public void StartupGroups_place_foundation_services_first_and_estate_management_
startupGroups.Select(group => group.ToArray()).ShouldBe([
[DockerServices.SqlServer],
- [DockerServices.EventStore],
+ [DockerServices.KurrentDb],
[
DockerServices.MessagingService,
DockerServices.SecurityService,
@@ -113,7 +143,7 @@ public void Shared_containers_are_retained_for_later_cleanup()
List<(DockerServices service, IContainer container)> containers =
[
(DockerServices.SqlServer, null!),
- (DockerServices.EventStore, null!)
+ (DockerServices.KurrentDb, null!)
];
IReadOnlyList<(DockerServices service, IContainer container)> remaining =
@@ -122,13 +152,125 @@ public void Shared_containers_are_retained_for_later_cleanup()
remaining.Select(container => container.service).ShouldBe([DockerServices.SqlServer]);
}
+ [Test]
+ public void Shared_SQL_server_is_not_started_by_a_scenario_helper()
+ {
+ DockerServices services = InspectableDockerHelper.GetServicesForStartupForTesting(
+ DockerServices.SqlServer | DockerServices.KurrentDb,
+ hasSharedSqlServer: true);
+
+ services.ShouldBe(DockerServices.KurrentDb);
+ }
+
+ [Test]
+ public void Scenario_helper_still_starts_SQL_server_without_a_shared_fixture()
+ {
+ DockerServices services = InspectableDockerHelper.GetServicesForStartupForTesting(
+ DockerServices.SqlServer | DockerServices.KurrentDb,
+ hasSharedSqlServer: false);
+
+ services.ShouldBe(DockerServices.SqlServer | DockerServices.KurrentDb);
+ }
+
+ [Test]
+ public void Scenario_database_names_include_the_scenario_id()
+ {
+ Guid firstTestId = Guid.NewGuid();
+ Guid secondTestId = Guid.NewGuid();
+
+ string firstDatabaseName = InspectableDockerHelper.GetScenarioDatabaseNameForTesting(
+ "TransactionProcessorReadModel",
+ firstTestId,
+ sharedSqlServer: true);
+ string secondDatabaseName = InspectableDockerHelper.GetScenarioDatabaseNameForTesting(
+ "TransactionProcessorReadModel",
+ secondTestId,
+ sharedSqlServer: true);
+
+ firstDatabaseName.ShouldBe($"TransactionProcessorReadModel-{firstTestId:N}");
+ secondDatabaseName.ShouldBe($"TransactionProcessorReadModel-{secondTestId:N}");
+ firstDatabaseName.ShouldNotBe(secondDatabaseName);
+ }
+
+ [Test]
+ public void Existing_scenario_scoped_database_names_are_not_suffixed_twice()
+ {
+ Guid testId = Guid.NewGuid();
+
+ string databaseName = InspectableDockerHelper.GetScenarioDatabaseNameForTesting(
+ $"PersistedGrantStore-{testId}",
+ testId,
+ sharedSqlServer: true);
+
+ databaseName.ShouldBe($"PersistedGrantStore-{testId:N}");
+ }
+
+ [Test]
+ public void Master_database_name_is_not_scenario_scoped()
+ {
+ InspectableDockerHelper helper = new() { TestId = Guid.NewGuid() };
+
+ helper.GetConnectionStringForTesting("master").ShouldContain("database=master;");
+ }
+
+ [Test]
+ public void Host_trace_folder_contains_the_test_run_timestamp()
+ {
+ DateTimeOffset firstRunStartedAt = new(2026, 10, 19, 16, 37, 0, TimeSpan.Zero);
+ DateTimeOffset secondRunStartedAt = new(2026, 10, 19, 16, 38, 0, TimeSpan.Zero);
+ string firstRunPath = InspectableDockerHelper.GetHostTraceFolderPathForTesting(
+ "MyScenario",
+ firstRunStartedAt,
+ isCI: false,
+ isWindows: true);
+ string secondRunPath = InspectableDockerHelper.GetHostTraceFolderPathForTesting(
+ "MyScenario",
+ secondRunStartedAt,
+ isCI: false,
+ isWindows: true);
+
+ firstRunPath.ShouldEndWith(Path.Combine("MyScenario", "20261019", "1637"));
+ secondRunPath.ShouldEndWith(Path.Combine("MyScenario", "20261019", "1638"));
+ firstRunPath.ShouldNotBe(secondRunPath);
+ }
+
+ [Test]
+ public void Local_host_trace_folder_uses_the_integration_tests_root()
+ {
+ string windowsLocalPath = InspectableDockerHelper.GetHostTraceFolderPathForTesting(
+ "MyScenario",
+ new DateTimeOffset(2026, 10, 19, 16, 37, 0, TimeSpan.Zero),
+ isCI: false,
+ isWindows: true);
+ string linuxLocalPath = InspectableDockerHelper.GetHostTraceFolderPathForTesting(
+ "MyScenario",
+ new DateTimeOffset(2026, 10, 19, 16, 37, 0, TimeSpan.Zero),
+ isCI: false,
+ isWindows: false);
+ string windowsCiPath = InspectableDockerHelper.GetHostTraceFolderPathForTesting(
+ "MyScenario",
+ new DateTimeOffset(2026, 10, 19, 16, 37, 0, TimeSpan.Zero),
+ isCI: true,
+ isWindows: true);
+ string linuxCiPath = InspectableDockerHelper.GetHostTraceFolderPathForTesting(
+ "MyScenario",
+ new DateTimeOffset(2026, 10, 19, 16, 37, 0, TimeSpan.Zero),
+ isCI: true,
+ isWindows: false);
+
+ windowsLocalPath.ShouldBe(Path.Combine(@"C:\home\integrationtests", "MyScenario", "20261019", "1637"));
+ linuxLocalPath.Replace('\\', '/').ShouldBe("/home/integrationtests/MyScenario/20261019/1637");
+ windowsCiPath.ShouldBe(Path.Combine(@"C:\Users\runneradmin\trace", "MyScenario", "20261019", "1637"));
+ linuxCiPath.Replace('\\', '/').ShouldBe("/home/runner/trace/MyScenario/20261019/1637");
+ }
+
[Test]
public void EventStore_health_checks_reuse_their_http_client()
{
InspectableDockerHelper helper = new();
- HttpClient firstClient = helper.GetEventStoreHealthCheckClientForTesting();
- HttpClient secondClient = helper.GetEventStoreHealthCheckClientForTesting();
+ HttpClient firstClient = helper.GetKurrentDbHealthCheckClientForTesting();
+ HttpClient secondClient = helper.GetKurrentDbHealthCheckClientForTesting();
secondClient.ShouldBeSameAs(firstClient);
}
diff --git a/Shared.IntegrationTesting.Tests/DockerHelperTest.feature b/Shared.IntegrationTesting.Tests/DockerHelperTest.feature
index 7a07e07..4263d6d 100644
--- a/Shared.IntegrationTesting.Tests/DockerHelperTest.feature
+++ b/Shared.IntegrationTesting.Tests/DockerHelperTest.feature
@@ -2,3 +2,5 @@
Feature: DockerHelperTest
Scenario: Test Container Startup Sequence
+
+Scenario: Test Container Startup Sequence 2
diff --git a/Shared.IntegrationTesting.Tests/DockerHelperTest.feature.cs b/Shared.IntegrationTesting.Tests/DockerHelperTest.feature.cs
index 96aa205..efdb9d7 100644
--- a/Shared.IntegrationTesting.Tests/DockerHelperTest.feature.cs
+++ b/Shared.IntegrationTesting.Tests/DockerHelperTest.feature.cs
@@ -107,7 +107,7 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa
private static global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages InitializeCucumberMessages()
{
- return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("DockerHelperTest.feature.ndjson", 3);
+ return new global::Reqnroll.Formatters.RuntimeSupport.FeatureLevelCucumberMessages("DockerHelperTest.feature.ndjson", 4);
}
[global::NUnit.Framework.TestAttribute()]
@@ -122,6 +122,30 @@ public void ScenarioInitialize(global::Reqnroll.ScenarioInfo scenarioInfo, globa
global::Reqnroll.RuleInfo ruleInfo = null;
#line 4
this.ScenarioInitialize(scenarioInfo, ruleInfo);
+#line hidden
+ if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
+ {
+ await testRunner.SkipScenarioAsync();
+ }
+ else
+ {
+ await this.ScenarioStartAsync();
+ }
+ await this.ScenarioCleanupAsync();
+ }
+
+ [global::NUnit.Framework.TestAttribute()]
+ [global::NUnit.Framework.DescriptionAttribute("Test Container Startup Sequence 2")]
+ public async global::System.Threading.Tasks.Task TestContainerStartupSequence2()
+ {
+ string[] tagsOfScenario = ((string[])(null));
+ global::System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new global::System.Collections.Specialized.OrderedDictionary();
+ string pickleIndex = "1";
+ global::Reqnroll.ScenarioInfo scenarioInfo = new global::Reqnroll.ScenarioInfo("Test Container Startup Sequence 2", null, tagsOfScenario, argumentsOfScenario, featureTags, pickleIndex);
+ string[] tagsOfRule = ((string[])(null));
+ global::Reqnroll.RuleInfo ruleInfo = null;
+#line 6
+this.ScenarioInitialize(scenarioInfo, ruleInfo);
#line hidden
if ((global::Reqnroll.TagHelper.ContainsIgnoreTag(scenarioInfo.CombinedTags) || global::Reqnroll.TagHelper.ContainsIgnoreTag(featureTags)))
{
diff --git a/Shared.IntegrationTesting.Tests/GenericSteps.cs b/Shared.IntegrationTesting.Tests/GenericSteps.cs
index c606e35..6c7c554 100644
--- a/Shared.IntegrationTesting.Tests/GenericSteps.cs
+++ b/Shared.IntegrationTesting.Tests/GenericSteps.cs
@@ -41,7 +41,7 @@ public async Task StartSystem() {
this.TestingContext.DockerHelper = new TestDockerHelper();
this.TestingContext.DockerHelper.Logger = logger;
- DockerServices services = DockerServices.SqlServer | DockerServices.EventStore | DockerServices.MessagingService | DockerServices.SecurityService |
+ DockerServices services = DockerServices.SqlServer | DockerServices.KurrentDb | DockerServices.MessagingService | DockerServices.SecurityService |
DockerServices.CallbackHandler | DockerServices.FileProcessor |
DockerServices.TestHost | DockerServices.TransactionProcessor |
DockerServices.TransactionProcessorAcl | DockerServices.ConfigurationHost |
@@ -50,10 +50,24 @@ public async Task StartSystem() {
this.TestingContext.Logger = logger;
this.TestingContext.Logger.LogInformation("About to Start Containers for Scenario Run");
this.TestingContext.DockerHelper.ScenarioName = scenarioName;
- await this.TestingContext.DockerHelper.StartContainersForScenarioRun(
- scenarioName,
- services,
- CancellationToken.None).ConfigureAwait(false);
+ System.Diagnostics.Stopwatch startupTimer = System.Diagnostics.Stopwatch.StartNew();
+ Setup.RecordInfrastructureStartupStarted();
+ try
+ {
+ this.TestingContext.DockerHelper.UseSharedSqlServer(
+ await Setup.GetSqlServerFixtureAsync().ConfigureAwait(false));
+ await this.TestingContext.DockerHelper.StartContainersForScenarioRun(
+ scenarioName,
+ services,
+ CancellationToken.None).ConfigureAwait(false);
+ }
+ finally
+ {
+ startupTimer.Stop();
+ Setup.RecordInfrastructureStartupCompleted();
+ this.TestingContext.Logger.LogInformation(
+ $"Scenario infrastructure startup completed in {startupTimer.Elapsed.TotalSeconds:N1}s");
+ }
this.TestingContext.Logger.LogInformation("Containers for Scenario Run Started");
}
diff --git a/Shared.IntegrationTesting.Tests/Setup.cs b/Shared.IntegrationTesting.Tests/Setup.cs
index 673487e..fdad5fa 100644
--- a/Shared.IntegrationTesting.Tests/Setup.cs
+++ b/Shared.IntegrationTesting.Tests/Setup.cs
@@ -12,9 +12,94 @@ namespace Shared.IntegrationTesting.Tests;
[Binding]
public class Setup
{
+ private static readonly SemaphoreSlim SqlServerFixtureLock = new(1, 1);
+ private static Shared.IntegrationTesting.TestContainers.SqlServerTestRunFixture? sqlServerFixture;
+ private static long InfrastructureStartupStartedTimestamp;
+ private static long InfrastructureStartupCompletedTimestamp;
+
+ public static async Task GetSqlServerFixtureAsync()
+ {
+ await SqlServerFixtureLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ if (sqlServerFixture is null)
+ {
+ System.Diagnostics.Stopwatch startupTimer = System.Diagnostics.Stopwatch.StartNew();
+ try
+ {
+ sqlServerFixture = await Shared.IntegrationTesting.TestContainers.SqlServerTestRunFixture
+ .StartAsync().ConfigureAwait(false);
+ }
+ finally
+ {
+ startupTimer.Stop();
+ LogManager.GetLogger(nameof(Setup)).Info(
+ "Shared SQL Server fixture startup completed in {0:N1}s",
+ startupTimer.Elapsed.TotalSeconds);
+ }
+ }
+
+ return sqlServerFixture;
+ }
+ finally
+ {
+ SqlServerFixtureLock.Release();
+ }
+ }
+
[BeforeTestRun]
- protected static Task GlobalSetup(){
+ public static Task GlobalSetup()
+ {
ShouldlyConfiguration.DefaultTaskTimeout = TimeSpan.FromMinutes(1);
return Task.CompletedTask;
}
+
+ [AfterTestRun]
+ public static async Task GlobalTeardown()
+ {
+ try
+ {
+ if (sqlServerFixture is not null)
+ {
+ await sqlServerFixture.DisposeAsync().ConfigureAwait(false);
+ sqlServerFixture = null;
+ }
+ }
+ finally
+ {
+ long startedTimestamp = Interlocked.Read(ref InfrastructureStartupStartedTimestamp);
+ long completedTimestamp = Interlocked.Read(ref InfrastructureStartupCompletedTimestamp);
+ TimeSpan totalStartup = startedTimestamp == 0 || completedTimestamp < startedTimestamp
+ ? TimeSpan.Zero
+ : TimeSpan.FromSeconds(
+ (completedTimestamp - startedTimestamp) / (double)System.Diagnostics.Stopwatch.Frequency);
+ LogManager.GetLogger(nameof(Setup)).Info(
+ "Integration-test infrastructure startup wall time (first scenario request to last scenario ready): {0:N1}s",
+ totalStartup.TotalSeconds);
+ }
+ }
+
+ internal static void RecordInfrastructureStartupStarted()
+ {
+ long timestamp = System.Diagnostics.Stopwatch.GetTimestamp();
+ Interlocked.CompareExchange(ref InfrastructureStartupStartedTimestamp, timestamp, 0);
+ }
+
+ internal static void RecordInfrastructureStartupCompleted()
+ {
+ long timestamp = System.Diagnostics.Stopwatch.GetTimestamp();
+ long previousTimestamp;
+ do
+ {
+ previousTimestamp = Interlocked.Read(ref InfrastructureStartupCompletedTimestamp);
+ if (previousTimestamp >= timestamp)
+ {
+ return;
+ }
+ }
+ while (Interlocked.CompareExchange(
+ ref InfrastructureStartupCompletedTimestamp,
+ timestamp,
+ previousTimestamp) != previousTimestamp);
+ }
}
diff --git a/Shared.IntegrationTesting.Tests/Shared.IntegrationTesting.Tests.csproj b/Shared.IntegrationTesting.Tests/Shared.IntegrationTesting.Tests.csproj
index 3817c0e..3ec91af 100644
--- a/Shared.IntegrationTesting.Tests/Shared.IntegrationTesting.Tests.csproj
+++ b/Shared.IntegrationTesting.Tests/Shared.IntegrationTesting.Tests.csproj
@@ -2,6 +2,8 @@
net10.0
+ true
+ Exe
enable
enable
Full
@@ -20,11 +22,8 @@
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
+
diff --git a/Shared.IntegrationTesting.UnitTests/Shared.IntegrationTesting.UnitTests.csproj b/Shared.IntegrationTesting.UnitTests/Shared.IntegrationTesting.UnitTests.csproj
index 5d8bfb9..8ff89d1 100644
--- a/Shared.IntegrationTesting.UnitTests/Shared.IntegrationTesting.UnitTests.csproj
+++ b/Shared.IntegrationTesting.UnitTests/Shared.IntegrationTesting.UnitTests.csproj
@@ -16,15 +16,12 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
all
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
diff --git a/Shared.IntegrationTesting/ContainerType.cs b/Shared.IntegrationTesting/ContainerType.cs
index 9adf8a9..21875e0 100644
--- a/Shared.IntegrationTesting/ContainerType.cs
+++ b/Shared.IntegrationTesting/ContainerType.cs
@@ -5,7 +5,7 @@ namespace Shared.IntegrationTesting;
public enum ContainerType
{
SqlServer,
- EventStore,
+ KurrentDb,
MessagingService,
SecurityService,
CallbackHandler,
@@ -24,7 +24,7 @@ public enum DockerServices
{
None = 0,
SqlServer = 1,
- EventStore = 2,
+ KurrentDb = 2,
MessagingService = 4,
SecurityService = 8,
CallbackHandler = 16,
diff --git a/Shared.IntegrationTesting/DockerPorts.cs b/Shared.IntegrationTesting/DockerPorts.cs
index 806dd11..151689e 100644
--- a/Shared.IntegrationTesting/DockerPorts.cs
+++ b/Shared.IntegrationTesting/DockerPorts.cs
@@ -8,9 +8,9 @@ public static class DockerPorts
public static readonly Int32 CallbackHandlerDockerPort = 5010;
- public static readonly Int32 EventStoreHttpDockerPort = 2113;
+ public static readonly Int32 KurrentDbHttpDockerPort = 2113;
- public static readonly Int32 EventStoreTcpDockerPort = 1113;
+ //public static readonly Int32 EventStoreTcpDockerPort = 1113;
public static readonly Int32 MessagingServiceDockerPort = 5006;
diff --git a/Shared.IntegrationTesting/Shared.IntegrationTesting.csproj b/Shared.IntegrationTesting/Shared.IntegrationTesting.csproj
index 652d867..4c4b376 100644
--- a/Shared.IntegrationTesting/Shared.IntegrationTesting.csproj
+++ b/Shared.IntegrationTesting/Shared.IntegrationTesting.csproj
@@ -12,9 +12,9 @@
-
-
+
+
diff --git a/Shared.IntegrationTesting/TestContainers/BaseDockerHelper.cs b/Shared.IntegrationTesting/TestContainers/BaseDockerHelper.cs
index 6f76c74..368607b 100644
--- a/Shared.IntegrationTesting/TestContainers/BaseDockerHelper.cs
+++ b/Shared.IntegrationTesting/TestContainers/BaseDockerHelper.cs
@@ -12,6 +12,7 @@ namespace Shared.IntegrationTesting.TestContainers;
using Docker.DotNet;
using EventStore.Client;
using HealthChecks;
+using KurrentDB.Client;
using Logger;
using Microsoft.Data.SqlClient;
using Shouldly;
@@ -58,12 +59,12 @@ public abstract class BaseDockerHelper{
protected readonly Object ContainerStateLock = new();
- protected String EventStoreContainerName;
+ protected String KurrentDbContainerName;
public String ConfigHostContainerName;
public Int32 ConfigHostPort;
- protected Int32 EventStoreHttpPort;
- protected Int32 EventStoreSecureHttpPort;
+ protected Int32 KurrentDbHttpPort;
+ protected Int32 KurrentDbSecureHttpPort;
protected String FileProcessorContainerName;
@@ -71,7 +72,7 @@ public abstract class BaseDockerHelper{
protected readonly IHealthCheckClient HealthCheckClient;
- protected static readonly HttpClient EventStoreHealthCheckClient = CreateEventStoreHealthCheckClient();
+ protected static readonly HttpClient KurrentDbHealthCheckClient = CreateKurrentDbHealthCheckClient();
protected Dictionary HostPorts = new();
@@ -107,6 +108,8 @@ public abstract class BaseDockerHelper{
protected Boolean UseSecureSqlServerDatabase;
+ protected SqlServerTestRunFixture SharedSqlServerFixture;
+
private String sqlTestConnString;
@@ -125,7 +128,7 @@ protected BaseDockerHelper(Boolean skipHealthChecks=false) {
}));
this.ImageDetails.Add(ContainerType.SqlServer, ("mcr.microsoft.com/mssql/server:2025-latest", true));
- this.ImageDetails.Add(ContainerType.EventStore, ("kurrentplatform/kurrentdb:26.2.2", true));
+ this.ImageDetails.Add(ContainerType.KurrentDb, ("kurrentplatform/kurrentdb:26.2.2", true));
this.ImageDetails.Add(ContainerType.MessagingService, ("stuartferguson/messagingservice:master", true));
this.ImageDetails.Add(ContainerType.SecurityService, ("stuartferguson/securityservice:master", true));
this.ImageDetails.Add(ContainerType.CallbackHandler, ("stuartferguson/callbackhandler:master", true));
@@ -142,7 +145,7 @@ protected BaseDockerHelper(Boolean skipHealthChecks=false) {
- public Boolean IsSecureEventStore{ get; protected set; }
+ public Boolean IsSecureKurrentDb { get; protected set; }
@@ -167,7 +170,7 @@ public virtual Dictionary GetCommonEnvironmentVariables(){
Int32 securityServicePort = this.GetSecurityServicePort();
return new Dictionary() {
- {"EventStoreSettings:ConnectionString", this.GenerateEventStoreConnectionString()},
+ {"EventStoreSettings:ConnectionString", this.GenerateKurrentDbConnectionString()},
{"AppSettings:PersistentSubscriptionPollingInSeconds", this.PersistentSubscriptionSettings.pollingInterval.ToString()},
{"AppSettings:InternalSubscriptionServiceCacheDuration", this.PersistentSubscriptionSettings.cacheDuration.ToString()},
{"AppSettings:SubscriptionConfiguration:PersistentSubscriptionPollingInSeconds", this.PersistentSubscriptionSettings.pollingInterval.ToString()},
@@ -180,7 +183,7 @@ public virtual Dictionary GetCommonEnvironmentVariables(){
{"AppSettings:TransactionProcessorApi", $"http://{this.TransactionProcessorContainerName}:{DockerPorts.TransactionProcessorDockerPort}"},
{"AppSettings:FileProcessorApi", $"http://{this.FileProcessorContainerName}:{DockerPorts.FileProcessorDockerPort}"},
{"ConnectionStrings:HealthCheck", this.SetConnectionString("master", this.UseSecureSqlServerDatabase)},
- {"\"EventStoreSettings:Insecure", this.IsSecureEventStore.ToString()}
+ {"EventStoreSettings:Insecure", this.IsSecureKurrentDb.ToString()}
};
}
@@ -189,7 +192,7 @@ protected virtual IReadOnlyList> GetStartupGroups(
{
List> startupGroups = [
[DockerServices.SqlServer],
- [DockerServices.EventStore],
+ [DockerServices.KurrentDb],
[
DockerServices.MessagingService,
DockerServices.SecurityService,
@@ -218,8 +221,7 @@ public static DockerClient GetDockerHost() {
public Int32? GetHostPort(ContainerType key) =>
key switch {
- ContainerType.CallbackHandler => this.CallbackHandlerPort,
- //ContainerType.EventStore => this.EventStoreHttpPort,
+ ContainerType.CallbackHandler => this.CallbackHandlerPort,
ContainerType.ConfigurationHost => this.ConfigHostPort,
ContainerType.FileProcessor => this.FileProcessorPort,
ContainerType.MessagingService => this.MessagingServicePort,
@@ -230,8 +232,8 @@ public static DockerClient GetDockerHost() {
ContainerType.SqlServer => this.SqlServerPort,
ContainerType.EstateManagementUI => this.EstateManagementUiPort,
ContainerType.EstateReporting => this.EstateReportingPort,
- _ when key == ContainerType.EventStore && this.IsSecureEventStore => this.EventStoreSecureHttpPort,
- _ when key == ContainerType.EventStore && this.IsSecureEventStore == false => this.EventStoreHttpPort,
+ _ when key == ContainerType.KurrentDb && this.IsSecureKurrentDb => this.KurrentDbSecureHttpPort,
+ _ when key == ContainerType.KurrentDb && this.IsSecureKurrentDb == false => this.KurrentDbHttpPort,
_ => null
};
@@ -291,7 +293,7 @@ public virtual ContainerBuilder SetupCallbackHandlerContainer(){
public virtual void SetupContainerNames(){
// Setup the container names
this.SqlServerContainerName= $"sqlserver{this.TestId:N}";
- this.EventStoreContainerName = $"eventstore{this.TestId:N}";
+ this.KurrentDbContainerName = $"kurrentdb{this.TestId:N}";
this.SecurityServiceContainerName = $"securityservice{this.TestId:N}";
this.TestHostContainerName = $"testhosts{this.TestId:N}";
this.CallbackHandlerContainerName = $"callbackhandler{this.TestId:N}";
@@ -307,10 +309,18 @@ public virtual void SetupContainerNames(){
protected Int32 EstateManagementUiPort;
protected Int32 SqlServerPort;
+ public void UseSharedSqlServer(SqlServerTestRunFixture fixture)
+ {
+ this.SharedSqlServerFixture = fixture ?? throw new ArgumentNullException(nameof(fixture));
+ this.SqlServerContainerName = fixture.ContainerName;
+ this.SqlServerPort = fixture.MappedPort;
+ this.SqlCredentials = (fixture.UserName, fixture.Password);
+ }
+
protected String EstateManagementUiContainerName;
- public virtual ContainerBuilder SetupEventStoreContainer(){
- this.Trace("About to Start Event Store Container");
+ public virtual ContainerBuilder SetupKurrentDbContainer(){
+ this.Trace("About to Start Kurrent Db Container");
Dictionary environmentVariables = new(){
{"EVENTSTORE_RUN_PROJECTIONS","all"},
@@ -318,18 +328,18 @@ public virtual ContainerBuilder SetupEventStoreContainer(){
{"EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP","true"},
{"EVENTSTORE_PROJECTION_EXECUTION_TIMEOUT","5000"}
};
- ContainerBuilder eventStoreContainer = new ContainerBuilder();
+ ContainerBuilder kurrentDbContainer = new();
- if (!this.IsSecureEventStore){
+ if (!this.IsSecureKurrentDb){
environmentVariables.Add("EVENTSTORE_INSECURE","true");
}
else{
- String certsPath = "/etc/eventstore/certs";
+ String certsPath = "/etc/kurrentdb/certs";
// Copy these to the container
String path = Path.Combine(Directory.GetCurrentDirectory(), "certs");
- eventStoreContainer = eventStoreContainer.MountHostFolder(path, certsPath);
+ kurrentDbContainer = kurrentDbContainer.MountHostFolder(path, certsPath);
// Certificates configuration
environmentVariables.Add("EVENTSTORE_CertificateFile",$"{certsPath}/node1/node.crt");
@@ -339,19 +349,19 @@ public virtual ContainerBuilder SetupEventStoreContainer(){
}
String logfilePath = "/var/log/kurrentdb";
- eventStoreContainer = eventStoreContainer.MountHostFolder(this.HostTraceFolder, logfilePath);
+ kurrentDbContainer = kurrentDbContainer.MountHostFolder(this.HostTraceFolder, logfilePath);
+
+ (String imageName, Boolean useLatest) imageDetails = this.GetImageDetails(ContainerType.KurrentDb).Data;
+ this.Trace($"About to Start Kurrent Db Container using image [{imageDetails.imageName}]");
- (String imageName, Boolean useLatest) imageDetails = this.GetImageDetails(ContainerType.EventStore).Data;
- this.Trace($"About to Start Event Store Container using image [{imageDetails.imageName}]");
-
- eventStoreContainer = eventStoreContainer.WithName(this.EventStoreContainerName) // similar to WithName()
+ kurrentDbContainer = kurrentDbContainer.WithName(this.KurrentDbContainerName) // similar to WithName()
.WithImage(imageDetails.imageName)
.WithEnvironment(environmentVariables)
- .WithPortBinding(DockerPorts.EventStoreHttpDockerPort, true);
+ .WithPortBinding(DockerPorts.KurrentDbHttpDockerPort, true);
- return eventStoreContainer;
+ return kurrentDbContainer;
}
public virtual ContainerBuilder SetupFileProcessorContainer(){
@@ -747,20 +757,20 @@ protected void CheckSqlConnection(IContainer databaseServerContainer){
}
}
- protected virtual EventStoreClientSettings ConfigureEventStoreSettings(String username = "admin", String password="changeit"){
- String connectionString = $"esdb://{username}:{password}@127.0.0.1:{this.EventStoreHttpPort}";
+ protected virtual KurrentDBClientSettings ConfigureKurrentDbSettings(String username = "admin", String password="changeit"){
+ String connectionString = $"esdb://{username}:{password}@127.0.0.1:{this.KurrentDbHttpPort}";
- connectionString = this.IsSecureEventStore switch{
+ connectionString = this.IsSecureKurrentDb switch{
true => $"{connectionString}?tls=true&tlsVerifyCert=false",
_ => $"{connectionString}?tls=false&tlsVerifyCert=false"
};
- return EventStoreClientSettings.Create(connectionString);
+ return KurrentDBClientSettings.Create(connectionString);
}
protected virtual async Task CreatePersistentSubscription((String streamName, String groupName, Int32 maxRetryCount) subscription){
- EventStorePersistentSubscriptionsClient client = new(this.ConfigureEventStoreSettings());
-
+ KurrentDBPersistentSubscriptionsClient client = new(this.ConfigureKurrentDbSettings());
+
PersistentSubscriptionSettings settings = new(resolveLinkTos:true, StreamPosition.Start, maxRetryCount:subscription.maxRetryCount);
this.Trace($"Creating persistent subscription Group [{subscription.groupName}] Stream [{subscription.streamName}] Retry Count [{subscription.maxRetryCount}]");
await client.CreateToStreamAsync(subscription.streamName, subscription.groupName, settings);
@@ -803,7 +813,7 @@ protected async Task DoSqlServerHealthCheck(
}
}
protected async Task DoEventStoreHealthCheck(CancellationToken cancellationToken){
- String scheme = this.IsSecureEventStore switch
+ String scheme = this.IsSecureKurrentDb switch
{
true => "https",
_ => "http"
@@ -812,9 +822,9 @@ protected async Task DoEventStoreHealthCheck(CancellationToken cancellationToken
await Retry.For(async cancellationToken =>
{
cancellationToken.ThrowIfCancellationRequested();
- String url = $"{scheme}://127.0.0.1:{this.EventStoreHttpPort}/ping";
+ String url = $"{scheme}://127.0.0.1:{this.KurrentDbHttpPort}/ping";
- using HttpResponseMessage pingResponse = await EventStoreHealthCheckClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ using HttpResponseMessage pingResponse = await KurrentDbHealthCheckClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
pingResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
},
cancellationToken,
@@ -826,9 +836,9 @@ await Retry.For(async cancellationToken =>
await Retry.For(async cancellationToken =>
{
cancellationToken.ThrowIfCancellationRequested();
- String url = $"{scheme}://127.0.0.1:{this.EventStoreHttpPort}/info";
+ String url = $"{scheme}://127.0.0.1:{this.KurrentDbHttpPort}/info";
- using HttpResponseMessage infoResponse = await EventStoreHealthCheckClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ using HttpResponseMessage infoResponse = await KurrentDbHealthCheckClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
infoResponse.StatusCode.ShouldBe(HttpStatusCode.OK);
String infoData = await infoResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
@@ -837,7 +847,7 @@ await Retry.For(async cancellationToken =>
}, cancellationToken);
}
- private static HttpClient CreateEventStoreHealthCheckClient()
+ private static HttpClient CreateKurrentDbHealthCheckClient()
{
HttpClientHandler httpClientHandler = new()
{
@@ -898,8 +908,8 @@ protected void Error(String message, Exception ex){
}
}
- protected virtual String GenerateEventStoreConnectionString(String userName = "admin", String password = "changeit"){
- String eventStoreAddress = $"esdb://{userName}:{password}@{this.EventStoreContainerName}:{DockerPorts.EventStoreHttpDockerPort}?tls=false";
+ protected virtual String GenerateKurrentDbConnectionString(String userName = "admin", String password = "changeit"){
+ String eventStoreAddress = $"esdb://{userName}:{password}@{this.KurrentDbContainerName}:{DockerPorts.KurrentDbHttpDockerPort}?tls=false";
return eventStoreAddress;
}
@@ -930,7 +940,7 @@ protected virtual async Task LoadEventStoreProjections(CancellationToken cancell
{
IReadOnlyList files = this.GetProjectionFiles();
var requiredProjections = this.GetRequiredProjections();
- EventStoreProjectionManagementClient projectionClient = new(this.ConfigureEventStoreSettings());
+ KurrentDBProjectionManagementClient projectionClient = new(this.ConfigureKurrentDbSettings());
List projectionNames = new();
foreach (FileInfo file in files)
@@ -988,7 +998,7 @@ protected virtual IReadOnlyList GetProjectionFiles()
}
protected virtual Task CreateContinuousProjection(
- EventStoreProjectionManagementClient projectionClient,
+ KurrentDBProjectionManagementClient projectionClient,
String projectionName,
String projection,
CancellationToken cancellationToken)
@@ -1001,7 +1011,7 @@ protected virtual Task CreateContinuousProjection(
}
protected virtual Task GetProjectionStatus(
- EventStoreProjectionManagementClient projectionClient,
+ KurrentDBProjectionManagementClient projectionClient,
String projectionName,
CancellationToken cancellationToken)
{
@@ -1037,13 +1047,49 @@ protected virtual String SetConnectionString(String databaseName,
encryptValue = ";Encrypt=False";
}
+ String scenarioDatabaseName = GetScenarioDatabaseName(
+ databaseName,
+ this.TestId,
+ this.SharedSqlServerFixture is not null);
+
String connectionString =
- $"server={this.SqlServerContainerName},1433;user id={this.SqlCredentials.usename};password={this.SqlCredentials.password};database={databaseName}{encryptValue}";
+ $"server={this.SqlServerContainerName},1433;user id={this.SqlCredentials.usename};password={this.SqlCredentials.password};database={scenarioDatabaseName}{encryptValue}";
return connectionString;
}
+ protected static String GetScenarioDatabaseName(
+ String databaseName,
+ Guid testId,
+ Boolean sharedSqlServer)
+ {
+ if (sharedSqlServer == false || databaseName == "master")
+ {
+ return databaseName;
+ }
+
+ String compactSuffix = $"-{testId:N}";
+ if (databaseName.EndsWith(compactSuffix, StringComparison.OrdinalIgnoreCase))
+ {
+ return databaseName;
+ }
+
+ String dashedSuffix = $"-{testId:D}";
+ return databaseName.EndsWith(dashedSuffix, StringComparison.OrdinalIgnoreCase)
+ ? $"{databaseName[..^dashedSuffix.Length]}{compactSuffix}"
+ : $"{databaseName}{compactSuffix}";
+ }
+
+ protected static DockerServices GetServicesForStartup(
+ DockerServices requiredServices,
+ Boolean hasSharedSqlServer)
+ {
+ return hasSharedSqlServer
+ ? requiredServices & ~DockerServices.SqlServer
+ : requiredServices;
+ }
+
protected async Task StartContainer2(
Func buildContainerFunc,
List networkServices,
@@ -1098,7 +1144,7 @@ await Retry.For(async cancellationToken =>
DockerServices.TestHost => ContainerType.TestHost,
DockerServices.TransactionProcessor => ContainerType.TransactionProcessor,
DockerServices.TransactionProcessorAcl => ContainerType.TransactionProcessorAcl,
- DockerServices.EventStore=> ContainerType.EventStore,
+ DockerServices.KurrentDb => ContainerType.KurrentDb,
DockerServices.SqlServer => ContainerType.SqlServer,
DockerServices.ConfigurationHost => ContainerType.ConfigurationHost,
DockerServices.EstateManagementUI => ContainerType.EstateManagementUI,
@@ -1119,7 +1165,7 @@ await Retry.For(async cancellationToken =>
switch (type)
{
- case ContainerType.EventStore:
+ case ContainerType.KurrentDb:
await DoEventStoreHealthCheck(cancellationToken);
break;
case ContainerType.SqlServer:
@@ -1179,11 +1225,11 @@ UInt16 GetPort(Int32 dockerPort) =>
startedContainer.GetMappedPublicPort(dockerPort);
switch (type) {
- case ContainerType.EventStore:
- Int32 port = GetPort(DockerPorts.EventStoreHttpDockerPort);
- EventStoreHttpPort = port;
- if (IsSecureEventStore)
- EventStoreSecureHttpPort = port;
+ case ContainerType.KurrentDb:
+ Int32 port = GetPort(DockerPorts.KurrentDbHttpDockerPort);
+ KurrentDbHttpPort = port;
+ if (IsSecureKurrentDb)
+ KurrentDbSecureHttpPort = port;
break;
case ContainerType.MessagingService:
diff --git a/Shared.IntegrationTesting/TestContainers/DockerHelper.cs b/Shared.IntegrationTesting/TestContainers/DockerHelper.cs
index b17bb64..7c62047 100644
--- a/Shared.IntegrationTesting/TestContainers/DockerHelper.cs
+++ b/Shared.IntegrationTesting/TestContainers/DockerHelper.cs
@@ -9,6 +9,7 @@ namespace Shared.IntegrationTesting.TestContainers;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@@ -20,14 +21,13 @@ protected DockerHelper(Boolean skipHealthChecks=false) :base(skipHealthChecks){
StringSerialiser.Initialise(new SystemTextJsonSerializer(new System.Text.Json.JsonSerializerOptions()));
}
- protected virtual void SetHostTraceFolder(String scenarioName) {
+ protected virtual void SetHostTraceFolder(String scenarioName, DateTimeOffset? runStartedAt = null) {
String ciEnvVar = Environment.GetEnvironmentVariable("CI");
Boolean isCI = (!String.IsNullOrEmpty(ciEnvVar) && String.Compare(ciEnvVar, Boolean.TrueString, StringComparison.InvariantCultureIgnoreCase) == 0);
+ Boolean isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
- this.HostTraceFolder = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
- ? (isCI ? $@"C:\Users\runneradmin\trace\{scenarioName}" : $@"C:\home\txnproc\trace\{scenarioName}")
- : (isCI ? $"/home/runner/trace/{scenarioName}" : $"/home/txnproc/trace/{scenarioName}");
+ this.HostTraceFolder = BuildHostTraceFolderPath(scenarioName, runStartedAt, isCI, isWindows);
if (Directory.Exists(this.HostTraceFolder) == false){
this.Trace($"[{this.HostTraceFolder}] does not exist");
@@ -41,6 +41,25 @@ protected virtual void SetHostTraceFolder(String scenarioName) {
this.Trace($"HostTraceFolder is [{this.HostTraceFolder}]");
}
+ protected static String BuildHostTraceFolderPath(
+ String scenarioName,
+ DateTimeOffset? runStartedAt,
+ Boolean isCI,
+ Boolean isWindows)
+ {
+ String rootFolder = isWindows
+ ? (isCI ? @"C:\Users\runneradmin\trace" : @"C:\home\integrationtests")
+ : (isCI ? "/home/runner/trace" : "/home/integrationtests");
+
+ return runStartedAt is null
+ ? Path.Combine(rootFolder, scenarioName)
+ : Path.Combine(
+ rootFolder,
+ scenarioName,
+ runStartedAt.Value.ToString("yyyyMMdd", CultureInfo.InvariantCulture),
+ runStartedAt.Value.ToString("HHmm", CultureInfo.InvariantCulture));
+ }
+
public override async Task StartContainersForScenarioRun(
String scenarioName,
@@ -53,25 +72,37 @@ public override async Task StartContainersForScenarioRun(
{
this.RequiredDockerServices = dockerServices;
- this.IsSecureEventStore = Environment.GetEnvironmentVariable("IsSecureEventStore") switch
+ this.IsSecureKurrentDb = Environment.GetEnvironmentVariable("IsSecureEventStore") switch
{
null => false,
{ Length: 0 } => false,
"false" => false,
_ => true
};
- this.SetHostTraceFolder(scenarioName);
+ this.SetHostTraceFolder(scenarioName, this.SharedSqlServerFixture?.StartedAt);
this.TestId = Guid.NewGuid();
this.Trace($"Test Id is {this.TestId}");
this.SetupContainerNames();
+ if (this.SharedSqlServerFixture is not null)
+ {
+ this.SqlServerContainerName = this.SharedSqlServerFixture.ContainerName;
+ }
this.ClientDetails = ("serviceClient", "Secret1");
- INetwork testNetwork = await this.SetupTestNetwork();
- this.TestNetworks.Add(testNetwork);
+ INetwork testNetwork;
+ if (this.SharedSqlServerFixture is null)
+ {
+ testNetwork = await this.SetupTestNetwork();
+ this.TestNetworks.Add(testNetwork);
+ }
+ else
+ {
+ testNetwork = this.SharedSqlServerFixture.Network;
+ }
List networks = [
testNetwork,
@@ -89,7 +120,7 @@ async Task StartWithTrace(Func
Func> setupContainerForService = service => service switch
{
DockerServices.SqlServer => this.ConfigureSqlContainer,
- DockerServices.EventStore => this.SetupEventStoreContainer,
+ DockerServices.KurrentDb => this.SetupKurrentDbContainer,
DockerServices.MessagingService => this.SetupMessagingServiceContainer,
DockerServices.SecurityService => this.SetupSecurityServiceContainer,
DockerServices.CallbackHandler => this.SetupCallbackHandlerContainer,
@@ -103,10 +134,16 @@ async Task StartWithTrace(Func
_ => throw new InvalidOperationException($"No startup plan exists for docker service [{service}]")
};
+ DockerServices servicesForStartup = GetServicesForStartup(
+ this.RequiredDockerServices,
+ this.SharedSqlServerFixture is not null);
+
foreach (IReadOnlyList startupGroup in this.GetStartupGroups())
{
cancellationToken.ThrowIfCancellationRequested();
- await Task.WhenAll(startupGroup.Select(service => StartWithTrace(setupContainerForService(service), service)));
+ await Task.WhenAll(startupGroup
+ .Where(service => (servicesForStartup & service) == service)
+ .Select(service => StartWithTrace(setupContainerForService(service), service)));
}
await this.LoadEventStoreProjections(cancellationToken);
@@ -114,8 +151,21 @@ async Task StartWithTrace(Func
cancellationToken.ThrowIfCancellationRequested();
await this.CreateSubscriptions(cancellationToken);
}
- catch
+ catch (Exception startupException)
{
+ try
+ {
+ await this.StopContainersForScenarioRun(DockerServices.None).ConfigureAwait(false);
+ }
+ catch (Exception cleanupException)
+ {
+ this.DisposeIntegrationTestCertificate();
+ throw new AggregateException(
+ "Scenario container startup failed and cleanup also failed.",
+ startupException,
+ cleanupException);
+ }
+
this.DisposeIntegrationTestCertificate();
throw;
}
diff --git a/Shared.IntegrationTesting/TestContainers/SqlServerTestRunFixture.cs b/Shared.IntegrationTesting/TestContainers/SqlServerTestRunFixture.cs
new file mode 100644
index 0000000..8a4e71d
--- /dev/null
+++ b/Shared.IntegrationTesting/TestContainers/SqlServerTestRunFixture.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+using DotNet.Testcontainers.Networks;
+using Microsoft.Data.SqlClient;
+
+namespace Shared.IntegrationTesting.TestContainers;
+
+public sealed class SqlServerTestRunFixture : IAsyncDisposable
+{
+ private const String SqlServerImage = "mcr.microsoft.com/mssql/server:2025-latest";
+ private const String SqlServerPassword = "thisisalongpassword123!";
+ private const Int32 HealthCheckAttempts = 10;
+
+ private readonly IContainer container;
+ private readonly INetwork network;
+ private Boolean IsDisposed;
+
+ private SqlServerTestRunFixture(
+ String runId,
+ DateTimeOffset startedAt,
+ String containerName,
+ IContainer container,
+ INetwork network)
+ {
+ this.RunId = runId;
+ this.StartedAt = startedAt;
+ this.ContainerName = containerName;
+ this.container = container;
+ this.network = network;
+ this.Network = network;
+ this.UserName = "sa";
+ this.Password = SqlServerPassword;
+ }
+
+ public String ContainerName { get; }
+
+ public String RunId { get; }
+
+ public DateTimeOffset StartedAt { get; }
+
+ public UInt16 MappedPort { get; private set; }
+
+ public INetwork Network { get; }
+
+ public String UserName { get; }
+
+ public String Password { get; }
+
+ public static async Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ String runId = Guid.NewGuid().ToString("N");
+ DateTimeOffset startedAt = DateTimeOffset.Now;
+ String containerName = $"shared-sqlserver-{runId}";
+ INetwork network = new NetworkBuilder()
+ .WithName($"shared-test-run-{runId}")
+ .WithLabel("test-run-id", runId)
+ .Build();
+ IContainer container = new ContainerBuilder(SqlServerImage)
+ .WithName(containerName)
+ .WithNetwork(network)
+ .WithNetworkAliases(containerName)
+ .WithEnvironment("ACCEPT_EULA", "Y")
+ .WithEnvironment("SA_PASSWORD", SqlServerPassword)
+ .WithPortBinding(DockerPorts.SqlServerDockerPort, true)
+ .Build();
+ SqlServerTestRunFixture fixture = new(runId, startedAt, containerName, container, network);
+
+ try
+ {
+ await container.StartAsync(cancellationToken).ConfigureAwait(false);
+ fixture.MappedPort = container.GetMappedPublicPort(DockerPorts.SqlServerDockerPort);
+ await fixture.WaitForReadiness(cancellationToken).ConfigureAwait(false);
+ return fixture;
+ }
+ catch (Exception startupException)
+ {
+ try
+ {
+ await fixture.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (Exception cleanupException)
+ {
+ throw new AggregateException(
+ "SQL Server test-run fixture startup failed and cleanup also failed.",
+ startupException,
+ cleanupException);
+ }
+
+ throw;
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (this.IsDisposed)
+ {
+ return;
+ }
+
+ this.IsDisposed = true;
+ List cleanupErrors = [];
+
+ try
+ {
+ await this.container.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ cleanupErrors.Add(exception);
+ }
+
+ try
+ {
+ await this.network.DeleteAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ cleanupErrors.Add(exception);
+ }
+
+ try
+ {
+ await this.network.DisposeAsync().ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ cleanupErrors.Add(exception);
+ }
+
+ if (cleanupErrors.Count > 0)
+ {
+ throw new AggregateException("Failed to clean up SQL Server test-run resources.", cleanupErrors);
+ }
+ }
+
+ private async Task WaitForReadiness(CancellationToken cancellationToken)
+ {
+ String connectionString = new SqlConnectionStringBuilder
+ {
+ DataSource = $"127.0.0.1,{this.MappedPort}",
+ InitialCatalog = "master",
+ UserID = "sa",
+ Password = this.Password,
+ Encrypt = false,
+ ConnectTimeout = 5
+ }.ConnectionString;
+
+ SqlException lastException = null;
+ for (Int32 attempt = 1; attempt <= HealthCheckAttempts; attempt++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ try
+ {
+ await using SqlConnection connection = new(connectionString);
+ await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
+ await using SqlCommand command = new("SELECT 1", connection);
+ await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
+ return;
+ }
+ catch (SqlException exception) when (attempt < HealthCheckAttempts)
+ {
+ lastException = exception;
+ await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken).ConfigureAwait(false);
+ }
+ catch (SqlException exception)
+ {
+ lastException = exception;
+ }
+ }
+
+ throw new InvalidOperationException(
+ $"SQL Server did not become ready after {HealthCheckAttempts} health checks.",
+ lastException);
+ }
+}
diff --git a/Shared.Tests/Shared.Tests.csproj b/Shared.Tests/Shared.Tests.csproj
index e88e766..38d3558 100644
--- a/Shared.Tests/Shared.Tests.csproj
+++ b/Shared.Tests/Shared.Tests.csproj
@@ -8,9 +8,8 @@
-
+
all
- runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -19,6 +18,7 @@
+
all
diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj
index 1d1bcec..6707c2c 100644
--- a/Shared/Shared.csproj
+++ b/Shared/Shared.csproj
@@ -23,6 +23,7 @@
+
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..3140116
--- /dev/null
+++ b/global.json
@@ -0,0 +1,5 @@
+{
+ "test": {
+ "runner": "Microsoft.Testing.Platform"
+ }
+}