diff --git a/Nimble.xcodeproj/project.pbxproj b/Nimble.xcodeproj/project.pbxproj index 1ffef9319..6243c8086 100644 --- a/Nimble.xcodeproj/project.pbxproj +++ b/Nimble.xcodeproj/project.pbxproj @@ -134,7 +134,10 @@ 8922828F2B283956002DA355 /* AsyncAwaitTest+Require.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8922828E2B283956002DA355 /* AsyncAwaitTest+Require.swift */; }; 8923E60D2B47CE7E00F3961A /* Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8923E60C2B47CE7E00F3961A /* Map.swift */; }; 8923E6102B47D08300F3961A /* MapTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8923E60E2B47D06E00F3961A /* MapTest.swift */; }; + 8924FE0B2C1CA6BB00A9062A /* Testing in Frameworks */ = {isa = PBXBuildFile; productRef = 8924FE0A2C1CA6BB00A9062A /* Testing */; }; 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 */; }; @@ -322,6 +325,8 @@ 8923E60E2B47D06E00F3961A /* MapTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTest.swift; sourceTree = ""; }; 892FDF1229D3EA7700523A80 /* AsyncExpression.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AsyncExpression.swift; sourceTree = ""; }; 8952ADDC2B4F159400D9305F /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 895644DC2C1B63910006EC12 /* NimbleSwiftTestingHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleSwiftTestingHandler.swift; sourceTree = ""; }; + 895644DE2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTestingSupportTest.swift; sourceTree = ""; }; 896962402A5FABD000A7929D /* AsyncAllPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPass.swift; sourceTree = ""; }; 896962452A5FAD4500A7929D /* AsyncAllPassTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncAllPassTest.swift; sourceTree = ""; }; 898F28AF25D9F4C30052B8D0 /* AlwaysFailMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlwaysFailMatcher.swift; sourceTree = ""; }; @@ -370,6 +375,7 @@ buildActionMask = 2147483647; files = ( 89D8AC872B3211EA00410644 /* CwlPosixPreconditionTesting in Frameworks */, + 8924FE0B2C1CA6BB00A9062A /* Testing in Frameworks */, 89D8AC852B3211C600410644 /* CwlCatchException in Frameworks */, 89D8AC892B3211EA00410644 /* CwlPreconditionTesting in Frameworks */, ); @@ -486,6 +492,7 @@ 89C297CB2A911CDA002A143F /* AsyncTimerSequenceTest.swift */, 89C297CD2A92AB34002A143F /* AsyncPromiseTest.swift */, 965B0D0B1B62C06D0005AE66 /* UserDescriptionTest.swift */, + 895644DE2C1B71DE0006EC12 /* SwiftTestingSupportTest.swift */, 6CAEDD091CAEA86F003F1584 /* LinuxSupport.swift */, 1F14FB61194180A7009F2A08 /* Helpers */, 1F925EE3195C11B000ED456B /* Matchers */, @@ -556,6 +563,7 @@ 89F5E090290B9D5C001F9377 /* AssertionRecorder+Async.swift */, 1FC494A91C29CBA40010975C /* NimbleEnvironment.swift */, 1FD8CD071968AB07008ED995 /* NimbleXCTestHandler.swift */, + 895644DC2C1B63910006EC12 /* NimbleSwiftTestingHandler.swift */, 1F1871BA1CA89E2500A34BF2 /* NonObjectiveC */, 1F1871C21CA89EDB00A34BF2 /* NMBExpectation.swift */, ); @@ -721,6 +729,7 @@ 89D8AC842B3211C600410644 /* CwlCatchException */, 89D8AC862B3211EA00410644 /* CwlPosixPreconditionTesting */, 89D8AC882B3211EA00410644 /* CwlPreconditionTesting */, + 8924FE0A2C1CA6BB00A9062A /* Testing */, ); productName = "Nimble-macOS"; productReference = 1F925EAD195C0D6300ED456B /* Nimble.framework */; @@ -782,6 +791,7 @@ packageReferences = ( 89D8AC812B32119300410644 /* XCRemoteSwiftPackageReference "CwlCatchException" */, 89D8AC822B3211A900410644 /* XCRemoteSwiftPackageReference "CwlPreconditionTesting" */, + 8924FE092C1CA6BB00A9062A /* XCRemoteSwiftPackageReference "swift-testing" */, ); productRefGroup = 1F1A742A1940169200FFFC47 /* Products */; projectDirPath = ""; @@ -866,6 +876,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 */, @@ -966,6 +977,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 */, @@ -1350,6 +1362,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 8924FE092C1CA6BB00A9062A /* XCRemoteSwiftPackageReference "swift-testing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/younata/swift-testing"; + requirement = { + branch = "public-issues-pitch"; + kind = branch; + }; + }; 89D8AC812B32119300410644 /* XCRemoteSwiftPackageReference "CwlCatchException" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/mattgallagher/CwlCatchException"; @@ -1369,6 +1389,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 8924FE0A2C1CA6BB00A9062A /* Testing */ = { + isa = XCSwiftPackageProductDependency; + package = 8924FE092C1CA6BB00A9062A /* XCRemoteSwiftPackageReference "swift-testing" */; + productName = Testing; + }; 89D8AC842B3211C600410644 /* CwlCatchException */ = { isa = XCSwiftPackageProductDependency; package = 89D8AC812B32119300410644 /* XCRemoteSwiftPackageReference "CwlCatchException" */; diff --git a/Nimble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Nimble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bf75cf0a2..d1bcdbdf2 100644 --- a/Nimble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Nimble.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "f14c33ce48473aca15a7346134d7546c72ed4d7b9e9e62243a4682949138cbe2", "pins" : [ { "identity" : "cwlcatchexception", @@ -17,7 +18,25 @@ "revision" : "dc9af4781f2afdd1e68e90f80b8603be73ea7abc", "version" : "2.2.0" } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c", + "version" : "600.0.0-prerelease-2024-06-12" + } + }, + { + "identity" : "swift-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/younata/swift-testing", + "state" : { + "branch" : "public-issues-pitch", + "revision" : "e945bad553d3c0b66ca489e2d52ad26119ce818d" + } } ], - "version" : 2 + "version" : 3 } diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 000000000..e54ec4c6f --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,68 @@ +// swift-tools-version:6.0 +import PackageDescription + +let package = Package( + name: "Nimble", + platforms: [ + .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1) + ], + products: [ + .library( + name: "Nimble", + targets: { + var targets: [String] = ["Nimble"] + #if os(macOS) + targets.append("NimbleObjectiveC") + #endif + return targets + }() + ), + ], + dependencies: [ + .package(url: "https://github.com/mattgallagher/CwlPreconditionTesting.git", .upToNextMajor(from: "2.2.0")), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), + .package(url: "https://github.com/younata/swift-testing", branch: "public-issues-pitch") + ], + targets: { + var testHelperDependencies: [PackageDescription.Target.Dependency] = ["Nimble"] + #if os(macOS) + testHelperDependencies.append("NimbleObjectiveC") + #endif + var targets: [Target] = [ + .target( + name: "Nimble", + dependencies: [ + .product(name: "CwlPreconditionTesting", package: "CwlPreconditionTesting", + condition: .when(platforms: [.macOS, .iOS, .macCatalyst, .visionOS])), + .product(name: "CwlPosixPreconditionTesting", package: "CwlPreconditionTesting", + condition: .when(platforms: [.tvOS, .watchOS])), + .product(name: "Testing", package: "swift-testing") + ], + exclude: ["Info.plist"] + ), + .target( + name: "NimbleSharedTestHelpers", + dependencies: testHelperDependencies + ), + .testTarget( + name: "NimbleTests", + dependencies: ["Nimble", "NimbleSharedTestHelpers"], + exclude: ["Info.plist"] + ), + ] +#if os(macOS) + targets.append(contentsOf: [ + .target( + name: "NimbleObjectiveC", + dependencies: ["Nimble"] + ), + .testTarget( + name: "NimbleObjectiveCTests", + dependencies: ["NimbleObjectiveC", "Nimble", "NimbleSharedTestHelpers"] + ) + ]) + #endif + return targets + }(), + swiftLanguageVersions: [.v5] +) diff --git a/Sources/Nimble/Adapters/AdapterProtocols.swift b/Sources/Nimble/Adapters/AdapterProtocols.swift index a28bf2be5..d7734879e 100644 --- a/Sources/Nimble/Adapters/AdapterProtocols.swift +++ b/Sources/Nimble/Adapters/AdapterProtocols.swift @@ -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() }() diff --git a/Sources/Nimble/Adapters/AssertionRecorder+Async.swift b/Sources/Nimble/Adapters/AssertionRecorder+Async.swift index 8633444a4..f6d3281be 100644 --- a/Sources/Nimble/Adapters/AssertionRecorder+Async.swift +++ b/Sources/Nimble/Adapters/AssertionRecorder+Async.swift @@ -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 @@ -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) } } diff --git a/Sources/Nimble/Adapters/AssertionRecorder.swift b/Sources/Nimble/Adapters/AssertionRecorder.swift index 239393eda..adb869132 100644 --- a/Sources/Nimble/Adapters/AssertionRecorder.swift +++ b/Sources/Nimble/Adapters/AssertionRecorder.swift @@ -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 @@ -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) } } diff --git a/Sources/Nimble/Adapters/NMBExpectation.swift b/Sources/Nimble/Adapters/NMBExpectation.swift index 44ced6562..f8d2d692f 100644 --- a/Sources/Nimble/Adapters/NMBExpectation.swift +++ b/Sources/Nimble/Adapters/NMBExpectation.swift @@ -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)) } } diff --git a/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift b/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift new file mode 100644 index 000000000..a37811e87 --- /dev/null +++ b/Sources/Nimble/Adapters/NimbleSwiftTestingHandler.swift @@ -0,0 +1,53 @@ +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) + ) + + let issue = Testing.Issue( + kind: .expectationFailed( + Testing.Expectation( + mismatchedErrorDescription: message, + differenceDescription: nil, + isPassing: false, + isRequired: false, + sourceLocation: testingLocation + ) + ), + sourceContext: SourceContext(sourceLocation: testingLocation) + ) + issue.record() +#endif +} diff --git a/Sources/Nimble/Adapters/NimbleXCTestHandler.swift b/Sources/Nimble/Adapters/NimbleXCTestHandler.swift index 286ae7ea6..8db21f44e 100644 --- a/Sources/Nimble/Adapters/NimbleXCTestHandler.swift +++ b/Sources/Nimble/Adapters/NimbleXCTestHandler.swift @@ -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 { @@ -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.") } } @@ -78,7 +88,7 @@ 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) @@ -86,7 +96,7 @@ public func recordFailure(_ message: String, location: SourceLocation) { 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() } diff --git a/Sources/Nimble/DSL+AsyncAwait.swift b/Sources/Nimble/DSL+AsyncAwait.swift index 3c4c1de22..522e4af53 100644 --- a/Sources/Nimble/DSL+AsyncAwait.swift +++ b/Sources/Nimble/DSL+AsyncAwait.swift @@ -3,78 +3,78 @@ import Dispatch #endif /// Make an ``AsyncExpectation`` on a given actual value. The value given is lazily evaluated. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @escaping () async throws -> T?) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @escaping () async throws -> T?) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: () -> (() async throws -> T)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: () -> (() async throws -> T)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: () -> (() async throws -> T?)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: () -> (() async throws -> T?)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: () -> (() async throws -> Void)) -> AsyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: () -> (() async throws -> Void)) -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make an ``AsyncExpectation`` on a given actual value. The value given is lazily evaluated. /// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation`. -public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation` -public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation` -public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make an ``AsyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided to avoid confusion between `expect -> SyncExpectation` and `expect -> AsyncExpectation` -public func expecta(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() async throws -> Void)) async -> AsyncExpectation { +public func expecta(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() async throws -> Void)) async -> AsyncExpectation { return AsyncExpectation( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } @@ -87,8 +87,18 @@ public func expecta(file: FileString = #file, line: UInt = #line, _ expression: /// /// @warning /// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions. -public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping (@escaping () -> Void) async -> Void) async { - await throwableUntil(timeout: timeout) { done in +public func waitUntil( + timeout: NimbleTimeInterval = PollingDefaults.timeout, + fileID: String = #fileID, + file: FileString = #filePath, + line: UInt = #line, + column: UInt = #column, + action: @escaping (@escaping () -> Void) async -> Void +) async { + await throwableUntil( + timeout: timeout, + sourceLocation: SourceLocation(fileID: fileID, filePath: file, line: line, column: column) + ) { done in await action(done) } } @@ -100,8 +110,18 @@ public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fil /// /// @warning /// Unlike the synchronous version of this call, this does not support catching Objective-C exceptions. -public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping (@escaping () -> Void) -> Void) async { - await throwableUntil(timeout: timeout, file: file, line: line) { done in +public func waitUntil( + timeout: NimbleTimeInterval = PollingDefaults.timeout, + fileID: String = #fileID, + file: FileString = #filePath, + line: UInt = #line, + column: UInt = #column, + action: @escaping (@escaping () -> Void) -> Void +) async { + await throwableUntil( + timeout: timeout, + sourceLocation: SourceLocation(fileID: fileID, filePath: file, line: line, column: column) + ) { done in action(done) } } @@ -113,14 +133,13 @@ private enum ErrorResult { private func throwableUntil( timeout: NimbleTimeInterval, - file: FileString = #file, - line: UInt = #line, + sourceLocation: SourceLocation, action: @escaping (@escaping () -> Void) async throws -> Void) async { let leeway = timeout.divided let result = await performBlock( timeoutInterval: timeout, leeway: leeway, - file: file, line: line) { @MainActor (done: @escaping (ErrorResult) -> Void) async throws -> Void in + sourceLocation: sourceLocation) { @MainActor (done: @escaping (ErrorResult) -> Void) async throws -> Void in do { try await action { done(.none) @@ -133,14 +152,37 @@ private func throwableUntil( switch result { case .incomplete: internalError("Reached .incomplete state for waitUntil(...).") case .blockedRunLoop: - fail(blockedRunLoopErrorMessageFor("-waitUntil()", leeway: leeway), - file: file, line: line) + fail( + blockedRunLoopErrorMessageFor("-waitUntil()", leeway: leeway), + fileID: sourceLocation.fileID, + file: sourceLocation.filePath, + line: sourceLocation.line, + column: sourceLocation.column + ) case .timedOut: - fail("Waited more than \(timeout.description)", file: file, line: line) + fail( + "Waited more than \(timeout.description)", + fileID: sourceLocation.fileID, + file: sourceLocation.filePath, + line: sourceLocation.line, + column: sourceLocation.column + ) case let .errorThrown(error): - fail("Unexpected error thrown: \(error)") + fail( + "Unexpected error thrown: \(error)", + fileID: sourceLocation.fileID, + file: sourceLocation.filePath, + line: sourceLocation.line, + column: sourceLocation.column + ) case .completed(.error(let error)): - fail("Unexpected error thrown: \(error)") + fail( + "Unexpected error thrown: \(error)", + fileID: sourceLocation.fileID, + file: sourceLocation.filePath, + line: sourceLocation.line, + column: sourceLocation.column + ) case .completed(.none): // success break } diff --git a/Sources/Nimble/DSL+Require.swift b/Sources/Nimble/DSL+Require.swift index d21c39f1d..189d23ef7 100644 --- a/Sources/Nimble/DSL+Require.swift +++ b/Sources/Nimble/DSL+Require.swift @@ -3,11 +3,11 @@ /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -17,11 +17,11 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -31,11 +31,11 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -45,11 +45,11 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -61,11 +61,11 @@ public func require(file: FileString = #file, line: UInt = #line, customError: E /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -77,11 +77,11 @@ public func requires(file: FileString = #file, line: UInt = #line, customErro /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -93,11 +93,11 @@ public func requires(file: FileString = #file, line: UInt = #line, customErro /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -109,11 +109,11 @@ public func requires(file: FileString = #file, line: UInt = #line, customErro /// /// This is provided as an alternative to ``require``, for when you want to be specific about whether you're using ``SyncRequirement`` or ``AsyncRequirement``. @discardableResult -public func requires(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncRequirement { +public func requires(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncRequirement { return SyncRequirement( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -123,11 +123,11 @@ public func requires(file: FileString = #file, line: UInt = #line, customError: /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @escaping () async throws -> T?) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping () async throws -> T?) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -137,11 +137,11 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (() async throws -> T)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -151,11 +151,11 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// `require` will return the result of the expression if the matcher passes, and throw an error if not. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func require(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) -> AsyncRequirement { +public func require(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -167,11 +167,11 @@ public func require(file: FileString = #file, line: UInt = #line, customError /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement`. @discardableResult -public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -183,11 +183,11 @@ public func requirea(file: FileString = #file, line: UInt = #line, customErro /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -199,11 +199,11 @@ public func requirea(file: FileString = #file, line: UInt = #line, customErro /// /// This is provided to avoid confusion between `require -> SyncRequirement` and `require -> AsyncRequirement` @discardableResult -public func requirea(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncRequirement { +public func requirea(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async -> AsyncRequirement { return AsyncRequirement( expression: AsyncExpression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true), customError: customError) } @@ -216,8 +216,8 @@ public func requirea(file: FileString = #file, line: UInt = #line, customErro /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { - try requires(file: file, line: line, customError: customError, expression()).toNot(beNil()) +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. @@ -226,8 +226,8 @@ public func unwrap(file: FileString = #file, line: UInt = #line, customError: /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { - try requires(file: file, line: line, customError: customError, expression()).toNot(beNil()) +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. @@ -236,8 +236,8 @@ public func unwrap(file: FileString = #file, line: UInt = #line, customError: /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { - try requires(file: file, line: line, customError: customError, expression()).toNot(beNil()) +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () throws -> T?) throws -> T { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) } /// Makes sure that the expression evaluates to a non-nil value, otherwise throw an error. @@ -246,8 +246,8 @@ public func unwraps(file: FileString = #file, line: UInt = #line, customError /// `unwraps` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwraps(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { - try requires(file: file, line: line, customError: customError, expression()).toNot(beNil()) +public func unwraps(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() throws -> T?)) throws -> T { + try requires(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -256,8 +256,8 @@ public func unwraps(file: FileString = #file, line: UInt = #line, customError /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @escaping () async throws -> T?) async throws -> T { - try await requirea(file: file, line: line, customError: customError, try await expression()).toNot(beNil()) +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @escaping () async throws -> T?) async throws -> T { + try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, try await expression()).toNot(beNil()) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -266,8 +266,8 @@ public func unwrap(file: FileString = #file, line: UInt = #line, customError: /// `unwrap` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrap(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) async throws -> T { - try await requirea(file: file, line: line, customError: customError, expression()).toNot(beNil()) +public func unwrap(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: () -> (() async throws -> T?)) async throws -> T { + try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -276,8 +276,8 @@ public func unwrap(file: FileString = #file, line: UInt = #line, customError: /// `unwrapa` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrapa(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { - try await requirea(file: file, line: line, customError: customError, try await expression()).toNot(beNil()) +public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure @escaping () async throws -> T?) async throws -> T { + try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, try await expression()).toNot(beNil()) } /// Makes sure that the async expression evaluates to a non-nil value, otherwise throw an error. @@ -286,6 +286,6 @@ public func unwrapa(file: FileString = #file, line: UInt = #line, customError /// `unwrapa` will return the result of the expression if it is non-nil, and throw an error if the value is nil. /// if a `customError` is given, then that will be thrown. Otherwise, a ``RequireError`` will be thrown. @discardableResult -public func unwrapa(file: FileString = #file, line: UInt = #line, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { - try await requirea(file: file, line: line, customError: customError, expression()).toNot(beNil()) +public func unwrapa(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, customError: Error? = nil, _ expression: @autoclosure () -> (() async throws -> T?)) async throws -> T { + try await requirea(fileID: fileID, file: file, line: line, column: column, customError: customError, expression()).toNot(beNil()) } diff --git a/Sources/Nimble/DSL+Wait.swift b/Sources/Nimble/DSL+Wait.swift index 7fc5089e8..5bd0d8ae8 100644 --- a/Sources/Nimble/DSL+Wait.swift +++ b/Sources/Nimble/DSL+Wait.swift @@ -19,8 +19,10 @@ public class NMBWait: NSObject { @objc public class func until( timeout: TimeInterval, - file: FileString = #file, + fileID: String = #fileID, + file: FileString = #filePath, line: UInt = #line, + column: UInt = #column, action: @escaping (@escaping () -> Void) -> Void) { // Convert TimeInterval to NimbleTimeInterval until(timeout: timeout.nimbleInterval, file: file, line: line, action: action) @@ -29,8 +31,10 @@ public class NMBWait: NSObject { public class func until( timeout: NimbleTimeInterval, - file: FileString = #file, + fileID: String = #fileID, + file: FileString = #filePath, line: UInt = #line, + column: UInt = #column, action: @escaping (@escaping () -> Void) -> Void) { return throwableUntil(timeout: timeout, file: file, line: line) { done in action(done) @@ -40,8 +44,10 @@ public class NMBWait: NSObject { // Using a throwable closure makes this method not objc compatible. public class func throwableUntil( timeout: NimbleTimeInterval, - file: FileString = #file, + fileID: String = #fileID, + file: FileString = #filePath, line: UInt = #line, + column: UInt = #column, action: @escaping (@escaping () -> Void) throws -> Void) { let awaiter = NimbleEnvironment.activeInstance.awaiter let leeway = timeout.divided @@ -63,7 +69,10 @@ public class NMBWait: NSObject { } } } - }.timeout(timeout, forcefullyAbortTimeout: leeway).wait("waitUntil(...)", file: file, line: line) + }.timeout(timeout, forcefullyAbortTimeout: leeway).wait( + "waitUntil(...)", + sourceLocation: SourceLocation(fileID: fileID, filePath: file, line: line, column: column) + ) switch result { case .incomplete: internalError("Reached .incomplete state for waitUntil(...).") @@ -86,19 +95,23 @@ public class NMBWait: NSObject { } #if canImport(Darwin) - @objc(untilFile:line:action:) + @objc(untilFileID:file:line:column:action:) public class func until( - _ file: FileString = #file, + _ fileID: String = #fileID, + file: FileString = #filePath, line: UInt = #line, + column: UInt = #column, action: @escaping (@escaping () -> Void) -> Void) { - until(timeout: .seconds(1), file: file, line: line, action: action) + until(timeout: .seconds(1), fileID: fileID, file: file, line: line, column: column, action: action) } #else public class func until( - _ file: FileString = #file, + _ fileID: String = #fileID, + file: FileString = #filePath, line: UInt = #line, + column: UInt = #column, action: @escaping (@escaping () -> Void) -> Void) { - until(timeout: .seconds(1), file: file, line: line, action: action) + until(timeout: .seconds(1), fileID: fileID, file: file, line: line, column: column, action: action) } #endif } @@ -116,8 +129,8 @@ internal func blockedRunLoopErrorMessageFor(_ fnName: String, leeway: NimbleTime /// This function manages the main run loop (`NSRunLoop.mainRunLoop()`) while this function /// is executing. Any attempts to touch the run loop may cause non-deterministic behavior. @available(*, noasync, message: "the sync variant of `waitUntil` does not work in async contexts. Use the async variant as a drop-in replacement") -public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, file: FileString = #file, line: UInt = #line, action: @escaping (@escaping () -> Void) -> Void) { - NMBWait.until(timeout: timeout, file: file, line: line, action: action) +public func waitUntil(timeout: NimbleTimeInterval = PollingDefaults.timeout, fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, action: @escaping (@escaping () -> Void) -> Void) { + NMBWait.until(timeout: timeout, fileID: fileID, file: file, line: line, column: column, action: action) } #endif // #if !os(WASI) diff --git a/Sources/Nimble/DSL.swift b/Sources/Nimble/DSL.swift index d61ac60d5..f199425d9 100644 --- a/Sources/Nimble/DSL.swift +++ b/Sources/Nimble/DSL.swift @@ -1,76 +1,76 @@ /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping () throws -> T?) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> T)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. -public func expect(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncExpectation { +public func expect(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make a ``SyncExpectation`` on a given actual value. The value given is lazily evaluated. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure @escaping () throws -> T?) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure @escaping () throws -> T?) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression, - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> T)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> T?)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } /// Make a ``SyncExpectation`` on a given actual value. The closure is lazily invoked. /// This is provided as an alternative to `expect` which avoids overloading with `expect -> AsyncExpectation`. -public func expects(file: FileString = #file, line: UInt = #line, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncExpectation { +public func expects(fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column, _ expression: @autoclosure () -> (() throws -> Void)) -> SyncExpectation { return SyncExpectation( expression: Expression( expression: expression(), - location: SourceLocation(file: file, line: line), + location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column), isClosure: true)) } @@ -81,13 +81,13 @@ public func fail(_ message: String, location: SourceLocation) { } /// Always fails the test with a message. -public func fail(_ message: String, file: FileString = #file, line: UInt = #line) { - fail(message, location: SourceLocation(file: file, line: line)) +public func fail(_ message: String, fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column) { + fail(message, location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column)) } /// Always fails the test. -public func fail(_ file: FileString = #file, line: UInt = #line) { - fail("fail() always fails", file: file, line: line) +public func fail(_ fileID: String = #fileID, file: FileString = #filePath, line: UInt = #line, column: UInt = #column) { + fail("fail() always fails", location: SourceLocation(fileID: fileID, filePath: file, line: line, column: column)) } /// Like Swift's precondition(), but raises NSExceptions instead of sigaborts @@ -95,7 +95,7 @@ internal func nimblePrecondition( _ expr: @autoclosure () -> Bool, _ name: @autoclosure () -> String, _ message: @autoclosure () -> String, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) { let result = expr() @@ -104,7 +104,7 @@ internal func nimblePrecondition( } } -internal func internalError(_ msg: String, file: FileString = #file, line: UInt = #line) -> Never { +internal func internalError(_ msg: String, file: FileString = #filePath, line: UInt = #line) -> Never { fatalError( """ Nimble Bug Found: \(msg) at \(file):\(line). diff --git a/Sources/Nimble/Polling+AsyncAwait.swift b/Sources/Nimble/Polling+AsyncAwait.swift index 6adb153c0..2238fb425 100644 --- a/Sources/Nimble/Polling+AsyncAwait.swift +++ b/Sources/Nimble/Polling+AsyncAwait.swift @@ -38,8 +38,7 @@ internal actor Poller { let result = await pollBlock( pollInterval: poll, timeoutInterval: timeout, - file: expression.location.file, - line: expression.location.line, + sourceLocation: expression.location, fnName: fnName) { if self.updateMatcherResult(result: try await matcherRunner()) .toBoolean(expectation: style) { diff --git a/Sources/Nimble/Polling.swift b/Sources/Nimble/Polling.swift index abb64e94e..c74facb61 100644 --- a/Sources/Nimble/Polling.swift +++ b/Sources/Nimble/Polling.swift @@ -95,8 +95,7 @@ internal func poll( let result = pollBlock( pollInterval: poll, timeoutInterval: timeout, - file: actualExpression.location.file, - line: actualExpression.location.line, + sourceLocation: actualExpression.location, fnName: fnName) { lastMatcherResult = try matcher.satisfies(uncachedExpression) if lastMatcherResult!.toBoolean(expectation: style) { diff --git a/Sources/Nimble/Utils/AsyncAwait.swift b/Sources/Nimble/Utils/AsyncAwait.swift index 1e5884370..141662259 100644 --- a/Sources/Nimble/Utils/AsyncAwait.swift +++ b/Sources/Nimble/Utils/AsyncAwait.swift @@ -198,13 +198,14 @@ private func runPoller( timeoutInterval: NimbleTimeInterval, pollInterval: NimbleTimeInterval, awaiter: Awaiter, - fnName: String = #function, file: FileString = #file, line: UInt = #line, + fnName: String, + sourceLocation: SourceLocation, expression: @escaping () async throws -> PollStatus ) async -> AsyncPollResult { awaiter.waitLock.acquireWaitingLock( fnName, - file: file, - line: line) + sourceLocation: sourceLocation + ) defer { awaiter.waitLock.releaseWaitingLock() @@ -257,7 +258,7 @@ private func runAwaitTrigger( awaiter: Awaiter, timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, - file: FileString, line: UInt, + sourceLocation: SourceLocation, _ closure: @escaping (@escaping (T) -> Void) async throws -> Void ) async -> AsyncPollResult { let timeoutQueue = awaiter.timeoutQueue @@ -283,8 +284,13 @@ private func runAwaitTrigger( if completionCount.value < 2 { promise.send(result) } else { - fail("waitUntil(..) expects its completion closure to be only called once", - file: file, line: line) + fail( + "waitUntil(..) expects its completion closure to be only called once", + fileID: sourceLocation.fileID, + file: sourceLocation.filePath, + line: sourceLocation.line, + column: sourceLocation.column + ) } } if let value = await promise.value { @@ -308,27 +314,29 @@ private func runAwaitTrigger( internal func performBlock( timeoutInterval: NimbleTimeInterval, leeway: NimbleTimeInterval, - file: FileString, line: UInt, + sourceLocation: SourceLocation, _ closure: @escaping (@escaping (T) -> Void) async throws -> Void ) async -> AsyncPollResult { await runAwaitTrigger( awaiter: NimbleEnvironment.activeInstance.awaiter, timeoutInterval: timeoutInterval, leeway: leeway, - file: file, line: line, closure) + sourceLocation: sourceLocation, + closure) } internal func pollBlock( pollInterval: NimbleTimeInterval, timeoutInterval: NimbleTimeInterval, - file: FileString, - line: UInt, - fnName: String = #function, + sourceLocation: SourceLocation, + fnName: String, expression: @escaping () async throws -> PollStatus) async -> AsyncPollResult { await runPoller( timeoutInterval: timeoutInterval, pollInterval: pollInterval, awaiter: NimbleEnvironment.activeInstance.awaiter, + fnName: fnName, + sourceLocation: sourceLocation, expression: expression ) } diff --git a/Sources/Nimble/Utils/PollAwait.swift b/Sources/Nimble/Utils/PollAwait.swift index 9c045c464..1bc1311ba 100644 --- a/Sources/Nimble/Utils/PollAwait.swift +++ b/Sources/Nimble/Utils/PollAwait.swift @@ -12,16 +12,15 @@ private let pollLeeway = NimbleTimeInterval.milliseconds(1) /// Stores debugging information about callers internal struct WaitingInfo: CustomStringConvertible, Sendable { let name: String - let file: FileString - let lineNumber: UInt + let sourceLocation: SourceLocation var description: String { - return "\(name) at \(file):\(lineNumber)" + return "\(name) at \(sourceLocation)" } } internal protocol WaitLock { - func acquireWaitingLock(_ fnName: String, file: FileString, line: UInt) + func acquireWaitingLock(_ fnName: String, sourceLocation: SourceLocation) func releaseWaitingLock() func isWaitingLocked() -> Bool } @@ -32,10 +31,10 @@ internal final class AssertionWaitLock: WaitLock, @unchecked Sendable { init() { } - func acquireWaitingLock(_ fnName: String, file: FileString, line: UInt) { + func acquireWaitingLock(_ fnName: String, sourceLocation: SourceLocation) { lock.lock() defer { lock.unlock() } - let info = WaitingInfo(name: fnName, file: file, lineNumber: line) + let info = WaitingInfo(name: fnName, sourceLocation: sourceLocation) nimblePrecondition( currentWaiter == nil, "InvalidNimbleAPIUsage", @@ -263,11 +262,11 @@ internal class AwaitPromiseBuilder { /// - The async expectation raised an unexpected error (swift) /// /// The returned PollResult will NEVER be .incomplete. - func wait(_ fnName: String = #function, file: FileString = #file, line: UInt = #line) -> PollResult { + func wait(_ fnName: String = #function, sourceLocation: SourceLocation) -> PollResult { waitLock.acquireWaitingLock( fnName, - file: file, - line: line) + sourceLocation: sourceLocation + ) let capture = NMBExceptionCapture(handler: ({ exception in _ = self.promise.resolveResult(.raisedException(exception)) @@ -401,8 +400,7 @@ internal class Awaiter { internal func pollBlock( pollInterval: NimbleTimeInterval, timeoutInterval: NimbleTimeInterval, - file: FileString, - line: UInt, + sourceLocation: SourceLocation, fnName: String = #function, expression: @escaping () throws -> PollStatus) -> PollResult { let awaiter = NimbleEnvironment.activeInstance.awaiter @@ -413,7 +411,7 @@ internal func pollBlock( return nil } .timeout(timeoutInterval, forcefullyAbortTimeout: timeoutInterval.divided) - .wait(fnName, file: file, line: line) + .wait(fnName, sourceLocation: sourceLocation) return result } diff --git a/Sources/Nimble/Utils/SourceLocation.swift b/Sources/Nimble/Utils/SourceLocation.swift index 64838ffe9..557e11219 100644 --- a/Sources/Nimble/Utils/SourceLocation.swift +++ b/Sources/Nimble/Utils/SourceLocation.swift @@ -11,21 +11,29 @@ public typealias FileString = StaticString public typealias FileString = String #endif -public final class SourceLocation: NSObject { - public let file: FileString +public final class SourceLocation: NSObject, Sendable { + public let fileID: String + @available(*, deprecated, renamed: "filePath") + public var file: FileString { filePath } + public let filePath: FileString public let line: UInt + public let column: UInt override init() { - file = "Unknown File" + fileID = "Unknown/File" + filePath = "Unknown File" line = 0 + column = 0 } - init(file: FileString, line: UInt) { - self.file = file + init(fileID: String, filePath: FileString, line: UInt, column: UInt) { + self.fileID = fileID + self.filePath = filePath self.line = line + self.column = column } override public var description: String { - return "\(file):\(line)" + return "\(filePath):\(line):\(column)" } } diff --git a/Sources/NimbleObjectiveC/DSL.m b/Sources/NimbleObjectiveC/DSL.m index 2ab37efe3..1aad14e36 100644 --- a/Sources/NimbleObjectiveC/DSL.m +++ b/Sources/NimbleObjectiveC/DSL.m @@ -154,13 +154,22 @@ NIMBLE_EXPORT void NMB_failWithMessage(NSString *msg, NSString *file, NSUInteger NIMBLE_EXPORT NMBWaitUntilTimeoutBlock NMB_waitUntilTimeoutBuilder(NSString *file, NSUInteger line) { return ^(NSTimeInterval timeout, void (^ _Nonnull action)(void (^ _Nonnull)(void))) { - [NMBWait untilTimeout:timeout file:file line:line action:action]; + [NMBWait untilTimeout:timeout + fileID:[NSString stringWithFormat:@"Unknown/%@", file] + file:file + line:line + column:0 + action:action]; }; } NIMBLE_EXPORT NMBWaitUntilBlock NMB_waitUntilBuilder(NSString *file, NSUInteger line) { return ^(void (^ _Nonnull action)(void (^ _Nonnull)(void))) { - [NMBWait untilFile:file line:line action:action]; + [NMBWait untilFileID:[NSString stringWithFormat:@"Unknown/%@", file] + file:file + line:line + column:0 + action:action]; }; } diff --git a/Sources/NimbleSharedTestHelpers/utils.swift b/Sources/NimbleSharedTestHelpers/utils.swift index ae234c88f..65908144d 100644 --- a/Sources/NimbleSharedTestHelpers/utils.swift +++ b/Sources/NimbleSharedTestHelpers/utils.swift @@ -8,12 +8,19 @@ import Foundation #endif import XCTest -public func failsWithErrorMessage(_ messages: [String], file: FileString = #file, line: UInt = #line, preferOriginalSourceLocation: Bool = false, closure: () throws -> Void) { - var filePath = file - var lineNumber = line +public func failsWithErrorMessage( + _ messages: [String], + fileID: String = #fileID, + filePath: FileString = #filePath, + line: UInt = #line, + column: UInt = #column, + preferOriginalSourceLocation: Bool = false, + closure: () throws -> Void +) { + var location = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column) let recorder = AssertionRecorder() - withAssertionHandler(recorder, file: file, line: line, closure: closure) + withAssertionHandler(recorder, fileID: fileID, file: filePath, line: line, column: column, closure: closure) for msg in messages { var lastFailure: AssertionRecord? @@ -31,8 +38,7 @@ public func failsWithErrorMessage(_ messages: [String], file: FileString = #file if preferOriginalSourceLocation { if let failure = lastFailure { - filePath = failure.location.file - lineNumber = failure.location.line + location = failure.location } } @@ -51,14 +57,21 @@ public func failsWithErrorMessage(_ messages: [String], file: FileString = #file } NimbleAssertionHandler.assert(false, message: FailureMessage(stringValue: message), - location: SourceLocation(file: filePath, line: lineNumber)) + location: location) } } // Verifies that the error message matches the given regex. -public func failsWithErrorRegex(_ regex: String, file: FileString = #file, line: UInt = #line, closure: () throws -> Void) { +public func failsWithErrorRegex( + _ regex: String, + fileID: String = #fileID, + filePath: FileString = #filePath, + line: UInt = #line, + column: UInt = #column, + closure: () throws -> Void +) { let recorder = AssertionRecorder() - withAssertionHandler(recorder, file: file, line: line, closure: closure) + withAssertionHandler(recorder, fileID: fileID, file: filePath, line: line, column: column, closure: closure) for assertion in recorder.assertions where assertion.message.stringValue.range(of: regex, options: .regularExpression) != nil && !assertion.success { return @@ -74,35 +87,46 @@ public func failsWithErrorRegex(_ regex: String, file: FileString = #file, line: """ NimbleAssertionHandler.assert(false, message: FailureMessage(stringValue: message), - location: SourceLocation(file: file, line: line)) + location: SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column)) } -public func failsWithErrorMessage(_ message: String, file: FileString = #file, line: UInt = #line, preferOriginalSourceLocation: Bool = false, closure: () throws -> Void) { +public func failsWithErrorMessage( + _ message: String, + fileID: String = #fileID, + filePath: FileString = #filePath, + line: UInt = #line, + column: UInt = #column, + preferOriginalSourceLocation: Bool = false, + closure: () throws -> Void +) { failsWithErrorMessage( [message], - file: file, + fileID: fileID, + filePath: filePath, line: line, + column: column, preferOriginalSourceLocation: preferOriginalSourceLocation, closure: closure ) } -public func failsWithErrorMessageForNil(_ message: String, file: FileString = #file, line: UInt = #line, preferOriginalSourceLocation: Bool = false, closure: () throws -> Void) { +public func failsWithErrorMessageForNil(_ message: String, fileID: String = #fileID, filePath: FileString = #filePath, line: UInt = #line, column: UInt = #column, preferOriginalSourceLocation: Bool = false, closure: () throws -> Void) { failsWithErrorMessage( "\(message) (use beNil() to match nils)", - file: file, + fileID: fileID, + filePath: filePath, line: line, + column: column, preferOriginalSourceLocation: preferOriginalSourceLocation, closure: closure ) } -public func failsWithErrorMessage(_ messages: [String], file: FileString = #file, line: UInt = #line, preferOriginalSourceLocation: Bool = false, closure: () async throws -> Void) async { - var filePath = file - var lineNumber = line +public func failsWithErrorMessage(_ messages: [String], fileID: String = #fileID, filePath: FileString = #filePath, line: UInt = #line, column: UInt = #column, preferOriginalSourceLocation: Bool = false, closure: () async throws -> Void) async { + var sourceLocation = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column) let recorder = AssertionRecorder() - await withAssertionHandler(recorder, file: file, line: line, closure: closure) + await withAssertionHandler(recorder, fileID: fileID, file: filePath, line: line, column: column, closure: closure) for msg in messages { var lastFailure: AssertionRecord? @@ -120,8 +144,7 @@ public func failsWithErrorMessage(_ messages: [String], file: FileString = #file if preferOriginalSourceLocation { if let failure = lastFailure { - filePath = failure.location.file - lineNumber = failure.location.line + sourceLocation = failure.location } } @@ -140,25 +163,29 @@ public func failsWithErrorMessage(_ messages: [String], file: FileString = #file } NimbleAssertionHandler.assert(false, message: FailureMessage(stringValue: message), - location: SourceLocation(file: filePath, line: lineNumber)) + location: sourceLocation) } } -public func failsWithErrorMessage(_ message: String, file: FileString = #file, line: UInt = #line, preferOriginalSourceLocation: Bool = false, closure: () async throws -> Void) async { +public func failsWithErrorMessage(_ message: String, fileID: String = #fileID, filePath: FileString = #filePath, line: UInt = #line, column: UInt = #column, preferOriginalSourceLocation: Bool = false, closure: () async throws -> Void) async { await failsWithErrorMessage( [message], - file: file, + fileID: fileID, + filePath: filePath, line: line, + column: column, preferOriginalSourceLocation: preferOriginalSourceLocation, closure: closure ) } -public func failsWithErrorMessageForNil(_ message: String, file: FileString = #file, line: UInt = #line, preferOriginalSourceLocation: Bool = false, closure: () async throws -> Void) async { +public func failsWithErrorMessageForNil(_ message: String, fileID: String = #fileID, filePath: FileString = #filePath, line: UInt = #line, column: UInt = #column, preferOriginalSourceLocation: Bool = false, closure: () async throws -> Void) async { await failsWithErrorMessage( "\(message) (use beNil() to match nils)", - file: file, + fileID: fileID, + filePath: filePath, line: line, + column: column, preferOriginalSourceLocation: preferOriginalSourceLocation, closure: closure ) @@ -174,16 +201,16 @@ public func suppressErrors(closure: () -> T) -> T { return output! } -public func producesStatus(_ status: ExpectationStatus, file: FileString = #file, line: UInt = #line, closure: () -> SyncExpectation) { +public func producesStatus(_ status: ExpectationStatus, fileID: String = #fileID, filePath: FileString = #filePath, line: UInt = #line, column: UInt = #column, closure: () -> SyncExpectation) { let expectation = suppressErrors(closure: closure) - expect(file: file, line: line, expectation.status).to(equal(status)) + expect(fileID: fileID, file: filePath, line: line, column: column, expectation.status).to(equal(status)) } -public func producesStatus(_ status: ExpectationStatus, file: FileString = #file, line: UInt = #line, closure: () -> AsyncExpectation) { +public func producesStatus(_ status: ExpectationStatus, fileID: String = #fileID, filePath: FileString = #filePath, line: UInt = #line, column: UInt = #column, closure: () -> AsyncExpectation) { let expectation = suppressErrors(closure: closure) - expect(file: file, line: line, expectation.status).to(equal(status)) + expect(fileID: fileID, file: filePath, line: line, column: column, expectation.status).to(equal(status)) } #if !os(WASI) @@ -198,19 +225,43 @@ public func deferToMainQueue(action: @escaping () -> Void) { #if canImport(Darwin) public class NimbleHelper: NSObject { @objc public class func expectFailureMessage(_ message: NSString, block: () -> Void, file: FileString, line: UInt) { - failsWithErrorMessage(String(describing: message), file: file, line: line, preferOriginalSourceLocation: true, closure: block) + failsWithErrorMessage( + String(describing: message), + fileID: "Unknown/\(file)", + filePath: file, + line: line, + column: 0, + preferOriginalSourceLocation: true, + closure: block + ) } @objc public class func expectFailureMessages(_ messages: [NSString], block: () -> Void, file: FileString, line: UInt) { - failsWithErrorMessage(messages.map({String(describing: $0)}), file: file, line: line, preferOriginalSourceLocation: true, closure: block) + failsWithErrorMessage( + messages.map({String(describing: $0)}), + fileID: "Unknown/\(file)", + filePath: file, + line: line, + column: 0, + preferOriginalSourceLocation: true, + closure: block + ) } @objc public class func expectFailureMessageForNil(_ message: NSString, block: () -> Void, file: FileString, line: UInt) { - failsWithErrorMessageForNil(String(describing: message), file: file, line: line, preferOriginalSourceLocation: true, closure: block) + failsWithErrorMessageForNil( + String(describing: message), + fileID: "Unknown/\(file)", + filePath: file, + line: line, + column: 0, + preferOriginalSourceLocation: true, + closure: block + ) } @objc public class func expectFailureMessageRegex(_ regex: NSString, block: () -> Void, file: FileString, line: UInt) { - + fail("erp!") } } #endif diff --git a/Tests/NimbleTests/SwiftTestingSupportTest.swift b/Tests/NimbleTests/SwiftTestingSupportTest.swift new file mode 100644 index 000000000..39e5b917e --- /dev/null +++ b/Tests/NimbleTests/SwiftTestingSupportTest.swift @@ -0,0 +1,33 @@ +#if canImport(Testing) +import Nimble +import Testing +import XCTest + +@Suite struct SwiftTestingSupportSuite { + @Test func reportsAssertionFailuresToSwiftTesting() { + withKnownIssue { + fail() + } + } + + @Test func reportsRequireErrorsToSwiftTesting() throws { + withKnownIssue { + try require(false).to(beTrue()) + } + } +} + +class MixedSwiftTestingXCTestSupport: XCTestCase { + func testAlsoRecordsErrorsToXCTest() { + XCTExpectFailure("This should fail") + fail() + + } + + func testAlsoRecordsRequireErrorsToXCTest() throws { + XCTExpectFailure("This should fail") + try require(false).to(beTrue()) + } +} + +#endif