Skip to content
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

Expose Testing test identifier from test context #125

Merged
merged 13 commits into from
Sep 6, 2024
Merged
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
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)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically this is a breaking change for folks referring directly to this .swiftTesting case, so we could introduce a new set of alternate APIs here instead.

If someone was merely switching over the cases in their test helper it should be source compatible, though.

/// 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
Loading