Skip to content

Commit

Permalink
Expose Testing test identifier from test context (#125)
Browse files Browse the repository at this point in the history
* Introduce test context data

* wip

* fix

* wip

* fix

* wip

* wip

* wip

* wip

* configs

* wip

* fix

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>
  • Loading branch information
stephencelis and mbrandonw authored Sep 6, 2024
1 parent 75f739f commit 0e10568
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 30 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Run tests
run: CONFIG=${{ matrix.config }} make test-examples
run: make CONFIG=${{ matrix.config }} test-examples

linux:
strategy:
Expand All @@ -72,7 +72,7 @@ jobs:
- name: Run tests
run: make test-${{ matrix.config }}
- name: Build for static-stdlib
run: CONFIG=${{ matrix.config }} make build-for-static-stdlib
run: make CONFIG=${{ matrix.config }} build-for-static-stdlib

wasm:
name: SwiftWasm
Expand Down
14 changes: 12 additions & 2 deletions Examples/ExamplesTests/SwiftTestingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
@Suite
struct SwiftTestingTests_Debug {
@Test func context() {
#expect(TestContext.current == .swiftTesting)
switch TestContext.current {
case .xcTest:
#expect(Bool(true))
default:
Issue.record()
}
}

@Test func reportIssue_NoMessage() {
Expand Down Expand Up @@ -68,7 +73,12 @@
@Suite
struct SwiftTestingTests_Release {
@Test func context() {
#expect(TestContext.current == .xcTest)
switch TestContext.current {
case .xcTest:
#expect(Bool(true))
default:
Issue.record()
}
}

@Test func reportIssueDoesNotFail() {
Expand Down
14 changes: 12 additions & 2 deletions Examples/ExamplesTests/XCTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import XCTest
#if DEBUG
class XCTestTests_Debug: XCTestCase {
func testContext() {
XCTAssertEqual(TestContext.current, .xcTest)
switch TestContext.current {
case .xcTest:
XCTAssert(true)
default:
XCTFail()
}
}

#if _runtime(_ObjC)
Expand Down Expand Up @@ -49,7 +54,12 @@ import XCTest
#else
class XCTestTests_Release: XCTestCase {
func testContext() {
XCTAssertEqual(TestContext.current, .xcTest)
switch TestContext.current {
case .xcTest:
XCTAssert(true)
default:
XCTFail()
}
}

func testReportIssueDoesNotFail() {
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
XCODE_PATH := $(shell xcode-select -p)
CONFIG := debug

# NB: We can't rely on `XCTExpectFailure` because it doesn't exist in `swift-corelibs-foundation`
PASS = \033[1;7;32m PASS \033[0m
Expand Down Expand Up @@ -50,4 +51,4 @@ test-linux:
-v "$(PWD):$(PWD)" \
-w "$(PWD)" \
swift:5.10 \
bash -c 'swift test'
bash -c 'swift test -c $(CONFIG)'
87 changes: 74 additions & 13 deletions Sources/IssueReporting/Internal/SwiftTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,24 +234,17 @@ func _withKnownIssue(
await withKnownIssue(message, isIntermittent, fileID, filePath, line, column, body)
}
@usableFromInline
func _currentTestIsNotNil() -> Bool {
guard let function = function(for: "$s25IssueReportingTestSupport08_currentC8IsNotNilypyF")
func _currentTestID() -> AnyHashable? {
guard let function = function(for: "$s25IssueReportingTestSupport08_currentC2IDypyF")
else {
#if DEBUG
return Test.current != nil
return Test.current?.id
#else
printError(
"""
'Test.current' was accessed without linking the Testing framework.
To fix this, add "IssueReportingTestSupport" as a dependency to your test target.
"""
)
return false
return nil
#endif
}

return (function as! @Sendable () -> Bool)()
return (function as! @Sendable () -> AnyHashable?)()
}

#if DEBUG
Expand Down Expand Up @@ -348,11 +341,15 @@ func _currentTestIsNotNil() -> Bool {
var sourceLocation: SourceLocation?
}

private struct SourceLocation: Sendable {
private struct SourceLocation: Hashable, Sendable {
var fileID: String
var _filePath: String
var line: Int
var column: Int
var moduleName: String {
let firstSlash = fileID.firstIndex(of: "/")!
return String(fileID[..<firstSlash])
}
}

struct Test: @unchecked Sendable {
Expand Down Expand Up @@ -388,6 +385,42 @@ func _currentTestIsNotNil() -> Bool {
var typeInfo: TypeInfo
}
private var isSynthesized = false

private var isSuite: Bool {
containingTypeInfo != nil && testCasesState == nil
}
fileprivate var id: ID {
var result = containingTypeInfo.map(ID.init)
?? ID(moduleName: sourceLocation.moduleName, nameComponents: [], sourceLocation: nil)

if !isSuite {
result.nameComponents.append(name)
result.sourceLocation = sourceLocation
}

return result
}
fileprivate struct ID: Hashable {
var moduleName: String
var nameComponents: [String]
var sourceLocation: SourceLocation?
init(moduleName: String, nameComponents: [String], sourceLocation: SourceLocation?) {
self.moduleName = moduleName
self.nameComponents = nameComponents
self.sourceLocation = sourceLocation
}
init(_ fullyQualifiedNameComponents: some Collection<String>) {
moduleName = fullyQualifiedNameComponents.first ?? ""
if fullyQualifiedNameComponents.count > 0 {
nameComponents = Array(fullyQualifiedNameComponents.dropFirst())
} else {
nameComponents = []
}
}
init(typeInfo: TypeInfo) {
self.init(typeInfo.fullyQualifiedNameComponents)
}
}
}

private protocol Trait: Sendable {}
Expand All @@ -398,6 +431,34 @@ func _currentTestIsNotNil() -> Bool {
case nameOnly(fullyQualifiedComponents: [String], unqualified: String, mangled: String?)
}
var _kind: _Kind

static let _fullyQualifiedNameComponentsCache: LockIsolated<
[ObjectIdentifier: [String]]
> = LockIsolated([:])
var fullyQualifiedNameComponents: [String] {
switch _kind {
case let .type(type):
if let cachedResult = Self
._fullyQualifiedNameComponentsCache.withLock({ $0[ObjectIdentifier(type)] })
{
return cachedResult
}
var result = String(reflecting: type)
.split(separator: ".")
.map(String.init)
if let firstComponent = result.first, firstComponent.starts(with: "(extension in ") {
result[0] = String(firstComponent.split(separator: ":", maxSplits: 1).last!)
}
result = result.filter { !$0.starts(with: "(unknown context at") }
Self._fullyQualifiedNameComponentsCache.withLock { [result] in
$0[ObjectIdentifier(type)] = result
}
return result

case let .nameOnly(fullyQualifiedComponents, _, _):
return fullyQualifiedComponents
}
}
}
#endif

Expand Down
26 changes: 22 additions & 4 deletions Sources/IssueReporting/TestContext.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/// A type representing the context in which a test is being run, i.e. either in Swift's native
/// A type representing the context in which a test is being run, _i.e._ either in Swift's native
/// Testing framework, or Xcode's XCTest framework.
public enum TestContext {
/// The Swift Testing framework.
case swiftTesting
case swiftTesting(Testing)

/// The XCTest framework.
case xcTest
Expand All @@ -21,10 +21,28 @@ public enum TestContext {
/// If executed outside of a test process, this will return `nil`.
public static var current: Self? {
guard isTesting else { return nil }
if _currentTestIsNotNil() {
return .swiftTesting
if let currentTestID = _currentTestID() {
return .swiftTesting(Testing(id: currentTestID))
} else {
return .xcTest
}
}

public struct Testing {
public let test: Test

public struct Test: Hashable, Identifiable, Sendable {
public let id: ID

public struct ID: Hashable, @unchecked Sendable {
fileprivate let rawValue: AnyHashable
}
}
}
}

extension TestContext.Testing {
fileprivate init(id: AnyHashable) {
self.init(test: Test(id: Test.ID(rawValue: id)))
}
}
8 changes: 4 additions & 4 deletions Sources/IssueReportingTestSupport/SwiftTesting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ private func __withKnownIssueAsync(
#endif
}

public func _currentTestIsNotNil() -> Any { __currentTestIsNotNil }
public func _currentTestID() -> Any { __currentTestID }
@Sendable
private func __currentTestIsNotNil() -> Bool {
private func __currentTestID() -> AnyHashable? {
#if canImport(Testing)
return Test.current != nil
return Test.current?.id
#else
return false
return nil
#endif
}
7 changes: 6 additions & 1 deletion Tests/IssueReportingTests/SwiftTestingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
@Suite
struct SwiftTestingTests {
@Test func context() {
#expect(TestContext.current == .swiftTesting)
switch TestContext.current {
case .swiftTesting:
#expect(Bool(true))
default:
Issue.record()
}
}

@Test func reportIssue_NoMessage() {
Expand Down
7 changes: 6 additions & 1 deletion Tests/IssueReportingTests/XCTestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ final class XCTestTests: XCTestCase {
}

func testTestContext() {
XCTAssertEqual(TestContext.current, .xcTest)
switch TestContext.current {
case .xcTest:
XCTAssert(true)
default:
XCTFail()
}
}
#endif

Expand Down

0 comments on commit 0e10568

Please sign in to comment.