Skip to content

Commit cd91e93

Browse files
authored
Allow move-only types as suites. (#619)
This PR enables using move-only types as suites. For example: ```swift @suite struct NumberOfBeesTests: ~Copyable { @test consuming func countBees() { var count = 0 for species in allSpecies { if species is Bee { count += species.populationCount } } #expect(count > 0) } } ``` Move-only types have a number of constraints in Swift, and those constraints aren't lifted in a test target, but generally speaking a move-only type should be able to do all the things any other type can do _as a test suite_. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent a46df3f commit cd91e93

File tree

5 files changed

+96
-32
lines changed

5 files changed

+96
-32
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public struct TypeInfo: Sendable {
1818
///
1919
/// - Parameters:
2020
/// - type: The concrete metatype.
21-
case type(_ type: Any.Type)
21+
case type(_ type: any ~Copyable.Type)
2222

2323
/// The type info represents a metatype, but a reference to that metatype is
2424
/// not available at runtime.
@@ -38,7 +38,7 @@ public struct TypeInfo: Sendable {
3838
///
3939
/// If this instance was created from a type name, or if it was previously
4040
/// encoded and decoded, the value of this property is `nil`.
41-
public var type: Any.Type? {
41+
public var type: (any ~Copyable.Type)? {
4242
if case let .type(type) = _kind {
4343
return type
4444
}
@@ -57,7 +57,7 @@ public struct TypeInfo: Sendable {
5757
///
5858
/// - Parameters:
5959
/// - type: The type which this instance should describe.
60-
init(describing type: Any.Type) {
60+
init(describing type: any ~Copyable.Type) {
6161
_kind = .type(type)
6262
}
6363

@@ -172,7 +172,9 @@ extension TypeInfo {
172172
}
173173
switch _kind {
174174
case let .type(type):
175-
return _mangledTypeName(type)
175+
// _mangledTypeName() works with move-only types, but its signature has
176+
// not been updated yet. SEE: rdar://134278607
177+
return _mangledTypeName(unsafeBitCast(type, to: Any.Type.self))
176178
case let .nameOnly(_, _, mangledName):
177179
return mangledName
178180
}
@@ -299,7 +301,9 @@ extension TypeInfo: Hashable {
299301
public static func ==(lhs: Self, rhs: Self) -> Bool {
300302
switch (lhs._kind, rhs._kind) {
301303
case let (.type(lhs), .type(rhs)):
302-
return lhs == rhs
304+
// == and ObjectIdentifier do not support move-only metatypes, so compare
305+
// the bits of the types directly. SEE: rdar://134276458
306+
return unsafeBitCast(lhs, to: UnsafeRawPointer.self) == unsafeBitCast(rhs, to: UnsafeRawPointer.self)
303307
default:
304308
return lhs.fullyQualifiedNameComponents == rhs.fullyQualifiedNameComponents
305309
}

Sources/Testing/Test+Macro.swift

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ extension Test {
106106
/// - Warning: This function is used to implement the `@Suite` macro. Do not
107107
/// call it directly.
108108
public static func __type(
109-
_ containingType: Any.Type,
109+
_ containingType: any ~Copyable.Type,
110110
displayName: String? = nil,
111111
traits: [any SuiteTrait],
112112
sourceLocation: SourceLocation
@@ -159,15 +159,21 @@ extension Test {
159159
/// call it directly.
160160
public static func __function(
161161
named testFunctionName: String,
162-
in containingType: Any.Type?,
162+
in containingType: (any ~Copyable.Type)?,
163163
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
164164
displayName: String? = nil,
165165
traits: [any TestTrait],
166166
sourceLocation: SourceLocation,
167167
parameters: [__Parameter] = [],
168168
testFunction: @escaping @Sendable () async throws -> Void
169169
) -> Self {
170-
let containingTypeInfo = containingType.map(TypeInfo.init(describing:))
170+
// Don't use Optional.map here due to a miscompile/crash. Expand out to an
171+
// if expression instead. SEE: rdar://134280902
172+
let containingTypeInfo: TypeInfo? = if let containingType {
173+
TypeInfo(describing: containingType)
174+
} else {
175+
nil
176+
}
171177
let caseGenerator = { @Sendable in Case.Generator(testFunction: testFunction) }
172178
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: [])
173179
}
@@ -235,7 +241,7 @@ extension Test {
235241
/// call it directly.
236242
public static func __function<C>(
237243
named testFunctionName: String,
238-
in containingType: Any.Type?,
244+
in containingType: (any ~Copyable.Type)?,
239245
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
240246
displayName: String? = nil,
241247
traits: [any TestTrait],
@@ -244,7 +250,11 @@ extension Test {
244250
parameters paramTuples: [__Parameter],
245251
testFunction: @escaping @Sendable (C.Element) async throws -> Void
246252
) -> Self where C: Collection & Sendable, C.Element: Sendable {
247-
let containingTypeInfo = containingType.map(TypeInfo.init(describing:))
253+
let containingTypeInfo: TypeInfo? = if let containingType {
254+
TypeInfo(describing: containingType)
255+
} else {
256+
nil
257+
}
248258
let parameters = paramTuples.parameters
249259
let caseGenerator = { @Sendable in Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction) }
250260
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
@@ -366,7 +376,7 @@ extension Test {
366376
/// call it directly.
367377
public static func __function<C1, C2>(
368378
named testFunctionName: String,
369-
in containingType: Any.Type?,
379+
in containingType: (any ~Copyable.Type)?,
370380
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
371381
displayName: String? = nil,
372382
traits: [any TestTrait],
@@ -375,7 +385,11 @@ extension Test {
375385
parameters paramTuples: [__Parameter],
376386
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
377387
) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
378-
let containingTypeInfo = containingType.map(TypeInfo.init(describing:))
388+
let containingTypeInfo: TypeInfo? = if let containingType {
389+
TypeInfo(describing: containingType)
390+
} else {
391+
nil
392+
}
379393
let parameters = paramTuples.parameters
380394
let caseGenerator = { @Sendable in try await Case.Generator(arguments: collection1(), collection2(), parameters: parameters, testFunction: testFunction) }
381395
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
@@ -390,7 +404,7 @@ extension Test {
390404
/// call it directly.
391405
public static func __function<C, E1, E2>(
392406
named testFunctionName: String,
393-
in containingType: Any.Type?,
407+
in containingType: (any ~Copyable.Type)?,
394408
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
395409
displayName: String? = nil,
396410
traits: [any TestTrait],
@@ -399,7 +413,11 @@ extension Test {
399413
parameters paramTuples: [__Parameter],
400414
testFunction: @escaping @Sendable ((E1, E2)) async throws -> Void
401415
) -> Self where C: Collection & Sendable, C.Element == (E1, E2), E1: Sendable, E2: Sendable {
402-
let containingTypeInfo = containingType.map(TypeInfo.init(describing:))
416+
let containingTypeInfo: TypeInfo? = if let containingType {
417+
TypeInfo(describing: containingType)
418+
} else {
419+
nil
420+
}
403421
let parameters = paramTuples.parameters
404422
let caseGenerator = { @Sendable in Case.Generator(arguments: try await collection(), parameters: parameters, testFunction: testFunction) }
405423
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
@@ -417,7 +435,7 @@ extension Test {
417435
/// call it directly.
418436
public static func __function<Key, Value>(
419437
named testFunctionName: String,
420-
in containingType: Any.Type?,
438+
in containingType: (any ~Copyable.Type)?,
421439
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
422440
displayName: String? = nil,
423441
traits: [any TestTrait],
@@ -426,7 +444,11 @@ extension Test {
426444
parameters paramTuples: [__Parameter],
427445
testFunction: @escaping @Sendable ((Key, Value)) async throws -> Void
428446
) -> Self where Key: Sendable, Value: Sendable {
429-
let containingTypeInfo = containingType.map(TypeInfo.init(describing:))
447+
let containingTypeInfo: TypeInfo? = if let containingType {
448+
TypeInfo(describing: containingType)
449+
} else {
450+
nil
451+
}
430452
let parameters = paramTuples.parameters
431453
let caseGenerator = { @Sendable in Case.Generator(arguments: try await dictionary(), parameters: parameters, testFunction: testFunction) }
432454
return Self(name: testFunctionName, displayName: displayName, traits: traits, sourceLocation: sourceLocation, containingTypeInfo: containingTypeInfo, xcTestCompatibleSelector: xcTestCompatibleSelector, testCases: caseGenerator, parameters: parameters)
@@ -438,7 +460,7 @@ extension Test {
438460
/// call it directly.
439461
public static func __function<C1, C2>(
440462
named testFunctionName: String,
441-
in containingType: Any.Type?,
463+
in containingType: (any ~Copyable.Type)?,
442464
xcTestCompatibleSelector: __XCTestCompatibleSelector?,
443465
displayName: String? = nil,
444466
traits: [any TestTrait],
@@ -447,7 +469,11 @@ extension Test {
447469
parameters paramTuples: [__Parameter],
448470
testFunction: @escaping @Sendable (C1.Element, C2.Element) async throws -> Void
449471
) -> Self where C1: Collection & Sendable, C1.Element: Sendable, C2: Collection & Sendable, C2.Element: Sendable {
450-
let containingTypeInfo = containingType.map(TypeInfo.init(describing:))
472+
let containingTypeInfo: TypeInfo? = if let containingType {
473+
TypeInfo(describing: containingType)
474+
} else {
475+
nil
476+
}
451477
let parameters = paramTuples.parameters
452478
let caseGenerator = { @Sendable in
453479
Case.Generator(arguments: try await zippedCollections(), parameters: parameters) {
@@ -460,22 +486,22 @@ extension Test {
460486

461487
// MARK: - Helper functions
462488

463-
/// A value that abstracts away whether or not the `try` keyword is needed on an
464-
/// expression.
489+
/// A function that abstracts away whether or not the `try` keyword is needed on
490+
/// an expression.
465491
///
466-
/// - Warning: This value is used to implement the `@Test` macro. Do not use
492+
/// - Warning: This function is used to implement the `@Test` macro. Do not use
467493
/// it directly.
468-
@inlinable public var __requiringTry: Void {
469-
@inlinable get throws {}
494+
@inlinable public func __requiringTry<T>(_ value: consuming T) throws -> T where T: ~Copyable {
495+
value
470496
}
471497

472-
/// A value that abstracts away whether or not the `await` keyword is needed on
473-
/// an expression.
498+
/// A function that abstracts away whether or not the `await` keyword is needed
499+
/// on an expression.
474500
///
475-
/// - Warning: This value is used to implement the `@Test` macro. Do not use
501+
/// - Warning: This function is used to implement the `@Test` macro. Do not use
476502
/// it directly.
477-
@inlinable public var __requiringAwait: Void {
478-
@inlinable get async {}
503+
@inlinable public func __requiringAwait<T>(_ value: consuming T, isolation: isolated (any Actor)? = #isolation) async -> T where T: ~Copyable {
504+
value
479505
}
480506

481507
#if !SWT_NO_GLOBAL_ACTORS

Sources/TestingMacros/TestDeclarationMacro.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -278,17 +278,17 @@ public struct TestDeclarationMacro: PeerMacro, Sendable {
278278
// detecting isolation to other global actors.
279279
lazy var isMainActorIsolated = !functionDecl.attributes(named: "MainActor", inModuleNamed: "Swift").isEmpty
280280
var forwardCall: (ExprSyntax) -> ExprSyntax = {
281-
"try await (\($0), Testing.__requiringTry, Testing.__requiringAwait).0"
281+
"try await Testing.__requiringTry(Testing.__requiringAwait(\($0)))"
282282
}
283283
let forwardInit = forwardCall
284284
if functionDecl.noasyncAttribute != nil {
285285
if isMainActorIsolated {
286286
forwardCall = {
287-
"try await MainActor.run { try (\($0), Testing.__requiringTry).0 }"
287+
"try await MainActor.run { try Testing.__requiringTry(\($0)) }"
288288
}
289289
} else {
290290
forwardCall = {
291-
"try { try (\($0), Testing.__requiringTry).0 }()"
291+
"try { try Testing.__requiringTry(\($0)) }()"
292292
}
293293
}
294294
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// This source file is part of the Swift.org open source project
3+
//
4+
// Copyright (c) 2023 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 @_spi(ForToolsIntegrationOnly) import Testing
12+
13+
@Suite("Non-Copyable Tests")
14+
struct NonCopyableTests: ~Copyable {
15+
@Test static func staticMe() {}
16+
@Test borrowing func borrowMe() {}
17+
@Test consuming func consumeMe() {}
18+
@Test mutating func mutateMe() {}
19+
20+
@Test borrowing func typeComparison() {
21+
let lhs = TypeInfo(describing: Self.self)
22+
let rhs = TypeInfo(describing: Self.self)
23+
24+
#expect(lhs == rhs)
25+
#expect(lhs.hashValue == rhs.hashValue)
26+
}
27+
28+
@available(_mangledTypeNameAPI, *)
29+
@Test borrowing func mangledTypeName() {
30+
#expect(TypeInfo(describing: Self.self).mangledName != nil)
31+
}
32+
}

Tests/TestingTests/ObjCInteropTests.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ struct ObjCAndXCTestInteropTests {
7878
#expect(steps.count > 0)
7979
for step in steps {
8080
let selector = try #require(step.test.xcTestCompatibleSelector)
81-
let testCaseClass = try #require(step.test.containingTypeInfo?.type as? NSObject.Type)
81+
// A compiler crash occurs here without the bitcast. SEE: rdar://134277439
82+
let type = unsafeBitCast(step.test.containingTypeInfo?.type, to: Any.Type?.self)
83+
let testCaseClass = try #require(type as? NSObject.Type)
8284
#expect(testCaseClass.instancesRespond(to: selector))
8385
}
8486
}

0 commit comments

Comments
 (0)