From f89835632e93e3aab60db8b29f36822ebaed0677 Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Wed, 3 Jun 2026 22:04:11 +0300 Subject: [PATCH 1/6] Correct test discovery --- build/TestAllPersistence.cs | 229 ++++++++++++------------------------ 1 file changed, 78 insertions(+), 151 deletions(-) diff --git a/build/TestAllPersistence.cs b/build/TestAllPersistence.cs index cabb74dc2..8a56fb944 100644 --- a/build/TestAllPersistence.cs +++ b/build/TestAllPersistence.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Nuke.Common; using Nuke.Common.IO; +using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Serilog; using static Nuke.Common.Tools.DotNet.DotNetTasks; @@ -20,25 +20,6 @@ partial class Build RunTestProjectsOneClassAtATime(persistenceDir); }); - /// - /// Files that are never test classes and should be skipped during discovery. - /// - static readonly HashSet SkippedFileNames = new(StringComparer.OrdinalIgnoreCase) - { - "NoParallelization", - "GlobalUsings", - "AssemblyInfo", - "Usings", - "ModuleInitializer", - }; - - /// - /// Regex patterns that indicate a file contains xUnit test methods. - /// - static readonly Regex TestAttributePattern = new( - @"\[\s*(Fact|Theory|InlineData|MemberData|ClassData)", - RegexOptions.Compiled); - /// /// Determines if a project is a leader election test project, /// which requires running each test method individually. @@ -50,144 +31,85 @@ static bool IsLeaderElectionProject(string projectPath) } /// - /// Discovers test classes from source files by looking for [Fact] or [Theory] attributes. - /// Returns the class name (file name without extension) for files that contain tests. + /// Discovers test classes from a compiled test project by running dotnet test --list-tests. + /// Returns the set of fully-qualified class names that contain at least one test. + /// This discovers inherited tests (e.g. TransportCompliance<>) that source-level regex misses. /// - static List DiscoverTestClasses(string projectDir) + static HashSet DiscoverTestClassesFromAssembly(string projectPath, string configuration, string frameworkOverride = null) { - var testFiles = Directory.GetFiles(projectDir, "*.cs", SearchOption.AllDirectories) - .Where(f => !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") - && !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")) - .OrderBy(f => f) - .ToList(); - - var testClasses = new List(); - foreach (var testFile in testFiles) + var args = $"test \"{projectPath}\" --no-build --list-tests --configuration {configuration}"; + if (!string.IsNullOrEmpty(frameworkOverride)) + args += $" --framework {frameworkOverride}"; + + var process = ProcessTasks.StartProcess("dotnet", args, logOutput: false, logInvocation: false); + process.AssertWaitForExit(); + + var classes = new HashSet(); + // Test lines are indented (start with whitespace): + // Namespace.ClassName.MethodName ([Fact]) + // Namespace.ClassName.MethodName(param: value) ([Theory] + [InlineData]) + // Header lines (e.g. "Test run for ...", "The following...") are not indented. + // Class name = everything before the last dot BEFORE the first '('. + // Truncating at '(' first avoids dots in parameter values (e.g. "http://example"). + foreach (var raw in process.Output.Select(line => line.Text)) { - var className = Path.GetFileNameWithoutExtension(testFile); + if (raw.Length == 0 || !char.IsWhiteSpace(raw[0])) continue; // skip header lines + var text = raw.Trim(); - if (SkippedFileNames.Contains(className)) - continue; + // Strip parameters before splitting: "method(param: val)" -> "method" + var paren = text.IndexOf('('); + var stripped = paren >= 0 ? text[..paren] : text; - try - { - var content = File.ReadAllText(testFile); - if (TestAttributePattern.IsMatch(content)) - { - testClasses.Add(className); - } - } - catch (Exception ex) - { - Log.Warning("Could not read {File}: {Message}", testFile, ex.Message); - } + var lastDot = stripped.LastIndexOf('.'); + if (lastDot > 0) + classes.Add(stripped[..lastDot]); } - return testClasses; + return classes; } /// - /// Discovers individual test method names from source files for leader election projects. - /// Returns tuples of (className, methodName). Also follows class inheritance into - /// compliance base classes in src/Testing/Wolverine.ComplianceTests/ so that inherited - /// [Fact]/[Theory] methods are attributed to the concrete class. + /// Discovers individual test methods from a compiled test project by running + /// dotnet test --list-tests. Returns tuples of (fully-qualified class name, method name). + /// This discovers inherited tests (e.g. TransportCompliance<>) that source-level regex misses. + /// Handles both [Fact] (simple method names) and [Theory] + [InlineData] (names with parameters). /// - static List<(string ClassName, string MethodName)> DiscoverTestMethods(string projectDir) + static List<(string ClassName, string MethodName)> DiscoverTestMethodsFromAssembly(string projectPath, string configuration, string frameworkOverride = null) { - var methodPattern = new Regex( - @"\[\s*(?:Fact|Theory).*?\]\s*(?:\[.*?\]\s*)*public\s+(?:async\s+)?(?:Task|void)\s+(\w+)\s*\(", - RegexOptions.Compiled | RegexOptions.Singleline); + var args = $"test \"{projectPath}\" --no-build --list-tests --configuration {configuration}"; + if (!string.IsNullOrEmpty(frameworkOverride)) + args += $" --framework {frameworkOverride}"; - var classDeclarationPattern = new Regex( - @"(?:public\s+|internal\s+|abstract\s+|sealed\s+)*class\s+(\w+)\s*(?::\s*([\w<>,\s\.]+?))?\s*(?:\{|where\b)", - RegexOptions.Compiled); - - var complianceBaseDir = FindComplianceTestsDir(projectDir); - - var testFiles = Directory.GetFiles(projectDir, "*.cs", SearchOption.AllDirectories) - .Where(f => !f.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") - && !f.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")) - .ToList(); + var process = ProcessTasks.StartProcess("dotnet", args, logOutput: false, logInvocation: false); + process.AssertWaitForExit(); var results = new List<(string, string)>(); - var seen = new HashSet<(string, string)>(); - - void AddMethod(string className, string methodName) + // Test lines are indented (start with whitespace): + // Namespace.ClassName.MethodName ([Fact]) + // Namespace.ClassName.MethodName(param: value) ([Theory] + [InlineData]) + // Header lines (e.g. "Test run for ...", "The following...") are not indented. + // First strip parameters to avoid dots in param values (e.g. "http://example"). + foreach (var raw in process.Output.Select(line => line.Text)) { - if (seen.Add((className, methodName))) - { - results.Add((className, methodName)); - } - } + if (raw.Length == 0 || !char.IsWhiteSpace(raw[0])) continue; + var text = raw.Trim(); - foreach (var testFile in testFiles) - { - var className = Path.GetFileNameWithoutExtension(testFile); - if (SkippedFileNames.Contains(className)) continue; + // Strip parameters: "method(param: val)" -> "method" + var paren = text.IndexOf('('); + var stripped = paren >= 0 ? text[..paren] : text; - try - { - var content = File.ReadAllText(testFile); - - foreach (Match match in methodPattern.Matches(content)) - { - AddMethod(className, match.Groups[1].Value); - } - - if (complianceBaseDir == null) continue; - - foreach (Match classMatch in classDeclarationPattern.Matches(content)) - { - var baseList = classMatch.Groups[2].Value; - if (string.IsNullOrWhiteSpace(baseList)) continue; + var lastDot = stripped.LastIndexOf('.'); + if (lastDot <= 0) continue; - var concreteClassName = classMatch.Groups[1].Value; - var baseTypeName = baseList.Split(',')[0].Trim().Split('<')[0].Trim(); - if (string.IsNullOrWhiteSpace(baseTypeName)) continue; + var className = stripped[..lastDot]; + var methodName = stripped[(lastDot + 1)..]; - var baseFile = Path.Combine(complianceBaseDir, baseTypeName + ".cs"); - if (!File.Exists(baseFile)) continue; - - try - { - var baseContent = File.ReadAllText(baseFile); - foreach (Match baseMatch in methodPattern.Matches(baseContent)) - { - AddMethod(concreteClassName, baseMatch.Groups[1].Value); - } - } - catch (Exception ex) - { - Log.Warning("Could not read compliance base {File}: {Message}", baseFile, ex.Message); - } - } - } - catch (Exception ex) - { - Log.Warning("Could not read {File}: {Message}", testFile, ex.Message); - } + results.Add((className, methodName)); } return results; } - /// - /// Walks up from the project directory to locate src/Testing/Wolverine.ComplianceTests/ - /// so inherited compliance tests can be discovered. - /// - static string FindComplianceTestsDir(string projectDir) - { - var current = new DirectoryInfo(projectDir); - while (current != null) - { - var candidate = Path.Combine(current.FullName, "src", "Testing", "Wolverine.ComplianceTests"); - if (Directory.Exists(candidate)) return candidate; - current = current.Parent; - } - - return null; - } - /// /// Runs a single dotnet test invocation with retry logic. /// Returns true if the test passed (on first attempt or retry). @@ -232,8 +154,10 @@ bool RunTestWithRetry(string projectPath, string filter, string description, int } /// - /// Improved test runner that discovers actual test classes from source, + /// Improved test runner that discovers actual test classes from compiled assemblies, /// runs each class in isolation, and supports leader election one-test-at-a-time mode. + /// Uses dotnet test --list-tests instead of source regex so inherited tests + /// (e.g. TransportCompliance<>) are correctly discovered. /// void RunTestProjectsOneClassAtATime(AbsolutePath directory) { @@ -247,20 +171,19 @@ void RunTestProjectsOneClassAtATime(AbsolutePath directory) foreach (var projectPath in testProjects) { - var projectDir = Path.GetDirectoryName(projectPath)!; var projectName = Path.GetFileNameWithoutExtension(projectPath); if (IsLeaderElectionProject(projectPath)) { // Leader election: run each test method individually - var testMethods = DiscoverTestMethods(projectDir); + var testMethods = DiscoverTestMethodsFromAssembly(projectPath, Configuration); Log.Information("Running leader election tests one method at a time for {Project} ({Count} tests)", projectName, testMethods.Count); foreach (var (className, methodName) in testMethods) { - var filter = $"FullyQualifiedName~{className}.{methodName}"; - var description = $"{projectName}/{className}.{methodName}"; + var filter = AppendCategoryFilter($"FullyQualifiedName~{className}.{methodName}"); + var description = $"{projectName}/{className.Split('.')[^1]}.{methodName}"; Log.Information(" Running {Description}...", description); if (!RunTestWithRetry(projectPath, filter, description)) @@ -271,15 +194,17 @@ void RunTestProjectsOneClassAtATime(AbsolutePath directory) } else { - // Normal: run one class at a time - var testClasses = DiscoverTestClasses(projectDir); + // Normal: run one class at a time, discovered from the compiled assembly + // so inherited tests (e.g. TransportCompliance<>) are not missed. + var testClasses = DiscoverTestClassesFromAssembly(projectPath, Configuration); Log.Information("Running tests one class at a time for {Project} ({Count} classes)", projectName, testClasses.Count); - foreach (var className in testClasses) + foreach (var fullClassName in testClasses) { - var filter = AppendCategoryFilter($"FullyQualifiedName~{className}"); - var description = $"{projectName}/{className}"; + var shortName = fullClassName.Split('.')[^1]; + var filter = AppendCategoryFilter($"FullyQualifiedName~{fullClassName}."); + var description = $"{projectName}/{shortName}"; Log.Information(" Running {Description}...", description); if (!RunTestWithRetry(projectPath, filter, description)) @@ -345,20 +270,19 @@ void RunWholeProjectWithRetry(string projectPath, string frameworkOverride = nul /// void RunSingleProjectOneClassAtATime(string projectPath, string frameworkOverride = null) { - var projectDir = Path.GetDirectoryName(projectPath)!; var projectName = Path.GetFileNameWithoutExtension(projectPath); var failedTests = new List(); if (IsLeaderElectionProject(projectPath)) { - var testMethods = DiscoverTestMethods(projectDir); + var testMethods = DiscoverTestMethodsFromAssembly(projectPath, Configuration, frameworkOverride); Log.Information("Running leader election tests one method at a time for {Project} ({Count} tests)", projectName, testMethods.Count); foreach (var (className, methodName) in testMethods) { var filter = AppendCategoryFilter($"FullyQualifiedName~{className}.{methodName}"); - var description = $"{projectName}/{className}.{methodName}"; + var description = $"{projectName}/{className.Split('.')[^1]}.{methodName}"; Log.Information(" Running {Description}...", description); if (!RunTestWithRetry(projectPath, filter, description, frameworkOverride: frameworkOverride)) @@ -369,14 +293,17 @@ void RunSingleProjectOneClassAtATime(string projectPath, string frameworkOverrid } else { - var testClasses = DiscoverTestClasses(projectDir); + // Normal: run one class at a time, discovered from the compiled assembly + // so inherited tests (e.g. TransportCompliance<>) are not missed. + var testClasses = DiscoverTestClassesFromAssembly(projectPath, Configuration, frameworkOverride); Log.Information("Running tests one class at a time for {Project} ({Count} classes)", projectName, testClasses.Count); - foreach (var className in testClasses) + foreach (var fullClassName in testClasses) { - var filter = AppendCategoryFilter($"FullyQualifiedName~{className}"); - var description = $"{projectName}/{className}"; + var shortName = fullClassName.Split('.')[^1]; + var filter = AppendCategoryFilter($"FullyQualifiedName~{fullClassName}."); + var description = $"{projectName}/{shortName}"; Log.Information(" Running {Description}...", description); if (!RunTestWithRetry(projectPath, filter, description, frameworkOverride: frameworkOverride)) @@ -397,4 +324,4 @@ void RunSingleProjectOneClassAtATime(string projectPath, string frameworkOverrid throw new Exception($"{failedTests.Count} test(s) failed in {projectName}"); } } -} +} \ No newline at end of file From 5cf3ff48bcdc0b736ea46a81d1cb3433018e2ef6 Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Mon, 8 Jun 2026 17:24:45 +0300 Subject: [PATCH 2/6] Run tests with flaky retries --- build/CITargets.cs | 55 +++---- build/TestAllPersistence.cs | 303 ++++++++++++++++++------------------ build/TestAllTransports.cs | 2 +- 3 files changed, 176 insertions(+), 184 deletions(-) diff --git a/build/CITargets.cs b/build/CITargets.cs index 2c246d507..b1ebdc52d 100644 --- a/build/CITargets.cs +++ b/build/CITargets.cs @@ -240,8 +240,8 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjectsWithFramework("net9.0", persistenceTests); StartDockerServices("postgresql", "sqlserver", "rabbitmq"); - RunSingleProjectOneClassAtATime(persistenceTests, frameworkOverride: "net9.0"); - RunSingleProjectOneClassAtATime(postgresqlTests); + RunTestProject(persistenceTests, frameworkOverride: "net9.0"); + RunTestProject(postgresqlTests); }); Target CISqlite => _ => _ @@ -252,7 +252,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(sqliteTests); - RunSingleProjectOneClassAtATime(sqliteTests); + RunTestProject(sqliteTests); }); Target CISqlServer => _ => _ @@ -264,7 +264,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(sqlServerTests); StartDockerServices("sqlserver"); - RunSingleProjectOneClassAtATime(sqlServerTests); + RunTestProject(sqlServerTests); }); Target CIMarten => _ => _ @@ -277,15 +277,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(martenTests, martenSubscriptionTests); StartDockerServices("postgresql"); - // #2810: run each project in a single invocation rather than one - // dotnet-test spawn per test class. MartenTests has 111 test files; - // the per-class spawn overhead (process start + assembly load + - // xUnit discovery + per-fixture Postgres/daemon warm-up) dominated - // the ~22 min wall clock. Execution stays serial via the project's - // CollectionPerAssembly attribute, so isolation between classes is - // unchanged at the concurrency level — see RunWholeProjectWithRetry. - RunWholeProjectWithRetry(martenTests); - RunWholeProjectWithRetry(martenSubscriptionTests); + RunTestProjects([martenTests, martenSubscriptionTests]); }); Target CIMySql => _ => _ @@ -297,7 +289,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(mySqlTests); StartDockerServices("mysql"); - RunSingleProjectOneClassAtATime(mySqlTests); + RunTestProject(mySqlTests); }); Target CIOracle => _ => _ @@ -309,7 +301,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(oracleTests); StartDockerServices("oracle"); - RunSingleProjectOneClassAtATime(oracleTests); + RunTestProject(oracleTests); }); Target CIEfCore => _ => _ @@ -325,8 +317,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat // See GH-2588. StartDockerServices("postgresql", "sqlserver", "rabbitmq"); - RunSingleProjectOneClassAtATime(efCoreTests); - RunSingleProjectOneClassAtATime(efCoreMultiTenancy); + RunTestProjects([efCoreTests, efCoreMultiTenancy]); }); // ─── Transport CI Targets ────────────────────────────────────────── @@ -341,8 +332,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(sqsTests, snsTests); StartDockerServices("localstack", "postgresql"); - RunSingleProjectOneClassAtATime(sqsTests); - RunSingleProjectOneClassAtATime(snsTests); + RunTestProjects([sqsTests, snsTests]); }); Target CIKafka => _ => _ @@ -354,7 +344,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); StartDockerServices("kafka", "postgresql"); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); Target CIMQTT => _ => _ @@ -366,7 +356,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); StartDockerServices("postgresql", "sqlserver"); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); Target CINATS => _ => _ @@ -378,7 +368,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); StartDockerServices("postgresql"); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); Target CIPulsar => _ => _ @@ -389,7 +379,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); Target CIRedis => _ => _ @@ -401,7 +391,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); StartDockerServices("postgresql"); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); Target CIHttp => _ => _ @@ -430,8 +420,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(rabbitTests, circuitTests); StartDockerServices("rabbitmq", "postgresql", "sqlserver"); - RunSingleProjectOneClassAtATime(rabbitTests); - RunSingleProjectOneClassAtATime(circuitTests); + RunTestProjects([rabbitTests, circuitTests]); }); /// @@ -452,7 +441,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); StartDockerServices("rabbitmq"); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); Target CICosmosDb => _ => _ @@ -464,8 +453,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(cosmosDbTests, leaderElectionTests); - RunSingleProjectOneClassAtATime(cosmosDbTests); - RunSingleProjectOneClassAtATime(leaderElectionTests); + RunTestProjects([cosmosDbTests, leaderElectionTests]); }); Target CIRavenDb => _ => _ @@ -477,8 +465,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjectsWithFramework("net9.0", ravenDbTests, leaderElectionTests); - RunSingleProjectOneClassAtATime(ravenDbTests, frameworkOverride: "net9.0"); - RunSingleProjectOneClassAtATime(leaderElectionTests, frameworkOverride: "net9.0"); + RunTestProjects([ravenDbTests, leaderElectionTests], frameworkOverride: "net9.0"); }); Target CIGrpc => _ => _ @@ -489,7 +476,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); // ─── AOT Smoke ────────────────────────────────────────────────────── @@ -535,7 +522,7 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjects(tests); StartDockerServices("asb-emulator"); - RunSingleProjectOneClassAtATime(tests); + RunTestProject(tests); }); Target CIPolecat => _ => _ @@ -547,6 +534,6 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat BuildTestProjectsWithFramework("net10.0", polecatTests); StartDockerServices("sqlserver"); - RunSingleProjectOneClassAtATime(polecatTests, frameworkOverride: "net10.0"); + RunTestProject(polecatTests, frameworkOverride: "net10.0"); }); } diff --git a/build/TestAllPersistence.cs b/build/TestAllPersistence.cs index 8a56fb944..6effb9d53 100644 --- a/build/TestAllPersistence.cs +++ b/build/TestAllPersistence.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Xml.Linq; using Nuke.Common; using Nuke.Common.IO; using Nuke.Common.Tooling; @@ -17,25 +18,15 @@ partial class Build .Executes(() => { var persistenceDir = RootDirectory / "src" / "Persistence"; - RunTestProjectsOneClassAtATime(persistenceDir); + RunAllTestsProjects(persistenceDir); }); - /// - /// Determines if a project is a leader election test project, - /// which requires running each test method individually. - /// - static bool IsLeaderElectionProject(string projectPath) - { - var projectName = Path.GetFileNameWithoutExtension(projectPath); - return projectName.Contains("LeaderElection", StringComparison.OrdinalIgnoreCase); - } - /// /// Discovers test classes from a compiled test project by running dotnet test --list-tests. /// Returns the set of fully-qualified class names that contain at least one test. /// This discovers inherited tests (e.g. TransportCompliance<>) that source-level regex misses. /// - static HashSet DiscoverTestClassesFromAssembly(string projectPath, string configuration, string frameworkOverride = null) + static string[] DiscoverTestClassesFromAssembly(string projectPath, string configuration, string frameworkOverride = null) { var args = $"test \"{projectPath}\" --no-build --list-tests --configuration {configuration}"; if (!string.IsNullOrEmpty(frameworkOverride)) @@ -65,7 +56,7 @@ static HashSet DiscoverTestClassesFromAssembly(string projectPath, strin classes.Add(stripped[..lastDot]); } - return classes; + return [.. classes]; } /// @@ -112,19 +103,21 @@ static HashSet DiscoverTestClassesFromAssembly(string projectPath, strin /// /// Runs a single dotnet test invocation with retry logic. - /// Returns true if the test passed (on first attempt or retry). + /// Returns Passed, Flaky (passed on retry), or Failed. /// - bool RunTestWithRetry(string projectPath, string filter, string description, int maxAttempts = 2, string frameworkOverride = null) - { + TestOutcome RunTestWithRetry(string projectPath, + string fullTestName, int maxAttempts = 3, string frameworkOverride = null) + { + var projectName = Path.GetFileNameWithoutExtension(projectPath); + var filter = $"FullyQualifiedName~{EscapeFilterValue(fullTestName)}"; + var description = $"{projectName}/{fullTestName}"; var framework = frameworkOverride ?? Framework; for (var attempt = 1; attempt <= maxAttempts; attempt++) { try { if (attempt > 1) - { - Log.Warning(" Retry attempt {Attempt} for {Description}", attempt, description); - } + Log.Warning(" Retry attempt {Attempt}/{MaxAttempts} for {Description}", attempt, maxAttempts, description); DotNetTest(c => c .SetProjectFile(projectPath) @@ -132,9 +125,10 @@ bool RunTestWithRetry(string projectPath, string filter, string description, int .EnableNoBuild() .EnableNoRestore() .SetFramework(framework) - .SetFilter(filter)); + .SetFilter(filter) + .AddLoggers($"trx;LogFilePrefix={projectName}-{attempt}.trx")); - return true; + return attempt > 1 ? TestOutcome.Flaky : TestOutcome.Passed; } catch (Exception ex) { @@ -142,24 +136,21 @@ bool RunTestWithRetry(string projectPath, string filter, string description, int { Log.Error(" {Description} failed after {Attempts} attempts: {Message}", description, maxAttempts, ex.Message); - return false; + return TestOutcome.Failed; } - Log.Warning(" {Description} failed on attempt {Attempt}, will retry: {Message}", - description, attempt, ex.Message); + Log.Warning(" {Description} failed on attempt {Attempt}/{MaxAttempts}, will retry: {Message}", + description, attempt, maxAttempts, ex.Message); } } - return false; + return TestOutcome.Failed; } /// - /// Improved test runner that discovers actual test classes from compiled assemblies, - /// runs each class in isolation, and supports leader election one-test-at-a-time mode. - /// Uses dotnet test --list-tests instead of source regex so inherited tests - /// (e.g. TransportCompliance<>) are correctly discovered. + /// Runs all test projects under a directory. /// - void RunTestProjectsOneClassAtATime(AbsolutePath directory) + void RunAllTestsProjects(AbsolutePath directory) { var testProjects = directory.GlobFiles("**/*Tests.csproj", "**/*Tests/*.csproj") .Select(p => p.ToString()) @@ -167,161 +158,175 @@ void RunTestProjectsOneClassAtATime(AbsolutePath directory) .OrderBy(p => p) .ToList(); - var failedTests = new List(); + RunTestProjects([..testProjects]); + } - foreach (var projectPath in testProjects) + /// + /// Runs multiple test project with flaky retry. + /// + void RunTestProjects(string[] projectPaths, string frameworkOverride = null) + { + var failedProjects = new List(); + foreach (var projectPath in projectPaths) { - var projectName = Path.GetFileNameWithoutExtension(projectPath); - - if (IsLeaderElectionProject(projectPath)) - { - // Leader election: run each test method individually - var testMethods = DiscoverTestMethodsFromAssembly(projectPath, Configuration); - Log.Information("Running leader election tests one method at a time for {Project} ({Count} tests)", - projectName, testMethods.Count); - - foreach (var (className, methodName) in testMethods) - { - var filter = AppendCategoryFilter($"FullyQualifiedName~{className}.{methodName}"); - var description = $"{projectName}/{className.Split('.')[^1]}.{methodName}"; - Log.Information(" Running {Description}...", description); - - if (!RunTestWithRetry(projectPath, filter, description)) - { - failedTests.Add(description); - } - } - } - else - { - // Normal: run one class at a time, discovered from the compiled assembly - // so inherited tests (e.g. TransportCompliance<>) are not missed. - var testClasses = DiscoverTestClassesFromAssembly(projectPath, Configuration); - Log.Information("Running tests one class at a time for {Project} ({Count} classes)", - projectName, testClasses.Count); - - foreach (var fullClassName in testClasses) - { - var shortName = fullClassName.Split('.')[^1]; - var filter = AppendCategoryFilter($"FullyQualifiedName~{fullClassName}."); - var description = $"{projectName}/{shortName}"; - Log.Information(" Running {Description}...", description); - - if (!RunTestWithRetry(projectPath, filter, description)) - { - failedTests.Add(description); - } - } - } + if (!RunWithFlakyRetry(projectPath, frameworkOverride: frameworkOverride)) + failedProjects.Add(projectPath); } - if (failedTests.Any()) - { - Log.Error("The following tests failed after retries:"); - foreach (var test in failedTests) - { - Log.Error(" - {Test}", test); - } - } + if (failedProjects.Count > 0) + throw new InvalidOperationException($"Tests failed: {string.Join(", ", failedProjects)}"); } /// - /// Appends Category!=Flaky to a test filter when running in CI. + /// Runs single test project with flaky retry. /// - static string AppendCategoryFilter(string filter) + void RunTestProject(string projectPath, string frameworkOverride = null) { - return filter + "&Category!=Flaky"; + RunTestProjects([projectPath], frameworkOverride: frameworkOverride); } /// - /// Runs an entire test project in a single dotnet test invocation, - /// retrying the whole project once on failure. Execution stays serial — the - /// project's [assembly: CollectionBehavior(CollectionPerAssembly)] - /// (e.g. MartenTests/NoParallelization.cs) keeps every test class in one - /// collection, so there's no concurrency and therefore no shared-schema / - /// shared-database collision risk. The win over - /// is process count: one - /// dotnet test spawn instead of one-per-class (111 for MartenTests), - /// eliminating the per-class process-start + assembly-load + xUnit-discovery - /// overhead that dominates the wall clock on the slow persistence jobs. - /// - /// Tradeoff vs. one-class-at-a-time: per-class retry granularity is lost - /// (a failure re-runs the whole project), and all classes share one process - /// (a hung daemon / leaked connection in one class can affect another rather - /// than being isolated to its own process). Used where the spawn overhead - /// outweighs those — see #2810. + /// Parses a TRX result file and returns the fully-qualified names of failed tests. /// - void RunWholeProjectWithRetry(string projectPath, string frameworkOverride = null) + static List ParseFailedTestNamesFromTrx(AbsolutePath trxPath) { - var projectName = Path.GetFileNameWithoutExtension(projectPath); - Log.Information("Running entire project {Project} in a single invocation (see #2810)", projectName); + var doc = XDocument.Load(trxPath.ToString()); + var ns = doc.Root?.GetDefaultNamespace() ?? XNamespace.None; - // No FullyQualifiedName filter — run the whole assembly. Still exclude - // Flaky-tagged tests, matching the one-class-at-a-time path's filter. - if (!RunTestWithRetry(projectPath, "Category!=Flaky", projectName, frameworkOverride: frameworkOverride)) + var failedTests = new List(); + + // + foreach (var result in doc.Descendants(ns + "UnitTestResult")) { - throw new Exception($"Tests failed in {projectName}"); + var outcome = (string)result.Attribute("outcome"); + if (outcome != "Failed") continue; + + var testName = (string)result.Attribute("testName"); + if (!string.IsNullOrEmpty(testName)) + failedTests.Add(testName); } + + return failedTests; } /// - /// Runs a single test project one class at a time with retry logic. - /// Used by individual Nuke targets for specific test projects. + /// Escapes special characters in a test filter value for dotnet test --filter. + /// Replaces '&' and '|' which are filter operators. /// - void RunSingleProjectOneClassAtATime(string projectPath, string frameworkOverride = null) + static string EscapeFilterValue(string value) + { + return value + .Replace("&", "%26") + .Replace("|", "%7C") + .Replace("=", "%3D") + .Replace("!", "%21") + .Replace("~", "%7E"); + } + + /// + /// Runs all tests in a project at once, then retries individual failures. + /// Uses TRX output to discover which tests failed on the first pass. + /// Flaky tests (pass on retry) are logged separately from hard failures. + /// Returns true only if all tests pass (possibly after retries). + /// + bool RunWithFlakyRetry(string projectPath, int maxAttempts = 3, string frameworkOverride = null) { var projectName = Path.GetFileNameWithoutExtension(projectPath); - var failedTests = new List(); + var framework = frameworkOverride ?? Framework; - if (IsLeaderElectionProject(projectPath)) + Log.Information("=== {Project}: Running all tests ===", projectName); + try { - var testMethods = DiscoverTestMethodsFromAssembly(projectPath, Configuration, frameworkOverride); - Log.Information("Running leader election tests one method at a time for {Project} ({Count} tests)", - projectName, testMethods.Count); - - foreach (var (className, methodName) in testMethods) - { - var filter = AppendCategoryFilter($"FullyQualifiedName~{className}.{methodName}"); - var description = $"{projectName}/{className.Split('.')[^1]}.{methodName}"; - Log.Information(" Running {Description}...", description); + DotNetTest(c => c + .SetProjectFile(projectPath) + .SetConfiguration(Configuration) + .EnableNoBuild() + .EnableNoRestore() + .SetFramework(framework) + .SetFilter("Category!=Flaky") + .AddLoggers($"trx;LogFilePrefix={projectName}")); + + Log.Information("=== {Project}: All tests passed ===", projectName); + return true; + } + catch (Exception ex) + { + Log.Warning(ex, "=== {Project} First round failed ===", projectName); + } - if (!RunTestWithRetry(projectPath, filter, description, frameworkOverride: frameworkOverride)) - { - failedTests.Add(description); - } - } + // Parse TRX for failed test names + var projectDir = (AbsolutePath)Path.GetDirectoryName(projectPath); + var trxDir = projectDir / "TestResults"; + var trxFiles = trxDir.GlobFiles($"{projectName}*.trx") + .OrderByDescending(f => new FileInfo(f).LastWriteTimeUtc) + .ToList(); + if (trxFiles.Count == 0) + { + Log.Error("No TRX file found in {ResultsDir}. Can't retry individual tests.", trxDir); + return false; } - else + + var failedTests = ParseFailedTestNamesFromTrx(trxFiles[0]); + + if (failedTests.Count == 0) { - // Normal: run one class at a time, discovered from the compiled assembly - // so inherited tests (e.g. TransportCompliance<>) are not missed. - var testClasses = DiscoverTestClassesFromAssembly(projectPath, Configuration, frameworkOverride); - Log.Information("Running tests one class at a time for {Project} ({Count} classes)", - projectName, testClasses.Count); + Log.Warning("=== {Project}: Build failed and no test failures found in TRX. ===", projectName); + return false; + } - foreach (var fullClassName in testClasses) - { - var shortName = fullClassName.Split('.')[^1]; - var filter = AppendCategoryFilter($"FullyQualifiedName~{fullClassName}."); - var description = $"{projectName}/{shortName}"; - Log.Information(" Running {Description}...", description); + Log.Warning("=== {Project}: Second round. Retrying {Count} test(s) ===", projectName, failedTests.Count); - if (!RunTestWithRetry(projectPath, filter, description, frameworkOverride: frameworkOverride)) - { - failedTests.Add(description); - } - } + // Retry each failed test individually + var result = new RunResult(); + foreach (var fullTestName in failedTests) + { + var outcome = RunTestWithRetry( + projectPath, + fullTestName, + maxAttempts, + framework); + + if (outcome == TestOutcome.Flaky) + result.FlakyTests.Add(fullTestName); + else if (outcome == TestOutcome.Failed) + result.FailedTests.Add(fullTestName); } - if (failedTests.Any()) + result.Print(projectName); + + if (result.FailedTests.Count > 0) + return false; + + Log.Information("=== {Project}: All tests passed (with flaky retries) ===", projectName); + return true; + } + + /// + /// Result of a test run: passed first try, passed on retry (flaky), or failed. + /// + enum TestOutcome { Passed, Flaky, Failed } + + class RunResult + { + public List FailedTests { get; private set; } = []; + public List FlakyTests { get; private set; } = []; + + public void Print(string projectName) { - Log.Error("The following tests failed after retries:"); - foreach (var test in failedTests) + if (FlakyTests.Count != 0) { - Log.Error(" - {Test}", test); + var tests = string.Join("\n ", FlakyTests.Select(t => $"[FLAKY] {t}")); + Log.Warning("=== {Project} Flaky tests ===\n{Tests}", + projectName, tests); } - throw new Exception($"{failedTests.Count} test(s) failed in {projectName}"); + if (FailedTests.Count != 0) + { + var tests = string.Join("\n ", FailedTests.Select(t => $"[FAILED] {t}")); + + Log.Error("=== {Project} Consistently failing tests ===\n{Tests}", + projectName, tests); + } } } } \ No newline at end of file diff --git a/build/TestAllTransports.cs b/build/TestAllTransports.cs index a227f62dc..f084a1596 100644 --- a/build/TestAllTransports.cs +++ b/build/TestAllTransports.cs @@ -8,6 +8,6 @@ partial class Build .Executes(() => { var transportsDir = RootDirectory / "src" / "Transports"; - RunTestProjectsOneClassAtATime(transportsDir); + RunAllTestsProjects(transportsDir); }); } From ea3ac3d1149f79ac23c2dbff47b4007a36e2b46d Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Mon, 8 Jun 2026 19:39:49 +0300 Subject: [PATCH 3/6] Fix CosmosDb tests --- Directory.Packages.props | 15 ++-- build/CITargets.cs | 3 +- build/TestAllPersistence.cs | 2 +- docker-compose.yml | 8 -- src/Persistence/CosmosDbTests/AppFixture.cs | 85 ++++++++++--------- .../CosmosDbTests/CosmosDbTests.csproj | 7 +- .../durability_agent_lifecycle.cs | 7 -- src/Persistence/CosmosDbTests/end_to_end.cs | 3 - ...rage_return_types_and_entity_attributes.cs | 3 - .../CosmosDbContainerFixture.cs | 36 -------- .../CosmosDbTests.LeaderElection.csproj | 18 +++- .../leader_election.cs | 76 ++--------------- .../Compliance/TransportCompliance.cs | 41 +++++---- wolverine.slnx | 1 + 14 files changed, 109 insertions(+), 196 deletions(-) delete mode 100644 src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbContainerFixture.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5368b2f19..f8f4140dc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -104,13 +104,14 @@ - - - - - - - + + + + + + + + diff --git a/build/CITargets.cs b/build/CITargets.cs index b1ebdc52d..8595eff9d 100644 --- a/build/CITargets.cs +++ b/build/CITargets.cs @@ -520,7 +520,8 @@ void BuildTestProjectsWithFramework(string frameworkOverride, params AbsolutePat var tests = RootDirectory / "src" / "Transports" / "Azure" / "Wolverine.AzureServiceBus.Tests" / "Wolverine.AzureServiceBus.Tests.csproj"; BuildTestProjects(tests); - StartDockerServices("asb-emulator"); + // Postgres is needed for leader election tests + StartDockerServices("asb-emulator", "postgresql"); RunTestProject(tests); }); diff --git a/build/TestAllPersistence.cs b/build/TestAllPersistence.cs index 6effb9d53..853c48298 100644 --- a/build/TestAllPersistence.cs +++ b/build/TestAllPersistence.cs @@ -106,7 +106,7 @@ static string[] DiscoverTestClassesFromAssembly(string projectPath, string confi /// Returns Passed, Flaky (passed on retry), or Failed. /// TestOutcome RunTestWithRetry(string projectPath, - string fullTestName, int maxAttempts = 3, string frameworkOverride = null) + string fullTestName, int maxAttempts = 2, string frameworkOverride = null) { var projectName = Path.GetFileNameWithoutExtension(projectPath); var filter = $"FullyQualifiedName~{EscapeFilterValue(fullTestName)}"; diff --git a/docker-compose.yml b/docker-compose.yml index 7c9f227c3..cd94ab8b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -160,14 +160,6 @@ services: networks: sb-emulator: - cosmosdb: - image: "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview" - ports: - - "8081:8081" - - "1234:1234" - environment: - - "PROTOCOL=https" - asb-sql-2: image: "mcr.microsoft.com/azure-sql-edge:latest" environment: diff --git a/src/Persistence/CosmosDbTests/AppFixture.cs b/src/Persistence/CosmosDbTests/AppFixture.cs index 5a1fb2d8d..5fe1a2ed4 100644 --- a/src/Persistence/CosmosDbTests/AppFixture.cs +++ b/src/Persistence/CosmosDbTests/AppFixture.cs @@ -1,25 +1,21 @@ -using System.Net; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; using Microsoft.Azure.Cosmos; +using System.Net; +using Testcontainers.CosmosDb; using Wolverine; -using Wolverine.CosmosDb; using Wolverine.CosmosDb.Internals; -using Wolverine.Persistence.Durability; namespace CosmosDbTests; public class AppFixture : IAsyncLifetime { - public const string AccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; public const string DatabaseName = "wolverine_tests"; + private const string CosmosDbImage = "mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-latest"; // Static container shared across all AppFixture instances - private static IContainer? _sharedContainer; + private static CosmosDbContainer _sharedContainer = null!; private static string _sharedConnectionString = null!; private static readonly SemaphoreSlim _lock = new(1, 1); - - public string ConnectionString => _sharedConnectionString; + public static string ConnectionString => _sharedConnectionString; public CosmosClient Client { get; private set; } = null!; public Container Container { get; private set; } = null!; @@ -31,20 +27,11 @@ private static async Task EnsureContainerStarted() { if (_sharedContainer != null) return; - _sharedContainer = new ContainerBuilder() - .WithImage("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") - .WithPortBinding(8081, true) - .WithPortBinding(1234, true) - .WithEnvironment("PROTOCOL", "https") - .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged("Gateway=OK")) + _sharedContainer = new CosmosDbBuilder(CosmosDbImage) .Build(); await _sharedContainer.StartAsync(); - - var host = _sharedContainer.Hostname; - var port = _sharedContainer.GetMappedPublicPort(8081); - _sharedConnectionString = $"AccountEndpoint=https://{host}:{port}/;AccountKey={AccountKey}"; + _sharedConnectionString = _sharedContainer.GetConnectionString(); } finally { @@ -59,40 +46,41 @@ public async Task InitializeAsync() var clientOptions = new CosmosClientOptions { HttpClientFactory = () => - { - HttpMessageHandler httpMessageHandler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }; - return new HttpClient(httpMessageHandler); + { + var handler = new HttpClientHandler(); + var port = _sharedContainer.GetMappedPublicPort(8081); + return new HttpClient(new FixRequestLocationHandler(port, handler)); }, - ConnectionMode = ConnectionMode.Gateway, - SerializerOptions = new CosmosSerializationOptions - { - PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase + ConnectionMode = ConnectionMode.Gateway, + SerializerOptions = new CosmosSerializationOptions + { + PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase } }; Client = new CosmosClient(ConnectionString, clientOptions); - // Retry database/container creation since the vnext emulator can be slow to initialize - for (var attempt = 1; attempt <= 10; attempt++) + // Retry database/container creation since the emulator can be slow to initialize + var maxRetries = 10; + for (var attempt = 1; attempt <= maxRetries; attempt++) { try { var databaseResponse = await Client.CreateDatabaseIfNotExistsAsync(DatabaseName); var containerProperties = new ContainerProperties(DocumentTypes.ContainerName, DocumentTypes.PartitionKeyPath); - var containerResponse = - await databaseResponse.Database.CreateContainerIfNotExistsAsync(containerProperties); + var containerResponse = await databaseResponse + .Database.CreateContainerIfNotExistsAsync(containerProperties); Container = containerResponse.Container; return; } - catch (CosmosException e) when (e.StatusCode == HttpStatusCode.ServiceUnavailable || - e.StatusCode == HttpStatusCode.InternalServerError) + catch (Exception ex) when ( + (ex is CosmosException cosmosEx && + (cosmosEx.StatusCode == HttpStatusCode.ServiceUnavailable || + cosmosEx.StatusCode == HttpStatusCode.InternalServerError)) + || ex is HttpRequestException) { - if (attempt == 10) throw; + if (attempt == maxRetries) throw; await Task.Delay(TimeSpan.FromSeconds(3)); } } @@ -101,7 +89,6 @@ public async Task InitializeAsync() public async Task DisposeAsync() { Client?.Dispose(); - // Container is shared - don't dispose it here; process exit handles cleanup } public CosmosDbMessageStore BuildMessageStore() @@ -120,3 +107,23 @@ public async Task ClearAll() public class CosmosDbCollection : ICollectionFixture { } + +public class FixRequestLocationHandler(int portNumber, HttpMessageHandler innerHandler) + : DelegatingHandler(innerHandler) +{ + // Workaround for dynamic port used instead of the default one. + // See https://stackoverflow.com/a/78729014 + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + const int defaultPort = 8081; + if (request.RequestUri?.Port != defaultPort) + return await base.SendAsync(request, cancellationToken); + + var builder = new UriBuilder(request.RequestUri) + { + Port = portNumber + }; + request.RequestUri = builder.Uri; + return await base.SendAsync(request, cancellationToken); + } +} \ No newline at end of file diff --git a/src/Persistence/CosmosDbTests/CosmosDbTests.csproj b/src/Persistence/CosmosDbTests/CosmosDbTests.csproj index 0a14b72a8..d357d399b 100644 --- a/src/Persistence/CosmosDbTests/CosmosDbTests.csproj +++ b/src/Persistence/CosmosDbTests/CosmosDbTests.csproj @@ -10,13 +10,14 @@ - + - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -24,7 +25,7 @@ - + diff --git a/src/Persistence/CosmosDbTests/durability_agent_lifecycle.cs b/src/Persistence/CosmosDbTests/durability_agent_lifecycle.cs index 59953d372..c6849da0f 100644 --- a/src/Persistence/CosmosDbTests/durability_agent_lifecycle.cs +++ b/src/Persistence/CosmosDbTests/durability_agent_lifecycle.cs @@ -9,14 +9,7 @@ namespace CosmosDbTests; -// Companion guard for the CosmosDb side of #2623 and the #2845 scheme fix. The CosmosDb -// durability agent is cluster-managed via the wolverinedb://cosmosdb/durability URI -// (MessageStoreCollection.BuildAgentAsync), and NodeAgentController calls StartAsync on it -// — that is the single poller. CosmosDbMessageStore.StartScheduledJobs must NOT also call -// StartTimers(), or two pollers would share this store and race (the #2623 bug). This test -// asserts exactly one polling agent across both sources and fails loudly if that regresses. [Collection("cosmosdb")] -[Trait("Category", "Flaky")] public class durability_agent_lifecycle : IAsyncLifetime { private readonly AppFixture _fixture; diff --git a/src/Persistence/CosmosDbTests/end_to_end.cs b/src/Persistence/CosmosDbTests/end_to_end.cs index 0e1c5830e..8dddea92b 100644 --- a/src/Persistence/CosmosDbTests/end_to_end.cs +++ b/src/Persistence/CosmosDbTests/end_to_end.cs @@ -1,16 +1,13 @@ -using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Shouldly; using Wolverine; using Wolverine.CosmosDb; -using Wolverine.CosmosDb.Internals; using Wolverine.Tracking; namespace CosmosDbTests; [Collection("cosmosdb")] -[Trait("Category", "Flaky")] public class end_to_end { private readonly AppFixture _fixture; diff --git a/src/Persistence/CosmosDbTests/using_storage_return_types_and_entity_attributes.cs b/src/Persistence/CosmosDbTests/using_storage_return_types_and_entity_attributes.cs index 82e4dab45..67dffb4b2 100644 --- a/src/Persistence/CosmosDbTests/using_storage_return_types_and_entity_attributes.cs +++ b/src/Persistence/CosmosDbTests/using_storage_return_types_and_entity_attributes.cs @@ -1,16 +1,13 @@ -using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Shouldly; using Wolverine; using Wolverine.CosmosDb; -using Wolverine.CosmosDb.Internals; using Wolverine.Tracking; namespace CosmosDbTests; [Collection("cosmosdb")] -[Trait("Category", "Flaky")] public class using_storage_return_types_and_entity_attributes { private readonly AppFixture _fixture; diff --git a/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbContainerFixture.cs b/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbContainerFixture.cs deleted file mode 100644 index 6bf011ca8..000000000 --- a/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbContainerFixture.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Runtime.CompilerServices; -using DotNet.Testcontainers.Builders; -using DotNet.Testcontainers.Containers; - -namespace CosmosDbTests.LeaderElection; - -public static class CosmosDbContainerFixture -{ - private static IContainer? _container; - - public const string AccountKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - - public static string ConnectionString { get; private set; } = - "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="; - - [ModuleInitializer] - internal static void Initialize() - { - _container = new ContainerBuilder() - .WithImage("mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview") - .WithPortBinding(8081, true) - .WithPortBinding(1234, true) - .WithEnvironment("PROTOCOL", "https") - .WithWaitStrategy(Wait.ForUnixContainer() - .UntilMessageIsLogged("Gateway=OK")) - .Build(); - -#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits - _container.StartAsync().GetAwaiter().GetResult(); -#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits - - var host = _container.Hostname; - var port = _container.GetMappedPublicPort(8081); - ConnectionString = $"AccountEndpoint=https://{host}:{port}/;AccountKey={AccountKey}"; - } -} diff --git a/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbTests.LeaderElection.csproj b/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbTests.LeaderElection.csproj index be4a67bc5..9d36ee1a9 100644 --- a/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbTests.LeaderElection.csproj +++ b/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/CosmosDbTests.LeaderElection.csproj @@ -5,11 +5,13 @@ - + - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -17,12 +19,20 @@ - + - + + + + + + + + + diff --git a/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/leader_election.cs b/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/leader_election.cs index 2b9cff3a5..98c4c6927 100644 --- a/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/leader_election.cs +++ b/src/Persistence/LeaderElection/CosmosDbTests.LeaderElection/leader_election.cs @@ -1,84 +1,24 @@ -using System.Net; -using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; using Wolverine; using Wolverine.CosmosDb; -using Wolverine.CosmosDb.Internals; using Wolverine.ComplianceTests; -using Xunit; using Xunit.Abstractions; namespace CosmosDbTests.LeaderElection; -// CI marker: add_second_node_see_balanced_nodes consistently fails after 2 -// retry attempts on the CosmosDB emulator (cluster-balance assertions race -// against the emulator's leader-lease TTL). Run only locally via the Flaky -// filter until the emulator behavior stabilizes. See #2618 (CI stabilization). -[Trait("Category", "Flaky")] -public class leader_election : LeadershipElectionCompliance -{ - public static string ConnectionString => CosmosDbContainerFixture.ConnectionString; - - public const string DatabaseName = "wolverine_tests"; - - public leader_election(ITestOutputHelper output) : base(output) - { - } - +[Collection("cosmosdb")] +public class leader_election(AppFixture fixture, ITestOutputHelper output) + : LeadershipElectionCompliance(output) +{ protected override void configureNode(WolverineOptions opts) { - opts.UseCosmosDbPersistence(DatabaseName); - - opts.Services.AddSingleton(new CosmosClient(ConnectionString, new CosmosClientOptions - { - HttpClientFactory = () => - { - HttpMessageHandler httpMessageHandler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }; - return new HttpClient(httpMessageHandler); - }, - ConnectionMode = ConnectionMode.Gateway, - SerializerOptions = new CosmosSerializationOptions - { - PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase - } - })); + opts.UseCosmosDbPersistence(AppFixture.DatabaseName); + opts.Services.AddSingleton(fixture.Client); } protected override async Task beforeBuildingHost() { - var clientOptions = new CosmosClientOptions - { - HttpClientFactory = () => - { - HttpMessageHandler httpMessageHandler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }; - return new HttpClient(httpMessageHandler); - }, - ConnectionMode = ConnectionMode.Gateway, - SerializerOptions = new CosmosSerializationOptions - { - PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase - } - }; - - using var client = new CosmosClient(ConnectionString, clientOptions); - - // Ensure database and container exist - var databaseResponse = await client.CreateDatabaseIfNotExistsAsync(DatabaseName); - var containerProperties = - new ContainerProperties(DocumentTypes.ContainerName, DocumentTypes.PartitionKeyPath); - await databaseResponse.Database.CreateContainerIfNotExistsAsync(containerProperties); - - // Clear existing data - var store = new CosmosDbMessageStore(client, DatabaseName, - databaseResponse.Database.GetContainer(DocumentTypes.ContainerName), new WolverineOptions()); - await store.Admin.ClearAllAsync(); + await fixture.InitializeAsync(); + await fixture.ClearAll(); } } diff --git a/src/Testing/Wolverine.ComplianceTests/Compliance/TransportCompliance.cs b/src/Testing/Wolverine.ComplianceTests/Compliance/TransportCompliance.cs index 4b7298b9a..6bd9bb4c3 100644 --- a/src/Testing/Wolverine.ComplianceTests/Compliance/TransportCompliance.cs +++ b/src/Testing/Wolverine.ComplianceTests/Compliance/TransportCompliance.cs @@ -33,7 +33,7 @@ protected TransportComplianceFixture(Uri destination, int defaultTimeInSeconds = public bool AllLocally { get; set; } - public bool MustReset { get; set; } = false; + public bool MustReset { get; set; } = true; public bool IsSenderOnlyTransport { get; set; } @@ -176,17 +176,25 @@ public virtual void BeforeEach() protected Uri theOutboundAddress = null!; protected IHost theReceiver = null!; protected IHost theSender = null!; + private readonly bool _ownFixture; protected TransportCompliance() - { + { Fixture = new T(); + _ownFixture = true; + } + + protected TransportCompliance(T fixture) + { + Fixture = fixture; + _ownFixture = false; } public T Fixture { get; } public async Task InitializeAsync() { - if (Fixture is IAsyncLifetime lifetime) + if (_ownFixture && Fixture is IAsyncLifetime lifetime) { await lifetime.InitializeAsync(); } @@ -195,11 +203,14 @@ public async Task InitializeAsync() theReceiver = Fixture.Receiver; theOutboundAddress = Fixture.OutboundAddress; - await Fixture.Sender.ResetResourceState(); - - if (Fixture.Receiver != null && !ReferenceEquals(Fixture.Sender, Fixture.Receiver)) - { - await Fixture.Receiver.ResetResourceState(); + if (Fixture.MustReset) + { + await Fixture.Sender.ResetResourceState(); + + if (Fixture.Receiver != null && !ReferenceEquals(Fixture.Sender, Fixture.Receiver)) + { + await Fixture.Receiver.ResetResourceState(); + } } Fixture.BeforeEach(); @@ -207,14 +218,12 @@ public async Task InitializeAsync() public async Task DisposeAsync() { - if (Fixture is IAsyncDisposable) - { - await Fixture.DisposeAsync(); - } - else - { - Fixture?.SafeDispose(); - } + if (!_ownFixture) + return; + if (Fixture is IAsyncDisposable) + await Fixture.DisposeAsync(); + else + Fixture?.SafeDispose(); } [Fact] diff --git a/wolverine.slnx b/wolverine.slnx index 343ebd8bf..5d526d438 100644 --- a/wolverine.slnx +++ b/wolverine.slnx @@ -81,6 +81,7 @@ + From 02f5318420957e15bd7b3f4a10655b0b8f4771e1 Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Mon, 8 Jun 2026 20:29:01 +0300 Subject: [PATCH 4/6] Disable Pulsar compliance tests --- .../InlinePulsarTransportComplianceTests.cs | 1 + .../Wolverine.Pulsar.Tests/PulsarTransportComplianceTests.cs | 1 + src/Transports/Pulsar/Wolverine.Pulsar.Tests/WithCloudEvents.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/Transports/Pulsar/Wolverine.Pulsar.Tests/InlinePulsarTransportComplianceTests.cs b/src/Transports/Pulsar/Wolverine.Pulsar.Tests/InlinePulsarTransportComplianceTests.cs index c9f442c05..4e7dd21ef 100644 --- a/src/Transports/Pulsar/Wolverine.Pulsar.Tests/InlinePulsarTransportComplianceTests.cs +++ b/src/Transports/Pulsar/Wolverine.Pulsar.Tests/InlinePulsarTransportComplianceTests.cs @@ -44,4 +44,5 @@ public override void BeforeEach() } [Collection("acceptance")] +[Trait("Category", "Flaky")] public class InlinePulsarTransportComplianceTests : TransportCompliance; \ No newline at end of file diff --git a/src/Transports/Pulsar/Wolverine.Pulsar.Tests/PulsarTransportComplianceTests.cs b/src/Transports/Pulsar/Wolverine.Pulsar.Tests/PulsarTransportComplianceTests.cs index f463a90b4..c551c0c45 100644 --- a/src/Transports/Pulsar/Wolverine.Pulsar.Tests/PulsarTransportComplianceTests.cs +++ b/src/Transports/Pulsar/Wolverine.Pulsar.Tests/PulsarTransportComplianceTests.cs @@ -43,4 +43,5 @@ public override void BeforeEach() } [Collection("acceptance")] +[Trait("Category", "Flaky")] public class PulsarTransportComplianceTests : TransportCompliance; \ No newline at end of file diff --git a/src/Transports/Pulsar/Wolverine.Pulsar.Tests/WithCloudEvents.cs b/src/Transports/Pulsar/Wolverine.Pulsar.Tests/WithCloudEvents.cs index 4f9f5d93c..3f257e327 100644 --- a/src/Transports/Pulsar/Wolverine.Pulsar.Tests/WithCloudEvents.cs +++ b/src/Transports/Pulsar/Wolverine.Pulsar.Tests/WithCloudEvents.cs @@ -48,6 +48,7 @@ public override void BeforeEach() } [Collection("acceptance")] +[Trait("Category", "Flaky")] public class with_cloud_events : TransportCompliance { // This test uses ErrorCausingMessage which contains a Dictionary. From 94c44cfe2d9639c5f25e904efd60642f0fe13e83 Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Mon, 8 Jun 2026 20:32:02 +0300 Subject: [PATCH 5/6] Fix ASB tests --- build/TestAllPersistence.cs | 2 +- .../AzureServiceBusTesting.cs | 16 ++++++++---- .../BufferedSendingAndReceivingCompliance.cs | 8 +++--- ..._1933_multi_tenant_conventional_routing.cs | 19 ++------------ .../InlineSendingAndReceivingCompliance.cs | 7 ++++-- .../end_to_end.cs | 5 ++-- .../end_to_end_with_CloudEvents.cs | 4 +-- ...d_receive_with_topics_and_subscriptions.cs | 7 ++++-- ...pics_and_subscriptions_with_custom_rule.cs | 25 +++++++++++-------- .../sending_compliance_with_prefixes.cs | 5 +++- 10 files changed, 50 insertions(+), 48 deletions(-) diff --git a/build/TestAllPersistence.cs b/build/TestAllPersistence.cs index 853c48298..a43405a0f 100644 --- a/build/TestAllPersistence.cs +++ b/build/TestAllPersistence.cs @@ -229,7 +229,7 @@ static string EscapeFilterValue(string value) /// Flaky tests (pass on retry) are logged separately from hard failures. /// Returns true only if all tests pass (possibly after retries). /// - bool RunWithFlakyRetry(string projectPath, int maxAttempts = 3, string frameworkOverride = null) + bool RunWithFlakyRetry(string projectPath, int maxAttempts = 2, string frameworkOverride = null) { var projectName = Path.GetFileNameWithoutExtension(projectPath); var framework = frameworkOverride ?? Framework; diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/AzureServiceBusTesting.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/AzureServiceBusTesting.cs index 3861f5808..3c4e2b2b5 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/AzureServiceBusTesting.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/AzureServiceBusTesting.cs @@ -26,17 +26,23 @@ public static AzureServiceBusConfiguration UseAzureServiceBusTesting(this Wolver } public static async Task DeleteAllEmulatorObjectsAsync() + => await DeleteAllEmulatorObjectsAsync(Servers.AzureServiceBusManagementConnectionString); + + public static async Task DeleteAllEmulatorObjectsAsync(string connectionString) { - var client = new ServiceBusAdministrationClient(Servers.AzureServiceBusManagementConnectionString); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + var ct = cts.Token; + + var client = new ServiceBusAdministrationClient(connectionString); - await foreach (var topic in client.GetTopicsAsync()) + await foreach (var topic in client.GetTopicsAsync().WithCancellation(ct)) { - await client.DeleteTopicAsync(topic.Name); + await client.DeleteTopicAsync(topic.Name, ct); } - await foreach (var queue in client.GetQueuesAsync()) + await foreach (var queue in client.GetQueuesAsync().WithCancellation(ct)) { - await client.DeleteQueueAsync(queue.Name); + await client.DeleteQueueAsync(queue.Name, ct); } } } diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/BufferedSendingAndReceivingCompliance.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/BufferedSendingAndReceivingCompliance.cs index 230679245..5815aa04e 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/BufferedSendingAndReceivingCompliance.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/BufferedSendingAndReceivingCompliance.cs @@ -12,6 +12,7 @@ public class BufferedComplianceFixture : TransportComplianceFixture, IAsyncLifet { public BufferedComplianceFixture() : base(new Uri("asb://queue/buffered-receiver"), 120) { + MustReset = false; } public async Task InitializeAsync() @@ -46,7 +47,9 @@ protected override Task AfterDisposeAsync() } [Trait("Category", "Flaky")] -public class BufferedSendingAndReceivingCompliance : TransportCompliance +public class BufferedSendingAndReceivingCompliance(BufferedComplianceFixture fixture) + : TransportCompliance(fixture), + IClassFixture { [Fact] public virtual async Task dlq_mechanics() @@ -63,9 +66,8 @@ public virtual async Task dlq_mechanics() var queue = transport.Queues[AzureServiceBusTransport.DeadLetterQueueName]; await queue.InitializeAsync(NullLogger.Instance); - var messageReceiver = transport.BusClient.CreateReceiver(AzureServiceBusTransport.DeadLetterQueueName); + await using var messageReceiver = transport.BusClient.CreateReceiver(AzureServiceBusTransport.DeadLetterQueueName); var queued = await messageReceiver.ReceiveMessageAsync(); queued.ShouldNotBeNull(); - } } \ No newline at end of file diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Bugs/Bug_1933_multi_tenant_conventional_routing.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Bugs/Bug_1933_multi_tenant_conventional_routing.cs index 607dc439e..deba930ba 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Bugs/Bug_1933_multi_tenant_conventional_routing.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Bugs/Bug_1933_multi_tenant_conventional_routing.cs @@ -1,4 +1,3 @@ -using Azure.Messaging.ServiceBus.Administration; using IntegrationTests; using JasperFx.Core; using Microsoft.Extensions.Hosting; @@ -28,7 +27,8 @@ public async Task DisposeAsync() try { - await DeleteTenantEmulatorObjectsAsync(); + await AzureServiceBusTesting.DeleteAllEmulatorObjectsAsync( + Servers.AzureServiceBusConnectionString); } catch { @@ -36,21 +36,6 @@ public async Task DisposeAsync() } } - private static async Task DeleteTenantEmulatorObjectsAsync() - { - var client = new ServiceBusAdministrationClient(Servers.AzureServiceBusConnectionString); - - await foreach (var topic in client.GetTopicsAsync()) - { - await client.DeleteTopicAsync(topic.Name); - } - - await foreach (var queue in client.GetQueuesAsync()) - { - await client.DeleteQueueAsync(queue.Name); - } - } - [Fact] public async Task should_receive_message_when_published_without_tenant_id() { diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/InlineSendingAndReceivingCompliance.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/InlineSendingAndReceivingCompliance.cs index 4af54a992..42dd9b840 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/InlineSendingAndReceivingCompliance.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/InlineSendingAndReceivingCompliance.cs @@ -7,7 +7,8 @@ namespace Wolverine.AzureServiceBus.Tests; public class InlineComplianceFixture : TransportComplianceFixture, IAsyncLifetime { public InlineComplianceFixture() : base(new Uri("asb://queue/inline-receiver"), 120) - { + { + MustReset = false; } public async Task InitializeAsync() @@ -46,4 +47,6 @@ protected override Task AfterDisposeAsync() } } -public class InlineSendingAndReceivingCompliance : TransportCompliance; \ No newline at end of file +public class InlineSendingAndReceivingCompliance(InlineComplianceFixture fixture) + : TransportCompliance(fixture), + IClassFixture; diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end.cs index cb24c63f0..481559efc 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end.cs @@ -4,7 +4,6 @@ using Wolverine.AzureServiceBus.Internal; using Wolverine.Configuration; using Wolverine.Tracking; -using Wolverine.Transports; using Xunit; namespace Wolverine.AzureServiceBus.Tests; @@ -84,7 +83,7 @@ public void builds_response_and_retry_queue_by_default() public async Task disable_system_queues() { #region sample_disable_system_queues_in_azure_service_bus - var host = await Host.CreateDefaultBuilder() + using var host = await Host.CreateDefaultBuilder() .UseWolverine(opts => { opts.UseAzureServiceBusTesting() @@ -116,7 +115,7 @@ public async Task send_and_receive_a_single_message() var session = await _host.TrackActivity() .IncludeExternalTransports() - .Timeout(5.Minutes()) + .Timeout(30.Seconds()) .SendMessageAndWaitAsync(message); session.Received.SingleMessage() diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end_with_CloudEvents.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end_with_CloudEvents.cs index 0f56e71d4..85c23daa2 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end_with_CloudEvents.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/end_to_end_with_CloudEvents.cs @@ -1,8 +1,6 @@ using JasperFx.Core; using Microsoft.Extensions.Hosting; using Shouldly; -using Wolverine.AzureServiceBus.Internal; -using Wolverine.Configuration; using Wolverine.Tracking; using Xunit; @@ -62,7 +60,7 @@ public async Task send_and_receive_a_single_message() var session = await _host.TrackActivity() .IncludeExternalTransports() - .Timeout(5.Minutes()) + .Timeout(30.Seconds()) .SendMessageAndWaitAsync(message); session.Received.SingleMessage() diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions.cs index 9f5aed97b..dbca66244 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions.cs @@ -6,7 +6,8 @@ namespace Wolverine.AzureServiceBus.Tests; public class TopicsComplianceFixture : TransportComplianceFixture, IAsyncLifetime { public TopicsComplianceFixture() : base(new Uri("asb://topic/topic1"), 120) - { + { + MustReset = false; } public async Task InitializeAsync() @@ -37,4 +38,6 @@ protected override Task AfterDisposeAsync() } } -public class TopicAndSubscriptionSendingAndReceivingCompliance : TransportCompliance; \ No newline at end of file +public class TopicAndSubscriptionSendingAndReceivingCompliance(TopicsComplianceFixture fixture) + : TransportCompliance(fixture), + IClassFixture; \ No newline at end of file diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions_with_custom_rule.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions_with_custom_rule.cs index e75da16f8..20d56bf82 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions_with_custom_rule.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/send_and_receive_with_topics_and_subscriptions_with_custom_rule.cs @@ -1,4 +1,5 @@ using Azure.Messaging.ServiceBus.Administration; +using JasperFx.Core; using Shouldly; using Wolverine.ComplianceTests.Compliance; using Wolverine.Tracking; @@ -6,9 +7,15 @@ namespace Wolverine.AzureServiceBus.Tests; -public class TopicsWithCustomRuleComplianceFixture() - : TransportComplianceFixture(new Uri("asb://topic/topic1"), 120), IAsyncLifetime +public class TopicsWithCustomRuleComplianceFixture + : TransportComplianceFixture, IAsyncLifetime { + public TopicsWithCustomRuleComplianceFixture() + : base(new Uri("asb://topic/topic1"), 120) + { + MustReset = false; + } + public async Task InitializeAsync() { await SenderIs(opts => @@ -43,19 +50,15 @@ protected override Task AfterDisposeAsync() } } -public class TopicAndSubscriptionWithCustomRuleSendingAndReceivingCompliance : TransportCompliance +public class TopicAndSubscriptionWithCustomRuleSendingAndReceivingCompliance( + TopicsWithCustomRuleComplianceFixture fixture) + : TransportCompliance(fixture), + IClassFixture { [Fact] public async Task ignores_message_not_matching_the_filter() { - /* - * Please note that this test may take a while to run, - * as it will wait for a message to be processed by the receiver - * but there should none be incoming because of the subscription - * filter. - */ - - var session = await theSender.TrackActivity(Fixture.DefaultTimeout) + var session = await theSender.TrackActivity(15.Seconds()) .AlsoTrack(theReceiver) .DoNotAssertOnExceptionsDetected() .ExecuteAndWaitAsync( diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/sending_compliance_with_prefixes.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/sending_compliance_with_prefixes.cs index aa5110353..e351c120e 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/sending_compliance_with_prefixes.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/sending_compliance_with_prefixes.cs @@ -12,6 +12,7 @@ public class PrefixedComplianceFixture : TransportComplianceFixture, IAsyncLifet { public PrefixedComplianceFixture() : base(new Uri("asb://queue/foo.buffered-receiver"), 120) { + MustReset = false; } public async Task InitializeAsync() @@ -48,7 +49,9 @@ protected override Task AfterDisposeAsync() } } -public class PrefixedSendingAndReceivingCompliance : TransportCompliance +public class PrefixedSendingAndReceivingCompliance(PrefixedComplianceFixture fixture) : + TransportCompliance(fixture), + IClassFixture { [Fact] public void prefix_was_applied_to_queues_for_the_receiver() From 0152d0c4b6a1cfc622aea08bf41010305d419dae Mon Sep 17 00:00:00 2001 From: Dmytro Pryvedeniuk Date: Thu, 11 Jun 2026 15:18:58 +0300 Subject: [PATCH 6/6] Disable RabbitMQ CircuitBreaker tests as flaky --- .../CircuitBreakingTests/CircuitBreakerIntegrationContext.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Transports/RabbitMQ/CircuitBreakingTests/CircuitBreakerIntegrationContext.cs b/src/Transports/RabbitMQ/CircuitBreakingTests/CircuitBreakerIntegrationContext.cs index 7156c7545..de0234593 100644 --- a/src/Transports/RabbitMQ/CircuitBreakingTests/CircuitBreakerIntegrationContext.cs +++ b/src/Transports/RabbitMQ/CircuitBreakingTests/CircuitBreakerIntegrationContext.cs @@ -14,6 +14,7 @@ namespace CircuitBreakingTests; [Collection("circuit_breaker")] +[Trait("Category", "Flaky")] public abstract class CircuitBreakerIntegrationContext : IDisposable, IObserver { private readonly IHost _host;