From cff1761e0fd42a6f466c03c9ac70727474856657 Mon Sep 17 00:00:00 2001 From: surpher Date: Sat, 5 Aug 2023 22:34:04 +1000 Subject: [PATCH] Resolve warnings for Xcode 14+ --- .swiftlint.yml | 13 +- .../Target-macOS-Tests-Shared.xcconfig | 2 +- PactSwift.xcfilelist | 2 + PactSwift.xcodeproj/project.pbxproj | 20 +- Scripts/build_file_list_and_swiftlint | 2 +- Sources/MockService+Concurrency.swift | 141 ++++++++++++ Sources/MockService+Extension.swift | 96 ++++++++ Sources/MockService.swift | 207 +----------------- Tests/Model/AnyEncodableTests.swift | 4 +- Tests/Model/PactBuilderTests.swift | 14 +- Tests/Model/PactTests.swift | 2 +- Tests/Services/MockServiceTests.swift | 16 +- Tests/Services/PactContractTests.swift | 18 +- 13 files changed, 296 insertions(+), 241 deletions(-) create mode 100644 Sources/MockService+Concurrency.swift create mode 100644 Sources/MockService+Extension.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 04668a0d..7e1fdcd0 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,6 +3,9 @@ disabled_rules: - identifier_name - large_tuple - operator_whitespace + - unused_declaration + - unused_import + opt_in_rules: - closure_body_length - closure_end_indentation @@ -46,13 +49,12 @@ opt_in_rules: - toggle_bool - trailing_whitespace - unneeded_parentheses_in_closure_argument - - unused_declaration - - unused_import - yoda_condition - xct_specific_matcher -attributes: - always_on_same_line: ["@IBAction", "@objc"] +analyzer_rules: + - unused_declaration + - unused_import line_length: warning: 180 @@ -65,6 +67,9 @@ modifier_order: nesting: type_level: 2 +included: + - Sources + excluded: - Carthage - Tests diff --git a/Configurations/Target-macOS-Tests-Shared.xcconfig b/Configurations/Target-macOS-Tests-Shared.xcconfig index 466da18f..8efbbe40 100644 --- a/Configurations/Target-macOS-Tests-Shared.xcconfig +++ b/Configurations/Target-macOS-Tests-Shared.xcconfig @@ -1,6 +1,6 @@ //:configuration = Debug SDKROOT = macosx -MACOSX_DEPLOYMENT_TARGET = 10.15 +MACOSX_DEPLOYMENT_TARGET = 11.0 CODE_SIGN_STYLE = Automatic COMBINE_HIDPI_IMAGES = YES diff --git a/PactSwift.xcfilelist b/PactSwift.xcfilelist index 7c335172..9b5eb9a4 100644 --- a/PactSwift.xcfilelist +++ b/PactSwift.xcfilelist @@ -1,3 +1,5 @@ +$(SRCROOT)/Sources/MockService+Concurrency.swift +$(SRCROOT)/Sources/MockService+Extension.swift $(SRCROOT)/Sources/Extensions/Bundle+PactSwift.swift $(SRCROOT)/Sources/Extensions/Task+Timeout.swift $(SRCROOT)/Sources/Extensions/Date+PactSwift.swift diff --git a/PactSwift.xcodeproj/project.pbxproj b/PactSwift.xcodeproj/project.pbxproj index 4a1d102b..71ca3032 100644 --- a/PactSwift.xcodeproj/project.pbxproj +++ b/PactSwift.xcodeproj/project.pbxproj @@ -70,6 +70,10 @@ AD78FB49264FD21900765BD3 /* PactContractTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD78FB47264FD21900765BD3 /* PactContractTests.swift */; }; AD8546F42513601800211E28 /* RandomDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8546F32513601800211E28 /* RandomDateTests.swift */; }; AD8546F52513601800211E28 /* RandomDateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8546F32513601800211E28 /* RandomDateTests.swift */; }; + AD854D882A7E75080005C502 /* MockService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D872A7E75080005C502 /* MockService+Extension.swift */; }; + AD854D892A7E75080005C502 /* MockService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D872A7E75080005C502 /* MockService+Extension.swift */; }; + AD854D8B2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */; }; + AD854D8C2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */; }; AD879B9D258242AC00F85B0B /* PactSwiftVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD879B9C258242AC00F85B0B /* PactSwiftVersion.swift */; }; AD879B9E258242AC00F85B0B /* PactSwiftVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD879B9C258242AC00F85B0B /* PactSwiftVersion.swift */; }; AD881808242C715B00BF510D /* PactSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AD8817FE242C715A00BF510D /* PactSwift.framework */; }; @@ -311,6 +315,8 @@ AD78FB47264FD21900765BD3 /* PactContractTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactContractTests.swift; sourceTree = ""; }; AD8546F025135F4200211E28 /* RandomDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDate.swift; sourceTree = ""; }; AD8546F32513601800211E28 /* RandomDateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomDateTests.swift; sourceTree = ""; }; + AD854D872A7E75080005C502 /* MockService+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockService+Extension.swift"; sourceTree = ""; }; + AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockService+Concurrency.swift"; sourceTree = ""; }; AD879B9C258242AC00F85B0B /* PactSwiftVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PactSwiftVersion.swift; sourceTree = ""; }; AD8817FE242C715A00BF510D /* PactSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PactSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AD881801242C715A00BF510D /* PactSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PactSwift.h; sourceTree = ""; }; @@ -460,13 +466,6 @@ path = Matchers; sourceTree = ""; }; - AD1AE4BA26BBC956001E09E2 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; AD641A362434354C00785CE1 /* Model */ = { isa = PBXGroup; children = ( @@ -546,7 +545,6 @@ ADD6993D242C861600C5C2C2 /* .swiftlint.yml */, ADCB2562242C83E20048BD18 /* Configurations */, ADC6FAE024302BA800026714 /* Documentation */, - AD1AE4BA26BBC956001E09E2 /* Frameworks */, ADF2E582267462090029507D /* Package.swift */, AD8817FF242C715A00BF510D /* Products */, AD8A32D6242CCF5600283080 /* Scripts */, @@ -633,6 +631,8 @@ ADCB2563242C844B0048BD18 /* Headers */, AD01C9392432A31F00B75C9D /* Matchers */, AD10361324468AB3002C97CA /* MockService.swift */, + AD854D8A2A7E75E20005C502 /* MockService+Concurrency.swift */, + AD854D872A7E75080005C502 /* MockService+Extension.swift */, ADB7C167243327E300A16CDE /* Model */, AD8DF434243C53750062CB1A /* PactBuilder.swift */, ADF17CB926B5019B008E7ECD /* PFMockService.swift */, @@ -1087,6 +1087,7 @@ buildActionMask = 2147483647; files = ( AD54435E27D32DCA00D4C464 /* DateTimeExpression.swift in Sources */, + AD854D8B2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */, AD641A3C24344ED400785CE1 /* Pacticipant.swift in Sources */, ADA17E41251377A4004F1A82 /* Date+PactSwift.swift in Sources */, AD879B9D258242AC00F85B0B /* PactSwiftVersion.swift in Sources */, @@ -1133,6 +1134,7 @@ AD4B97102513A3DB00C37560 /* RandomString.swift in Sources */, AD8DF439243EEBDB0062CB1A /* SomethingLike.swift in Sources */, AD9D7D9026C8B3DA00FE4137 /* FromProviderState.swift in Sources */, + AD854D882A7E75080005C502 /* MockService+Extension.swift in Sources */, ADD3C27424416CDF002E73B9 /* EqualTo.swift in Sources */, ADA4B1E426D31EDA00A5AE88 /* VersionSelector.swift in Sources */, ADDE4704244D101700E4F7EE /* ErrorReportable.swift in Sources */, @@ -1215,6 +1217,7 @@ buildActionMask = 2147483647; files = ( AD54435F27D32DCA00D4C464 /* DateTimeExpression.swift in Sources */, + AD854D8C2A7E75E20005C502 /* MockService+Concurrency.swift in Sources */, AD8FC7E42463BB9F00361854 /* Interaction.swift in Sources */, ADA17E42251377A4004F1A82 /* Date+PactSwift.swift in Sources */, AD879B9E258242AC00F85B0B /* PactSwiftVersion.swift in Sources */, @@ -1261,6 +1264,7 @@ AD4B97112513A3DB00C37560 /* RandomString.swift in Sources */, AD8FC7ED2463BB9F00361854 /* Response.swift in Sources */, AD9D7D9126C8B3DA00FE4137 /* FromProviderState.swift in Sources */, + AD854D892A7E75080005C502 /* MockService+Extension.swift in Sources */, AD8FC7F22463BBB800361854 /* DecimalLike.swift in Sources */, ADA4B1E526D31EDA00A5AE88 /* VersionSelector.swift in Sources */, AD8FC7EB2463BB9F00361854 /* ProviderState.swift in Sources */, diff --git a/Scripts/build_file_list_and_swiftlint b/Scripts/build_file_list_and_swiftlint index b3587c40..398c6da5 100755 --- a/Scripts/build_file_list_and_swiftlint +++ b/Scripts/build_file_list_and_swiftlint @@ -41,7 +41,7 @@ if which swiftlint >/dev/null; then # Run swiftlint (TODO: - swiftlint by iterating through the $1.xcfilelist) # swiftlint --config $2 -- #filename0 #filename1 #filename2 ... - swiftlint --path Sources --config $2 + swiftlint --config $2 # Output an empty derived file touch $DERIVED_FILE_DIR/swiftlint.txt diff --git a/Sources/MockService+Concurrency.swift b/Sources/MockService+Concurrency.swift new file mode 100644 index 00000000..29407402 --- /dev/null +++ b/Sources/MockService+Concurrency.swift @@ -0,0 +1,141 @@ +// +// Created by Marko Justinek on 5/8/2023. +// Copyright © 2023 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation + +#if canImport(_Concurrency) && compiler(>=5.7) +@_implementationOnly import PactSwiftMockServer + +public extension MockService { + + /// Runs the Pact test against the code making the API request + /// + /// - Parameters: + /// - file: The file to report the failing test in + /// - line: The line on which to report the failing test + /// - verify: An array of specific `Interaction`s to verify. If none provided, the latest defined interaction is used + /// - timeout: Time before the test times out. Default is 10 seconds + /// - testFunction: Your async code making the API request + /// + /// The `testFunction` closure is passed a `String` representing the url of the active Mock Server. + /// + /// ``` + /// try await mockService.run { baseURL in + /// // async code making the request with provided `baseURL` + /// // assert response can be processed + /// } + /// ``` + /// + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func run(_ file: FileString? = #file, line: UInt? = #line, verify interactions: [Interaction]? = nil, timeout: TimeInterval? = nil, testFunction: @escaping @Sendable (_ baseURL: String) async throws -> Void) async throws { + // Use the provided set or if not provided only the current interaction + pact.interactions = interactions ?? [currentInteraction] + + if checkForInvalidInteractions(pact.interactions, file: file, line: line) { + // Remove interactions with errors + pact.interactions.removeAll { $0.encodingErrors.isEmpty == false } + self.interactions.removeAll() + } else { + // Prepare a brand spanking new MockServer (Mock Provider) on its own port + let mockServer = MockServer() + + // Set the expectations so we don't wait for this async magic indefinitely + try await setupPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer, testFunction: testFunction) + + // At the same time start listening to verification that Mock Server received the expected request + try await verifyPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer) + } + } +} + +// MARK: - Internal + +extension MockService { + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func setupPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer, testFunction: @escaping @Sendable (String) async throws -> Void) async throws { + Logger.log(message: "Setting up pact test", data: pact.data) + do { + // Set up a Mock Server with Pact data and on desired http protocol + try await mockServer.setup(pact: pact.data!, protocol: transferProtocolScheme) + + // If Mock Server spun up, run the test function + let task = Task(timeout: timeout) { + try await testFunction(mockServer.baseUrl) + } + // await task completion (value is Void) + try await task.value + } catch { + // Failed to spin up a Mock Server. This could be due to bad Pact data. Most likely to Pact data. + failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) + throw error + } + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func verifyPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer) async throws { + do { + let task = Task(timeout: timeout) { + try await mockServer.verify() + } + // await task completion (value is discarded) + _ = try await task.value + + // If the comsumer (in testFunction:) made the promised request to Mock Server, go and finalize the test. + // Only finalize when running in simulator or macOS. Running on a physical iOS device makes little sense due to + // writing a pact file to device's disk. `libpact_ffi` does the actual file writing it writes it onto the + // disk of the device it is being run on. + #if targetEnvironment(simulator) || os(macOS) + let message = try await finalize(file: file, line: line) + Logger.log(message: message, data: self.pact.data) + #else + print("[INFO]: Running on an iOS device. Writing Pact interaction into a contract skipped.") + #endif + } catch { + failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) + throw error + } + } + + /// Writes a Pact contract file in JSON format + /// + /// By default Pact contracts are written to `/tmp/pacts` folder. + /// Set `PACT_OUTPUT_DIR` to `$(PATH)/to/desired/dir/` in `Build` phase of your `Scheme` to change the location. + /// + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func finalize(file: FileString? = nil, line: UInt? = nil) async throws -> String { + // Spin up a fresh Mock Server with a directory to write to + let mockServer = MockServer(directory: pactsDirectory, merge: self.merge) + + // Gather all the interactions this MockService has received to set up and prepare Pact data with them all + pact.interactions = interactions.filter { $0.encodingErrors.isEmpty } + + // Validate the Pact `Data` is hunky dory + guard let pactData = pact.data else { + throw MockServerError.nullPointer + } + + // Ask Mock Server to do the actual Pact file writing to disk + do { + return try await mockServer.finalize(pact: pactData) + } catch { + failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) + throw error + } + } +} +#endif diff --git a/Sources/MockService+Extension.swift b/Sources/MockService+Extension.swift new file mode 100644 index 00000000..88b95cc7 --- /dev/null +++ b/Sources/MockService+Extension.swift @@ -0,0 +1,96 @@ +// +// Created by Marko Justinek on 5/8/2023. +// Copyright © 2023 Marko Justinek. All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +import Foundation +import XCTest + +#if compiler(>=5.5) +@_implementationOnly import PactSwiftMockServer +#else +import PactSwiftMockServer +#endif + +extension MockService { + + /// Check there are no invalid interactions + func checkForInvalidInteractions(_ interactions: [Interaction], file: FileString? = nil, line: UInt? = nil) -> Bool { + let errors = interactions.flatMap(\.encodingErrors) + for error in errors { + failWith(error.localizedDescription, file: file, line: line) + } + return errors.isEmpty == false + } + + /// Writes a Pact contract file in JSON format + /// + /// - parameter completion: Result of the writing the Pact contract to JSON + /// + /// By default Pact contracts are written to `/tmp/pacts` folder. + /// Set `PACT_OUTPUT_DIR` to `$(PATH)/to/desired/dir/` in `Build` phase of your `Scheme` to change the location. + /// + func finalize(file: FileString? = nil, line: UInt? = nil, completion: ((Result) -> Void)? = nil) { + // Spin up a fresh Mock Server with a directory to write to + let mockServer = MockServer(directory: pactsDirectory, merge: self.merge) + + // Gather all the interactions this MockService has received to set up and prepare Pact data with them all + pact.interactions = interactions.filter { $0.encodingErrors.isEmpty } + + // Validate the Pact `Data` is hunky dory + guard let pactData = pact.data else { + completion?(.failure(.nullPointer)) + return + } + + // Ask Mock Server to do the actual Pact file writing to disk + mockServer.finalize(pact: pactData) { [unowned self] in + switch $0 { + case .success(let message): + completion?(.success(message)) + case .failure(let error): + failWith(error.description) + completion?(.failure(error)) + } + } + } + + /// Waits for test to be completed and fails if timed out + func waitForPactTestWith(timeout: TimeInterval, file: FileString?, line: UInt?, action: (@escaping () -> Void) -> Void) { + let expectation = XCTestExpectation(description: "waitForPactTest") + action { + expectation.fulfill() + } + + let result = XCTWaiter().wait(for: [expectation], timeout: timeout) + if result != .completed { + let message = "Test did not complete within \(timeout) second timeout! Did you run testCompleted() block?" + if let file = file, let line = line { + errorReporter.reportFailure(message, file: file, line: line) + } else { + errorReporter.reportFailure(message) + } + } + } + + /// Fail the test and raise the failure in `file` at `line` + func failWith(_ message: String, file: FileString? = nil, line: UInt? = nil) { + if let file = file, let line = line { + errorReporter.reportFailure(message, file: file, line: line) + } else { + errorReporter.reportFailure(message) + } + } +} diff --git a/Sources/MockService.swift b/Sources/MockService.swift index e76d5971..f7f83cff 100644 --- a/Sources/MockService.swift +++ b/Sources/MockService.swift @@ -16,7 +16,6 @@ // import Foundation -import XCTest #if compiler(>=5.5) @_implementationOnly import PactSwiftMockServer @@ -33,14 +32,14 @@ open class MockService { // MARK: - Private properties - private var pact: Pact - private var interactions: [Interaction] = [] - private var currentInteraction: Interaction! - private let errorReporter: ErrorReportable - private let pactsDirectory: URL? - private let merge: Bool + var pact: Pact + var interactions: [Interaction] = [] + var currentInteraction: Interaction! + let errorReporter: ErrorReportable + let pactsDirectory: URL? + let merge: Bool - private var transferProtocolScheme: PactSwiftMockServer.TransferProtocol + var transferProtocolScheme: PactSwiftMockServer.TransferProtocol // MARK: - Initializers @@ -151,150 +150,6 @@ open class MockService { verifyPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer) } } - - #if canImport(_Concurrency) && compiler(>=5.7) - /// Runs the Pact test against the code making the API request - /// - /// - Parameters: - /// - file: The file to report the failing test in - /// - line: The line on which to report the failing test - /// - verify: An array of specific `Interaction`s to verify. If none provided, the latest defined interaction is used - /// - timeout: Time before the test times out. Default is 10 seconds - /// - testFunction: Your async code making the API request - /// - /// The `testFunction` closure is passed a `String` representing the url of the active Mock Server. - /// - /// ``` - /// try await mockService.run { baseURL in - /// // async code making the request with provided `baseURL` - /// // assert response can be processed - /// } - /// ``` - /// - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - public func run(_ file: FileString? = #file, line: UInt? = #line, verify interactions: [Interaction]? = nil, timeout: TimeInterval? = nil, testFunction: @escaping @Sendable (_ baseURL: String) async throws -> Void) async throws { - // Use the provided set or if not provided only the current interaction - pact.interactions = interactions ?? [currentInteraction] - - if checkForInvalidInteractions(pact.interactions, file: file, line: line) { - // Remove interactions with errors - pact.interactions.removeAll { $0.encodingErrors.isEmpty == false } - self.interactions.removeAll() - } else { - // Prepare a brand spanking new MockServer (Mock Provider) on its own port - let mockServer = MockServer() - - // Set the expectations so we don't wait for this async magic indefinitely - try await setupPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer, testFunction: testFunction) - - // At the same time start listening to verification that Mock Server received the expected request - try await verifyPactInteraction(timeout: timeout ?? Constants.kTimeout, file: file, line: line, mockServer: mockServer) - } - } - #endif - - /// Check there are no invalid interactions - private func checkForInvalidInteractions(_ interactions: [Interaction], file: FileString? = nil, line: UInt? = nil) -> Bool { - let errors = interactions.flatMap(\.encodingErrors) - for error in errors { - failWith(error.localizedDescription, file: file, line: line) - } - return errors.isEmpty == false - } -} - -// MARK: - Internal - -extension MockService { - - /// Writes a Pact contract file in JSON format - /// - /// - parameter completion: Result of the writing the Pact contract to JSON - /// - /// By default Pact contracts are written to `/tmp/pacts` folder. - /// Set `PACT_OUTPUT_DIR` to `$(PATH)/to/desired/dir/` in `Build` phase of your `Scheme` to change the location. - /// - func finalize(file: FileString? = nil, line: UInt? = nil, completion: ((Result) -> Void)? = nil) { - // Spin up a fresh Mock Server with a directory to write to - let mockServer = MockServer(directory: pactsDirectory, merge: self.merge) - - // Gather all the interactions this MockService has received to set up and prepare Pact data with them all - pact.interactions = interactions.filter { $0.encodingErrors.isEmpty } - - // Validate the Pact `Data` is hunky dory - guard let pactData = pact.data else { - completion?(.failure(.nullPointer)) - return - } - - // Ask Mock Server to do the actual Pact file writing to disk - mockServer.finalize(pact: pactData) { [unowned self] in - switch $0 { - case .success(let message): - completion?(.success(message)) - case .failure(let error): - failWith(error.description) - completion?(.failure(error)) - } - } - } - - #if canImport(_Concurrency) && compiler(>=5.7) - /// Writes a Pact contract file in JSON format - /// - /// By default Pact contracts are written to `/tmp/pacts` folder. - /// Set `PACT_OUTPUT_DIR` to `$(PATH)/to/desired/dir/` in `Build` phase of your `Scheme` to change the location. - /// - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - func finalize(file: FileString? = nil, line: UInt? = nil) async throws -> String { - // Spin up a fresh Mock Server with a directory to write to - let mockServer = MockServer(directory: pactsDirectory, merge: self.merge) - - // Gather all the interactions this MockService has received to set up and prepare Pact data with them all - pact.interactions = interactions.filter { $0.encodingErrors.isEmpty } - - // Validate the Pact `Data` is hunky dory - guard let pactData = pact.data else { - throw MockServerError.nullPointer - } - - // Ask Mock Server to do the actual Pact file writing to disk - do { - return try await mockServer.finalize(pact: pactData) - } catch { - failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) - throw error - } - } - #endif - - /// Waits for test to be completed and fails if timed out - func waitForPactTestWith(timeout: TimeInterval, file: FileString?, line: UInt?, action: (@escaping () -> Void) -> Void) { - let expectation = XCTestExpectation(description: "waitForPactTest") - action { - expectation.fulfill() - } - - let result = XCTWaiter().wait(for: [expectation], timeout: timeout) - if result != .completed { - let message = "Test did not complete within \(timeout) second timeout! Did you run testCompleted() block?" - if let file = file, let line = line { - errorReporter.reportFailure(message, file: file, line: line) - } else { - errorReporter.reportFailure(message) - } - } - } - - /// Fail the test and raise the failure in `file` at `line` - func failWith(_ message: String, file: FileString? = nil, line: UInt? = nil) { - if let file = file, let line = line { - errorReporter.reportFailure(message, file: file, line: line) - } else { - errorReporter.reportFailure(message) - } - } - } // MARK: - Private @@ -327,53 +182,6 @@ private extension MockService { } } - #if canImport(_Concurrency) && compiler(>=5.7) - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - func setupPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer, testFunction: @escaping @Sendable (String) async throws -> Void) async throws { - Logger.log(message: "Setting up pact test", data: pact.data) - do { - // Set up a Mock Server with Pact data and on desired http protocol - try await mockServer.setup(pact: pact.data!, protocol: transferProtocolScheme) - - // If Mock Server spun up, run the test function - let task = Task(timeout: timeout) { - try await testFunction(mockServer.baseUrl) - } - // await task completion (value is Void) - try await task.value - } catch { - // Failed to spin up a Mock Server. This could be due to bad Pact data. Most likely to Pact data. - failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) - throw error - } - } - - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - func verifyPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer) async throws { - do { - let task = Task(timeout: timeout) { - try await mockServer.verify() - } - // await task completion (value is discarded) - _ = try await task.value - - // If the comsumer (in testFunction:) made the promised request to Mock Server, go and finalize the test. - // Only finalize when running in simulator or macOS. Running on a physical iOS device makes little sense due to - // writing a pact file to device's disk. `libpact_ffi` does the actual file writing it writes it onto the - // disk of the device it is being run on. - #if targetEnvironment(simulator) || os(macOS) - let message = try await finalize(file: file, line: line) - Logger.log(message: message, data: self.pact.data) - #else - print("[INFO]: Running on an iOS device. Writing Pact interaction into a contract skipped.") - #endif - } catch { - failWith((error as? MockServerError)?.description ?? error.localizedDescription, file: file, line: line) - throw error - } - } - #endif - func verifyPactInteraction(timeout: TimeInterval, file: FileString?, line: UInt?, mockServer: MockServer) { waitForPactTestWith(timeout: timeout, file: file, line: line) { [unowned self] completion in // Ask Mock Server to verify the promised request (testFunction:) has been made @@ -407,5 +215,4 @@ private extension MockService { } } } - } diff --git a/Tests/Model/AnyEncodableTests.swift b/Tests/Model/AnyEncodableTests.swift index 5a6e0ef6..230b36bc 100644 --- a/Tests/Model/AnyEncodableTests.swift +++ b/Tests/Model/AnyEncodableTests.swift @@ -79,8 +79,8 @@ class AnyEncodableTests: XCTestCase { "Goo": [ "one": [1, 23.45], "two": true - ] - ], + ] as [String : Any] + ] as [String : Any], for: .body ).encoded().node diff --git a/Tests/Model/PactBuilderTests.swift b/Tests/Model/PactBuilderTests.swift index 557a98c1..f89dac80 100644 --- a/Tests/Model/PactBuilderTests.swift +++ b/Tests/Model/PactBuilderTests.swift @@ -48,7 +48,7 @@ class PactBuilderTests: XCTestCase { } func testPact_SetsNestedMatchers_in_SomethingLike() throws { - let testBody: Any = [ + let testBody: [String : Any] = [ "body": Matcher.SomethingLike( [ "refresh_token": Matcher.SomethingLike("xxxxxx"), @@ -56,7 +56,7 @@ class PactBuilderTests: XCTestCase { [ "id": Matcher.SomethingLike("xxxxxx-xxxx-xxxx-xxxx-xxxxxxx"), "email": Matcher.RegexLike(value: "admin@xxxxx.au", pattern: #"^([-\.\w]+@[-\w]+\.)+[\w-]{2,4}$"#) - ] + ] as [String : Any] ) ] ) @@ -295,7 +295,7 @@ class PactBuilderTests: XCTestCase { } func testPact_SetsExampleGenerator_RandomDateTime() throws { - let testBody: Any = [ + let testBody: [String : Any] = [ "data": ExampleGenerator.RandomDate(format: "dd-MM-yyyy"), "foo": ExampleGenerator.RandomDateTime(format: "HH:mm (dd/MM)") ] @@ -391,11 +391,11 @@ class PactBuilderTests: XCTestCase { // MARK: - Testing parsing for headers func testPact_ProcessesMatchers_InHeaders() throws { - let testHeaders: Any = [ + let testHeaders: [String : Any] = [ "foo": Matcher.SomethingLike("bar"), "bar": ExampleGenerator.RandomBool(), ] - let testBody: Any = [ + let testBody: [String : Any] = [ "foo": Matcher.SomethingLike("baz"), ] @@ -412,11 +412,11 @@ class PactBuilderTests: XCTestCase { func testPact_ProcessesMatcher_InRequestPath() throws { let path = Matcher.RegexLike(value: "/some/1234", pattern: #"^/some/\d{4}+$"#) - let testHeaders: Any = [ + let testHeaders: [String : Any] = [ "foo": Matcher.SomethingLike("bar"), "bar": ExampleGenerator.RandomBool(), ] - let testBody: Any = [ + let testBody: [String : Any] = [ "foo": Matcher.SomethingLike("baz"), ] diff --git a/Tests/Model/PactTests.swift b/Tests/Model/PactTests.swift index 6a4905fb..11cc8522 100644 --- a/Tests/Model/PactTests.swift +++ b/Tests/Model/PactTests.swift @@ -202,7 +202,7 @@ class PactTests: XCTestCase { ], "fuu": ["xyz", "abc"], "num": [1, 2, 3] - ] + ] as [String : Any] let interaction = Interaction( description: "test Encodable Pact", diff --git a/Tests/Services/MockServiceTests.swift b/Tests/Services/MockServiceTests.swift index 9defecc4..3df9749c 100644 --- a/Tests/Services/MockServiceTests.swift +++ b/Tests/Services/MockServiceTests.swift @@ -442,7 +442,7 @@ class MockServiceTests: XCTestCase { body: [ "name": Matcher.SomethingLike("Joe"), "age": Matcher.IntegerLike(42) - ] + ] as [String : Any] ) .willRespondWith( status: 201, @@ -580,7 +580,7 @@ class MockServiceTests: XCTestCase { "nullable_key": Matcher.MatchNull(), "one_of_string": Matcher.OneOf("white", "gray", "blue", "yellow", "green", "black"), "one_of_int": Matcher.OneOf(3, 1, 2, 4) - ] + ] as [String : Any] ) let testExpectation = expectation(description: #function) @@ -625,7 +625,7 @@ class MockServiceTests: XCTestCase { .willRespondWith(status: 200, body: [ "foo": "bar", "baz": Matcher.EachLike(1) - ]) + ] as [String : Any]) let testExpectation = expectation(description: #function) @@ -672,7 +672,7 @@ class MockServiceTests: XCTestCase { Matcher.EachLike("one", min: 2, max: 10, count: 5), Matcher.EachLike(1, min: 1, max: 25), ] - ]) + ] as [String : Any]) let testExpectation = expectation(description: #function) @@ -727,7 +727,7 @@ class MockServiceTests: XCTestCase { body: [ "identifier": Matcher.FromProviderState(parameter: "userId", value: .int(100)), "name": Matcher.SomethingLike("Mary") - ] + ] as [String : Any] ) let testExpectation = expectation(description: #function) @@ -779,7 +779,7 @@ class MockServiceTests: XCTestCase { "randomTime": ExampleGenerator.RandomTime(format: "HH:mm - ss"), "randomDateTime": ExampleGenerator.RandomDateTime(format: "HH:mm - dd.MM.yy"), "specificDateTime": ExampleGenerator.DateTime(format: testDateTimeFormat, use: testDateTime), - ] + ] as [String : Any] ) let testExpectation = expectation(description: #function) @@ -891,7 +891,7 @@ class MockServiceTests: XCTestCase { "3rd_level_nested": Matcher.EachLike(Matcher.IntegerLike(369)) ] ) - ] + ] as [Any] ), "regex_array": Matcher.EachLike( [ @@ -906,7 +906,7 @@ class MockServiceTests: XCTestCase { ) ] ) - ] + ] as [String : Any] ) let testExpectation = expectation(description: #function) diff --git a/Tests/Services/PactContractTests.swift b/Tests/Services/PactContractTests.swift index b5723f35..de96abc4 100644 --- a/Tests/Services/PactContractTests.swift +++ b/Tests/Services/PactContractTests.swift @@ -246,15 +246,15 @@ class PactContractTests: XCTestCase { Matcher.DecimalLike(123.23), Matcher.RegexLike(value: "2021-05-17", pattern: #"\d{4}-\d{2}-\d{2}"#), Matcher.IncludesLike("in", "array", generate: "Included in explicit array") - ], + ] as [Any], "key_for_datetime_expression": ExampleGenerator.DateTimeExpression(expression: "today +1 day", format: "yyyy-MM-dd") - ] + ] as [String : Any] ), "array_of_strings": Matcher.EachLike( Matcher.SomethingLike("A string") ), "includes_like": Matcher.IncludesLike("included", generate: "Value _included_ is included in this string") - ] + ] as [String : Any] ) mockService.run { [unowned self] baseURL, completed in @@ -451,7 +451,7 @@ class PactContractTests: XCTestCase { "3rd_level_nested": Matcher.EachLike(Matcher.IntegerLike(369), count: 2) ] ) - ] + ] as [Any] ), "regex_array": Matcher.EachLike( [ @@ -467,7 +467,7 @@ class PactContractTests: XCTestCase { ] ), "enum_value": Matcher.OneOf("night", "morning", "mid-day", "afternoon", "evening") - ] + ] as [String : Any] ) mockService.run { [unowned self] baseURL, completed in @@ -545,7 +545,7 @@ class PactContractTests: XCTestCase { "randomCode": Matcher.FromProviderState(parameter: "rndCode", value: .string("some-random-code")), "foo": Matcher.SomethingLike("bar"), "baz": Matcher.SomethingLike("qux") - ] + ] as [String : Any] ) mockService.run { [unowned self] baseURL, completed in @@ -621,7 +621,7 @@ class PactContractTests: XCTestCase { "referencedArticles": Matcher.EachLike([ "bundleId": Matcher.SomethingLike("someId") ]) - ]) + ] as [String : Any]) ] ]) ] @@ -658,11 +658,11 @@ class PactContractTests: XCTestCase { "abc": Matcher.EachKeyLike([ "field1": Matcher.SomethingLike("value1"), "field2": Matcher.IntegerLike(123) - ]), + ] as [String : Any]), "xyz": Matcher.EachKeyLike([ "field1": Matcher.SomethingLike("value2"), "field2": Matcher.IntegerLike(456) - ]) + ] as [String : Any]) ] )