-
Notifications
You must be signed in to change notification settings - Fork 117
Description
Motivation
As a test case author, I want to explicitly enable, or disable, a test when:
- all conditional traits evaluate to
true
- any conditional training evaluate to
true
I always prefer explicit vs implicit. Take the following example
@Test(
.enabled(if: conditionA),
.enabled(if: conditionB),
)
func myTest() {
// test implementation
}
It is unclear by ready the code whether the various conditions are ||
(OR-ed), or whether they are &&
(AND-ed). A developer must be aware of Swift Testing behaviour to know when the test will be enabled or disabled.
Proposed solution
One option is to introduce a .any
and .all
traits, that would accept a list of traits, or any number of traits.
Consider the following, which is more explicit. In the first, conditionA && conditionB
must be true to enable the test, while the conditionA || conditionB
must be true to enable the second test
@Test(
.all(
.enabled(if: conditionA),
.enabled(if: conditionB),
)
)
func myTestBothConditionsMustBeTrue() {
// test implementation
}
@Test(
.any(
.enabled(if: conditionA),
.enabled(if: conditionB),
)
)
func myTestAnyConditionMustBeTrue() {
// test implementation
}
The .any
and .all
become especially useful when custom condition traits are added, where using the single .enabled(if: condition)
is more challenging.
extension Trait where Self == Testing.ConditionTrait {
public static var enableOnlyOnMacOS: Self {
enabled(if: isMacOS(), "Test is only supported on MacOS")
}
public static var enableIfRealSigningIdentityTestEnabled: Self {
enabled(if: isRealSigningIdentityTestEnabled(), "Test only enabled during real signing indentity")
}
public static func enableIfEnvVarSet(_ envVarName: String) -> Self {
let envKey = EnvironmentKey(envVarName)
return enabled(
if: isEnvironmentVariableSet(envKey),
"'\(envVarName)' env var is not set"
)
}
}
@Test (
.all(
.enableOnlyOnMacOS,
.enableIfRealSigningIdentityTestEnabled,
.enableIfEnvVarSet("MY_ENV_VARIABLE"),
)
)
funct myTest() {
}
Alternatives considered
I'm open to other solution
Additional information
No response
Activity
grynspan commentedon Mar 21, 2025
Tracked internally as rdar://136052793.
grynspan commentedon Mar 21, 2025
I suspect we would probably implement this in terms of standard Boolean operators, not a bespoke DSL.
hrayatnia commentedon Mar 29, 2025
@bkhouri @grynspan I like to take a lead on this issue. Before I start coding, I came up with a rough idea to ask for your opinion and then I will implement the rest of it.
grynspan commentedon Mar 29, 2025
My thinking here was that we would implement
||
and&&
operators that would compose multiple instances ofConditionTrait
into compound ones (with additional implementation details inside the type to support compound operations without losing track of which specific trait caused a test to be skipped.)Then you'd be able to write something like:
@Test(.enabled(if: abc123) || .enabled(if: def456))
What makes this somewhat more complicated (regardless of how we spell the operations) is that
.disabled(if: x)
, which is effectively identical to.enabled(if: !x)
, may not clearly compose. For instance,.disabled(if: x) || .disabled(if: y)
equates to.enabled(if: !x) || .enabled(if: !y)
whereas you probably meant it to be.enabled(if: !x) && .enabled(if: !y)
.To solve that, we may need to teach the operator implementations to treat
.enabled
and.disabled
differently, which is feasible but adds more complexity to the overall implementation.hrayatnia commentedon Mar 30, 2025
@grynspan I completely agree that preserving correct information is essential. However, the example logic doesn’t seem accurate.
According to De Morgan’s laws, the correct transformation should be:
In abstract terms, the pseudo-code representation would look like this:
That said, I recognize that this approach isn't optimized—it introduces unnecessary overhead without simplifying the logic. I'll work on a more efficient solution.
grynspan commentedon Mar 30, 2025
That's not quite how these operators combine today if you just use them within a single trait, and I suspect that could be problematic. For instance:
Should probably be equivalent to:
Not to:
hrayatnia commentedon Mar 30, 2025
Correct me if I'm wrong, the expected end result should compose/merge logics instead of seeing like a boolean value? In other words (as you said), instead of
.disabled(if: x) || .disabled(if: y) == .enabled(if: !(x && y))
it should be.disabled(if: x) || .disabled(if: y) == .disabled(if: x || y) == .enabled(if: !(x || y))
right?If that’s not the case, the suggested logic interprets it as a boolean-like value (with ExpressibleByBooleanLiteral) and all nine fundamental laws for conjunction and disjunction applies to it.
grynspan commentedon Mar 30, 2025
Right, my gut sense is that
.disabled(if: x) || .disabled(if: y)
and.disabled(if: x || y)
should evaluate to the same result (regardless of how we actually spell the symbols, using functions or operators.)Does that make sense to you?
hrayatnia commentedon Mar 30, 2025
Yes, it does, thanks for sharing that. It’s more of a grouping strategy rather than being a separation of logic.
Like
.disable(if: Sensory.allCases, operator: ||, validate: \.isAvailable)
should be equal to.disable(if: Sensory.camera.isAvailable) || .disable(if: Sensory.gps.isAvailable) || ...
(or any other way / symbol for strategic grouping/composing/chaining/merging (for this specific issue grouping)).7 remaining items
grynspan commentedon Apr 5, 2025
As far as I know, we have no plans to make those symbols public, and
.pending()
is not a proposed API (I'm honestly not sure what it would do?)hrayatnia commentedon Apr 5, 2025
Sorry for not being accurate in my previous comment. I didn’t mean that people would introduce new APIs to swift-testing. What I meant was that if swift-testing were to make
ConditionTraits.Kind
public (which, as it stands, doesn’t seem to be the plan), people outside the library might get creative and try to introduce new methods. The.pending
method was purely hypothetical—just an example to illustrate the possibility of using other unary operators for their conditions.However, since it seems it's not the case then no need to even entertain the hypotheticals.
bkhouri commentedon Apr 7, 2025
My original request, which may not have been clear, is to make usage of custom traits more explicit. That I, I want do define custom ConditionalTrais, and make their uses in the application of a trait explicit.
Say I have the following defined in a module imported by a test
I want to be able to make this test more explicit with respect to the conditionals, without loosing the information/metadata the current implementaiton provideS, and where the solution indicate which conditions were [not] met causing the the test to be enabled/disabled.
grynspan commentedon Apr 7, 2025
@bkhouri That's already supported and functional though.
bkhouri commentedon Apr 8, 2025
But it's not explicit. I would like to have an equivalent of, where it's explicit, and where I can select
.all
, or.any
, or whichever the API will allow.e.g:
hrayatnia commentedon Apr 9, 2025
Would it be okay if I prototype all proposed solutions and prepare a range of test cases with condition traits—from simple to complex—for us to use as a basis for discussion?
Regarding the DSL-style prefix approach (whether using extensions, enums, result builders, ...), I have some concerns about readability. In more complex scenarios, it tends to shift the developer's focus away from what really matters—the test logic itself—and toward the condition traits. (Granted SwiftUI is like that, but views are hierarchical, and focus should be on views.)
Additionally, this approach tends to evolve into a deeply nested structure of conditions (akin to S-expressions or Lisp-style trees), introducing a learning curve for each new method or pattern added.
Also, using names like
any
andall
without clear indication of their scope (whether they apply to traits or conditions) leads to ambiguity. This could be improved with more explicit naming such asany(of:)
,anyOf
,all(of:)
,merge(all:)
, orcompose(all:)
—names that make it clear they operate on conditions.Personally, I lean toward an infix-style solution using custom operators. It feels more idiomatic to Swift and helps guard against misuse by making intent and structure clearer at a glance.
@bkhouri I would like to know your opinion on
I truly appreciate your time.
Appendix:
If it was in LISP
grynspan commentedon Apr 9, 2025
It is a non-goal of the testing library to add multiple ways to accomplish the exact same task. Both AND and OR can already be expressed, trivially, with combinations of
.enabled(if:)
and.disabled(if:)
.What we are missing right now is a way to express AND/OR operations on
ConditionTrait
instances in a way that can be a) shuffled off into a helper function and b) reports which specific subcondition caused a test to be skipped.Any changes here should be designed with that specific goal in mind. I don't think the Testing Workgroup would approve a proposal adding
.any
for the sake of adding it, without it providing any additional functionality.stmontgomery commentedon Apr 9, 2025
There are a lot of ideas being explored here, several which would result in a pretty sophisticated but also potentially complicated API. I'd like to come back to an earlier idea suggested above, about overloading the standard
&&
and||
operators. Was there a reason that wasn't feasible, or wouldn't solve the need? The comment indicated that for it to be possible, the underlying implementation and representation (e.g. inConditionTrait.Kind
) would need to be modified, but that part seems doable.As a general guideline, we have tried to make Swift Testing's APIs easy to learn and understand by even a casual user, and to do that we have leaned on existing Swift APIs, syntax, and concepts wherever possible. I would say we have a strong preference towards using standard Swift language features instead of using custom DSLs. The trait system and built-in traits are probably the area where we adhere to that principle the least strongly, but still, whenever there is an opportunity to use existing things, I think we should try hard to go that route.
hrayatnia commentedon Apr 9, 2025
@stmontgomery @grynspan I agree with both of your comments completely, and I'm inclined to infix operator solution (
&&
,||
). However, since AND/OR is not doing boolean AND/OR but rather merging logics, I suggested a custom operator. but regardless of the symbol, if the standard operator is a go, I will start working on it in a waya) shuffled off into a helper function
b) reports which specific subcondition caused a test to be skipped.
P.S.: Regarding the
ConditionTrait.Kind
, I was only pointing out that if it gets public, people could use other unary operators, which could affect the underlying merging behavior (it is just a side effect; which is not the plan to make it public, so no need to be worried about).P.S. 2: here I only addressed what is the current system, why it's needed, and what possible solutions are.
bkhouri commentedon Apr 10, 2025
Personally, I just want the readability to be explicit on the application of the conditional traits. Using the
&&
and||
overload in this exampleHow would it look using the
&&
and||
operators?@hrayatnia : I'd be happy to see code example of different solutions on the application.
hrayatnia commentedon Apr 10, 2025
@bkhouri that would be like:
or
Right now, I'm working on the behind-the-scenes logic for subconditions and other parts. As soon as that one is finished, I will provide more examples; however, meanwhile, there are some examples of all suggested cases here (sorry, I have the habit of making code collapsible when text is too large; in example segments you find some examples).
Improves grouped condition trait preparation swiftlang#1034
WIP: - Handles single condition trait in group swiftlang#1034
hrayatnia commentedon Apr 21, 2025
Sorry for the long absence. I got time to work on this issue again over Easter.
First Solution
Second Solution
Third Solution
[Fourth]: Applying aggregator from the runner perspective.
modified files:
ConditionTrait
GroupedConditionTrait
(in solution 1,3)GroupedConditionTrait
(@bkhouri I guess it would be your point of interest since it includes some test cases)I came up with a few solutions which are neither optimized nor formatted yet. I like to continue with the first solution and clean it up (both in terms of memory usage and code style). However, before I continue, I would like to know your opinions.