Skip to content

Commit

Permalink
Basic swift testing support (#1154)
Browse files Browse the repository at this point in the history
* Provide basic integration with Swift Testing

This is not reliable, in fact, failure reporting is flaky.

* Spike utilizing my changes to Swift Testing to allow direct recording of issues

* link to my branch of swift testing enabling public issues

* Support the current version of Swift Testing

* Fix broken tests

* Fix a broken test helper

* Correct a long-broken test because expectFailureMessageRegex has been a no-op for ages
  • Loading branch information
younata authored Sep 12, 2024
1 parent 3e40449 commit f12ca52
Show file tree
Hide file tree
Showing 21 changed files with 448 additions and 198 deletions.
8 changes: 8 additions & 0 deletions Nimble.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@
8923E60D2B47CE7E00F3961A /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8923E60C2B47CE7E00F3961A /* Map.swift */; };
8923E6102B47D08300F3961A /* MapTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8923E60E2B47D06E00F3961A /* MapTest.swift */; };
892FDF1329D3EA7700523A80 /* AsyncExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892FDF1229D3EA7700523A80 /* AsyncExpression.swift */; };
895644DD2C1B63910006EC12 /* NimbleSwiftTestingHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895644DC2C1B63910006EC12 /* NimbleSwiftTestingHandler.swift */; };
895644DF2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895644DE2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift */; };
896962412A5FABD000A7929D /* AsyncAllPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962402A5FABD000A7929D /* AsyncAllPass.swift */; };
8969624A2A5FAD5F00A7929D /* AsyncAllPassTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */; };
898F28B025D9F4C30052B8D0 /* AlwaysFailMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */; };
Expand Down Expand Up @@ -324,6 +326,8 @@
8923E60E2B47D06E00F3961A /* MapTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTest.swift; sourceTree = "<group>"; };
892FDF1229D3EA7700523A80 /* AsyncExpression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncExpression.swift; sourceTree = "<group>"; };
8952ADDC2B4F159400D9305F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
895644DC2C1B63910006EC12 /* NimbleSwiftTestingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleSwiftTestingHandler.swift; sourceTree = "<group>"; };
895644DE2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTestingSupportTest.swift; sourceTree = "<group>"; };
896962402A5FABD000A7929D /* AsyncAllPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPass.swift; sourceTree = "<group>"; };
896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPassTest.swift; sourceTree = "<group>"; };
898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysFailMatcher.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -490,6 +494,7 @@
89C297CB2A911CDA002A143F /* AsyncTimerSequenceTest.swift */,
89C297CD2A92AB34002A143F /* AsyncPromiseTest.swift */,
965B0D0B1B62C06D0005AE66 /* UserDescriptionTest.swift */,
895644DE2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift */,
6CAEDD091CAEA86F003F1584 /* LinuxSupport.swift */,
1F14FB61194180A7009F2A08 /* Helpers */,
1F925EE3195C11B000ED456B /* Matchers */,
Expand Down Expand Up @@ -561,6 +566,7 @@
89F5E090290B9D5C001F9377 /* AssertionRecorder+Async.swift */,
1FC494A91C29CBA40010975C /* NimbleEnvironment.swift */,
1FD8CD071968AB07008ED995 /* NimbleXCTestHandler.swift */,
895644DC2C1B63910006EC12 /* NimbleSwiftTestingHandler.swift */,
1F1871BA1CA89E2500A34BF2 /* NonObjectiveC */,
1F1871C21CA89EDB00A34BF2 /* NMBExpectation.swift */,
);
Expand Down Expand Up @@ -872,6 +878,7 @@
CDF5C57B2647B89B0036532C /* Equal+Tuple.swift in Sources */,
857D1849253610A900D8693A /* BeWithin.swift in Sources */,
1FD8CD4D1968AB07008ED995 /* BeLessThan.swift in Sources */,
895644DD2C1B63910006EC12 /* NimbleSwiftTestingHandler.swift in Sources */,
1FD8CD471968AB07008ED995 /* BeGreaterThan.swift in Sources */,
F8A1BE301CB3710900031679 /* XCTestObservationCenter+Register.m in Sources */,
1FD8CD311968AB07008ED995 /* AdapterProtocols.swift in Sources */,
Expand Down Expand Up @@ -974,6 +981,7 @@
1F4A568C1A3B3407009E1637 /* ObjCBeTrueTest.m in Sources */,
DDEFAEB51A93CBE6005CA37A /* ObjCAllPassTest.m in Sources */,
1F4A56801A3B333F009E1637 /* ObjCBeLessThanTest.m in Sources */,
895644DF2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift in Sources */,
857D184F2536124400D8693A /* BeWithinTest.swift in Sources */,
8922828F2B283956002DA355 /* AsyncAwaitTest+Require.swift in Sources */,
1F0648CD19639F5A001F9C46 /* ObjectWithLazyProperty.swift in Sources */,
Expand Down
10 changes: 7 additions & 3 deletions Sources/Nimble/Adapters/AdapterProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ public protocol AssertionHandler {
}

