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 =>