Skip to content

Commit f4b188f

Browse files
committed
Introduce 'MarkerTrait' to unify the representation of boolean test attributes
1 parent 40edfec commit f4b188f

File tree

8 files changed

+191
-72
lines changed

8 files changed

+191
-72
lines changed

Sources/Testing/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ add_library(Testing
9696
Traits/Comment+Macro.swift
9797
Traits/ConditionTrait.swift
9898
Traits/ConditionTrait+Macro.swift
99-
Traits/HiddenTrait.swift
10099
Traits/IssueHandlingTrait.swift
100+
Traits/MarkerTrait.swift
101101
Traits/ParallelizationTrait.swift
102102
Traits/Tags/Tag.Color.swift
103103
Traits/Tags/Tag.Color+Loading.swift

Sources/Testing/Running/Runner.Plan.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,20 @@ extension Runner.Plan {
179179
return
180180
}
181181

182+
var traits = [any Trait]()
183+
#if DEBUG
184+
// For debugging purposes, keep track of the fact that this suite was
185+
// synthesized.
186+
traits.append(.synthesized)
187+
#endif
188+
182189
let typeInfo = TypeInfo(fullyQualifiedNameComponents: nameComponents, unqualifiedName: unqualifiedName)
183190

184191
// Note: When a suite is synthesized, it does not have an accurate
185192
// source location, so we use the source location of a close descendant
186193
// test. We do this instead of falling back to some "unknown"
187194
// placeholder in an attempt to preserve the correct sort ordering.
188-
graph.value = Test(traits: [], sourceLocation: sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true)
195+
graph.value = Test(traits: traits, sourceLocation: sourceLocation, containingTypeInfo: typeInfo)
189196
}
190197
}
191198

Sources/Testing/Test.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -192,29 +192,18 @@ public struct Test: Sendable {
192192
containingTypeInfo != nil && testCasesState == nil
193193
}
194194

195-
/// Whether or not this instance was synthesized at runtime.
196-
///
197-
/// During test planning, suites that are not explicitly marked with the
198-
/// `@Suite` attribute are synthesized from available type information before
199-
/// being added to the plan. For such suites, the value of this property is
200-
/// `true`.
201-
@_spi(ForToolsIntegrationOnly)
202-
public var isSynthesized: Bool = false
203-
204195
/// Initialize an instance of this type representing a test suite type.
205196
init(
206197
displayName: String? = nil,
207198
traits: [any Trait],
208199
sourceLocation: SourceLocation,
209-
containingTypeInfo: TypeInfo,
210-
isSynthesized: Bool = false
200+
containingTypeInfo: TypeInfo
211201
) {
212202
self.name = containingTypeInfo.unqualifiedName
213203
self.displayName = displayName
214204
self.traits = traits
215205
self.sourceLocation = sourceLocation
216206
self.containingTypeInfo = containingTypeInfo
217-
self.isSynthesized = isSynthesized
218207
}
219208

220209
/// Initialize an instance of this type representing a test function.

