Skip to content

Introduce 'MarkerTrait' to unify the representation of boolean test attributes #1123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Testing/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ add_library(Testing
Traits/Comment+Macro.swift
Traits/ConditionTrait.swift
Traits/ConditionTrait+Macro.swift
Traits/HiddenTrait.swift
Traits/IssueHandlingTrait.swift
Traits/MarkerTrait.swift
Traits/ParallelizationTrait.swift
Traits/Tags/Tag.Color.swift
Traits/Tags/Tag.Color+Loading.swift
Expand Down
9 changes: 8 additions & 1 deletion Sources/Testing/Running/Runner.Plan.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,20 @@ extension Runner.Plan {
return
}

var traits = [any Trait]()
#if DEBUG
// For debugging purposes, keep track of the fact that this suite was
// synthesized.
traits.append(.synthesized)
#endif

let typeInfo = TypeInfo(fullyQualifiedNameComponents: nameComponents, unqualifiedName: unqualifiedName)

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

Expand Down
13 changes: 1 addition & 12 deletions Sources/Testing/Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -192,29 +192,18 @@ public struct Test: Sendable {
containingTypeInfo != nil && testCasesState == nil
}

/// Whether or not this instance was synthesized at runtime.
///
/// During test planning, suites that are not explicitly marked with the
/// `@Suite` attribute are synthesized from available type information before
/// being added to the plan. For such suites, the value of this property is
/// `true`.
@_spi(ForToolsIntegrationOnly)
public var isSynthesized: Bool = false

/// Initialize an instance of this type representing a test suite type.
init(
displayName: String? = nil,
traits: [any Trait],
sourceLocation: SourceLocation,
containingTypeInfo: TypeInfo,
isSynthesized: Bool = false
containingTypeInfo: TypeInfo
) {
self.name = containingTypeInfo.unqualifiedName
self.displayName = displayName
self.traits = traits
self.sourceLocation = sourceLocation
self.containingTypeInfo = containingTypeInfo
self.isSynthesized = isSynthesized
}

/// Initialize an instance of this type representing a test function.
Expand Down
38 changes: 0 additions & 38 deletions Sources/Testing/Traits/HiddenTrait.swift

This file was deleted.

121 changes: 121 additions & 0 deletions Sources/Testing/Traits/MarkerTrait.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//
// 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 which indicates a boolean value when used as a test trait.
///
/// Any attribute of a test which can be represented as a boolean value may use
/// an instance of this type to indicate having that attribute by adding it to
/// that test's traits.
///
/// Instances of this type are considered equal if they have an identical
/// private reference to a value of reference type, so each unique marker must
/// be a shared instance.
///
/// This type is not part of the public interface of the testing library.
struct MarkerTrait: TestTrait, SuiteTrait {
/// A stored value of a reference type used solely for equality checking, so
/// that two marker instances may be considered equal only if they have
/// identical values for this property.
///
/// @Comment {
/// - Bug: We cannot use a custom class for this purpose because in some
/// scenarios, more than one instance of the testing library may be loaded
/// in to a test runner process and on certain platforms this can cause
/// runtime warnings. ([148912491](rdar://148912491))
/// }
nonisolated(unsafe) private let _identity: AnyObject = ManagedBuffer<Void, Void>.create(minimumCapacity: 0) { _ in () }

let isRecursive: Bool
}

extension MarkerTrait: Equatable {
static func == (lhs: Self, rhs: Self) -> Bool {
lhs._identity === rhs._identity
}
}

#if DEBUG
// MARK: - Hidden tests

/// Storage for the ``Trait/hidden`` property.
private let _hiddenMarker = MarkerTrait(isRecursive: true)

extension Trait where Self == MarkerTrait {
/// A trait that indicates that a test should be hidden from automatic
/// discovery and only run if explicitly requested.
///
/// This is different from disabled or skipped, and is primarily meant to be
/// used on tests defined in this project's own test suite, so that example
/// tests can be defined using the `@Test` attribute but not run by default
/// except by the specific unit test(s) which have requested to run them.
///
/// When this trait is applied to a suite, it is recursively inherited by all
/// child suites and tests.
static var hidden: Self {
_hiddenMarker
}
}

extension Test {
/// Whether this test is hidden, whether directly or via a trait inherited
/// from a parent test.
///
/// ## See Also
///
/// - ``Trait/hidden``
var isHidden: Bool {
containsTrait(.hidden)
}
}

// MARK: - Synthesized tests

/// Storage for the ``Trait/synthesized`` property.
private let _synthesizedMarker = MarkerTrait(isRecursive: false)

extension Trait where Self == MarkerTrait {
/// A trait that indicates a test was synthesized at runtime.
///
/// During test planning, suites that are not explicitly marked with the
/// `@Suite` attribute are synthesized from available type information before
/// being added to the plan. This trait can be applied to such suites to keep
/// track of them.
///
/// When this trait is applied to a suite, it is _not_ recursively inherited
/// by all child suites or tests.
static var synthesized: Self {
_synthesizedMarker
}
}
#endif

extension Test {
/// Whether or not this instance was synthesized at runtime.
///
/// During test planning, suites that are not explicitly marked with the
/// `@Suite` attribute are synthesized from available type information before
/// being added to the plan. For such suites, the value of this property is
/// `true`.
///
/// In release builds, this information is not tracked and the value of this
/// property is always `false`.
///
/// ## See Also
///
/// - ``Trait/synthesized``
@_spi(ForToolsIntegrationOnly)
public var isSynthesized: Bool {
#if DEBUG
containsTrait(.synthesized)
#else
false
#endif
}
}
12 changes: 12 additions & 0 deletions Sources/Testing/Traits/Trait.swift
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,15 @@ extension SuiteTrait {
false
}
}

extension Test {
/// Whether or not this test contains the specified trait.
///
/// - Parameters:
/// - trait: The trait to search for. Must conform to `Equatable`.
///
/// - Returns: Whether or not this test contains `trait`.
func containsTrait<T>(_ trait: T) -> Bool where T: Trait & Equatable {
traits.contains { ($0 as? T) == trait }
}
}
20 changes: 0 additions & 20 deletions Tests/TestingTests/Traits/HiddenTraitTests.swift

This file was deleted.

48 changes: 48 additions & 0 deletions Tests/TestingTests/Traits/MarkerTraitTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// 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(ForToolsIntegrationOnly) import Testing

@Suite("Marker Trait Tests", .tags(.traitRelated))
struct MarkerTraitTests {
@Test("Equatable implementation")
func equality() {
let markerA = MarkerTrait(isRecursive: true)
let markerB = MarkerTrait(isRecursive: true)
let markerC = markerB
#expect(markerA == markerA)
#expect(markerA != markerB)
#expect(markerB == markerC)
}

@Test(".hidden trait")
func hiddenTrait() throws {
do {
let test = Test(/* no traits */) {}
#expect(!test.isHidden)
}
do {
let test = Test(.hidden) {}
#expect(test.isHidden)
}
}

@Test(".synthesized trait")
func synthesizedTrait() throws {
do {
let test = Test(/* no traits */) {}
#expect(!test.isSynthesized)
}
do {
let test = Test(.synthesized) {}
#expect(test.isSynthesized)
}
}
}