/// Global backing interface for assertions that Nimble creates.
/// Defaults to a private test handler that passes through to XCTest.
/// Defaults to a private test handler that passes through to Swift Testing or XCTest.
///
/// If XCTest is not available, you must assign your own assertion handler
/// If neither Swift Testing or XCTest is available, you must assign your own assertion handler
/// before using any matchers, otherwise Nimble will abort the program.
///
/// @see AssertionHandler
public var NimbleAssertionHandler: AssertionHandler = { () -> AssertionHandler in
// swiftlint:disable:previous identifier_name
return isXCTestAvailable() ? NimbleXCTestHandler() : NimbleXCTestUnavailableHandler()
if isSwiftTestingAvailable() || isXCTestAvailable() {
return NimbleTestingHandler()
}

return NimbleTestingUnavailableHandler()
}()
6 changes: 4 additions & 2 deletions Sources/Nimble/Adapters/AssertionRecorder+Async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
///
/// @see AssertionHandler
public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
file: FileString = #file,
fileID: String = #fileID,
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
closure: () async throws -> Void) async {
let environment = NimbleEnvironment.activeInstance
let oldRecorder = environment.assertionHandler
Expand All @@ -23,7 +25,7 @@ public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
} catch {
let failureMessage = FailureMessage()
failureMessage.stringValue = "unexpected error thrown: <\(error)>"
let location = SourceLocation(file: file, line: line)
let location = SourceLocation(fileID: fileID, filePath: file, line: line, column: column)
tempAssertionHandler.assert(false, message: failureMessage, location: location)
}
}
Expand Down
10 changes: 8 additions & 2 deletions Sources/Nimble/Adapters/AssertionRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,10 @@ extension NMBExceptionCapture {
///
/// @see AssertionHandler
public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
file: FileString = #file,
fileID: String = #fileID,
file: FileString = #filePath,
line: UInt = #line,
column: UInt = #column,
closure: () throws -> Void) {
let environment = NimbleEnvironment.activeInstance
let oldRecorder = environment.assertionHandler
Expand All @@ -80,7 +82,11 @@ public func withAssertionHandler(_ tempAssertionHandler: AssertionHandler,
} catch {
let failureMessage = FailureMessage()
failureMessage.stringValue = "unexpected error thrown: <\(error)>"
let location = SourceLocation(file: file, line: line)
let location = SourceLocation(
fileID: fileID,
filePath: file,
line: line, column: column
)
tempAssertionHandler.assert(false, message: failureMessage, location: location)
}
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/Nimble/Adapters/NMBExpectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public class NMBExpectation: NSObject {
}

@objc public class func failWithMessage(_ message: String, file: FileString, line: UInt) {
fail(message, location: SourceLocation(file: file, line: line))
fail(message, location: SourceLocation(fileID: "Unknown/\(file)", filePath: file, line: line, column: 0))
}
}

Expand Down
44 changes: 44 additions & 0 deletions Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Foundation
#if canImport(Testing)
import Testing
#endif

public class NimbleSwiftTestingHandler: AssertionHandler {
public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
if !assertion {
recordTestingFailure("\(message.stringValue)\n", location: location)
}
}
}

func isSwiftTestingAvailable() -> Bool {
#if canImport(Testing)
true
#else
false
#endif
}

func isRunningSwiftTest() -> Bool {
#if canImport(Testing)
Test.current != nil
#else
false
#endif
}

public func recordTestingFailure(_ message: String, location: SourceLocation) {
#if canImport(Testing)
let testingLocation = Testing.SourceLocation(
fileID: location.fileID,
filePath: "\(location.filePath)",
line: Int(location.line),
column: Int(location.column)
)

Testing.Issue.record(
"\(message)",
sourceLocation: testingLocation
)
#endif
}
24 changes: 17 additions & 7 deletions Sources/Nimble/Adapters/NimbleXCTestHandler.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import Foundation
import XCTest

/// Default handler for Nimble. This assertion handler passes failures along to
/// XCTest.
/// Default handler for Nimble. This assertion handler passes on to Swift Testing or XCTest.
public class NimbleTestingHandler: AssertionHandler {
public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
if isRunningSwiftTest() {
NimbleSwiftTestingHandler().assert(assertion, message: message, location: location)
} else {
NimbleXCTestHandler().assert(assertion, message: message, location: location)
}
}
}

/// This assertion handler passes failures along to XCTest.
public class NimbleXCTestHandler: AssertionHandler {
public func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
if !assertion {
Expand All @@ -27,11 +37,11 @@ public class NimbleShortXCTestHandler: AssertionHandler {
}
}

/// Fallback handler in case XCTest is unavailable. This assertion handler will abort
/// Fallback handler in case XCTest/Swift Testing is unavailable. This assertion handler will abort
/// the program if it is invoked.
class NimbleXCTestUnavailableHandler: AssertionHandler {
class NimbleTestingUnavailableHandler: AssertionHandler {
func assert(_ assertion: Bool, message: FailureMessage, location: SourceLocation) {
fatalError("XCTest is not available and no custom assertion handler was configured. Aborting.")
fatalError("XCTest and Swift Testing are not available and no custom assertion handler was configured. Aborting.")
}
}

Expand Down Expand Up @@ -78,15 +88,15 @@ public func recordFailure(_ message: String, location: SourceLocation) {
#else
if let testCase = CurrentTestCaseTracker.sharedInstance.currentTestCase {
let line = Int(location.line)
let location = XCTSourceCodeLocation(filePath: location.file, lineNumber: line)
let location = XCTSourceCodeLocation(filePath: location.filePath, lineNumber: line)
let sourceCodeContext = XCTSourceCodeContext(location: location)
let issue = XCTIssue(type: .assertionFailure, compactDescription: message, sourceCodeContext: sourceCodeContext)
testCase.record(issue)
} else {
let msg = """
Attempted to report a test failure to XCTest while no test case was running. The failure was:
\"\(message)\"
It occurred at: \(location.file):\(location.line)
It occurred at: \(location)
"""
NSException(name: .internalInconsistencyException, reason: msg, userInfo: nil).raise()
}
Expand Down
Loading

0 comments on commit f12ca52

Please sign in to comment.