From cfd50757b86e55f9b9c1217fa6187dd56a5330ba Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Sat, 19 Apr 2025 00:46:42 +0200 Subject: [PATCH 01/12] WIP: - Adds condition trait grouping Introduces condition trait grouping with AND/OR operators. This allows for combining multiple condition traits to control test execution based on complex conditions. It also incorporates skip information to identify the source of the skip when a test is disabled. --- Sources/Testing/Traits/ConditionTrait.swift | 120 +++++++++++++++++- .../Traits/ConditionTraitTests.swift | 34 +++++ 2 files changed, 149 insertions(+), 5 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index a3b98c99d..87116311b 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -96,6 +96,7 @@ public struct ConditionTrait: TestTrait, SuiteTrait { public var isRecursive: Bool { true } + internal var isInverted: Bool = false } // MARK: - @@ -123,7 +124,10 @@ extension Trait where Self == ConditionTrait { _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation ) -> Self { - Self(kind: .conditional(condition), comments: Array(comment), sourceLocation: sourceLocation) + Self(kind: .conditional(condition), + comments: Array(comment), + sourceLocation: sourceLocation, + isInverted: false) } /// Constructs a condition trait that disables a test if it returns `false`. @@ -142,7 +146,10 @@ extension Trait where Self == ConditionTrait { sourceLocation: SourceLocation = #_sourceLocation, _ condition: @escaping @Sendable () async throws -> Bool ) -> Self { - Self(kind: .conditional(condition), comments: Array(comment), sourceLocation: sourceLocation) + Self(kind: .conditional(condition), + comments: Array(comment), + sourceLocation: sourceLocation, + isInverted: false) } /// Constructs a condition trait that disables a test unconditionally. @@ -157,7 +164,10 @@ extension Trait where Self == ConditionTrait { _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation ) -> Self { - Self(kind: .unconditional(false), comments: Array(comment), sourceLocation: sourceLocation) + Self(kind: .unconditional(false), + comments: Array(comment), + sourceLocation: sourceLocation, + isInverted: true) } /// Constructs a condition trait that disables a test if its value is true. @@ -182,7 +192,10 @@ extension Trait where Self == ConditionTrait { _ comment: Comment? = nil, sourceLocation: SourceLocation = #_sourceLocation ) -> Self { - Self(kind: .conditional { !(try condition()) }, comments: Array(comment), sourceLocation: sourceLocation) + Self(kind: .conditional { !(try condition()) }, + comments: Array(comment), + sourceLocation: sourceLocation, + isInverted: true) } /// Constructs a condition trait that disables a test if its value is true. @@ -201,6 +214,103 @@ extension Trait where Self == ConditionTrait { sourceLocation: SourceLocation = #_sourceLocation, _ condition: @escaping @Sendable () async throws -> Bool ) -> Self { - Self(kind: .conditional { !(try await condition()) }, comments: Array(comment), sourceLocation: sourceLocation) + Self(kind: .conditional { !(try await condition()) }, + comments: Array(comment), + sourceLocation: sourceLocation, + isInverted: true) + } +} + + +extension Trait where Self == ConditionTrait { + + public static func && (lhs: Self, rhs: Self) -> GroupedConditionTrait { + GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.and]) + } + + public static func || (lhs: Self, rhs: Self) -> GroupedConditionTrait { + GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.or]) + } +} + +public struct GroupedConditionTrait: TestTrait, SuiteTrait { + + var conditionTraits: [ConditionTrait] + var operations: [Operation] = [] + + public func prepare(for test: Test) async throws { + for (index, operation) in operations.enumerated() { + try await operation.operate(conditionTraits[index], conditionTraits[index + 1], includeSkipInfo: true) + } + } + + public func evaluate() async throws -> Bool { + var result: Bool = true + for (index, operation) in operations.enumerated() { + let isEnabled = try await operation.operate(conditionTraits[index], + conditionTraits[index + 1]) + result = result && isEnabled + } + return result + } +} + +extension Trait where Self == GroupedConditionTrait { + + static func && (lhs: Self, rhs: ConditionTrait) -> Self { + Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.and]) + } + + static func && (lhs: Self, rhs: Self) -> Self { + Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.and] + rhs.operations) + } + static func || (lhs: Self, rhs: ConditionTrait) -> Self { + Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.or]) + } + + static func || (lhs: Self, rhs: Self) -> Self { + Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.or] + rhs.operations) + } +} + + +extension GroupedConditionTrait { + enum Operation { + case `and` + case `or` + + @discardableResult + func operate(_ lhs: ConditionTrait,_ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { + let (l,r) = try await evaluate(lhs, rhs) + + var skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) = (nil, lhs.sourceLocation) + let isEnabled: Bool + switch self { + case .and: + isEnabled = l && r + + if !isEnabled { + skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) + } + case .or: + isEnabled = ((l != lhs.isInverted) || (r != rhs.isInverted)) != lhs.isInverted && rhs.isInverted + + if !isEnabled { + skipSide = (lhs.comments, lhs.sourceLocation) + } + } + + guard isEnabled || !includeSkipInfo else { + let sourceContext = SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation) + throw SkipInfo(comment: skipSide.comments?.first, sourceContext: sourceContext) + } + return isEnabled + } + + private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { + let l = try await lhs.evaluate() + let r = try await rhs.evaluate() + return (l, r) + } } } diff --git a/Tests/TestingTests/Traits/ConditionTraitTests.swift b/Tests/TestingTests/Traits/ConditionTraitTests.swift index 6b5311202..c2921394e 100644 --- a/Tests/TestingTests/Traits/ConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/ConditionTraitTests.swift @@ -59,4 +59,38 @@ struct ConditionTraitTests { result = try await enabledFalse.evaluate() #expect(!result) } + + @Test("AND operator", arguments: [((Conditions.condition1 && Conditions.condition1), true), + (Conditions.condition1 && Conditions.condition3, false)]) + func ANDOperator(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { + #expect(try await condition.evaluate() == expected) + } + + + @Test("OR operator",arguments: [(Conditions.condition1 || Conditions.condition3, true), + (Conditions.condition4 || Conditions.condition4, true), + (Conditions.condition2 || Conditions.condition2, false),]) + func OROperator(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { + print(condition) + let result = try await condition.evaluate() + #expect( result == expected) + } + + @Test("Mix Operator Logic on Condition Traits", arguments: [(Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) + func MixOperator(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { + let result = try await condition.evaluate() + #expect( result == expected) + } + + @Test("Applying mixed traits", Conditions.condition2 || Conditions.condition2) + func applyMixedTraits() { + #expect(true) + } + + private enum Conditions { + static let condition1 = ConditionTrait.enabled(if: true, "Some comment for condition1") + static let condition2 = ConditionTrait.enabled(if: false, "Some comment for condition2") + static let condition3 = ConditionTrait.disabled(if: true, "Some comment for condition3") + static let condition4 = ConditionTrait.disabled(if: false, "Some comment for condition4") + } } From a70a6b7afbd9bcd07f7208ecec89e53401b5d80d Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Sat, 19 Apr 2025 00:55:42 +0200 Subject: [PATCH 02/12] Improves grouped condition trait preparation #1034 Updates the `GroupedConditionTrait` to prepare for tests concurrently, improving performance. Also adds a check to ensure there are at least two condition traits before attempting to operate on them, preventing potential errors. --- Sources/Testing/Traits/ConditionTrait.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index 87116311b..69ea0a78e 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -239,11 +239,23 @@ public struct GroupedConditionTrait: TestTrait, SuiteTrait { var operations: [Operation] = [] public func prepare(for test: Test) async throws { - for (index, operation) in operations.enumerated() { - try await operation.operate(conditionTraits[index], conditionTraits[index + 1], includeSkipInfo: true) - } + let traitCount = conditionTraits.count + guard traitCount >= 2 else { return } + + try await withThrowingTaskGroup(of: Void.self) { group in + for (index, operation) in operations.enumerated() where index < traitCount - 1 { + let trait1 = conditionTraits[index] + let trait2 = conditionTraits[index + 1] + group.addTask { + try await operation.operate(trait1, trait2, includeSkipInfo: true) + } + } + + try await group.waitForAll() + } } + @_spi(Experimental) public func evaluate() async throws -> Bool { var result: Bool = true for (index, operation) in operations.enumerated() { From 91a746397934d1415ea50beb05cffef5c71010f2 Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Sat, 19 Apr 2025 01:04:02 +0200 Subject: [PATCH 03/12] WIP: - Handles single condition trait in group #1034 Addresses a scenario where a grouped condition trait might contain only a single condition. This change ensures that the single condition is properly handled, by evaluating or preparing it. --- Sources/Testing/Traits/ConditionTrait.swift | 44 ++++++++++++--------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index 69ea0a78e..8de8a7c85 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -239,31 +239,37 @@ public struct GroupedConditionTrait: TestTrait, SuiteTrait { var operations: [Operation] = [] public func prepare(for test: Test) async throws { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { return } - - try await withThrowingTaskGroup(of: Void.self) { group in - for (index, operation) in operations.enumerated() where index < traitCount - 1 { - let trait1 = conditionTraits[index] - let trait2 = conditionTraits[index + 1] - group.addTask { - try await operation.operate(trait1, trait2, includeSkipInfo: true) - } - } - - try await group.waitForAll() + let traitCount = conditionTraits.count + guard traitCount >= 2 else { + if let firstTrait = conditionTraits.first { + try await firstTrait.prepare(for: test) } + return + } + for (index, operation) in operations.enumerated() where index < traitCount - 1 { + let trait1 = conditionTraits[index] + let trait2 = conditionTraits[index + 1] + try await operation.operate(trait1, trait2, includeSkipInfo: true) + + } } @_spi(Experimental) public func evaluate() async throws -> Bool { - var result: Bool = true - for (index, operation) in operations.enumerated() { - let isEnabled = try await operation.operate(conditionTraits[index], - conditionTraits[index + 1]) - result = result && isEnabled + let traitCount = conditionTraits.count + guard traitCount >= 2 else { + if let firstTrait = conditionTraits.first { + return try await firstTrait.evaluate() } - return result + preconditionFailure() + } + var result: Bool = true + for (index, operation) in operations.enumerated() { + let isEnabled = try await operation.operate(conditionTraits[index], + conditionTraits[index + 1]) + result = result && isEnabled + } + return result } } From 1ed40b603860289d7b5c1c74523c406516bddc97 Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Sun, 20 Apr 2025 18:43:26 +0200 Subject: [PATCH 04/12] Refactors grouped condition trait into separate file Moves the `GroupedConditionTrait` and its related extensions and enums to a dedicated file, improving code organization and maintainability. Updates tests to reflect the new file structure. --- Sources/Testing/Traits/ConditionTrait.swift | 99 ---------------- .../Traits/GroupedConditionTrait.swift | 110 ++++++++++++++++++ .../Traits/ConditionTraitTests.swift | 34 ------ .../Traits/GroupedConditionTraitTests.swift | 38 ++++++ 4 files changed, 148 insertions(+), 133 deletions(-) create mode 100644 Sources/Testing/Traits/GroupedConditionTrait.swift create mode 100644 Tests/TestingTests/Traits/GroupedConditionTraitTests.swift diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index 8de8a7c85..add70b1cd 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -233,102 +233,3 @@ extension Trait where Self == ConditionTrait { } } -public struct GroupedConditionTrait: TestTrait, SuiteTrait { - - var conditionTraits: [ConditionTrait] - var operations: [Operation] = [] - - public func prepare(for test: Test) async throws { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - try await firstTrait.prepare(for: test) - } - return - } - for (index, operation) in operations.enumerated() where index < traitCount - 1 { - let trait1 = conditionTraits[index] - let trait2 = conditionTraits[index + 1] - try await operation.operate(trait1, trait2, includeSkipInfo: true) - - } - } - - @_spi(Experimental) - public func evaluate() async throws -> Bool { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - return try await firstTrait.evaluate() - } - preconditionFailure() - } - var result: Bool = true - for (index, operation) in operations.enumerated() { - let isEnabled = try await operation.operate(conditionTraits[index], - conditionTraits[index + 1]) - result = result && isEnabled - } - return result - } -} - -extension Trait where Self == GroupedConditionTrait { - - static func && (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.and]) - } - - static func && (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.and] + rhs.operations) - } - static func || (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.or]) - } - - static func || (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.or] + rhs.operations) - } -} - - -extension GroupedConditionTrait { - enum Operation { - case `and` - case `or` - - @discardableResult - func operate(_ lhs: ConditionTrait,_ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { - let (l,r) = try await evaluate(lhs, rhs) - - var skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) = (nil, lhs.sourceLocation) - let isEnabled: Bool - switch self { - case .and: - isEnabled = l && r - - if !isEnabled { - skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) - } - case .or: - isEnabled = ((l != lhs.isInverted) || (r != rhs.isInverted)) != lhs.isInverted && rhs.isInverted - - if !isEnabled { - skipSide = (lhs.comments, lhs.sourceLocation) - } - } - - guard isEnabled || !includeSkipInfo else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation) - throw SkipInfo(comment: skipSide.comments?.first, sourceContext: sourceContext) - } - return isEnabled - } - - private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { - let l = try await lhs.evaluate() - let r = try await rhs.evaluate() - return (l, r) - } - } -} diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift new file mode 100644 index 000000000..1d3863718 --- /dev/null +++ b/Sources/Testing/Traits/GroupedConditionTrait.swift @@ -0,0 +1,110 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + + +public struct GroupedConditionTrait: TestTrait, SuiteTrait { + + var conditionTraits: [ConditionTrait] + var operations: [Operation] = [] + + public func prepare(for test: Test) async throws { + let traitCount = conditionTraits.count + guard traitCount >= 2 else { + if let firstTrait = conditionTraits.first { + try await firstTrait.prepare(for: test) + } + return + } + for (index, operation) in operations.enumerated() where index < traitCount - 1 { + let trait1 = conditionTraits[index] + let trait2 = conditionTraits[index + 1] + try await operation.operate(trait1, trait2, includeSkipInfo: true) + + } + } + + @_spi(Experimental) + public func evaluate() async throws -> Bool { + let traitCount = conditionTraits.count + guard traitCount >= 2 else { + if let firstTrait = conditionTraits.first { + return try await firstTrait.evaluate() + } + preconditionFailure() + } + var result: Bool = true + for (index, operation) in operations.enumerated() { + let isEnabled = try await operation.operate(conditionTraits[index], + conditionTraits[index + 1]) + result = result && isEnabled + } + return result + } +} + +extension Trait where Self == GroupedConditionTrait { + + static func && (lhs: Self, rhs: ConditionTrait) -> Self { + Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.and]) + } + + static func && (lhs: Self, rhs: Self) -> Self { + Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.and] + rhs.operations) + } + static func || (lhs: Self, rhs: ConditionTrait) -> Self { + Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.or]) + } + + static func || (lhs: Self, rhs: Self) -> Self { + Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.or] + rhs.operations) + } +} + + +extension GroupedConditionTrait { + enum Operation { + case `and` + case `or` + + @discardableResult + func operate(_ lhs: ConditionTrait,_ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { + let (l,r) = try await evaluate(lhs, rhs) + + var skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) = (nil, lhs.sourceLocation) + let isEnabled: Bool + switch self { + case .and: + isEnabled = l && r + + if !isEnabled { + skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) + } + case .or: + isEnabled = ((l != lhs.isInverted) || (r != rhs.isInverted)) != lhs.isInverted && rhs.isInverted + + if !isEnabled { + skipSide = (lhs.comments, lhs.sourceLocation) + } + } + + guard isEnabled || !includeSkipInfo else { + let sourceContext = SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation) + throw SkipInfo(comment: skipSide.comments?.first, sourceContext: sourceContext) + } + return isEnabled + } + + private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { + let l = try await lhs.evaluate() + let r = try await rhs.evaluate() + return (l, r) + } + } +} diff --git a/Tests/TestingTests/Traits/ConditionTraitTests.swift b/Tests/TestingTests/Traits/ConditionTraitTests.swift index c2921394e..6b5311202 100644 --- a/Tests/TestingTests/Traits/ConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/ConditionTraitTests.swift @@ -59,38 +59,4 @@ struct ConditionTraitTests { result = try await enabledFalse.evaluate() #expect(!result) } - - @Test("AND operator", arguments: [((Conditions.condition1 && Conditions.condition1), true), - (Conditions.condition1 && Conditions.condition3, false)]) - func ANDOperator(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { - #expect(try await condition.evaluate() == expected) - } - - - @Test("OR operator",arguments: [(Conditions.condition1 || Conditions.condition3, true), - (Conditions.condition4 || Conditions.condition4, true), - (Conditions.condition2 || Conditions.condition2, false),]) - func OROperator(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { - print(condition) - let result = try await condition.evaluate() - #expect( result == expected) - } - - @Test("Mix Operator Logic on Condition Traits", arguments: [(Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) - func MixOperator(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { - let result = try await condition.evaluate() - #expect( result == expected) - } - - @Test("Applying mixed traits", Conditions.condition2 || Conditions.condition2) - func applyMixedTraits() { - #expect(true) - } - - private enum Conditions { - static let condition1 = ConditionTrait.enabled(if: true, "Some comment for condition1") - static let condition2 = ConditionTrait.enabled(if: false, "Some comment for condition2") - static let condition3 = ConditionTrait.disabled(if: true, "Some comment for condition3") - static let condition4 = ConditionTrait.disabled(if: false, "Some comment for condition4") - } } diff --git a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift new file mode 100644 index 000000000..2d327b0d7 --- /dev/null +++ b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift @@ -0,0 +1,38 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// +@testable @_spi(Experimental) import Testing + +@Suite("Grouped Condition Trait Tests", .tags(.traitRelated)) +struct GroupedConditionTraitTests { + + @Test("evaluate grouped conditions",arguments: [((Conditions.condition1 && Conditions.condition1), true), + (Conditions.condition3 && Conditions.condition1, false), + (Conditions.condition1 || Conditions.condition3, true), + (Conditions.condition4 || Conditions.condition4, true), + (Conditions.condition2 || Conditions.condition2, false), (Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) + func evaluateCondition(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { + let result = try await condition.evaluate() + #expect( result == expected) + } + + + + @Test("Applying mixed traits", Conditions.condition2 || Conditions.condition1) + func applyMixedTraits() { + #expect(true) + } + + private enum Conditions { + static let condition1 = ConditionTrait.enabled(if: true, "Some comment for condition1") + static let condition2 = ConditionTrait.enabled(if: false, "Some comment for condition2") + static let condition3 = ConditionTrait.disabled(if: true, "Some comment for condition3") + static let condition4 = ConditionTrait.disabled(if: false, "Some comment for condition4") + } +} From 166c51a10301ca07ccc055d28d485ab590a54b7d Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Mon, 21 Apr 2025 08:57:01 +0200 Subject: [PATCH 05/12] Fixes grouped condition logic Corrects the logic for grouped conditions, particularly when both conditions are inverted. This ensures accurate evaluation of combined boolean expressions. Updates a test case to reflect the corrected logic. --- Sources/Testing/Traits/GroupedConditionTrait.swift | 6 +++++- Tests/TestingTests/Traits/GroupedConditionTraitTests.swift | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift index 1d3863718..a9f780399 100644 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ b/Sources/Testing/Traits/GroupedConditionTrait.swift @@ -87,7 +87,11 @@ extension GroupedConditionTrait { skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) } case .or: - isEnabled = ((l != lhs.isInverted) || (r != rhs.isInverted)) != lhs.isInverted && rhs.isInverted + isEnabled = if (lhs.isInverted && rhs.isInverted) { + !(!l || !r) + } else { + l || r + } if !isEnabled { skipSide = (lhs.comments, lhs.sourceLocation) diff --git a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift index 2d327b0d7..a5fd41b84 100644 --- a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift @@ -24,7 +24,7 @@ struct GroupedConditionTraitTests { - @Test("Applying mixed traits", Conditions.condition2 || Conditions.condition1) + @Test("Applying mixed traits", Conditions.condition1 || Conditions.condition3) func applyMixedTraits() { #expect(true) } From 28461226ce659e47bfadd6707698acb57786e3c0 Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Mon, 21 Apr 2025 09:24:28 +0200 Subject: [PATCH 06/12] Corrects logic for inverted conditions in AND group Fixes an issue where the AND grouped condition was not correctly evaluating inverted conditions. This ensures the correct boolean result is returned when dealing with inverted conditions within an AND group, improving the overall accuracy of grouped condition evaluations. --- Sources/Testing/Traits/GroupedConditionTrait.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift index a9f780399..c2bc1509c 100644 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ b/Sources/Testing/Traits/GroupedConditionTrait.swift @@ -81,7 +81,11 @@ extension GroupedConditionTrait { let isEnabled: Bool switch self { case .and: - isEnabled = l && r + isEnabled = if (lhs.isInverted && rhs.isInverted) { + !(!l && !r) + } else { + l && r + } if !isEnabled { skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) From 7936a1fe44e53383262b4b870bf548d5674e043e Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Mon, 21 Apr 2025 12:23:50 +0200 Subject: [PATCH 07/12] Second Solution: condition trait logic Removes `GroupedConditionTrait` and implements `&&` and `||` operators directly on `ConditionTrait`. This simplifies the condition evaluation process and allows for more concise trait definitions. --- Sources/Testing/Traits/ConditionTrait.swift | 47 +++++-- .../Traits/GroupedConditionTrait.swift | 118 ------------------ .../Traits/GroupedConditionTraitTests.swift | 14 ++- 3 files changed, 47 insertions(+), 132 deletions(-) delete mode 100644 Sources/Testing/Traits/GroupedConditionTrait.swift diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index add70b1cd..71a1668a8 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -223,13 +223,42 @@ extension Trait where Self == ConditionTrait { extension Trait where Self == ConditionTrait { - - public static func && (lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.and]) - } - - public static func || (lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.or]) - } + + static func &&(lhs: Self, rhs: Self) -> Self { + Self(kind: .conditional { + let l = try await lhs.evaluate() + let r = try await rhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + l || r + } else { + l && r + } + guard isEnabled else { + let context = SourceContext(backtrace: nil, sourceLocation: l == false ? lhs.sourceLocation : rhs.sourceLocation) + throw SkipInfo(sourceContext: context) + } + return isEnabled + }, comments: lhs.comments, + sourceLocation: lhs.sourceLocation) + } + + static func ||(lhs: Self, rhs: Self) -> Self { + Self(kind: .conditional { + let l = try await lhs.evaluate() + let r = try await rhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + l && r + } else { + l || r + } + + guard isEnabled else { + let context = SourceContext(backtrace: nil, sourceLocation: l == false ? lhs.sourceLocation : rhs.sourceLocation) + throw SkipInfo(sourceContext: context) + } + + return isEnabled + }, comments: lhs.comments, + sourceLocation: lhs.sourceLocation) + } } - diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift deleted file mode 100644 index c2bc1509c..000000000 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - - -public struct GroupedConditionTrait: TestTrait, SuiteTrait { - - var conditionTraits: [ConditionTrait] - var operations: [Operation] = [] - - public func prepare(for test: Test) async throws { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - try await firstTrait.prepare(for: test) - } - return - } - for (index, operation) in operations.enumerated() where index < traitCount - 1 { - let trait1 = conditionTraits[index] - let trait2 = conditionTraits[index + 1] - try await operation.operate(trait1, trait2, includeSkipInfo: true) - - } - } - - @_spi(Experimental) - public func evaluate() async throws -> Bool { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - return try await firstTrait.evaluate() - } - preconditionFailure() - } - var result: Bool = true - for (index, operation) in operations.enumerated() { - let isEnabled = try await operation.operate(conditionTraits[index], - conditionTraits[index + 1]) - result = result && isEnabled - } - return result - } -} - -extension Trait where Self == GroupedConditionTrait { - - static func && (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.and]) - } - - static func && (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.and] + rhs.operations) - } - static func || (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.or]) - } - - static func || (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.or] + rhs.operations) - } -} - - -extension GroupedConditionTrait { - enum Operation { - case `and` - case `or` - - @discardableResult - func operate(_ lhs: ConditionTrait,_ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { - let (l,r) = try await evaluate(lhs, rhs) - - var skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) = (nil, lhs.sourceLocation) - let isEnabled: Bool - switch self { - case .and: - isEnabled = if (lhs.isInverted && rhs.isInverted) { - !(!l && !r) - } else { - l && r - } - - if !isEnabled { - skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) - } - case .or: - isEnabled = if (lhs.isInverted && rhs.isInverted) { - !(!l || !r) - } else { - l || r - } - - if !isEnabled { - skipSide = (lhs.comments, lhs.sourceLocation) - } - } - - guard isEnabled || !includeSkipInfo else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation) - throw SkipInfo(comment: skipSide.comments?.first, sourceContext: sourceContext) - } - return isEnabled - } - - private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { - let l = try await lhs.evaluate() - let r = try await rhs.evaluate() - return (l, r) - } - } -} diff --git a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift index a5fd41b84..ba74763e3 100644 --- a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift @@ -12,19 +12,23 @@ @Suite("Grouped Condition Trait Tests", .tags(.traitRelated)) struct GroupedConditionTraitTests { - @Test("evaluate grouped conditions",arguments: [((Conditions.condition1 && Conditions.condition1), true), + @Test("evaluate grouped conditions", arguments: [((Conditions.condition1 && Conditions.condition1), true), (Conditions.condition3 && Conditions.condition1, false), (Conditions.condition1 || Conditions.condition3, true), (Conditions.condition4 || Conditions.condition4, true), (Conditions.condition2 || Conditions.condition2, false), (Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) - func evaluateCondition(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { - let result = try await condition.evaluate() - #expect( result == expected) + func evaluateCondition(_ condition: ConditionTrait, _ expected: Bool) async throws { + do { + let result = try await condition.evaluate() + #expect( result == expected) + } catch { + + } } - @Test("Applying mixed traits", Conditions.condition1 || Conditions.condition3) + @Test("Applying mixed traits", Conditions.condition4 || Conditions.condition3) func applyMixedTraits() { #expect(true) } From 5e0b2f1f3bf2768f07a1bd7e2ab6b4cd5a66fc2e Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Mon, 21 Apr 2025 13:21:10 +0200 Subject: [PATCH 08/12] Solution 3: grouped condition trait evaluation Updates grouped condition traits to use closures for evaluation, improving logic and error handling. This change replaces the previous approach of storing condition traits and operations with a single closure that encapsulates the entire evaluation logic. This simplifies the code and makes it easier to reason about, especially in cases where conditions are nested. The closure-based approach also allows for more precise error handling, ensuring that the correct source location is used when skipping tests due to failed conditions. --- Sources/Testing/Traits/ConditionTrait.swift | 36 ++++- .../Traits/GroupedConditionTrait.swift | 146 ++++++++---------- .../Traits/GroupedConditionTraitTests.swift | 8 +- 3 files changed, 107 insertions(+), 83 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index add70b1cd..8c1501542 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -225,11 +225,43 @@ extension Trait where Self == ConditionTrait { extension Trait where Self == ConditionTrait { public static func && (lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.and]) + GroupedConditionTrait(isInverted: lhs.isInverted && rhs.isInverted, + conditionClosure: { + let rhsEvaluation = try await rhs.evaluate() + let lhsEvaluation = try await lhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + lhsEvaluation || rhsEvaluation + } else { + lhsEvaluation && rhsEvaluation + } + + guard isEnabled else { + let sourceContext = SourceContext(backtrace: nil, sourceLocation: !(lhsEvaluation != lhs.isInverted) ? lhs.sourceLocation : rhs.sourceLocation) + let error = SkipInfo(sourceContext: sourceContext) + throw error + } + return isEnabled + }) } public static func || (lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.or]) + GroupedConditionTrait(isInverted: lhs.isInverted && rhs.isInverted, + conditionClosure: { + let rhsEvaluation = try await rhs.evaluate() + let lhsEvaluation = try await lhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + lhsEvaluation && rhsEvaluation + } else { + lhsEvaluation || rhsEvaluation + } + + guard isEnabled else { + let sourceContext = SourceContext(backtrace: nil, sourceLocation: !(lhsEvaluation != lhs.isInverted) ? lhs.sourceLocation : rhs.sourceLocation) + let error = SkipInfo(sourceContext: sourceContext) + throw error + } + return isEnabled + }) } } diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift index c2bc1509c..cbb812dec 100644 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ b/Sources/Testing/Traits/GroupedConditionTrait.swift @@ -11,108 +11,96 @@ public struct GroupedConditionTrait: TestTrait, SuiteTrait { - var conditionTraits: [ConditionTrait] - var operations: [Operation] = [] + let isInverted: Bool + + let conditionClosure: @Sendable () async throws -> Bool + + public func prepare(for test: Test) async throws { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - try await firstTrait.prepare(for: test) - } - return - } - for (index, operation) in operations.enumerated() where index < traitCount - 1 { - let trait1 = conditionTraits[index] - let trait2 = conditionTraits[index + 1] - try await operation.operate(trait1, trait2, includeSkipInfo: true) - - } + let conditionResult = try await evaluate() } @_spi(Experimental) public func evaluate() async throws -> Bool { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - return try await firstTrait.evaluate() - } - preconditionFailure() - } - var result: Bool = true - for (index, operation) in operations.enumerated() { - let isEnabled = try await operation.operate(conditionTraits[index], - conditionTraits[index + 1]) - result = result && isEnabled - } - return result + try await conditionClosure() } } extension Trait where Self == GroupedConditionTrait { static func && (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.and]) + Self(isInverted: lhs.isInverted && rhs.isInverted, + conditionClosure: { + let rhsEvaluation = try await rhs.evaluate() + let lhsEvaluation = try await lhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + lhsEvaluation || rhsEvaluation + } else { + lhsEvaluation && rhsEvaluation + } + + guard isEnabled else { + let sourceContext = SourceContext(backtrace: nil, sourceLocation: rhs.sourceLocation) + let error = SkipInfo(sourceContext: sourceContext) + throw error + } + return isEnabled + }) } static func && (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.and] + rhs.operations) + Self(isInverted: lhs.isInverted && rhs.isInverted, + conditionClosure: { + let rhsEvaluation = try await rhs.evaluate() + let lhsEvaluation = try await lhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + lhsEvaluation || rhsEvaluation + } else { + lhsEvaluation && rhsEvaluation + } + + guard isEnabled else { + preconditionFailure("the step should've detected erailer that it was disabled") + } + return isEnabled + }) } static func || (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.or]) + Self(isInverted: lhs.isInverted && rhs.isInverted, + conditionClosure: { + let rhsEvaluation = try await rhs.evaluate() + let lhsEvaluation = try await lhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + lhsEvaluation && rhsEvaluation + } else { + lhsEvaluation || rhsEvaluation + } + + guard isEnabled else { + let sourceContext = SourceContext(backtrace: nil, sourceLocation: rhs.sourceLocation) + let error = SkipInfo(sourceContext: sourceContext) + throw error + } + return isEnabled + }) } static func || (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.or] + rhs.operations) - } -} - - -extension GroupedConditionTrait { - enum Operation { - case `and` - case `or` - - @discardableResult - func operate(_ lhs: ConditionTrait,_ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { - let (l,r) = try await evaluate(lhs, rhs) - - var skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) = (nil, lhs.sourceLocation) - let isEnabled: Bool - switch self { - case .and: - isEnabled = if (lhs.isInverted && rhs.isInverted) { - !(!l && !r) - } else { - l && r - } - - if !isEnabled { - skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) - } - case .or: - isEnabled = if (lhs.isInverted && rhs.isInverted) { - !(!l || !r) - } else { - l || r - } - - if !isEnabled { - skipSide = (lhs.comments, lhs.sourceLocation) - } + Self(isInverted: lhs.isInverted && rhs.isInverted, + conditionClosure: { + let rhsEvaluation = try await rhs.evaluate() + let lhsEvaluation = try await lhs.evaluate() + let isEnabled = if (lhs.isInverted && rhs.isInverted) { + lhsEvaluation && rhsEvaluation + } else { + lhsEvaluation || rhsEvaluation } - guard isEnabled || !includeSkipInfo else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation) - throw SkipInfo(comment: skipSide.comments?.first, sourceContext: sourceContext) + guard isEnabled else { + preconditionFailure("the step should've detected erailer that it was disabled") } return isEnabled - } - - private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { - let l = try await lhs.evaluate() - let r = try await rhs.evaluate() - return (l, r) - } + }) } } diff --git a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift index a5fd41b84..82e621715 100644 --- a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift @@ -18,8 +18,12 @@ struct GroupedConditionTraitTests { (Conditions.condition4 || Conditions.condition4, true), (Conditions.condition2 || Conditions.condition2, false), (Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) func evaluateCondition(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { - let result = try await condition.evaluate() - #expect( result == expected) + do { + let result = try await condition.evaluate() + #expect( result == expected) + } catch { + + } } From eec099bb663b8d03d437caca96a8b023ff362c53 Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Mon, 21 Apr 2025 16:02:16 +0200 Subject: [PATCH 09/12] Solution 3: condition trait composition logic Introduces a `combine` function to simplify the composition of `ConditionTrait` and `GroupedConditionTrait` instances using logical AND and OR operators. This change reduces code duplication and makes the logic more readable. Also, the `evaluate` method in `GroupedConditionTrait` is updated to only evaluate the condition and discard the result, which is the intended behavior. --- Sources/Testing/Traits/ConditionTrait.swift | 57 +++----- .../Traits/GroupedConditionTrait.swift | 136 +++++++++--------- .../Traits/GroupedConditionTraitTests.swift | 12 +- 3 files changed, 91 insertions(+), 114 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index 5b2644430..c7376d22a 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -223,44 +223,25 @@ extension Trait where Self == ConditionTrait { extension Trait where Self == ConditionTrait { - - public static func && (lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(isInverted: lhs.isInverted && rhs.isInverted, - conditionClosure: { - let rhsEvaluation = try await rhs.evaluate() - let lhsEvaluation = try await lhs.evaluate() - let isEnabled = if (lhs.isInverted && rhs.isInverted) { - lhsEvaluation || rhsEvaluation - } else { - lhsEvaluation && rhsEvaluation - } - - guard isEnabled else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: !(lhsEvaluation != lhs.isInverted) ? lhs.sourceLocation : rhs.sourceLocation) - let error = SkipInfo(sourceContext: sourceContext) - throw error - } - return isEnabled - }) + public static func &&(lhs: Self, rhs: Self) -> GroupedConditionTrait { + GroupedConditionTrait.combine( + lhs: .init(isInverted: lhs.isInverted, conditionClosure: lhs.evaluate), + rhs: .init(isInverted: rhs.isInverted, conditionClosure: rhs.evaluate), + op: .and + ) { lhsRes, rhsRes in + let loc = (lhsRes != lhs.isInverted ? lhs.sourceLocation : rhs.sourceLocation) + throw SkipInfo(sourceContext: .init(backtrace: nil, sourceLocation: loc)) + } } - - public static func || (lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(isInverted: lhs.isInverted && rhs.isInverted, - conditionClosure: { - let rhsEvaluation = try await rhs.evaluate() - let lhsEvaluation = try await lhs.evaluate() - let isEnabled = if (lhs.isInverted && rhs.isInverted) { - lhsEvaluation && rhsEvaluation - } else { - lhsEvaluation || rhsEvaluation - } - - guard isEnabled else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: !(lhsEvaluation != lhs.isInverted) ? lhs.sourceLocation : rhs.sourceLocation) - let error = SkipInfo(sourceContext: sourceContext) - throw error - } - return isEnabled - }) + + public static func ||(lhs: Self, rhs: Self) -> GroupedConditionTrait { + GroupedConditionTrait.combine( + lhs: .init(isInverted: lhs.isInverted, conditionClosure: lhs.evaluate), + rhs: .init(isInverted: rhs.isInverted, conditionClosure: rhs.evaluate), + op: .or + ) { lhsRes, rhsRes in + let loc = (lhsRes != lhs.isInverted ? lhs.sourceLocation : rhs.sourceLocation) + throw SkipInfo(sourceContext: .init(backtrace: nil, sourceLocation: loc)) + } } } diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift index cbb812dec..8a98e4a9e 100644 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ b/Sources/Testing/Traits/GroupedConditionTrait.swift @@ -18,7 +18,7 @@ public struct GroupedConditionTrait: TestTrait, SuiteTrait { public func prepare(for test: Test) async throws { - let conditionResult = try await evaluate() + _ = try await evaluate() } @_spi(Experimental) @@ -27,80 +27,74 @@ public struct GroupedConditionTrait: TestTrait, SuiteTrait { } } + extension Trait where Self == GroupedConditionTrait { - - static func && (lhs: Self, rhs: ConditionTrait) -> Self { - Self(isInverted: lhs.isInverted && rhs.isInverted, - conditionClosure: { - let rhsEvaluation = try await rhs.evaluate() - let lhsEvaluation = try await lhs.evaluate() - let isEnabled = if (lhs.isInverted && rhs.isInverted) { - lhsEvaluation || rhsEvaluation - } else { - lhsEvaluation && rhsEvaluation - } - - guard isEnabled else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: rhs.sourceLocation) - let error = SkipInfo(sourceContext: sourceContext) - throw error - } - return isEnabled - }) + static func &&(lhs: Self, rhs: Self) -> Self { + .combine(lhs: lhs, rhs: rhs, op: .and) { _, _ in + preconditionFailure("the step should've detected earlier that it was disabled") + } } - - static func && (lhs: Self, rhs: Self) -> Self { - Self(isInverted: lhs.isInverted && rhs.isInverted, - conditionClosure: { - let rhsEvaluation = try await rhs.evaluate() - let lhsEvaluation = try await lhs.evaluate() - let isEnabled = if (lhs.isInverted && rhs.isInverted) { - lhsEvaluation || rhsEvaluation - } else { - lhsEvaluation && rhsEvaluation - } - - guard isEnabled else { - preconditionFailure("the step should've detected erailer that it was disabled") - } - return isEnabled - }) + + static func &&(lhs: Self, rhs: ConditionTrait) -> Self { + .combine(lhs: lhs, rhs: .init(isInverted: rhs.isInverted, conditionClosure: rhs.evaluate), + op: .and) { _, _ in + let sourceContext = SourceContext(backtrace: nil, + sourceLocation: rhs.sourceLocation) + throw SkipInfo(sourceContext: sourceContext) + } } - static func || (lhs: Self, rhs: ConditionTrait) -> Self { - Self(isInverted: lhs.isInverted && rhs.isInverted, - conditionClosure: { - let rhsEvaluation = try await rhs.evaluate() - let lhsEvaluation = try await lhs.evaluate() - let isEnabled = if (lhs.isInverted && rhs.isInverted) { - lhsEvaluation && rhsEvaluation - } else { - lhsEvaluation || rhsEvaluation - } - - guard isEnabled else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: rhs.sourceLocation) - let error = SkipInfo(sourceContext: sourceContext) - throw error - } - return isEnabled - }) + + static func ||(lhs: Self, rhs: Self) -> Self { + .combine(lhs: lhs, rhs: rhs, op: .or) { _, _ in + preconditionFailure("the step should've detected earlier that it was disabled") + } } - - static func || (lhs: Self, rhs: Self) -> Self { - Self(isInverted: lhs.isInverted && rhs.isInverted, - conditionClosure: { - let rhsEvaluation = try await rhs.evaluate() - let lhsEvaluation = try await lhs.evaluate() - let isEnabled = if (lhs.isInverted && rhs.isInverted) { - lhsEvaluation && rhsEvaluation - } else { - lhsEvaluation || rhsEvaluation - } - - guard isEnabled else { - preconditionFailure("the step should've detected erailer that it was disabled") + + static func ||(lhs: Self, rhs: ConditionTrait) -> Self { + .combine(lhs: lhs, rhs: .init(isInverted: rhs.isInverted, conditionClosure: rhs.evaluate), + op: .or) { _, _ in + let sourceContext = SourceContext(backtrace: nil, + sourceLocation: rhs.sourceLocation) + throw SkipInfo(sourceContext: sourceContext) + } + } +} + + +internal extension GroupedConditionTrait { + enum Operation { case and, or } + + static func combine( + lhs: GroupedConditionTrait, + rhs: GroupedConditionTrait, + op: Operation, + onFailure: @Sendable @escaping (_ lhs: Bool, _ rhs: Bool) throws -> Void + ) -> GroupedConditionTrait { + GroupedConditionTrait( + isInverted: lhs.isInverted && rhs.isInverted, + conditionClosure: { + let lhsEvaluated = try await lhs.evaluate() + let rhsEvaluated = try await rhs.evaluate() + + let isEnabled: Bool + switch op { + case .and: + isEnabled = (lhs.isInverted && rhs.isInverted) + ? (lhsEvaluated || rhsEvaluated) + : (lhsEvaluated && rhsEvaluated) + case .or: + isEnabled = (lhs.isInverted && rhs.isInverted) + ? (lhsEvaluated && rhsEvaluated) + : (lhsEvaluated || rhsEvaluated) + } + + guard isEnabled else { + try onFailure(lhsEvaluated, rhsEvaluated) + preconditionFailure("Unreachable: failure handler didn't throw") + } + return isEnabled } - return isEnabled - }) + ) } } + diff --git a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift index 086713cc9..cb565707b 100644 --- a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift @@ -13,10 +13,11 @@ struct GroupedConditionTraitTests { @Test("evaluate grouped conditions", arguments: [((Conditions.condition1 && Conditions.condition1), true), - (Conditions.condition3 && Conditions.condition1, false), - (Conditions.condition1 || Conditions.condition3, true), - (Conditions.condition4 || Conditions.condition4, true), - (Conditions.condition2 || Conditions.condition2, false), (Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) + (Conditions.condition3 && Conditions.condition1, false), + (Conditions.condition1 || Conditions.condition3, true), + (Conditions.condition4 || Conditions.condition4, true), + (Conditions.condition2 || Conditions.condition2, false), + (Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) func evaluateCondition(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { do { let result = try await condition.evaluate() @@ -28,7 +29,8 @@ struct GroupedConditionTraitTests { - @Test("Applying mixed traits", Conditions.condition4 || Conditions.condition3) + @Test("Applying mixed traits", + Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4) func applyMixedTraits() { #expect(true) } From a9e47d6966a676349990996b2280fb25c458d247 Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Tue, 22 Apr 2025 09:52:54 +0200 Subject: [PATCH 10/12] Refactors GroupedConditionTrait for clarity Improves the structure and logic of `GroupedConditionTrait` for better readability and maintainability. Simplifies the evaluation process and introduces helper functions to clarify the logic behind AND/OR operations. Adds handling for `SkipInfo` within grouped conditions to ensure skipping is properly propagated. --- .../Traits/GroupedConditionTrait.swift | 242 +++++++++++------- .../Traits/GroupedConditionTraitTests.swift | 2 +- 2 files changed, 152 insertions(+), 92 deletions(-) diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift index c2bc1509c..e8023b6db 100644 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ b/Sources/Testing/Traits/GroupedConditionTrait.swift @@ -10,109 +10,169 @@ public struct GroupedConditionTrait: TestTrait, SuiteTrait { - - var conditionTraits: [ConditionTrait] - var operations: [Operation] = [] - - public func prepare(for test: Test) async throws { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - try await firstTrait.prepare(for: test) - } - return + internal let conditionTraits: [ConditionTrait] + internal let operations: [Operation] + + /// Initializes a new `GroupedConditionTrait`. + /// - Parameters: + /// - conditionTraits: An array of `ConditionTrait`s to group. Defaults to an empty array. + /// - operations: An array of `Operation`s to apply between the condition traits. Defaults to an empty array. + public init(conditionTraits: [ConditionTrait] = [], operations: [Operation] = []) { + self.conditionTraits = conditionTraits + self.operations = operations } - for (index, operation) in operations.enumerated() where index < traitCount - 1 { - let trait1 = conditionTraits[index] - let trait2 = conditionTraits[index + 1] - try await operation.operate(trait1, trait2, includeSkipInfo: true) - + + /// Prepares the trait for a test by evaluating the grouped conditions. + /// - Parameter test: The `Test` for which to prepare. + public func prepare(for test: Test) async throws { + try await evaluate() } - } - - @_spi(Experimental) - public func evaluate() async throws -> Bool { - let traitCount = conditionTraits.count - guard traitCount >= 2 else { - if let firstTrait = conditionTraits.first { - return try await firstTrait.evaluate() - } - preconditionFailure() + + /// Evaluates the grouped condition traits based on the specified operations. + /// + /// - Returns: `true` if the grouped conditions evaluate to true, `false` otherwise. + /// - Throws: `SkipInfo` if a condition evaluates to skip and the overall result is false. + @_spi(Experimental) + public func evaluate() async throws -> Bool { + switch conditionTraits.count { + case 0: + preconditionFailure("GroupedConditionTrait must have at least one condition trait.") + case 1: + return try await conditionTraits.first!.evaluate() + default: + return try await evaluateGroupedConditions() + } } - var result: Bool = true - for (index, operation) in operations.enumerated() { - let isEnabled = try await operation.operate(conditionTraits[index], - conditionTraits[index + 1]) - result = result && isEnabled + + private func evaluateGroupedConditions() async throws -> Bool { + var result: Bool? + var skipInfo: SkipInfo? + + for (index, operation) in operations.enumerated() where index < conditionTraits.count - 1 { + do { + let isEnabled = try await operation.operate( + conditionTraits[index], + conditionTraits[index + 1], + includeSkipInfo: true + ) + result = updateResult(currentResult: result, isEnabled: isEnabled, operation: operation) + } catch let error as SkipInfo { + result = updateResult(currentResult: result, isEnabled: false, operation: operation) + skipInfo = error + } + } + + if let skipInfo = skipInfo, !result! { + throw skipInfo + } + + return result! } - return result - } -} -extension Trait where Self == GroupedConditionTrait { - - static func && (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.and]) - } - - static func && (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.and] + rhs.operations) - } - static func || (lhs: Self, rhs: ConditionTrait) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [.or]) - } - - static func || (lhs: Self, rhs: Self) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [.or] + rhs.operations) - } + private func updateResult(currentResult: Bool?, isEnabled: Bool, operation: Operation) -> Bool { + if let currentResult = currentResult { + return operation == .and ? currentResult && isEnabled : currentResult || isEnabled + } else { + return isEnabled + } + } } - extension GroupedConditionTrait { - enum Operation { - case `and` - case `or` - - @discardableResult - func operate(_ lhs: ConditionTrait,_ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { - let (l,r) = try await evaluate(lhs, rhs) - - var skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) = (nil, lhs.sourceLocation) - let isEnabled: Bool - switch self { - case .and: - isEnabled = if (lhs.isInverted && rhs.isInverted) { - !(!l && !r) - } else { - l && r + /// Represents the logical operation to apply between two condition traits. + public enum Operation : Sendable { + /// Logical AND operation. + case and + /// Logical OR operation. + case or + + /// Applies the logical operation between two condition traits. + /// - Parameters: + /// - lhs: The left-hand side `ConditionTrait`. + /// - rhs: The right-hand side `ConditionTrait`. + /// - includeSkipInfo: A Boolean value indicating whether to include `SkipInfo` in the evaluation. Defaults to `false`. + /// - Returns: `true` if the operation results in true, `false` otherwise. + /// - Throws: `SkipInfo` if the operation results in false and `includeSkipInfo` is true. + @discardableResult + func operate(_ lhs: ConditionTrait, _ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { + let (leftResult, rightResult) = try await evaluate(lhs, rhs) + + let isEnabled: Bool + let skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) + + switch self { + case .and: + isEnabled = evaluateAnd(left: lhs, right: rhs, leftResult: leftResult, rightResult: rightResult) + skipSide = !isEnabled && !rightResult ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) + case .or: + isEnabled = evaluateOr(left: lhs, right: rhs, leftResult: leftResult, rightResult: rightResult) + skipSide = !isEnabled ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) + } + + guard isEnabled || !includeSkipInfo else { + throw SkipInfo(comment: skipSide.comments?.first, sourceContext: SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation)) + } + return isEnabled } - - if !isEnabled { - skipSide = r ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) + + private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { + async let leftEvaluation = lhs.evaluate() + async let rightEvaluation = rhs.evaluate() + return (try await leftEvaluation, try await rightEvaluation) } - case .or: - isEnabled = if (lhs.isInverted && rhs.isInverted) { - !(!l || !r) - } else { - l || r + + private func evaluateAnd(left: ConditionTrait, right: ConditionTrait, leftResult: Bool, rightResult: Bool) -> Bool { + return left.isInverted && right.isInverted ? leftResult || rightResult : leftResult && rightResult } - - if !isEnabled { - skipSide = (lhs.comments, lhs.sourceLocation) + + private func evaluateOr(left: ConditionTrait, right: ConditionTrait, leftResult: Bool, rightResult: Bool) -> Bool { + return left.isInverted && right.isInverted ? leftResult && rightResult : leftResult || rightResult } - } - - guard isEnabled || !includeSkipInfo else { - let sourceContext = SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation) - throw SkipInfo(comment: skipSide.comments?.first, sourceContext: sourceContext) - } - return isEnabled } - - private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { - let l = try await lhs.evaluate() - let r = try await rhs.evaluate() - return (l, r) +} + +extension Trait where Self == GroupedConditionTrait { + private static func createGroupedTrait(lhs: Self, rhs: ConditionTrait, operation: GroupedConditionTrait.Operation) -> Self { + Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [operation]) + } + + private static func createGroupedTrait(lhs: Self, rhs: Self, operation: GroupedConditionTrait.Operation) -> Self { + Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [operation] + rhs.operations) + } + + /// Creates a new `GroupedConditionTrait` by performing a logical AND with another `ConditionTrait`. + /// - Parameters: + /// - lhs: The left-hand side `GroupedConditionTrait`. + /// - rhs: The right-hand side `ConditionTrait`. + /// - Returns: A new `GroupedConditionTrait` representing the logical AND of the two. + static func && (lhs: Self, rhs: ConditionTrait) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .and) + } + + /// Creates a new `GroupedConditionTrait` by performing a logical AND with another `GroupedConditionTrait`. + /// - Parameters: + /// - lhs: The left-hand side `GroupedConditionTrait`. + /// - rhs: The right-hand side `GroupedConditionTrait`. + /// - Returns: A new `GroupedConditionTrait` representing the logical AND of the two. + static func && (lhs: Self, rhs: Self) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .and) + } + + /// Creates a new `GroupedConditionTrait` by performing a logical OR with another `ConditionTrait`. + /// - Parameters: + /// - lhs: The left-hand side `GroupedConditionTrait`. + /// - rhs: The right-hand side `ConditionTrait`. + /// - Returns: A new `GroupedConditionTrait` representing the logical OR of the two. + static func || (lhs: Self, rhs: ConditionTrait) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .or) + } + + /// Creates a new `GroupedConditionTrait` by performing a logical OR with another `GroupedConditionTrait`. + /// - Parameters: + /// - lhs: The left-hand side `GroupedConditionTrait`. + /// - rhs: The right-hand side `GroupedConditionTrait`. + /// - Returns: A new `GroupedConditionTrait` representing the logical OR of the two. + static func || (lhs: Self, rhs: Self) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .or) } - } } diff --git a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift index a5fd41b84..b5d44dbea 100644 --- a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift @@ -24,7 +24,7 @@ struct GroupedConditionTraitTests { - @Test("Applying mixed traits", Conditions.condition1 || Conditions.condition3) + @Test("Applying mixed traits", Conditions.condition2 || Conditions.condition2 || Conditions.condition2 || Conditions.condition2) func applyMixedTraits() { #expect(true) } From e7f05dc93ce53cfcdc9db41c6af6d2c25342bf66 Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Tue, 22 Apr 2025 09:58:03 +0200 Subject: [PATCH 11/12] fix merge error after merging `GroupConditionTrait` --- Sources/Testing/Traits/ConditionTrait.swift | 18 ++---------------- .../Testing/Traits/GroupedConditionTrait.swift | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index c7376d22a..b2a1d149d 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -224,24 +224,10 @@ extension Trait where Self == ConditionTrait { extension Trait where Self == ConditionTrait { public static func &&(lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait.combine( - lhs: .init(isInverted: lhs.isInverted, conditionClosure: lhs.evaluate), - rhs: .init(isInverted: rhs.isInverted, conditionClosure: rhs.evaluate), - op: .and - ) { lhsRes, rhsRes in - let loc = (lhsRes != lhs.isInverted ? lhs.sourceLocation : rhs.sourceLocation) - throw SkipInfo(sourceContext: .init(backtrace: nil, sourceLocation: loc)) + GroupedConditionTrait(conditionTraits: [lhs,rhs], operations: [.and]) } - } public static func ||(lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait.combine( - lhs: .init(isInverted: lhs.isInverted, conditionClosure: lhs.evaluate), - rhs: .init(isInverted: rhs.isInverted, conditionClosure: rhs.evaluate), - op: .or - ) { lhsRes, rhsRes in - let loc = (lhsRes != lhs.isInverted ? lhs.sourceLocation : rhs.sourceLocation) - throw SkipInfo(sourceContext: .init(backtrace: nil, sourceLocation: loc)) - } + GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.or]) } } diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift index b6ab2ecac..b908454bb 100644 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ b/Sources/Testing/Traits/GroupedConditionTrait.swift @@ -25,7 +25,7 @@ public struct GroupedConditionTrait: TestTrait, SuiteTrait { /// Prepares the trait for a test by evaluating the grouped conditions. /// - Parameter test: The `Test` for which to prepare. public func prepare(for test: Test) async throws { - try await evaluate() + _ = try await evaluate() } /// Evaluates the grouped condition traits based on the specified operations. From a9a85b0b87593b0fe70ee5085f0aa31b4afd6ecd Mon Sep 17 00:00:00 2001 From: Sam Rayatnia Date: Mon, 5 May 2025 18:54:30 +0200 Subject: [PATCH 12/12] Adds documentation to `ConditionTrait` custom operators - renamed `GroupedConditionTrait` to `GroupedConditionTraits` - Add `GroupedConditionTraits` to CMakeList file --- Sources/Testing/CMakeLists.txt | 1 + Sources/Testing/Traits/ConditionTrait.swift | 78 +++++++- .../Traits/GroupedConditionTrait.swift | 179 ------------------ .../Traits/GroupedConditionTraits.swift | 141 ++++++++++++++ .../Traits/GroupedConditionTraitTests.swift | 2 +- 5 files changed, 216 insertions(+), 185 deletions(-) delete mode 100644 Sources/Testing/Traits/GroupedConditionTrait.swift create mode 100644 Sources/Testing/Traits/GroupedConditionTraits.swift diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 5b84aeaf3..fabff7b4e 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -95,6 +95,7 @@ add_library(Testing Traits/Comment.swift Traits/Comment+Macro.swift Traits/ConditionTrait.swift + Traits/GroupedConditionTraits.swift Traits/ConditionTrait+Macro.swift Traits/HiddenTrait.swift Traits/IssueHandlingTrait.swift diff --git a/Sources/Testing/Traits/ConditionTrait.swift b/Sources/Testing/Traits/ConditionTrait.swift index 6a27f6b59..4a48f2835 100644 --- a/Sources/Testing/Traits/ConditionTrait.swift +++ b/Sources/Testing/Traits/ConditionTrait.swift @@ -226,11 +226,79 @@ extension Trait where Self == ConditionTrait { extension Trait where Self == ConditionTrait { - public static func &&(lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(conditionTraits: [lhs,rhs], operations: [.and]) - } + /// Combines two ``ConditionTrait`` conditions using the AND (`&&`) operator. + /// + /// Use this operator to group two conditions such that + /// the resulting ``GroupedConditionTraits`` + /// evaluates to `true` **only if both** subconditions are `true`. + /// + /// - Example: + /// ```swift + /// struct AppFeature { + /// static let isFeatureEnabled: Bool = true + /// static let osIsAndroid: Bool = true + /// + /// static let featureCondition: ConditionTrait = .disabled(if: isFeatureEnabled) + /// static let osCondition: ConditionTrait = .disabled(if: osIsAndroid) + /// } + /// + /// @Test(AppFeature.featureCondition && AppFeature.osCondition) + /// func foo() {} + /// + /// @Test(.disabled(if: AppFeature.isFeatureEnabled && AppFeature.osIsAndroid)) + /// func bar() {} + /// ``` + /// In this example, both `foo` and `bar` will be disabled only when **both** + /// `AppFeature.isFeatureEnabled` is `true` and the OS is Android. + /// + /// - Parameters: + /// - lhs: The left-hand side condition. + /// - rhs: The right-hand side condition. + /// - Returns: A ``GroupedConditionTraits`` instance + /// representing the AND of the two conditions. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } + public static func &&(lhs: Self, rhs: Self) -> GroupedConditionTraits { + GroupedConditionTraits(conditionTraits: [lhs, rhs], operations: [.and]) + } - public static func ||(lhs: Self, rhs: Self) -> GroupedConditionTrait { - GroupedConditionTrait(conditionTraits: [lhs, rhs], operations: [.or]) + /// Combines two ``ConditionTrait`` conditions using the OR (`||`) operator. + /// + /// Use this operator to group two conditions such that + /// the resulting ``GroupedConditionTraits`` + /// evaluates to `true` if **either** of the subconditions is `true`. + /// + /// - Example: + /// ```swift + /// struct AppFeature { + /// static let isInternalBuild: Bool = false + /// static let isSimulator: Bool = true + /// + /// static let buildCondition: ConditionTrait = .enabled(if: isInternalBuild) + /// static let platformCondition: ConditionTrait = .enabled(if: isSimulator) + /// } + /// + /// @Test(AppFeature.buildCondition || AppFeature.platformCondition) + /// func foo() {} + /// + /// @Test(.enabled(if: AppFeature.isInternalBuild || AppFeature.isSimulator)) + /// func bar() {} + /// ``` + /// In this example, both `foo` and `bar` will be enabled when **either** + /// the build is internal or running on a simulator. + /// + /// - Parameters: + /// - lhs: The left-hand side condition. + /// - rhs: The right-hand side condition. + /// - Returns: A ``GroupedConditionTraits`` instance + /// representing the OR of the two conditions. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.2) + /// } + public static func ||(lhs: Self, rhs: Self) -> GroupedConditionTraits { + GroupedConditionTraits(conditionTraits: [lhs, rhs], operations: [.or]) } } diff --git a/Sources/Testing/Traits/GroupedConditionTrait.swift b/Sources/Testing/Traits/GroupedConditionTrait.swift deleted file mode 100644 index b908454bb..000000000 --- a/Sources/Testing/Traits/GroupedConditionTrait.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2025 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for Swift project authors -// - - -public struct GroupedConditionTrait: TestTrait, SuiteTrait { - internal let conditionTraits: [ConditionTrait] - internal let operations: [Operation] - - /// Initializes a new `GroupedConditionTrait`. - /// - Parameters: - /// - conditionTraits: An array of `ConditionTrait`s to group. Defaults to an empty array. - /// - operations: An array of `Operation`s to apply between the condition traits. Defaults to an empty array. - public init(conditionTraits: [ConditionTrait] = [], operations: [Operation] = []) { - self.conditionTraits = conditionTraits - self.operations = operations - } - - /// Prepares the trait for a test by evaluating the grouped conditions. - /// - Parameter test: The `Test` for which to prepare. - public func prepare(for test: Test) async throws { - _ = try await evaluate() - } - - /// Evaluates the grouped condition traits based on the specified operations. - /// - /// - Returns: `true` if the grouped conditions evaluate to true, `false` otherwise. - /// - Throws: `SkipInfo` if a condition evaluates to skip and the overall result is false. - @_spi(Experimental) - public func evaluate() async throws -> Bool { - switch conditionTraits.count { - case 0: - preconditionFailure("GroupedConditionTrait must have at least one condition trait.") - case 1: - return try await conditionTraits.first!.evaluate() - default: - return try await evaluateGroupedConditions() - } - } - - private func evaluateGroupedConditions() async throws -> Bool { - var result: Bool? - var skipInfo: SkipInfo? - - for (index, operation) in operations.enumerated() where index < conditionTraits.count - 1 { - do { - let isEnabled = try await operation.operate( - conditionTraits[index], - conditionTraits[index + 1], - includeSkipInfo: true - ) - result = updateResult(currentResult: result, isEnabled: isEnabled, operation: operation) - } catch let error as SkipInfo { - result = updateResult(currentResult: result, isEnabled: false, operation: operation) - skipInfo = error - } - } - - if let skipInfo = skipInfo, !result! { - throw skipInfo - } - - return result! - } - - private func updateResult(currentResult: Bool?, isEnabled: Bool, operation: Operation) -> Bool { - if let currentResult = currentResult { - return operation == .and ? currentResult && isEnabled : currentResult || isEnabled - } else { - return isEnabled - } - } -} - -extension GroupedConditionTrait { - /// Represents the logical operation to apply between two condition traits. - public enum Operation : Sendable { - /// Logical AND operation. - case and - /// Logical OR operation. - case or - - /// Applies the logical operation between two condition traits. - /// - Parameters: - /// - lhs: The left-hand side `ConditionTrait`. - /// - rhs: The right-hand side `ConditionTrait`. - /// - includeSkipInfo: A Boolean value indicating whether to include `SkipInfo` in the evaluation. Defaults to `false`. - /// - Returns: `true` if the operation results in true, `false` otherwise. - /// - Throws: `SkipInfo` if the operation results in false and `includeSkipInfo` is true. - @discardableResult - func operate(_ lhs: ConditionTrait, _ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { - let (leftResult, rightResult) = try await evaluate(lhs, rhs) - - let isEnabled: Bool - let skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) - - switch self { - case .and: - isEnabled = evaluateAnd(left: lhs, right: rhs, leftResult: leftResult, rightResult: rightResult) - skipSide = !isEnabled && !rightResult ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) - case .or: - isEnabled = evaluateOr(left: lhs, right: rhs, leftResult: leftResult, rightResult: rightResult) - skipSide = !isEnabled ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) - } - - guard isEnabled || !includeSkipInfo else { - throw SkipInfo(comment: skipSide.comments?.first, sourceContext: SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation)) - } - return isEnabled - } - - private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { - async let leftEvaluation = lhs.evaluate() - async let rightEvaluation = rhs.evaluate() - return (try await leftEvaluation, try await rightEvaluation) - } - - private func evaluateAnd(left: ConditionTrait, right: ConditionTrait, leftResult: Bool, rightResult: Bool) -> Bool { - return left.isInverted && right.isInverted ? leftResult || rightResult : leftResult && rightResult - } - - private func evaluateOr(left: ConditionTrait, right: ConditionTrait, leftResult: Bool, rightResult: Bool) -> Bool { - return left.isInverted && right.isInverted ? leftResult && rightResult : leftResult || rightResult - } - } -} - -extension Trait where Self == GroupedConditionTrait { - private static func createGroupedTrait(lhs: Self, rhs: ConditionTrait, operation: GroupedConditionTrait.Operation) -> Self { - Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [operation]) - } - - private static func createGroupedTrait(lhs: Self, rhs: Self, operation: GroupedConditionTrait.Operation) -> Self { - Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [operation] + rhs.operations) - } - - /// Creates a new `GroupedConditionTrait` by performing a logical AND with another `ConditionTrait`. - /// - Parameters: - /// - lhs: The left-hand side `GroupedConditionTrait`. - /// - rhs: The right-hand side `ConditionTrait`. - /// - Returns: A new `GroupedConditionTrait` representing the logical AND of the two. - static func && (lhs: Self, rhs: ConditionTrait) -> Self { - createGroupedTrait(lhs: lhs, rhs: rhs, operation: .and) - } - - /// Creates a new `GroupedConditionTrait` by performing a logical AND with another `GroupedConditionTrait`. - /// - Parameters: - /// - lhs: The left-hand side `GroupedConditionTrait`. - /// - rhs: The right-hand side `GroupedConditionTrait`. - /// - Returns: A new `GroupedConditionTrait` representing the logical AND of the two. - static func && (lhs: Self, rhs: Self) -> Self { - createGroupedTrait(lhs: lhs, rhs: rhs, operation: .and) - } - - /// Creates a new `GroupedConditionTrait` by performing a logical OR with another `ConditionTrait`. - /// - Parameters: - /// - lhs: The left-hand side `GroupedConditionTrait`. - /// - rhs: The right-hand side `ConditionTrait`. - /// - Returns: A new `GroupedConditionTrait` representing the logical OR of the two. - static func || (lhs: Self, rhs: ConditionTrait) -> Self { - createGroupedTrait(lhs: lhs, rhs: rhs, operation: .or) - } - - /// Creates a new `GroupedConditionTrait` by performing a logical OR with another `GroupedConditionTrait`. - /// - Parameters: - /// - lhs: The left-hand side `GroupedConditionTrait`. - /// - rhs: The right-hand side `GroupedConditionTrait`. - /// - Returns: A new `GroupedConditionTrait` representing the logical OR of the two. - static func || (lhs: Self, rhs: Self) -> Self { - createGroupedTrait(lhs: lhs, rhs: rhs, operation: .or) - } -} - diff --git a/Sources/Testing/Traits/GroupedConditionTraits.swift b/Sources/Testing/Traits/GroupedConditionTraits.swift new file mode 100644 index 000000000..3c48e427d --- /dev/null +++ b/Sources/Testing/Traits/GroupedConditionTraits.swift @@ -0,0 +1,141 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for Swift project authors +// + +/// A type to aggregate ``ConditionTrait`` in a way that it merge subcondition +/// instead of logical operation on conditions. +/// +public struct GroupedConditionTraits: TestTrait, SuiteTrait { + fileprivate let conditionTraits: [ConditionTrait] + fileprivate let operations: [Operation] + + internal init(conditionTraits: [ConditionTrait] = [], operations: [Operation] = []) { + self.conditionTraits = conditionTraits + self.operations = operations + } + + public func prepare(for test: Test) async throws { + _ = try await evaluate() + } + + @_spi(Experimental) + public func evaluate() async throws -> Bool { + switch conditionTraits.count { + case 0: + preconditionFailure("GroupedConditionTrait must have at least one condition trait.") + case 1: + return try await conditionTraits.first!.evaluate() + default: + return try await evaluateGroupedConditions() + } + } + + private func evaluateGroupedConditions() async throws -> Bool { + var result: Bool? + var skipInfo: SkipInfo? + + for (index, operation) in operations.enumerated() where index < conditionTraits.count - 1 { + do { + let isEnabled = try await operation.operate( + conditionTraits[index], + conditionTraits[index + 1], + includeSkipInfo: true + ) + result = updateResult(currentResult: result, isEnabled: isEnabled, operation: operation) + } catch let error as SkipInfo { + result = updateResult(currentResult: result, isEnabled: false, operation: operation) + skipInfo = error + } + } + + if let skipInfo = skipInfo, !result! { + throw skipInfo + } + + return result! + } + + private func updateResult(currentResult: Bool?, isEnabled: Bool, operation: Operation) -> Bool { + if let currentResult = currentResult { + return operation == .and ? currentResult && isEnabled : currentResult || isEnabled + } else { + return isEnabled + } + } +} + +internal extension GroupedConditionTraits { + enum Operation : Sendable { + case and + case or + + @discardableResult + fileprivate func operate(_ lhs: ConditionTrait, _ rhs: ConditionTrait, includeSkipInfo: Bool = false) async throws -> Bool { + let (leftResult, rightResult) = try await evaluate(lhs, rhs) + + let isEnabled: Bool + let skipSide: (comments: [Comment]?, sourceLocation: SourceLocation) + + switch self { + case .and: + isEnabled = evaluateAnd(left: lhs, right: rhs, leftResult: leftResult, rightResult: rightResult) + skipSide = !isEnabled && !rightResult ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) + case .or: + isEnabled = evaluateOr(left: lhs, right: rhs, leftResult: leftResult, rightResult: rightResult) + skipSide = !isEnabled ? (lhs.comments, lhs.sourceLocation) : (rhs.comments, rhs.sourceLocation) + } + + guard isEnabled || !includeSkipInfo else { + throw SkipInfo(comment: skipSide.comments?.first, sourceContext: SourceContext(backtrace: nil, sourceLocation: skipSide.sourceLocation)) + } + return isEnabled + } + + private func evaluate(_ lhs: ConditionTrait, _ rhs: ConditionTrait) async throws -> (Bool, Bool) { + async let leftEvaluation = lhs.evaluate() + async let rightEvaluation = rhs.evaluate() + return (try await leftEvaluation, try await rightEvaluation) + } + + private func evaluateAnd(left: ConditionTrait, right: ConditionTrait, leftResult: Bool, rightResult: Bool) -> Bool { + return left.isInverted && right.isInverted ? leftResult || rightResult : leftResult && rightResult + } + + private func evaluateOr(left: ConditionTrait, right: ConditionTrait, leftResult: Bool, rightResult: Bool) -> Bool { + return left.isInverted && right.isInverted ? leftResult && rightResult : leftResult || rightResult + } + } +} + +public extension Trait where Self == GroupedConditionTraits { + private static func createGroupedTrait(lhs: Self, rhs: ConditionTrait, operation: GroupedConditionTraits.Operation) -> Self { + Self(conditionTraits: lhs.conditionTraits + [rhs], operations: lhs.operations + [operation]) + } + + private static func createGroupedTrait(lhs: Self, rhs: Self, operation: GroupedConditionTraits.Operation) -> Self { + Self(conditionTraits: lhs.conditionTraits + rhs.conditionTraits, operations: lhs.operations + [operation] + rhs.operations) + } + + static func && (lhs: Self, rhs: ConditionTrait) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .and) + } + + static func && (lhs: Self, rhs: Self) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .and) + } + + static func || (lhs: Self, rhs: ConditionTrait) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .or) + } + + static func || (lhs: Self, rhs: Self) -> Self { + createGroupedTrait(lhs: lhs, rhs: rhs, operation: .or) + } +} + diff --git a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift index aaa10ee24..bae9af605 100644 --- a/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift +++ b/Tests/TestingTests/Traits/GroupedConditionTraitTests.swift @@ -18,7 +18,7 @@ struct GroupedConditionTraitTests { (Conditions.condition4 || Conditions.condition4, true), (Conditions.condition2 || Conditions.condition2, false), (Conditions.condition1 && Conditions.condition2 || Conditions.condition3 && Conditions.condition4, false)]) - func evaluateCondition(_ condition: GroupedConditionTrait, _ expected: Bool) async throws { + func evaluateCondition(_ condition: GroupedConditionTraits, _ expected: Bool) async throws { do { let result = try await condition.evaluate() #expect( result == expected)