Skip to content

Commit 0e10568

Browse files
Expose Testing test identifier from test context (#125)
* Introduce test context data * wip * fix * wip * fix * wip * wip * wip * wip * configs * wip * fix --------- Co-authored-by: Brandon Williams <mbrandonw@hey.com>
1 parent 75f739f commit 0e10568

File tree

9 files changed

+140
-30
lines changed

9 files changed

+140
-30
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
- name: Select Xcode
5555
run: sudo xcode-select -s /Applications/Xcode_15.4.app
5656
- name: Run tests
57-
run: CONFIG=${{ matrix.config }} make test-examples
57+
run: make CONFIG=${{ matrix.config }} test-examples
5858

5959
linux:
6060
strategy:
@@ -72,7 +72,7 @@ jobs:
7272
- name: Run tests
7373
run: make test-${{ matrix.config }}
7474
- name: Build for static-stdlib
75-
run: CONFIG=${{ matrix.config }} make build-for-static-stdlib
75+
run: make CONFIG=${{ matrix.config }} build-for-static-stdlib
7676

7777
wasm:
7878
name: SwiftWasm

Examples/ExamplesTests/SwiftTestingTests.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
@Suite
77
struct SwiftTestingTests_Debug {
88
@Test func context() {
9-
#expect(TestContext.current == .swiftTesting)
9+
switch TestContext.current {
10+
case .xcTest:
11+
#expect(Bool(true))
12+
default:
13+
Issue.record()
14+
}
1015
}
1116

1217
@Test func reportIssue_NoMessage() {
@@ -68,7 +73,12 @@
6873
@Suite
6974
struct SwiftTestingTests_Release {
7075
@Test func context() {
71-
#expect(TestContext.current == .xcTest)
76+
switch TestContext.current {
77+
case .xcTest:
78+
#expect(Bool(true))
79+
default:
80+
Issue.record()
81+
}
7282
}
7383

7484
@Test func reportIssueDoesNotFail() {

Examples/ExamplesTests/XCTestTests.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import XCTest
44
#if DEBUG
55
class XCTestTests_Debug: XCTestCase {
66
func testContext() {
7-
XCTAssertEqual(TestContext.current, .xcTest)
7+
switch TestContext.current {
8+
case .xcTest:
9+
XCTAssert(true)
10+
default:
11+
XCTFail()
12+
}
813
}
914

1015
#if _runtime(_ObjC)
@@ -49,7 +54,12 @@ import XCTest
4954
#else
5055
class XCTestTests_Release: XCTestCase {
5156
func testContext() {
52-
XCTAssertEqual(TestContext.current, .xcTest)
57+
switch TestContext.current {
58+
case .xcTest:
59+
XCTAssert(true)
60+
default:
61+
XCTFail()
62+
}
5363
}
5464

5565
func testReportIssueDoesNotFail() {

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
XCODE_PATH := $(shell xcode-select -p)
2+
CONFIG := debug
23

34
# NB: We can't rely on `XCTExpectFailure` because it doesn't exist in `swift-corelibs-foundation`
45
PASS = \033[1;7;32m PASS \033[0m
@@ -50,4 +51,4 @@ test-linux:
5051
-v "$(PWD):$(PWD)" \
5152
-w "$(PWD)" \
5253
swift:5.10 \
53-
bash -c 'swift test'
54+
bash -c 'swift test -c $(CONFIG)'

Sources/IssueReporting/Internal/SwiftTesting.swift

Lines changed: 74 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -234,24 +234,17 @@ func _withKnownIssue(
234234
await withKnownIssue(message, isIntermittent, fileID, filePath, line, column, body)
235235
}
236236
@usableFromInline
237-
func _currentTestIsNotNil() -> Bool {
238-
guard let function = function(for: "$s25IssueReportingTestSupport08_currentC8IsNotNilypyF")
237+
func _currentTestID() -> AnyHashable? {
238+
guard let function = function(for: "$s25IssueReportingTestSupport08_currentC2IDypyF")
239239
else {
240240
#if DEBUG
241-
return Test.current != nil
241+
return Test.current?.id
242242
#else
243-
printError(
244-
"""
245-
'Test.current' was accessed without linking the Testing framework.
246-
247-
To fix this, add "IssueReportingTestSupport" as a dependency to your test target.
248-
"""
249-
)
250-
return false
243+
return nil
251244
#endif
252245
}
253246

254-
return (function as! @Sendable () -> Bool)()
247+
return (function as! @Sendable () -> AnyHashable?)()
255248
}
256249

257250
#if DEBUG
@@ -348,11 +341,15 @@ func _currentTestIsNotNil() -> Bool {
348341
var sourceLocation: SourceLocation?
349342
}
350343

351-
private struct SourceLocation: Sendable {
344+
private struct SourceLocation: Hashable, Sendable {
352345
var fileID: String
353346
var _filePath: String
354347
var line: Int
355348
var column: Int
349+
var moduleName: String {
350+
let firstSlash = fileID.firstIndex(of: "/")!
351+
return String(fileID[..<firstSlash])
352+
}
356353
}
357354

358355
struct Test: @unchecked Sendable {
@@ -388,6 +385,42 @@ func _currentTestIsNotNil() -> Bool {
388385
var typeInfo: TypeInfo
389386
}
390387
private var isSynthesized = false
388+
389+
private var isSuite: Bool {
390+
containingTypeInfo != nil && testCasesState == nil
391+
}
392+
fileprivate var id: ID {
393+
var result = containingTypeInfo.map(ID.init)
394+
?? ID(moduleName: sourceLocation.moduleName, nameComponents: [], sourceLocation: nil)
395+
396+
if !isSuite {
397+
result.nameComponents.append(name)
398+
result.sourceLocation = sourceLocation
399+
}
400+
401+
return result
402+
}
403+
fileprivate struct ID: Hashable {
404+
var moduleName: String
405+
var nameComponents: [String]
406+
var sourceLocation: SourceLocation?
407+
init(moduleName: String, nameComponents: [String], sourceLocation: SourceLocation?) {
408+
self.moduleName = moduleName
409+
self.nameComponents = nameComponents
410+
self.sourceLocation = sourceLocation
411+
}
412+
init(_ fullyQualifiedNameComponents: some Collection<String>) {
413+
moduleName = fullyQualifiedNameComponents.first ?? ""
414+
if fullyQualifiedNameComponents.count > 0 {
415+
nameComponents = Array(fullyQualifiedNameComponents.dropFirst())
416+
} else {
417+
nameComponents = []
418+
}
419+
}
420+
init(typeInfo: TypeInfo) {
421+
self.init(typeInfo.fullyQualifiedNameComponents)
422+
}
423+
}
391424
}
392425

393426
private protocol Trait: Sendable {}
@@ -398,6 +431,34 @@ func _currentTestIsNotNil() -> Bool {
398431
case nameOnly(fullyQualifiedComponents: [String], unqualified: String, mangled: String?)
399432
}
400433
var _kind: _Kind
434+
435+
static let _fullyQualifiedNameComponentsCache: LockIsolated<
436+
[ObjectIdentifier: [String]]
437+
> = LockIsolated([:])
438+
var fullyQualifiedNameComponents: [String] {
439+
switch _kind {
440+
case let .type(type):
441+
if let cachedResult = Self
442+
._fullyQualifiedNameComponentsCache.withLock({ $0[ObjectIdentifier(type)] })
443+
{
444+
return cachedResult
445+
}
446+
var result = String(reflecting: type)
447+
.split(separator: ".")
448+
.map(String.init)
449+
if let firstComponent = result.first, firstComponent.starts(with: "(extension in ") {
450+
result[0] = String(firstComponent.split(separator: ":", maxSplits: 1).last!)
451+
}
452+
result = result.filter { !$0.starts(with: "(unknown context at") }
453+
Self._fullyQualifiedNameComponentsCache.withLock { [result] in
454+
$0[ObjectIdentifier(type)] = result
455+
}
456+
return result
457+
458+
case let .nameOnly(fullyQualifiedComponents, _, _):
459+
return fullyQualifiedComponents
460+
}
461+
}
401462
}
402463
#endif
403464

Sources/IssueReporting/TestContext.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
/// A type representing the context in which a test is being run, i.e. either in Swift's native
1+
/// A type representing the context in which a test is being run, _i.e._ either in Swift's native
22
/// Testing framework, or Xcode's XCTest framework.
33
public enum TestContext {
44
/// The Swift Testing framework.
5-
case swiftTesting
5+
case swiftTesting(Testing)
66

77
/// The XCTest framework.
88
case xcTest
@@ -21,10 +21,28 @@ public enum TestContext {
2121
/// If executed outside of a test process, this will return `nil`.
2222
public static var current: Self? {
2323
guard isTesting else { return nil }
24-
if _currentTestIsNotNil() {
25-
return .swiftTesting
24+
if let currentTestID = _currentTestID() {
25+
return .swiftTesting(Testing(id: currentTestID))
2626
} else {
2727
return .xcTest
2828
}
2929
}
30+
31+
public struct Testing {
32+
public let test: Test
33+
34+
public struct Test: Hashable, Identifiable, Sendable {
35+
public let id: ID
36+
37+
public struct ID: Hashable, @unchecked Sendable {
38+
fileprivate let rawValue: AnyHashable
39+
}
40+
}
41+
}
42+
}
43+
44+
extension TestContext.Testing {
45+
fileprivate init(id: AnyHashable) {
46+
self.init(test: Test(id: Test.ID(rawValue: id)))
47+
}
3048
}

Sources/IssueReportingTestSupport/SwiftTesting.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ private func __withKnownIssueAsync(
100100
#endif
101101
}
102102

103-
public func _currentTestIsNotNil() -> Any { __currentTestIsNotNil }
103+
public func _currentTestID() -> Any { __currentTestID }
104104
@Sendable
105-
private func __currentTestIsNotNil() -> Bool {
105+
private func __currentTestID() -> AnyHashable? {
106106
#if canImport(Testing)
107-
return Test.current != nil
107+
return Test.current?.id
108108
#else
109-
return false
109+
return nil
110110
#endif
111111
}

Tests/IssueReportingTests/SwiftTestingTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
@Suite
66
struct SwiftTestingTests {
77
@Test func context() {
8-
#expect(TestContext.current == .swiftTesting)
8+
switch TestContext.current {
9+
case .swiftTesting:
10+
#expect(Bool(true))
11+
default:
12+
Issue.record()
13+
}
914
}
1015

1116
@Test func reportIssue_NoMessage() {

Tests/IssueReportingTests/XCTestTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ final class XCTestTests: XCTestCase {
88
}
99

1010
func testTestContext() {
11-
XCTAssertEqual(TestContext.current, .xcTest)
11+
switch TestContext.current {
12+
case .xcTest:
13+
XCTAssert(true)
14+
default:
15+
XCTFail()
16+
}
1217
}
1318
#endif
1419

0 commit comments

Comments
 (0)