using TUnit.Assertions.Core; using TUnit.Assertions.Exceptions; namespace TUnit.Assertions.Chaining; /// /// Combines two assertions with Or logic - at least one must pass. /// Used internally by Or chaining. Most users won't interact with this directly. /// public class OrAssertion : Assertion { private readonly Assertion _first; private readonly Assertion _second; public OrAssertion( Assertion first, Assertion second) : base(first.InternalContext) { _first = first ?? throw new ArgumentNullException(nameof(first)); _second = second ?? throw new ArgumentNullException(nameof(second)); } /// /// Throws when attempting to mix And with Or operators. /// public new AndContinuation And => throw new MixedAndOrAssertionsException(); public override async Task AssertAsync() { using var currentScope = AssertionScope.CreateIsolatedScope(); Exception? firstException = null; // Try first assertion - use ExecuteCoreAsync to avoid recursion if (currentScope != null) { // Inside Assert.Multiple - track exception count var exceptionCountBefore = currentScope.ExceptionCount; await _first.ExecuteCoreAsync(); if (currentScope.ExceptionCount > exceptionCountBefore) { // First assertion failed - store the exception firstException = currentScope.GetLastException(); } else { // First assertion passed - we're done return default; } } else { // Not in Assert.Multiple - use exception handling try { var result = await _first.ExecuteCoreAsync(); // First passed - return success return result; } catch (AssertionException ex) { firstException = ex; } } // First failed, try second assertion - use ExecuteCoreAsync to avoid recursion if (currentScope != null) { var exceptionCountBefore = currentScope.ExceptionCount; await _second.ExecuteCoreAsync(); if (currentScope.ExceptionCount > exceptionCountBefore) { // Both failed - build combined message var secondException = currentScope.GetLastException(); currentScope.RemoveLastExceptions(2); var combinedExpectation = BuildCombinedExpectation(); var butIndex = secondException.Message.IndexOf("but "); if (butIndex >= 0) { var restOfMessage = secondException.Message.Substring(butIndex); var combinedException = new AssertionException($""" Expected {combinedExpectation} {restOfMessage} """); currentScope.AddException(combinedException); } else { currentScope.AddException((AssertionException)secondException); } return default; } else { // Second passed, but first failed - remove first failure currentScope.RemoveLastExceptions(1); return default; } } else { // Not in Assert.Multiple try { var result = await _second.ExecuteCoreAsync(); // Second passed - return success (first failed but Or means at least one passes) return result; } catch (AssertionException ex) { // Both failed - throw combined exception var combinedExpectation = BuildCombinedExpectation(); var butIndex = ex.Message.IndexOf("but "); if (butIndex >= 0) { var restOfMessage = ex.Message.Substring(butIndex); throw new AssertionException($""" Expected {combinedExpectation} {restOfMessage} """); } throw; } } } private string BuildCombinedExpectation() { var firstExpectation = _first.InternalGetExpectation(); var firstBecause = _first.InternalBecauseMessage; var secondExpectation = _second.InternalGetExpectation(); if (firstBecause != null) { var becausePrefix = firstBecause.StartsWith("because ", StringComparison.OrdinalIgnoreCase) ? firstBecause : $"because {firstBecause}"; return $"{firstExpectation}, {becausePrefix}\n\tor {secondExpectation}"; } return $"{firstExpectation}\n\tor {secondExpectation}"; } protected override string GetExpectation() => BuildCombinedExpectation(); }