From 897f3e7ba0113e1d123c2014d3f66ee5e8033b13 Mon Sep 17 00:00:00 2001 From: JerrettDavis Date: Mon, 22 Jun 2026 13:08:53 -0500 Subject: [PATCH] fix(tests): skip container integration tests on transient Docker/registry failures CI failed when Docker Hub returned a transient InternalServerError pulling mysql:8.0 ("Head https://registry-1.docker.io/.../manifests/8.0: unknown"), hard-failing the run. Only one of eight MySQL scenarios failed in the same run, confirming an infrastructure flake rather than a product defect. Add ContainerStartup.StartOrSkipAsync which starts the container and converts transient Docker daemon/registry failures into a SkipException (following the existing Xunit.SkippableFact pattern used by the Snowflake tests). The skip is thrown eagerly in the test method body (before the TinyBDD pipeline, which would otherwise wrap it in BddStepException), so SkippableFact reports a skip instead of a failure. Genuine assertion/product failures still fail the build. Convert all Testcontainers-backed integration tests (MySQL, PostgreSQL, SQL Server, Oracle, Firebird, end-to-end, query-schema) to [SkippableFact] and eager container start. Add unit tests for the failure-detection logic. Co-Authored-By: Claude Opus 4.6 --- .../Integration/ContainerStartup.cs | 81 ++++++++++++ .../Integration/ContainerStartupTests.cs | 45 +++++++ .../EndToEndReverseEngineeringTests.cs | 118 ++++++++++-------- .../FirebirdSchemaIntegrationTests.cs | 42 ++++--- .../MySqlSchemaIntegrationTests.cs | 42 ++++--- .../OracleSchemaIntegrationTests.cs | 42 ++++--- .../PostgreSqlSchemaIntegrationTests.cs | 32 +++-- .../QuerySchemaMetadataIntegrationTests.cs | 36 +++--- .../SqlServerSchemaIntegrationTests.cs | 32 +++-- 9 files changed, 325 insertions(+), 145 deletions(-) create mode 100644 tests/JD.Efcpt.Build.Tests/Integration/ContainerStartup.cs create mode 100644 tests/JD.Efcpt.Build.Tests/Integration/ContainerStartupTests.cs 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 =>