Sources/Testing/Traits/HiddenTrait.swift

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
/// A type which indicates a boolean value when used as a test trait.
12+
///
13+
/// Any attribute of a test which can be represented as a boolean value may use
14+
/// an instance of this type to indicate having that attribute by adding it to
15+
/// that test's traits.
16+
///
17+
/// Instances of this type are considered equal if they have an identical
18+
/// private reference to a value of reference type, so each unique marker must
19+
/// be a shared instance.
20+
///
21+
/// This type is not part of the public interface of the testing library.
22+
struct MarkerTrait: TestTrait, SuiteTrait {
23+
/// A stored value of a reference type used solely for equality checking, so
24+
/// that two marker instances may be considered equal only if they have
25+
/// identical values for this property.
26+
///
27+
/// @Comment {
28+
/// - Bug: We cannot use a custom class for this purpose because in some
29+
/// scenarios, more than one instance of the testing library may be loaded
30+
/// in to a test runner process and on certain platforms this can cause
31+
/// runtime warnings. ([148912491](rdar://148912491))
32+
/// }
33+
nonisolated(unsafe) private let _identity: AnyObject = ManagedBuffer<Void, Void>.create(minimumCapacity: 0) { _ in () }
34+
35+
let isRecursive: Bool
36+
}
37+
38+
extension MarkerTrait: Equatable {
39+
static func == (lhs: Self, rhs: Self) -> Bool {
40+
lhs._identity === rhs._identity
41+
}
42+
}
43+
44+
#if DEBUG
45+
// MARK: - Hidden tests
46+
47+
/// Storage for the ``Trait/hidden`` property.
48+
private let _hiddenMarker = MarkerTrait(isRecursive: true)
49+
50+
extension Trait where Self == MarkerTrait {
51+
/// A trait that indicates that a test should be hidden from automatic
52+
/// discovery and only run if explicitly requested.
53+
///
54+
/// This is different from disabled or skipped, and is primarily meant to be
55+
/// used on tests defined in this project's own test suite, so that example
56+
/// tests can be defined using the `@Test` attribute but not run by default
57+
/// except by the specific unit test(s) which have requested to run them.
58+
///
59+
/// When this trait is applied to a suite, it is recursively inherited by all
60+
/// child suites and tests.
61+
static var hidden: Self {
62+
_hiddenMarker
63+
}
64+
}
65+
66+
extension Test {
67+
/// Whether this test is hidden, whether directly or via a trait inherited
68+
/// from a parent test.
69+
///
70+
/// ## See Also
71+
///
72+
/// - ``Trait/hidden``
73+
var isHidden: Bool {
74+
containsTrait(.hidden)
75+
}
76+
}
77+
78+
// MARK: - Synthesized tests
79+
80+
/// Storage for the ``Trait/synthesized`` property.
81+
private let _synthesizedMarker = MarkerTrait(isRecursive: false)
82+
83+
extension Trait where Self == MarkerTrait {
84+
/// A trait that indicates a test was synthesized at runtime.
85+
///
86+
/// During test planning, suites that are not explicitly marked with the
87+
/// `@Suite` attribute are synthesized from available type information before
88+
/// being added to the plan. This trait can be applied to such suites to keep
89+
/// track of them.
90+
///
91+
/// When this trait is applied to a suite, it is _not_ recursively inherited
92+
/// by all child suites or tests.
93+
static var synthesized: Self {
94+
_synthesizedMarker
95+
}
96+
}
97+
#endif
98+
99+
extension Test {
100+
/// Whether or not this instance was synthesized at runtime.
101+
///
102+
/// During test planning, suites that are not explicitly marked with the
103+
/// `@Suite` attribute are synthesized from available type information before
104+
/// being added to the plan. For such suites, the value of this property is
105+
/// `true`.
106+
///
107+
/// In release builds, this information is not tracked and the value of this
108+
/// property is always `false`.
109+
///
110+
/// ## See Also
111+
///
112+
/// - ``Trait/synthesized``
113+
@_spi(ForToolsIntegrationOnly)
114+
public var isSynthesized: Bool {
115+
#if DEBUG
116+
containsTrait(.synthesized)
117+
#else
118+
false
119+
#endif
120+
}
121+
}

Sources/Testing/Traits/Trait.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,15 @@ extension SuiteTrait {
285285
false
286286
}
287287
}
288+
289+
extension Test {
290+
/// Whether or not this test contains the specified trait.
291+
///
292+
/// - Parameters:
293+
/// - trait: The trait to search for. Must conform to `Equatable`.
294+
///
295+
/// - Returns: Whether or not this test contains `trait`.
296+
func containsTrait<T>(_ trait: T) -> Bool where T: Trait & Equatable {
297+
traits.contains { ($0 as? T) == trait }
298+
}
299+
}

Tests/TestingTests/Traits/HiddenTraitTests.swift

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
5+
// Licensed under Apache License v2.0 with Runtime Library Exception
6+
//
7+
// See https://swift.org/LICENSE.txt for license information
8+
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
//
10+
11+
@testable import Testing
12+
13+
@Suite("Marker Trait Tests", .tags(.traitRelated))
14+
struct MarkerTraitTests {
15+
@Test("Equatable implementation")
16+
func equality() {
17+
let markerA = MarkerTrait(isRecursive: true)
18+
let markerB = MarkerTrait(isRecursive: true)
19+
let markerC = markerB
20+
#expect(markerA == markerA)
21+
#expect(markerA != markerB)
22+
#expect(markerB == markerC)
23+
}
24+
25+
@Test(".hidden trait")
26+
func hiddenTrait() throws {
27+
do {
28+
let test = Test(/* no traits */) {}
29+
#expect(!test.isHidden)
30+
}
31+
do {
32+
let test = Test(.hidden) {}
33+
#expect(test.isHidden)
34+
}
35+
}
36+
37+
@Test(".synthesized trait")
38+
func synthesizedTrait() throws {
39+
do {
40+
let test = Test(/* no traits */) {}
41+
#expect(!test.isSynthesized)
42+
}
43+
do {
44+
let test = Test(.synthesized) {}
45+
#expect(test.isSynthesized)
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)