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

Report skipped tests to Xcode #1098

Merged
merged 2 commits into from
Apr 27, 2022
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
175 changes: 149 additions & 26 deletions Sources/Quick/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ final public class Example: _ExampleBase {
Executes the example closure, as well as all before and after
closures defined in the its surrounding example groups.
*/
public func run() { // swiftlint:disable:this function_body_length
public func run() {
let world = World.sharedWorld

if world.numberOfExamplesRun == 0 {
Expand All @@ -83,31 +83,11 @@ final public class Example: _ExampleBase {
do {
try closure()
} catch {
let description = "Test \(name) threw unexpected error: \(error.localizedDescription)"
#if SWIFT_PACKAGE
let file = callsite.file.description
#else
let file = callsite.file
#endif

// XCTIssue is unavailable (not implemented yet) on swift-corelibs-xctest (for non-Apple platforms)
#if canImport(Darwin)
let location = XCTSourceCodeLocation(filePath: file, lineNumber: Int(callsite.line))
let sourceCodeContext = XCTSourceCodeContext(location: location)
let issue = XCTIssue(
type: .thrownError,
compactDescription: description,
sourceCodeContext: sourceCodeContext
)
QuickSpec.current.record(issue)
#else
QuickSpec.current.recordFailure(
withDescription: description,
inFile: file,
atLine: Int(callsite.line),
expected: false
)
#endif
if let testSkippedError = error as? XCTSkip {
self.reportSkippedTest(testSkippedError, name: name, callsite: callsite)
} else {
self.reportFailedTest(error, name: name, callsite: callsite)
}
}

self.group!.phase = .aftersExecuting
Expand Down Expand Up @@ -141,6 +121,149 @@ final public class Example: _ExampleBase {
}
return aggregateFlags
}

#if canImport(Darwin)
static let recordSkipSelector = NSSelectorFromString("recordSkipWithDescription:sourceCodeContext:")
#endif

private func reportSkippedTest(_ testSkippedError: XCTSkip, name: String, callsite: Callsite) { // swiftlint:disable:this function_body_length
#if !canImport(Darwin)
return // This functionality is only supported by Apple's proprietary XCTest, not by swift-corelibs-xctest
#else // `NSSelectorFromString` requires the Objective-C runtime, which is not available on Linux.

let messageSuffix = """
\n
If nobody else has done so yet, please submit an issue to https://github.com/Quick/Quick/issues

For now, we'll just benignly ignore skipped tests.
"""

guard let testRun = QuickSpec.current.testRun else {
print("""
[Quick Warning]: `QuickSpec.current.testRun` was unexpectededly `nil`.
""" + messageSuffix)
return
}

guard let skippedTestContextAny = testSkippedError.errorUserInfo["XCTestErrorUserInfoKeySkippedTestContext"] else {
print("""
[Quick Warning]: The internals of Apple's XCTestCaseRun have changed.
We expected the `errorUserInfo` dictionary of the XCTSKip error to contain a value for the key
"XCTestErrorUserInfoKeySkippedTestContext", but it didn't.
""" + messageSuffix)
return
}

// Uses an internal type "XCTSkippedTestContext", but "NSObject" will be sufficient for `perform(_:with:_with:)`.
guard let skippedTestContext = skippedTestContextAny as? NSObject else {
print("""
[Quick Warning]: The internals of Apple's XCTestCaseRun have changed.
We expected `skippedTestContextAny` to have type `NSObject`,
but we got an object of type \(type(of: skippedTestContextAny))
""" + messageSuffix)
return
}

if isLegacyXcode(testRun: testRun) {
reportSkippedTest_legacy(testRun: testRun, skippedTestContext: skippedTestContext)
return
}

guard let sourceCodeContextAny = skippedTestContext.value(forKey: "sourceCodeContext") else {
print("""
[Quick Warning]: The internals of Apple's XCTestCaseRun have changed.
We expected `XCTSkippedTestContext` to have a `sourceCodeContext` property, but it did not.
""" + messageSuffix)
return
}

guard let sourceCodeContext = sourceCodeContextAny as? XCTSourceCodeContext else {
print("""
[Quick Warning]: The internals of Apple's XCTestCaseRun have changed.
We expected `XCTSkippedTestContext.sourceCodeContext` to have type `XCTSourceCodeContext`,
but we got an object of type \(type(of: sourceCodeContextAny)).
""" + messageSuffix)
return
}

guard testRun.responds(to: Self.recordSkipSelector) else {
print("""
[Quick Warning]: The internals of Apple's XCTestCaseRun have changed, as it no longer responds to
the -[XCTSkip \(NSStringFromSelector(Self.recordSkipSelector))] message necessary to report skipped tests to Xcode.
""" + messageSuffix)
return
}

testRun.perform(Self.recordSkipSelector, with: testSkippedError.message, with: sourceCodeContext)
#endif
}

#if canImport(Darwin)
private func isLegacyXcode(testRun: XCTestRun) -> Bool {
!testRun.responds(to: Self.recordSkipSelector)
}

/// Attempt to report a test skip for old Xcode versions (namely Xcode 12.4).
///
/// As of Xcode 12.4, the `XCTSkippedTestContext` object contained these fields:
/// - `filePath: NSString`
/// - `lineNumber: NSString`
///
/// After Xcode 12.4, those fields were extracted into a new `sourceCodeContext: XCTSkippedTestContext` property.
/// - Parameters:
/// - testRun: The test run that was skipped
/// - skippedTestContext: an `XCTSkippedTestContext` object
private func reportSkippedTest_legacy(testRun: XCTestRun, skippedTestContext: NSObject) {
let legacyRecordSkipSelector = NSSelectorFromString("recordSkipWithDescription:inFile:atLine:")

guard let imp = testRun.method(for: legacyRecordSkipSelector),
let description = skippedTestContext.value(forKey: "message") as? NSString,
let filePath = skippedTestContext.value(forKey: "filePath") as? NSString,
let lineNumber = skippedTestContext.value(forKey: "lineNumber") as? UInt32 else {
return
}

typealias MethodSigniture = @convention(c) (NSObject, Selector, NSString, NSString, UInt32) -> Void

let methodImp = unsafeBitCast(imp, to: MethodSigniture.self)

methodImp(
/* self */ testRun,
/* selector */ legacyRecordSkipSelector,
/* recordSkipWithDescription: */ description,
/* inFile: */ filePath,
/* atLine: */ lineNumber
)
}
#endif

private func reportFailedTest(_ error: Error, name: String, callsite: Callsite) {
let description = "Test \(name) threw unexpected error: \(error.localizedDescription)"

#if SWIFT_PACKAGE
let file = callsite.file.description
#else
let file = callsite.file
#endif

#if !SWIFT_PACKAGE
let location = XCTSourceCodeLocation(filePath: file, lineNumber: Int(callsite.line))
let sourceCodeContext = XCTSourceCodeContext(location: location)
let issue = XCTIssue(
type: .thrownError,
compactDescription: description,
sourceCodeContext: sourceCodeContext
)
QuickSpec.current.record(issue)
#else
QuickSpec.current.recordFailure(
withDescription: description,
inFile: file,
atLine: Int(callsite.line),
expected: false
)
#endif
}
}

extension Example {
Expand Down
16 changes: 16 additions & 0 deletions Tests/QuickTests/QuickTests/FunctionalTests/ItTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,19 @@ class FunctionalTests_ImplicitErrorItSpec: QuickSpec {
}
}

final class FunctionalTests_SkippingTestsSpec: QuickSpec {
override func spec() {
it("supports skipping tests") { throw XCTSkip("This test is intentionally skipped") }
it("supports not skipping tests") { }
}
}

final class ItTests: XCTestCase, XCTestCaseProvider {
static var allTests: [(String, (ItTests) -> () throws -> Void)] {
return [
("testAllExamplesAreExecuted", testAllExamplesAreExecuted),
("testImplicitErrorHandling", testImplicitErrorHandling),
("testSkippingExamplesAreCorrectlyReported", testSkippingExamplesAreCorrectlyReported),
]
}

Expand Down Expand Up @@ -172,4 +180,12 @@ final class ItTests: XCTestCase, XCTestCaseProvider {
XCTAssertEqual(result.unexpectedExceptionCount, 1)
XCTAssertEqual(result.totalFailureCount, 1)
}

func testSkippingExamplesAreCorrectlyReported() {
let result = qck_runSpec(FunctionalTests_SkippingTestsSpec.self)!
XCTAssertTrue(result.hasSucceeded)
XCTAssertEqual(result.executionCount, 2)
XCTAssertEqual(result.skipCount, 1)
XCTAssertEqual(result.totalFailureCount, 0)
}
}