From b25e0ec16cfbe721143d1c4a3e085178ba7474e5 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Thu, 22 May 2025 14:17:09 -0500 Subject: [PATCH] Introduce 'MarkerTrait' to unify the representation of boolean test attributes --- Sources/Testing/CMakeLists.txt | 2 +- Sources/Testing/Running/Runner.Plan.swift | 9 +- Sources/Testing/Test.swift | 13 +- Sources/Testing/Traits/HiddenTrait.swift | 38 ------ Sources/Testing/Traits/MarkerTrait.swift | 121 ++++++++++++++++++ Sources/Testing/Traits/Trait.swift | 12 ++ .../Traits/HiddenTraitTests.swift | 20 --- .../Traits/MarkerTraitTests.swift | 48 +++++++ 8 files changed, 191 insertions(+), 72 deletions(-) delete mode 100644 Sources/Testing/Traits/HiddenTrait.swift create mode 100644 Sources/Testing/Traits/MarkerTrait.swift delete mode 100644 Tests/TestingTests/Traits/HiddenTraitTests.swift create mode 100644 Tests/TestingTests/Traits/MarkerTraitTests.swift diff --git a/Sources/Testing/CMakeLists.txt b/Sources/Testing/CMakeLists.txt index 5b84aeaf3..d710fd6c8 100644 --- a/Sources/Testing/CMakeLists.txt +++ b/Sources/Testing/CMakeLists.txt @@ -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 diff --git a/Sources/Testing/Running/Runner.Plan.swift b/Sources/Testing/Running/Runner.Plan.swift index c89fdecb5..a2624092c 100644 --- a/Sources/Testing/Running/Runner.Plan.swift +++ b/Sources/Testing/Running/Runner.Plan.swift @@ -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) } } diff --git a/Sources/Testing/Test.swift b/Sources/Testing/Test.swift index 738daf72d..edbdd919b 100644 --- a/Sources/Testing/Test.swift +++ b/Sources/Testing/Test.swift @@ -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. diff --git a/Sources/Testing/Traits/HiddenTrait.swift b/Sources/Testing/Traits/HiddenTrait.swift deleted file mode 100644 index 38e163330..000000000 --- a/Sources/Testing/Traits/HiddenTrait.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 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 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. -/// -/// This type is not part of the public interface of the testing library. -struct HiddenTrait: TestTrait, SuiteTrait { - var isRecursive: Bool { - true - } -} - -extension Trait where Self == HiddenTrait { - static var hidden: Self { - HiddenTrait() - } -} - -extension Test { - /// Whether this test is hidden, whether directly or via a trait inherited - /// from a parent test. - var isHidden: Bool { - traits.contains { $0 is HiddenTrait } - } -} diff --git a/Sources/Testing/Traits/MarkerTrait.swift b/Sources/Testing/Traits/MarkerTrait.swift new file mode 100644 index 000000000..e7de91d52 --- /dev/null +++ b/Sources/Testing/Traits/MarkerTrait.swift @@ -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.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 + } +} diff --git a/Sources/Testing/Traits/Trait.swift b/Sources/Testing/Traits/Trait.swift index 70aafe90e..7a85f4e5f 100644 --- a/Sources/Testing/Traits/Trait.swift +++ b/Sources/Testing/Traits/Trait.swift @@ -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(_ trait: T) -> Bool where T: Trait & Equatable { + traits.contains { ($0 as? T) == trait } + } +} diff --git a/Tests/TestingTests/Traits/HiddenTraitTests.swift b/Tests/TestingTests/Traits/HiddenTraitTests.swift deleted file mode 100644 index 0d2a6723f..000000000 --- a/Tests/TestingTests/Traits/HiddenTraitTests.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 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 import Testing - -@Suite("Hidden Trait Tests", .tags(.traitRelated)) -struct HiddenTraitTests { - @Test(".hidden trait") - func hiddenTrait() throws { - let test = Test(.hidden) {} - #expect(test.isHidden) - } -} diff --git a/Tests/TestingTests/Traits/MarkerTraitTests.swift b/Tests/TestingTests/Traits/MarkerTraitTests.swift new file mode 100644 index 000000000..79d2a4462 --- /dev/null +++ b/Tests/TestingTests/Traits/MarkerTraitTests.swift @@ -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) + } + } +}