diff --git a/tests/JD.Efcpt.Build.Tests/Integration/ContainerStartup.cs b/tests/JD.Efcpt.Build.Tests/Integration/ContainerStartup.cs
new file mode 100644
index 0000000..a9fa229
--- /dev/null
+++ b/tests/JD.Efcpt.Build.Tests/Integration/ContainerStartup.cs
@@ -0,0 +1,81 @@
+using DotNet.Testcontainers.Containers;
+using Xunit;
+
+namespace JD.Efcpt.Build.Tests.Integration;
+
+///
+/// Helpers for starting Testcontainers-backed integration tests in a way that is
+/// resilient to transient Docker/registry infrastructure failures.
+///
+///
+/// These integration tests require a working Docker daemon and the ability to pull
+/// images from a registry (e.g. Docker Hub). CI runners periodically hit transient
+/// registry errors such as
+/// Docker API responded with status code='InternalServerError', response='{"message":"Head ... manifests/8.0: unknown: "}'.
+/// Such failures are environmental, not product defects, so they should mark the test
+/// as skipped (with a clear reason) rather than failing the build. Genuine
+/// product failures (assertion failures, schema-reader bugs) still fail as normal.
+///
+public static class ContainerStartup
+{
+ ///
+ /// Starts the supplied container, converting transient Docker/registry
+ /// infrastructure failures into a test skip via .
+ ///
+ public static async Task StartOrSkipAsync(IContainer container)
+ {
+ try
+ {
+ await container.StartAsync();
+ }
+ catch (Exception ex) when (IsDockerInfrastructureFailure(ex))
+ {
+ throw new SkipException(
+ "Skipping container-based integration test: Docker/registry is unavailable " +
+ $"or returned a transient error ({ex.GetType().Name}: {Flatten(ex)}).");
+ }
+ }
+
+ ///
+ /// Determines whether an exception (or any inner exception) represents a transient
+ /// Docker daemon / registry infrastructure failure rather than a product defect.
+ ///
+ public static bool IsDockerInfrastructureFailure(Exception? ex)
+ {
+ for (var current = ex; current is not null; current = current.InnerException)
+ {
+ var typeName = current.GetType().Name;
+ if (typeName is "DockerApiException" or "DockerContainerNotFoundException")
+ {
+ return true;
+ }
+
+ var message = current.Message ?? string.Empty;
+ if (message.Contains("registry-1.docker.io", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("manifests", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("InternalServerError", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("Docker API responded", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("toomanyrequests", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("Cannot connect to the Docker daemon", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("Docker is either not running", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("error pulling image", StringComparison.OrdinalIgnoreCase)
+ || message.Contains("failed to resolve reference", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static string Flatten(Exception ex)
+ {
+ var inner = ex;
+ while (inner.InnerException is not null)
+ {
+ inner = inner.InnerException;
+ }
+
+ return inner.Message;
+ }
+}
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/ContainerStartupTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/ContainerStartupTests.cs
new file mode 100644
index 0000000..8511283
--- /dev/null
+++ b/tests/JD.Efcpt.Build.Tests/Integration/ContainerStartupTests.cs
@@ -0,0 +1,45 @@
+using Xunit;
+
+namespace JD.Efcpt.Build.Tests.Integration;
+
+public sealed class ContainerStartupTests
+{
+ [Fact]
+ public void Detects_transient_docker_registry_error_from_ci()
+ {
+ // The exact transient failure observed on CI when pulling mysql:8.0 from Docker Hub.
+ var ex = new Exception(
+ "Docker API responded with status code='InternalServerError', " +
+ "response='{\"message\":\"Head \\\"https://registry-1.docker.io/v2/library/mysql/manifests/8.0\\\": unknown: \"}'");
+
+ Assert.True(ContainerStartup.IsDockerInfrastructureFailure(ex));
+ }
+
+ [Fact]
+ public void Detects_docker_daemon_unavailable()
+ {
+ var ex = new Exception("Cannot connect to the Docker daemon at unix:///var/run/docker.sock.");
+ Assert.True(ContainerStartup.IsDockerInfrastructureFailure(ex));
+ }
+
+ [Fact]
+ public void Detects_registry_rate_limit()
+ {
+ var ex = new InvalidOperationException(
+ "wrapper", new Exception("toomanyrequests: You have reached your pull rate limit."));
+ Assert.True(ContainerStartup.IsDockerInfrastructureFailure(ex));
+ }
+
+ [Fact]
+ public void Does_not_treat_product_assertion_failure_as_infrastructure()
+ {
+ var ex = new Exception("Expected 3 tables but found 2.");
+ Assert.False(ContainerStartup.IsDockerInfrastructureFailure(ex));
+ }
+
+ [Fact]
+ public void Handles_null()
+ {
+ Assert.False(ContainerStartup.IsDockerInfrastructureFailure(null));
+ }
+}
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/EndToEndReverseEngineeringTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/EndToEndReverseEngineeringTests.cs
index 7654e5e..021d058 100644
--- a/tests/JD.Efcpt.Build.Tests/Integration/EndToEndReverseEngineeringTests.cs
+++ b/tests/JD.Efcpt.Build.Tests/Integration/EndToEndReverseEngineeringTests.cs
@@ -40,7 +40,7 @@ private static async Task SetupSqlServerWithSampleSchema()
var container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
var connectionString = container.GetConnectionString();
// Create a sample schema with multiple tables
@@ -181,68 +181,80 @@ private static string[] GetGeneratedFiles(string directory, string pattern)
// ========== Tests ==========
[Scenario("Generate models from SQL Server schema")]
- [Fact]
+ [SkippableFact]
public async Task Generate_models_from_sql_server_schema()
- => await Given("SQL Server with Customers, Orders, Products tables", SetupSqlServerWithSampleSchema)
- .When("execute reverse engineering pipeline", ExecuteReverseEngineering)
- .Then("query schema task succeeds", r => r.QuerySuccess)
- .And("run efcpt task succeeds", r => r.RunSuccess)
- .And("fingerprint file exists", r => File.Exists(Path.Combine(r.OutputDir, "efcpt-fingerprint.txt")))
- .And("schema model file exists", r => File.Exists(Path.Combine(r.OutputDir, "schema-model.json")))
- .Finally(r => r.Context.Dispose())
- .AssertPassed();
+ {
+ var ctx = await SetupSqlServerWithSampleSchema();
+ await Given("SQL Server with Customers, Orders, Products tables", () => Task.FromResult(ctx))
+ .When("execute reverse engineering pipeline", ExecuteReverseEngineering)
+ .Then("query schema task succeeds", r => r.QuerySuccess)
+ .And("run efcpt task succeeds", r => r.RunSuccess)
+ .And("fingerprint file exists", r => File.Exists(Path.Combine(r.OutputDir, "efcpt-fingerprint.txt")))
+ .And("schema model file exists", r => File.Exists(Path.Combine(r.OutputDir, "schema-model.json")))
+ .Finally(r => r.Context.Dispose())
+ .AssertPassed();
+ }
[Scenario("Generated models contain expected files")]
- [Fact]
+ [SkippableFact]
public async Task Generated_models_contain_expected_files()
- => await Given("SQL Server with sample schema", SetupSqlServerWithSampleSchema)
- .When("execute reverse engineering", ExecuteReverseEngineering)
- .Then("tasks succeed", r => r.QuerySuccess && r.RunSuccess)
- .And("sample model file is generated", r => File.Exists(Path.Combine(r.OutputDir, "SampleModel.cs")))
- .And("sample model has content", r =>
- {
- var sampleFile = Path.Combine(r.OutputDir, "SampleModel.cs");
- return File.Exists(sampleFile) && new FileInfo(sampleFile).Length > 0;
- })
- .Finally(r => r.Context.Dispose())
- .AssertPassed();
+ {
+ var ctx = await SetupSqlServerWithSampleSchema();
+ await Given("SQL Server with sample schema", () => Task.FromResult(ctx))
+ .When("execute reverse engineering", ExecuteReverseEngineering)
+ .Then("tasks succeed", r => r.QuerySuccess && r.RunSuccess)
+ .And("sample model file is generated", r => File.Exists(Path.Combine(r.OutputDir, "SampleModel.cs")))
+ .And("sample model has content", r =>
+ {
+ var sampleFile = Path.Combine(r.OutputDir, "SampleModel.cs");
+ return File.Exists(sampleFile) && new FileInfo(sampleFile).Length > 0;
+ })
+ .Finally(r => r.Context.Dispose())
+ .AssertPassed();
+ }
[Scenario("Generated models are valid C# code")]
- [Fact]
+ [SkippableFact]
public async Task Generated_models_are_valid_csharp_code()
- => await Given("SQL Server with sample schema", SetupSqlServerWithSampleSchema)
- .When("execute reverse engineering", ExecuteReverseEngineering)
- .Then("tasks succeed", r => r.QuerySuccess && r.RunSuccess)
- .And("generated .cs file exists", r =>
- {
- var csFiles = GetGeneratedFiles(r.OutputDir, "*.cs");
- return csFiles.Length > 0;
- })
- .And("generated file has content", r =>
- {
- var csFiles = GetGeneratedFiles(r.OutputDir, "*.cs");
- return csFiles.All(f => new FileInfo(f).Length > 0);
- })
- .And("generated file contains expected comment", r =>
- {
- var sampleFile = Path.Combine(r.OutputDir, "SampleModel.cs");
- if (!File.Exists(sampleFile)) return false;
- var content = File.ReadAllText(sampleFile);
- return content.Contains("// generated from");
- })
- .Finally(r => r.Context.Dispose())
- .AssertPassed();
+ {
+ var ctx = await SetupSqlServerWithSampleSchema();
+ await Given("SQL Server with sample schema", () => Task.FromResult(ctx))
+ .When("execute reverse engineering", ExecuteReverseEngineering)
+ .Then("tasks succeed", r => r.QuerySuccess && r.RunSuccess)
+ .And("generated .cs file exists", r =>
+ {
+ var csFiles = GetGeneratedFiles(r.OutputDir, "*.cs");
+ return csFiles.Length > 0;
+ })
+ .And("generated file has content", r =>
+ {
+ var csFiles = GetGeneratedFiles(r.OutputDir, "*.cs");
+ return csFiles.All(f => new FileInfo(f).Length > 0);
+ })
+ .And("generated file contains expected comment", r =>
+ {
+ var sampleFile = Path.Combine(r.OutputDir, "SampleModel.cs");
+ if (!File.Exists(sampleFile)) return false;
+ var content = File.ReadAllText(sampleFile);
+ return content.Contains("// generated from");
+ })
+ .Finally(r => r.Context.Dispose())
+ .AssertPassed();
+ }
[Scenario("Schema fingerprint changes when database schema changes")]
- [Fact]
+ [SkippableFact]
public async Task Schema_fingerprint_changes_when_database_schema_changes()
- => await Given("SQL Server with sample schema", SetupSqlServerWithSampleSchema)
- .When("execute reverse engineering, modify schema, execute again", ExecuteModifyAndRegenerate)
- .Then("initial generation succeeds", r => r.InitialQuerySuccess && r.InitialRunSuccess)
- .And("modified generation succeeds", r => r.ModifiedQuerySuccess && r.ModifiedRunSuccess)
- .And("fingerprints are different", r => r.InitialFingerprint != r.ModifiedFingerprint)
- .Finally(r => r.Context.Dispose())
- .AssertPassed();
+ {
+ var ctx = await SetupSqlServerWithSampleSchema();
+ await Given("SQL Server with sample schema", () => Task.FromResult(ctx))
+ .When("execute reverse engineering, modify schema, execute again", ExecuteModifyAndRegenerate)
+ .Then("initial generation succeeds", r => r.InitialQuerySuccess && r.InitialRunSuccess)
+ .And("modified generation succeeds", r => r.ModifiedQuerySuccess && r.ModifiedRunSuccess)
+ .And("fingerprints are different", r => r.InitialFingerprint != r.ModifiedFingerprint)
+ .Finally(r => r.Context.Dispose())
+ .AssertPassed();
+ }
private static async Task ExecuteModifyAndRegenerate(TestContext context)
{
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/FirebirdSchemaIntegrationTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/FirebirdSchemaIntegrationTests.cs
index 1c1c973..80f4b64 100644
--- a/tests/JD.Efcpt.Build.Tests/Integration/FirebirdSchemaIntegrationTests.cs
+++ b/tests/JD.Efcpt.Build.Tests/Integration/FirebirdSchemaIntegrationTests.cs
@@ -39,7 +39,7 @@ private static async Task SetupEmptyDatabase()
var container = new FirebirdSqlBuilder("jacobalberty/firebird:v4.0")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
return new TestContext(container, container.GetConnectionString());
}
@@ -156,10 +156,11 @@ private static async Task ExecuteComputeFingerprintWithChange
// ========== Tests ==========
[Scenario("Reads tables from Firebird database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_tables_from_database()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("returns test tables", r => r.Schema.Tables.Count >= 3)
.And("contains customers table", r => r.Schema.Tables.Any(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)))
@@ -170,10 +171,11 @@ await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads columns with correct metadata")]
- [Fact]
+ [SkippableFact]
public async Task Reads_columns_with_metadata()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("customers table has correct column count", r =>
r.Schema.Tables.First(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)).Columns.Count == 4)
@@ -184,10 +186,11 @@ await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads indexes from Firebird database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_indexes_from_database()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("products table has indexes", r =>
r.Schema.Tables.First(t => t.Name.Equals("PRODUCTS", StringComparison.OrdinalIgnoreCase)).Indexes.Count > 0)
@@ -196,10 +199,11 @@ await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Computes deterministic fingerprint")]
- [Fact]
+ [SkippableFact]
public async Task Computes_deterministic_fingerprint()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("fingerprint computed twice", ExecuteComputeFingerprint)
.Then("fingerprints are equal", r => string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.And("fingerprint is not empty", r => !string.IsNullOrEmpty(r.Fingerprint1))
@@ -208,10 +212,11 @@ await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Fingerprint changes when schema changes")]
- [Fact]
+ [SkippableFact]
public async Task Fingerprint_changes_when_schema_changes()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("schema is modified", ExecuteComputeFingerprintWithChange)
.Then("fingerprints are different", r => !string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.Finally(r => r.Context.Dispose())
@@ -219,10 +224,11 @@ await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Uses factory to create reader")]
- [Fact]
+ [SkippableFact]
public async Task Factory_creates_correct_reader()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("schema read via factory", ExecuteReadSchemaViaFactory)
.Then("returns valid schema", r => r.Schema.Tables.Count >= 3)
.And("contains customers table", r => r.Schema.Tables.Any(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)))
@@ -231,10 +237,11 @@ await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
}
[Scenario("fb alias works")]
- [Fact]
+ [SkippableFact]
public async Task Fb_alias_works()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("schema read via fb alias", ExecuteReadSchemaViaFbAlias)
.Then("returns valid schema", r => r.Schema.Tables.Count >= 3)
.And("contains customers table", r => r.Schema.Tables.Any(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)))
@@ -243,10 +250,11 @@ await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Excludes system tables")]
- [Fact]
+ [SkippableFact]
public async Task Excludes_system_tables()
{
- await Given("a Firebird container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a Firebird container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("no RDB$ tables included", r =>
!r.Schema.Tables.Any(t => t.Name.StartsWith("RDB$", StringComparison.OrdinalIgnoreCase)))
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/MySqlSchemaIntegrationTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/MySqlSchemaIntegrationTests.cs
index 3ea1790..5e35f89 100644
--- a/tests/JD.Efcpt.Build.Tests/Integration/MySqlSchemaIntegrationTests.cs
+++ b/tests/JD.Efcpt.Build.Tests/Integration/MySqlSchemaIntegrationTests.cs
@@ -34,7 +34,7 @@ private static async Task SetupEmptyDatabase()
var container = new MySqlBuilder("mysql:8.0")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
return new TestContext(container, container.GetConnectionString());
}
@@ -140,10 +140,11 @@ private static async Task ExecuteComputeFingerprintWithChange
// ========== Tests ==========
[Scenario("Reads tables from MySQL database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_tables_from_database()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("returns all tables", r => r.Schema.Tables.Count == 3)
.And("contains customers table", r => r.Schema.Tables.Any(t => t.Name == "customers"))
@@ -154,10 +155,11 @@ await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads columns with correct metadata")]
- [Fact]
+ [SkippableFact]
public async Task Reads_columns_with_metadata()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("customers table has correct column count", r =>
r.Schema.Tables.First(t => t.Name == "customers").Columns.Count == 4)
@@ -172,10 +174,11 @@ await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads indexes from MySQL database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_indexes_from_database()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("products table has indexes", r =>
r.Schema.Tables.First(t => t.Name == "products").Indexes.Count > 0)
@@ -186,10 +189,11 @@ await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Identifies primary key indexes")]
- [Fact]
+ [SkippableFact]
public async Task Identifies_primary_key_indexes()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("customers table has PRIMARY index", r =>
r.Schema.Tables.First(t => t.Name == "customers").Indexes
@@ -199,10 +203,11 @@ await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Computes deterministic fingerprint")]
- [Fact]
+ [SkippableFact]
public async Task Computes_deterministic_fingerprint()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("fingerprint computed twice", ExecuteComputeFingerprint)
.Then("fingerprints are equal", r => string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.And("fingerprint is not empty", r => !string.IsNullOrEmpty(r.Fingerprint1))
@@ -211,10 +216,11 @@ await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Fingerprint changes when schema changes")]
- [Fact]
+ [SkippableFact]
public async Task Fingerprint_changes_when_schema_changes()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("schema is modified", ExecuteComputeFingerprintWithChange)
.Then("fingerprints are different", r => !string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.Finally(r => r.Context.Dispose())
@@ -222,10 +228,11 @@ await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Uses factory to create reader")]
- [Fact]
+ [SkippableFact]
public async Task Factory_creates_correct_reader()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("schema read via factory", ExecuteReadSchemaViaFactory)
.Then("returns valid schema", r => r.Schema.Tables.Count == 3)
.Finally(r => r.Context.Dispose())
@@ -233,10 +240,11 @@ await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("MariaDB alias works")]
- [Fact]
+ [SkippableFact]
public async Task Mariadb_alias_works()
{
- await Given("a MySQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a MySQL container with test schema", () => Task.FromResult(ctx))
.When("schema read via mariadb alias", ExecuteReadSchemaViaMariaDbAlias)
.Then("returns valid schema", r => r.Schema.Tables.Count == 3)
.Finally(r => r.Context.Dispose())
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/OracleSchemaIntegrationTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/OracleSchemaIntegrationTests.cs
index f217d62..00e707f 100644
--- a/tests/JD.Efcpt.Build.Tests/Integration/OracleSchemaIntegrationTests.cs
+++ b/tests/JD.Efcpt.Build.Tests/Integration/OracleSchemaIntegrationTests.cs
@@ -43,7 +43,7 @@ private static async Task SetupEmptyDatabase()
var container = new OracleBuilder("gvenzl/oracle-xe:21.3.0-slim-faststart")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
return new TestContext(container, container.GetConnectionString());
}
@@ -160,10 +160,11 @@ private static async Task ExecuteComputeFingerprintWithChange
// ========== Tests ==========
[Scenario("Reads tables from Oracle database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_tables_from_database()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("returns test tables", r => r.Schema.Tables.Count >= 3)
.And("contains customers table", r => r.Schema.Tables.Any(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)))
@@ -174,10 +175,11 @@ await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads columns with correct metadata")]
- [Fact]
+ [SkippableFact]
public async Task Reads_columns_with_metadata()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("customers table has correct column count", r =>
r.Schema.Tables.First(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)).Columns.Count == 4)
@@ -188,10 +190,11 @@ await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads indexes from Oracle database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_indexes_from_database()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("products table has indexes", r =>
r.Schema.Tables.First(t => t.Name.Equals("PRODUCTS", StringComparison.OrdinalIgnoreCase)).Indexes.Count > 0)
@@ -200,10 +203,11 @@ await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Computes deterministic fingerprint")]
- [Fact]
+ [SkippableFact]
public async Task Computes_deterministic_fingerprint()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("fingerprint computed twice", ExecuteComputeFingerprint)
.Then("fingerprints are equal", r => string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.And("fingerprint is not empty", r => !string.IsNullOrEmpty(r.Fingerprint1))
@@ -212,10 +216,11 @@ await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Fingerprint changes when schema changes")]
- [Fact]
+ [SkippableFact]
public async Task Fingerprint_changes_when_schema_changes()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("schema is modified", ExecuteComputeFingerprintWithChange)
.Then("fingerprints are different", r => !string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.Finally(r => r.Context.Dispose())
@@ -223,10 +228,11 @@ await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Uses factory to create reader")]
- [Fact]
+ [SkippableFact]
public async Task Factory_creates_correct_reader()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("schema read via factory", ExecuteReadSchemaViaFactory)
.Then("returns valid schema", r => r.Schema.Tables.Count >= 3)
.And("contains customers table", r => r.Schema.Tables.Any(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)))
@@ -235,10 +241,11 @@ await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
}
[Scenario("oracledb alias works")]
- [Fact]
+ [SkippableFact]
public async Task Oracledb_alias_works()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("schema read via oracledb alias", ExecuteReadSchemaViaOracleDbAlias)
.Then("returns valid schema", r => r.Schema.Tables.Count >= 3)
.And("contains customers table", r => r.Schema.Tables.Any(t => t.Name.Equals("CUSTOMERS", StringComparison.OrdinalIgnoreCase)))
@@ -247,10 +254,11 @@ await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Excludes system schemas")]
- [Fact]
+ [SkippableFact]
public async Task Excludes_system_schemas()
{
- await Given("an Oracle container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("an Oracle container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("no SYS tables included", r =>
!r.Schema.Tables.Any(t => t.Schema.Equals("SYS", StringComparison.OrdinalIgnoreCase)))
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/PostgreSqlSchemaIntegrationTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/PostgreSqlSchemaIntegrationTests.cs
index d4fb035..24454f2 100644
--- a/tests/JD.Efcpt.Build.Tests/Integration/PostgreSqlSchemaIntegrationTests.cs
+++ b/tests/JD.Efcpt.Build.Tests/Integration/PostgreSqlSchemaIntegrationTests.cs
@@ -34,7 +34,7 @@ private static async Task SetupEmptyDatabase()
var container = new PostgreSqlBuilder("postgres:16-alpine")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
return new TestContext(container, container.GetConnectionString());
}
@@ -126,10 +126,11 @@ private static async Task ExecuteComputeFingerprintWithChange
// ========== Tests ==========
[Scenario("Reads tables from PostgreSQL database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_tables_from_database()
{
- await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a PostgreSQL container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("returns both tables", r => r.Schema.Tables.Count == 2)
.And("contains users table", r => r.Schema.Tables.Any(t => t.Name == "users"))
@@ -139,10 +140,11 @@ await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads columns with correct metadata")]
- [Fact]
+ [SkippableFact]
public async Task Reads_columns_with_metadata()
{
- await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a PostgreSQL container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("users table has correct column count", r =>
r.Schema.Tables.First(t => t.Name == "users").Columns.Count == 4)
@@ -156,10 +158,11 @@ await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Reads indexes from PostgreSQL database")]
- [Fact]
+ [SkippableFact]
public async Task Reads_indexes_from_database()
{
- await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a PostgreSQL container with test schema", () => Task.FromResult(ctx))
.When("schema is read", ExecuteReadSchema)
.Then("orders table has indexes", r =>
r.Schema.Tables.First(t => t.Name == "orders").Indexes.Count > 0)
@@ -168,10 +171,11 @@ await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Computes deterministic fingerprint")]
- [Fact]
+ [SkippableFact]
public async Task Computes_deterministic_fingerprint()
{
- await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a PostgreSQL container with test schema", () => Task.FromResult(ctx))
.When("fingerprint computed twice", ExecuteComputeFingerprint)
.Then("fingerprints are equal", r => string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.And("fingerprint is not empty", r => !string.IsNullOrEmpty(r.Fingerprint1))
@@ -180,10 +184,11 @@ await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Fingerprint changes when schema changes")]
- [Fact]
+ [SkippableFact]
public async Task Fingerprint_changes_when_schema_changes()
{
- await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a PostgreSQL container with test schema", () => Task.FromResult(ctx))
.When("schema is modified", ExecuteComputeFingerprintWithChange)
.Then("fingerprints are different", r => !string.Equals(r.Fingerprint1, r.Fingerprint2, StringComparison.Ordinal))
.Finally(r => r.Context.Dispose())
@@ -191,10 +196,11 @@ await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
}
[Scenario("Uses factory to create reader")]
- [Fact]
+ [SkippableFact]
public async Task Factory_creates_correct_reader()
{
- await Given("a PostgreSQL container with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("a PostgreSQL container with test schema", () => Task.FromResult(ctx))
.When("schema read via factory", ExecuteReadSchemaViaFactory)
.Then("returns valid schema", r => r.Schema.Tables.Count == 2)
.Finally(r => r.Context.Dispose())
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/QuerySchemaMetadataIntegrationTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/QuerySchemaMetadataIntegrationTests.cs
index de60485..8976060 100644
--- a/tests/JD.Efcpt.Build.Tests/Integration/QuerySchemaMetadataIntegrationTests.cs
+++ b/tests/JD.Efcpt.Build.Tests/Integration/QuerySchemaMetadataIntegrationTests.cs
@@ -34,10 +34,11 @@ private sealed record TaskResult(
bool Success);
[Scenario("Queries schema from real SQL Server and produces deterministic fingerprint")]
- [Fact]
+ [SkippableFact]
public async Task Queries_schema_and_produces_deterministic_fingerprint()
{
- await Given("SQL Server with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("SQL Server with test schema", () => Task.FromResult(ctx))
.When("execute QuerySchemaMetadata task", ExecuteQuerySchemaMetadata)
.Then("task succeeds", r => r.Success)
.And("fingerprint is generated", r => !string.IsNullOrEmpty(r.Task.SchemaFingerprint))
@@ -47,10 +48,11 @@ await Given("SQL Server with test schema", SetupDatabaseWithSchema)
}
[Scenario("Identical schema produces identical fingerprint")]
- [Fact]
+ [SkippableFact]
public async Task Identical_schema_produces_identical_fingerprint()
{
- await Given("SQL Server with test schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("SQL Server with test schema", () => Task.FromResult(ctx))
.When("execute task twice", ExecuteTaskTwice)
.Then("both tasks succeed", r => r.Item1.Success && r.Item2.Success)
.And("fingerprints are identical", r => r.Item1.Task.SchemaFingerprint == r.Item2.Task.SchemaFingerprint)
@@ -59,10 +61,11 @@ await Given("SQL Server with test schema", SetupDatabaseWithSchema)
}
[Scenario("Schema change produces different fingerprint")]
- [Fact]
+ [SkippableFact]
public async Task Schema_change_produces_different_fingerprint()
{
- await Given("SQL Server with initial schema", SetupDatabaseWithSchema)
+ var ctx = await SetupDatabaseWithSchema();
+ await Given("SQL Server with initial schema", () => Task.FromResult(ctx))
.When("execute task, modify schema, execute again", ExecuteTaskModifySchemaExecuteAgain)
.Then("both tasks succeed", r => r.Item1.Success && r.Item2.Success)
.And("fingerprints are different", r => r.Item1.Task.SchemaFingerprint != r.Item2.Task.SchemaFingerprint)
@@ -71,10 +74,11 @@ await Given("SQL Server with initial schema", SetupDatabaseWithSchema)
}
[Scenario("Captures schema elements: tables, columns, indexes")]
- [Fact]
+ [SkippableFact]
public async Task Captures_complete_schema_elements()
{
- await Given("SQL Server with comprehensive schema", SetupComprehensiveSchema)
+ var ctx = await SetupComprehensiveSchema();
+ await Given("SQL Server with comprehensive schema", () => Task.FromResult(ctx))
.When("execute QuerySchemaMetadata task", ExecuteQuerySchemaMetadata)
.Then("task succeeds", r => r.Success)
.And("schema model contains expected tables", VerifySchemaModelContainsTables)
@@ -83,10 +87,11 @@ await Given("SQL Server with comprehensive schema", SetupComprehensiveSchema)
}
[Scenario("Handles empty database gracefully")]
- [Fact]
+ [SkippableFact]
public async Task Handles_empty_database_gracefully()
{
- await Given("SQL Server with empty database", SetupEmptyDatabase)
+ var ctx = await SetupEmptyDatabase();
+ await Given("SQL Server with empty database", () => Task.FromResult(ctx))
.When("execute QuerySchemaMetadata task", ExecuteQuerySchemaMetadata)
.Then("task succeeds", r => r.Success)
.And("fingerprint is generated for empty schema", r => !string.IsNullOrEmpty(r.Task.SchemaFingerprint))
@@ -95,10 +100,11 @@ await Given("SQL Server with empty database", SetupEmptyDatabase)
}
[Scenario("Fails gracefully with invalid connection string")]
- [Fact]
+ [SkippableFact]
public async Task Fails_gracefully_with_invalid_connection_string()
{
- await Given("invalid connection string", SetupInvalidConnectionString)
+ var ctx = await SetupInvalidConnectionString();
+ await Given("invalid connection string", () => Task.FromResult(ctx))
.When("execute QuerySchemaMetadata task", ExecuteQuerySchemaMetadata)
.Then("task fails", r => !r.Success)
.And("error is logged", r => r.Context.Engine.Errors.Count > 0)
@@ -113,7 +119,7 @@ private static async Task SetupDatabaseWithSchema()
var container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
var connectionString = container.GetConnectionString();
await CreateTestSchema(connectionString);
@@ -130,7 +136,7 @@ private static async Task SetupComprehensiveSchema()
var container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
var connectionString = container.GetConnectionString();
await CreateComprehensiveSchema(connectionString);
@@ -147,7 +153,7 @@ private static async Task SetupEmptyDatabase()
var container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
var connectionString = container.GetConnectionString();
// Don't create any schema - leave database empty
diff --git a/tests/JD.Efcpt.Build.Tests/Integration/SqlServerSchemaIntegrationTests.cs b/tests/JD.Efcpt.Build.Tests/Integration/SqlServerSchemaIntegrationTests.cs
index fe1c157..580b91b 100644
--- a/tests/JD.Efcpt.Build.Tests/Integration/SqlServerSchemaIntegrationTests.cs
+++ b/tests/JD.Efcpt.Build.Tests/Integration/SqlServerSchemaIntegrationTests.cs
@@ -35,7 +35,7 @@ private static async Task SetupEmptyDatabase()
var container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest")
.Build();
- await container.StartAsync();
+ await ContainerStartup.StartOrSkipAsync(container);
var connectionString = container.GetConnectionString();
return new TestContext(container, connectionString);
@@ -137,10 +137,11 @@ private static IEnumerable FilterDefaultTables(IReadOnlyList Task.FromResult(ctx))
.When("read schema", ExecuteReadSchema)
.Then("schema is not null", r => r.Schema != null)
.And("no user tables exist", r => !FilterDefaultTables(r.Schema.Tables).Any())
@@ -149,10 +150,11 @@ await Given("SQL Server with empty database", SetupEmptyDatabase)
}
[Scenario("Read single table schema")]
- [Fact]
+ [SkippableFact]
public async Task Read_single_table_schema()
{
- await Given("SQL Server with Users table", SetupSingleTableDatabase)
+ var ctx = await SetupSingleTableDatabase();
+ await Given("SQL Server with Users table", () => Task.FromResult(ctx))
.When("read schema", ExecuteReadSchema)
.Then("exactly one user table exists", r => FilterDefaultTables(r.Schema.Tables).Count() == 1)
.And("table schema is dbo", r =>
@@ -193,10 +195,11 @@ await Given("SQL Server with Users table", SetupSingleTableDatabase)
}
[Scenario("Read schema with indexes")]
- [Fact]
+ [SkippableFact]
public async Task Read_schema_with_indexes()
{
- await Given("SQL Server with Products table and index", SetupDatabaseWithIndexes)
+ var ctx = await SetupDatabaseWithIndexes();
+ await Given("SQL Server with Products table and index", () => Task.FromResult(ctx))
.When("read schema", ExecuteReadSchema)
.Then("Products table exists", r =>
{
@@ -220,10 +223,11 @@ await Given("SQL Server with Products table and index", SetupDatabaseWithIndexes
}
[Scenario("Schema fingerprint is consistent")]
- [Fact]
+ [SkippableFact]
public async Task Schema_fingerprint_is_consistent()
{
- await Given("SQL Server with TestTable", SetupDatabaseForFingerprinting)
+ var ctx = await SetupDatabaseForFingerprinting();
+ await Given("SQL Server with TestTable", () => Task.FromResult(ctx))
.When("read schema and compute fingerprints twice", ExecuteComputeFingerprintTwice)
.Then("fingerprints are identical", r => r.Fingerprint1 == r.Fingerprint2)
.And("fingerprint is not empty", r => !string.IsNullOrEmpty(r.Fingerprint1))
@@ -243,10 +247,11 @@ private static (TestContext Context, string Fingerprint1, string Fingerprint2) E
}
[Scenario("Schema changes produce different fingerprints")]
- [Fact]
+ [SkippableFact]
public async Task Schema_changes_produce_different_fingerprints()
{
- await Given("SQL Server with VersionedTable", SetupDatabaseForChanges)
+ var ctx = await SetupDatabaseForChanges();
+ await Given("SQL Server with VersionedTable", () => Task.FromResult(ctx))
.When("read schema, add column, read schema again", ExecuteChangeAndCompare)
.Then("fingerprints are different", r => r.Fingerprint1 != r.Fingerprint2)
.Finally(r => r.Context.Dispose())
@@ -274,10 +279,11 @@ await ExecuteSql(context.ConnectionString,
}
[Scenario("Read multiple tables in deterministic order")]
- [Fact]
+ [SkippableFact]
public async Task Read_multiple_tables_in_deterministic_order()
{
- await Given("SQL Server with Zebras, Apples, Monkeys tables", SetupDatabaseWithMultipleTables)
+ var ctx = await SetupDatabaseWithMultipleTables();
+ await Given("SQL Server with Zebras, Apples, Monkeys tables", () => Task.FromResult(ctx))
.When("read schema", ExecuteReadSchema)
.Then("exactly 3 user tables exist", r => FilterDefaultTables(r.Schema.Tables).Count() == 3)
.And("tables are sorted alphabetically", r =>