diff --git a/.github/actions/run-tuist-generation/action.yml b/.github/actions/run-tuist-generation/action.yml index 9df7ce267..b472248f6 100644 --- a/.github/actions/run-tuist-generation/action.yml +++ b/.github/actions/run-tuist-generation/action.yml @@ -3,8 +3,7 @@ description: Installs and run Tuist to generate Xcode projects runs: using: "composite" steps: + - uses: jdx/mise-action@v2 - name: Run Tuist shell: bash - run: | - ./scripts/install-tuist.sh - tuist generate + run: tuist generate diff --git a/.gitignore b/.gitignore index 7b403b735..b34f7e24e 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ Tests/TestCodeGenConfigurations/**/Podfile.lock Tests/TestCodeGenConfigurations/**/TestMocks Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/**/*.graphql.swift Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Sources/PackageTwo +Tests/TestCodeGenConfigurations/CodegenXCFramework/CodegenXCFramework/MyAPI/** !Tests/TestCodeGenConfigurations/**/SchemaConfiguration.swift !Tests/TestCodeGenConfigurations/Other-CustomTarget/AnimalKingdomAPI/AnimalKingdomAPI.h !Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Sources/PackageTwo/PackageTwo.swift diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 000000000..04bfd95d8 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,2 @@ +[tools] +tuist = "4.55.6" diff --git a/.tuist-version b/.tuist-version deleted file mode 100644 index 9febccac0..000000000 --- a/.tuist-version +++ /dev/null @@ -1 +0,0 @@ -3.42.2 diff --git a/Configuration/Shared/Project-Release.xcconfig b/Configuration/Shared/Project-Release.xcconfig index b266f8940..bbd1255e5 100644 --- a/Configuration/Shared/Project-Release.xcconfig +++ b/Configuration/Shared/Project-Release.xcconfig @@ -2,4 +2,3 @@ COPY_PHASE_STRIP = YES DEPLOYMENT_POSTPROCESSING = YES -STRIP_INSTALLED_PRODUCT = YES diff --git a/Configuration/Shared/Workspace-Deployment-Targets.xcconfig b/Configuration/Shared/Workspace-Deployment-Targets.xcconfig index 7b044dd48..23dc5afbf 100644 --- a/Configuration/Shared/Workspace-Deployment-Targets.xcconfig +++ b/Configuration/Shared/Workspace-Deployment-Targets.xcconfig @@ -1,8 +1,9 @@ // Base Deployment Targets -IPHONEOS_DEPLOYMENT_TARGET = 12.0 -MACOSX_DEPLOYMENT_TARGET = 10.14.6 -TVOS_DEPLOYMENT_TARGET = 12.0 -WATCHOS_DEPLOYMENT_TARGET = 5.0 +IPHONEOS_DEPLOYMENT_TARGET = 15.0 +MACOSX_DEPLOYMENT_TARGET = 12.0 +TVOS_DEPLOYMENT_TARGET = 15.0 +WATCHOS_DEPLOYMENT_TARGET = 8.0 +XROS_DEPLOYMENT_TARGET = 1.0 WARNING_CFLAGS[sdk=iphone*] = $(inherited) -DAPI_TO_BE_DEPRECATED=12_0 WARNING_CFLAGS[sdk=macosx*] = $(inherited) -DAPI_TO_BE_DEPRECATED=10_14 diff --git a/Configuration/Shared/Workspace-Language.xcconfig b/Configuration/Shared/Workspace-Language.xcconfig index 523640345..b779b69ae 100644 --- a/Configuration/Shared/Workspace-Language.xcconfig +++ b/Configuration/Shared/Workspace-Language.xcconfig @@ -19,7 +19,7 @@ CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES ENABLE_STRICT_OBJC_MSGSEND = YES // Swift -SWIFT_VERSION = 5.0 +SWIFT_VERSION = 6.0 SWIFT_PRECOMPILE_BRIDGING_HEADER = YES SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(CONFIGURATION:upper) SWIFT_SWIFT3_OBJC_INFERENCE = Off diff --git a/Configuration/Shared/Workspace-Packaging.xcconfig b/Configuration/Shared/Workspace-Packaging.xcconfig index f8e5532d2..b6ca546f5 100644 --- a/Configuration/Shared/Workspace-Packaging.xcconfig +++ b/Configuration/Shared/Workspace-Packaging.xcconfig @@ -1,4 +1,2 @@ PRODUCT_NAME = $(TARGET_NAME) PRODUCT_BUNDLE_IDENTIFIER = com.apollographql.$(TARGET_NAME:rfc1034identifier).$(PLATFORM_NAME) - -ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO diff --git a/Configuration/Shared/Workspace-Target-Codegen.xcconfig b/Configuration/Shared/Workspace-Target-Codegen.xcconfig index 4d7c317cc..938c937c9 100644 --- a/Configuration/Shared/Workspace-Target-Codegen.xcconfig +++ b/Configuration/Shared/Workspace-Target-Codegen.xcconfig @@ -1,3 +1,3 @@ SDKROOT = macosx SUPPORTED_PLATFORMS = macosx -MACOSX_DEPLOYMENT_TARGET = 10.15 +MACOSX_DEPLOYMENT_TARGET = 12.0 diff --git a/apollo-ios/Design/3093-graphql-defer.md b/Design/3093-graphql-defer.md similarity index 100% rename from apollo-ios/Design/3093-graphql-defer.md rename to Design/3093-graphql-defer.md diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Operations/Queries/AllAnimalsDeferQuery.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Operations/Queries/AllAnimalsDeferQuery.graphql.swift index b9d87f6e5..cd41f07aa 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Operations/Queries/AllAnimalsDeferQuery.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/Sources/Operations/Queries/AllAnimalsDeferQuery.graphql.swift @@ -1339,9 +1339,9 @@ public struct AllAnimalsDeferQuery: GraphQLQuery { public static var responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( deferredFragments: [ - DeferredFragmentIdentifiers.deferredPetAnimal: Data.AllAnimal.AsPet.DeferredPetAnimal.self, - DeferredFragmentIdentifiers.deferredCat: Data.AllAnimal.AsCat.DeferredCat.self, - DeferredFragmentIdentifiers.deferredDog: Data.AllAnimal.AsDog.DeferredDog.self, + DeferredFragmentIdentifiers.deferredPetAnimal: Data.AllAnimal.AsPet.DeferredPetAnimal.self, + DeferredFragmentIdentifiers.deferredCat: Data.AllAnimal.AsCat.DeferredCat.self, + DeferredFragmentIdentifiers.deferredDog: Data.AllAnimal.AsDog.DeferredDog.self, ] - ) + ) } diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Bird+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Bird+Mock.graphql.swift index 77dd55596..054dbcc92 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Bird+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Bird+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Bird: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Bird @@ -26,17 +26,17 @@ public class Bird: MockObject { public extension Mock where O == Bird { convenience init( - bodyTemperature: Int? = nil, - favoriteToy: String? = nil, - height: Mock? = nil, + bodyTemperature: Int = 0, + favoriteToy: String = "", + height: Mock = Mock(), humanName: String? = nil, - id: AnimalKingdomAPI.ID? = nil, - laysEggs: Bool? = nil, + id: AnimalKingdomAPI.ID = "", + laysEggs: Bool = false, owner: Mock? = nil, - predators: [(any AnyMock)]? = nil, + predators: [(any AnyMock)] = [], skinCovering: GraphQLEnum? = nil, - species: String? = nil, - wingspan: Double? = nil + species: String = "", + wingspan: Double = 0.0 ) { self.init() _setScalar(bodyTemperature, for: \.bodyTemperature) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Cat+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Cat+Mock.graphql.swift index 4505fa489..16211a8ca 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Cat+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Cat+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Cat: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Cat @@ -26,17 +26,17 @@ public class Cat: MockObject { public extension Mock where O == Cat { convenience init( - bodyTemperature: Int? = nil, - favoriteToy: String? = nil, - height: Mock? = nil, + bodyTemperature: Int = 0, + favoriteToy: String = "", + height: Mock = Mock(), humanName: String? = nil, - id: AnimalKingdomAPI.ID? = nil, - isJellicle: Bool? = nil, - laysEggs: Bool? = nil, + id: AnimalKingdomAPI.ID = "", + isJellicle: Bool = false, + laysEggs: Bool = false, owner: Mock? = nil, - predators: [(any AnyMock)]? = nil, + predators: [(any AnyMock)] = [], skinCovering: GraphQLEnum? = nil, - species: String? = nil + species: String = "" ) { self.init() _setScalar(bodyTemperature, for: \.bodyTemperature) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Crocodile+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Crocodile+Mock.graphql.swift index 216665346..6d7fe377b 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Crocodile+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Crocodile+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Crocodile: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Crocodile @@ -22,12 +22,12 @@ public class Crocodile: MockObject { public extension Mock where O == Crocodile { convenience init( - age: Int? = nil, - height: Mock? = nil, - id: AnimalKingdomAPI.ID? = nil, - predators: [(any AnyMock)]? = nil, + age: Int = 0, + height: Mock = Mock(), + id: AnimalKingdomAPI.ID = "", + predators: [(any AnyMock)] = [], skinCovering: GraphQLEnum? = nil, - species: String? = nil, + species: String = "", tag: String? = nil ) { self.init() diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Dog+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Dog+Mock.graphql.swift index d01b7cbc2..1fdb4a3da 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Dog+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Dog+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Dog: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Dog @@ -28,17 +28,17 @@ public class Dog: MockObject { public extension Mock where O == Dog { convenience init( birthdate: AnimalKingdomAPI.CustomDate? = nil, - bodyTemperature: Int? = nil, - favoriteToy: String? = nil, - height: Mock? = nil, + bodyTemperature: Int = 0, + favoriteToy: String = "", + height: Mock = Mock(), houseDetails: AnimalKingdomAPI.Object? = nil, humanName: String? = nil, - id: AnimalKingdomAPI.ID? = nil, - laysEggs: Bool? = nil, + id: AnimalKingdomAPI.ID = "", + laysEggs: Bool = false, owner: Mock? = nil, - predators: [(any AnyMock)]? = nil, + predators: [(any AnyMock)] = [], skinCovering: GraphQLEnum? = nil, - species: String? = nil + species: String = "" ) { self.init() _setScalar(birthdate, for: \.birthdate) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Fish+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Fish+Mock.graphql.swift index 0228c48ad..5ca817c42 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Fish+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Fish+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Fish: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Fish @@ -23,14 +23,14 @@ public class Fish: MockObject { public extension Mock where O == Fish { convenience init( - favoriteToy: String? = nil, - height: Mock? = nil, + favoriteToy: String = "", + height: Mock = Mock(), humanName: String? = nil, - id: AnimalKingdomAPI.ID? = nil, + id: AnimalKingdomAPI.ID = "", owner: Mock? = nil, - predators: [(any AnyMock)]? = nil, + predators: [(any AnyMock)] = [], skinCovering: GraphQLEnum? = nil, - species: String? = nil + species: String = "" ) { self.init() _setScalar(favoriteToy, for: \.favoriteToy) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Height+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Height+Mock.graphql.swift index 27f919e55..d20d00ea4 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Height+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Height+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Height: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Height @@ -20,11 +20,11 @@ public class Height: MockObject { public extension Mock where O == Height { convenience init( - centimeters: Double? = nil, - feet: Int? = nil, + centimeters: Double = 0.0, + feet: Int = 0, inches: Int? = nil, - meters: Int? = nil, - relativeSize: GraphQLEnum? = nil + meters: Int = 0, + relativeSize: GraphQLEnum = .case(.large) ) { self.init() _setScalar(centimeters, for: \.centimeters) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Human+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Human+Mock.graphql.swift index 301408b6a..394996841 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Human+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Human+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Human: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Human @@ -23,14 +23,14 @@ public class Human: MockObject { public extension Mock where O == Human { convenience init( - bodyTemperature: Int? = nil, - firstName: String? = nil, - height: Mock? = nil, - id: AnimalKingdomAPI.ID? = nil, - laysEggs: Bool? = nil, - predators: [(any AnyMock)]? = nil, + bodyTemperature: Int = 0, + firstName: String = "", + height: Mock = Mock(), + id: AnimalKingdomAPI.ID = "", + laysEggs: Bool = false, + predators: [(any AnyMock)] = [], skinCovering: GraphQLEnum? = nil, - species: String? = nil + species: String = "" ) { self.init() _setScalar(bodyTemperature, for: \.bodyTemperature) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Interfaces.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Interfaces.graphql.swift index 0fcfc230e..166cb8787 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Interfaces.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Interfaces.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public extension MockObject { typealias Animal = Interface diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Unions.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Unions.graphql.swift index d0935b93f..d69e15691 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Unions.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/MockObject+Unions.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public extension MockObject { typealias ClassroomPet = Union diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Mutation+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Mutation+Mock.graphql.swift index 572ea857c..c60765ff2 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Mutation+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Mutation+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Mutation: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Mutation @@ -16,7 +16,7 @@ public class Mutation: MockObject { public extension Mock where O == Mutation { convenience init( - adoptPet: (any AnyMock)? = nil + adoptPet: (any AnyMock) = Mock() ) { self.init() _setEntity(adoptPet, for: \.adoptPet) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/PetRock+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/PetRock+Mock.graphql.swift index d1405035f..78974ff95 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/PetRock+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/PetRock+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class PetRock: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.PetRock @@ -19,9 +19,9 @@ public class PetRock: MockObject { public extension Mock where O == PetRock { convenience init( - favoriteToy: String? = nil, + favoriteToy: String = "", humanName: String? = nil, - id: AnimalKingdomAPI.ID? = nil, + id: AnimalKingdomAPI.ID = "", owner: Mock? = nil ) { self.init() diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Query+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Query+Mock.graphql.swift index 8405fbed7..f93555408 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Query+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Query+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Query: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Query @@ -19,10 +19,10 @@ public class Query: MockObject { public extension Mock where O == Query { convenience init( - allAnimals: [(any AnyMock)]? = nil, + allAnimals: [(any AnyMock)] = [], classroomPets: [(any AnyMock)?]? = nil, - findPet: [(any AnyMock)]? = nil, - pets: [(any AnyMock)]? = nil + findPet: [(any AnyMock)] = [], + pets: [(any AnyMock)] = [] ) { self.init() _setList(allAnimals, for: \.allAnimals) diff --git a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Rat+Mock.graphql.swift b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Rat+Mock.graphql.swift index 351b82b29..da1cb1145 100644 --- a/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Rat+Mock.graphql.swift +++ b/Sources/AnimalKingdomAPI/AnimalKingdomAPI/TestMocks/Rat+Mock.graphql.swift @@ -2,7 +2,7 @@ // This file was automatically generated and should not be edited. import ApolloTestSupport -import AnimalKingdomAPI +@testable import AnimalKingdomAPI public class Rat: MockObject { public static let objectType: ApolloAPI.Object = AnimalKingdomAPI.Objects.Rat @@ -23,14 +23,14 @@ public class Rat: MockObject { public extension Mock where O == Rat { convenience init( - favoriteToy: String? = nil, - height: Mock? = nil, + favoriteToy: String = "", + height: Mock = Mock(), humanName: String? = nil, - id: AnimalKingdomAPI.ID? = nil, + id: AnimalKingdomAPI.ID = "", owner: Mock? = nil, - predators: [(any AnyMock)]? = nil, + predators: [(any AnyMock)] = [], skinCovering: GraphQLEnum? = nil, - species: String? = nil + species: String = "" ) { self.init() _setScalar(favoriteToy, for: \.favoriteToy) diff --git a/SwiftScripts/Package.resolved b/SwiftScripts/Package.resolved index 48198726a..26a0633c8 100644 --- a/SwiftScripts/Package.resolved +++ b/SwiftScripts/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "5c1e7a481cf669c7bf65db2d4b08d6a7f4776d0e323ee43eee2b80f7919d8545", "pins" : [ { "identity" : "inflectorkit", @@ -14,17 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { - "revision" : "46989693916f56d1186bd59ac15124caef896560", - "version" : "1.3.1" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics", - "state" : { - "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", - "version" : "1.3.0" + "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", + "version" : "1.6.1" } }, { @@ -32,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", - "version" : "1.1.0" + "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", + "version" : "1.2.1" } }, { @@ -41,19 +33,19 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-docc-plugin", "state" : { - "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", - "version" : "1.3.0" + "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", + "version" : "1.4.5" } }, { "identity" : "swift-docc-symbolkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-symbolkit", + "location" : "https://github.com/swiftlang/swift-docc-symbolkit", "state" : { "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", "version" : "1.0.0" } } ], - "version" : 2 + "version" : 3 } diff --git a/SwiftScripts/Package.swift b/SwiftScripts/Package.swift index e97da20ac..275f4c226 100644 --- a/SwiftScripts/Package.swift +++ b/SwiftScripts/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -13,41 +13,50 @@ let package = Package( .package(name: "ApolloCodegen", path: "../apollo-ios-codegen"), .package(name: "ApolloPagination", path: "../apollo-ios-pagination"), .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.2.0")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0") + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"), ], targets: [ - .target(name: "TargetConfig", - dependencies: [ - .product(name: "ApolloCodegenLib", package: "ApolloCodegen"), - ]), + .target( + name: "TargetConfig", + dependencies: [ + .product(name: "ApolloCodegenLib", package: "ApolloCodegen") + ] + ), .target(name: "SwiftScriptHelpers"), - .executableTarget(name: "Codegen", - dependencies: [ - .product(name: "ApolloCodegenLib", package: "ApolloCodegen"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .target(name: "TargetConfig"), - .target(name: "SwiftScriptHelpers") - ]), - .executableTarget(name: "SchemaDownload", - dependencies: [ - .product(name: "ApolloCodegenLib", package: "ApolloCodegen"), - .target(name: "TargetConfig"), - .target(name: "SwiftScriptHelpers") - ]), - .executableTarget(name: "DocumentationGenerator", - dependencies: [ - .product(name: "ApolloCodegenLib", package: "ApolloCodegen"), - .product(name: "Apollo", package: "Apollo"), - .product(name: "ApolloAPI", package: "Apollo"), - .product(name: "ApolloSQLite", package: "Apollo"), - .product(name: "ApolloWebSocket", package: "Apollo"), - .product(name: "ApolloPagination", package: "ApolloPagination"), - .target(name: "SwiftScriptHelpers") - ] - ), - .testTarget(name: "CodegenTests", - dependencies: [ - "Codegen" - ]), + .executableTarget( + name: "Codegen", + dependencies: [ + .product(name: "ApolloCodegenLib", package: "ApolloCodegen"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .target(name: "TargetConfig"), + .target(name: "SwiftScriptHelpers"), + ] + ), + .executableTarget( + name: "SchemaDownload", + dependencies: [ + .product(name: "ApolloCodegenLib", package: "ApolloCodegen"), + .target(name: "TargetConfig"), + .target(name: "SwiftScriptHelpers"), + ] + ), + .executableTarget( + name: "DocumentationGenerator", + dependencies: [ + .product(name: "ApolloCodegenLib", package: "ApolloCodegen"), + .product(name: "Apollo", package: "Apollo"), + .product(name: "ApolloAPI", package: "Apollo"), + .product(name: "ApolloSQLite", package: "Apollo"), + .product(name: "ApolloWebSocket", package: "Apollo"), + .product(name: "ApolloPagination", package: "ApolloPagination"), + .target(name: "SwiftScriptHelpers"), + ] + ), + .testTarget( + name: "CodegenTests", + dependencies: [ + "Codegen" + ] + ), ] ) diff --git a/SwiftScripts/Sources/Codegen/Codegen.swift b/SwiftScripts/Sources/Codegen/Codegen.swift index 38911010c..6165e24b8 100644 --- a/SwiftScripts/Sources/Codegen/Codegen.swift +++ b/SwiftScripts/Sources/Codegen/Codegen.swift @@ -52,7 +52,7 @@ struct Codegen: AsyncParsableCommand { // This more necessary if you're using a sub-folder, but make sure // there's actually a place to write out what you're doing. - try ApolloFileManager.default.createDirectoryIfNeeded(atPath: targetURL.path) + try await ApolloFileManager.default.createDirectoryIfNeeded(atPath: targetURL.path) // Actually attempt to generate code. try await ApolloCodegen.build( diff --git a/SwiftScripts/Tests/CodegenTests/CodegenTests.swift b/SwiftScripts/Tests/CodegenTests/CodegenTests.swift index 732f78bc7..89c065b5b 100644 --- a/SwiftScripts/Tests/CodegenTests/CodegenTests.swift +++ b/SwiftScripts/Tests/CodegenTests/CodegenTests.swift @@ -41,7 +41,7 @@ final class CodegenTests: XCTestCase { #endif } - static var allTests = [ + static nonisolated(unsafe) var allTests = [ ("testExample", testExample), ] } diff --git a/Tests/ApolloCodegenInternalTestHelpers/GraphQLJSFrontend+TestHelpers.swift b/Tests/ApolloCodegenInternalTestHelpers/GraphQLJSFrontend+TestHelpers.swift index 85bf5b3d2..a8ffb53c0 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/GraphQLJSFrontend+TestHelpers.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/GraphQLJSFrontend+TestHelpers.swift @@ -1,6 +1,7 @@ import Foundation import GraphQLCompiler @testable import ApolloCodegenLib +import ApolloInternalTestHelpers extension GraphQLJSFrontend { @@ -8,14 +9,14 @@ extension GraphQLJSFrontend { schema: String, document: String, config: ApolloCodegen.ConfigurationContext - ) async throws -> CompilationResult { - async let schemaSource = try makeSource(schema, filePath: "") - async let documentSource = try makeSource(document, filePath: "") + ) throws -> CompilationResult { + let schemaSource = try makeSource(schema, filePath: "") + let documentSource = try makeSource(document, filePath: "") - let schema = try await loadSchema(from: [schemaSource]) - let document = try await parseDocument(documentSource) + let schema = try loadSchema(from: [schemaSource]) + let document = try parseDocument(documentSource) - return try await compile( + return try compile( schema: schema, document: document, reduceGeneratedSchemaTypes: false, @@ -26,10 +27,10 @@ extension GraphQLJSFrontend { public func compile( schema: String, document: String - ) async throws -> CompilationResult { + ) throws -> CompilationResult { let config = ApolloCodegen.ConfigurationContext(config: .mock()) - return try await compile( + return try compile( schema: schema, document: document, config: config @@ -40,14 +41,14 @@ extension GraphQLJSFrontend { schema: String, documents: [String], config: ApolloCodegen.ConfigurationContext - ) async throws -> CompilationResult { - async let schemaSource = try makeSource(schema, filePath: "") + ) throws -> CompilationResult { + let schemaSource = try makeSource(schema, filePath: "") - let sources: [GraphQLSource] = try await documents.enumerated().asyncMap { - try await makeSource($0.element, filePath: "Doc_\($0.offset)") + let sources: [GraphQLSource] = try documents.enumerated().map { + try makeSource($0.element, filePath: "Doc_\($0.offset)") } - return try await compile( + return try compile( schema: schemaSource, definitions: sources, config: config @@ -58,15 +59,15 @@ extension GraphQLJSFrontend { schema schemaSource: GraphQLSource, definitions: [GraphQLSource], config: ApolloCodegen.ConfigurationContext - ) async throws -> CompilationResult { - let schema = try await loadSchema(from: [schemaSource]) + ) throws -> CompilationResult { + let schema = try loadSchema(from: [schemaSource]) - let documents: [GraphQLDocument] = try await definitions.asyncMap { - return try await parseDocument($0) + let documents: [GraphQLDocument] = try definitions.map { + return try parseDocument($0) } - let mergedDocument = try await mergeDocuments(documents) - return try await compile( + let mergedDocument = try mergeDocuments(documents) + return try compile( schema: schema, document: mergedDocument, reduceGeneratedSchemaTypes: false, @@ -77,10 +78,10 @@ extension GraphQLJSFrontend { public func compile( schema: String, documents: [String] - ) async throws -> CompilationResult { + ) throws -> CompilationResult { let config = ApolloCodegen.ConfigurationContext(config: .mock()) - return try await compile( + return try compile( schema: schema, documents: documents, config: config @@ -91,14 +92,14 @@ extension GraphQLJSFrontend { schemaJSON: String, document: String, config: ApolloCodegen.ConfigurationContext - ) async throws -> CompilationResult { - async let documentSource = try makeSource(document, filePath: "") - async let schemaSource = try makeSource(schemaJSON, filePath: "schema.json") + ) throws -> CompilationResult { + let documentSource = try makeSource(document, filePath: "") + let schemaSource = try makeSource(schemaJSON, filePath: "schema.json") - let schema = try await loadSchema(from: [schemaSource]) - let document = try await parseDocument(documentSource) + let schema = try loadSchema(from: [schemaSource]) + let document = try parseDocument(documentSource) - return try await compile( + return try compile( schema: schema, document: document, reduceGeneratedSchemaTypes: false, @@ -109,10 +110,10 @@ extension GraphQLJSFrontend { public func compile( schemaJSON: String, document: String - ) async throws -> CompilationResult { + ) throws -> CompilationResult { let config = ApolloCodegen.ConfigurationContext(config: .mock()) - return try await compile( + return try compile( schemaJSON: schemaJSON, document: document, config: config diff --git a/Tests/ApolloCodegenInternalTestHelpers/IR+InclusionConditionsMock.swift b/Tests/ApolloCodegenInternalTestHelpers/IR+InclusionConditionsMock.swift index c11ae48cd..b665dc0a8 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/IR+InclusionConditionsMock.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/IR+InclusionConditionsMock.swift @@ -2,7 +2,7 @@ @testable import IR import OrderedCollections -extension IR.InclusionConditions { +extension InclusionConditions { public static func mock( _ conditions: OrderedSet @@ -13,7 +13,9 @@ extension IR.InclusionConditions { } -extension IR.InclusionCondition: ExpressibleByStringLiteral { +extension InclusionCondition: @retroactive ExpressibleByExtendedGraphemeClusterLiteral {} +extension InclusionCondition: @retroactive ExpressibleByUnicodeScalarLiteral {} +extension InclusionCondition: @retroactive ExpressibleByStringLiteral { public init(stringLiteral: String) { self.init(stringLiteral, isInverted: false) diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockFileManager.swift b/Tests/ApolloCodegenInternalTestHelpers/MockFileManager.swift index a313627ee..04358f4ee 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockFileManager.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockFileManager.swift @@ -6,10 +6,10 @@ import XCTest public class MockApolloFileManager: ApolloFileManager { /// Translates to the `FileManager` functions that can be mocked. public enum Closure: CustomStringConvertible { - case fileExists(_ handler: (String, UnsafeMutablePointer?) -> Bool) - case removeItem(_ handler: (String) throws -> Void) - case createFile(_ handler: (String, Data?, FileAttributes?) -> Bool) - case createDirectory(_ handler: (String, Bool, FileAttributes?) throws -> Void) + case fileExists(_ handler: @Sendable (String, UnsafeMutablePointer?) -> Bool) + case removeItem(_ handler: @Sendable (String) throws -> Void) + case createFile(_ handler: @Sendable (String, Data?, FileAttributes?) -> Bool) + case createDirectory(_ handler: @Sendable (String, Bool, FileAttributes?) throws -> Void) // These are based on the return string from the #function macro. They are used in overriden // functions to lookup the provided closure. Be aware that if the function signature changes @@ -43,7 +43,7 @@ public class MockApolloFileManager: ApolloFileManager { /// through to `super`. Defaults to `true`. /// - requireAllClosuresCalled: If `true` all mocked closures must be called otherwise the test /// will fail. Defaults to `true`. - public init( + public nonisolated init( strict: Bool = true, requireAllClosuresCalled: Bool = true ) { @@ -69,7 +69,7 @@ public class MockApolloFileManager: ApolloFileManager { return _base.closuresToBeCalled.isEmpty } - class MockFileManager: FileManager { + class MockFileManager: FileManager, @unchecked Sendable { private let lock = NSLock() diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift b/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift index 33e8e3924..19816fe15 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockMergedSelections.swift @@ -7,7 +7,7 @@ extension IR.MergedSelections.MergedSource { public static func mock( _ field: IRTestWrapper?, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> Self { self.init( @@ -18,7 +18,7 @@ extension IR.MergedSelections.MergedSource { public static func mock( _ typeCase: IRTestWrapper?, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> Self { self.init( @@ -29,7 +29,7 @@ extension IR.MergedSelections.MergedSource { public static func mock( _ fragment: IRTestWrapper?, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> Self { let fragment = try XCTUnwrap(fragment, file: file, line: line) @@ -42,7 +42,7 @@ extension IR.MergedSelections.MergedSource { public static func mock( for field: IRTestWrapper?, from fragment: IRTestWrapper?, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> Self { self.init( @@ -54,7 +54,7 @@ extension IR.MergedSelections.MergedSource { public static func mock( for field: IRTestWrapper?, from fragment: IRTestWrapper?, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> Self { self.init( diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockNetworkSession.swift b/Tests/ApolloCodegenInternalTestHelpers/MockNetworkSession.swift index 6308437cb..96a74cb2f 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockNetworkSession.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockNetworkSession.swift @@ -14,11 +14,13 @@ public final class MockNetworkSession: NetworkSession { self.abandon = abandon } - public func loadData(with urlRequest: URLRequest, completionHandler: @escaping (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTask? { + public func loadData(with urlRequest: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) async -> Void) -> URLSessionDataTask? { guard !abandon else { return nil } let response = HTTPURLResponse(url: urlRequest.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil) - completionHandler(data, response, error) + Task { [data, error] in + await completionHandler(data, response, error) + } return nil } diff --git a/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift b/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift index 0276cea49..6746c651a 100644 --- a/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift +++ b/Tests/ApolloCodegenTests/ApolloCodegenConfigurationCodableTests.swift @@ -116,7 +116,7 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { "fieldAccessors" : "camelCase", "inputObjects" : "none" }, - "deprecatedEnumCases" : "exclude", + "deprecatedEnumCases" : "exclude", "operationDocumentFormat" : [ "definition" ], @@ -1291,7 +1291,7 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { "fieldAccessors" : "idiomatic", "inputObjects" : "camelCase" }, - "deprecatedEnumCases" : "include", + "deprecatedEnumCases" : "include", "operationDocumentFormat" : [ "definition" ], @@ -1776,7 +1776,7 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { "fieldAccessors" : "idiomatic", "inputObjects" : "camelCase" }, - "deprecatedEnumCases" : "include", + "deprecatedEnumCases" : "include", "operationDocumentFormat" : [ "definition" ], @@ -1883,7 +1883,7 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { "fieldAccessors" : "idiomatic", "inputObjects" : "camelCase" }, - "deprecatedEnumCases" : "include", + "deprecatedEnumCases" : "include", "operationDocumentFormat" : [ "definition" ], @@ -2097,7 +2097,7 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { "fieldAccessors" : "idiomatic", "inputObjects" : "camelCase" }, - "deprecatedEnumCases" : "include", + "deprecatedEnumCases" : "include", "operationDocumentFormat" : [ "definition" ], @@ -2204,7 +2204,7 @@ class ApolloCodegenConfigurationCodableTests: XCTestCase { "fieldAccessors" : "idiomatic", "inputObjects" : "camelCase" }, - "deprecatedEnumCases" : "include", + "deprecatedEnumCases" : "include", "operationDocumentFormat" : [ "definition" ], diff --git a/Tests/ApolloCodegenTests/ApolloCodegenConfigurationTests.swift b/Tests/ApolloCodegenTests/ApolloCodegenConfigurationTests.swift index 9da383859..c636ea420 100644 --- a/Tests/ApolloCodegenTests/ApolloCodegenConfigurationTests.swift +++ b/Tests/ApolloCodegenTests/ApolloCodegenConfigurationTests.swift @@ -16,15 +16,15 @@ class ApolloCodegenConfigurationTests: XCTestCase { // MARK: Lifecycle - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() testFilePathBuilder = TestFilePathBuilder(test: self) directoryURL = testFilePathBuilder.testIsolatedOutputFolder .appendingPathComponent("Configuration") .appendingPathComponent(self.testRun!.test.name) - try ApolloFileManager.default.createDirectoryIfNeeded(atPath: directoryURL.path) + try await ApolloFileManager.default.createDirectoryIfNeeded(atPath: directoryURL.path) filename = UUID().uuidString fileURL = directoryURL.appendingPathComponent(filename) @@ -36,8 +36,8 @@ class ApolloCodegenConfigurationTests: XCTestCase { )) } - override func tearDownWithError() throws { - try ApolloFileManager.default.deleteDirectory(atPath: directoryURL.path) + override func tearDown() async throws { + try await ApolloFileManager.default.deleteDirectory(atPath: directoryURL.path) testFilePathBuilder = nil config = nil @@ -47,7 +47,7 @@ class ApolloCodegenConfigurationTests: XCTestCase { fileURL = nil filename = nil - try super.tearDownWithError() + try await super.tearDown() } // MARK: Test Helpers diff --git a/Tests/ApolloCodegenTests/ApolloCodegenTests.swift b/Tests/ApolloCodegenTests/ApolloCodegenTests.swift index 1855c2f48..824b45ccf 100644 --- a/Tests/ApolloCodegenTests/ApolloCodegenTests.swift +++ b/Tests/ApolloCodegenTests/ApolloCodegenTests.swift @@ -4,7 +4,7 @@ import ApolloInternalTestHelpers @testable import ApolloCodegenLib import IR import GraphQLCompiler -import Nimble +@preconcurrency import Nimble class ApolloCodegenTests: XCTestCase { private var directoryURL: URL { testFileManager.directoryURL } @@ -65,8 +65,8 @@ class ApolloCodegenTests: XCTestCase { containing data: Data, named filename: String, inDirectory directory: String? = nil - ) throws -> String { - return try self.testFileManager.createFile( + ) async throws -> String { + return try await self.testFileManager.createFile( containing: data, named: filename, inDirectory: directory @@ -75,11 +75,11 @@ class ApolloCodegenTests: XCTestCase { @discardableResult private func createFile( - body: @autoclosure () -> String = "Test File", + body: @autoclosure @Sendable () -> String = "Test File", filename: String, inDirectory directory: String? = nil - ) throws -> String { - return try self.testFileManager.createFile( + ) async throws -> String { + return try await self.testFileManager.createFile( body: body(), named: filename, inDirectory: directory @@ -92,7 +92,7 @@ class ApolloCodegenTests: XCTestCase { named operationName: String, filename: String, inDirectory directory: String? = nil - ) throws -> String { + ) async throws -> String { let query: String = """ \(type.rawValue) \(operationName) { @@ -101,14 +101,14 @@ class ApolloCodegenTests: XCTestCase { } } """ - return try createFile(body: query, filename: filename, inDirectory: directory) + return try await createFile(body: query, filename: filename, inDirectory: directory) } // MARK: CompilationResult Tests func test_compileResults_givenOperation_withGraphQLErrors_shouldThrow() async throws { // given - let schemaPath = try createFile(containing: schemaData, named: "schema.graphqls") + let schemaPath = try await createFile(containing: schemaData, named: "schema.graphqls") let operationData: Data = """ @@ -119,7 +119,7 @@ class ApolloCodegenTests: XCTestCase { } } """.data(using: .utf8)! - try createFile(containing: operationData, named: "operation.graphql") + try await createFile(containing: operationData, named: "operation.graphql") let config = ApolloCodegen.ConfigurationContext(config: ApolloCodegenConfiguration.mock(input: .init( schemaPath: schemaPath, @@ -150,7 +150,7 @@ class ApolloCodegenTests: XCTestCase { func test_compileResults_givenOperations_withNoErrors_shouldReturn() async throws { // given - let schemaPath = try createFile(containing: schemaData, named: "schema.graphqls") + let schemaPath = try await createFile(containing: schemaData, named: "schema.graphqls") let booksData: Data = """ @@ -160,7 +160,7 @@ class ApolloCodegenTests: XCTestCase { } } """.data(using: .utf8)! - try createFile(containing: booksData, named: "books-operation.graphql") + try await createFile(containing: booksData, named: "books-operation.graphql") let authorsData: Data = """ @@ -170,7 +170,7 @@ class ApolloCodegenTests: XCTestCase { } } """.data(using: .utf8)! - try createFile(containing: authorsData, named: "authors-operation.graphql") + try await createFile(containing: authorsData, named: "authors-operation.graphql") let config = ApolloCodegen.ConfigurationContext(config: ApolloCodegenConfiguration.mock(input: .init( schemaPath: schemaPath, @@ -189,9 +189,9 @@ class ApolloCodegenTests: XCTestCase { func test_compileResults_givenIDScalarIsReferenced_referencedTypesShouldIncludeScalar() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls", inDirectory: "CustomRoot") + try await createFile(containing: schemaData, named: "schema.graphqls", inDirectory: "CustomRoot") - try createFile( + try await createFile( body: """ query getAuthors { authors { @@ -225,7 +225,7 @@ class ApolloCodegenTests: XCTestCase { func test_compileResults_givenRelativeSearchPath_relativeToRootURL_hasOperations_shouldReturnOperationsRelativeToRoot() async throws { // given - let schemaPath = try createFile(containing: schemaData, named: "schema.graphqls") + let schemaPath = try await createFile(containing: schemaData, named: "schema.graphqls") let rootURL = directoryURL.appendingPathComponent("CustomRoot") @@ -237,7 +237,7 @@ class ApolloCodegenTests: XCTestCase { } } """.data(using: .utf8)! - try createFile(containing: booksData, named: "books-operation.graphql", inDirectory: "CustomRoot") + try await createFile(containing: booksData, named: "books-operation.graphql", inDirectory: "CustomRoot") let authorsData: Data = """ @@ -247,7 +247,7 @@ class ApolloCodegenTests: XCTestCase { } } """.data(using: .utf8)! - try createFile(containing: authorsData, named: "authors-operation.graphql") + try await createFile(containing: authorsData, named: "authors-operation.graphql") let config = ApolloCodegen.ConfigurationContext(config: ApolloCodegenConfiguration.mock(input: .init( schemaPath: schemaPath, @@ -269,7 +269,7 @@ class ApolloCodegenTests: XCTestCase { func test_compileResults_givenRelativeSchemaSearchPath_relativeToRootURL_shouldReturnSchemaRelativeToRoot() async throws { // given - try createFile( + try await createFile( body: """ type QueryTwo { string: String! @@ -277,9 +277,9 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schema1.graphqls") - try createFile(containing: schemaData, named: "schema.graphqls", inDirectory: "CustomRoot") + try await createFile(containing: schemaData, named: "schema.graphqls", inDirectory: "CustomRoot") - try createFile( + try await createFile( body: """ query getAuthors { authors { @@ -311,7 +311,7 @@ class ApolloCodegenTests: XCTestCase { func test__compileResults__givenMultipleSchemaFiles_withDependentTypes_compilesResult() async throws { // given - try createFile( + try await createFile( body: """ type Query { books: [Book!]! @@ -320,7 +320,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schema1.graphqls") - try createFile( + try await createFile( body: """ type Book { title: String! @@ -334,7 +334,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schema2.graphqls") - try createFile( + try await createFile( body: """ query getAuthors { authors { @@ -362,7 +362,7 @@ class ApolloCodegenTests: XCTestCase { func test__compileResults__givenMultipleSchemaFiles_withDifferentRootTypes_compilesResult() async throws { // given - try createFile( + try await createFile( body: """ type Query { string: String! @@ -370,7 +370,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schema1.graphqls") - try createFile( + try await createFile( body: """ type Subscription { bool: Boolean! @@ -378,7 +378,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schema2.graphqls") - try createFile( + try await createFile( body: """ query TestQuery { string @@ -386,7 +386,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "TestQuery.graphql") - try createFile( + try await createFile( body: """ subscription TestSubscription { bool @@ -414,7 +414,7 @@ class ApolloCodegenTests: XCTestCase { func test__compileResults__givenMultipleSchemaFiles_withSchemaTypeExtension_compilesResultWithExtension() async throws { // given - try createFile( + try await createFile( body: """ type Query { string: String! @@ -422,7 +422,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schema1.graphqls") - try createFile( + try await createFile( body: """ extend type Query { bool: Boolean! @@ -430,7 +430,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schemaExtension.graphqls") - try createFile( + try await createFile( body: """ query TestQuery { string @@ -463,9 +463,9 @@ class ApolloCodegenTests: XCTestCase { contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.JSONSchema ) - try createFile(body: introspectionJSON, filename: "schemaJSON.json") + try await createFile(body: introspectionJSON, filename: "schemaJSON.json") - try createFile( + try await createFile( body: """ extend type Query { testExtensionField: Boolean! @@ -473,7 +473,7 @@ class ApolloCodegenTests: XCTestCase { """, filename: "schemaExtension.graphqls") - try createFile( + try await createFile( body: """ query TestQuery { testExtensionField @@ -508,8 +508,8 @@ class ApolloCodegenTests: XCTestCase { contentsOf: ApolloCodegenInternalTestHelpers.Resources.StarWars.JSONSchema ) - try createFile(body: introspectionJSON, filename: "schemaJSON1.json") - try createFile(body: introspectionJSON, filename: "schemaJSON2.json") + try await createFile(body: introspectionJSON, filename: "schemaJSON1.json") + try await createFile(body: introspectionJSON, filename: "schemaJSON2.json") // when let config = ApolloCodegen.ConfigurationContext(config: ApolloCodegenConfiguration.mock(input: .init( @@ -548,9 +548,9 @@ class ApolloCodegenTests: XCTestCase { func test__compileResults__givenSchemaSearchPaths_withMixedMatches_doesNotThrowError() async throws { // given - let schemaPath = try createFile(containing: schemaData, named: "schema.graphqls") + let schemaPath = try await createFile(containing: schemaData, named: "schema.graphqls") - let operationPath = try createOperationFile( + let operationPath = try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" @@ -577,7 +577,7 @@ class ApolloCodegenTests: XCTestCase { func test__compileResults__givenOperationSearchPath_withNoMatches_throwsError() async throws { // given - let schemaPath = try createFile(containing: schemaData, named: "schema.graphqls") + let schemaPath = try await createFile(containing: schemaData, named: "schema.graphqls") let config = ApolloCodegen.ConfigurationContext(config: .mock( input: .init( @@ -598,9 +598,9 @@ class ApolloCodegenTests: XCTestCase { func test__compileResults__givenOperationSearchPaths_withMixedMatches_doesNotThrowError() async throws { // given - let schemaPath = try createFile(containing: schemaData, named: "schema.graphqls") + let schemaPath = try await createFile(containing: schemaData, named: "schema.graphqls") - let operationPath = try createOperationFile( + let operationPath = try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" @@ -655,13 +655,7 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -702,6 +696,7 @@ class ApolloCodegenTests: XCTestCase { directoryURL.appendingPathComponent("Sources/Schema/CustomScalars/ID.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/AllAnimalsQuery.graphql.swift").path, + directoryURL.appendingPathComponent("Sources/Operations/Queries/AllAnimalsDeferQuery.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/AllAnimalsIncludeSkipQuery.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/ClassroomPetsQuery.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/DogQuery.graphql.swift").path, @@ -733,12 +728,11 @@ class ApolloCodegenTests: XCTestCase { ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value + let filePaths = await fileManager.writtenFiles // then expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) } func test_fileGenerators_givenSchemaAndMultipleOperationDocuments_operations_absolute_shouldGenerateSchemaAndOperationsFiles() async throws { @@ -771,13 +765,7 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -813,6 +801,7 @@ class ApolloCodegenTests: XCTestCase { directoryURL.appendingPathComponent("Sources/CustomScalars/ID.swift").path, operationsOutputURL.appendingPathComponent("Queries/AllAnimalsQuery.graphql.swift").path, + operationsOutputURL.appendingPathComponent("Queries/AllAnimalsDeferQuery.graphql.swift").path, operationsOutputURL.appendingPathComponent("Queries/DogQuery.graphql.swift").path, operationsOutputURL.appendingPathComponent("Queries/AllAnimalsIncludeSkipQuery.graphql.swift").path, operationsOutputURL.appendingPathComponent("Queries/ClassroomPetsQuery.graphql.swift").path, @@ -844,12 +833,11 @@ class ApolloCodegenTests: XCTestCase { ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value + let filePaths = await fileManager.writtenFiles // then expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) } func test_fileGenerators_givenSchemaAndOperationDocuments_whenAppendingFileSuffix_shouldGenerateSchemaFilenamesWithSuffix() async throws { @@ -882,13 +870,7 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -899,7 +881,7 @@ class ApolloCodegenTests: XCTestCase { directoryURL.appendingPathComponent("Sources/Schema/Enums/SkinCovering.enum.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Schema/Enums/SkinCovering.enum.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Schema/Enums/RelativeSize.enum.graphql.swift").path, - + directoryURL.appendingPathComponent("Sources/Schema/Interfaces/Pet.interface.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Schema/Interfaces/Animal.interface.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Schema/Interfaces/WarmBlooded.interface.graphql.swift").path, @@ -929,6 +911,7 @@ class ApolloCodegenTests: XCTestCase { directoryURL.appendingPathComponent("Sources/Schema/CustomScalars/ID.scalar.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/AllAnimalsQuery.graphql.swift").path, + directoryURL.appendingPathComponent("Sources/Operations/Queries/AllAnimalsDeferQuery.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/AllAnimalsIncludeSkipQuery.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/ClassroomPetsQuery.graphql.swift").path, directoryURL.appendingPathComponent("Sources/Operations/Queries/DogQuery.graphql.swift").path, @@ -960,12 +943,11 @@ class ApolloCodegenTests: XCTestCase { ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value + let filePaths = await fileManager.writtenFiles // then expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) } func test_fileGenerators_givenTestMockOutput_absolutePath_shouldGenerateTestMocks() async throws { @@ -994,19 +976,11 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - if path.contains("/TestMocks/") { - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) - let expectedPaths: Set = [ + let expectedPaths = [ directoryURL.appendingPathComponent("TestMocks/Height+Mock.graphql.swift").path, directoryURL.appendingPathComponent("TestMocks/Query+Mock.graphql.swift").path, directoryURL.appendingPathComponent("TestMocks/Cat+Mock.graphql.swift").path, @@ -1032,12 +1006,11 @@ class ApolloCodegenTests: XCTestCase { ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value + let filePaths = await fileManager.writtenFiles // then - expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + expect(filePaths).to(contain(expectedPaths)) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) } // MARK: Custom Root URL Tests @@ -1070,13 +1043,7 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -1117,6 +1084,7 @@ class ApolloCodegenTests: XCTestCase { directoryURL.appendingPathComponent("RelativePath/Sources/Schema/CustomScalars/ID.swift").path, directoryURL.appendingPathComponent("RelativePath/Sources/Operations/Queries/AllAnimalsQuery.graphql.swift").path, + directoryURL.appendingPathComponent("RelativePath/Sources/Operations/Queries/AllAnimalsDeferQuery.graphql.swift").path, directoryURL.appendingPathComponent("RelativePath/Sources/Operations/Queries/DogQuery.graphql.swift").path, directoryURL.appendingPathComponent("RelativePath/Sources/Operations/Queries/AllAnimalsIncludeSkipQuery.graphql.swift").path, directoryURL.appendingPathComponent("RelativePath/Sources/Operations/Queries/ClassroomPetsQuery.graphql.swift").path, @@ -1149,14 +1117,14 @@ class ApolloCodegenTests: XCTestCase { ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value + let filePaths = await fileManager.writtenFiles // then expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) } + @MainActor func test_fileGenerators_givenCustomRootDirectoryPath_operations_absolute__shouldGenerateFilesWithCustomRootPath() async throws { // given let schemaPath = ApolloCodegenInternalTestHelpers.Resources.AnimalKingdom.Schema.path @@ -1185,13 +1153,7 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -1232,6 +1194,7 @@ class ApolloCodegenTests: XCTestCase { directoryURL.appendingPathComponent("RelativePath/Sources/CustomScalars/ID.swift").path, directoryURL.appendingPathComponent("RelativeOperations/Queries/AllAnimalsQuery.graphql.swift").path, + directoryURL.appendingPathComponent("RelativeOperations/Queries/AllAnimalsDeferQuery.graphql.swift").path, directoryURL.appendingPathComponent("RelativeOperations/Queries/DogQuery.graphql.swift").path, directoryURL.appendingPathComponent("RelativeOperations/Queries/AllAnimalsIncludeSkipQuery.graphql.swift").path, directoryURL.appendingPathComponent("RelativeOperations/Queries/ClassroomPetsQuery.graphql.swift").path, @@ -1264,35 +1227,34 @@ class ApolloCodegenTests: XCTestCase { ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value + let filePaths = await fileManager.writtenFiles // then expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) } // MARK: Old File Deletion Tests func test__fileDeletion__givenPruneGeneratedFiles_false__doesNotDeleteUnusedGeneratedFiles() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" ) - let testFile = try createFile( + let testFile = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "SchemaModule" ) - let testInSourcesFile = try createFile( + let testInSourcesFile = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "SchemaModule/Sources" ) - let testInOtherFolderFile = try createFile( + let testInOtherFolderFile = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "SchemaModule/OtherFolder" ) @@ -1314,43 +1276,43 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInSourcesFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderFile)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInSourcesFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderFile) }.to(beTrue()) } func test__fileDeletion__givenGeneratedFilesExist_InSchemaModuleDirectory_deletesOnlyGeneratedFiles() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" ) - let testFile = try createFile( + let testFile = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "SchemaModule" ) - let testInSourcesFile = try createFile( + let testInSourcesFile = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "SchemaModule/Sources" ) - let testInOtherFolderFile = try createFile( + let testInOtherFolderFile = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "SchemaModule/OtherFolder" ) - let testUserFile = try createFile( + let testUserFile = try await createFile( filename: "TestUserFileA.swift", inDirectory: "SchemaModule" ) - let testInSourcesUserFile = try createFile( + let testInSourcesUserFile = try await createFile( filename: "TestUserFileB.swift", inDirectory: "SchemaModule/Sources" ) - let testInOtherFolderUserFile = try createFile( + let testInOtherFolderUserFile = try await createFile( filename: "TestUserFileC.swift", inDirectory: "SchemaModule/OtherFolder" ) @@ -1371,40 +1333,40 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testFile)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInSourcesFile)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderFile)).to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testFile) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInSourcesFile) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderFile) }.to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInSourcesUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderUserFile)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInSourcesUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderUserFile) }.to(beTrue()) } func test__fileDeletion__givenGeneratedFilesExist_InOperationAbsoluteDirectory_deletesOnlyGeneratedFiles() async throws { // given let absolutePath = "OperationPath" - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" ) - let testFile = try createFile( + let testFile = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: absolutePath ) - let testInChildFile = try createFile( + let testInChildFile = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "\(absolutePath)/Child" ) - let testUserFile = try createFile( + let testUserFile = try await createFile( filename: "TestFileA.swift", inDirectory: absolutePath ) - let testInChildUserFile = try createFile( + let testInChildUserFile = try await createFile( filename: "TestFileB.swift", inDirectory: "\(absolutePath)/Child" ) @@ -1425,55 +1387,54 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testFile)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInChildFile)).to(beFalse()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInChildUserFile)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testFile) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInChildFile) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInChildUserFile) }.to(beTrue()) } func test__fileDeletion__givenGeneratedFilesExist_InOperationRelativeDirectories_deletesOnlyRelativeGeneratedFilesInOperationSearchPaths() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql", inDirectory: "code" ) - let testGeneratedFileInRootPath = try createFile( + let testGeneratedFileInRootPath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code" ) - let testGeneratedFileInChildPath = try createFile( + let testGeneratedFileInChildPath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child" ) - let testGeneratedFileInNestedChildPath = try createFile( + let testGeneratedFileInNestedChildPath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/one/two" ) - let testGeneratedFileNotInRelativePath = try createFile( + let testGeneratedFileNotInRelativePath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: nil ) - let testGeneratedFileNotInRelativeChildPath = try createFile( + let testGeneratedFileNotInRelativeChildPath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child" ) - let testUserFileInRootPath = try createFile( + let testUserFileInRootPath = try await createFile( filename: "TestUserFileA.swift", inDirectory: "code" ) - let testUserFileInChildPath = try createFile( + let testUserFileInChildPath = try await createFile( filename: "TestUserFileB.swift", inDirectory: "code/child" ) - let testUserFileInNestedChildPath = try createFile( + let testUserFileInNestedChildPath = try await createFile( filename: "TestUserFileC.swift", inDirectory: "code/one/two" ) @@ -1494,59 +1455,57 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath)).to(beFalse()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath)).to(beTrue()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath) }.to(beTrue()) } func test__fileDeletion__givenGeneratedFilesExist_InOperationRelativeDirectories_operationSearchPathWithoutDirectories_deletesOnlyRelativeGeneratedFilesInOperationSearchPaths() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "code.graphql" ) - let testGeneratedFileInRootPath = try createFile( + let testGeneratedFileInRootPath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code" ) - let testGeneratedFileInChildPath = try createFile( + let testGeneratedFileInChildPath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child" ) - let testGeneratedFileInNestedChildPath = try createFile( + let testGeneratedFileInNestedChildPath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/one/two" ) - let testGeneratedFileNotInRelativePath = try createFile( + let testGeneratedFileNotInRelativePath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: nil ) - let testGeneratedFileNotInRelativeChildPath = try createFile( + let testGeneratedFileNotInRelativeChildPath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child" ) - let testUserFileInRootPath = try createFile( + let testUserFileInRootPath = try await createFile( filename: "TestUserFileA.swift", inDirectory: "code" ) - let testUserFileInChildPath = try createFile( + let testUserFileInChildPath = try await createFile( filename: "TestUserFileB.swift", inDirectory: "code/child" ) - let testUserFileInNestedChildPath = try createFile( + let testUserFileInNestedChildPath = try await createFile( filename: "TestUserFileC.swift", inDirectory: "code/one/two" ) @@ -1567,28 +1526,29 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath)).to(beTrue()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath)).to(beTrue()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath) }.to(beTrue()) } func test__fileDeletion__inOperationRelativeDirectory__whenSymlinkIsUsed() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") let schemaDirectory = "SchemaModule" let codeDirectory = "code" let relativeSubPath = "Operations" let operationFilename = "TestQuery.graphql" - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: operationFilename, @@ -1600,8 +1560,8 @@ class ApolloCodegenTests: XCTestCase { let fileValidationPath = symLinkDestURL.appendingPathComponent("\(operationFilename).swift").path //setup symlink folder - try testFileManager.fileManager.createDirectory(at: symLinkDestURL, withIntermediateDirectories: true) - try testFileManager.fileManager.createSymbolicLink(at: symLinkURL, withDestinationURL: symLinkDestURL) + try await testFileManager.fileManager.createDirectory(at: symLinkDestURL, withIntermediateDirectories: true) + try await testFileManager.fileManager.createSymbolicLink(at: symLinkURL, withDestinationURL: symLinkDestURL) // when let config = ApolloCodegenConfiguration.mock( @@ -1623,77 +1583,77 @@ class ApolloCodegenTests: XCTestCase { // running codegen multiple times to validate symlink related file creation/deletion bug try await ApolloCodegen.build(with: config, withRootURL: directoryURL) - expect(ApolloFileManager.default.doesFileExist(atPath: fileValidationPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: fileValidationPath) }.to(beTrue()) try await ApolloCodegen.build(with: config, withRootURL: directoryURL) - expect(ApolloFileManager.default.doesFileExist(atPath: fileValidationPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: fileValidationPath) }.to(beTrue()) try await ApolloCodegen.build(with: config, withRootURL: directoryURL) - expect(ApolloFileManager.default.doesFileExist(atPath: fileValidationPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: fileValidationPath) }.to(beTrue()) } func test__fileDeletion__givenGeneratedFilesExist_InOperationRelativeDirectoriesWithSubPath_deletesOnlyRelativeGeneratedFilesInOperationSearchPaths() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - let testGeneratedFileInRootPath = try createFile( + let testGeneratedFileInRootPath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code" ) - let testGeneratedFileInChildPath = try createFile( + let testGeneratedFileInChildPath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child" ) - let testGeneratedFileInNestedChildPath = try createFile( + let testGeneratedFileInNestedChildPath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/one/two" ) - let testGeneratedFileNotInRelativePath = try createFile( + let testGeneratedFileNotInRelativePath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: nil ) - let testGeneratedFileNotInRelativeChildPath = try createFile( + let testGeneratedFileNotInRelativeChildPath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child" ) - let testGeneratedFileInRootPathSubpath = try createFile( + let testGeneratedFileInRootPathSubpath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code/subpath" ) - let testGeneratedFileInChildPathSubpath = try createFile( + let testGeneratedFileInChildPathSubpath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child/subpath" ) - let testGeneratedFileInNestedChildPathSubpath = try createFile( + let testGeneratedFileInNestedChildPathSubpath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/one/two/subpath" ) - let testGeneratedFileNotInRelativePathSubpath = try createFile( + let testGeneratedFileNotInRelativePathSubpath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: "subpath" ) - let testGeneratedFileNotInRelativeChildPathSubpath = try createFile( + let testGeneratedFileNotInRelativeChildPathSubpath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child/subpath" ) - let testUserFileInRootPath = try createOperationFile( + let testUserFileInRootPath = try await createOperationFile( type: .query, named: "OperationA", filename: "TestUserFileOperationA.graphql", inDirectory: "code" ) - let testUserFileInChildPath = try createOperationFile( + let testUserFileInChildPath = try await createOperationFile( type: .query, named: "OperationB", filename: "TestUserFileOperationB.graphql", inDirectory: "code/child" ) - let testUserFileInNestedChildPath = try createOperationFile( + let testUserFileInNestedChildPath = try await createOperationFile( type: .query, named: "OperationC", filename: "TestUserFileOperationC.graphql", @@ -1716,92 +1676,102 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath)).to(beTrue()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath)).to(beTrue()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPathSubpath)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPathSubpath)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPathSubpath)).to(beFalse()) - - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePathSubpath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPathSubpath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath) } + .to(beTrue()) + + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath) } + .to(beTrue()) + + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPathSubpath) } + .to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPathSubpath) } + .to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPathSubpath) } + .to(beFalse()) + + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePathSubpath) } + .to(beTrue()) + await expect { + await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPathSubpath) + } + .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath) }.to(beTrue()) } func test__fileDeletion__givenGeneratedFilesExist_InOperationRelativeDirectoriesWithSubPath_operationSearchPathWithNoDirectories_deletesOnlyRelativeGeneratedFilesInOperationSearchPaths() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "code.graphql" ) - let testGeneratedFileInRootPath = try createFile( + let testGeneratedFileInRootPath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code" ) - let testGeneratedFileInChildPath = try createFile( + let testGeneratedFileInChildPath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child" ) - let testGeneratedFileInNestedChildPath = try createFile( + let testGeneratedFileInNestedChildPath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/one/two" ) - let testGeneratedFileNotInRelativePath = try createFile( + let testGeneratedFileNotInRelativePath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: nil ) - let testGeneratedFileNotInRelativeChildPath = try createFile( + let testGeneratedFileNotInRelativeChildPath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child" ) - let testGeneratedFileInRootPathSubpath = try createFile( + let testGeneratedFileInRootPathSubpath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code/subpath" ) - let testGeneratedFileInChildPathSubpath = try createFile( + let testGeneratedFileInChildPathSubpath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child/subpath" ) - let testGeneratedFileInNestedChildPathSubpath = try createFile( + let testGeneratedFileInNestedChildPathSubpath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/one/two/subpath" ) - let testGeneratedFileNotInRelativePathSubpath = try createFile( + let testGeneratedFileNotInRelativePathSubpath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: "subpath" ) - let testGeneratedFileNotInRelativeChildPathSubpath = try createFile( + let testGeneratedFileNotInRelativeChildPathSubpath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child/subpath" ) - let testUserFileInRootPath = try createOperationFile( + let testUserFileInRootPath = try await createOperationFile( type: .query, named: "OperationA", filename: "TestUserFileOperationA.graphql", inDirectory: "code" ) - let testUserFileInChildPath = try createOperationFile( + let testUserFileInChildPath = try await createOperationFile( type: .query, named: "OperationB", filename: "TestUserFileOperationB.graphql", inDirectory: "code/child" ) - let testUserFileInNestedChildPath = try createOperationFile( + let testUserFileInNestedChildPath = try await createOperationFile( type: .query, named: "OperationC", filename: "TestUserFileOperationC.graphql", @@ -1824,86 +1794,92 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath) } + .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath) } + .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPathSubpath)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPathSubpath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPathSubpath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPathSubpath) } + .to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPathSubpath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPathSubpath) } + .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePathSubpath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPathSubpath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePathSubpath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPathSubpath) }.to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath) }.to(beTrue()) } func test__fileDeletion__givenGeneratedFilesExist_InOperationRelativeDirectoriesWithSubPath_operationSearchPathWithoutGlobstar_deletesOnlyRelativeGeneratedFilesInOperationSearchPaths() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - let testGeneratedFileInRootPath = try createFile( + let testGeneratedFileInRootPath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code" ) - let testGeneratedFileInChildPath = try createFile( + let testGeneratedFileInChildPath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child" ) - let testGeneratedFileInNestedChildPath = try createFile( + let testGeneratedFileInNestedChildPath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/child/A" ) - let testGeneratedFileNotInRelativePath = try createFile( + let testGeneratedFileNotInRelativePath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: nil ) - let testGeneratedFileNotInRelativeChildPath = try createFile( + let testGeneratedFileNotInRelativeChildPath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child" ) - let testGeneratedFileInRootPathSubpath = try createFile( + let testGeneratedFileInRootPathSubpath = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: "code/subpath" ) - let testGeneratedFileInChildPathSubpath = try createFile( + let testGeneratedFileInChildPathSubpath = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "code/child/subpath" ) - let testGeneratedFileInNestedChildPathSubpath = try createFile( + let testGeneratedFileInNestedChildPathSubpath = try await createFile( filename: "TestGeneratedC.graphql.swift", inDirectory: "code/child/next/subpath" ) - let testGeneratedFileNotInRelativePathSubpath = try createFile( + let testGeneratedFileNotInRelativePathSubpath = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: "subpath" ) - let testGeneratedFileNotInRelativeChildPathSubpath = try createFile( + let testGeneratedFileNotInRelativeChildPathSubpath = try await createFile( filename: "TestGeneratedE.graphql.swift", inDirectory: "other/child/subpath" ) - let testUserFileInRootPath = try createOperationFile( + let testUserFileInRootPath = try await createOperationFile( type: .query, named: "OperationA", filename: "TestUserFileOperationA.graphql", inDirectory: "code" ) - let testUserFileInChildPath = try createOperationFile( + let testUserFileInChildPath = try await createOperationFile( type: .query, named: "OperationB", filename: "TestUserFileOperationB.graphql", inDirectory: "code/child" ) - let testUserFileInNestedChildPath = try createOperationFile( + let testUserFileInNestedChildPath = try await createOperationFile( type: .query, named: "OperationC", filename: "TestUserFileOperationC.graphql", @@ -1926,50 +1902,56 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPath) } + .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPath) } + .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPathSubpath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPathSubpath)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPathSubpath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInRootPathSubpath) } + .to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInChildPathSubpath) } + .to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileInNestedChildPathSubpath) } + .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePathSubpath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPathSubpath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativePathSubpath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testGeneratedFileNotInRelativeChildPathSubpath) }.to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInRootPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInChildPath) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFileInNestedChildPath) }.to(beTrue()) } func test__fileDeletion__givenGeneratedTestMockFilesExist_InAbsoluteDirectory_deletesOnlyGeneratedFiles() async throws { // given let absolutePath = "TestMocksPath" - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" ) - let testFile = try createFile( + let testFile = try await createFile( filename: "TestGeneratedA.graphql.swift", inDirectory: absolutePath ) - let testInChildFile = try createFile( + let testInChildFile = try await createFile( filename: "TestGeneratedB.graphql.swift", inDirectory: "\(absolutePath)/Child" ) - let testUserFile = try createFile( + let testUserFile = try await createFile( filename: "TestFileA.swift", inDirectory: absolutePath ) - let testInChildUserFile = try createFile( + let testInChildUserFile = try await createFile( filename: "TestFileB.swift", inDirectory: "\(absolutePath)/Child" ) @@ -1991,41 +1973,41 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testFile)).to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInChildFile)).to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testFile) }.to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInChildFile) }.to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInChildUserFile)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInChildUserFile) }.to(beTrue()) } func test__fileDeletion__givenGeneratedTestMockFilesExist_InSwiftPackageDirectory_deletesOnlyGeneratedFiles() async throws { // given - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" ) - let testInTestMocksFolderFile = try createFile( + let testInTestMocksFolderFile = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: "SchemaModule/TestMocks" ) - let testUserFile = try createFile( + let testUserFile = try await createFile( filename: "TestUserFileA.swift", inDirectory: "SchemaModule" ) - let testInSourcesUserFile = try createFile( + let testInSourcesUserFile = try await createFile( filename: "TestUserFileB.swift", inDirectory: "SchemaModule/Sources" ) - let testInOtherFolderUserFile = try createFile( + let testInOtherFolderUserFile = try await createFile( filename: "TestUserFileC.swift", inDirectory: "SchemaModule/OtherFolder" ) - let testInTestMocksFolderUserFile = try createFile( + let testInTestMocksFolderUserFile = try await createFile( filename: "TestUserFileD.swift", inDirectory: "SchemaModule/TestMocks" ) @@ -2047,43 +2029,43 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderFile)).to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderFile) }.to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInSourcesUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderUserFile)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInSourcesUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderUserFile) }.to(beTrue()) } func test__fileDeletion__givenGeneratedTestMockFilesExist_InSwiftPackageWithCustomTargetNameDirectory_deletesOnlyGeneratedFiles() async throws { // given let testMockTargetName = "ApolloTestTarget" - try createFile(containing: schemaData, named: "schema.graphqls") + try await createFile(containing: schemaData, named: "schema.graphqls") - try createOperationFile( + try await createOperationFile( type: .query, named: "TestQuery", filename: "TestQuery.graphql" ) - let testInTestMocksFolderFile = try createFile( + let testInTestMocksFolderFile = try await createFile( filename: "TestGeneratedD.graphql.swift", inDirectory: "SchemaModule/\(testMockTargetName)" ) - let testUserFile = try createFile( + let testUserFile = try await createFile( filename: "TestUserFileA.swift", inDirectory: "SchemaModule" ) - let testInSourcesUserFile = try createFile( + let testInSourcesUserFile = try await createFile( filename: "TestUserFileB.swift", inDirectory: "SchemaModule/Sources" ) - let testInOtherFolderUserFile = try createFile( + let testInOtherFolderUserFile = try await createFile( filename: "TestUserFileC.swift", inDirectory: "SchemaModule/OtherFolder" ) - let testInTestMocksFolderUserFile = try createFile( + let testInTestMocksFolderUserFile = try await createFile( filename: "TestUserFileD.swift", inDirectory: "SchemaModule/\(testMockTargetName)" ) @@ -2105,19 +2087,19 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderFile)).to(beFalse()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderFile) }.to(beFalse()) - expect(ApolloFileManager.default.doesFileExist(atPath: testUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInSourcesUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderUserFile)).to(beTrue()) - expect(ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderUserFile)).to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInSourcesUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInOtherFolderUserFile) }.to(beTrue()) + await expect { await ApolloFileManager.default.doesFileExist(atPath: testInTestMocksFolderUserFile) }.to(beTrue()) } // MARK: Suffix Renaming Tests func test__fileRenaming__givenGeneratedPreSuffixFilesExist_withAppendSchemaFilenameSuffix_true_shouldRenameFileWithSuffix() async throws { // given - try createFile( + try await createFile( containing: """ type Query { allBooks: [Book!]! @@ -2137,7 +2119,7 @@ class ApolloCodegenTests: XCTestCase { named: "schema.graphqls" ) - try createFile( + try await createFile( containing: """ query AllBooks { allBooks { @@ -2155,10 +2137,10 @@ class ApolloCodegenTests: XCTestCase { let customScalarsDirectory = "SchemaModule/Sources/Schema/CustomScalars" let preSuffixFilename = "CustomDate.swift" - try createFile(filename: preSuffixFilename, inDirectory: customScalarsDirectory) + try await createFile(filename: preSuffixFilename, inDirectory: customScalarsDirectory) - expect(ApolloFileManager.default.doesFileExist( - atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)")) + await expect { await ApolloFileManager.default.doesFileExist( + atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)") } .to(beTrue()) // when @@ -2179,17 +2161,17 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist( - atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/CustomDate.scalar.swift")) + await expect { await ApolloFileManager.default.doesFileExist( + atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/CustomDate.scalar.swift") } .to(beTrue()) - expect(ApolloFileManager.default.doesFileExist( - atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)")) + await expect { await ApolloFileManager.default.doesFileExist( + atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)") } .to(beFalse()) } func test__fileRenaming__givenGeneratedPreSuffixFilesExist_withAppendSchemaFilenameSuffix_false_shouldNotRenameFile() async throws { // given - try createFile( + try await createFile( containing: """ type Query { allBooks: [Book!]! @@ -2209,7 +2191,7 @@ class ApolloCodegenTests: XCTestCase { named: "schema.graphqls" ) - try createFile( + try await createFile( containing: """ query AllBooks { allBooks { @@ -2227,10 +2209,10 @@ class ApolloCodegenTests: XCTestCase { let customScalarsDirectory = "SchemaModule/Sources/Schema/CustomScalars" let preSuffixFilename = "CustomDate.swift" - try createFile(filename: preSuffixFilename, inDirectory: customScalarsDirectory) + try await createFile(filename: preSuffixFilename, inDirectory: customScalarsDirectory) - expect(ApolloFileManager.default.doesFileExist( - atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)")) + await expect { await ApolloFileManager.default.doesFileExist( + atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)") } .to(beTrue()) // when @@ -2251,11 +2233,11 @@ class ApolloCodegenTests: XCTestCase { try await ApolloCodegen.build(with: config, withRootURL: directoryURL) // then - expect(ApolloFileManager.default.doesFileExist( - atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/CustomDate.scalar.swift")) + await expect { await ApolloFileManager.default.doesFileExist( + atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/CustomDate.scalar.swift") } .to(beFalse()) - expect(ApolloFileManager.default.doesFileExist( - atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)")) + await expect { await ApolloFileManager.default.doesFileExist( + atPath: self.directoryURL.relativePath + "/\(customScalarsDirectory)/\(preSuffixFilename)") } .to(beTrue()) } @@ -2645,8 +2627,8 @@ class ApolloCodegenTests: XCTestCase { } """.data(using: .utf8)! - try createFile(containing: schemaDefData, named: "schema.graphqls") - try createFile(containing: operationData, named: "operation.graphql") + try await createFile(containing: schemaDefData, named: "schema.graphqls") + try await createFile(containing: operationData, named: "operation.graphql") let config = ApolloCodegenConfiguration.mock( input: .init( @@ -2724,27 +2706,27 @@ class ApolloCodegenTests: XCTestCase { // MARK: Path Match Exclusion Tests - func test__match__givenFilesInSpecialExcludedPaths_shouldNotReturnExcludedPaths() throws { + func test__match__givenFilesInSpecialExcludedPaths_shouldNotReturnExcludedPaths() async throws { // given - try createFile(filename: "included.file") - - try createFile(filename: "excludedBuildFolder.file", inDirectory: ".build") - try createFile(filename: "excludedBuildSubfolderOne.file", inDirectory: ".build/subfolder") - try createFile(filename: "excludedBuildSubfolderTwo.file", inDirectory: ".build/subfolder/two") - try createFile(filename: "excludedNestedOneBuildFolder.file", inDirectory: "nested/.build") - try createFile(filename: "excludedNestedTwoBuildFolder.file", inDirectory: "nested/two/.build") - - try createFile(filename: "excludedSwiftpmFolder.file", inDirectory: ".swiftpm") - try createFile(filename: "excludedSwiftpmSubfolderOne.file", inDirectory: ".swiftpm/subfolder") - try createFile(filename: "excludedSwiftpmSubfolderTwo.file", inDirectory: ".swiftpm/subfolder/two") - try createFile(filename: "excludedNestedOneSwiftpmFolder.file", inDirectory: "nested/.swiftpm") - try createFile(filename: "excludedNestedTwoSwiftpmFolder.file", inDirectory: "nested/two/.swiftpm") - - try createFile(filename: "excludedPodsFolder.file", inDirectory: ".Pods") - try createFile(filename: "excludedPodsSubfolderOne.file", inDirectory: ".Pods/subfolder") - try createFile(filename: "excludedPodsSubfolderTwo.file", inDirectory: ".Pods/subfolder/two") - try createFile(filename: "excludedNestedOnePodsFolder.file", inDirectory: "nested/.Pods") - try createFile(filename: "excludedNestedTwoPodsFolder.file", inDirectory: "nested/two/.Pods") + try await createFile(filename: "included.file") + + try await createFile(filename: "excludedBuildFolder.file", inDirectory: ".build") + try await createFile(filename: "excludedBuildSubfolderOne.file", inDirectory: ".build/subfolder") + try await createFile(filename: "excludedBuildSubfolderTwo.file", inDirectory: ".build/subfolder/two") + try await createFile(filename: "excludedNestedOneBuildFolder.file", inDirectory: "nested/.build") + try await createFile(filename: "excludedNestedTwoBuildFolder.file", inDirectory: "nested/two/.build") + + try await createFile(filename: "excludedSwiftpmFolder.file", inDirectory: ".swiftpm") + try await createFile(filename: "excludedSwiftpmSubfolderOne.file", inDirectory: ".swiftpm/subfolder") + try await createFile(filename: "excludedSwiftpmSubfolderTwo.file", inDirectory: ".swiftpm/subfolder/two") + try await createFile(filename: "excludedNestedOneSwiftpmFolder.file", inDirectory: "nested/.swiftpm") + try await createFile(filename: "excludedNestedTwoSwiftpmFolder.file", inDirectory: "nested/two/.swiftpm") + + try await createFile(filename: "excludedPodsFolder.file", inDirectory: ".Pods") + try await createFile(filename: "excludedPodsSubfolderOne.file", inDirectory: ".Pods/subfolder") + try await createFile(filename: "excludedPodsSubfolderTwo.file", inDirectory: ".Pods/subfolder/two") + try await createFile(filename: "excludedNestedOnePodsFolder.file", inDirectory: "nested/.Pods") + try await createFile(filename: "excludedNestedTwoPodsFolder.file", inDirectory: "nested/two/.Pods") // when let matches = try ApolloCodegen.match( @@ -2758,27 +2740,27 @@ class ApolloCodegenTests: XCTestCase { expect(matches.contains(where: { $0.contains(".Pods") })).to(beFalse()) } - func test__match__givenFilesInSpecialExcludedPaths_usingRelativeDirectory_shouldNotReturnExcludedPaths() throws { + func test__match__givenFilesInSpecialExcludedPaths_usingRelativeDirectory_shouldNotReturnExcludedPaths() async throws { // given - try createFile(filename: "included.file") - - try createFile(filename: "excludedBuildFolder.file", inDirectory: ".build") - try createFile(filename: "excludedBuildSubfolderOne.file", inDirectory: ".build/subfolder") - try createFile(filename: "excludedBuildSubfolderTwo.file", inDirectory: ".build/subfolder/two") - try createFile(filename: "excludedNestedOneBuildFolder.file", inDirectory: "nested/.build") - try createFile(filename: "excludedNestedTwoBuildFolder.file", inDirectory: "nested/two/.build") - - try createFile(filename: "excludedSwiftpmFolder.file", inDirectory: ".swiftpm") - try createFile(filename: "excludedSwiftpmSubfolderOne.file", inDirectory: ".swiftpm/subfolder") - try createFile(filename: "excludedSwiftpmSubfolderTwo.file", inDirectory: ".swiftpm/subfolder/two") - try createFile(filename: "excludedNestedOneSwiftpmFolder.file", inDirectory: "nested/.swiftpm") - try createFile(filename: "excludedNestedTwoSwiftpmFolder.file", inDirectory: "nested/two/.swiftpm") - - try createFile(filename: "excludedPodsFolder.file", inDirectory: ".Pods") - try createFile(filename: "excludedPodsSubfolderOne.file", inDirectory: ".Pods/subfolder") - try createFile(filename: "excludedPodsSubfolderTwo.file", inDirectory: ".Pods/subfolder/two") - try createFile(filename: "excludedNestedOnePodsFolder.file", inDirectory: "nested/.Pods") - try createFile(filename: "excludedNestedTwoPodsFolder.file", inDirectory: "nested/two/.Pods") + try await createFile(filename: "included.file") + + try await createFile(filename: "excludedBuildFolder.file", inDirectory: ".build") + try await createFile(filename: "excludedBuildSubfolderOne.file", inDirectory: ".build/subfolder") + try await createFile(filename: "excludedBuildSubfolderTwo.file", inDirectory: ".build/subfolder/two") + try await createFile(filename: "excludedNestedOneBuildFolder.file", inDirectory: "nested/.build") + try await createFile(filename: "excludedNestedTwoBuildFolder.file", inDirectory: "nested/two/.build") + + try await createFile(filename: "excludedSwiftpmFolder.file", inDirectory: ".swiftpm") + try await createFile(filename: "excludedSwiftpmSubfolderOne.file", inDirectory: ".swiftpm/subfolder") + try await createFile(filename: "excludedSwiftpmSubfolderTwo.file", inDirectory: ".swiftpm/subfolder/two") + try await createFile(filename: "excludedNestedOneSwiftpmFolder.file", inDirectory: "nested/.swiftpm") + try await createFile(filename: "excludedNestedTwoSwiftpmFolder.file", inDirectory: "nested/two/.swiftpm") + + try await createFile(filename: "excludedPodsFolder.file", inDirectory: ".Pods") + try await createFile(filename: "excludedPodsSubfolderOne.file", inDirectory: ".Pods/subfolder") + try await createFile(filename: "excludedPodsSubfolderTwo.file", inDirectory: ".Pods/subfolder/two") + try await createFile(filename: "excludedNestedOnePodsFolder.file", inDirectory: "nested/.Pods") + try await createFile(filename: "excludedNestedTwoPodsFolder.file", inDirectory: "nested/two/.Pods") // when let matches = try ApolloCodegen.match( @@ -2911,7 +2893,7 @@ class ApolloCodegenTests: XCTestCase { func test__fileRendering__givenLocalCacheMutationQuery_whenSelectionSetInitializersEmpty_andFileMergingNone_shouldGenerateFullSelectionSetInitializers() async throws { // given - try createFile( + try await createFile( body: """ type Query { allAnimals: [Animal!] @@ -2924,7 +2906,7 @@ class ApolloCodegenTests: XCTestCase { filename: "schema.graphqls" ) - try createFile( + try await createFile( body: """ query TestOperation @apollo_client_ios_localCacheMutation { allAnimals { @@ -2938,7 +2920,7 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) let expectation = expectation(description: "Received local cache mutation file data.") - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in if path.hasSuffix("TestOperationLocalCacheMutation.graphql.swift") { expect(data?.asString).to(equalLineByLine(""" init( @@ -2983,14 +2965,14 @@ class ApolloCodegenTests: XCTestCase { ) // then - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) await fulfillment(of: [expectation], timeout: 1) } func test__fileRendering__givenLocalCacheMutationFragment_whenSelectionSetInitializersEmpty_andFileMergingNone_shouldGenerateFullSelectionSetInitializers() async throws { // given - try createFile( + try await createFile( body: """ type Query { allAnimals: [Animal!] @@ -3003,7 +2985,7 @@ class ApolloCodegenTests: XCTestCase { filename: "schema.graphqls" ) - try createFile( + try await createFile( body: """ query TestOperation { allAnimals { @@ -3021,7 +3003,7 @@ class ApolloCodegenTests: XCTestCase { let fileManager = MockApolloFileManager(strict: false) let expectation = expectation(description: "Received local cache mutation file data.") - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in if path.hasSuffix("PredatorFragment.graphql.swift") { expect(data?.asString).to(equalLineByLine(""" init( @@ -3067,7 +3049,7 @@ class ApolloCodegenTests: XCTestCase { ) // then - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect{ await fileManager.allClosuresCalled }.to(beTrue()) await fulfillment(of: [expectation], timeout: 1) } diff --git a/Tests/ApolloCodegenTests/ApolloSchemaDownloaderInternalTests.swift b/Tests/ApolloCodegenTests/ApolloSchemaDownloaderInternalTests.swift index 138b02358..3dd028a1c 100644 --- a/Tests/ApolloCodegenTests/ApolloSchemaDownloaderInternalTests.swift +++ b/Tests/ApolloCodegenTests/ApolloSchemaDownloaderInternalTests.swift @@ -1,4 +1,5 @@ import XCTest +import Nimble import ApolloInternalTestHelpers import ApolloCodegenInternalTestHelpers @testable import ApolloCodegenLib @@ -41,7 +42,7 @@ class ApolloSchemaDownloaderInternalTests: XCTestCase { withRootURL: nil ) - XCTAssertTrue(ApolloFileManager.default.doesFileExist(atPath: configuration.outputPath)) + await expect { await ApolloFileManager.default.doesFileExist(atPath: configuration.outputPath) }.to(beTrue()) let frontend = try await GraphQLJSFrontend() let source = try await frontend.makeSource(from: URL(fileURLWithPath: configuration.outputPath)) @@ -76,7 +77,7 @@ class ApolloSchemaDownloaderInternalTests: XCTestCase { withRootURL: nil ) - XCTAssertTrue(ApolloFileManager.default.doesFileExist(atPath: configuration.outputPath)) + await expect { await ApolloFileManager.default.doesFileExist(atPath: configuration.outputPath) }.to(beTrue()) let frontend = try await GraphQLJSFrontend() let source = try await frontend.makeSource(from: URL(fileURLWithPath: configuration.outputPath)) @@ -225,17 +226,17 @@ class ApolloSchemaDownloaderInternalTests: XCTestCase { // given let path = "./subfolder/output.test" - mockFileManager.base.changeCurrentDirectoryPath(TestFileHelper.sourceRootURL().path) + await mockFileManager.base.changeCurrentDirectoryPath(TestFileHelper.sourceRootURL().path) - mockFileManager.mock(closure: .fileExists({ path, isDirectory in + await mockFileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in let expected = TestFileHelper.sourceRootURL() .appendingPathComponent("subfolder/output.test").path @@ -257,17 +258,17 @@ class ApolloSchemaDownloaderInternalTests: XCTestCase { // given let path = "/absolute/path/subfolder/output.test" - mockFileManager.base.changeCurrentDirectoryPath(TestFileHelper.sourceRootURL().path) + await mockFileManager.base.changeCurrentDirectoryPath(TestFileHelper.sourceRootURL().path) - mockFileManager.mock(closure: .fileExists({ path, isDirectory in + await mockFileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in let expected = "/absolute/path/subfolder/output.test" // then @@ -288,17 +289,17 @@ class ApolloSchemaDownloaderInternalTests: XCTestCase { // given let path = "output.test" - mockFileManager.base.changeCurrentDirectoryPath(TestFileHelper.sourceRootURL().path) + await mockFileManager.base.changeCurrentDirectoryPath(TestFileHelper.sourceRootURL().path) - mockFileManager.mock(closure: .fileExists({ path, isDirectory in + await mockFileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in let expected = "/rootURL/path/output.test" // then diff --git a/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/FileGeneratorTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/FileGeneratorTests.swift index 4e38b0943..0e72b50c1 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/FileGeneratorTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/FileGeneratorTests.swift @@ -55,8 +55,9 @@ class FileGeneratorTests: XCTestCase { buildConfig() buildSubject() - fileManager.mock(closure: .createFile({ path, data, attributes in - let expected = self.fileTarget.resolvePath(forConfig: self.config) + let expected = self.fileTarget.resolvePath(forConfig: self.config) + + await fileManager.mock(closure: .createFile({ path, data, attributes in // then let actual = URL(fileURLWithPath: path).deletingLastPathComponent().path @@ -69,7 +70,7 @@ class FileGeneratorTests: XCTestCase { _ = try await subject.generate(forConfig: config, fileManager: fileManager) // then - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } func test__generate__shouldFirstUppercaseFilename() async throws { @@ -77,7 +78,7 @@ class FileGeneratorTests: XCTestCase { buildConfig() buildSubject() - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in let expected = "LowercasedType.graphql.swift" // then @@ -91,7 +92,7 @@ class FileGeneratorTests: XCTestCase { _ = try await subject.generate(forConfig: config, fileManager: fileManager) // then - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } func test__generate__shouldAddExtensionToFilePath() async throws { @@ -99,7 +100,7 @@ class FileGeneratorTests: XCTestCase { buildConfig() buildSubject(extension: "test") - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in let expected = "LowercasedType.test" // then @@ -113,7 +114,7 @@ class FileGeneratorTests: XCTestCase { _ = try await subject.generate(forConfig: config, fileManager: fileManager) // then - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } func test__generate__shouldWriteRenderedTemplate() async throws { @@ -124,7 +125,7 @@ class FileGeneratorTests: XCTestCase { let (actual, _) = template.render() let expectedData = actual.data(using: .utf8) - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in // then expect(data).to(equal(expectedData)) @@ -135,6 +136,6 @@ class FileGeneratorTests: XCTestCase { _ = try await subject.generate(forConfig: config, fileManager: fileManager) // then - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } } diff --git a/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/OperationManifestFileGeneratorTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/OperationManifestFileGeneratorTests.swift index a9f9ae6b5..14eba4ec2 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/OperationManifestFileGeneratorTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/OperationManifestFileGeneratorTests.swift @@ -81,15 +81,15 @@ class OperationManifestFileGeneratorTests: XCTestCase { ) ] - fileManager.mock(closure: .fileExists({ path, isDirectory in + await fileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in expect(path).to(equal("\(filePath).json")) return true @@ -98,7 +98,7 @@ class OperationManifestFileGeneratorTests: XCTestCase { // when try await subject.generate(operationManifest: manifest, fileManager: fileManager) - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenOperation_withPathExtension_shouldWriteToAbsolutePathWithSinglePathExtension() async throws { @@ -120,15 +120,15 @@ class OperationManifestFileGeneratorTests: XCTestCase { ) ] - fileManager.mock(closure: .fileExists({ path, isDirectory in + await fileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in expect(path).to(equal("\(filePath).json")) return true @@ -137,7 +137,7 @@ class OperationManifestFileGeneratorTests: XCTestCase { // when try await subject.generate(operationManifest: manifest, fileManager: fileManager) - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenOperation_shouldWriteToRelativePath() async throws { @@ -159,19 +159,20 @@ class OperationManifestFileGeneratorTests: XCTestCase { ) ] - fileManager.mock(closure: .fileExists({ path, isDirectory in + await fileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - fileManager.mock(closure: .createFile({ path, data, attributes in - let expectedPath = URL(fileURLWithPath: String(filePath.dropFirst(2)), relativeTo: self.subject.config.rootURL) - .resolvingSymlinksInPath() - .appendingPathExtension("json") - .path + let expectedPath = URL(fileURLWithPath: String(filePath.dropFirst(2)), relativeTo: self.subject.config.rootURL) + .resolvingSymlinksInPath() + .appendingPathExtension("json") + .path + + await fileManager.mock(closure: .createFile({ path, data, attributes in expect(path).to(equal(expectedPath)) return true @@ -180,7 +181,7 @@ class OperationManifestFileGeneratorTests: XCTestCase { // when try await subject.generate(operationManifest: manifest, fileManager: fileManager) - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenOperation_withPathExtension_shouldWriteToRelativePathWithSinglePathExtension() async throws { @@ -202,19 +203,20 @@ class OperationManifestFileGeneratorTests: XCTestCase { ) ] - fileManager.mock(closure: .fileExists({ path, isDirectory in + await fileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - fileManager.mock(closure: .createFile({ path, data, attributes in - let expectedPath = URL(fileURLWithPath: String(filePath.dropFirst(2)), relativeTo: self.subject.config.rootURL) - .resolvingSymlinksInPath() - .appendingPathExtension("json") - .path + let expectedPath = URL(fileURLWithPath: String(filePath.dropFirst(2)), relativeTo: self.subject.config.rootURL) + .resolvingSymlinksInPath() + .appendingPathExtension("json") + .path + + await fileManager.mock(closure: .createFile({ path, data, attributes in expect(path).to(equal(expectedPath)) return true @@ -223,7 +225,7 @@ class OperationManifestFileGeneratorTests: XCTestCase { // when try await subject.generate(operationManifest: manifest, fileManager: fileManager) - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenOperations_whenFileExists_shouldOverwrite() async throws { @@ -245,15 +247,15 @@ class OperationManifestFileGeneratorTests: XCTestCase { ) ] - fileManager.mock(closure: .fileExists({ path, isDirectory in + await fileManager.mock(closure: .fileExists({ path, isDirectory in return true })) - fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in + await fileManager.mock(closure: .createDirectory({ path, intermediateDirectories, attributes in // no-op })) - fileManager.mock(closure: .createFile({ path, data, attributes in + await fileManager.mock(closure: .createFile({ path, data, attributes in expect(path).to(equal("\(filePath).json")) expect(String(data: data!, encoding: .utf8)).to(equal( @@ -273,7 +275,7 @@ class OperationManifestFileGeneratorTests: XCTestCase { // when try await subject.generate(operationManifest: manifest, fileManager: fileManager) - expect(self.fileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.fileManager.allClosuresCalled }.to(beTrue()) } // MARK: - Template Type Selection Tests diff --git a/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/SchemaModuleFileGeneratorTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/SchemaModuleFileGeneratorTests.swift index dc0fc1c77..e3890356f 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/SchemaModuleFileGeneratorTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/FileGenerators/SchemaModuleFileGeneratorTests.swift @@ -34,7 +34,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { to: rootURL.path )) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in // then expect(path).to(equal(fileURL.path)) @@ -45,7 +45,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { _ = try await SchemaModuleFileGenerator.generate(configuration, fileManager: mockFileManager) // then - expect(self.mockFileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.mockFileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenModuleTypeEmbeddedInTarget_lowercaseSchemaName_shouldGenerateNamespaceFileWithCapitalizedName() async throws { @@ -58,7 +58,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { to: rootURL.path )) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in // then expect(path).to(equal(fileURL.path)) @@ -69,7 +69,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { _ = try await SchemaModuleFileGenerator.generate(configuration, fileManager: mockFileManager) // then - expect(self.mockFileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.mockFileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenModuleTypeEmbeddedInTarget_uppercaseSchemaName_shouldGenerateNamespaceFileWithUppercaseName() async throws { @@ -82,7 +82,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { to: rootURL.path )) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in // then expect(path).to(equal(fileURL.path)) @@ -93,7 +93,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { _ = try await SchemaModuleFileGenerator.generate(configuration, fileManager: mockFileManager) // then - expect(self.mockFileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.mockFileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenModuleTypeEmbeddedInTarget_capitalizedSchemaName_shouldGenerateNamespaceFileWithCapitalizedName() async throws { @@ -106,7 +106,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { to: rootURL.path )) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in // then expect(path).to(equal(fileURL.path)) @@ -117,7 +117,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { _ = try await SchemaModuleFileGenerator.generate(configuration, fileManager: mockFileManager) // then - expect(self.mockFileManager.allClosuresCalled).to(beTrue()) + await expect{ await self.mockFileManager.allClosuresCalled }.to(beTrue()) } func test__generate__givenModuleType_other_shouldNotGenerateFile() async throws { @@ -132,7 +132,7 @@ class SchemaModuleFileGeneratorTests: XCTestCase { to: rootURL.path )) - mockFileManager.mock(closure: .createFile({ path, data, attributes in + await mockFileManager.mock(closure: .createFile({ path, data, attributes in // then fail("Unexpected module file created at \(path)") @@ -143,6 +143,6 @@ class SchemaModuleFileGeneratorTests: XCTestCase { _ = try await SchemaModuleFileGenerator.generate(configuration, fileManager: mockFileManager) // then - expect(self.mockFileManager.allClosuresCalled).to(beFalse()) + await expect{ await self.mockFileManager.allClosuresCalled }.to(beFalse()) } } diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/DeferredFragmentsMetadataTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/DeferredFragmentsMetadataTemplateTests.swift index fd89addf9..3ccca8ce4 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/DeferredFragmentsMetadataTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/DeferredFragmentsMetadataTemplateTests.swift @@ -89,13 +89,16 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let root = DeferredFragmentIdentifier(label: "root", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.root: Data.AllAnimal.Root.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.root: Data.AllAnimal.Root.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -136,13 +139,16 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let root = DeferredFragmentIdentifier(label: "root", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.root: Data.AllAnimal.Root.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.root: Data.AllAnimal.Root.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -188,13 +194,16 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let root = DeferredFragmentIdentifier(label: "root", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.root: Data.AllAnimal.AsDog.Root.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.root: Data.AllAnimal.AsDog.Root.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -245,15 +254,18 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let one = DeferredFragmentIdentifier(label: "one", fieldPath: ["allAnimals"]) static let two = DeferredFragmentIdentifier(label: "two", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.one: Data.AllAnimal.AsDog.One.self, - DeferredFragmentIdentifiers.two: Data.AllAnimal.AsDog.Two.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.one: Data.AllAnimal.AsDog.One.self, + DeferredFragmentIdentifiers.two: Data.AllAnimal.AsDog.Two.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -310,15 +322,18 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let one = DeferredFragmentIdentifier(label: "one", fieldPath: ["allAnimals"]) static let two = DeferredFragmentIdentifier(label: "two", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.one: Data.AllAnimal.AsDog.One.self, - DeferredFragmentIdentifiers.two: Data.AllAnimal.AsCat.Two.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.one: Data.AllAnimal.AsDog.One.self, + DeferredFragmentIdentifiers.two: Data.AllAnimal.AsCat.Two.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -378,15 +393,18 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let outer = DeferredFragmentIdentifier(label: "outer", fieldPath: ["allAnimals"]) static let inner = DeferredFragmentIdentifier(label: "inner", fieldPath: ["allAnimals", "friend"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.outer: Data.AllAnimal.AsDog.Outer.self, - DeferredFragmentIdentifiers.inner: Data.AllAnimal.AsDog.Outer.Friend.AsCat.Inner.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.outer: Data.AllAnimal.AsDog.Outer.self, + DeferredFragmentIdentifiers.inner: Data.AllAnimal.AsDog.Outer.Friend.AsCat.Inner.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -431,13 +449,16 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let root = DeferredFragmentIdentifier(label: "root", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.root: AnimalFragment.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.root: AnimalFragment.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -484,13 +505,16 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let root = DeferredFragmentIdentifier(label: "root", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.root: DogFragment.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.root: DogFragment.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -540,13 +564,16 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let root = DeferredFragmentIdentifier(label: "root", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.root: Data.Root.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.root: Data.Root.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -596,13 +623,16 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { expect(rendered).to(equalLineByLine(""" // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let root = DeferredFragmentIdentifier(label: "root", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.root: Data.AsDog.Root.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.root: Data.AsDog.Root.self, + ] + ) """, atLine: 2, ignoringExtraLines: false) @@ -649,14 +679,17 @@ class DeferredFragmentsMetadataTemplateTests: XCTestCase { let rendered = renderSubject() expect(rendered).to(equalLineByLine(""" + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { static let deferredDog = DeferredFragmentIdentifier(label: "deferredDog", fieldPath: ["allAnimals"]) } - public static var deferredFragments: [DeferredFragmentIdentifier: any ApolloAPI.SelectionSet.Type]? {[ - DeferredFragmentIdentifiers.deferredDog: Data.AllAnimal.AsDog.DeferredDog.self, - DeferredFragmentIdentifiers.deferredDog: DogFragment.self, - ]} + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + deferredFragments: [ + DeferredFragmentIdentifiers.deferredDog: Data.AllAnimal.AsDog.DeferredDog.self, + DeferredFragmentIdentifiers.deferredDog: DogFragment.self, + ] + ) """, atLine: 4, ignoringExtraLines: false) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/FragmentTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/FragmentTemplateTests.swift index 57da584b7..4aac41024 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/FragmentTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/FragmentTemplateTests.swift @@ -1,20 +1,34 @@ -import XCTest +import ApolloCodegenInternalTestHelpers +import IR import Nimble +import XCTest + @testable import ApolloCodegenLib -import IR -import ApolloCodegenInternalTestHelpers -class FragmentTemplateTests: XCTestCase { +final class FragmentTemplateTests: XCTestCase, @unchecked Sendable { - var schemaSDL: String! - var document: String! - var ir: IRBuilder! - var fragment: IR.NamedFragment! - var subject: FragmentTemplate! + // MARK: - Helpers - override func setUp() { - super.setUp() - schemaSDL = """ + private func buildFragmentTemplate( + named fragmentName: String = "TestFragment", + config: ApolloCodegenConfiguration = .mock(), + schemaSDL: String = FragmentTemplateTests.defaultSchema, + document: String = FragmentTemplateTests.defaultDocument + ) async throws -> (fragment: NamedFragment, template: FragmentTemplate) { + let ir: IRBuilder = try await .mock(schema: schemaSDL, document: document) + let fragmentDefinition = try XCTUnwrap(ir.compilationResult[fragment: fragmentName]) + let fragment = await ir.build(fragment: fragmentDefinition) + return + ( + fragment, + FragmentTemplate( + fragment: fragment, + config: ApolloCodegen.ConfigurationContext(config: config) + ) + ) + } + + private static let defaultSchema: String = """ type Query { allAnimals: [Animal!] } @@ -24,59 +38,34 @@ class FragmentTemplateTests: XCTestCase { } """ - document = """ + private static let defaultDocument: String = """ fragment TestFragment on Query { allAnimals { species } } """ - } - override func tearDown() { - schemaSDL = nil - document = nil - ir = nil - fragment = nil - subject = nil - super.tearDown() - } - - // MARK: - Helpers - - private func buildSubjectAndFragment( - named fragmentName: String = "TestFragment", - config: ApolloCodegenConfiguration = .mock() - ) async throws { - ir = try await .mock(schema: schemaSDL, document: document) - let fragmentDefinition = try XCTUnwrap(ir.compilationResult[fragment: fragmentName]) - fragment = await ir.build(fragment: fragmentDefinition) - subject = FragmentTemplate( - fragment: fragment, - config: ApolloCodegen.ConfigurationContext(config: config) - ) - } - - private func renderSubject() -> String { - subject.renderBodyTemplate(nonFatalErrorRecorder: .init()).description + private func render(_ template: FragmentTemplate) -> String { + template.renderBodyTemplate(nonFatalErrorRecorder: .init()).description } // MARK: - Target Configuration Tests func test__target__givenModuleImports_targetHasModuleImports() async throws { // given - document = """ - fragment TestFragment on Query @import(module: "ModuleA") { - allAnimals { - species + let document = """ + fragment TestFragment on Query @import(module: "ModuleA") { + allAnimals { + species + } } - } - """ + """ // when - try await buildSubjectAndFragment() + let (_, template) = try await buildFragmentTemplate(document: document) - guard case let .operationFile(actual) = subject.target else { + guard case let .operationFile(actual) = template.target else { fail("expected operationFile target") return } @@ -90,43 +79,45 @@ class FragmentTemplateTests: XCTestCase { func test__render__givenFragment_generatesFragmentDeclarationDefinitionAndBoilerplate() async throws { // given let expected = - """ - struct TestFragment: TestSchema.SelectionSet, Fragment { - static var fragmentDefinition: StaticString { - #"fragment TestFragment on Query { __typename allAnimals { __typename species } }"# - } + """ + struct TestFragment: TestSchema.SelectionSet, Fragment { + static var fragmentDefinition: StaticString { + #"fragment TestFragment on Query { __typename allAnimals { __typename species } }"# + } - let __data: DataDict - init(_dataDict: DataDict) { __data = _dataDict } - """ + let __data: DataDict + init(_dataDict: DataDict) { __data = _dataDict } + """ // when - try await buildSubjectAndFragment() + let (_, template) = try await buildFragmentTemplate() - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) expect(String(actual.reversed())).to(equalLineByLine("\n}", ignoringExtraLines: true)) } - + func test__render__givenFragment_generatesFragmentDeclarationWithoutDefinition() async throws { // given let expected = - """ - struct TestFragment: TestSchema.SelectionSet, Fragment { - let __data: DataDict - init(_dataDict: DataDict) { __data = _dataDict } - """ + """ + struct TestFragment: TestSchema.SelectionSet, Fragment { + let __data: DataDict + init(_dataDict: DataDict) { __data = _dataDict } + """ // when - try await buildSubjectAndFragment(config: .mock( - options: .init( - operationDocumentFormat: .operationId + let (_, template) = try await buildFragmentTemplate( + config: .mock( + options: .init( + operationDocumentFormat: .operationId + ) ) - )) + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) @@ -135,25 +126,28 @@ class FragmentTemplateTests: XCTestCase { func test__render__givenLowercaseFragment_generatesTitleCaseTypeName() async throws { // given - document = """ - fragment testFragment on Query { - allAnimals { - species + let document = """ + fragment testFragment on Query { + allAnimals { + species + } } - } - """ + """ let expected = - """ - struct TestFragment: TestSchema.SelectionSet, Fragment { - static var fragmentDefinition: StaticString { - #"fragment testFragment on Query { __typename allAnimals { __typename species } }"# - """ + """ + struct TestFragment: TestSchema.SelectionSet, Fragment { + static var fragmentDefinition: StaticString { + #"fragment testFragment on Query { __typename allAnimals { __typename species } }"# + """ // when - try await buildSubjectAndFragment(named: "testFragment") + let (_, template) = try await buildFragmentTemplate( + named: "testFragment", + document: document + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) @@ -161,29 +155,33 @@ class FragmentTemplateTests: XCTestCase { func test__render__givenFragmentWithUnderscoreInName_rendersDeclarationWithName() async throws { // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } + let schemaSDL = """ + type Query { + allAnimals: [Animal!] + } - interface Animal { - species: String! - } - """ + interface Animal { + species: String! + } + """ - document = """ - fragment Test_Fragment on Animal { - species - } - """ + let document = """ + fragment Test_Fragment on Animal { + species + } + """ let expected = """ - struct Test_Fragment: TestSchema.SelectionSet, Fragment { - """ + struct Test_Fragment: TestSchema.SelectionSet, Fragment { + """ // when - try await buildSubjectAndFragment(named: "Test_Fragment") - let actual = renderSubject() + let (_, template) = try await buildFragmentTemplate( + named: "Test_Fragment", + schemaSDL: schemaSDL, + document: document + ) + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) @@ -191,29 +189,32 @@ class FragmentTemplateTests: XCTestCase { func test__render_parentType__givenFragmentTypeConditionAs_Object_rendersParentType() async throws { // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } + let schemaSDL = """ + type Query { + allAnimals: [Animal!] + } - type Animal { - species: String! - } - """ + type Animal { + species: String! + } + """ - document = """ - fragment TestFragment on Animal { - species - } - """ + let document = """ + fragment TestFragment on Animal { + species + } + """ let expected = """ - static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Animal } - """ + static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Animal } + """ // when - try await buildSubjectAndFragment() - let actual = renderSubject() + let (_, template) = try await buildFragmentTemplate( + schemaSDL: schemaSDL, + document: document + ) + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, atLine: 9, ignoringExtraLines: true)) @@ -221,29 +222,32 @@ class FragmentTemplateTests: XCTestCase { func test__render_parentType__givenFragmentTypeConditionAs_Interface_rendersParentType() async throws { // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } + let schemaSDL = """ + type Query { + allAnimals: [Animal!] + } - interface Animal { - species: String! - } - """ + interface Animal { + species: String! + } + """ - document = """ - fragment TestFragment on Animal { - species - } - """ + let document = """ + fragment TestFragment on Animal { + species + } + """ let expected = """ - static var __parentType: any ApolloAPI.ParentType { TestSchema.Interfaces.Animal } - """ + static var __parentType: any ApolloAPI.ParentType { TestSchema.Interfaces.Animal } + """ // when - try await buildSubjectAndFragment() - let actual = renderSubject() + let (_, template) = try await buildFragmentTemplate( + schemaSDL: schemaSDL, + document: document + ) + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, atLine: 9, ignoringExtraLines: true)) @@ -251,98 +255,109 @@ class FragmentTemplateTests: XCTestCase { func test__render_parentType__givenFragmentTypeConditionAs_Union_rendersParentType() async throws { // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } + let schemaSDL = """ + type Query { + allAnimals: [Animal!] + } - type Dog { - species: String! - } + type Dog { + species: String! + } - union Animal = Dog - """ + union Animal = Dog + """ - document = """ - fragment TestFragment on Animal { - ... on Dog { - species + let document = """ + fragment TestFragment on Animal { + ... on Dog { + species + } } - } - """ + """ let expected = """ - static var __parentType: any ApolloAPI.ParentType { TestSchema.Unions.Animal } - """ + static var __parentType: any ApolloAPI.ParentType { TestSchema.Unions.Animal } + """ // when - try await buildSubjectAndFragment() - let actual = renderSubject() + let (_, template) = try await buildFragmentTemplate( + schemaSDL: schemaSDL, + document: document + ) + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, atLine: 9, ignoringExtraLines: true)) } - func test__render__givenFragmentOnRootOperationTypeWithOnlyTypenameField_generatesFragmentDefinition_withNoSelections() async throws { + func + test__render__givenFragmentOnRootOperationTypeWithOnlyTypenameField_generatesFragmentDefinition_withNoSelections() + async throws + { // given - document = """ - fragment TestFragment on Query { - __typename - } - """ + let document = """ + fragment TestFragment on Query { + __typename + } + """ - try await buildSubjectAndFragment() + let (_, template) = try await buildFragmentTemplate( + document: document + ) let expected = """ - struct TestFragment: TestSchema.SelectionSet, Fragment { - static var fragmentDefinition: StaticString { - #"fragment TestFragment on Query { __typename }"# - } + struct TestFragment: TestSchema.SelectionSet, Fragment { + static var fragmentDefinition: StaticString { + #"fragment TestFragment on Query { __typename }"# + } - let __data: DataDict - init(_dataDict: DataDict) { __data = _dataDict } + let __data: DataDict + init(_dataDict: DataDict) { __data = _dataDict } - static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Query } - } + static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Query } + } - """ + """ // when - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected)) } - func test__render__givenFragmentWithOnlyTypenameField_generatesFragmentDefinition_withTypeNameSelection() async throws { + func test__render__givenFragmentWithOnlyTypenameField_generatesFragmentDefinition_withTypeNameSelection() async throws + { // given - document = """ - fragment TestFragment on Animal { - __typename - } - """ + let document = """ + fragment TestFragment on Animal { + __typename + } + """ - try await buildSubjectAndFragment() + let (_, template) = try await buildFragmentTemplate( + document: document + ) let expected = """ - struct TestFragment: TestSchema.SelectionSet, Fragment { - static var fragmentDefinition: StaticString { - #"fragment TestFragment on Animal { __typename }"# - } + struct TestFragment: TestSchema.SelectionSet, Fragment { + static var fragmentDefinition: StaticString { + #"fragment TestFragment on Animal { __typename }"# + } - let __data: DataDict - init(_dataDict: DataDict) { __data = _dataDict } + let __data: DataDict + init(_dataDict: DataDict) { __data = _dataDict } - static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Animal } - static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - ] } - } + static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Animal } + static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + ] } + } - """ + """ // when - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected)) @@ -352,15 +367,15 @@ class FragmentTemplateTests: XCTestCase { func test__render__givenModuleType_swiftPackageManager_generatesFragmentDefinition_withPublicAccess() async throws { // given - try await buildSubjectAndFragment(config: .mock(.swiftPackage())) + let (_, template) = try await buildFragmentTemplate(config: .mock(.swiftPackage())) let expected = """ - public struct TestFragment: TestSchema.SelectionSet, Fragment { - public static var fragmentDefinition: StaticString { - """ + public struct TestFragment: TestSchema.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + """ // when - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) @@ -368,51 +383,57 @@ class FragmentTemplateTests: XCTestCase { func test__render__givenModuleType_other_generatesFragmentDefinition_withPublicAccess() async throws { // given - try await buildSubjectAndFragment(config: .mock(.other)) + let (_, template) = try await buildFragmentTemplate(config: .mock(.other)) let expected = """ - public struct TestFragment: TestSchema.SelectionSet, Fragment { - public static var fragmentDefinition: StaticString { - """ + public struct TestFragment: TestSchema.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + """ // when - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } - func test__render__givenModuleType_embeddedInTarget_withInternalAccessModifier_generatesFragmentDefinition_withInternalAccess() async throws { + func + test__render__givenModuleType_embeddedInTarget_withInternalAccessModifier_generatesFragmentDefinition_withInternalAccess() + async throws + { // given - try await buildSubjectAndFragment( + let (_, template) = try await buildFragmentTemplate( config: .mock(.embeddedInTarget(name: "TestTarget", accessModifier: .internal)) ) let expected = """ - struct TestFragment: TestSchema.SelectionSet, Fragment { - static var fragmentDefinition: StaticString { - """ + struct TestFragment: TestSchema.SelectionSet, Fragment { + static var fragmentDefinition: StaticString { + """ // when - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } - func test__render__givenModuleType_embeddedInTarget_withPublicAccessModifier_generatesFragmentDefinition_withPublicAccess() async throws { + func + test__render__givenModuleType_embeddedInTarget_withPublicAccessModifier_generatesFragmentDefinition_withPublicAccess() + async throws + { // given - try await buildSubjectAndFragment( + let (_, template) = try await buildFragmentTemplate( config: .mock(.embeddedInTarget(name: "TestTarget", accessModifier: .public)) ) let expected = """ - struct TestFragment: TestSchema.SelectionSet, Fragment { - public static var fragmentDefinition: StaticString { - """ + struct TestFragment: TestSchema.SelectionSet, Fragment { + public static var fragmentDefinition: StaticString { + """ // when - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) @@ -422,7 +443,7 @@ class FragmentTemplateTests: XCTestCase { func test__render_givenInitializerConfigIncludesNamedFragments_rendersInitializer() async throws { // given - schemaSDL = """ + let schemaSDL = """ type Query { allAnimals: [Animal!] } @@ -432,7 +453,7 @@ class FragmentTemplateTests: XCTestCase { } """ - document = """ + let document = """ fragment TestFragment on Animal { species } @@ -456,12 +477,17 @@ class FragmentTemplateTests: XCTestCase { """ // when - try await buildSubjectAndFragment( - config: .mock(options: .init( - selectionSetInitializers: [.namedFragments] - ))) + let (_, template) = try await buildFragmentTemplate( + config: .mock( + options: .init( + selectionSetInitializers: [.namedFragments] + ) + ), + schemaSDL: schemaSDL, + document: document + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) @@ -469,7 +495,7 @@ class FragmentTemplateTests: XCTestCase { func test__render_givenNamedFragment_configIncludesSpecificFragment_rendersInitializer() async throws { // given - schemaSDL = """ + let schemaSDL = """ type Query { allAnimals: [Animal!] } @@ -479,7 +505,7 @@ class FragmentTemplateTests: XCTestCase { } """ - document = """ + let document = """ fragment TestFragment on Animal { species } @@ -503,12 +529,17 @@ class FragmentTemplateTests: XCTestCase { """ // when - try await buildSubjectAndFragment( - config: .mock(options: .init( - selectionSetInitializers: [.fragment(named: "TestFragment")] - ))) + let (_, template) = try await buildFragmentTemplate( + config: .mock( + options: .init( + selectionSetInitializers: [.fragment(named: "TestFragment")] + ) + ), + schemaSDL: schemaSDL, + document: document + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) @@ -516,7 +547,7 @@ class FragmentTemplateTests: XCTestCase { func test__render_givenNamedFragment_configDoesNotIncludeNamedFragments_doesNotRenderInitializer() async throws { // given - schemaSDL = """ + let schemaSDL = """ type Query { allAnimals: [Animal!] } @@ -526,27 +557,34 @@ class FragmentTemplateTests: XCTestCase { } """ - document = """ + let document = """ fragment TestFragment on Animal { species } """ // when - try await buildSubjectAndFragment( - config: .mock(options: .init( - selectionSetInitializers: [.operations] - ))) + let (_, template) = try await buildFragmentTemplate( + config: .mock( + options: .init( + selectionSetInitializers: [.operations] + ) + ), + schemaSDL: schemaSDL, + document: document + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine("}", atLine: 16, ignoringExtraLines: true)) } - func test__render_givenNamedFragments_configIncludeSpecificFragmentWithOtherName_doesNotRenderInitializer() async throws { + func test__render_givenNamedFragments_configIncludeSpecificFragmentWithOtherName_doesNotRenderInitializer() + async throws + { // given - schemaSDL = """ + let schemaSDL = """ type Query { allAnimals: [Animal!] } @@ -556,19 +594,24 @@ class FragmentTemplateTests: XCTestCase { } """ - document = """ + let document = """ fragment TestFragment on Animal { species } """ // when - try await buildSubjectAndFragment( - config: .mock(options: .init( - selectionSetInitializers: [.fragment(named: "OtherFragment")] - ))) + let (_, template) = try await buildFragmentTemplate( + config: .mock( + options: .init( + selectionSetInitializers: [.fragment(named: "OtherFragment")] + ) + ), + schemaSDL: schemaSDL, + document: document + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine("}", atLine: 16, ignoringExtraLines: true)) @@ -576,7 +619,7 @@ class FragmentTemplateTests: XCTestCase { func test__render_givenNamedFragments_asLocalCacheMutation_rendersInitializer() async throws { // given - schemaSDL = """ + let schemaSDL = """ type Query { allAnimals: [Animal!] } @@ -586,7 +629,7 @@ class FragmentTemplateTests: XCTestCase { } """ - document = """ + let document = """ fragment TestFragment on Animal @apollo_client_ios_localCacheMutation { species } @@ -610,18 +653,26 @@ class FragmentTemplateTests: XCTestCase { """ // when - try await buildSubjectAndFragment( - config: .mock(options: .init( - selectionSetInitializers: [] - ))) + let (_, template) = try await buildFragmentTemplate( + config: .mock( + options: .init( + selectionSetInitializers: [] + ) + ), + schemaSDL: schemaSDL, + document: document + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, atLine: 20, ignoringExtraLines: true)) } - func test__render_givenOperationSelectionSet_initializerConfig_all_fieldMergingConfig_notAll_doesNotRenderInitializer() async throws { + func + test__render_givenOperationSelectionSet_initializerConfig_all_fieldMergingConfig_notAll_doesNotRenderInitializer() + async throws + { let tests: [ApolloCodegenConfiguration.FieldMerging] = [ .none, .ancestors, @@ -629,36 +680,40 @@ class FragmentTemplateTests: XCTestCase { .siblings, [.ancestors, .namedFragments], [.siblings, .ancestors], - [.siblings, .namedFragments] + [.siblings, .namedFragments], ] for test in tests { // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } + let schemaSDL = """ + type Query { + allAnimals: [Animal!] + } - type Animal { - species: String! - } - """ + type Animal { + species: String! + } + """ - document = """ - fragment TestFragment on Animal { - species - } - """ + let document = """ + fragment TestFragment on Animal { + species + } + """ // when - try await buildSubjectAndFragment(config: .mock( - options: .init( - selectionSetInitializers: [.all] + let (_, template) = try await buildFragmentTemplate( + config: .mock( + options: .init( + selectionSetInitializers: [.all] + ), + experimentalFeatures: .init(fieldMerging: test) ), - experimentalFeatures: .init(fieldMerging: test) - )) + schemaSDL: schemaSDL, + document: document + ) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine("}", atLine: 16, ignoringExtraLines: true)) @@ -666,89 +721,96 @@ class FragmentTemplateTests: XCTestCase { } // MARK: Local Cache Mutation Tests - func test__render__givenFragment__asLocalCacheMutation_generatesFragmentDeclarationDefinitionAsMutableSelectionSetAndBoilerplate() async throws { + func + test__render__givenFragment__asLocalCacheMutation_generatesFragmentDeclarationDefinitionAsMutableSelectionSetAndBoilerplate() + async throws + { // given - document = """ - fragment TestFragment on Query @apollo_client_ios_localCacheMutation { - allAnimals { - species + let document = """ + fragment TestFragment on Query @apollo_client_ios_localCacheMutation { + allAnimals { + species + } } - } - """ + """ let expected = - """ - struct TestFragment: TestSchema.MutableSelectionSet, Fragment { - """ + """ + struct TestFragment: TestSchema.MutableSelectionSet, Fragment { + """ // when - try await buildSubjectAndFragment() + let (_, template) = try await buildFragmentTemplate(document: document) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) expect(String(actual.reversed())).to(equalLineByLine("\n}", ignoringExtraLines: true)) } - func test__render__givenFragment__asLocalCacheMutation_generatesFragmentDefinitionStrippingLocalCacheMutationDirective() async throws { + func + test__render__givenFragment__asLocalCacheMutation_generatesFragmentDefinitionStrippingLocalCacheMutationDirective() + async throws + { // given - document = """ - fragment TestFragment on Query @apollo_client_ios_localCacheMutation { - allAnimals { - species + let document = """ + fragment TestFragment on Query @apollo_client_ios_localCacheMutation { + allAnimals { + species + } } - } - """ + """ let expected = - """ - struct TestFragment: TestSchema.MutableSelectionSet, Fragment { - static var fragmentDefinition: StaticString { - #"fragment TestFragment on Query { __typename allAnimals { __typename species } }"# - } - """ + """ + struct TestFragment: TestSchema.MutableSelectionSet, Fragment { + static var fragmentDefinition: StaticString { + #"fragment TestFragment on Query { __typename allAnimals { __typename species } }"# + } + """ // when - try await buildSubjectAndFragment() + let (_, template) = try await buildFragmentTemplate(document: document) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) expect(String(actual.reversed())).to(equalLineByLine("\n}", ignoringExtraLines: true)) } - func test__render__givenFragment__asLocalCacheMutation_generatesFragmentDefinitionAsMutableSelectionSet() async throws { + func test__render__givenFragment__asLocalCacheMutation_generatesFragmentDefinitionAsMutableSelectionSet() async throws + { // given - document = """ - fragment TestFragment on Query @apollo_client_ios_localCacheMutation { - allAnimals { - species + let document = """ + fragment TestFragment on Query @apollo_client_ios_localCacheMutation { + allAnimals { + species + } } - } - """ + """ let expected = - """ - var __data: DataDict - init(_dataDict: DataDict) { __data = _dataDict } + """ + var __data: DataDict + init(_dataDict: DataDict) { __data = _dataDict } - static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Query } - static var __selections: [ApolloAPI.Selection] { [ - .field("allAnimals", [AllAnimal]?.self), - ] } + static var __parentType: any ApolloAPI.ParentType { TestSchema.Objects.Query } + static var __selections: [ApolloAPI.Selection] { [ + .field("allAnimals", [AllAnimal]?.self), + ] } - var allAnimals: [AllAnimal]? { - get { __data["allAnimals"] } - set { __data["allAnimals"] = newValue } - } - """ + var allAnimals: [AllAnimal]? { + get { __data["allAnimals"] } + set { __data["allAnimals"] = newValue } + } + """ // when - try await buildSubjectAndFragment() + let (_, template) = try await buildFragmentTemplate(document: document) - let actual = renderSubject() + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, atLine: 6, ignoringExtraLines: true)) @@ -758,143 +820,151 @@ class FragmentTemplateTests: XCTestCase { func test__casing__givenLowercasedSchemaName_generatesWithFirstUppercasedNamespace() async throws { // given - try await buildSubjectAndFragment(config: .mock(schemaNamespace: "mySchema")) + let (_, template) = try await buildFragmentTemplate(config: .mock(schemaNamespace: "mySchema")) // then let expected = """ struct TestFragment: MySchema.SelectionSet, Fragment { """ - let actual = renderSubject() + let actual = render(template) expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } func test__casing__givenUppercasedSchemaName_generatesWithUppercasedNamespace() async throws { // given - try await buildSubjectAndFragment(config: .mock(schemaNamespace: "MY_SCHEMA")) + let (_, template) = try await buildFragmentTemplate(config: .mock(schemaNamespace: "MY_SCHEMA")) // then let expected = """ struct TestFragment: MY_SCHEMA.SelectionSet, Fragment { """ - let actual = renderSubject() + let actual = render(template) expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } func test__casing__givenCapitalizedSchemaName_generatesWithCapitalizedNamespace() async throws { // given - try await buildSubjectAndFragment(config: .mock(schemaNamespace: "MySchema")) + let (_, template) = try await buildFragmentTemplate(config: .mock(schemaNamespace: "MySchema")) // then let expected = """ struct TestFragment: MySchema.SelectionSet, Fragment { """ - let actual = renderSubject() + let actual = render(template) expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } - + // MARK: - Reserved Keyword Tests - + func test__render__givenFragmentReservedKeywordName_rendersEscapedName() async throws { let keywords = ["Type", "type"] - + try await keywords.asyncForEach { keyword in // given - schemaSDL = """ - type Query { - getUser(id: String): User - } + let schemaSDL = """ + type Query { + getUser(id: String): User + } - type User { - id: String! - name: String! - } - """ + type User { + id: String! + name: String! + } + """ - document = """ - fragment \(keyword) on User { - name - } - """ + let document = """ + fragment \(keyword) on User { + name + } + """ let expected = """ - struct \(keyword.firstUppercased)_Fragment: TestSchema.SelectionSet, Fragment { - """ + struct \(keyword.firstUppercased)_Fragment: TestSchema.SelectionSet, Fragment { + """ // when - try await buildSubjectAndFragment(named: keyword) - let actual = renderSubject() + let (_, template) = try await buildFragmentTemplate(named: keyword, schemaSDL: schemaSDL, document: document) + let actual = render(template) // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } } - + // MARK: - Protocol conformance - + func test__render__givenFragmentWithIdKeyField_rendersIdentifiableConformance() async throws { // given - schemaSDL = """ - type Query { - getUser(id: String): User - } - - type User @typePolicy(keyFields: "id") { - id: String! - name: String! - } - """ - - document = """ - fragment NodeFragment on User { - id - } - """ - + let schemaSDL = """ + type Query { + getUser(id: String): User + } + + type User @typePolicy(keyFields: "id") { + id: String! + name: String! + } + """ + + let document = """ + fragment NodeFragment on User { + id + } + """ + let expected = """ - struct NodeFragment: TestSchema.SelectionSet, Fragment, Identifiable { - """ - + struct NodeFragment: TestSchema.SelectionSet, Fragment, Identifiable { + """ + // when - try await buildSubjectAndFragment(named: "NodeFragment") - let actual = renderSubject() - + let (_, template) = try await buildFragmentTemplate( + named: "NodeFragment", + schemaSDL: schemaSDL, + document: document + ) + let actual = render(template) + // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } - + func test__render_givenFragment_withoutUsingIDField_doesNotRenderIdentifiableConformance() async throws { // given - schemaSDL = """ - type Query { - getUser(id: String): User - } - - type User @typePolicy(keyFields: "id") { - id: String! - name: String! - } - """ - - document = """ - fragment UserFragment on User { - name - } - """ - + let schemaSDL = """ + type Query { + getUser(id: String): User + } + + type User @typePolicy(keyFields: "id") { + id: String! + name: String! + } + """ + + let document = """ + fragment UserFragment on User { + name + } + """ + let expected = """ - struct UserFragment: TestSchema.SelectionSet, Fragment { - """ - + struct UserFragment: TestSchema.SelectionSet, Fragment { + """ + // when - try await buildSubjectAndFragment(named: "UserFragment") - let actual = renderSubject() - + let (_, template) = try await buildFragmentTemplate( + named: "UserFragment", + schemaSDL: schemaSDL, + document: document + ) + let actual = render(template) + // then expect(actual).to(equalLineByLine(expected, ignoringExtraLines: true)) } diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/InputObjectTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/InputObjectTemplateTests.swift index 1a10db48c..d326736e6 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/InputObjectTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/InputObjectTemplateTests.swift @@ -798,7 +798,7 @@ class InputObjectTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - func test__render__given_NonNullableField_WithDefault__generates_OptionalParameter_InitializerNilDefault() throws { + func test__render__given_nonNullableField_WithDefault__generates_OptionalParameter_InitializerNilDefault() throws { // given buildSubject(fields: [ GraphQLInputField.mock("nonNullableWithDefault", type: .nonNull(.scalar(.integer())), defaultValue: .int(3)) @@ -809,7 +809,7 @@ class InputObjectTemplateTests: XCTestCase { nonNullableWithDefault: Int? = nil ) { __data = InputDict([ - "nonNullableWithDefault": nonNullableWithDefault + "nonNullableWithDefault": nonNullableWithDefault ?? GraphQLNullable.none ]) } @@ -966,7 +966,7 @@ class InputObjectTemplateTests: XCTestCase { nonNullableListNullableItemWithDefault: [String?]? = nil ) { __data = InputDict([ - "nonNullableListNullableItemWithDefault": nonNullableListNullableItemWithDefault + "nonNullableListNullableItemWithDefault": nonNullableListNullableItemWithDefault ?? GraphQLNullable.none ]) } @@ -1018,7 +1018,7 @@ class InputObjectTemplateTests: XCTestCase { nonNullableListNonNullableItemWithDefault: [String]? = nil ) { __data = InputDict([ - "nonNullableListNonNullableItemWithDefault": nonNullableListNonNullableItemWithDefault + "nonNullableListNonNullableItemWithDefault": nonNullableListNonNullableItemWithDefault ?? GraphQLNullable.none ]) } diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/LegacyAPQOperationManifestTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/LegacyAPQOperationManifestTemplateTests.swift index 15d31b37f..e6441849e 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/LegacyAPQOperationManifestTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/LegacyAPQOperationManifestTemplateTests.swift @@ -90,10 +90,10 @@ class LegacyAPQOperationManifestTemplateTests: XCTestCase { } """ ) - ].asyncMap { + ].asyncMap { [operationIdentiferFactory] in OperationManifestTemplate.OperationManifestItem( operation: OperationDescriptor($0), - identifier: try await self.operationIdentiferFactory.identifier(for: $0) + identifier: try await operationIdentiferFactory!.identifier(for: $0) ) } @@ -145,10 +145,10 @@ class LegacyAPQOperationManifestTemplateTests: XCTestCase { ) ] ) - ].asyncMap { + ].asyncMap { [operationIdentiferFactory] in OperationManifestTemplate.OperationManifestItem( operation: OperationDescriptor($0), - identifier: try await self.operationIdentiferFactory.identifier(for: $0) + identifier: try await operationIdentiferFactory!.identifier(for: $0) ) } diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/LocalCacheMutationDefinitionTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/LocalCacheMutationDefinitionTemplateTests.swift index a9941244b..7ae75dffb 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/LocalCacheMutationDefinitionTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/LocalCacheMutationDefinitionTemplateTests.swift @@ -105,7 +105,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - public class TestOperationLocalCacheMutation: LocalCacheMutation { + public struct TestOperationLocalCacheMutation: LocalCacheMutation { public static let operationType: GraphQLOperationType = .query """ @@ -128,7 +128,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { public static let operationType: GraphQLOperationType = .query """ @@ -151,7 +151,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .query """ @@ -174,7 +174,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - public class TestOperationLocalCacheMutation: LocalCacheMutation { + public struct TestOperationLocalCacheMutation: LocalCacheMutation { public static let operationType: GraphQLOperationType = .query """ @@ -197,7 +197,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .query """ @@ -220,7 +220,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - public class TestOperationLocalCacheMutation: LocalCacheMutation { + public struct TestOperationLocalCacheMutation: LocalCacheMutation { public static let operationType: GraphQLOperationType = .query """ @@ -243,7 +243,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .query """ @@ -268,7 +268,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { // given let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .query """ @@ -393,7 +393,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { let expected = """ - class LowercaseOperationLocalCacheMutation: LocalCacheMutation { + struct LowercaseOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .query """ @@ -419,7 +419,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .query """ @@ -459,7 +459,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .mutation """ @@ -499,7 +499,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { static let operationType: GraphQLOperationType = .subscription """ @@ -572,7 +572,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { try await buildSubjectAndOperation() let expected = """ - public class TestOperationLocalCacheMutation: LocalCacheMutation { + public struct TestOperationLocalCacheMutation: LocalCacheMutation { """ // when @@ -588,7 +588,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { try await buildSubjectAndOperation() let expected = """ - public class TestOperationLocalCacheMutation: LocalCacheMutation { + public struct TestOperationLocalCacheMutation: LocalCacheMutation { """ // when @@ -604,7 +604,7 @@ class LocalCacheMutationDefinitionTemplateTests: XCTestCase { try await buildSubjectAndOperation() let expected = """ - class TestOperationLocalCacheMutation: LocalCacheMutation { + struct TestOperationLocalCacheMutation: LocalCacheMutation { """ // when diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/MockObjectTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/MockObjectTemplateTests.swift index 807649cf1..ed3dc6135 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/MockObjectTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/MockObjectTemplateTests.swift @@ -67,12 +67,12 @@ class MockObjectTemplateTests: XCTestCase { buildSubject(name: "Dog", moduleType: .swiftPackage()) let expected = """ - public class Dog: MockObject { + public final class Dog: MockObject { public static let objectType: ApolloAPI.Object = TestSchema.Objects.Dog public static let _mockFields = MockFields() public typealias MockValueCollectionType = Array> - public struct MockFields { + public struct MockFields: Sendable { } } @@ -92,12 +92,12 @@ class MockObjectTemplateTests: XCTestCase { buildSubject(name: "dog") let expected = """ - public class Dog: MockObject { + public final class Dog: MockObject { public static let objectType: ApolloAPI.Object = TestSchema.Objects.Dog public static let _mockFields = MockFields() public typealias MockValueCollectionType = Array> - public struct MockFields { + public struct MockFields: Sendable { } } @@ -175,7 +175,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @Field("customScalar") public var customScalar @Field("object") public var object @Field<[Cat]>("objectList") public var objectList @@ -206,7 +206,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @Field("customScalar") public var customScalar @Field>("enumType") public var enumType @Field("object") public var object @@ -283,7 +283,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @Field("Any") public var `Any` @Field("Protocol") public var `Protocol` @Field("Self") public var `Self` @@ -360,7 +360,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @Field("actor") public var actor } """ @@ -383,7 +383,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @Field("actor") public var actor } """ @@ -406,7 +406,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @Field("actor") public var actor } """ @@ -855,12 +855,12 @@ class MockObjectTemplateTests: XCTestCase { ) let expectedClassDefinition = """ - public class Dog: MockObject { + public final class Dog: MockObject { public static let objectType: ApolloAPI.Object = TestSchema.Objects.Dog public static let _mockFields = MockFields() public typealias MockValueCollectionType = Array> - public struct MockFields { + public struct MockFields: Sendable { """ let expectedExtensionDefinition = """ @@ -886,12 +886,12 @@ class MockObjectTemplateTests: XCTestCase { ) let expectedClassDefinition = """ - public class Dog: MockObject { + public final class Dog: MockObject { public static let objectType: ApolloAPI.Object = TestSchema.Objects.Dog public static let _mockFields = MockFields() public typealias MockValueCollectionType = Array> - public struct MockFields { + public struct MockFields: Sendable { """ let expectedExtensionDefinition = """ @@ -917,12 +917,12 @@ class MockObjectTemplateTests: XCTestCase { ) let expectedClassDefinition = """ - class Dog: MockObject { + final class Dog: MockObject { static let objectType: ApolloAPI.Object = TestSchema.Objects.Dog static let _mockFields = MockFields() typealias MockValueCollectionType = Array> - struct MockFields { + struct MockFields: Sendable { """ let expectedExtensionDefinition = """ @@ -950,7 +950,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @available(*, deprecated, message: "Cause I said so!") @Field("string") public var string } @@ -973,7 +973,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public struct MockFields { + public struct MockFields: Sendable { @Field("string") public var string } """ @@ -1001,12 +1001,12 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public class \(keyword.firstUppercased)_Object: MockObject { + public final class \(keyword.firstUppercased)_Object: MockObject { public static let objectType: ApolloAPI.Object = TestSchema.Objects.\(keyword.firstUppercased)_Object public static let _mockFields = MockFields() public typealias MockValueCollectionType = Array> - public struct MockFields { + public struct MockFields: Sendable { @Field("string") public var string } } @@ -1038,7 +1038,7 @@ class MockObjectTemplateTests: XCTestCase { ) let expected = """ - public class MyCustomObject: MockObject { + public final class MyCustomObject: MockObject { public static let objectType: ApolloAPI.Object = TestSchema.Objects.MyCustomObject public static let _mockFields = MockFields() public typealias MockValueCollectionType = Array> diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/OperationDefinitionTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/OperationDefinitionTemplateTests.swift index f56820d74..b40d09bdb 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/OperationDefinitionTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/OperationDefinitionTemplateTests.swift @@ -94,7 +94,7 @@ class OperationDefinitionTemplateTests: XCTestCase { // given let expected = """ - class TestOperationQuery: GraphQLQuery { + struct TestOperationQuery: GraphQLQuery { static let operationName: String = "TestOperation" """ @@ -119,7 +119,7 @@ class OperationDefinitionTemplateTests: XCTestCase { let expected = """ - class TestOperationQuery: GraphQLQuery { + struct TestOperationQuery: GraphQLQuery { static let operationName: String = "TestOperationQuery" """ @@ -158,7 +158,7 @@ class OperationDefinitionTemplateTests: XCTestCase { let expected = """ - class TestOperationQueryMutation: GraphQLMutation { + struct TestOperationQueryMutation: GraphQLMutation { static let operationName: String = "TestOperationQuery" """ @@ -197,7 +197,7 @@ class OperationDefinitionTemplateTests: XCTestCase { let expected = """ - class TestOperationMutation: GraphQLMutation { + struct TestOperationMutation: GraphQLMutation { static let operationName: String = "TestOperation" """ @@ -236,7 +236,7 @@ class OperationDefinitionTemplateTests: XCTestCase { let expected = """ - class TestOperationSubscription: GraphQLSubscription { + struct TestOperationSubscription: GraphQLSubscription { static let operationName: String = "TestOperation" """ @@ -271,7 +271,7 @@ class OperationDefinitionTemplateTests: XCTestCase { let expected = """ - class LowercaseOperationQuery: GraphQLQuery { + struct LowercaseOperationQuery: GraphQLQuery { static let operationName: String = "lowercaseOperation" static let operationDocument: ApolloAPI.OperationDocument = .init( definition: .init( @@ -1098,6 +1098,7 @@ class OperationDefinitionTemplateTests: XCTestCase { // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { """, atLine: 64, @@ -1106,11 +1107,12 @@ class OperationDefinitionTemplateTests: XCTestCase { expect(actual).to(equalLineByLine( """ - ]} + ] + ) } """, - atLine: 74, + atLine: 76, ignoringExtraLines: false )) } @@ -1160,6 +1162,7 @@ class OperationDefinitionTemplateTests: XCTestCase { // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { """, atLine: 64, @@ -1168,11 +1171,12 @@ class OperationDefinitionTemplateTests: XCTestCase { expect(actual).to(equalLineByLine( """ - ]} + ] + ) } """, - atLine: 74, + atLine: 76, ignoringExtraLines: false )) } @@ -1222,6 +1226,7 @@ class OperationDefinitionTemplateTests: XCTestCase { // MARK: - Deferred Fragment Metadata + public typealias ResponseFormat = IncrementalDeferredResponseFormat enum DeferredFragmentIdentifiers { """, atLine: 64, @@ -1230,11 +1235,12 @@ class OperationDefinitionTemplateTests: XCTestCase { expect(actual).to(equalLineByLine( """ - ]} + ] + ) } """, - atLine: 74, + atLine: 76, ignoringExtraLines: false )) } diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/PersistedQueriesOperationManifestTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/PersistedQueriesOperationManifestTemplateTests.swift index eeaf20152..325a17861 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/PersistedQueriesOperationManifestTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/PersistedQueriesOperationManifestTemplateTests.swift @@ -51,10 +51,10 @@ class PersistedQueriesOperationManifestTemplateTests: XCTestCase { } """ - let operations = try await [operation].asyncMap { + let operations = try await [operation].asyncMap { [operationIdentiferFactory] in OperationManifestTemplate.OperationManifestItem( operation: OperationDescriptor($0), - identifier: try await self.operationIdentiferFactory.identifier(for: $0) + identifier: try await operationIdentiferFactory!.identifier(for: $0) ) } @@ -96,10 +96,10 @@ class PersistedQueriesOperationManifestTemplateTests: XCTestCase { } """ ) - ].asyncMap { + ].asyncMap { [operationIdentiferFactory] in OperationManifestTemplate.OperationManifestItem( operation: OperationDescriptor($0), - identifier: try await self.operationIdentiferFactory.identifier(for: $0) + identifier: try await operationIdentiferFactory!.identifier(for: $0) ) } @@ -161,10 +161,10 @@ class PersistedQueriesOperationManifestTemplateTests: XCTestCase { ) ] ) - ].asyncMap { + ].asyncMap { [operationIdentiferFactory] in OperationManifestTemplate.OperationManifestItem( operation: OperationDescriptor($0), - identifier: try await self.operationIdentiferFactory.identifier(for: $0) + identifier: try await operationIdentiferFactory!.identifier(for: $0) ) } @@ -218,10 +218,10 @@ class PersistedQueriesOperationManifestTemplateTests: XCTestCase { ) ] ) - ].asyncMap { + ].asyncMap { [operationIdentiferFactory] in OperationManifestTemplate.OperationManifestItem( operation: OperationDescriptor($0), - identifier: try await self.operationIdentiferFactory.identifier(for: $0) + identifier: try await operationIdentiferFactory!.identifier(for: $0) ) } @@ -275,10 +275,10 @@ class PersistedQueriesOperationManifestTemplateTests: XCTestCase { } """# - let operations = try await [operation].asyncMap { + let operations = try await [operation].asyncMap { [operationIdentiferFactory] in OperationManifestTemplate.OperationManifestItem( operation: OperationDescriptor($0), - identifier: try await self.operationIdentiferFactory.identifier(for: $0) + identifier: try await operationIdentiferFactory!.identifier(for: $0) ) } diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SwiftPackageManagerModuleTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SwiftPackageManagerModuleTemplateTests.swift index 7758a4658..a9bead0ea 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SwiftPackageManagerModuleTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SwiftPackageManagerModuleTemplateTests.swift @@ -83,10 +83,11 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let expected = """ platforms: [ - .iOS(.v12), - .macOS(.v10_14), - .tvOS(.v12), - .watchOS(.v5), + .iOS(.v15), + .macOS(.v12), + .tvOS(.v15), + .watchOS(.v8), + .visionOS(.v1), ], """ @@ -111,7 +112,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } func test__packageDescription__givenUppercasedSchemaName_generatesProductWithUppercasedName() { @@ -128,7 +129,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } func test__packageDescription__givenCapitalizedSchemaName_generatesProductWithCapitalizedName() { @@ -145,7 +146,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } func test__packageDescription__generatesDefaultVersionDependency() { @@ -168,7 +169,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } func test__packageDescription__generatesBranchVersionDependency() { @@ -194,7 +195,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } func test__packageDescription__generatesCommitVersionDependency() { @@ -220,7 +221,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } func test__packageDescription__generatesExactVersionDependency() { @@ -246,7 +247,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } func test__packageDescription__generatesFromVersionDependency() { @@ -272,7 +273,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } func test__packageDescription__generatesLocalVersionDependency() { @@ -298,7 +299,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfigNone_withLowercaseSchemaName_generatesTargetWithCapitalizedName() { @@ -314,14 +315,14 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { ], path: "./Sources" ), - ] + ], """ // when let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 19, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 20, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfigNone_withUppercaseSchemaName_generatesTargetWithUppercasedName() { @@ -337,14 +338,14 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { ], path: "./Sources" ), - ] + ], """ // when let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 19, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 20, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfigNone_withCapitalizedSchemaName_generatesTargetWithCapitalizedName() { @@ -360,14 +361,14 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { ], path: "./Sources" ), - ] + ], """ // when let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 19, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 20, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfig_absolute_generatesTargets() { @@ -383,14 +384,14 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { ], path: "./Sources" ), - ] + ], """ // when let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 19, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 20, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfig_swiftPackage_noTargetName_generatesProduct() { @@ -408,7 +409,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } @@ -433,14 +434,14 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { ], path: "./TestMocks" ), - ] + ], """ // when let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 20, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 21, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfig_swiftPackage_withTargetName_generatesProduct() { @@ -458,7 +459,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfig_swiftPackage_withTargetName_generatesTargets() { @@ -482,14 +483,14 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { ], path: "./CustomMocks" ), - ] + ], """ // when let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 20, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 21, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfig_withLowercaseSchemaName_generatesTestMockTargetWithCapitalizedTargetDependency() { @@ -506,7 +507,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 32, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 33, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfig_withUppercaseSchemaName_generatesTestMockTargetWithUppercaseTargetDependency() { @@ -523,7 +524,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 32, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 33, ignoringExtraLines: true)) } func test__packageDescription__givenTestMockConfig_withCapitalizedSchemaName_generatesTestMockTargetWithCapitalizedTargetDependency() { @@ -540,7 +541,7 @@ class SwiftPackageManagerModuleTemplateTests: XCTestCase { let actual = renderSubject() // then - expect(actual).to(equalLineByLine(expected, atLine: 32, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 33, ignoringExtraLines: true)) } } diff --git a/Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+ReduceGeneratedSchemaTypesTests.swift b/Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+ReduceGeneratedSchemaTypesTests.swift index d191ac7d0..d82028395 100644 --- a/Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+ReduceGeneratedSchemaTypesTests.swift +++ b/Tests/ApolloCodegenTests/Configuration/ApolloCodegenConfiguration+ReduceGeneratedSchemaTypesTests.swift @@ -9,15 +9,15 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa private var directoryURL: URL { testFileManager.directoryURL } private var testFileManager: TestIsolatedFileManager! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() testFileManager = try testIsolatedFileManager() - try createMockSchema() + try await createMockSchema() } - override func tearDownWithError() throws { + override func tearDown() async throws { testFileManager = nil - try super.tearDownWithError() + try await super.tearDown() } private func createSubject( @@ -49,8 +49,8 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa // MARK: Mock File Setup - private func createMockSchema() throws { - try createFile( + private func createMockSchema() async throws { + try await createFile( body: """ type Query { allAnimals: [Animal]! @@ -137,11 +137,11 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa @discardableResult private func createFile( - body: @autoclosure () -> String = "Test File", + body: @Sendable @autoclosure () -> String = "Test File", filename: String, inDirectory directory: String? = nil - ) throws -> String { - return try self.testFileManager.createFile( + ) async throws -> String { + return try await self.testFileManager.createFile( body: body(), named: filename, inDirectory: directory @@ -151,7 +151,7 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa // MARK: - Tests func test_givenSchemaAndOperationDocuments_andInterfaceWithTypePolicy_reducingGeneratedSchemaTypes_generatesOnlyReferencedObjects() async throws { - try createFile( + try await createFile( body: """ query AllAnimalsQuery { allAnimals { @@ -169,15 +169,9 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa let subject = createSubject() - let fileManager = MockApolloFileManager(strict: false) + let fileManager = await MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -200,15 +194,15 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value - + + let filePaths = await fileManager.writtenFiles + expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect { await fileManager.allClosuresCalled }.to(beTrue()) } func test_givenSchemaAndOperationDocuments_andInterfaceWithTypePolicy_generatesAllInterfaceObjects() async throws { - try createFile( + try await createFile( body: """ query AllAnimalsQuery { allAnimals { @@ -226,15 +220,9 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa let subject = createSubject(reduceGeneratedSchemaTypes: false) - let fileManager = MockApolloFileManager(strict: false) - - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() + let fileManager = await MockApolloFileManager(strict: false) - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -258,16 +246,15 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa compilationResult: compilationResult, ir: ir, fileManager: fileManager - ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value - + ) + let filePaths = await fileManager.writtenFiles + expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect { await fileManager.allClosuresCalled }.to(beTrue()) } func test_givenSchemaAndOperationDocuments_andTypesWithTypePolicy_reducingGeneratedSchemaTypes_generatesOnlyReferencedAndTypePolicyObjects() async throws { - try createFile( + try await createFile( body: """ query AllPetFoodQuery { allPetFood { @@ -284,15 +271,9 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa let subject = createSubject() - let fileManager = MockApolloFileManager(strict: false) + let fileManager = await MockApolloFileManager(strict: false) - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() - - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -316,15 +297,14 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value - + let filePaths = await fileManager.writtenFiles + expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect { await fileManager.allClosuresCalled }.to(beTrue()) } func test_givenSchemaAndOperationDocuments_andTypesWithTypePolicy_generatesInterfaceAndTypePolicyObjects() async throws { - try createFile( + try await createFile( body: """ query AllPetFoodQuery { allPetFood { @@ -341,15 +321,9 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa let subject = createSubject(reduceGeneratedSchemaTypes: false) - let fileManager = MockApolloFileManager(strict: false) - - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() + let fileManager = await MockApolloFileManager(strict: false) - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -374,15 +348,14 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value - + let filePaths = await fileManager.writtenFiles + expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect { await fileManager.allClosuresCalled }.to(beTrue()) } func test_givenSchemaAndOperationDocuments_andInterface_reducingGeneratedSchemaTypes_generatesOnlyReferencedObjects() async throws { - try createFile( + try await createFile( body: """ query AllPetBedsQuery { allPetBeds { @@ -399,15 +372,9 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa let subject = createSubject() - let fileManager = MockApolloFileManager(strict: false) - - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() + let fileManager = await MockApolloFileManager(strict: false) - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -430,15 +397,14 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value - + let filePaths = await fileManager.writtenFiles + expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect { await fileManager.allClosuresCalled }.to(beTrue()) } func test_givenSchemaAndOperationDocuments_andInterface_generatesAllInterfaceObjects() async throws { - try createFile( + try await createFile( body: """ query AllPetBedsQuery { allPetBeds { @@ -455,15 +421,9 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa let subject = createSubject(reduceGeneratedSchemaTypes: false) - let fileManager = MockApolloFileManager(strict: false) - - let filePathStore = ApolloFileManager.WrittenFiles() - let concurrentTasks = ConcurrentTaskContainer() + let fileManager = await MockApolloFileManager(strict: false) - fileManager.mock(closure: .createFile({ path, data, attributes in - concurrentTasks.dispatch { - await filePathStore.addWrittenFile(path: path) - } + await fileManager.mock(closure: .createFile({ path, data, attributes in return true })) @@ -488,11 +448,10 @@ final class ApolloCodegenConfiguration_ReduceGeneratedSchemaTypesTests: XCTestCa ir: ir, fileManager: fileManager ) - await concurrentTasks.waitForAllTasks() - let filePaths = await filePathStore.value - + let filePaths = await fileManager.writtenFiles + expect(filePaths).to(equal(expectedPaths)) - expect(fileManager.allClosuresCalled).to(beTrue()) + await expect { await fileManager.allClosuresCalled }.to(beTrue()) } } diff --git a/Tests/ApolloCodegenTests/FileManagerExtensionTests.swift b/Tests/ApolloCodegenTests/FileManagerExtensionTests.swift index 8313d1fa2..af767441e 100644 --- a/Tests/ApolloCodegenTests/FileManagerExtensionTests.swift +++ b/Tests/ApolloCodegenTests/FileManagerExtensionTests.swift @@ -34,12 +34,12 @@ class FileManagerExtensionTests: XCTestCase { // MARK: Presence - func test_doesFileExist_givenFileExistsAndIsDirectory_shouldReturnFalse() { + func test_doesFileExist_givenFileExistsAndIsDirectory_shouldReturnFalse() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -47,16 +47,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesFileExist(atPath: self.uniquePath)).to(beFalse()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesFileExist(atPath: self.uniquePath) }.to(beFalse()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_doesFileExist_givenFileExistsAndIsNotDirectory_shouldReturnTrue() { + func test_doesFileExist_givenFileExistsAndIsNotDirectory_shouldReturnTrue() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false @@ -64,16 +64,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesFileExist(atPath: self.uniquePath)).to(beTrue()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesFileExist(atPath: self.uniquePath) }.to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_doesFileExist_givenFileDoesNotExistAndIsDirectory_shouldReturnFalse() { + func test_doesFileExist_givenFileDoesNotExistAndIsDirectory_shouldReturnFalse() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -81,16 +81,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesFileExist(atPath: self.uniquePath)).to(beFalse()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesFileExist(atPath: self.uniquePath) }.to(beFalse()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_doesFileExist_givenFileDoesNotExistAndIsNotDirectory_shouldReturnFalse() { + func test_doesFileExist_givenFileDoesNotExistAndIsNotDirectory_shouldReturnFalse() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false @@ -98,16 +98,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesFileExist(atPath: self.uniquePath)).to(beFalse()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesFileExist(atPath: self.uniquePath) }.to(beFalse()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_doesDirectoryExist_givenFilesExistsAndIsDirectory_shouldReturnTrue() { + func test_doesDirectoryExist_givenFilesExistsAndIsDirectory_shouldReturnTrue() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -115,16 +115,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesDirectoryExist(atPath: self.uniquePath)).to(beTrue()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesDirectoryExist(atPath: self.uniquePath) }.to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_doesDirectoryExist_givenFileExistsAndIsNotDirectory_shouldReturnFalse() { + func test_doesDirectoryExist_givenFileExistsAndIsNotDirectory_shouldReturnFalse() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false @@ -132,16 +132,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesDirectoryExist(atPath: self.uniquePath)).to(beFalse()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesDirectoryExist(atPath: self.uniquePath) }.to(beFalse()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_doesDirectoryExist_givenFileDoesNotExistAndIsDirectory_shouldReturnFalse() { + func test_doesDirectoryExist_givenFileDoesNotExistAndIsDirectory_shouldReturnFalse() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -149,16 +149,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesDirectoryExist(atPath: self.uniquePath)).to(beFalse()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesDirectoryExist(atPath: self.uniquePath) }.to(beFalse()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_doesDirectoryExist_givenFileDoesNotExistAndIsNotDirectory_shouldFalse() { + func test_doesDirectoryExist_givenFileDoesNotExistAndIsNotDirectory_shouldFalse() async { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false @@ -166,18 +166,18 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(mocked.doesDirectoryExist(atPath: self.uniquePath)).to(beFalse()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.doesDirectoryExist(atPath: self.uniquePath) }.to(beFalse()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } // MARK: Deletion - func test_deleteFile_givenFileExistsAndIsDirectory_shouldThrow() throws { + func test_deleteFile_givenFileExistsAndIsDirectory_shouldThrow() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -185,61 +185,61 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.deleteFile(atPath: self.uniquePath)) + await expect { try await mocked.deleteFile(atPath: self.uniquePath) } .to(throwError(FileManagerPathError.notAFile(path: self.uniquePath))) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteFile_givenFileExistsAndIsNotDirectory_shouldSucceed() throws { + func test_deleteFile_givenFileExistsAndIsNotDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false return true })) - mocked.mock(closure: .removeItem({ path in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .removeItem({ [uniquePath] path in + expect(path).to(equal(uniquePath)) })) // then - expect(try mocked.deleteFile(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteFile(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteFile_givenFileExistsAndIsNotDirectoryAndError_shouldThrow() throws { + func test_deleteFile_givenFileExistsAndIsNotDirectoryAndError_shouldThrow() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false return true })) - mocked.mock(closure: .removeItem({ path in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .removeItem({ [uniquePath, uniqueError] path in + expect(path).to(equal(uniquePath)) - throw self.uniqueError + throw uniqueError! })) // then - expect(try mocked.deleteFile(atPath: self.uniquePath)).to(throwError(self.uniqueError)) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteFile(atPath: self.uniquePath) }.to(throwError(self.uniqueError)) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteFile_givenFileDoesNotExistAndIsDirectory_shouldSucceed() throws { + func test_deleteFile_givenFileDoesNotExistAndIsDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -247,16 +247,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.deleteFile(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteFile(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteFile_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() throws { + func test_deleteFile_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false @@ -264,60 +264,60 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.deleteFile(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteFile(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteDirectory_givenFileExistsAndIsDirectory_shouldSucceed() throws { + func test_deleteDirectory_givenFileExistsAndIsDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true return true })) - mocked.mock(closure: .removeItem({ path in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .removeItem({ [uniquePath] path in + expect(path).to(equal(uniquePath)) })) // then - expect(try mocked.deleteDirectory(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteDirectory(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteDirectory_givenFileExistsAndIsDirectoryAndError_shouldThrow() throws { + func test_deleteDirectory_givenFileExistsAndIsDirectoryAndError_shouldThrow() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true return true })) - mocked.mock(closure: .removeItem({ path in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .removeItem({ [uniquePath, uniqueError] path in + expect(path).to(equal(uniquePath)) - throw self.uniqueError + throw uniqueError! })) // then - expect(try mocked.deleteDirectory(atPath: self.uniquePath)).to(throwError(self.uniqueError)) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteDirectory(atPath: self.uniquePath) }.to(throwError(self.uniqueError)) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteDirectory_givenFileExistsAndIsNotDirectory_shouldThrow() throws { + func test_deleteDirectory_givenFileExistsAndIsNotDirectory_shouldThrow() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false @@ -326,17 +326,17 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.deleteDirectory(atPath: self.uniquePath)) + await expect { try await mocked.deleteDirectory(atPath: self.uniquePath) } .to(throwError(FileManagerPathError.notADirectory(path: self.uniquePath))) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteDirectory_givenFileDoesNotExistAndIsDirectory_shouldSucceed() throws { + func test_deleteDirectory_givenFileDoesNotExistAndIsDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -344,16 +344,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.deleteDirectory(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteDirectory(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_deleteDirectory_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() throws { + func test_deleteDirectory_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false @@ -361,8 +361,8 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.deleteDirectory(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.deleteDirectory(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } // MARK: Creation @@ -372,7 +372,7 @@ class FileManagerExtensionTests: XCTestCase { let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -380,9 +380,11 @@ class FileManagerExtensionTests: XCTestCase { return true })) - mocked.mock(closure: .createFile({ path, data, attr in - expect(path).to(equal(self.uniquePath)) - expect(data).to(equal(self.uniqueData)) + let expectedPath = self.uniquePath + let expectedData = self.uniqueData + await mocked.mock(closure: .createFile({ path, data, attr in + expect(path).to(equal(expectedPath)) + expect(data).to(equal(expectedData)) expect(attr).to(beNil()) return true @@ -393,7 +395,7 @@ class FileManagerExtensionTests: XCTestCase { await expect { try await mocked.createFile(atPath: self.uniquePath, data:self.uniqueData) }.notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } func test_createFile_givenContainingDirectoryDoesExistAndFileNotCreated_shouldThrow() async throws { @@ -401,7 +403,7 @@ class FileManagerExtensionTests: XCTestCase { let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -409,9 +411,11 @@ class FileManagerExtensionTests: XCTestCase { return true })) - mocked.mock(closure: .createFile({ path, data, attr in - expect(path).to(equal(self.uniquePath)) - expect(data).to(equal(self.uniqueData)) + let expectedPath = self.uniquePath + let expectedData = self.uniqueData + await mocked.mock(closure: .createFile({ path, data, attr in + expect(path).to(equal(expectedPath)) + expect(data).to(equal(expectedData)) expect(attr).to(beNil()) return false @@ -422,7 +426,7 @@ class FileManagerExtensionTests: XCTestCase { await expect { try await mocked.createFile(atPath: self.uniquePath, data:self.uniqueData) }.to(throwError(FileManagerPathError.cannotCreateFile(at: self.uniquePath))) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } func test_createFile_givenContainingDirectoryDoesNotExistAndFileCreated_shouldNotThrow() async throws { @@ -430,7 +434,7 @@ class FileManagerExtensionTests: XCTestCase { let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -438,15 +442,17 @@ class FileManagerExtensionTests: XCTestCase { return false })) - mocked.mock(closure: .createFile({ path, data, attr in - expect(path).to(equal(self.uniquePath)) - expect(data).to(equal(self.uniqueData)) + let expectedPath = self.uniquePath + let expectedData = self.uniqueData + await mocked.mock(closure: .createFile({ path, data, attr in + expect(path).to(equal(expectedPath)) + expect(data).to(equal(expectedData)) expect(attr).to(beNil()) return true })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attr in + await mocked.mock(closure: .createDirectory({ path, createIntermediates, attr in expect(path).to(equal(parentPath)) expect(createIntermediates).to(beTrue()) expect(attr).to(beNil()) @@ -456,7 +462,7 @@ class FileManagerExtensionTests: XCTestCase { await expect { try await mocked.createFile(atPath: self.uniquePath, data:self.uniqueData) }.notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } func test_createFile_givenContainingDirectoryDoesNotExistAndFileNotCreated_shouldThrow() async throws { @@ -464,7 +470,7 @@ class FileManagerExtensionTests: XCTestCase { let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -472,15 +478,17 @@ class FileManagerExtensionTests: XCTestCase { return false })) - mocked.mock(closure: .createFile({ path, data, attr in - expect(path).to(equal(self.uniquePath)) - expect(data).to(equal(self.uniqueData)) + let expectedPath = self.uniquePath + let expectedData = self.uniqueData + await mocked.mock(closure: .createFile({ path, data, attr in + expect(path).to(equal(expectedPath)) + expect(data).to(equal(expectedData)) expect(attr).to(beNil()) return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attr in + await mocked.mock(closure: .createDirectory({ path, createIntermediates, attr in expect(path).to(equal(parentPath)) expect(createIntermediates).to(beTrue()) expect(attr).to(beNil()) @@ -490,7 +498,7 @@ class FileManagerExtensionTests: XCTestCase { await expect { try await mocked.createFile(atPath: self.uniquePath, data:self.uniqueData) }.to(throwError(FileManagerPathError.cannotCreateFile(at: self.uniquePath))) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } func test_createFile_givenContainingDirectoryDoesNotExistAndError_shouldThrow() async throws { @@ -498,7 +506,7 @@ class FileManagerExtensionTests: XCTestCase { let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -506,19 +514,19 @@ class FileManagerExtensionTests: XCTestCase { return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attr in + await mocked.mock(closure: .createDirectory({ [uniqueError] path, createIntermediates, attr in expect(path).to(equal(parentPath)) expect(createIntermediates).to(beTrue()) expect(attr).to(beNil()) - throw self.uniqueError + throw uniqueError! })) // then await expect{ try await mocked.createFile(atPath: self.uniquePath, data:self.uniqueData) }.to(throwError(self.uniqueError)) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } func test_createFile_givenOverwriteFalse_whenFileExists_shouldNotThrow_shouldNotOverwrite() async throws { @@ -527,7 +535,7 @@ class FileManagerExtensionTests: XCTestCase { let directoryPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager(strict: true, requireAllClosuresCalled: false) - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in switch path { case directoryPath: isDirectory?.pointee = true case filePath: isDirectory?.pointee = false @@ -537,7 +545,7 @@ class FileManagerExtensionTests: XCTestCase { return true })) - mocked.mock(closure: .createFile({ path, data, attr in + await mocked.mock(closure: .createFile({ path, data, attr in fail("Tried to create file when overwrite was false") return false @@ -553,12 +561,12 @@ class FileManagerExtensionTests: XCTestCase { }.notTo(throwError()) } - func test_createContainingDirectory_givenFileExistsAndIsDirectory_shouldReturnEarly() throws { + func test_createContainingDirectory_givenFileExistsAndIsDirectory_shouldReturnEarly() async throws { // given let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -568,16 +576,16 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createContainingDirectory_givenFileExistsAndIsNotDirectory_shouldSucceed() throws { + func test_createContainingDirectory_givenFileExistsAndIsNotDirectory_shouldSucceed() async throws { // given let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -585,23 +593,23 @@ class FileManagerExtensionTests: XCTestCase { return true })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in + await mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in expect(path).to(equal(parentPath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) })) // then - expect(try mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createContainingDirectory_givenFileDoesNotExistAndIsDirectory_shouldSucceed() throws { + func test_createContainingDirectory_givenFileDoesNotExistAndIsDirectory_shouldSucceed() async throws { // given let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -609,23 +617,23 @@ class FileManagerExtensionTests: XCTestCase { return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in + await mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in expect(path).to(equal(parentPath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) })) // then - expect(try mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createContainingDirectory_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() throws { + func test_createContainingDirectory_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() async throws { // given let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -633,23 +641,23 @@ class FileManagerExtensionTests: XCTestCase { return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in + await mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in expect(path).to(equal(parentPath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) })) // then - expect(try mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createContainingDirectory_givenError_shouldThrow() throws { + func test_createContainingDirectory_givenError_shouldThrow() async throws { // given let parentPath = URL(fileURLWithPath: self.uniquePath).deletingLastPathComponent().path let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in + await mocked.mock(closure: .fileExists({ path, isDirectory in expect(path).to(equal(parentPath)) expect(isDirectory).notTo(beNil()) @@ -657,26 +665,26 @@ class FileManagerExtensionTests: XCTestCase { return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in + await mocked.mock(closure: .createDirectory({ [uniqueError] path, createIntermediates, attributes in expect(path).to(equal(parentPath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) - throw self.uniqueError + throw uniqueError! })) // then - expect(try mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath)) + await expect { try await mocked.createContainingDirectoryIfNeeded(forPath: self.uniquePath) } .to(throwError(self.uniqueError)) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createDirectory_givenFileExistsAndIsDirectory_shouldReturnEarly() throws { + func test_createDirectory_givenFileExistsAndIsDirectory_shouldReturnEarly() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true @@ -685,102 +693,102 @@ class FileManagerExtensionTests: XCTestCase { })) // then - expect(try mocked.createDirectoryIfNeeded(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createDirectoryIfNeeded(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createDirectory_givenFileExistsAndIsNotDirectory_shouldSucceed() throws { + func test_createDirectory_givenFileExistsAndIsNotDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false return true })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .createDirectory({ [uniquePath] path, createIntermediates, attributes in + expect(path).to(equal(uniquePath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) })) // then - expect(try mocked.createDirectoryIfNeeded(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createDirectoryIfNeeded(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createDirectory_givenFileDoesNotExistAndIsDirectory_shouldSucceed() throws { + func test_createDirectory_givenFileDoesNotExistAndIsDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = true return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .createDirectory({ [uniquePath] path, createIntermediates, attributes in + expect(path).to(equal(uniquePath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) })) // then - expect(try mocked.createDirectoryIfNeeded(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createDirectoryIfNeeded(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createDirectory_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() throws { + func test_createDirectory_givenFileDoesNotExistAndIsNotDirectory_shouldSucceed() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .createDirectory({ [uniquePath] path, createIntermediates, attributes in + expect(path).to(equal(uniquePath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) })) // then - expect(try mocked.createDirectoryIfNeeded(atPath: self.uniquePath)).notTo(throwError()) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { try await mocked.createDirectoryIfNeeded(atPath: self.uniquePath) }.notTo(throwError()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } - func test_createDirectory_givenError_shouldThrow() throws { + func test_createDirectory_givenError_shouldThrow() async throws { // given let mocked = MockApolloFileManager() - mocked.mock(closure: .fileExists({ path, isDirectory in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .fileExists({ [uniquePath] path, isDirectory in + expect(path).to(equal(uniquePath)) expect(isDirectory).notTo(beNil()) isDirectory?.pointee = false return false })) - mocked.mock(closure: .createDirectory({ path, createIntermediates, attributes in - expect(path).to(equal(self.uniquePath)) + await mocked.mock(closure: .createDirectory({ [uniquePath, uniqueError] path, createIntermediates, attributes in + expect(path).to(equal(uniquePath)) expect(createIntermediates).to(beTrue()) expect(attributes).to(beNil()) - throw self.uniqueError + throw uniqueError! })) // then - expect(try mocked.createDirectoryIfNeeded(atPath: self.uniquePath)) + await expect { try await mocked.createDirectoryIfNeeded(atPath: self.uniquePath) } .to(throwError(self.uniqueError)) - expect(mocked.allClosuresCalled).to(beTrue()) + await expect { await mocked.allClosuresCalled }.to(beTrue()) } } diff --git a/Tests/ApolloCodegenTests/GlobTests.swift b/Tests/ApolloCodegenTests/GlobTests.swift index cf044287e..3f394968f 100644 --- a/Tests/ApolloCodegenTests/GlobTests.swift +++ b/Tests/ApolloCodegenTests/GlobTests.swift @@ -11,13 +11,13 @@ class GlobTests: XCTestCase { // MARK: Setup - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() testFilePathBuilder = TestFilePathBuilder(test: self) baseURL = testFilePathBuilder.testIsolatedOutputFolder .appendingPathComponent("Glob/\(UUID().uuidString)") - try fileManager.createDirectoryIfNeeded(atPath: baseURL.path) + try await fileManager.createDirectoryIfNeeded(atPath: baseURL.path) } override func tearDownWithError() throws { @@ -35,9 +35,9 @@ class GlobTests: XCTestCase { } } - private func changeCurrentDirectory(to directory: String) throws { - try fileManager.createDirectoryIfNeeded(atPath: directory) - expect(self.fileManager.base.changeCurrentDirectoryPath(directory)).to(beTrue()) + private func changeCurrentDirectory(to directory: String) async throws { + try await fileManager.createDirectoryIfNeeded(atPath: directory) + await expect { await self.fileManager.base.changeCurrentDirectoryPath(directory) }.to(beTrue()) } // MARK: Tests @@ -384,7 +384,7 @@ class GlobTests: XCTestCase { let pattern = ["**/*.one"] // when - try changeCurrentDirectory(to: baseURL.path) + try await changeCurrentDirectory(to: baseURL.path) try await create(files: [ baseURL.appendingPathComponent("file.one").path, @@ -414,7 +414,7 @@ class GlobTests: XCTestCase { let pattern = ["**/*.one"] // when - try changeCurrentDirectory(to: baseURL.appendingPathComponent("a/").path) + try await changeCurrentDirectory(to: baseURL.appendingPathComponent("a/").path) try await create(files: [ baseURL.appendingPathComponent("file.one").path, @@ -442,7 +442,7 @@ class GlobTests: XCTestCase { let pattern = ["./**/*.one"] // when - try changeCurrentDirectory(to: baseURL.path) + try await changeCurrentDirectory(to: baseURL.path) try await create(files: [ baseURL.appendingPathComponent("file.one").path, diff --git a/Tests/ApolloCodegenTests/TestHelpers/LineByLineComparison.swift b/Tests/ApolloCodegenTests/TestHelpers/LineByLineComparison.swift index 34606e629..b7d2d6ae6 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/LineByLineComparison.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/LineByLineComparison.swift @@ -102,9 +102,9 @@ extension String { public func equalLineByLine( toFileAt expectedFileURL: URL, trimmingImports trimImports: Bool = false -) -> Nimble.Matcher { - return Matcher.define() { actual in - guard ApolloFileManager.default.doesFileExist(atPath: expectedFileURL.path) else { +) -> Nimble.AsyncMatcher { + return AsyncMatcher.define() { actual in + guard await ApolloFileManager.default.doesFileExist(atPath: expectedFileURL.path) else { return MatcherResult( status: .fail, message: .fail("File not found at \(expectedFileURL)") @@ -121,6 +121,6 @@ public func equalLineByLine( let expected = fileContents.trimmingCharacters(in: .whitespacesAndNewlines) - return try equalLineByLine(expected).satisfies(actual) + return try await equalLineByLine(expected).satisfies(actual) } } diff --git a/Tests/ApolloCodegenTests/URLDownloaderTests.swift b/Tests/ApolloCodegenTests/URLDownloaderTests.swift index 08ab33b03..7ac53b0b5 100644 --- a/Tests/ApolloCodegenTests/URLDownloaderTests.swift +++ b/Tests/ApolloCodegenTests/URLDownloaderTests.swift @@ -4,7 +4,7 @@ import ApolloCodegenInternalTestHelpers import XCTest fileprivate class FailingNetworkSession: NetworkSession { - func loadData(with urlRequest: URLRequest, completionHandler: @escaping (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTask? { + func loadData(with urlRequest: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) async -> Void) -> URLSessionDataTask? { XCTFail("You must call setRequestHandler before using downloader!") return nil @@ -102,8 +102,10 @@ class URLDownloaderTests: XCTestCase { func testDownloadError_withIncorrectResponseType_shouldThrow() throws { class CustomNetworkSession: NetworkSession { - func loadData(with urlRequest: URLRequest, completionHandler: @escaping (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTask? { - completionHandler(nil, URLResponse(), nil) + func loadData(with urlRequest: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) async -> Void) -> URLSessionDataTask? { + Task { + await completionHandler(nil, URLResponse(), nil) + } return nil } diff --git a/Tests/ApolloCodegenTests/URLExtensionsTests.swift b/Tests/ApolloCodegenTests/URLExtensionsTests.swift index f870f2a8d..f5a122e3c 100644 --- a/Tests/ApolloCodegenTests/URLExtensionsTests.swift +++ b/Tests/ApolloCodegenTests/URLExtensionsTests.swift @@ -1,5 +1,6 @@ import Foundation import XCTest +import Nimble import ApolloCodegenInternalTestHelpers import ApolloInternalTestHelpers @testable import ApolloCodegenLib @@ -29,7 +30,7 @@ class URLExtensionsTests: XCTestCase { func testGettingChildFileURL() throws { let apolloCodegenTests = FileFinder.findParentFolder() - let expectedFileURL = URL(fileURLWithPath: #file) + let expectedFileURL = URL(fileURLWithPath: #filePath) let fileURL = try apolloCodegenTests.childFileURL(fileName: "URLExtensionsTests.swift") @@ -63,37 +64,37 @@ class URLExtensionsTests: XCTestCase { XCTAssertEqual(childURL, expectedURL) } - func testIsDirectoryForExistingDirectory() { + func testIsDirectoryForExistingDirectory() async { let parentDirectory = FileFinder.findParentFolder() - XCTAssertTrue(ApolloFileManager.default.doesDirectoryExist(atPath: parentDirectory.path)) + await expect { await ApolloFileManager.default.doesDirectoryExist(atPath: parentDirectory.path) }.to(beTrue()) XCTAssertTrue(parentDirectory.isDirectoryURL) } - func testIsDirectoryForExistingFile() { + func testIsDirectoryForExistingFile() async { let currentFileURL = FileFinder.fileURL() - XCTAssertTrue(ApolloFileManager.default.doesFileExist(atPath: currentFileURL.path)) + await expect { await ApolloFileManager.default.doesFileExist(atPath: currentFileURL.path) }.to(beTrue()) XCTAssertFalse(currentFileURL.isDirectoryURL) } - func testIsSwiftFileForExistingFile() { + func testIsSwiftFileForExistingFile() async { let currentFileURL = FileFinder.fileURL() - XCTAssertTrue(ApolloFileManager.default.doesFileExist(atPath: currentFileURL.path)) + await expect { await ApolloFileManager.default.doesFileExist(atPath: currentFileURL.path) }.to(beTrue()) XCTAssertTrue(currentFileURL.isSwiftFileURL) } - func testIsSwiftFileForNonExistentFileWithSingleExtension() { + func testIsSwiftFileForNonExistentFileWithSingleExtension() async { let currentDirectory = FileFinder.findParentFolder() let doesntExist = currentDirectory.appendingPathComponent("test.swift") - XCTAssertFalse(ApolloFileManager.default.doesFileExist(atPath: doesntExist.path)) + await expect { await ApolloFileManager.default.doesFileExist(atPath: doesntExist.path) }.to(beFalse()) XCTAssertTrue(doesntExist.isSwiftFileURL) } - func testIsSwiftFileForNonExistentFileWithMultipleExtensions() { + func testIsSwiftFileForNonExistentFileWithMultipleExtensions() async { let currentDirectory = FileFinder.findParentFolder() let doesntExist = currentDirectory.appendingPathComponent("test.graphql.swift") - XCTAssertFalse(ApolloFileManager.default.doesFileExist(atPath: doesntExist.path)) + await expect { await ApolloFileManager.default.doesFileExist(atPath: doesntExist.path) }.to(beFalse()) XCTAssertTrue(doesntExist.isSwiftFileURL) } diff --git a/Tests/ApolloInternalTestHelpers/ApolloStore+Helpers.swift b/Tests/ApolloInternalTestHelpers/ApolloStore+Helpers.swift new file mode 100644 index 000000000..668e7f8f8 --- /dev/null +++ b/Tests/ApolloInternalTestHelpers/ApolloStore+Helpers.swift @@ -0,0 +1,11 @@ +@testable @_spi(Execution) import Apollo + +extension ApolloStore { + + public func loadRecord(forKey key: CacheKey) async throws -> Record { + return try await withinReadTransaction { transaction in + return try await transaction.loadObject(forKey: key).get() + } + } + +} diff --git a/Tests/ApolloInternalTestHelpers/AsyncMap.swift b/Tests/ApolloInternalTestHelpers/AsyncMap.swift index dd9a3d281..a2c2460c7 100644 --- a/Tests/ApolloInternalTestHelpers/AsyncMap.swift +++ b/Tests/ApolloInternalTestHelpers/AsyncMap.swift @@ -2,7 +2,7 @@ import Foundation extension Sequence { public func asyncMap( - _ transform: (Element) async throws -> T + _ transform: @Sendable (Element) async throws -> T ) async rethrows -> [T] { var values = [T]() @@ -14,7 +14,7 @@ extension Sequence { } public func asyncForEach( - _ body: (Element) async throws -> Void + _ body: @Sendable (Element) async throws -> Void ) async rethrows { for element in self { try await body(element) diff --git a/Tests/ApolloInternalTestHelpers/AsyncResultObserver.swift b/Tests/ApolloInternalTestHelpers/AsyncResultObserver.swift index dd3d38e7d..571dd76c7 100644 --- a/Tests/ApolloInternalTestHelpers/AsyncResultObserver.swift +++ b/Tests/ApolloInternalTestHelpers/AsyncResultObserver.swift @@ -6,7 +6,7 @@ import XCTest /// /// By default, expectations returned from `AsyncResultObserver` only expect to be called once, which is similar to how other built-in expectations work. Unexpected fulfillments will result in test failures. Usually this is what you want, and you add additional expectations with their own assertions if you expect further results. /// If multiple fulfillments of a single expectation are expected however, you can use the standard `expectedFulfillmentCount` property to change that. -public final class AsyncResultObserver { +public final actor AsyncResultObserver { public typealias ResultHandler = @Sendable (Result) throws -> Void private final class AsyncResultExpectation: XCTestExpectation, @unchecked Sendable { @@ -24,7 +24,7 @@ public final class AsyncResultObserver } private let testCase: XCTestCase - + // We keep track of the file and line number associated with the constructor as a fallback, in addition te keeping // these for each expectation. That way, we can still show a failure within the context of the test in case unexpected // results are received (which by definition do not have an associated expectation). @@ -48,23 +48,33 @@ public final class AsyncResultObserver return expectation } - @Sendable public func handler(_ result: Result) { - guard let expectation = expectations.first else { - XCTFail("Unexpected result received by handler", file: file, line: line) - return - } - - do { - try expectation.handler(result) - } catch { - testCase.record(error, file: expectation.file, line: expectation.line) - } - - expectation.fulfill() - - if expectation.numberOfFulfillments >= expectation.expectedFulfillmentCount { - expectations.removeFirst() + private func isolatedDo(_ block: @Sendable (isolated AsyncResultObserver) -> Void) { + block(self) + } + + public nonisolated func handler(_ result: Result) { + Task { + await isolatedDo { isolatedSelf in + + guard let expectation = isolatedSelf.expectations.first else { + XCTFail("Unexpected result received by handler", file: file, line: line) + return + } + + do { + try expectation.handler(result) + } catch { + isolatedSelf.testCase.record(error, file: expectation.file, line: expectation.line) + } + + expectation.fulfill() + + if expectation.numberOfFulfillments >= expectation.expectedFulfillmentCount { + isolatedSelf.expectations.removeFirst() + } + } } } + } diff --git a/Tests/ApolloInternalTestHelpers/ConcurrentTaskContainer.swift b/Tests/ApolloInternalTestHelpers/ConcurrentTaskContainer.swift index 307bcb837..c378fba7e 100644 --- a/Tests/ApolloInternalTestHelpers/ConcurrentTaskContainer.swift +++ b/Tests/ApolloInternalTestHelpers/ConcurrentTaskContainer.swift @@ -1,61 +1,44 @@ import Foundation -public class ConcurrentTaskContainer { - private let actor = Actor() - - private actor Actor { - private var tasks = [UUID: Task]() - private var waitForAllTaskContinuations = [CheckedContinuation]() - - deinit { - for task in tasks.values { - task.cancel() - } - } - - func dispatch(_ operation: @escaping @Sendable () async throws -> Void) { - let taskID = UUID() - let task = Task { - try await operation() - self.tasks.removeValue(forKey: taskID) - self.didFinishTask() - } - tasks[taskID] = task - } - - func didFinishTask() { - if tasks.isEmpty { - let continuations = waitForAllTaskContinuations - waitForAllTaskContinuations = [] - - for continuation in continuations { - continuation.resume() - } - } - } - - func waitForAllTasks() async { - guard !tasks.isEmpty else { return } - await withCheckedContinuation { continuation in - waitForAllTaskContinuations.append(continuation) - } - +public actor ConcurrentTaskContainer { + + private var tasks = [UUID: Task]() + private var waitForAllTaskContinuations = [CheckedContinuation]() + + public init() { } + + deinit { + for task in tasks.values { + task.cancel() } } - - public init() { } - - @discardableResult - public func dispatch( - _ operation: @escaping @Sendable () async throws -> Void - ) -> Task { - return Task { - await actor.dispatch(operation) + + public func dispatch(_ operation: @escaping @Sendable () async throws -> Void) { + let taskID = UUID() + let task = Task { + try await operation() + self.tasks.removeValue(forKey: taskID) + self.didFinishTask() } + tasks[taskID] = task } - + + private func didFinishTask() { + if tasks.isEmpty { + let continuations = waitForAllTaskContinuations + waitForAllTaskContinuations = [] + + for continuation in continuations { + continuation.resume() + } + } + } + public func waitForAllTasks() async { - await actor.waitForAllTasks() + guard !tasks.isEmpty else { return } + await withCheckedContinuation { continuation in + waitForAllTaskContinuations.append(continuation) + } + } - } diff --git a/Tests/ApolloInternalTestHelpers/MockGraphQLServer.swift b/Tests/ApolloInternalTestHelpers/MockGraphQLServer.swift index cca60eac9..ea81b4c21 100644 --- a/Tests/ApolloInternalTestHelpers/MockGraphQLServer.swift +++ b/Tests/ApolloInternalTestHelpers/MockGraphQLServer.swift @@ -63,13 +63,19 @@ public actor MockGraphQLServer { } } + public init() {} + private var customDelay: UInt64? // Since RequestExpectation is generic over a specific GraphQLOperation, we can't store these in the dictionary // directly. Moreover, there is no way to specify the type relationship that holds between the key and value. // To work around this, we store values as Any and use a generic subscript as a type-safe way to access them. private var requestExpectations: [AnyHashable: Any] = [:] + public func setDelay(milliseconds: UInt64) { + customDelay = milliseconds * 1_000_000 + } + private subscript(_ operationType: Operation.Type) -> RequestExpectation? { @@ -98,7 +104,7 @@ public actor MockGraphQLServer { _ operationType: Operation.Type, file: StaticString = #filePath, line: UInt = #line, - requestHandler: @escaping RequestHandler + requestHandler: @escaping @Sendable RequestHandler ) -> XCTestExpectation { let expectation = RequestExpectation( description: "Served request for \(String(describing: operationType))", @@ -137,7 +143,7 @@ public actor MockGraphQLServer { ) async throws -> JSONObject { if let expectation = self[request.operation] ?? self[type(of: request.operation)] { // Dispatch after a small random delay to spread out concurrent requests and simulate somewhat real-world conditions. - try await Task.sleep(nanoseconds: UInt64.random(in: 10...50) * 1_000_000) + try await Task.sleep(nanoseconds: customDelay ?? UInt64.random(in: 10...50) * 1_000_000) expectation.fulfill() return expectation.handler(request) diff --git a/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift b/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift index f816b6b12..85a6c6571 100644 --- a/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift +++ b/Tests/ApolloInternalTestHelpers/MockLocalCacheMutation.swift @@ -1,7 +1,7 @@ import Foundation import ApolloAPI -open class MockLocalCacheMutation: LocalCacheMutation { +open class MockLocalCacheMutation: LocalCacheMutation, @unchecked Sendable { open class var operationType: GraphQLOperationType { .query } public typealias Data = SelectionSet @@ -13,12 +13,12 @@ open class MockLocalCacheMutation: LocalC } open class MockLocalCacheMutationFromMutation: - MockLocalCacheMutation { + MockLocalCacheMutation, @unchecked Sendable { override open class var operationType: GraphQLOperationType { .mutation } } open class MockLocalCacheMutationFromSubscription: - MockLocalCacheMutation { + MockLocalCacheMutation, @unchecked Sendable { override open class var operationType: GraphQLOperationType { .subscription } } diff --git a/Tests/ApolloInternalTestHelpers/MockResponseProvider.swift b/Tests/ApolloInternalTestHelpers/MockResponseProvider.swift index d7b449429..4e1560bc7 100644 --- a/Tests/ApolloInternalTestHelpers/MockResponseProvider.swift +++ b/Tests/ApolloInternalTestHelpers/MockResponseProvider.swift @@ -74,7 +74,7 @@ extension MockResponseProvider { public static func registerRequestHandler(for url: URL, handler: @escaping SingleResponseHandler) async { await requestStorage.registerRequestHandler(for: Self.self, url: url, handler: { request in let (response, data) = try await handler(request) - var didYieldData = false + nonisolated(unsafe) var didYieldData = false let stream = AsyncThrowingStream { if didYieldData { diff --git a/Tests/ApolloInternalTestHelpers/MockSchemaMetadata.swift b/Tests/ApolloInternalTestHelpers/MockSchemaMetadata.swift index 2d5534f1e..edb82b53d 100644 --- a/Tests/ApolloInternalTestHelpers/MockSchemaMetadata.swift +++ b/Tests/ApolloInternalTestHelpers/MockSchemaMetadata.swift @@ -5,10 +5,9 @@ extension Object { public static let mock = Object(typename: "Mock", implementedInterfaces: []) } -public class MockSchemaMetadata: SchemaMetadata { - public init() { } +public class MockSchemaMetadata: SchemaMetadata { - public static var _configuration: SchemaConfiguration.Type = SchemaConfiguration.self + private nonisolated(unsafe) static var _configuration: SchemaConfiguration.Type = SchemaConfiguration.self public static let configuration: any ApolloAPI.SchemaConfiguration.Type = SchemaConfiguration.self @MainActor @@ -17,7 +16,7 @@ public class MockSchemaMetadata: SchemaMetadata { stub_cacheKeyInfoForType_Object(nil) } - private static var _objectTypeForTypeName: ((String) -> Object?)? + private nonisolated(unsafe) static var _objectTypeForTypeName: ((String) -> Object?)? public static var objectTypeForTypeName: ((String) -> Object?)? { _objectTypeForTypeName } @@ -49,7 +48,8 @@ public class MockSchemaMetadata: SchemaMetadata { } public class SchemaConfiguration: ApolloAPI.SchemaConfiguration { - static var stub_cacheKeyInfoForType_Object: ((Object, ObjectData) -> CacheKeyInfo?)? + + fileprivate static nonisolated(unsafe) var stub_cacheKeyInfoForType_Object: ((Object, ObjectData) -> CacheKeyInfo?)? public static func cacheKeyInfo(for type: Object, object: ObjectData) -> CacheKeyInfo? { stub_cacheKeyInfoForType_Object?(type, object) @@ -91,7 +91,7 @@ public struct MockCacheKeyProvider { // MARK: - Custom Mock Schemas public enum MockSchema1: SchemaMetadata { - public static var configuration: any SchemaConfiguration.Type = MockSchema1Configuration.self + public static let configuration: any SchemaConfiguration.Type = MockSchema1Configuration.self public static func objectType(forTypename __typename: String) -> Object? { Object(typename: __typename, implementedInterfaces: []) @@ -105,7 +105,7 @@ public enum MockSchema1Configuration: SchemaConfiguration { } public enum MockSchema2: SchemaMetadata { - public static var configuration: any SchemaConfiguration.Type = MockSchema2Configuration.self + public static let configuration: any SchemaConfiguration.Type = MockSchema2Configuration.self public static func objectType(forTypename __typename: String) -> Object? { Object(typename: __typename, implementedInterfaces: []) diff --git a/Tests/ApolloInternalTestHelpers/MockURLProtocol.swift b/Tests/ApolloInternalTestHelpers/MockURLProtocol.swift index fc106735a..9068f8f57 100644 --- a/Tests/ApolloInternalTestHelpers/MockURLProtocol.swift +++ b/Tests/ApolloInternalTestHelpers/MockURLProtocol.swift @@ -4,7 +4,7 @@ public enum MockURLError: Swift.Error { case requestNotHandled } -public final class MockURLProtocol: URLProtocol { +public final class MockURLProtocol: URLProtocol, @unchecked Sendable { override class public func canInit(with request: URLRequest) -> Bool { return true diff --git a/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift b/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift index 6bdc09e04..370438265 100644 --- a/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift +++ b/Tests/ApolloInternalTestHelpers/TestCacheProvider.swift @@ -17,12 +17,13 @@ public class InMemoryTestCacheProvider: TestCacheProvider { public protocol CacheDependentTesting { var cacheType: any TestCacheProvider.Type { get } - var cache: (any NormalizedCache)! { get } + var store: ApolloStore! { get } } extension CacheDependentTesting where Self: XCTestCase { - public func makeNormalizedCache() async throws -> any NormalizedCache { + public func makeTestStore() async throws -> ApolloStore { let (cache, tearDownHandler) = await cacheType.makeNormalizedCache() + nonisolated(unsafe) let `self` = self if let tearDownHandler = tearDownHandler { self.addTeardownBlock { @@ -34,10 +35,7 @@ extension CacheDependentTesting where Self: XCTestCase { } } - return cache - } - - public func mergeRecordsIntoCache(_ records: RecordSet) async { - _ = try! await cache.merge(records: records) + return ApolloStore(cache: cache) } + } diff --git a/Tests/ApolloInternalTestHelpers/TestFileHelper.swift b/Tests/ApolloInternalTestHelpers/TestFileHelper.swift index 284cc8a48..4a7f9d9b8 100644 --- a/Tests/ApolloInternalTestHelpers/TestFileHelper.swift +++ b/Tests/ApolloInternalTestHelpers/TestFileHelper.swift @@ -3,7 +3,7 @@ import Apollo public struct TestFileHelper { - public static func testParentFolder(for file: StaticString = #file) -> URL { + public static func testParentFolder(for file: StaticString = #filePath) -> URL { let fileAsString = file.withUTF8Buffer { String(decoding: $0, as: UTF8.self) } @@ -11,14 +11,14 @@ public struct TestFileHelper { return url.deletingLastPathComponent() } - public static func uploadServerFolder(from file: StaticString = #file) -> URL { + public static func uploadServerFolder(from file: StaticString = #filePath) -> URL { self.testParentFolder(for: file) .deletingLastPathComponent() // test root .deletingLastPathComponent() // source root .appendingPathComponent("SimpleUploadServer") } - public static func uploadsFolder(from file: StaticString = #file) -> URL { + public static func uploadsFolder(from file: StaticString = #filePath) -> URL { self.uploadServerFolder(from: file) .appendingPathComponent("uploads") } diff --git a/Tests/ApolloInternalTestHelpers/TestFilePathBuilder.swift b/Tests/ApolloInternalTestHelpers/TestFilePathBuilder.swift index 694f38ba1..34b078d5b 100644 --- a/Tests/ApolloInternalTestHelpers/TestFilePathBuilder.swift +++ b/Tests/ApolloInternalTestHelpers/TestFilePathBuilder.swift @@ -6,7 +6,7 @@ import XCTest /// /// To create a directory and write files to it during a unit test, use `TestIsolatedFileManager`, /// which uses this object internally for computing it's temporary directory. -public struct TestFilePathBuilder { +public struct TestFilePathBuilder: Sendable { let testName: String public let testIsolatedOutputFolder: URL diff --git a/Tests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift b/Tests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift index f4de11c00..02b51cd2a 100644 --- a/Tests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift +++ b/Tests/ApolloInternalTestHelpers/TestIsolatedFileManager.swift @@ -9,9 +9,9 @@ import XCTest /// /// You can create a file manager from within a specific unit test with the /// `testIsolatedFileManager()` function on `XCTestCase`. -public class TestIsolatedFileManager { +public actor TestIsolatedFileManager { - public var directoryURL: URL { filePathBuilder.testIsolatedOutputFolder } + nonisolated public var directoryURL: URL { filePathBuilder.testIsolatedOutputFolder } public let fileManager: FileManager public let filePathBuilder: TestFilePathBuilder @@ -119,10 +119,12 @@ public extension XCTestCase { ) addTeardownBlock { - try manager.cleanUp() + try await manager.cleanUp() } return manager } } + +extension FileManager: @unchecked @retroactive Sendable {} diff --git a/Tests/ApolloInternalTestHelpers/TestObserver.swift b/Tests/ApolloInternalTestHelpers/TestObserver.swift index 9394f5b4a..3ffdef432 100644 --- a/Tests/ApolloInternalTestHelpers/TestObserver.swift +++ b/Tests/ApolloInternalTestHelpers/TestObserver.swift @@ -35,6 +35,7 @@ public class TestObserver: NSObject, XCTestObservation { } public nonisolated func testCaseDidFinish(_ testCase: XCTestCase) { + nonisolated(unsafe) let testCase = testCase MainActor.assumeIsolated { onFinish(testCase) if stopAfterEachTest { stop() } diff --git a/Tests/ApolloInternalTestHelpers/XCTAssertHelpers.swift b/Tests/ApolloInternalTestHelpers/XCTAssertHelpers.swift index 4e7ca2d31..77cb7cc05 100644 --- a/Tests/ApolloInternalTestHelpers/XCTAssertHelpers.swift +++ b/Tests/ApolloInternalTestHelpers/XCTAssertHelpers.swift @@ -45,11 +45,11 @@ public func XCTAssertMatch(_ valueExpression: @autoclosure ( // We need overloaded versions instead of relying on default arguments // due to https://bugs.swift.org/browse/SR-1534 -public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line) rethrows { +public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #filePath, line: UInt = #line) rethrows { try XCTAssertSuccessResult(expression(), file: file, line: line, {_ in }) } -public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line, _ successHandler: (_ value: Success) throws -> Void) rethrows { +public func XCTAssertSuccessResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #filePath, line: UInt = #line, _ successHandler: (_ value: Success) throws -> Void) rethrows { let result = try expression() switch result { @@ -60,11 +60,11 @@ public func XCTAssertSuccessResult(_ expression: @autoclosure () throws } } -public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line) rethrows { +public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #filePath, line: UInt = #line) rethrows { try XCTAssertFailureResult(expression(), file: file, line: line, {_ in }) } -public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #file, line: UInt = #line, _ errorHandler: (_ error: any Error) throws -> Void) rethrows { +public func XCTAssertFailureResult(_ expression: @autoclosure () throws -> Result, file: StaticString = #filePath, line: UInt = #line, _ errorHandler: (_ error: any Error) throws -> Void) rethrows { let result = try expression() switch result { @@ -84,7 +84,13 @@ public func XCTAssertFailureResult(_ expression: @autoclosure () throws /// - test: An autoclosure for the condition to test for truthiness. /// - timeout: The timeout, at which point the test will fail. Defaults to 1 second. /// - message: A message to send on failure. -public func XCTAssertTrueEventually(_ test: @autoclosure () -> Bool, timeout: TimeInterval = 1.0, message: String = "", file: StaticString = #file, line: UInt = #line) { +public func XCTAssertTrueEventually( + _ test: @autoclosure () -> Bool, + timeout: TimeInterval = 1.0, + message: String = "", + file: StaticString = #filePath, + line: UInt = #line +) { let runLoop = RunLoop.current let timeoutDate = Date(timeIntervalSinceNow: timeout) repeat { @@ -106,7 +112,13 @@ public func XCTAssertTrueEventually(_ test: @autoclosure () -> Bool, timeout: Ti /// - test: An autoclosure for the condition to test for falsiness. /// - timeout: The timeout, at which point the test will fail. Defaults to 1 second. /// - message: A message to send on failure. -public func XCTAssertFalseEventually(_ test: @autoclosure () -> Bool, timeout: TimeInterval = 1.0, message: String = "", file: StaticString = #file, line: UInt = #line) { +public func XCTAssertFalseEventually( + _ test: @autoclosure () -> Bool, + timeout: TimeInterval = 1.0, + message: String = "", + file: StaticString = #filePath, + line: UInt = #line +) { XCTAssertTrueEventually(!test(), timeout: timeout, message: message, file: file, line: line) } @@ -145,7 +157,7 @@ public struct XCTFailure: Error, CustomNSError { public let errorCode: Int = 0 /// The user-info dictionary. - public let errorUserInfo: [String : Any] = [ + public let errorUserInfo: [String : any Sendable & Hashable] = [ // Make sure the thrown error doesn't show up as a test failure, because we already record // a more detailed failure (with the right source location) ourselves. "XCTestErrorUserInfoKeyShouldIgnore": true diff --git a/Tests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift b/Tests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift index 2d60db9e0..c4f52d0c3 100644 --- a/Tests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift +++ b/Tests/ApolloInternalTestHelpers/XCTestCase+Helpers.swift @@ -1,3 +1,5 @@ +import Apollo +import ApolloAPI import XCTest extension XCTestExpectation { @@ -7,9 +9,10 @@ extension XCTestExpectation { } } -public extension XCTestCase { +extension XCTestCase { /// Record the specified`error` as an `XCTIssue`. - func record(_ error: any Error, compactDescription: String? = nil, file: StaticString = #filePath, line: UInt = #line) { + public func record(_ error: any Error, compactDescription: String? = nil, file: StaticString = #filePath, line: UInt = #line) + { var issue = XCTIssue(type: .thrownError, compactDescription: compactDescription ?? String(describing: error)) issue.associatedError = error @@ -19,10 +22,10 @@ public extension XCTestCase { record(issue) } - + /// Invoke a throwing closure, and record any thrown errors without rethrowing. This is useful if you need to run code that may throw /// in a place where throwing isn't allowed, like `measure` blocks. - func whileRecordingErrors(file: StaticString = #file, line: UInt = #line, _ perform: () throws -> Void) { + public func whileRecordingErrors(file: StaticString = #filePath, line: UInt = #line, _ perform: () throws -> Void) { do { try perform() } catch { @@ -33,16 +36,17 @@ public extension XCTestCase { } } } - -} -import Apollo -import ApolloAPI +} public extension XCTestCase { - #warning("TODO: See if we can delete this when we refactor all of the tests.") /// Make an `AsyncResultObserver` for receiving results of the specified GraphQL operation. - func makeResultObserver(for operation: Operation, file: StaticString = #filePath, line: UInt = #line) -> AsyncResultObserver, any Error> { + func makeResultObserver( + for operation: Operation, + file: StaticString = #filePath, + line: UInt = #line + ) -> AsyncResultObserver, any Error> { + nonisolated(unsafe) let `self` = self return AsyncResultObserver(testCase: self, file: file, line: line) } } diff --git a/Tests/ApolloPaginationTests/AsyncGraphQLQueryPagerCoordinatorTests.swift b/Tests/ApolloPaginationTests/AsyncGraphQLQueryPagerCoordinatorTests.swift deleted file mode 100644 index 2ab5ab9df..000000000 --- a/Tests/ApolloPaginationTests/AsyncGraphQLQueryPagerCoordinatorTests.swift +++ /dev/null @@ -1,225 +0,0 @@ -import Apollo -import ApolloAPI -import ApolloInternalTestHelpers -import Combine -import XCTest - -@testable import ApolloPagination - -final class AsyncGraphQLQueryPagerCoordinatorTests: - XCTestCase, CacheDependentTesting, MockResponseProvider { - private typealias ReverseQuery = MockQuery - private typealias ForwardQuery = MockQuery - - var cacheType: any TestCacheProvider.Type { - InMemoryTestCacheProvider.self - } - - var cache: (any NormalizedCache)! - var server: MockGraphQLServer! - var client: ApolloClient! - var cancellables: [AnyCancellable] = [] - - @MainActor - override func setUp() async throws { - try await super.setUp() - - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) - - server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) - - client = ApolloClient(networkTransport: networkTransport, store: store) - MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) - } - - override func tearDownWithError() throws { - cache = nil - server = nil - client = nil - cancellables.forEach { $0.cancel() } - cancellables = [] - - try super.tearDownWithError() - } - - func test_canLoadMore() async throws { - let pager = createForwardPager() - - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - - await pager.fetch() - await fulfillment(of: [serverExpectation]) - - var canLoadMore = await pager.canLoadNext - XCTAssertTrue(canLoadMore) - - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let secondPageFetch = expectation(description: "Second Page") - secondPageFetch.expectedFulfillmentCount = 2 - let subscription = await pager.subscribe(onUpdate: { _ in - secondPageFetch.fulfill() - }) - try await pager.loadNext() - await fulfillment(of: [secondPageExpectation, secondPageFetch]) - subscription.cancel() - canLoadMore = await pager.canLoadNext - XCTAssertFalse(canLoadMore) - } - - func test_canLoadPrevious() async throws { - let pager = createReversePager() - - let serverExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForLastItem(server: server) - - await pager.fetch() - await fulfillment(of: [serverExpectation]) - - var canLoadMore = await pager.canLoadPrevious - XCTAssertTrue(canLoadMore) - - let secondPageExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForPreviousItem(server: server) - let secondPageFetch = expectation(description: "Second Page") - secondPageFetch.expectedFulfillmentCount = 2 - let subscription = await pager.subscribe(onUpdate: { _ in - secondPageFetch.fulfill() - }) - try await pager.loadPrevious() - await fulfillment(of: [secondPageExpectation, secondPageFetch]) - subscription.cancel() - canLoadMore = await pager.canLoadPrevious - XCTAssertFalse(canLoadMore) - } - - @available(iOS 16.0, macOS 13.0, *) - func test_actor_canResetMidflight() async throws { - server.customDelay = .milliseconds(150) - let pager = createForwardPager() - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - - await pager.subscribe(onUpdate: { _ in - XCTFail("We should never get results back") - }).store(in: &cancellables) - - Task { - try await pager.loadAll() - } - - Task { - try? await Task.sleep(for: .milliseconds(10)) - await pager.reset() - } - - await fulfillment(of: [serverExpectation], timeout: 1.0) - } - - @available(iOS 16.0, macOS 13.0, *) - func test__reset__loadingState() async throws { - server.customDelay = .milliseconds(150) - let pager = createForwardPager() - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - - await pager.subscribe(onUpdate: { _ in - XCTFail("We should never get results back") - }).store(in: &cancellables) - - Task { - try await pager.loadAll() - } - - Task { - try? await Task.sleep(for: .milliseconds(10)) - await pager.reset() - } - - await fulfillment(of: [serverExpectation], timeout: 1.0) - async let isLoadingAll = pager.isLoadingAll - async let isFetching = pager.isFetching - let loadingStates = await [isFetching, isLoadingAll] - loadingStates.forEach { XCTAssertFalse($0) } - } - - @available(iOS 16.0, macOS 13.0, *) - func test__reset__midflight_isFetching_isFalse() async throws { - server.customDelay = .milliseconds(1) - let pager = createForwardPager() - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - - await pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1.0) - - server.customDelay = .seconds(3) - Task { - try? await pager.loadNext() - } - let cancellationExpectation = expectation(description: "finished cancellation") - Task { - try? await Task.sleep(for: .milliseconds(50)) - await pager.reset() - cancellationExpectation.fulfill() - } - - await fulfillment(of: [cancellationExpectation]) - let isFetching = await pager.isFetching - XCTAssertFalse(isFetching) - } - - private func createReversePager() -> AsyncGraphQLQueryPagerCoordinator { - let initialQuery = ReverseQuery() - initialQuery.__variables = ["id": "2001", "first": 2, "before": "Y3Vyc29yMw=="] - return AsyncGraphQLQueryPagerCoordinator( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, - extractPageInfo: { data in - switch data { - case .initial(let data, _), .paginated(let data, _): - return CursorBasedPagination.Reverse( - hasPrevious: data.hero.friendsConnection.pageInfo.hasPreviousPage, - startCursor: data.hero.friendsConnection.pageInfo.startCursor - ) - } - }, - pageResolver: { pageInfo, direction in - guard direction == .previous else { return nil } - let nextQuery = ReverseQuery() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "before": pageInfo.startCursor, - ] - return nextQuery - } - ) - } - - private func createForwardPager() -> AsyncGraphQLQueryPagerCoordinator { - let initialQuery = ForwardQuery() - initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return AsyncGraphQLQueryPagerCoordinator( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, - extractPageInfo: { data in - switch data { - case .initial(let data, _), .paginated(let data, _): - return CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - } - }, - pageResolver: { pageInfo, direction in - guard direction == .next else { return nil } - let nextQuery = ForwardQuery() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": pageInfo.endCursor, - ] - return nextQuery - } - ) - } -} diff --git a/Tests/ApolloPaginationTests/AsyncGraphQLQueryPagerTests.swift b/Tests/ApolloPaginationTests/AsyncGraphQLQueryPagerTests.swift deleted file mode 100644 index 144d81cf1..000000000 --- a/Tests/ApolloPaginationTests/AsyncGraphQLQueryPagerTests.swift +++ /dev/null @@ -1,481 +0,0 @@ -import Apollo -import ApolloAPI -import ApolloInternalTestHelpers -import XCTest - -@testable import ApolloPagination - -final class AsyncGraphQLQueryPagerTests: XCTestCase { - private typealias Query = MockQuery - - private var store: ApolloStore! - private var server: MockGraphQLServer! - private var networkTransport: MockNetworkTransport! - private var client: ApolloClient! - - override func setUp() { - super.setUp() - store = ApolloStore(cache: InMemoryNormalizedCache()) - server = MockGraphQLServer() - networkTransport = MockNetworkTransport(server: server, store: store) - client = ApolloClient(networkTransport: networkTransport, store: store) - } - - func test_forwardInit_simple() async throws { - let initialQuery = Query() - initialQuery.__variables = [ - "id": "2001", - "first": 2, - "after": GraphQLNullable.null, - ] - let pager = AsyncGraphQLQueryPager( - client: client, - initialQuery: initialQuery, - extractPageInfo: { data in - CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - }, - pageResolver: { page, direction in - switch direction { - case .next: - let nextQuery = Query() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": page.endCursor, - ] - return nextQuery - case .previous: - return nil - } - } - ) - - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - await pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1) - - var canLoadMore = await pager.canLoadNext - XCTAssertTrue(canLoadMore) - - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let secondPageFetch = expectation(description: "Second Page") - secondPageFetch.expectedFulfillmentCount = 2 - let subscription = pager.sink { _ in - secondPageFetch.fulfill() - } - try await pager.loadNext() - await fulfillment(of: [secondPageExpectation, secondPageFetch]) - subscription.cancel() - canLoadMore = await pager.canLoadNext - XCTAssertFalse(canLoadMore) - } - - func test_forwardInit_simple_mapping() async throws { - struct ViewModel { - let name: String - } - - let initialQuery = Query() - initialQuery.__variables = [ - "id": "2001", - "first": 2, - "after": GraphQLNullable.null, - ] - let pager = AsyncGraphQLQueryPager( - client: client, - initialQuery: initialQuery, - extractPageInfo: { data in - CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - }, - pageResolver: { page, direction in - switch direction { - case .next: - let nextQuery = Query() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": page.endCursor, - ] - return nextQuery - case .previous: - return nil - } - } - ) - - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - await pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1) - - var canLoadMore = await pager.canLoadNext - XCTAssertTrue(canLoadMore) - - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let secondPageFetch = expectation(description: "Second Page") - secondPageFetch.expectedFulfillmentCount = 2 - let subscription = pager - .compactMap { output -> [ViewModel]? in - guard case .success(let output) = output else { return nil } - let models = output.allData.flatMap { data in - data.hero.friendsConnection.friends.map { friend in ViewModel(name: friend.name) } - } - return models - } - .sink { _ in - secondPageFetch.fulfill() - } - - try await pager.loadNext() - await fulfillment(of: [secondPageExpectation, secondPageFetch]) - subscription.cancel() - canLoadMore = await pager.canLoadNext - XCTAssertFalse(canLoadMore) - } - - func test_forwardInit_singleQuery_transform() async throws { - let initialQuery = Query() - initialQuery.__variables = [ - "id": "2001", - "first": 2, - "after": GraphQLNullable.null, - ] - let pager = AsyncGraphQLQueryPager( - client: client, - initialQuery: initialQuery, - extractPageInfo: { data in - CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - }, - pageResolver: { page, direction in - switch direction { - case .next: - let nextQuery = Query() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": page.endCursor, - ] - return nextQuery - case .previous: - return nil - } - } - ) - - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - await pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1) - - var canLoadMore = await pager.canLoadNext - XCTAssertTrue(canLoadMore) - - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let secondPageFetch = expectation(description: "Second Page") - secondPageFetch.expectedFulfillmentCount = 2 - let subscription = pager.sink { _ in - secondPageFetch.fulfill() - } - try await pager.loadNext() - await fulfillment(of: [secondPageExpectation, secondPageFetch]) - subscription.cancel() - canLoadMore = await pager.canLoadNext - XCTAssertFalse(canLoadMore) - } - - func test_forwardInit_singleQuery_transform_mapping() async throws { - struct ViewModel { - let name: String - } - - let initialQuery = Query() - initialQuery.__variables = [ - "id": "2001", - "first": 2, - "after": GraphQLNullable.null, - ] - let pager = AsyncGraphQLQueryPager( - client: client, - initialQuery: initialQuery, - extractPageInfo: { data in - CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - }, - pageResolver: { page, direction in - switch direction { - case .next: - let nextQuery = Query() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": page.endCursor, - ] - return nextQuery - case .previous: - return nil - } - } - ) - - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - await pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1) - - var canLoadMore = await pager.canLoadNext - XCTAssertTrue(canLoadMore) - - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let secondPageFetch = expectation(description: "Second Page") - secondPageFetch.expectedFulfillmentCount = 2 - let subscription = pager - .compactMap { result in - switch result { - case .success(let output): - return output.allData.flatMap { data in - data.hero.friendsConnection.friends.map { friend in friend.name } - }.map(ViewModel.init(name:)) - case .failure(let error): - XCTFail("Unexpected failure: \(error)") - return nil - } - }.sink { _ in - secondPageFetch.fulfill() - } - try await pager.loadNext() - await fulfillment(of: [secondPageExpectation, secondPageFetch]) - subscription.cancel() - canLoadMore = await pager.canLoadNext - XCTAssertFalse(canLoadMore) - } - - func test_concatenatesPages_matchingInitialAndPaginated() async throws { - struct ViewModel { - let name: String - } - - let anyPager = createPager() - - let fetchExpectation = expectation(description: "Initial Fetch") - fetchExpectation.assertForOverFulfill = false - let subscriptionExpectation = expectation(description: "Subscription") - subscriptionExpectation.expectedFulfillmentCount = 2 - var expectedViewModels: [ViewModel]? - let subscriber = anyPager - .compactMap { result in - switch result { - case .success(let data): - return data.allData.flatMap { data in - data.hero.friendsConnection.friends.map { - ViewModel(name: $0.name) - } - } - case .failure(let error): - XCTFail(error.localizedDescription) - return nil - } - }.sink { viewModels in - expectedViewModels = viewModels - fetchExpectation.fulfill() - subscriptionExpectation.fulfill() - } - - await fetchFirstPage(pager: anyPager) - await fulfillment(of: [fetchExpectation], timeout: 1) - try await fetchSecondPage(pager: anyPager) - - await fulfillment(of: [subscriptionExpectation], timeout: 1) - let results = try XCTUnwrap(expectedViewModels) - XCTAssertEqual(results.count, 3) - XCTAssertEqual(results.map(\.name), ["Luke Skywalker", "Han Solo", "Leia Organa"]) - subscriber.cancel() - } - - func test_passesBackSeparateData() async throws { - let anyPager = createPager() - - let initialExpectation = expectation(description: "Initial") - let secondExpectation = expectation(description: "Second") - var expectedViewModel: String? - let subscriber = anyPager - .compactMap { result in - switch result { - case .success(let output): - if let latestPage = output.nextPages.last { - return latestPage.data?.hero.friendsConnection.friends.last?.name - } - return output.initialPage?.data?.hero.friendsConnection.friends.last?.name - case .failure(let error): - XCTFail(error.localizedDescription) - return nil - } - } - .sink { viewModel in - let oldValue = expectedViewModel - expectedViewModel = viewModel - if oldValue == nil { - initialExpectation.fulfill() - } else { - secondExpectation.fulfill() - } - } - - await fetchFirstPage(pager: anyPager) - await fulfillment(of: [initialExpectation], timeout: 1) - XCTAssertEqual(expectedViewModel, "Han Solo") - - try await fetchSecondPage(pager: anyPager) - await fulfillment(of: [secondExpectation], timeout: 1) - XCTAssertEqual(expectedViewModel, "Leia Organa") - subscriber.cancel() - } - - func test_loadAll() async throws { - let pager = createPager() - - let firstPageExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - let lastPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let loadAllExpectation = expectation(description: "Load all pages") - let subscriber = pager.sink { _ in - loadAllExpectation.fulfill() - } - try await pager.loadAll() - await fulfillment(of: [firstPageExpectation, lastPageExpectation, loadAllExpectation], timeout: 5) - subscriber.cancel() - } - - func test_errors_partialSuccess() async throws { - let pager = createPager() - var expectedResults: [Result, any Error>] = [] - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPageWithErrors(server: server) - let fetchExpectation = expectation(description: "Fetch") - let subscription = pager.sink { output in - expectedResults.append(output) - fetchExpectation.fulfill() - } - await pager.fetch() - await fulfillment(of: [serverExpectation, fetchExpectation], timeout: 3) - XCTAssertEqual(expectedResults.count, 1) - let result = try XCTUnwrap(expectedResults.first) - let successValue = try result.get() - XCTAssertFalse(successValue.allErrors.isEmpty) - XCTAssertEqual(successValue.initialPage?.data?.hero.name, "R2-D2") - let canLoadNext = await pager.canLoadNext - XCTAssertTrue(canLoadNext) - subscription.cancel() - } - - func test_errors_noData() async throws { - let pager = createPager() - var expectedResults: [Result, any Error>] = [] - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPageErrorsOnly(server: server) - let fetchExpectation = expectation(description: "Fetch") - let subscription = pager.sink { output in - expectedResults.append(output) - fetchExpectation.fulfill() - } - await pager.fetch() - await fulfillment(of: [serverExpectation, fetchExpectation], timeout: 3) - XCTAssertEqual(expectedResults.count, 1) - let result = try XCTUnwrap(expectedResults.first) - let successValue = try result.get() - XCTAssertFalse(successValue.allErrors.isEmpty) - XCTAssertNil(successValue.initialPage?.data) - subscription.cancel() - } - - func test_errors_noData_loadAll() async throws { - let pager = createPager() - var expectedResults: [Result, any Error>] = [] - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPageErrorsOnly(server: server) - let fetchExpectation = expectation(description: "Fetch") - let subscription = pager.sink { output in - expectedResults.append(output) - XCTAssertEqual(expectedResults.count, 1) - do { - let result = try XCTUnwrap(expectedResults.first) - let successValue = try result.get() - XCTAssertFalse(successValue.allErrors.isEmpty) - XCTAssertNil(successValue.initialPage?.data) - } catch { - XCTFail(error.localizedDescription) - } - fetchExpectation.fulfill() - } - try await pager.loadAll(fetchFromInitialPage: true) - await fulfillment(of: [serverExpectation, fetchExpectation], timeout: 3) - subscription.cancel() - } - - func test_errors_noDataOnSecondPage_loadAll() async throws { - let pager = createPager() - var expectedResults: [Result, any Error>] = [] - - let firstPageExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - let lastPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPageErrorsOnly(server: server) - let loadAllExpectation = expectation(description: "Load all pages") - let subscriber = pager.sink { output in - expectedResults.append(output) - loadAllExpectation.fulfill() - } - try await pager.loadAll() - await fulfillment(of: [firstPageExpectation, lastPageExpectation, loadAllExpectation], timeout: 5) - let result = try XCTUnwrap(expectedResults.first) - let successValue = try result.get() - XCTAssertFalse(successValue.allErrors.isEmpty) - XCTAssertNotNil(successValue.initialPage) - XCTAssertNil(successValue.nextPages[0].data) - subscriber.cancel() - } - - // MARK: - Test helpers - - private func createPager() -> AsyncGraphQLQueryPager> { - let initialQuery = Query() - initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return .init(pager: AsyncGraphQLQueryPagerCoordinator( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, - extractPageInfo: { data in - switch data { - case .initial(let data, _), .paginated(let data, _): - return CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - } - }, - pageResolver: { pageInfo, direction in - guard direction == .next else { return nil } - let nextQuery = Query() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": pageInfo.endCursor, - ] - return nextQuery - } - )) - } - - private func fetchFirstPage(pager: AsyncGraphQLQueryPager) async { - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - await pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1) - } - - private func fetchSecondPage(pager: AsyncGraphQLQueryPager) async throws { - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - try await pager.loadNext() - await fulfillment(of: [serverExpectation], timeout: 1) - } -} diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index ad2faddaf..dd0c4ce54 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -1,7 +1,7 @@ import Apollo import ApolloAPI import ApolloInternalTestHelpers -import Combine +@preconcurrency import Combine import XCTest @testable import ApolloPagination @@ -14,27 +14,25 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! var cancellables: [AnyCancellable] = [] - @MainActor override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) - MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) + await MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) } override func tearDownWithError() throws { - cache = nil + store = nil server = nil client = nil cancellables.removeAll() @@ -44,13 +42,12 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { // MARK: - Test Helpers - private func createPager() -> AsyncGraphQLQueryPagerCoordinator { + private func createPager() -> GraphQLQueryPagerCoordinator { let initialQuery = Query() initialQuery.__variables = ["id": "2001", "first": 1, "after": "Y3Vyc29yMw==", "before": GraphQLNullable.null] - return AsyncGraphQLQueryPagerCoordinator( + return GraphQLQueryPagerCoordinator( client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, + initialQuery: initialQuery, extractPageInfo: { data in switch data { case .initial(let data, _), .paginated(let data, _): @@ -69,7 +66,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { nextQuery.__variables = [ "id": "2001", "first": 1, - "after": pageInfo.endCursor, + "after": pageInfo.endCursor ?? .null, "before": GraphQLNullable.null, ] return nextQuery @@ -78,7 +75,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { previousQuery.__variables = [ "id": "2001", "first": 1, - "before": pageInfo.startCursor, + "before": pageInfo.startCursor ?? .null, "after": GraphQLNullable.null, ] return previousQuery @@ -91,7 +88,9 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { func test_fetchMultiplePages_async() async throws { let pager = createPager() - let serverExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList(server: server) + let serverExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList( + server: server + ) var results: [Result, any Error>] = [] let firstPageExpectation = expectation(description: "First page") @@ -110,7 +109,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { XCTAssertEqual(output.initialPage?.source, .server) } - let secondPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) + let secondPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) let secondPageFetch = expectation(description: "Second Page") secondPageFetch.expectedFulfillmentCount = 2 subscription = await pager.subscribe(onUpdate: { _ in @@ -141,7 +140,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { var nextCount = await pager.nextPageVarMap.values.count XCTAssertEqual(nextCount, 1) - let previousPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) + let previousPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) let previousPageFetch = expectation(description: "Previous Page") previousPageFetch.assertForOverFulfill = false previousPageFetch.expectedFulfillmentCount = 2 @@ -177,9 +176,11 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { func test_loadAll_async() async throws { let pager = createPager() - let firstPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList(server: server) - let previousPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) - let lastPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) + let firstPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList( + server: server + ) + let previousPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) + let lastPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) let loadAllExpectation = expectation(description: "Load all pages") await pager.subscribe(onUpdate: { _ in @@ -206,18 +207,21 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { // MARK: - GraphQLQueryPager tests func test_fetchMultiplePages() async throws { - let pager = GraphQLQueryPagerCoordinator(pager: createPager()) - let serverExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList(server: server) + let pager = createPager() + let serverExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList( + server: server + ) var results: [Result, any Error>] = [] let firstPageExpectation = expectation(description: "First page") - var subscription = await pager.publisher.sink { _ in + var subscription = await pager.$currentValue.compactMap { $0 }.sink { value in firstPageExpectation.fulfill() } - pager.fetch() + await pager.fetch() await fulfillment(of: [serverExpectation, firstPageExpectation], timeout: 1) subscription.cancel() - var result = try await XCTUnwrapping(await pager.pager.currentValue) + + var result = try await XCTUnwrapping(await pager.currentValue) results.append(result) XCTAssertSuccessResult(result) { output in XCTAssertTrue(output.nextPages.isEmpty) @@ -226,18 +230,18 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { XCTAssertEqual(output.initialPage?.source, .server) } - let secondPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) + let secondPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) let secondPageFetch = expectation(description: "Second Page") secondPageFetch.expectedFulfillmentCount = 2 - subscription = await pager.publisher.sink { _ in + subscription = await pager.$currentValue.sink { _ in secondPageFetch.fulfill() } - pager.loadNext() + try await pager.loadNext() await fulfillment(of: [secondPageExpectation, secondPageFetch], timeout: 1) subscription.cancel() - result = try await XCTUnwrapping(await pager.pager.currentValue) + result = try await XCTUnwrapping(await pager.currentValue) results.append(result) try XCTAssertSuccessResult(result) { output in @@ -253,19 +257,19 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { XCTAssertEqual(page.source, .server) } - let previousPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) + let previousPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) let previousPageFetch = expectation(description: "Previous Page") previousPageFetch.assertForOverFulfill = false previousPageFetch.expectedFulfillmentCount = 2 - subscription = await pager.publisher.sink { _ in + subscription = await pager.$currentValue.sink { _ in previousPageFetch.fulfill() } - pager.loadPrevious() + try await pager.loadPrevious() await fulfillment(of: [previousPageExpectation, previousPageFetch], timeout: 1) subscription.cancel() - result = try await XCTUnwrapping(await pager.pager.currentValue) + result = try await XCTUnwrapping(await pager.currentValue) results.append(result) try XCTAssertSuccessResult(result) { output in @@ -283,23 +287,25 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { } func test_loadAll() async throws { - let pager = GraphQLQueryPagerCoordinator(pager: createPager()) + let pager = createPager() - let firstPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList(server: server) - let previousPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) - let lastPageExpectation = Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) + let firstPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForFirstFetchInMiddleOfList( + server: server + ) + let previousPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForPreviousPage(server: server) + let lastPageExpectation = await Mocks.Hero.BidirectionalFriendsQuery.expectationForLastPage(server: server) let loadAllExpectation = expectation(description: "Load all pages") - pager.subscribe(onUpdate: { _ in + let cancellable = await pager.subscribe(onUpdate: { _ in loadAllExpectation.fulfill() }) - pager.loadAll() + try await pager.loadAll() await fulfillment( of: [firstPageExpectation, lastPageExpectation, previousPageExpectation, loadAllExpectation], timeout: 5 ) - let result = try await XCTUnwrapping(try await pager.pager.currentValue?.get()) + let result = try await XCTUnwrapping(try await pager.currentValue?.get()) XCTAssertFalse(result.previousPages.isEmpty) XCTAssertEqual(result.initialPage?.data?.hero.friendsConnection.friends.count, 1) XCTAssertFalse(result.nextPages.isEmpty) @@ -310,5 +316,6 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { ).flatMap { $0 } + (result.initialPage?.data?.hero.friendsConnection.friends ?? []) XCTAssertEqual(Set(friends).count, 3) + cancellable.cancel() } } diff --git a/Tests/ApolloPaginationTests/ConcurrencyTest.swift b/Tests/ApolloPaginationTests/ConcurrencyTest.swift index d42352af2..9a18f3a49 100644 --- a/Tests/ApolloPaginationTests/ConcurrencyTest.swift +++ b/Tests/ApolloPaginationTests/ConcurrencyTest.swift @@ -1,7 +1,7 @@ import Apollo import ApolloAPI import ApolloInternalTestHelpers -import Combine +@preconcurrency import Combine import XCTest @testable import ApolloPagination @@ -19,92 +19,63 @@ final class ConcurrencyTests: XCTestCase { super.setUp() store = ApolloStore(cache: InMemoryNormalizedCache()) server = MockGraphQLServer() - networkTransport = MockNetworkTransport(server: server, store: store) + networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) } // MARK: - Test helpers private func loadDataFromManyThreads( - pager: AsyncGraphQLQueryPagerCoordinator, + pager: GraphQLQueryPagerCoordinator, expectation: XCTestExpectation ) async { + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: self.server) await withTaskGroup(of: Void.self) { group in - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: self.server) group.addTask { try? await pager.loadNext() } group.addTask { try? await pager.loadNext() } group.addTask { try? await pager.loadNext() } group.addTask { try? await pager.loadNext() } group.addTask { try? await pager.loadNext() } - group.addTask { await self.fulfillment(of: [serverExpectation, expectation], timeout: 100) } await group.waitForAll() } + + await self.fulfillment(of: [serverExpectation, expectation], timeout: 100) } private func loadDataFromManyThreadsThrowing( - pager: AsyncGraphQLQueryPagerCoordinator + pager: GraphQLQueryPagerCoordinator ) async throws { + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: self.server) try await withThrowingTaskGroup(of: Void.self) { group in - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: self.server) group.addTask { try await pager.loadNext() } group.addTask { try await pager.loadNext() } group.addTask { try await pager.loadNext() } group.addTask { try await pager.loadNext() } group.addTask { try await pager.loadNext() } - group.addTask { await self.fulfillment(of: [serverExpectation], timeout: 100) } try await group.waitForAll() } + await self.fulfillment(of: [serverExpectation], timeout: 100) } private func loadDataFromManyThreads( pager: GraphQLQueryPagerCoordinator - ) { - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: self.server) + ) async throws { + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: self.server) - (0..<5).forEach { _ in - pager.loadNext() + for _ in (0..<5) { + try await pager.loadNext() } - wait(for: [serverExpectation], timeout: 1.0) - } - - private func createPager() -> AsyncGraphQLQueryPagerCoordinator { - let initialQuery = Query() - initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return AsyncGraphQLQueryPagerCoordinator( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, - extractPageInfo: { data in - switch data { - case .initial(let data, _), .paginated(let data, _): - return CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - } - }, - pageResolver: { pageInfo, direction in - guard direction == .next else { return nil } - let nextQuery = Query() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": pageInfo.endCursor, - ] - return nextQuery - } - ) + await fulfillment(of: [serverExpectation], timeout: 1.0) } - private func createNonisolatedPager() -> GraphQLQueryPagerCoordinator { + private func createPager() -> GraphQLQueryPagerCoordinator { let initialQuery = Query() initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] return GraphQLQueryPagerCoordinator( client: client, initialQuery: initialQuery, - watcherDispatchQueue: .main, extractPageInfo: { data in switch data { case .initial(let data, _), .paginated(let data, _): @@ -120,15 +91,15 @@ final class ConcurrencyTests: XCTestCase { nextQuery.__variables = [ "id": "2001", "first": 2, - "after": pageInfo.endCursor, + "after": pageInfo.endCursor ?? .null, ] return nextQuery } ) } - private func fetchFirstPage(pager: AsyncGraphQLQueryPagerCoordinator) async { - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + private func fetchFirstPage(pager: GraphQLQueryPagerCoordinator) async { + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) await pager.fetch() await fulfillment(of: [serverExpectation], timeout: 1.0) } @@ -137,7 +108,7 @@ final class ConcurrencyTests: XCTestCase { func test_concurrentFetches() async throws { let pager = createPager() - var results: [Result, any Error>] = [] + nonisolated(unsafe) var results: [Result, any Error>] = [] let resultsExpectation = expectation(description: "Results arrival") resultsExpectation.expectedFulfillmentCount = 2 await pager.subscribe { result in @@ -158,26 +129,4 @@ final class ConcurrencyTests: XCTestCase { } } - func test_concurrentFetches_nonisolated() throws { - let pager = createNonisolatedPager() - var results: [Result, any Error>] = [] - let initialExpectation = expectation(description: "Initial") - initialExpectation.assertForOverFulfill = false - let nextExpectation = expectation(description: "Next") - nextExpectation.expectedFulfillmentCount = 2 - pager.subscribe(onUpdate: { - results.append($0) - initialExpectation.fulfill() - nextExpectation.fulfill() - }) - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - pager.fetch() - wait(for: [serverExpectation, initialExpectation], timeout: 1.0) - - XCTAssertEqual(results.count, 1) - loadDataFromManyThreads(pager: pager) - wait(for: [nextExpectation], timeout: 1) - - XCTAssertEqual(results.count, 2) - } } diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 978d632e5..b1d5dca08 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -1,7 +1,7 @@ import Apollo import ApolloAPI import ApolloInternalTestHelpers -import Combine +@preconcurrency import Combine import XCTest @testable import ApolloPagination @@ -14,27 +14,25 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! var cancellables: [AnyCancellable] = [] - @MainActor override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) - MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) + await MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) } override func tearDownWithError() throws { - cache = nil + store = nil server = nil client = nil cancellables.forEach { $0.cancel() } @@ -46,7 +44,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { func test_fetchMultiplePages() async throws { let pager = createPager() - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) var results: [Result, any Error>] = [] let firstPageExpectation = expectation(description: "First page") @@ -65,7 +63,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { XCTAssertEqual(output.initialPage?.source, .server) } - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) let secondPageFetch = expectation(description: "Second Page") secondPageFetch.expectedFulfillmentCount = 2 subscription = await pager.subscribe(onUpdate: { _ in @@ -100,18 +98,18 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { func test_variableMapping() async throws { let pager = createPager() - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) await pager.fetch() await fulfillment(of: [serverExpectation]) - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) let secondPageFetch = expectation(description: "Second Page") secondPageFetch.expectedFulfillmentCount = 2 let subscription = await pager.subscribe(onUpdate: { _ in secondPageFetch.fulfill() }) - try await pager.loadNext(cachePolicy: .fetchIgnoringCacheData) + try await pager.loadNext(fetchBehavior: .NetworkOnly) await fulfillment(of: [secondPageExpectation, secondPageFetch]) subscription.cancel() @@ -124,12 +122,9 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { "after": "Y3Vyc29yMg==", ] - let expectedVariables = Set(nextQuery.__variables?.underlyingJsonValues ?? []) + let expectedVariables = PageVariables(nextQuery.__variables!) let actualVariables = try await XCTUnwrapping(await pager.nextPageVarMap.keys.first) - XCTAssertEqual(expectedVariables.count, actualVariables.count) - XCTAssertEqual(expectedVariables.count, 3) - XCTAssertEqual(expectedVariables, actualVariables) } @@ -139,7 +134,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { var nextPageInfo = await pager.nextPageInfo XCTAssertNil(nextPageInfo) - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) await pager.fetch() await fulfillment(of: [serverExpectation]) @@ -152,13 +147,13 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { ) XCTAssertEqual(page, expectedFirstPage) - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) let secondPageFetch = expectation(description: "Second Page") secondPageFetch.expectedFulfillmentCount = 2 let subscription = await pager.subscribe(onUpdate: { _ in secondPageFetch.fulfill() }) - try await pager.loadNext(cachePolicy: .fetchIgnoringCacheData) + try await pager.loadNext(fetchBehavior: .NetworkOnly) await fulfillment(of: [secondPageExpectation, secondPageFetch]) subscription.cancel() @@ -175,7 +170,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { func test_fetchMultiplePages_mutateHero() async throws { let pager = createPager() - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) let firstPageExpectation = expectation(description: "First page") var subscription = await pager.subscribe(onUpdate: { _ in firstPageExpectation.fulfill() @@ -191,7 +186,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { XCTAssertEqual(output.initialPage?.source, .server) } - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) let secondPageFetch = expectation(description: "Second Page") secondPageFetch.expectedFulfillmentCount = 2 subscription = await pager.subscribe(onUpdate: { _ in @@ -218,10 +213,10 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { let mutationExpectation = expectation(description: "Mutation") mutationExpectation.expectedFulfillmentCount = 3 // once for subscribe, 2 for pages refreshing await pager.subscribe(onUpdate: { _ in mutationExpectation.fulfill() }).store(in: &cancellables) - client.store.withinReadWriteTransaction { transaction in + try await client.store.withinReadWriteTransaction { transaction in let cacheMutation = MockLocalCacheMutation() cacheMutation.__variables = ["id": "2001"] - try! transaction.update(cacheMutation) { data in + try! await transaction.update(cacheMutation) { data in data.hero?.name = "C3PO" transactionExpectation.fulfill() } @@ -238,8 +233,8 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { func test_loadAll() async throws { let pager = createPager() - let firstPageExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - let lastPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let firstPageExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let lastPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) let loadAllExpectation = expectation(description: "Load all pages") await pager.subscribe(onUpdate: { _ in loadAllExpectation.fulfill() @@ -251,7 +246,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { func test_failingFetch_finishes() async throws { let initialQuery = Query() initialQuery.__variables = ["id": "2001", "flirst": 2, "after": GraphQLNullable.none] - let pager = AsyncGraphQLQueryPagerCoordinator( + let pager = GraphQLQueryPagerCoordinator( client: client, initialQuery: initialQuery, extractPageInfo: { data in @@ -269,12 +264,12 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { nextQuery.__variables = [ "id": "2001", "first": 2, - "after": pageInfo.endCursor, + "after": pageInfo.endCursor ?? .null, ] return nextQuery } ) - let lastPageExpectation = Mocks.Hero.FriendsQuery.failingExpectation(server: server) + let lastPageExpectation = await Mocks.Hero.FriendsQuery.failingExpectation(server: server) let cancellable = await pager.subscribe { result in try? XCTAssertThrowsError(result.get()) @@ -284,13 +279,12 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { cancellable.cancel() } - private func createPager() -> AsyncGraphQLQueryPagerCoordinator { + private func createPager() -> GraphQLQueryPagerCoordinator { let initialQuery = Query() initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return AsyncGraphQLQueryPagerCoordinator( + return GraphQLQueryPagerCoordinator( client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, + initialQuery: initialQuery, extractPageInfo: { data in switch data { case .initial(let data, _), .paginated(let data, _): @@ -306,7 +300,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { nextQuery.__variables = [ "id": "2001", "first": 2, - "after": pageInfo.endCursor, + "after": pageInfo.endCursor ?? .null, ] return nextQuery } @@ -315,10 +309,10 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { } private extension Mocks.Hero.FriendsQuery { - static func failingExpectation(server: MockGraphQLServer) -> XCTestExpectation { + static func failingExpectation(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "flirst": 2, "after": GraphQLNullable.none] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "endCursor": "Y3Vyc29yMg==", @@ -336,26 +330,26 @@ private extension Mocks.Hero.FriendsQuery { "id": "1002", ], ] - let friendsConnection: [String: AnyHashable] = [ + let friendsConnection = [ "__typename": "FriendsConnection", "totalCount": 3, "friends": friends, "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } diff --git a/Tests/ApolloPaginationTests/FriendsQuery+TestHelpers.swift b/Tests/ApolloPaginationTests/FriendsQuery+TestHelpers.swift index fed0b74f4..af5a54fcb 100644 --- a/Tests/ApolloPaginationTests/FriendsQuery+TestHelpers.swift +++ b/Tests/ApolloPaginationTests/FriendsQuery+TestHelpers.swift @@ -3,10 +3,10 @@ import ApolloInternalTestHelpers import XCTest extension Mocks.Hero.FriendsQuery { - static func expectationForFirstPage(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForFirstPage(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "endCursor": "Y3Vyc29yMg==", @@ -31,27 +31,27 @@ extension Mocks.Hero.FriendsQuery { "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } - static func expectationForSecondPage(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForSecondPage(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 2, "after": "Y3Vyc29yMg=="] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "endCursor": "Y3Vyc29yMw==", @@ -71,27 +71,27 @@ extension Mocks.Hero.FriendsQuery { "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } - static func expectationForFirstPageWithErrors(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForFirstPageWithErrors(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "endCursor": "Y3Vyc29yMg==", @@ -116,19 +116,19 @@ extension Mocks.Hero.FriendsQuery { "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data, + "data": data as JSONValue, "errors": [ [ "message": "uh oh!" @@ -141,10 +141,10 @@ extension Mocks.Hero.FriendsQuery { } } - static func expectationForFirstPageErrorsOnly(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForFirstPageErrorsOnly(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return server.expect(query) { _ in + return await server.expect(query) { _ in return [ "errors": [ [ @@ -158,10 +158,10 @@ extension Mocks.Hero.FriendsQuery { } } - static func expectationForSecondPageErrorsOnly(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForSecondPageErrorsOnly(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 2, "after": "Y3Vyc29yMg=="] - return server.expect(query) { _ in + return await server.expect(query) { _ in return [ "errors": [ [ @@ -177,10 +177,10 @@ extension Mocks.Hero.FriendsQuery { } extension Mocks.Hero.ReverseFriendsQuery { - static func expectationForPreviousItem(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForPreviousItem(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 2, "before": "Y3Vyc29yMg=="] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "startCursor": "Y3Vyc29yZg==", @@ -200,26 +200,26 @@ extension Mocks.Hero.ReverseFriendsQuery { "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } - static func expectationForLastItem(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForLastItem(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 2, "before": "Y3Vyc29yMw=="] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "startCursor": "Y3Vyc29yMg==", @@ -244,29 +244,29 @@ extension Mocks.Hero.ReverseFriendsQuery { "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } } extension Mocks.Hero.BidirectionalFriendsQuery { - static func expectationForFirstFetchInMiddleOfList(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForFirstFetchInMiddleOfList(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 1, "before": GraphQLNullable.null, "after": "Y3Vyc29yMw=="] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "startCursor": "Y3Vyc29yMw==", @@ -281,34 +281,34 @@ extension Mocks.Hero.BidirectionalFriendsQuery { "id": "1003", ], ] - let friendsConnection: [String: AnyHashable] = [ + let friendsConnection = [ "__typename": "FriendsConnection", "totalCount": 3, "friends": friends, "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } - static func expectationForLastPage(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForLastPage(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 1, "after": "Y3Vyc29yMg==", "before": GraphQLNullable.null] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "startCursor": "Y3Vyc29yMg==", @@ -330,27 +330,27 @@ extension Mocks.Hero.BidirectionalFriendsQuery { "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } - static func expectationForPreviousPage(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForPreviousPage(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "first": 1, "before": "Y3Vyc29yMw==", "after": GraphQLNullable.null] - return server.expect(query) { _ in + return await server.expect(query) { _ in let pageInfo: [AnyHashable: AnyHashable] = [ "__typename": "PageInfo", "startCursor": "Y3Vyc29yMq==", @@ -372,29 +372,29 @@ extension Mocks.Hero.BidirectionalFriendsQuery { "pageInfo": pageInfo, ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friendsConnection": friendsConnection, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } } extension Mocks.Hero.OffsetFriendsQuery { - static func expectationForFirstPage(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForFirstPage(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "offset": 0, "limit": 2] - return server.expect(query) { _ in + return await server.expect(query) { _ in let friends: [[String: AnyHashable]] = [ [ "__typename": "Human", @@ -408,27 +408,27 @@ extension Mocks.Hero.OffsetFriendsQuery { ], ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friends": friends, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } - static func expectationForLastPage(server: MockGraphQLServer) -> XCTestExpectation { + static func expectationForLastPage(server: MockGraphQLServer) async -> XCTestExpectation { let query = MockQuery() query.__variables = ["id": "2001", "offset": 2, "limit": 2] - return server.expect(query) { _ in + return await server.expect(query) { _ in let friends: [[String: AnyHashable]] = [ [ "__typename": "Human", @@ -437,19 +437,19 @@ extension Mocks.Hero.OffsetFriendsQuery { ], ] - let hero: [String: AnyHashable] = [ + let hero = [ "__typename": "Droid", "id": "2001", "name": "R2-D2", "friends": friends, ] - let data: [String: AnyHashable] = [ + let data = [ "hero": hero ] return [ - "data": data + "data": data as JSONValue ] } } diff --git a/Tests/ApolloPaginationTests/GraphQLQueryPagerCoordinatorTests.swift b/Tests/ApolloPaginationTests/GraphQLQueryPagerCoordinatorTests.swift index 578ed981c..c58d35a88 100644 --- a/Tests/ApolloPaginationTests/GraphQLQueryPagerCoordinatorTests.swift +++ b/Tests/ApolloPaginationTests/GraphQLQueryPagerCoordinatorTests.swift @@ -1,54 +1,202 @@ import Apollo import ApolloAPI import ApolloInternalTestHelpers -import Combine +@preconcurrency import Combine import XCTest @testable import ApolloPagination -final class GraphQLQueryPagerCoordinatorTests: XCTestCase, CacheDependentTesting { +final class GraphQLQueryPagerCoordinatorTests: + XCTestCase, CacheDependentTesting, MockResponseProvider { + private typealias ReverseQuery = MockQuery private typealias ForwardQuery = MockQuery var cacheType: any TestCacheProvider.Type { InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! var cancellables: [AnyCancellable] = [] - @MainActor override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) - MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) + await MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) } - override func tearDownWithError() throws { - cache = nil + override func tearDown() async throws { + store = nil server = nil client = nil cancellables.forEach { $0.cancel() } cancellables = [] - try super.tearDownWithError() + try await super.tearDown() } - private func createForwardPager() -> AsyncGraphQLQueryPagerCoordinator { + func test_canLoadMore() async throws { + let pager = createForwardPager() + + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + + await pager.fetch() + await fulfillment(of: [serverExpectation]) + + var canLoadMore = await pager.canLoadNext + XCTAssertTrue(canLoadMore) + + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageFetch = expectation(description: "Second Page") + secondPageFetch.expectedFulfillmentCount = 2 + let subscription = await pager.subscribe(onUpdate: { _ in + secondPageFetch.fulfill() + }) + try await pager.loadNext() + await fulfillment(of: [secondPageExpectation, secondPageFetch]) + subscription.cancel() + canLoadMore = await pager.canLoadNext + XCTAssertFalse(canLoadMore) + } + + func test_canLoadPrevious() async throws { + let pager = createReversePager() + + let serverExpectation = await Mocks.Hero.ReverseFriendsQuery.expectationForLastItem(server: server) + + await pager.fetch() + await fulfillment(of: [serverExpectation]) + + var canLoadMore = await pager.canLoadPrevious + XCTAssertTrue(canLoadMore) + + let secondPageExpectation = await Mocks.Hero.ReverseFriendsQuery.expectationForPreviousItem(server: server) + let secondPageFetch = expectation(description: "Second Page") + secondPageFetch.expectedFulfillmentCount = 2 + let subscription = await pager.subscribe(onUpdate: { _ in + secondPageFetch.fulfill() + }) + try await pager.loadPrevious() + await fulfillment(of: [secondPageExpectation, secondPageFetch]) + subscription.cancel() + canLoadMore = await pager.canLoadPrevious + XCTAssertFalse(canLoadMore) + } + + @available(iOS 16.0, macOS 13.0, *) + func test_actor_canResetMidflight() async throws { + await server.setDelay(milliseconds: 150) + let pager = createForwardPager() + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + + await pager.subscribe(onUpdate: { _ in + XCTFail("We should never get results back") + }).store(in: &cancellables) + + Task { + try await pager.loadAll() + } + + Task { + try? await Task.sleep(for: .milliseconds(10)) + await pager.reset() + } + + await fulfillment(of: [serverExpectation], timeout: 1.0) + } + + @available(iOS 16.0, macOS 13.0, *) + func test__reset__loadingState() async throws { + await server.setDelay(milliseconds: 150) + let pager = createForwardPager() + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + + await pager.subscribe(onUpdate: { _ in + XCTFail("We should never get results back") + }).store(in: &cancellables) + + Task { + try await pager.loadAll() + } + + Task { + try? await Task.sleep(for: .milliseconds(10)) + await pager.reset() + } + + await fulfillment(of: [serverExpectation], timeout: 1.0) + async let isLoadingAll = pager.isLoadingAll + async let isFetching = pager.isFetching + let loadingStates = await [isFetching, isLoadingAll] + loadingStates.forEach { XCTAssertFalse($0) } + } + + @available(iOS 16.0, macOS 13.0, *) + func test__reset__midflight_isFetching_isFalse() async throws { + await server.setDelay(milliseconds: 1) + let pager = createForwardPager() + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + + await pager.fetch() + await fulfillment(of: [serverExpectation], timeout: 1.0) + + await server.setDelay(milliseconds: 3000) + Task { + try? await pager.loadNext() + } + let cancellationExpectation = expectation(description: "finished cancellation") + Task { + try? await Task.sleep(for: .milliseconds(50)) + await pager.reset() + cancellationExpectation.fulfill() + } + + await fulfillment(of: [cancellationExpectation]) + let isFetching = await pager.isFetching + XCTAssertFalse(isFetching) + } + + private func createReversePager() -> GraphQLQueryPagerCoordinator { + let initialQuery = ReverseQuery() + initialQuery.__variables = ["id": "2001", "first": 2, "before": "Y3Vyc29yMw=="] + return GraphQLQueryPagerCoordinator( + client: client, + initialQuery: initialQuery, + extractPageInfo: { data in + switch data { + case .initial(let data, _), .paginated(let data, _): + return CursorBasedPagination.Reverse( + hasPrevious: data.hero.friendsConnection.pageInfo.hasPreviousPage, + startCursor: data.hero.friendsConnection.pageInfo.startCursor + ) + } + }, + pageResolver: { pageInfo, direction in + guard direction == .previous else { return nil } + let nextQuery = ReverseQuery() + nextQuery.__variables = [ + "id": "2001", + "first": 2, + "before": pageInfo.startCursor ?? .null, + ] + return nextQuery + } + ) + } + + private func createForwardPager() -> GraphQLQueryPagerCoordinator { let initialQuery = ForwardQuery() initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return AsyncGraphQLQueryPagerCoordinator( + return GraphQLQueryPagerCoordinator( client: client, initialQuery: initialQuery, - watcherDispatchQueue: .main, extractPageInfo: { data in switch data { case .initial(let data, _), .paginated(let data, _): @@ -64,89 +212,10 @@ final class GraphQLQueryPagerCoordinatorTests: XCTestCase, CacheDependentTesting nextQuery.__variables = [ "id": "2001", "first": 2, - "after": pageInfo.endCursor, + "after": pageInfo.endCursor ?? .null, ] return nextQuery } ) } - - // MARK: - Reset Tests - - @available(iOS 16.0, macOS 13.0, *) - func test__reset__calls_callback() async throws { - server.customDelay = .milliseconds(1) - let pager = GraphQLQueryPagerCoordinator(pager: createForwardPager()) - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - - pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1) - server.customDelay = .milliseconds(200) - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let callbackExpectation = expectation(description: "Callback") - pager.loadNext(completion: { _ in - callbackExpectation.fulfill() - }) - try await Task.sleep(for: .milliseconds(50)) - pager.reset() - await fulfillment(of: [callbackExpectation, secondPageExpectation], timeout: 1) - } - - @available(iOS 16.0, macOS 13.0, *) - func test__reset__calls_callback_manyQueuedRequests() throws { - server.customDelay = .milliseconds(1) - let pager = GraphQLQueryPagerCoordinator(pager: createForwardPager()) - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - var results: [Result, any Error>] = [] - var errors: [PaginationError?] = [] - - pager.fetch() - wait(for: [serverExpectation], timeout: 1) - server.customDelay = .milliseconds(150) - pager.subscribe { result in - results.append(result) - } - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - pager.loadNext(completion: { error in - errors.append(error) - }) - pager.loadNext(completion: { error in - errors.append(error) - }) - DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(10)) { - pager.reset() - } - - wait(for: [secondPageExpectation], timeout: 2) - XCTAssertEqual(results.count, 1) // once for original fetch - XCTAssertEqual(errors.count, 2) - XCTAssertTrue(errors.contains(where: { PaginationError.isCancellation(error: $0) })) - } - - @available(iOS 16.0, macOS 13.0, *) - func test__reset__calls_callback_deinit() async throws { - server.customDelay = .milliseconds(1) - var pager: GraphQLQueryPagerCoordinator! = GraphQLQueryPagerCoordinator(pager: createForwardPager()) - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - var results: [Result, any Error>] = [] - var errors: [PaginationError?] = [] - - pager.fetch() - await fulfillment(of: [serverExpectation], timeout: 1) - server.customDelay = .milliseconds(150) - pager.subscribe { result in - results.append(result) - } - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - pager.loadNext(completion: { error in - errors.append(error) - }) - try await Task.sleep(for: .milliseconds(50)) - pager = nil - - await fulfillment(of: [secondPageExpectation], timeout: 2) - XCTAssertEqual(results.count, 1) // once for original fetch - XCTAssertEqual(errors.count, 1) - XCTAssertTrue(errors.contains(where: { PaginationError.isCancellation(error: $0) })) - } } diff --git a/Tests/ApolloPaginationTests/GraphQLQueryPagerTests.swift b/Tests/ApolloPaginationTests/GraphQLQueryPagerTests.swift index 075f026d2..d7440903c 100644 --- a/Tests/ApolloPaginationTests/GraphQLQueryPagerTests.swift +++ b/Tests/ApolloPaginationTests/GraphQLQueryPagerTests.swift @@ -1,130 +1,263 @@ import Apollo import ApolloAPI import ApolloInternalTestHelpers -import Combine import XCTest @testable import ApolloPagination final class GraphQLQueryPagerTests: XCTestCase { private typealias Query = MockQuery - private typealias ReverseQuery = MockQuery private var store: ApolloStore! private var server: MockGraphQLServer! private var networkTransport: MockNetworkTransport! private var client: ApolloClient! - private var subscriptions: Set = [] override func setUp() { super.setUp() store = ApolloStore(cache: InMemoryNormalizedCache()) server = MockGraphQLServer() - networkTransport = MockNetworkTransport(server: server, store: store) + networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) } - override func tearDown() { - super.tearDown() - subscriptions.removeAll() + func test_forwardInit_simple() async throws { + let initialQuery = Query() + initialQuery.__variables = [ + "id": "2001", + "first": 2, + "after": GraphQLNullable.null, + ] + let pager = GraphQLQueryPager( + client: client, + initialQuery: initialQuery, + extractPageInfo: { data in + CursorBasedPagination.Forward( + hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, + endCursor: data.hero.friendsConnection.pageInfo.endCursor + ) + }, + pageResolver: { page, direction in + switch direction { + case .next: + let nextQuery = Query() + nextQuery.__variables = [ + "id": "2001", + "first": 2, + "after": page.endCursor ?? .null, + ] + return nextQuery + case .previous: + return nil + } + } + ) + + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + await pager.fetch() + await fulfillment(of: [serverExpectation], timeout: 1) + + var canLoadMore = await pager.canLoadNext + XCTAssertTrue(canLoadMore) + + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageFetch = expectation(description: "Second Page") + secondPageFetch.expectedFulfillmentCount = 2 + let subscription = pager.sink { _ in + secondPageFetch.fulfill() + } + try await pager.loadNext() + await fulfillment(of: [secondPageExpectation, secondPageFetch]) + subscription.cancel() + canLoadMore = await pager.canLoadNext + XCTAssertFalse(canLoadMore) } - // MARK: - Test helpers + func test_forwardInit_simple_mapping() async throws { + struct ViewModel { + let name: String + } - private func createPager() -> GraphQLQueryPager> { let initialQuery = Query() - initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return .init(pager: GraphQLQueryPagerCoordinator( + initialQuery.__variables = [ + "id": "2001", + "first": 2, + "after": GraphQLNullable.null, + ] + let pager = GraphQLQueryPager( client: client, initialQuery: initialQuery, - watcherDispatchQueue: .main, extractPageInfo: { data in - switch data { - case .initial(let data, _), .paginated(let data, _): - return CursorBasedPagination.Forward( - hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, - endCursor: data.hero.friendsConnection.pageInfo.endCursor - ) - } + CursorBasedPagination.Forward( + hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, + endCursor: data.hero.friendsConnection.pageInfo.endCursor + ) }, - pageResolver: { pageInfo, direction in - guard direction == .next else { return nil } - let nextQuery = Query() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "after": pageInfo.endCursor, - ] - return nextQuery + pageResolver: { page, direction in + switch direction { + case .next: + let nextQuery = Query() + nextQuery.__variables = [ + "id": "2001", + "first": 2, + "after": page.endCursor ?? .null, + ] + return nextQuery + case .previous: + return nil + } } - )) + ) + + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + await pager.fetch() + await fulfillment(of: [serverExpectation], timeout: 1) + + var canLoadMore = await pager.canLoadNext + XCTAssertTrue(canLoadMore) + + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageFetch = expectation(description: "Second Page") + secondPageFetch.expectedFulfillmentCount = 2 + let subscription = pager + .compactMap { output -> [ViewModel]? in + guard case .success(let output) = output else { return nil } + let models = output.allData.flatMap { data in + data.hero.friendsConnection.friends.map { friend in ViewModel(name: friend.name) } + } + return models + } + .sink { _ in + secondPageFetch.fulfill() + } + + try await pager.loadNext() + await fulfillment(of: [secondPageExpectation, secondPageFetch]) + subscription.cancel() + canLoadMore = await pager.canLoadNext + XCTAssertFalse(canLoadMore) } - private func createReversePager() -> GraphQLQueryPager> { - let initialQuery = ReverseQuery() - initialQuery.__variables = ["id": "2001", "first": 2, "before": "Y3Vyc29yMw=="] - return .init(pager: GraphQLQueryPagerCoordinator( + func test_forwardInit_singleQuery_transform() async throws { + let initialQuery = Query() + initialQuery.__variables = [ + "id": "2001", + "first": 2, + "after": GraphQLNullable.null, + ] + let pager = GraphQLQueryPager( client: client, initialQuery: initialQuery, - watcherDispatchQueue: .main, extractPageInfo: { data in - switch data { - case .initial(let data, _), .paginated(let data, _): - return CursorBasedPagination.Reverse( - hasPrevious: data.hero.friendsConnection.pageInfo.hasPreviousPage, - startCursor: data.hero.friendsConnection.pageInfo.startCursor - ) - } + CursorBasedPagination.Forward( + hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, + endCursor: data.hero.friendsConnection.pageInfo.endCursor + ) }, - pageResolver: { pageInfo, direction in - guard direction == .previous else { return nil } - let nextQuery = ReverseQuery() - nextQuery.__variables = [ - "id": "2001", - "first": 2, - "before": pageInfo.startCursor, - ] - return nextQuery + pageResolver: { page, direction in + switch direction { + case .next: + let nextQuery = Query() + nextQuery.__variables = [ + "id": "2001", + "first": 2, + "after": page.endCursor ?? .null, + ] + return nextQuery + case .previous: + return nil + } } - )) - } + ) + + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + await pager.fetch() + await fulfillment(of: [serverExpectation], timeout: 1) + + var canLoadMore = await pager.canLoadNext + XCTAssertTrue(canLoadMore) - // This is due to a timing issue in unit tests only wherein we deinit immediately after waiting for expectations - private func ignoringCancellations(error: (any Error)?) { - if PaginationError.isCancellation(error: error as? PaginationError) { - return - } else { - XCTAssertNil(error) + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageFetch = expectation(description: "Second Page") + secondPageFetch.expectedFulfillmentCount = 2 + let subscription = pager.sink { _ in + secondPageFetch.fulfill() } + try await pager.loadNext() + await fulfillment(of: [secondPageExpectation, secondPageFetch]) + subscription.cancel() + canLoadMore = await pager.canLoadNext + XCTAssertFalse(canLoadMore) } - private func fetchFirstPage(pager: GraphQLQueryPager) { - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) - pager.fetch() - wait(for: [serverExpectation], timeout: 1.0) - } + func test_forwardInit_singleQuery_transform_mapping() async throws { + struct ViewModel { + let name: String + } - private func fetchSecondPage(pager: GraphQLQueryPager) throws { - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - pager.loadNext(completion: ignoringCancellations(error:)) - wait(for: [serverExpectation], timeout: 1.0) - } + let initialQuery = Query() + initialQuery.__variables = [ + "id": "2001", + "first": 2, + "after": GraphQLNullable.null, + ] + let pager = GraphQLQueryPager( + client: client, + initialQuery: initialQuery, + extractPageInfo: { data in + CursorBasedPagination.Forward( + hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, + endCursor: data.hero.friendsConnection.pageInfo.endCursor + ) + }, + pageResolver: { page, direction in + switch direction { + case .next: + let nextQuery = Query() + nextQuery.__variables = [ + "id": "2001", + "first": 2, + "after": page.endCursor ?? .null, + ] + return nextQuery + case .previous: + return nil + } + } + ) - private func reverseFetchLastPage(pager: GraphQLQueryPager) { - let serverExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForLastItem(server: server) - pager.fetch() - wait(for: [serverExpectation], timeout: 1.0) - } + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + await pager.fetch() + await fulfillment(of: [serverExpectation], timeout: 1) - private func reverseFetchPreviousPage(pager: GraphQLQueryPager) throws { - let serverExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForPreviousItem(server: server) - pager.loadPrevious(completion: ignoringCancellations(error:)) - wait(for: [serverExpectation], timeout: 1.0) - } + var canLoadMore = await pager.canLoadNext + XCTAssertTrue(canLoadMore) - // MARK: - Tests + let secondPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let secondPageFetch = expectation(description: "Second Page") + secondPageFetch.expectedFulfillmentCount = 2 + let subscription = pager + .compactMap { result in + switch result { + case .success(let output): + return output.allData.flatMap { data in + data.hero.friendsConnection.friends.map { friend in friend.name } + }.map(ViewModel.init(name:)) + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + return nil + } + }.sink { _ in + secondPageFetch.fulfill() + } + try await pager.loadNext() + await fulfillment(of: [secondPageExpectation, secondPageFetch]) + subscription.cancel() + canLoadMore = await pager.canLoadNext + XCTAssertFalse(canLoadMore) + } - func test_concatenatesPages_matchingInitialAndPaginated() throws { + func test_concatenatesPages_matchingInitialAndPaginated() async throws { struct ViewModel { let name: String } @@ -136,72 +269,55 @@ final class GraphQLQueryPagerTests: XCTestCase { let subscriptionExpectation = expectation(description: "Subscription") subscriptionExpectation.expectedFulfillmentCount = 2 var expectedViewModels: [ViewModel]? - let subscription = anyPager.compactMap { result in - switch result { - case .success(let data): - return data.allData.flatMap { data in - data.hero.friendsConnection.friends.map { - ViewModel(name: $0.name) + let subscriber = anyPager + .compactMap { result in + switch result { + case .success(let data): + return data.allData.flatMap { data in + data.hero.friendsConnection.friends.map { + ViewModel(name: $0.name) + } } + case .failure(let error): + XCTFail(error.localizedDescription) + return nil } - case .failure(let error): - XCTFail(error.localizedDescription) - return nil + }.sink { viewModels in + expectedViewModels = viewModels + fetchExpectation.fulfill() + subscriptionExpectation.fulfill() } - }.sink { viewModels in - expectedViewModels = viewModels - fetchExpectation.fulfill() - subscriptionExpectation.fulfill() - } - fetchFirstPage(pager: anyPager) - wait(for: [fetchExpectation], timeout: 1) - try fetchSecondPage(pager: anyPager) + await fetchFirstPage(pager: anyPager) + await fulfillment(of: [fetchExpectation], timeout: 1) + try await fetchSecondPage(pager: anyPager) - wait(for: [subscriptionExpectation], timeout: 1.0) + await fulfillment(of: [subscriptionExpectation], timeout: 1) let results = try XCTUnwrap(expectedViewModels) XCTAssertEqual(results.count, 3) XCTAssertEqual(results.map(\.name), ["Luke Skywalker", "Han Solo", "Leia Organa"]) - subscription.cancel() - } - - func test_transformless_init() throws { - let pager = createPager() - let fetchExpectation = expectation(description: "Initial Fetch") - var expectedViewModels: [PaginationOutput] = [] - pager.sink { result in - switch result { - case .success(let value): - expectedViewModels.append(value) - fetchExpectation.fulfill() - default: - XCTFail("Failed to get view models from pager") - } - }.store(in: &subscriptions) - - fetchFirstPage(pager: pager) - wait(for: [fetchExpectation], timeout: 1) - XCTAssertFalse(expectedViewModels.isEmpty) - XCTAssertEqual(expectedViewModels.count, 1) + subscriber.cancel() } - func test_passesBackSeparateData() throws { + func test_passesBackSeparateData() async throws { let anyPager = createPager() let initialExpectation = expectation(description: "Initial") let secondExpectation = expectation(description: "Second") var expectedViewModel: String? - anyPager - .map { result in + let subscriber = anyPager + .compactMap { result in switch result { case .success(let output): - return output.allData.last.flatMap(\.hero.friendsConnection.friends.last?.name) + if let latestPage = output.nextPages.last { + return latestPage.data?.hero.friendsConnection.friends.last?.name + } + return output.initialPage?.data?.hero.friendsConnection.friends.last?.name case .failure(let error): XCTFail(error.localizedDescription) return nil } } - .receive(on: RunLoop.main) .sink { viewModel in let oldValue = expectedViewModel expectedViewModel = viewModel @@ -210,83 +326,155 @@ final class GraphQLQueryPagerTests: XCTestCase { } else { secondExpectation.fulfill() } - }.store(in: &subscriptions) + } - fetchFirstPage(pager: anyPager) - wait(for: [initialExpectation], timeout: 1.0) + await fetchFirstPage(pager: anyPager) + await fulfillment(of: [initialExpectation], timeout: 1) XCTAssertEqual(expectedViewModel, "Han Solo") - XCTAssertTrue(anyPager.canLoadNext) - XCTAssertFalse(anyPager.canLoadPrevious) - try fetchSecondPage(pager: anyPager) - wait(for: [secondExpectation], timeout: 1.0) + try await fetchSecondPage(pager: anyPager) + await fulfillment(of: [secondExpectation], timeout: 1) XCTAssertEqual(expectedViewModel, "Leia Organa") - XCTAssertFalse(anyPager.canLoadNext) - XCTAssertFalse(anyPager.canLoadPrevious) + subscriber.cancel() } - func test_reversePager_loadPrevious() throws { - let anyPager = createReversePager() + func test_loadAll() async throws { + let pager = createPager() - let initialExpectation = expectation(description: "Initial") - let secondExpectation = expectation(description: "Second") - var expectedViewModel: String? - let subscriber = anyPager - .compactMap { result in - switch result { - case .success(let output): - if let latestPage = output.previousPages.last { - return latestPage.data?.hero.friendsConnection.friends.first?.name - } - return output.initialPage?.data?.hero.friendsConnection.friends.first?.name - case .failure(let error): - XCTFail(error.localizedDescription) - return nil - } - } - .receive(on: RunLoop.main) - .sink { viewModel in - let oldValue = expectedViewModel - expectedViewModel = viewModel - if oldValue == nil { - initialExpectation.fulfill() - } else { - secondExpectation.fulfill() - } + let firstPageExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let lastPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + let loadAllExpectation = expectation(description: "Load all pages") + let subscriber = pager.sink { _ in + loadAllExpectation.fulfill() + } + try await pager.loadAll() + await fulfillment(of: [firstPageExpectation, lastPageExpectation, loadAllExpectation], timeout: 5) + subscriber.cancel() + } + + func test_errors_partialSuccess() async throws { + let pager = createPager() + var expectedResults: [Result, any Error>] = [] + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPageWithErrors(server: server) + let fetchExpectation = expectation(description: "Fetch") + let subscription = pager.sink { output in + expectedResults.append(output) + fetchExpectation.fulfill() + } + await pager.fetch() + await fulfillment(of: [serverExpectation, fetchExpectation], timeout: 3) + XCTAssertEqual(expectedResults.count, 1) + let result = try XCTUnwrap(expectedResults.first) + let successValue = try result.get() + XCTAssertFalse(successValue.allErrors.isEmpty) + XCTAssertEqual(successValue.initialPage?.data?.hero.name, "R2-D2") + let canLoadNext = await pager.canLoadNext + XCTAssertTrue(canLoadNext) + subscription.cancel() + } + + func test_errors_noData() async throws { + let pager = createPager() + var expectedResults: [Result, any Error>] = [] + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPageErrorsOnly(server: server) + let fetchExpectation = expectation(description: "Fetch") + let subscription = pager.sink { output in + expectedResults.append(output) + fetchExpectation.fulfill() + } + await pager.fetch() + await fulfillment(of: [serverExpectation, fetchExpectation], timeout: 3) + XCTAssertEqual(expectedResults.count, 1) + let result = try XCTUnwrap(expectedResults.first) + let successValue = try result.get() + XCTAssertFalse(successValue.allErrors.isEmpty) + XCTAssertNil(successValue.initialPage?.data) + subscription.cancel() + } + + func test_errors_noData_loadAll() async throws { + let pager = createPager() + var expectedResults: [Result, any Error>] = [] + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPageErrorsOnly(server: server) + let fetchExpectation = expectation(description: "Fetch") + let subscription = pager.sink { output in + expectedResults.append(output) + XCTAssertEqual(expectedResults.count, 1) + do { + let result = try XCTUnwrap(expectedResults.first) + let successValue = try result.get() + XCTAssertFalse(successValue.allErrors.isEmpty) + XCTAssertNil(successValue.initialPage?.data) + } catch { + XCTFail(error.localizedDescription) } + fetchExpectation.fulfill() + } + try await pager.loadAll(fetchFromInitialPage: true) + await fulfillment(of: [serverExpectation, fetchExpectation], timeout: 3) + subscription.cancel() + } - reverseFetchLastPage(pager: anyPager) - wait(for: [initialExpectation], timeout: 1.0) - XCTAssertEqual(expectedViewModel, "Han Solo") - XCTAssertFalse(anyPager.canLoadNext) - XCTAssertTrue(anyPager.canLoadPrevious) - - try reverseFetchPreviousPage(pager: anyPager) - wait(for: [secondExpectation], timeout: 1.0) - XCTAssertEqual(expectedViewModel, "Luke Skywalker") - XCTAssertFalse(anyPager.canLoadNext) - XCTAssertFalse(anyPager.canLoadPrevious) + func test_errors_noDataOnSecondPage_loadAll() async throws { + let pager = createPager() + var expectedResults: [Result, any Error>] = [] + + let firstPageExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let lastPageExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPageErrorsOnly(server: server) + let loadAllExpectation = expectation(description: "Load all pages") + let subscriber = pager.sink { output in + expectedResults.append(output) + loadAllExpectation.fulfill() + } + try await pager.loadAll() + await fulfillment(of: [firstPageExpectation, lastPageExpectation, loadAllExpectation], timeout: 5) + let result = try XCTUnwrap(expectedResults.first) + let successValue = try result.get() + XCTAssertFalse(successValue.allErrors.isEmpty) + XCTAssertNotNil(successValue.initialPage) + XCTAssertNil(successValue.nextPages[0].data) subscriber.cancel() } - // MARK: - Reset Tests + // MARK: - Test helpers - @available(iOS 16.0, macOS 13.0, *) - func test_pager_reset_calls_callback() async throws { - server.customDelay = .milliseconds(1) - let pager = createPager() - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + private func createPager() -> GraphQLQueryPager> { + let initialQuery = Query() + initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] + return .init(pager: GraphQLQueryPagerCoordinator( + client: client, + initialQuery: initialQuery, + extractPageInfo: { data in + switch data { + case .initial(let data, _), .paginated(let data, _): + return CursorBasedPagination.Forward( + hasNext: data.hero.friendsConnection.pageInfo.hasNextPage, + endCursor: data.hero.friendsConnection.pageInfo.endCursor + ) + } + }, + pageResolver: { pageInfo, direction in + guard direction == .next else { return nil } + let nextQuery = Query() + nextQuery.__variables = [ + "id": "2001", + "first": 2, + "after": pageInfo.endCursor ?? .null, + ] + return nextQuery + } + )) + } + + private func fetchFirstPage(pager: GraphQLQueryPager) async { + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + await pager.fetch() + await fulfillment(of: [serverExpectation], timeout: 1) + } - pager.fetch() + private func fetchSecondPage(pager: GraphQLQueryPager) async throws { + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) + try await pager.loadNext() await fulfillment(of: [serverExpectation], timeout: 1) - server.customDelay = .milliseconds(200) - let secondPageExpectation = Mocks.Hero.FriendsQuery.expectationForSecondPage(server: server) - let callbackExpectation = expectation(description: "Callback") - pager.loadNext(completion: { _ in - callbackExpectation.fulfill() - }) - try await Task.sleep(for: .milliseconds(50)) - pager.reset() - await fulfillment(of: [callbackExpectation, secondPageExpectation], timeout: 1) } } diff --git a/Tests/ApolloPaginationTests/Mocks.swift b/Tests/ApolloPaginationTests/Mocks.swift index c86056f0b..f3582d3c1 100644 --- a/Tests/ApolloPaginationTests/Mocks.swift +++ b/Tests/ApolloPaginationTests/Mocks.swift @@ -5,14 +5,14 @@ import XCTest enum Mocks { enum Hero { - class BidirectionalFriendsQuery: MockSelectionSet { + class BidirectionalFriendsQuery: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] { [ .field("hero", Hero?.self, arguments: ["id": .variable("id")]) ]} var hero: Hero { __data["hero"] } - class Hero: MockSelectionSet { + class Hero: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("id", String.self), @@ -28,7 +28,7 @@ enum Mocks { var id: String { __data["id"] } var friendsConnection: FriendsConnection { __data["friendsConnection"] } - class FriendsConnection: MockSelectionSet { + class FriendsConnection: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("totalCount", Int.self), @@ -40,7 +40,7 @@ enum Mocks { var friends: [Character] { __data["friends"] } var pageInfo: PageInfo { __data["pageInfo"] } - class Character: MockSelectionSet { + class Character: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("name", String.self), @@ -51,7 +51,7 @@ enum Mocks { var id: String { __data["id"] } } - class PageInfo: MockSelectionSet { + class PageInfo: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("startCursor", Optional.self), @@ -68,14 +68,14 @@ enum Mocks { } } } - class ReverseFriendsQuery: MockSelectionSet { + class ReverseFriendsQuery: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] { [ .field("hero", Hero?.self, arguments: ["id": .variable("id")]) ]} var hero: Hero { __data["hero"] } - class Hero: MockSelectionSet { + class Hero: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("id", String.self), @@ -90,7 +90,7 @@ enum Mocks { var id: String { __data["id"] } var friendsConnection: FriendsConnection { __data["friendsConnection"] } - class FriendsConnection: MockSelectionSet { + class FriendsConnection: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("totalCount", Int.self), @@ -102,7 +102,7 @@ enum Mocks { var friends: [Character] { __data["friends"] } var pageInfo: PageInfo { __data["pageInfo"] } - class Character: MockSelectionSet { + class Character: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("name", String.self), @@ -113,7 +113,7 @@ enum Mocks { var id: String { __data["id"] } } - class PageInfo: MockSelectionSet { + class PageInfo: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("startCursor", Optional.self), @@ -126,14 +126,14 @@ enum Mocks { } } } - class FriendsQuery: MockSelectionSet { + class FriendsQuery: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] { [ .field("hero", Hero?.self, arguments: ["id": .variable("id")]) ]} var hero: Hero { __data["hero"] } - class Hero: MockSelectionSet { + class Hero: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("id", String.self), @@ -148,7 +148,7 @@ enum Mocks { var id: String { __data["id"] } var friendsConnection: FriendsConnection { __data["friendsConnection"] } - class FriendsConnection: MockSelectionSet { + class FriendsConnection: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("totalCount", Int.self), @@ -160,7 +160,7 @@ enum Mocks { var friends: [Character] { __data["friends"] } var pageInfo: PageInfo { __data["pageInfo"] } - class Character: MockSelectionSet { + class Character: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("name", String.self), @@ -171,7 +171,7 @@ enum Mocks { var id: String { __data["id"] } } - class PageInfo: MockSelectionSet { + class PageInfo: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("endCursor", Optional.self), @@ -185,14 +185,14 @@ enum Mocks { } } - class OffsetFriendsQuery: MockSelectionSet { + class OffsetFriendsQuery: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] { [ .field("hero", Hero?.self, arguments: ["id": .variable("id")]) ]} var hero: Hero { __data["hero"] } - class Hero: MockSelectionSet { + class Hero: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("id", String.self), @@ -207,7 +207,7 @@ enum Mocks { var id: String { __data["id"] } var friends: [Character] { __data["friends"] } - class Character: MockSelectionSet { + class Character: MockSelectionSet, @unchecked Sendable { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("name", String.self), diff --git a/Tests/ApolloPaginationTests/OffsetTests.swift b/Tests/ApolloPaginationTests/OffsetTests.swift index de069068b..0de8ad906 100644 --- a/Tests/ApolloPaginationTests/OffsetTests.swift +++ b/Tests/ApolloPaginationTests/OffsetTests.swift @@ -19,18 +19,17 @@ final class OffsetTests: XCTestCase { super.setUp() store = ApolloStore(cache: InMemoryNormalizedCache()) server = MockGraphQLServer() - networkTransport = MockNetworkTransport(server: server, store: store) + networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) } - private func createPager() async -> AsyncGraphQLQueryPager> { + private func createPager() async -> GraphQLQueryPager> { let pageSize = 2 let initialQuery = Query() initialQuery.__variables = ["id": "2001", "offset": 0, "limit": pageSize] - let pager = AsyncGraphQLQueryPagerCoordinator( + let pager = GraphQLQueryPagerCoordinator( client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, + initialQuery: initialQuery, extractPageInfo: { data in switch data { case .initial(let data, let output), .paginated(let data, let output): @@ -57,7 +56,7 @@ final class OffsetTests: XCTestCase { return nextQuery } ) - return AsyncGraphQLQueryPager(pager: pager) + return GraphQLQueryPager(pager: pager) } // This is due to a timing issue in unit tests only wherein we deinit immediately after waiting for expectations @@ -69,14 +68,14 @@ final class OffsetTests: XCTestCase { } } - private func fetchFirstPage(pager: AsyncGraphQLQueryPager) async { - let serverExpectation = Mocks.Hero.OffsetFriendsQuery.expectationForFirstPage(server: server) + private func fetchFirstPage(pager: GraphQLQueryPager) async { + let serverExpectation = await Mocks.Hero.OffsetFriendsQuery.expectationForFirstPage(server: server) await pager.fetch() await fulfillment(of: [serverExpectation], timeout: 1.0) } - private func fetchSecondPage(pager: AsyncGraphQLQueryPager) async throws { - let serverExpectation = Mocks.Hero.OffsetFriendsQuery.expectationForLastPage(server: server) + private func fetchSecondPage(pager: GraphQLQueryPager) async throws { + let serverExpectation = await Mocks.Hero.OffsetFriendsQuery.expectationForLastPage(server: server) try await pager.loadNext() await fulfillment(of: [serverExpectation], timeout: 1.0) } diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index b6db7b2f6..d7ba0a52a 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -1,7 +1,7 @@ import Apollo import ApolloAPI import ApolloInternalTestHelpers -import Combine +@preconcurrency import Combine import XCTest @testable import ApolloPagination @@ -14,27 +14,25 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! var cancellables: [AnyCancellable] = [] - @MainActor override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) - MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) + await MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) } override func tearDownWithError() throws { - cache = nil + store = nil server = nil client = nil cancellables.forEach { $0.cancel() } @@ -46,7 +44,7 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { func test_fetchMultiplePages() async throws { let pager = createPager() - let serverExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForLastItem(server: server) + let serverExpectation = await Mocks.Hero.ReverseFriendsQuery.expectationForLastItem(server: server) var results: [Result, any Error>] = [] let firstPageExpectation = expectation(description: "First page") @@ -65,7 +63,7 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { XCTAssertEqual(output.initialPage?.source, .server) } - let secondPageExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForPreviousItem(server: server) + let secondPageExpectation = await Mocks.Hero.ReverseFriendsQuery.expectationForPreviousItem(server: server) let secondPageFetch = expectation(description: "Second Page") secondPageFetch.expectedFulfillmentCount = 2 subscription = await pager.subscribe(onUpdate: { _ in @@ -100,8 +98,8 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { func test_loadAll() async throws { let pager = createPager() - let firstPageExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForLastItem(server: server) - let lastPageExpectation = Mocks.Hero.ReverseFriendsQuery.expectationForPreviousItem(server: server) + let firstPageExpectation = await Mocks.Hero.ReverseFriendsQuery.expectationForLastItem(server: server) + let lastPageExpectation = await Mocks.Hero.ReverseFriendsQuery.expectationForPreviousItem(server: server) let loadAllExpectation = expectation(description: "Load all pages") await pager.subscribe(onUpdate: { _ in loadAllExpectation.fulfill() @@ -110,13 +108,12 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { await fulfillment(of: [firstPageExpectation, lastPageExpectation, loadAllExpectation], timeout: 5) } - private func createPager() -> AsyncGraphQLQueryPagerCoordinator { + private func createPager() -> GraphQLQueryPagerCoordinator { let initialQuery = Query() initialQuery.__variables = ["id": "2001", "first": 2, "before": "Y3Vyc29yMw=="] - return AsyncGraphQLQueryPagerCoordinator( + return GraphQLQueryPagerCoordinator( client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, + initialQuery: initialQuery, extractPageInfo: { data in switch data { case .initial(let data, _), .paginated(let data, _): @@ -132,7 +129,7 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { nextQuery.__variables = [ "id": "2001", "first": 2, - "before": pageInfo.startCursor, + "before": pageInfo.startCursor ?? .null, ] return nextQuery } diff --git a/Tests/ApolloPaginationTests/SubscribeTests.swift b/Tests/ApolloPaginationTests/SubscribeTests.swift index 01b3bf461..a844028f8 100644 --- a/Tests/ApolloPaginationTests/SubscribeTests.swift +++ b/Tests/ApolloPaginationTests/SubscribeTests.swift @@ -1,7 +1,7 @@ import Apollo import ApolloAPI import ApolloInternalTestHelpers -import Combine +@preconcurrency import Combine import XCTest @testable import ApolloPagination @@ -11,27 +11,25 @@ final class SubscribeTest: XCTestCase, CacheDependentTesting { InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! var cancellables: [AnyCancellable] = [] - @MainActor override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() - let networkTransport = MockNetworkTransport(server: server, store: store) + let networkTransport = MockNetworkTransport(mockServer: server, store: store) client = ApolloClient(networkTransport: networkTransport, store: store) - MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) + await MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) } override func tearDownWithError() throws { - cache = nil + store = nil server = nil client = nil @@ -57,7 +55,7 @@ final class SubscribeTest: XCTestCase, CacheDependentTesting { otherResults.append(result) }.store(in: &cancellables) - let serverExpectation = Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) + let serverExpectation = await Mocks.Hero.FriendsQuery.expectationForFirstPage(server: server) await pager.fetch() await fulfillment(of: [serverExpectation, initialFetchExpectation], timeout: 1.0) @@ -72,13 +70,12 @@ final class SubscribeTest: XCTestCase, CacheDependentTesting { } } - private func createPager() -> AsyncGraphQLQueryPagerCoordinator { + private func createPager() -> GraphQLQueryPagerCoordinator { let initialQuery = Query() initialQuery.__variables = ["id": "2001", "first": 2, "after": GraphQLNullable.null] - return AsyncGraphQLQueryPagerCoordinator( + return GraphQLQueryPagerCoordinator( client: client, - initialQuery: initialQuery, - watcherDispatchQueue: .main, + initialQuery: initialQuery, extractPageInfo: { data in switch data { case .initial(let data, _), .paginated(let data, _): @@ -94,7 +91,7 @@ final class SubscribeTest: XCTestCase, CacheDependentTesting { nextQuery.__variables = [ "id": "2001", "first": 2, - "after": pageInfo.endCursor, + "after": pageInfo.endCursor ?? .null, ] return nextQuery } diff --git a/Tests/ApolloTests/ApolloClientOperationTests.swift b/Tests/ApolloTests/ApolloClientOperationTests.swift index a4c41b5aa..7fabc11b1 100644 --- a/Tests/ApolloTests/ApolloClientOperationTests.swift +++ b/Tests/ApolloTests/ApolloClientOperationTests.swift @@ -59,7 +59,7 @@ final class ApolloClientOperationTests: XCTestCase { } } - let jsonObject: JSONObject = [ + static let jsonObject: JSONObject = [ "data": [ "createReview": [ "__typename": "Review", @@ -72,8 +72,8 @@ final class ApolloClientOperationTests: XCTestCase { func test__performMutation_givenPublishResultToStore_true_publishResultsToStore() async throws { let mutation = MockMutation() - let serverRequestExpectation = await server.expect(MockMutation.self) { _ in - self.jsonObject + let serverRequestExpectation = await server.expect(MockMutation.self) { @Sendable _ in + Self.jsonObject } // when @@ -115,8 +115,8 @@ final class ApolloClientOperationTests: XCTestCase { func test__performMutation_givenPublishResultToStore_false_doesNotPublishResultsToStore() async throws { let mutation = MockMutation() - let serverRequestExpectation = await server.expect(MockMutation.self) { _ in - self.jsonObject + let serverRequestExpectation = await server.expect(MockMutation.self) { @Sendable _ in + Self.jsonObject } // when diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index 8e329c1df..5ec7f8f6a 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -5,6 +5,7 @@ import XCTest @testable import Apollo @testable import Nimble +#warning("TODO: Test if cache returns result, then server returns failed result, APQ retry still occurs") class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { private static let endpoint = TestURL.mockServer.url @@ -83,7 +84,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { } } - private func mockResponseData() -> Data { + private static func mockResponseData() -> Data { """ { "data": { @@ -299,7 +300,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -333,7 +334,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -367,7 +368,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -402,7 +403,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -436,7 +437,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -469,7 +470,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -505,7 +506,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -541,7 +542,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -577,7 +578,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -613,7 +614,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -649,7 +650,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: Self.endpoint) { lastRequest = $0 - return (HTTPURLResponse.mock(), self.mockResponseData()) + return (HTTPURLResponse.mock(), Self.mockResponseData()) } _ = try await network.send( @@ -704,7 +705,7 @@ class AutomaticPersistedQueriesTests: XCTestCase, MockResponseProvider { ) return (response, data) } else { - return (response, self.mockResponseData()) + return (response, Self.mockResponseData()) } } diff --git a/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift b/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift index 0d5bd996f..2d2500620 100644 --- a/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift +++ b/Tests/ApolloTests/Cache/DeferOperationCacheReadTests.swift @@ -102,15 +102,14 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() let networkTransport = MockNetworkTransport(mockServer: server, store: store) @@ -121,13 +120,13 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { override func tearDownWithError() throws { try super.tearDownWithError() - cache = nil + store = nil server = nil client = nil } func test__fetch__givenPartialAndIncrementalDataIsCached_returnsAllDeferredFragmentsAsFulfilled() async throws { - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "animal": CacheReference("QUERY_ROOT.animal") ], @@ -156,7 +155,7 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { } func test__fetch__givenOnlyPartialDataIsCached_returnsAllDeferredFragmentsAsPending() async throws { - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "animal": CacheReference("QUERY_ROOT.animal") ], @@ -183,7 +182,7 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { test__fetch__givenPartialAndSomeIncrementalDataIsCached_returnsCachedDeferredFragmentAsFulfilledAndUncachedDeferredFragmentsAsPending() async throws { - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "animal": CacheReference("QUERY_ROOT.animal") ], @@ -210,7 +209,7 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { test__fetch__givenNestedIncrementalDataIsNotCached_returnsNestedDeferredFragmentsAsPending_otherDeferredFragmentsAsFulfilled() async throws { - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "animal": CacheReference("QUERY_ROOT.animal") ], @@ -243,7 +242,7 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { test__fetch__givenMissingValueInDeferredFragment_returnsDeferredFragmentAsPending_otherDeferredFragmentsAsFulfilled() async throws { - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "animal": CacheReference("QUERY_ROOT.animal") ], @@ -272,7 +271,7 @@ class DeferOperationCacheReadTests: XCTestCase, CacheDependentTesting { } func test__fetch__givenMissingValueInPartialData_shouldReturnNil() async throws { - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "animal": CacheReference("QUERY_ROOT.animal") ], diff --git a/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift b/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift index fe8d6c277..256f319fc 100644 --- a/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift +++ b/Tests/ApolloTests/Cache/DeferOperationCacheWriteTests.swift @@ -86,20 +86,17 @@ class DeferOperationCacheWriteTests: XCTestCase, CacheDependentTesting, StoreLoa InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! var store: ApolloStore! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - store = ApolloStore(cache: cache) + store = try await makeTestStore() } override func tearDownWithError() throws { try super.tearDownWithError() - - cache = nil + store = nil } @@ -307,7 +304,7 @@ class DeferOperationCacheWriteTests: XCTestCase, CacheDependentTesting, StoreLoa let friendSelectionSet = DeferredFriendName(_dataDict: friendData) - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "animal": CacheReference("QUERY_ROOT.animal"), ], diff --git a/Tests/ApolloTests/Cache/FetchQueryTests.swift b/Tests/ApolloTests/Cache/FetchQueryTests.swift index 5f2c20241..cbec8b0dd 100644 --- a/Tests/ApolloTests/Cache/FetchQueryTests.swift +++ b/Tests/ApolloTests/Cache/FetchQueryTests.swift @@ -13,15 +13,14 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { static let defaultWaitTimeout: TimeInterval = 1 - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() let networkTransport = MockNetworkTransport(mockServer: server, store: store) @@ -30,7 +29,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { } override func tearDownWithError() throws { - cache = nil + store = nil server = nil client = nil @@ -57,7 +56,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -108,7 +107,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -172,7 +171,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": [ "name": "R2-D2", @@ -210,7 +209,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -266,7 +265,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -304,7 +303,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -338,7 +337,7 @@ class FetchQueryTests: XCTestCase, CacheDependentTesting { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", diff --git a/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift b/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift index 18c217c7b..57d7a532e 100644 --- a/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift +++ b/Tests/ApolloTests/Cache/LoadQueryFromStoreTests.swift @@ -16,18 +16,15 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, static let defaultWaitTimeout: TimeInterval = 5.0 - var cache: (any NormalizedCache)! var store: ApolloStore! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - store = ApolloStore(cache: cache) + store = try await makeTestStore() } override func tearDown() async throws { - cache = nil store = nil await Self.cleanUpRequestHandlers() @@ -53,7 +50,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": ["__typename": "Droid", "name": "R2-D2"], ]) @@ -87,7 +84,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero(episode:JEDI)": CacheReference("hero(episode:JEDI)")], "hero(episode:JEDI)": ["__typename": "Droid", "name": "R2-D2"], ]) @@ -122,7 +119,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": ["__typename": "Droid"], ]) @@ -155,7 +152,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": ["__typename": "Droid", "name": NSNull()], ]) @@ -207,7 +204,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -266,7 +263,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("2001")], "2001": [ "name": "R2-D2", @@ -326,7 +323,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -378,7 +375,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": [ "name": "R2-D2", @@ -439,7 +436,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": ["__typename": "Droid", "name": "R2-D2"], ]) @@ -483,7 +480,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("2001")], "2001": [ "name": "R2-D2", @@ -542,7 +539,7 @@ class LoadQueryFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading, } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["starshipCoordinates": CacheReference("starshipCoordinates")], "starshipCoordinates": [ "__typename": "Starship", diff --git a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift index 721d73526..b66a41f87 100644 --- a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift +++ b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift @@ -12,18 +12,15 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { static let defaultWaitTimeout: TimeInterval = 5.0 - var cache: (any NormalizedCache)! var store: ApolloStore! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - store = ApolloStore(cache: cache) + store = try await makeTestStore() } override func tearDownWithError() throws { - cache = nil store = nil try super.tearDownWithError() @@ -47,7 +44,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -70,7 +67,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["test": "data"], ]) @@ -106,7 +103,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let query = MockQuery() query.__variables = ["episode": Episode.JEDI] - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero(episode:JEDI)": CacheReference("hero(episode:JEDI)")], "hero(episode:JEDI)": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -145,7 +142,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let query = MockQuery() query.__variables = ["episode": Episode.PHANTOM_MENACE] - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero(episode:JEDI)": CacheReference("hero(episode:JEDI)")], "hero(episode:JEDI)": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -190,7 +187,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("2001")], "2001": [ "name": "R2-D2", @@ -252,7 +249,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "2001": ["name": "R2-D2", "__typename": "Droid", "primaryFunction": "Protocol"] ]) @@ -303,13 +300,13 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "2001": ["name": "R2-D2", "__typename": "Droid"] ]) // when - _ = try await store.withinReadTransaction { transaction in - await expect { + try await store.withinReadTransaction { transaction in + _ = await expect { _ = try await transaction.readObject( ofType: GivenSelectionSet.self, withKey: "2001" @@ -354,7 +351,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let cacheMutation = MockLocalCacheMutation() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -418,7 +415,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let cacheMutation = MockLocalCacheMutation() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "nickname": NSNull()] ]) @@ -430,8 +427,9 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - let record = try! await self.cache.loadRecords(forKeys: ["QUERY_ROOT.hero"]).first?.value - expect(record?["nickname"]).to(equal(NSNull())) + let record = try await self.store.loadRecord(forKey: "QUERY_ROOT.hero") + + expect(record["nickname"]).to(equal(NSNull())) let query = MockQuery() @@ -482,7 +480,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let cacheMutation = MockLocalCacheMutation() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "nickname": NSNull()] ]) @@ -540,7 +538,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let cacheMutation = MockLocalCacheMutation() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -597,7 +595,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let cacheMutation = MockLocalCacheMutation() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -646,7 +644,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let cacheMutation = MockLocalCacheMutationFromMutation() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "MUTATION_ROOT": ["hero": CacheReference("MUTATION_ROOT.hero")], "MUTATION_ROOT.hero": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -704,7 +702,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "hero(episode:JEDI)": CacheReference("hero(episode:JEDI)"), "hero(episode:PHANTOM_MENACE)": CacheReference("hero(episode:PHANTOM_MENACE)") @@ -811,7 +809,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "primaryFunction": "Protocol"] ]) @@ -930,7 +928,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "primaryFunction": "Protocol"] ]) @@ -1072,7 +1070,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2", "primaryFunction": "Protocol"] ]) @@ -1157,7 +1155,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("2001")], "2001": [ "name": "R2-D2", @@ -1238,7 +1236,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let cacheMutation = MockLocalCacheMutation() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "type": "droid"] ]) @@ -1749,10 +1747,9 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { // then let heroKey = "QUERY_ROOT.hero" - let records = try await self.cache.loadRecords(forKeys: [heroKey]) - let heroRecord = records[heroKey] + let heroRecord = try await self.store.loadRecord(forKey: heroKey) - expect(heroRecord?.fields["name"] as? String).to(equal("Han Solo")) + expect(heroRecord.fields["name"] as? String).to(equal("Han Solo")) } @MainActor func test_writeDataForOperation_givenSelectionSetManuallyInitializedWithNamedFragmentInInclusionConditionIsFulfilled_writesFieldsForNamedFragment() async throws { @@ -2023,7 +2020,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("QUERY_ROOT.hero")], "QUERY_ROOT.hero": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -2086,7 +2083,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("2001")], "2001": [ "name": "R2-D2", @@ -2209,7 +2206,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("2001")], "2001": [ "name": "R2-D2", @@ -2267,7 +2264,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { // 1. Merge all required records into the cache with lowercase key // - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["\(heroKey.lowercased())": CacheReference("QUERY_ROOT.\(heroKey.lowercased())")], "QUERY_ROOT.\(heroKey.lowercased())": ["__typename": "Droid", "name": "R2-D2"] ]) @@ -2332,7 +2329,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { // // 1. Merge all required records into the cache // - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": [ "hero(episode:NEWHOPE)": CacheReference("1002"), "hero(episode:JEDI)": CacheReference("1101"), @@ -2427,7 +2424,7 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let query = MockQuery() - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": ["__typename": "Droid", "name": "R2-D2"] ]) diff --git a/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift b/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift index 8225b998c..19324c94a 100644 --- a/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift +++ b/Tests/ApolloTests/Cache/StoreConcurrencyTests.swift @@ -10,19 +10,16 @@ class StoreConcurrencyTests: XCTestCase, CacheDependentTesting { } var defaultWaitTimeout: TimeInterval = 60 - - var cache: (any NormalizedCache)! + var store: ApolloStore! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - store = ApolloStore(cache: cache) + store = try await makeTestStore() } - override func tearDownWithError() throws { - cache = nil + override func tearDownWithError() throws { store = nil try super.tearDownWithError() @@ -60,7 +57,7 @@ class StoreConcurrencyTests: XCTestCase, CacheDependentTesting { // MARK - Tests func testMultipleReadsInitiatedFromMainThread() async throws { - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("2001")], "2001": [ "name": "R2-D2", @@ -84,7 +81,7 @@ class StoreConcurrencyTests: XCTestCase, CacheDependentTesting { allReadsCompletedExpectation.expectedFulfillmentCount = numberOfReads for _ in 0.. Void) { + func isolatedDo(_ block: @Sendable @escaping (isolated SimpleSubscriber) -> Void) { block(self) } diff --git a/Tests/ApolloTests/Cache/WatchQueryTests.swift b/Tests/ApolloTests/Cache/WatchQueryTests.swift index b3111b4d4..657267330 100644 --- a/Tests/ApolloTests/Cache/WatchQueryTests.swift +++ b/Tests/ApolloTests/Cache/WatchQueryTests.swift @@ -5,7 +5,7 @@ import XCTest @testable import Apollo -class WatchQueryTests: XCTestCase, CacheDependentTesting { +class WatchQueryTests: XCTestCase, CacheDependentTesting, @unchecked Sendable { var cacheType: any TestCacheProvider.Type { InMemoryTestCacheProvider.self @@ -13,15 +13,14 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { static let defaultWaitTimeout: TimeInterval = 1 - var cache: (any NormalizedCache)! + var store: ApolloStore! var server: MockGraphQLServer! var client: ApolloClient! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - let store = ApolloStore(cache: cache) + store = try await makeTestStore() server = MockGraphQLServer() let networkTransport = MockNetworkTransport(mockServer: server, store: store) @@ -30,7 +29,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { } override func tearDownWithError() throws { - cache = nil + store = nil server = nil client = nil @@ -78,7 +77,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { } let initialWatcherResultExpectation = - resultObserver.expectation( + await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -110,7 +109,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let refetchedWatcherResultExpectation = resultObserver.expectation( + let refetchedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received refetched result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -168,7 +167,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -200,7 +199,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -259,7 +258,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -293,7 +292,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let noUpdatedResultExpectation = resultObserver.expectation( + let noUpdatedResultExpectation = await resultObserver.expectation( description: "Other query shouldn't trigger refetch" ) { _ in } noUpdatedResultExpectation.isInverted = true @@ -342,8 +341,8 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ) addTeardownBlock { watcher.cancel() } - // Initial fetch from server") { _ in - let serverRequestExpectation = await server.expect(MockQuery.self) { request in + // Initial fetch from server + let serverRequestExpectation = await server.expect(MockQuery.self) { @Sendable request in expect(request.operation.__variables?["episode"] as? String).to(equal("EMPIRE")) return [ "data": [ @@ -356,7 +355,9 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation(description: "Watcher received initial result") { + let initialWatcherResultExpectation = await resultObserver.expectation( + description: "Watcher received initial result" + ) { result in try XCTAssertSuccessResult(result) { graphQLResult in XCTAssertEqual(graphQLResult.source, .server) @@ -376,7 +377,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ) // Fetch same query from server with different argument but returning same object with changed data - let secondServerRequestExpectation = await server.expect(MockQuery.self) { request in + let secondServerRequestExpectation = await server.expect(MockQuery.self) { @Sendable request in expect(request.operation.__variables?["episode"] as? String).to(equal("JEDI")) return [ "data": [ @@ -389,7 +390,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Updated result after refetching query" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -492,7 +493,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -526,7 +527,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -616,6 +617,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { } } } + MockSchemaMetadata.stub_cacheKeyInfoForType_Object(IDCacheKeyProvider.resolver) let watchedQuery = MockQuery() @@ -630,7 +632,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { addTeardownBlock { watcher.cancel() } // Initial fetch from server - let serverRequestExpectation = await server.expect(MockQuery.self) { request in + let serverRequestExpectation = await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -647,7 +649,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -670,7 +672,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Fetch other query with list of updated keys from server let secondServerRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -686,7 +688,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -791,7 +793,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Initial fetch from server let serverRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -808,7 +810,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -831,7 +833,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Fetch other query with list of updated keys from server let secondServerRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -849,7 +851,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { } let refetchServerRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -866,7 +868,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -980,7 +982,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Initial fetch from server let serverRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -997,7 +999,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1020,7 +1022,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Fetch other query with list of updated keys from server let secondServerRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -1037,7 +1039,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { _ in } updatedWatcherResultExpectation.isInverted = true @@ -1136,7 +1138,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1160,7 +1162,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ) // Update object directly in store - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1347,7 +1349,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { }) // Initial fetch from cache - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1368,7 +1370,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Fetch other query with list of updated keys from server let secondServerRequestExpectation = await server.expect( MockQuery.self - ) { request in + ) { @Sendable request in [ "data": [ "hero": [ @@ -1385,7 +1387,9 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let noRefetchExpectation = resultObserver.expectation(description: "Initial query shouldn't trigger refetch") { _ in + let noRefetchExpectation = await resultObserver.expectation( + description: "Initial query shouldn't trigger refetch" + ) { _ in } noRefetchExpectation.isInverted = true @@ -1436,7 +1440,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1471,7 +1475,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { secondServerRequestExpectation.expectedFulfillmentCount = numberOfFetches - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1551,7 +1555,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1586,7 +1590,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { secondServerRequestExpectation.expectedFulfillmentCount = numberOfFetches - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1704,7 +1708,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { addTeardownBlock { watcher.cancel() } // Initial fetch from server - let serverRequestExpectation = await server.expect(HeroAndFriendsNamesWithIDsQuery.self) { request in + let serverRequestExpectation = await server.expect(HeroAndFriendsNamesWithIDsQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -1719,7 +1723,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1756,7 +1760,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ) // Update same query directly in store - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1850,7 +1854,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Initial fetch from server let serverRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -1865,7 +1869,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -1903,7 +1907,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Fetch other query from server let secondServerRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -1919,7 +1923,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let updatedWatcherResultExpectation = resultObserver.expectation( + let updatedWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received updated result from cache" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -2012,7 +2016,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Initial fetch from server let serverRequestExpectation = - await server.expect(MockQuery.self) { request in + await server.expect(MockQuery.self) { @Sendable request in [ "data": [ "hero": [ @@ -2027,7 +2031,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { ] } - let initialWatcherResultExpectation = resultObserver.expectation( + let initialWatcherResultExpectation = await resultObserver.expectation( description: "Watcher received initial result from server" ) { result in try XCTAssertSuccessResult(result) { graphQLResult in @@ -2065,7 +2069,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { // Make sure it gets released watcher = nil - cache = nil + store = nil server = nil client = nil diff --git a/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift index 3087f6c50..b3874f73a 100644 --- a/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift @@ -7,17 +7,17 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { // MARK: - Helpers - private static let executor: GraphQLExecutor = { - let executor = GraphQLExecutor(executionSource: NetworkResponseExecutionSource()) + private static let executor: GraphQLExecutor = { + let executor = GraphQLExecutor(executionSource: NetworkResponseExecutionSource()) return executor }() - private func normalizeRecords( + private static func normalizeRecords( _ selectionSet: S.Type, with variables: GraphQLOperation.Variables? = nil, from object: JSONObject ) async throws -> RecordSet { - return try await GraphQLExecutor_ResultNormalizer_FromResponse_Tests.executor.execute( + return try await executor.execute( selectionSet: selectionSet, on: object, withRootCacheReference: CacheReference.RootQuery, @@ -47,7 +47,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then XCTAssertEqual(records["QUERY_ROOT"]?["hero"] as? CacheReference, @@ -78,7 +78,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, with: variables, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, with: variables, from: object) // then XCTAssertEqual(records["QUERY_ROOT"]?["hero(episode:JEDI)"] as? CacheReference, @@ -115,7 +115,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, with: variables, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, with: variables, from: object) // then XCTAssertEqual(records["QUERY_ROOT"]?["hero(episode:EMPIRE)"] as? CacheReference, @@ -159,7 +159,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then XCTAssertEqual(records["QUERY_ROOT"]?["hero"] as? CacheReference, @@ -216,7 +216,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then XCTAssertEqual(records["QUERY_ROOT"]?["hero"] as? CacheReference, @@ -267,7 +267,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then let hero = try XCTUnwrap(records["QUERY_ROOT.hero"]) @@ -325,7 +325,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then let hero = try XCTUnwrap(records["QUERY_ROOT.hero"]) @@ -384,7 +384,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then let hero = try XCTUnwrap(records["QUERY_ROOT.hero"]) @@ -458,7 +458,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then let han = try XCTUnwrap(records["QUERY_ROOT.hero.friend"]) @@ -532,7 +532,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then let luke = try XCTUnwrap(records["QUERY_ROOT.hero.friend"]) @@ -576,7 +576,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then XCTAssertEqual(records["QUERY_ROOT"]?["hero"] as? CacheReference, @@ -614,7 +614,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { ] // when - let records = try await normalizeRecords(GivenSelectionSet.self, from: object) + let records = try await Self.normalizeRecords(GivenSelectionSet.self, from: object) // then XCTAssertEqual(records["QUERY_ROOT"]?["hero"] as? CacheReference, diff --git a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift index d7e9b39e6..0b6aa3c3e 100644 --- a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift @@ -9,12 +9,12 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { // MARK: - Helpers - private static let executor: GraphQLExecutor = { - let executor = GraphQLExecutor(executionSource: NetworkResponseExecutionSource()) + private static let executor: GraphQLExecutor = { + let executor = GraphQLExecutor(executionSource: NetworkResponseExecutionSource()) return executor }() - private func readValues( + private static func readValues( _ selectionSet: T.Type, from object: JSONObject, variables: GraphQLOperation.Variables? = nil @@ -28,7 +28,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { return T(_dataDict: dataDict) } - private func readValues( + private static func readValues( _ selectionSet: T.Type, in operation: Operation.Type, from object: JSONObject, @@ -55,7 +55,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": "Luke Skywalker"] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.name).to(equal("Luke Skywalker")) @@ -69,7 +69,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = [:] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["name"])) @@ -85,7 +85,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": NSNull()] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["name"])) @@ -101,7 +101,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": 10] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.name).to(equal("10")) @@ -115,7 +115,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": false] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then if case JSONDecodingError.couldNotConvert(let value, let expectedType) = error.underlying { @@ -138,7 +138,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["customScalar": Int(12345678)] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.customScalar).to(equal("12345678")) @@ -154,7 +154,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["customScalar": Int64(989561700)] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.customScalar).to(equal("989561700")) @@ -170,7 +170,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["customScalar": Double(1234.5678)] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.customScalar).to(equal("1234.5678")) @@ -195,7 +195,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["customScalar": Int64(989561700)] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.customScalar).to(equal(GivenCustomScalar(value: 989561700))) @@ -211,7 +211,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": "Luke Skywalker"] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.name).to(equal("Luke Skywalker")) @@ -225,7 +225,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = [:] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["name"])) @@ -241,7 +241,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": NSNull()] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.name).to(beNil()) @@ -255,7 +255,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": 10] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.name).to(equal("10")) @@ -269,7 +269,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["name": false] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then if case JSONDecodingError.couldNotConvert(let value, let expectedType) = error.underlying { @@ -296,7 +296,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["size": "SMALL"] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.size).to(equal(GraphQLEnum(MockEnum.SMALL))) @@ -310,7 +310,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["size": "GIGANTIC"] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.size).to(equal(GraphQLEnum.unknown("GIGANTIC"))) @@ -324,7 +324,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = [:] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["size"])) @@ -342,7 +342,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["size": NSNull()] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["size"])) @@ -358,7 +358,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["size": 10] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then if case JSONDecodingError.couldNotConvert(let value, let expectedType) = error.underlying { @@ -377,7 +377,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["size": 10.0] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then if case JSONDecodingError.couldNotConvert(let value, let expectedType) = error.underlying { @@ -398,7 +398,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": ["Purple", "Potatoes", "iPhone"]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(["Purple", "Potatoes", "iPhone"])) @@ -412,7 +412,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [] as JSONValue] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(Array())) @@ -426,7 +426,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = [:] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) }.to(throwError { (error: GraphQLExecutionError) in + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) }.to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["favorites"])) expect(error.underlying).to(matchError(JSONDecodingError.missingValue)) @@ -441,7 +441,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": NSNull()] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) }.to(throwError { (error: GraphQLExecutionError) in + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) }.to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["favorites"])) expect(error.underlying).to(matchError(JSONDecodingError.nullValue)) @@ -456,7 +456,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [10, 20, 30]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(["10", "20", "30"])) @@ -472,7 +472,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": ["10", "20", "30"]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal([ @@ -490,7 +490,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [true, false, true]] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then if case JSONDecodingError.couldNotConvert(let value, let expectedType) = error.underlying { @@ -511,7 +511,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": ["Purple", "Potatoes", "iPhone"]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(["Purple", "Potatoes", "iPhone"])) @@ -525,7 +525,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [] as JSONValue] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(Array())) @@ -539,7 +539,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = [:] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["favorites"])) @@ -555,7 +555,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": NSNull()] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(beNil()) @@ -569,7 +569,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [10, 20, 30]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(["10", "20", "30"])) @@ -583,7 +583,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [true, false, false]] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then if case JSONDecodingError.couldNotConvert(let value, let expectedType) = error.underlying { @@ -604,7 +604,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": ["Purple", "Potatoes", "iPhone"]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(["Purple", "Potatoes", "iPhone"])) @@ -618,7 +618,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [] as JSONValue] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(Array())) @@ -632,7 +632,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = [:] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["favorites"])) @@ -648,7 +648,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": NSNull()] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["favorites"])) @@ -663,7 +663,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["favorites": ["Red", NSNull(), "Bird"] as JSONValue] - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites! as [String?]).to(equal(["Red", nil, "Bird"])) @@ -679,7 +679,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": ["Purple", "Potatoes", "iPhone"]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal(["Purple", "Potatoes", "iPhone"])) @@ -695,7 +695,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": ["Purple"]] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.favorites).to(equal([GraphQLEnum.unknown("Purple")])) @@ -711,7 +711,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["favorites": [10]] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then if case JSONDecodingError.couldNotConvert(let value, let expectedType) = error.underlying { @@ -745,7 +745,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.child?.name).to(equal("Luke Skywalker")) @@ -767,7 +767,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["child": ["__typename": "Child"]] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["child", "name"])) @@ -796,7 +796,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - await expect { try await self.readValues(GivenSelectionSet.self, from: object) } + await expect { try await Self.readValues(GivenSelectionSet.self, from: object) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["child", "name"])) @@ -856,7 +856,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.child?.__typename).to(equal("Human")) @@ -908,7 +908,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(AnAnimal.self, from: object) + let data = try await Self.readValues(AnAnimal.self, from: object) // then expect(data.animal.__typename).to(equal("Animal")) @@ -964,7 +964,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(AnAnimal.self, from: object, variables: ["varA": true]) + let data = try await Self.readValues(AnAnimal.self, from: object, variables: ["varA": true]) // then expect(data.animal.__typename).to(equal("Animal")) @@ -1019,7 +1019,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(AnAnimal.self, from: object, variables: ["varA": false]) + let data = try await Self.readValues(AnAnimal.self, from: object, variables: ["varA": false]) // then expect(data.animal.__typename).to(equal("Animal")) @@ -1077,7 +1077,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when + then - await expect { try await self.readValues(AnAnimal.self, from: object, variables: ["varA": false]) } + await expect { try await Self.readValues(AnAnimal.self, from: object, variables: ["varA": false]) } .to(throwError { (error: GraphQLExecutionError) in // then expect(error.path).to(equal(["animal.species"])) @@ -1126,7 +1126,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(AnAnimal.Animal.DeferredSpecies.self, in: MockQuery.self, from: object) + let data = try await Self.readValues(AnAnimal.Animal.DeferredSpecies.self, in: MockQuery.self, from: object) // then expect(data.species).to(equal("Canis familiaris")) @@ -1181,7 +1181,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object) + let data = try await Self.readValues(GivenSelectionSet.self, from: object) // then expect(data.child?.name).to(equal("Han Solo")) @@ -1203,7 +1203,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1220,7 +1220,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1237,7 +1237,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": GraphQLNullable(true)] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1254,7 +1254,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": GraphQLNullable(false)] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1271,7 +1271,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["one": true, "two": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1289,7 +1289,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1310,7 +1310,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1331,7 +1331,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1356,7 +1356,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1381,7 +1381,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1417,7 +1417,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1453,7 +1453,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1489,7 +1489,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1525,7 +1525,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1561,7 +1561,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1602,7 +1602,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.id).to(equal("1234")) @@ -1622,7 +1622,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1639,7 +1639,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1656,7 +1656,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": GraphQLNullable(false)] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1673,7 +1673,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": GraphQLNullable(true)] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1693,7 +1693,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": false] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1714,7 +1714,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1739,7 +1739,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let variables = ["variable": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1763,7 +1763,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { "include": true] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1787,7 +1787,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1811,7 +1811,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -1835,7 +1835,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1857,7 +1857,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1877,7 +1877,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1898,7 +1898,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1918,7 +1918,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1939,7 +1939,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1959,7 +1959,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(equal("Luke Skywalker")) @@ -1980,7 +1980,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -2000,7 +2000,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: variables) // then expect(data.name).to(beNil()) @@ -2031,7 +2031,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { for test in tests { // when - let data = try await readValues(GivenSelectionSet.self, from: object, variables: test.variables) + let data = try await Self.readValues(GivenSelectionSet.self, from: object, variables: test.variables) // then if test.expectedResult { diff --git a/Tests/ApolloTests/InputDictTests.swift b/Tests/ApolloTests/InputDictTests.swift index bcab21a5a..c960b96b5 100644 --- a/Tests/ApolloTests/InputDictTests.swift +++ b/Tests/ApolloTests/InputDictTests.swift @@ -1,9 +1,17 @@ -import XCTest -import Nimble import ApolloAPI +import Nimble +import XCTest final class InputDictTests: XCTestCase { + /// ```graphql + /// input OperationInput { + /// stringField: String! + /// nullableField: String + /// requiredFieldWithDefaultValue: String! = "Default" + /// nullableFieldWithDefaultValue: String = "Default" + /// } + /// ``` private struct OperationInput: InputObject { public private(set) var __data: InputDict @@ -13,11 +21,15 @@ final class InputDictTests: XCTestCase { public init( stringField: String, - nullableField: GraphQLNullable = nil + nullableField: GraphQLNullable = nil, + requiredFieldWithDefaultValue: String? = nil, + nullableFieldWithDefaultValue: GraphQLNullable = nil ) { __data = InputDict([ "stringField": stringField, - "nullableField": nullableField + "nullableField": nullableField, + "requiredFieldWithDefaultValue": requiredFieldWithDefaultValue ?? GraphQLNullable.none, + "nullableFieldWithDefaultValue": nullableFieldWithDefaultValue, ]) } @@ -30,9 +42,25 @@ final class InputDictTests: XCTestCase { get { __data["nullableField"] } set { __data["nullableField"] = newValue } } + + public var requiredFieldWithDefaultValue: String? { + get { __data["requiredFieldWithDefaultValue"] } + set { __data["requiredFieldWithDefaultValue"] = newValue } + } + + public var nullableFieldWithDefaultValue: GraphQLNullable { + get { __data["nullableFieldWithDefaultValue"] } + set { __data["nullableFieldWithDefaultValue"] = newValue } + } + } - func test__accessor__givenInputObjectNullableFieldInitializedWithDefaultValue_whenAccessingField_shouldEqualNilOrGraphQLNullableNone() throws { + // MARK: - Field Accessor Tests + + func + test__accessor__given_nullableField_initializedWithDefaultValue_whenAccessingField_shouldEqualNilOrGraphQLNullableNone() + throws + { // given let input = OperationInput(stringField: "Something") @@ -48,7 +76,7 @@ final class InputDictTests: XCTestCase { XCTAssertEqual(nullableValue, nil) } - func test__accessor__givenInputObjectNullableFieldInitializedWithValue_whenAccessingField_shouldEqualnitializedValue() throws { + func test__accessor__given_nullableField_initializedWithValue_whenAccessingField_shouldEqualnitializedValue() throws { // given let input = OperationInput(stringField: "Something", nullableField: "AnotherThing") @@ -64,7 +92,9 @@ final class InputDictTests: XCTestCase { expect(nullableValue.unwrapped).to(equal("AnotherThing")) } - func test__accessor__givenInputObjectNullableFieldInitializedWithNullValue_whenAccessingField_shouldEqualGraphQLNullableNull() throws { + func test__accessor__given_nullableField_initializedWithNullValue_whenAccessingField_shouldEqualGraphQLNullableNull() + throws + { // given let input = OperationInput(stringField: "Something", nullableField: .null) @@ -78,4 +108,345 @@ final class InputDictTests: XCTestCase { expect(nullableValue).to(equal(GraphQLNullable.null)) } + func + test__accessor__given_requiredFieldWithDefaultValue_using_defaultValue_whenAccessingField_should_beNil() + throws + { + // given + let input = OperationInput( + stringField: "Something" + ) + + // when + let value = input.requiredFieldWithDefaultValue + + // then + expect(value).to(beNil()) + } + + func + test__accessor__given_requiredFieldWithDefaultValue_initializedWith_nil_whenAccessingField_should_beNil() + throws + { + // given + let input = OperationInput( + stringField: "Something", + requiredFieldWithDefaultValue: nil + ) + + // when + let value = input.requiredFieldWithDefaultValue + + // then + expect(value).to(beNil()) + } + + func + test__accessor__given_requiredFieldWithDefaultValue_initializedWith_newValue_whenAccessingField_should_haveNewValue() + throws + { + // given + let expected = "TestValue" + + let input = OperationInput( + stringField: "Something", + requiredFieldWithDefaultValue: expected + ) + + // when + let value = input.requiredFieldWithDefaultValue + + // then + expect(value).to(equal(expected)) + } + + func + test__accessor__given_nullableFieldWithDefaultValue_using_defaultValue_whenAccessingField_should_be_none() + throws + { + // given + let input = OperationInput( + stringField: "Something" + ) + + // when + let value = input.nullableFieldWithDefaultValue + + // then + expect(value).to(equal(GraphQLNullable.none)) + } + + func + test__accessor__given_nullableFieldWithDefaultValue_initializedWith_nil_whenAccessingField_should_be_none() + throws + { + // given + let input = OperationInput( + stringField: "Something", + nullableFieldWithDefaultValue: nil + ) + + // when + let value = input.nullableFieldWithDefaultValue + + // then + expect(value).to(equal(GraphQLNullable.none)) + } + + func + test__accessor__given_nullableFieldWithDefaultValue_initializedWith_newValue_whenAccessingField_should_haveNewValue() + throws + { + // given + let expected = "TestValue" + + let input = OperationInput( + stringField: "Something", + nullableFieldWithDefaultValue: .some(expected) + ) + + // when + let value = input.nullableFieldWithDefaultValue + + // then + expect(value.unwrapped).to(equal(expected)) + } + + // MARK: - Setter Tests + + func + test__setter__given_nullableField_setTo_value_whenAccessingField_shouldEqualValue() + throws + { + // given + let expected = "TestValue" + var input = OperationInput(stringField: "Something") + + // when + input.stringField = expected + input.nullableField = .some(expected) + + // then + expect(input.stringField).to(equal(expected)) + expect(input.nullableField.unwrapped).to(equal(expected)) + } + + func test__setter__given_nullableField_setTo_null_whenAccessingField_shouldEqual_null() throws { + // given + var input = OperationInput(stringField: "Something", nullableField: "AnotherThing") + + // when + input.nullableField = .null + + // then + expect(input.nullableField).to(equal(GraphQLNullable.null)) + } + + func test__setter__given_nullableField_setTo_none_whenAccessingField_shouldEqual_none() throws { + // given + var input = OperationInput(stringField: "Something", nullableField: "AnotherThing") + + // when + input.nullableField = .none + + // then + expect(input.nullableField).to(equal(GraphQLNullable.none)) + } + + func + test__setter__given_requiredFieldWithDefaultValue_setTo_nil_whenAccessingField_should_beNil() + throws + { + // given + var input = OperationInput( + stringField: "Something" + ) + + // when + input.requiredFieldWithDefaultValue = nil + + // then + expect(input.requiredFieldWithDefaultValue).to(beNil()) + } + + func + test__setter__given_requiredFieldWithDefaultValue_setTo_newValue_whenAccessingField_should_haveNewValue() + throws + { + // given + let expected = "TestValue" + + var input = OperationInput( + stringField: "Something" + ) + + // when + input.requiredFieldWithDefaultValue = expected + + // then + expect(input.requiredFieldWithDefaultValue).to(equal(expected)) + } + + func + test__setter__given_nullableFieldWithDefaultValue_setTo_nil_whenAccessingField_should_beNil() + throws + { + // given + var input = OperationInput( + stringField: "Something" + ) + + // when + input.nullableFieldWithDefaultValue = nil + + // then + expect(input.nullableFieldWithDefaultValue).to(equal(GraphQLNullable.none)) + } + + func + test__setter__given_nullableFieldWithDefaultValue_setTo_null_whenAccessingField_should_beNil() + throws + { + // given + var input = OperationInput( + stringField: "Something" + ) + + // when + input.nullableFieldWithDefaultValue = .null + + // then + expect(input.nullableFieldWithDefaultValue).to(equal(.null)) + } + + func + test__setter__given_nullableFieldWithDefaultValue_setTo_newValue_whenAccessingField_should_haveNewValue() + throws + { + // given + let expected = "TestValue" + + var input = OperationInput( + stringField: "Something" + ) + + // when + input.nullableFieldWithDefaultValue = .some(expected) + + // then + expect(input.nullableFieldWithDefaultValue.unwrapped).to(equal(expected)) + } + + // MARK: - jsonEncodableValue Tests + + func + test__jsonEncodableValue__given_requiredFieldWithDefaultValue_using_defaultValue_whenAccessingField_shouldEqualGraphQLNullable_none() + throws + { + // given + let input = OperationInput( + stringField: "Something" + ) + + // when + let jsonObject = input._jsonEncodableValue as? JSONObject + let value: Any? = jsonObject?["requiredFieldWithDefaultValue"] + + // then + expect(value).to(beNil()) + } + + func + test__jsonEncodableValue__given_requiredFieldWithDefaultValue_initializedWith_nil_whenAccessingField_shouldEqualGraphQLNullable_none() + throws + { + // given + let input = OperationInput( + stringField: "Something", + requiredFieldWithDefaultValue: nil + ) + + // when + let jsonObject = input._jsonEncodableValue as? JSONObject + let value: Any? = jsonObject?["requiredFieldWithDefaultValue"] + + // then + expect(value).to(beNil()) + } + + func + test__jsonEncodableValue__given_requiredFieldWithDefaultValue_initializedWith_newValue_whenAccessingField_should_haveNewValue() + throws + { + // given + let expected = "TestValue" + + let input = OperationInput( + stringField: "Something", + requiredFieldWithDefaultValue: expected + ) + + // when + let jsonObject = input._jsonEncodableValue?._jsonValue as? JSONObject + let value: Any? = jsonObject?["requiredFieldWithDefaultValue"] + + // then + expect(value as? String).to(equal(expected)) + } + + func + test__jsonEncodableValue__given_nullableFieldWithDefaultValue_using_defaultValue_whenAccessingField_shouldEqualGraphQLNullable_none() + throws + { + // given + let input = OperationInput( + stringField: "Something" + ) + + // when + let jsonObject = input._jsonEncodableValue as? JSONObject + let value: Any? = jsonObject?["nullableFieldWithDefaultValue"] + + // then + expect(value).to(beNil()) + } + + func + test__jsonEncodableValue__given_nullableFieldWithDefaultValue_initializedWith_nil_whenAccessingField_shouldEqualGraphQLNullable_none() + throws + { + // given + let input = OperationInput( + stringField: "Something", + nullableFieldWithDefaultValue: nil + ) + + // when + let jsonObject = input._jsonEncodableValue as? JSONObject + let value: Any? = jsonObject?["nullableFieldWithDefaultValue"] + + // then + expect(value).to(beNil()) + } + + func + test__jsonEncodableValue__given_nullableFieldWithDefaultValue_initializedWith_newValue_whenAccessingField_should_haveNewValue() + throws + { + // given + let expected = "TestValue" + + let input = OperationInput( + stringField: "Something", + nullableFieldWithDefaultValue: .some(expected) + ) + + // when + let jsonObject = input._jsonEncodableValue?._jsonValue as? JSONObject + let value: Any? = jsonObject?["nullableFieldWithDefaultValue"] + + // then + expect(value as? String).to(equal(expected)) + } + } diff --git a/Tests/ApolloTests/Interceptors/GraphQLInterceptor_ErrorHandling_Tests.swift b/Tests/ApolloTests/Interceptors/GraphQLInterceptor_ErrorHandling_Tests.swift index 37f98132e..694418a05 100644 --- a/Tests/ApolloTests/Interceptors/GraphQLInterceptor_ErrorHandling_Tests.swift +++ b/Tests/ApolloTests/Interceptors/GraphQLInterceptor_ErrorHandling_Tests.swift @@ -9,22 +9,19 @@ class GraphQLInterceptor_ErrorHandling_Tests: XCTestCase, CacheDependentTesting, InMemoryTestCacheProvider.self } - var cache: (any NormalizedCache)! var store: ApolloStore! var session: MockURLSession! override func setUp() async throws { try await super.setUp() - cache = try await makeNormalizedCache() - store = ApolloStore(cache: cache) + store = try await makeTestStore() session = MockURLSession(responseProvider: Self.self) } override func tearDown() async throws { await Self.cleanUpRequestHandlers() session = nil - cache = nil store = nil try await super.tearDown() @@ -185,7 +182,7 @@ class GraphQLInterceptor_ErrorHandling_Tests: XCTestCase, CacheDependentTesting, } // Set up initial cache state - await mergeRecordsIntoCache([ + try await store.publish(records: [ "QUERY_ROOT": ["hero": CacheReference("hero")], "hero": ["__typename": "Droid", "name": "R2-D2"], ]) diff --git a/Tests/ApolloTests/JSONResponseParser_SingleResponseParsingTests.swift b/Tests/ApolloTests/JSONResponseParser_SingleResponseParsingTests.swift index 31c3e74a9..0dc413f53 100644 --- a/Tests/ApolloTests/JSONResponseParser_SingleResponseParsingTests.swift +++ b/Tests/ApolloTests/JSONResponseParser_SingleResponseParsingTests.swift @@ -5,6 +5,7 @@ import XCTest @testable import Apollo +#warning("TODO: We need a to test to make sure that there is either data or errors in the result?") class JSONResponseParser_SingleResponseParsingTests: XCTestCase { // MARK: Parsing Tests (Extensions) diff --git a/Tests/ApolloTests/Network/ApolloURLSessionTests.swift b/Tests/ApolloTests/Network/ApolloURLSessionTests.swift index a07533582..3786d7f0d 100644 --- a/Tests/ApolloTests/Network/ApolloURLSessionTests.swift +++ b/Tests/ApolloTests/Network/ApolloURLSessionTests.swift @@ -169,16 +169,15 @@ class ApolloURLSessionTests: XCTestCase, MockResponseProvider { statusCode: -1 ) - let task = Task { - await expect { - try await self.session.chunks(for: request) - }.to(throwError(CancellationError())) + let task = Task { [session] in + return try await session!.chunks(for: request) } task.cancel() - let expectation = await task.value - expect(expectation.status).to(equal(.passed)) + await expect { + try await task.value + }.to(throwError(CancellationError())) } func test__request__multipleSimultaneousRequests() async throws { @@ -205,8 +204,8 @@ class ApolloURLSessionTests: XCTestCase, MockResponseProvider { await withThrowingTaskGroup(of: Void.self) { group in for (index, request) in requests { - group.addTask { - let (chunks, response) = try await self.session.chunks(for: request) + group.addTask { [session, responseStrings] in + let (chunks, response) = try await session!.chunks(for: request) guard let httpResponse = response as? HTTPURLResponse else { fail() return diff --git a/Tests/ApolloTests/Network/AsyncHTTPResponseChunkSequenceTests.swift b/Tests/ApolloTests/Network/AsyncHTTPResponseChunkSequenceTests.swift index 2f2903b04..a7936f3cc 100644 --- a/Tests/ApolloTests/Network/AsyncHTTPResponseChunkSequenceTests.swift +++ b/Tests/ApolloTests/Network/AsyncHTTPResponseChunkSequenceTests.swift @@ -7,8 +7,7 @@ class AsyncHTTPResponseChunkSequenceTests: XCTestCase, MockResponseProvider { var session: MockURLSession! var sessionConfiguration: URLSessionConfiguration! - - @MainActor + override func setUp() { super.setUp() diff --git a/Tests/ApolloTests/RequestChainNetworkTransportTests.swift b/Tests/ApolloTests/RequestChainNetworkTransportTests.swift index 260e60913..443a2556f 100644 --- a/Tests/ApolloTests/RequestChainNetworkTransportTests.swift +++ b/Tests/ApolloTests/RequestChainNetworkTransportTests.swift @@ -11,6 +11,8 @@ import XCTest To test: - Retrying - Cache reads and writes based on cache policy (and if source is cache) + - cache only request gets sent through interceptors still + - cache read after failed network fetch """ ) class RequestChainNetworkTransportTests: XCTestCase, MockResponseProvider { @@ -40,7 +42,7 @@ class RequestChainNetworkTransportTests: XCTestCase, MockResponseProvider { } } - func emptyResponseData() -> Data { + static func emptyResponseData() -> Data { return """ { "data": {} @@ -109,7 +111,7 @@ class RequestChainNetworkTransportTests: XCTestCase, MockResponseProvider { await Self.registerRequestHandler(for: serverUrl) { request in ( .mock(), - self.emptyResponseData() + Self.emptyResponseData() ) } @@ -178,7 +180,7 @@ class RequestChainNetworkTransportTests: XCTestCase, MockResponseProvider { return ( .mock(), - self.emptyResponseData() + Self.emptyResponseData() ) } diff --git a/Tests/ApolloTests/TestMockTests.swift b/Tests/ApolloTests/TestMockTests.swift index ec4c31abf..b39ee4ca0 100644 --- a/Tests/ApolloTests/TestMockTests.swift +++ b/Tests/ApolloTests/TestMockTests.swift @@ -589,7 +589,7 @@ extension MockObject { typealias ClassroomPet = Union } -class Dog: MockObject { +final class Dog: MockObject { static let objectType: Object = TestMockSchema.Types.Dog static let _mockFields = MockFields() @@ -629,7 +629,7 @@ extension Mock where O == Dog { } -class Cat: MockObject { +final class Cat: MockObject { static let objectType: Object = TestMockSchema.Types.Cat static let _mockFields = MockFields() @@ -643,7 +643,7 @@ class Cat: MockObject { } } -class Height: MockObject { +final class Height: MockObject { static let objectType: Object = TestMockSchema.Types.Height static let _mockFields = MockFields() diff --git a/Tests/CodegenCLITests/Commands/FetchSchemaTests.swift b/Tests/CodegenCLITests/Commands/FetchSchemaTests.swift index 6703562dd..ee89a73f7 100644 --- a/Tests/CodegenCLITests/Commands/FetchSchemaTests.swift +++ b/Tests/CodegenCLITests/Commands/FetchSchemaTests.swift @@ -37,7 +37,7 @@ class FetchSchemaTests: XCTestCase { let mockConfiguration = ApolloCodegenConfiguration.mock() let mockFileManager = MockApolloFileManager(strict: true) - mockFileManager.mock(closure: .contents({ path in + await mockFileManager.mock(closure: .contents({ path in let actualPath = URL(fileURLWithPath: path).standardizedFileURL.path let expectedPath = URL(fileURLWithPath: inputPath).standardizedFileURL.path diff --git a/Tests/CodegenCLITests/Commands/GenerateOperationManifestTests.swift b/Tests/CodegenCLITests/Commands/GenerateOperationManifestTests.swift index 19cbaff5a..6a74e6da9 100644 --- a/Tests/CodegenCLITests/Commands/GenerateOperationManifestTests.swift +++ b/Tests/CodegenCLITests/Commands/GenerateOperationManifestTests.swift @@ -37,7 +37,7 @@ class GenerateOperationManifestTests: XCTestCase { let mockConfiguration = ApolloCodegenConfiguration.mock() let mockFileManager = MockApolloFileManager(strict: true) - mockFileManager.mock(closure: .contents({ path in + await mockFileManager.mock(closure: .contents({ path in let actualPath = URL(fileURLWithPath: path).standardizedFileURL.path let expectedPath = URL(fileURLWithPath: inputPath).standardizedFileURL.path @@ -208,7 +208,7 @@ class GenerateOperationManifestTests: XCTestCase { MockApolloCodegen.buildHandler = { configuration in } MockApolloSchemaDownloader.fetchHandler = { configuration in } - try fileManager.createFile( + try await fileManager.createFile( body: """ { "pins": [ @@ -259,7 +259,7 @@ class GenerateOperationManifestTests: XCTestCase { MockApolloCodegen.buildHandler = { configuration in } MockApolloSchemaDownloader.fetchHandler = { configuration in } - try fileManager.createFile( + try await fileManager.createFile( body: """ { "pins": [ diff --git a/Tests/CodegenCLITests/Commands/GenerateTests.swift b/Tests/CodegenCLITests/Commands/GenerateTests.swift index bff729150..d973b8994 100644 --- a/Tests/CodegenCLITests/Commands/GenerateTests.swift +++ b/Tests/CodegenCLITests/Commands/GenerateTests.swift @@ -38,7 +38,7 @@ class GenerateTests: XCTestCase { let mockConfiguration = ApolloCodegenConfiguration.mock() let mockFileManager = MockApolloFileManager(strict: true) - mockFileManager.mock(closure: .contents({ path in + await mockFileManager.mock(closure: .contents({ path in let actualPath = URL(fileURLWithPath: path).standardizedFileURL.path let expectedPath = URL(fileURLWithPath: inputPath).standardizedFileURL.path @@ -347,7 +347,7 @@ class GenerateTests: XCTestCase { MockApolloCodegen.buildHandler = { configuration in } MockApolloSchemaDownloader.fetchHandler = { configuration in } - try fileManager.createFile( + try await fileManager.createFile( body: """ { "pins": [ @@ -399,7 +399,7 @@ class GenerateTests: XCTestCase { MockApolloCodegen.buildHandler = { configuration in } MockApolloSchemaDownloader.fetchHandler = { configuration in } - try fileManager.createFile( + try await fileManager.createFile( body: """ { "pins": [ diff --git a/Tests/CodegenCLITests/Commands/InitializeTests.swift b/Tests/CodegenCLITests/Commands/InitializeTests.swift index 5542cb34c..7ad451597 100644 --- a/Tests/CodegenCLITests/Commands/InitializeTests.swift +++ b/Tests/CodegenCLITests/Commands/InitializeTests.swift @@ -152,15 +152,15 @@ class InitializeTests: XCTestCase { let subject = try parse(options) // when - mockFileManager.mock(closure: .fileExists({ path, isDirectory in + await mockFileManager.mock(closure: .fileExists({ path, isDirectory in return false })) - mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, fileAttributes in + await mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, fileAttributes in // no-op })) - mockFileManager.mock(closure: .createFile({ path, data, fileAttributes in + await mockFileManager.mock(closure: .createFile({ path, data, fileAttributes in let actualPath = URL(fileURLWithPath: path).standardizedFileURL.path let expectedPath = URL(fileURLWithPath: outputPath).standardizedFileURL.path @@ -179,7 +179,7 @@ class InitializeTests: XCTestCase { try await subject._run(fileManager: mockFileManager) // then - expect(self.mockFileManager.allClosuresCalled).to(beTrue()) + await expect { await self.mockFileManager.allClosuresCalled }.to(beTrue()) } func test__output__givenParameters_pathCustom_overwriteDefault_whenFileExists_shouldThrow() async throws { @@ -193,11 +193,11 @@ class InitializeTests: XCTestCase { let subject = try parse(options) // when - mockFileManager.mock(closure: .fileExists({ path, isDirectory in + await mockFileManager.mock(closure: .fileExists({ path, isDirectory in return true })) - mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, fileAttributes in + await mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, fileAttributes in // no-op })) @@ -222,15 +222,15 @@ class InitializeTests: XCTestCase { let subject = try parse(options) // when - mockFileManager.mock(closure: .fileExists({ path, isDirectory in + await mockFileManager.mock(closure: .fileExists({ path, isDirectory in return true })) - mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, fileAttributes in + await mockFileManager.mock(closure: .createDirectory({ path, intermediateDirectories, fileAttributes in // no-op })) - mockFileManager.mock(closure: .createFile({ path, data, fileAttributes in + await mockFileManager.mock(closure: .createFile({ path, data, fileAttributes in let actualPath = URL(fileURLWithPath: path).standardizedFileURL.path let expectedPath = URL(fileURLWithPath: outputPath).standardizedFileURL.path @@ -249,7 +249,7 @@ class InitializeTests: XCTestCase { try await subject._run(fileManager: mockFileManager) // then - expect(self.mockFileManager.allClosuresCalled).to(beTrue()) + await expect { await self.mockFileManager.allClosuresCalled }.to(beTrue()) } func test__output__givenParameters_printTrue_shouldPrintToStandardOutput() async throws { diff --git a/Tests/CodegenCLITests/Support/MockApolloCodegen.swift b/Tests/CodegenCLITests/Support/MockApolloCodegen.swift index f1ed2fd89..7f24081fa 100644 --- a/Tests/CodegenCLITests/Support/MockApolloCodegen.swift +++ b/Tests/CodegenCLITests/Support/MockApolloCodegen.swift @@ -3,7 +3,7 @@ import CodegenCLI import ApolloCodegenLib class MockApolloCodegen: CodegenProvider { - static var buildHandler: ((ApolloCodegenConfiguration) throws -> Void)? = nil + nonisolated(unsafe) static var buildHandler: ((ApolloCodegenConfiguration) throws -> Void)? = nil static func build( with configuration: ApolloCodegenConfiguration, diff --git a/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift b/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift index fa636730e..35d52cafa 100644 --- a/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift +++ b/Tests/CodegenCLITests/Support/MockApolloSchemaDownloader.swift @@ -3,7 +3,7 @@ import CodegenCLI import ApolloCodegenLib class MockApolloSchemaDownloader: SchemaDownloadProvider { - static var fetchHandler: ((ApolloSchemaDownloadConfiguration) throws -> Void)? = nil + nonisolated(unsafe) static var fetchHandler: ((ApolloSchemaDownloadConfiguration) throws -> Void)? = nil static func fetch( configuration: ApolloSchemaDownloadConfiguration, diff --git a/Tests/CodegenCLITests/Support/MockFileManager.swift b/Tests/CodegenCLITests/Support/MockFileManager.swift index ec75c5047..4f200d55c 100644 --- a/Tests/CodegenCLITests/Support/MockFileManager.swift +++ b/Tests/CodegenCLITests/Support/MockFileManager.swift @@ -43,7 +43,7 @@ public class MockApolloFileManager: ApolloFileManager { /// - strict: If `true` then all called closures must be mocked otherwise the call will fail. /// When `false` any called closure that is not mocked will fall through to `super`. As a /// byproduct of `false`, all mocked closures must be called otherwise the test will fail. - public init(strict: Bool = true) { + public nonisolated init(strict: Bool = true) { super.init(base: MockFileManager(strict: strict)) } @@ -63,7 +63,7 @@ public class MockApolloFileManager: ApolloFileManager { return _base.closuresToBeCalled.isEmpty } - class MockFileManager: FileManager { + class MockFileManager: FileManager, @unchecked Sendable { fileprivate var closures: [String: Closure] = [:] fileprivate var closuresToBeCalled: Set = [] diff --git a/Tests/CodegenCLITests/Support/MockLogLevelSetter.swift b/Tests/CodegenCLITests/Support/MockLogLevelSetter.swift index d518dda01..0454e6fdd 100644 --- a/Tests/CodegenCLITests/Support/MockLogLevelSetter.swift +++ b/Tests/CodegenCLITests/Support/MockLogLevelSetter.swift @@ -9,7 +9,7 @@ extension LogLevelSetter { } struct MockLogLevelSetter: LogLevelSetter { - static var levelHandler: ((CodegenLogger.LogLevel) -> Void)? = nil + nonisolated(unsafe) static var levelHandler: ((CodegenLogger.LogLevel) -> Void)? = nil static func SetLoggingLevel(_ level: CodegenLogger.LogLevel) { guard let levelHandler = levelHandler else { diff --git a/Tests/CodegenCLITests/VersionCheckerTests.swift b/Tests/CodegenCLITests/VersionCheckerTests.swift index 0c9e501b0..43149d2a0 100644 --- a/Tests/CodegenCLITests/VersionCheckerTests.swift +++ b/Tests/CodegenCLITests/VersionCheckerTests.swift @@ -97,20 +97,20 @@ class VersionCheckerTests: XCTestCase { packageVersion: packageResolvedVersion, inDirectory directory: String? = nil, apolloVersion: String, - _ test: (() throws -> Void) - ) rethrows { - expect(try self.fileManager.createFile( + _ test: (() async throws -> Void) + ) async rethrows { + await expect { try await self.fileManager.createFile( body: packageVersion.fileBody(apolloVersion: apolloVersion), named: "Package.resolved", inDirectory: directory - )).notTo(throwError()) + ) }.notTo(throwError()) - try test() + try await test() } - func test__matchCLIVersionToApolloVersion__givenNoPackageResolvedFileInProject_returnsNoApolloVersionFound() throws { + func test__matchCLIVersionToApolloVersion__givenNoPackageResolvedFileInProject_returnsNoApolloVersionFound() async throws { // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -118,16 +118,16 @@ class VersionCheckerTests: XCTestCase { expect(result).to(equal(.noApolloVersionFound)) } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInProjectRoot_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInProjectRoot_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch() async throws { // given for packageVersion in packageResolvedVersion.allCases { - try testPackageResolvedFile( + try await testPackageResolvedFile( packageVersion: packageVersion, apolloVersion: Constants.CLIVersion ) { // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -137,18 +137,18 @@ class VersionCheckerTests: XCTestCase { } } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInProjectRoot_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMismatch() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInProjectRoot_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMismatch() async throws { // given let apolloVersion = "1.0.0.test-1" for packageVersion in packageResolvedVersion.allCases { - try testPackageResolvedFile( + try await testPackageResolvedFile( packageVersion: packageVersion, apolloVersion: apolloVersion ) { // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -160,17 +160,17 @@ class VersionCheckerTests: XCTestCase { } } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspace_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspace_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch() async throws { // given for packageVersion in packageResolvedVersion.allCases { - try testPackageResolvedFile( + try await testPackageResolvedFile( packageVersion: packageVersion, inDirectory: "MyProject.xcworkspace/xcshareddata/swiftpm", apolloVersion: Constants.CLIVersion ) { // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -180,19 +180,19 @@ class VersionCheckerTests: XCTestCase { } } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspace_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMismatch() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspace_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMismatch() async throws { // given let apolloVersion = "1.0.0.test-1" for packageVersion in packageResolvedVersion.allCases { - try testPackageResolvedFile( + try await testPackageResolvedFile( packageVersion: packageVersion, inDirectory: "MyProject.xcworkspace/xcshareddata/swiftpm", apolloVersion: apolloVersion ) { // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -204,17 +204,17 @@ class VersionCheckerTests: XCTestCase { } } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeProject_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeProject_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch() async throws { // given for packageVersion in packageResolvedVersion.allCases { - try testPackageResolvedFile( + try await testPackageResolvedFile( packageVersion: packageVersion, inDirectory: "MyProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm", apolloVersion: Constants.CLIVersion ) { // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -224,19 +224,19 @@ class VersionCheckerTests: XCTestCase { } } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeProject_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMismatch() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeProject_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMismatch() async throws { // given let apolloVersion = "1.0.0.test-1" for packageVersion in packageResolvedVersion.allCases { - try testPackageResolvedFile( + try await testPackageResolvedFile( packageVersion: packageVersion, inDirectory: "MyProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm", apolloVersion: apolloVersion ) { // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -248,23 +248,23 @@ class VersionCheckerTests: XCTestCase { } } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspaceAndProject_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch_fromWorkspace() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspaceAndProject_withKnownResolvedFileFormats_hasMatchingVersion_returns_versionMatch_fromWorkspace() async throws { // given for packageVersion in packageResolvedVersion.allCases { - try fileManager.createFile( + try await fileManager.createFile( body: packageVersion.fileBody(apolloVersion: Constants.CLIVersion), named: "Package.resolved", inDirectory: "MyProject.xcworkspace/xcshareddata/swiftpm" ) - try fileManager.createFile( + try await fileManager.createFile( body: packageVersion.fileBody(apolloVersion: Constants.CLIVersion), named: "Package.resolved", inDirectory: "MyProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm" ) // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) @@ -273,24 +273,24 @@ class VersionCheckerTests: XCTestCase { } } - func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspaceAndProject_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMatch_fromWorkspace() throws { + func test__matchCLIVersionToApolloVersion__givenPackageResolvedFileInXcodeWorkspaceAndProject_withKnownResolvedFileFormats_hasNonMatchingVersion_returns_versionMatch_fromWorkspace() async throws { // given for packageVersion in packageResolvedVersion.allCases { - try fileManager.createFile( + try await fileManager.createFile( body: packageVersion.fileBody(apolloVersion: Constants.CLIVersion), named: "Package.resolved", inDirectory: "MyProject.xcworkspace/xcshareddata/swiftpm" ) let apolloProjectVersion = "1.0.0.test-1" - try fileManager.createFile( + try await fileManager.createFile( body: packageVersion.fileBody(apolloVersion: apolloProjectVersion), named: "Package.resolved", inDirectory: "MyProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm" ) // when - let result = try VersionChecker.matchCLIVersionToApolloVersion( + let result = try await VersionChecker.matchCLIVersionToApolloVersion( projectRootURL: fileManager.directoryURL ) diff --git a/Tests/TestCodeGenConfigurations/CodegenXCFramework/CodegenXCFramework.xcodeproj/project.pbxproj b/Tests/TestCodeGenConfigurations/CodegenXCFramework/CodegenXCFramework.xcodeproj/project.pbxproj index 649bac19b..121d036b5 100644 --- a/Tests/TestCodeGenConfigurations/CodegenXCFramework/CodegenXCFramework.xcodeproj/project.pbxproj +++ b/Tests/TestCodeGenConfigurations/CodegenXCFramework/CodegenXCFramework.xcodeproj/project.pbxproj @@ -560,7 +560,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -591,7 +591,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Package.swift b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Package.swift index 8c5aa37ce..9bee607c9 100644 --- a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Package.swift +++ b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "TestApp", - platforms: [.macOS(.v10_15)], + platforms: [.macOS(.v12)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( diff --git a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Tests/TestAppTests/TestAppTests.swift b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Tests/TestAppTests/TestAppTests.swift index c9e870732..843fdf596 100644 --- a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Tests/TestAppTests/TestAppTests.swift +++ b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-InSchemaModule/Tests/TestAppTests/TestAppTests.swift @@ -1,64 +1,77 @@ +import ApolloTestSupport import XCTest + +@testable import Apollo @testable import TestApp -import Apollo -import ApolloTestSupport final class TestAppTests: XCTestCase { - func testCacheKeyResolution() throws { + func testCacheKeyResolution() async throws { let store = ApolloStore() - - let response = GraphQLResponse( - operation: AnimalKingdomAPI.DogQuery(), - body: ["data": [ - "allAnimals": [ - [ - "__typename": "Dog", - "id": "1", - "skinCovering": "Fur", - "species": "Canine", - "houseDetails": "Single Level Ranch" + + let data = + [ + "data": [ + "allAnimals": [ + [ + "__typename": "Dog", + "id": "1", + "skinCovering": "Fur", + "species": "Canine", + "houseDetails": "Single Level Ranch", + ] ] ] - ]]) - - let (_, records) = try response.parseResult() - - let expectation = expectation(description: "Publish Record then Fetch") - - store.publish(records: records!) { _ in - store.withinReadTransaction { transaction in - let dog = try! transaction.readObject( - ofType: AnimalKingdomAPI.DogQuery.Data.AllAnimal.self, - withKey: "Dog:1") - - XCTAssertEqual(dog.id, "1") - expectation.fulfill() - } + ] as JSONObject + + let parser = JSONResponseParser( + response: HTTPURLResponse(), + operationVariables: [:], + includeCacheRecords: true + ) + + let result: ParsedResult = try await parser.parseSingleResponse(body: data) + let records = result.cacheRecords + + try await store.publish(records: records!) + + try await store.withinReadTransaction { transaction in + let dog = try! await transaction.readObject( + ofType: AnimalKingdomAPI.DogQuery.Data.AllAnimal.self, + withKey: "Dog:1" + ) + + XCTAssertEqual(dog.id, "1") } - - waitForExpectations(timeout: 1.0) } - + func test_mockObject_initialization() throws { // given let mockDog: Mock = Mock(id: "100") - + // then XCTAssertEqual(mockDog.id, "100") } } -class MockNetworkTransport: NetworkTransport { - func send( - operation: Operation, - cachePolicy: CachePolicy, - contextIdentifier: UUID?, - context: (any RequestContext)?, - callbackQueue: DispatchQueue, - completionHandler: @escaping (Result, any Error>) -> Void - ) -> any Cancellable where Operation : GraphQLOperation { - return EmptyCancellable() +final class MockNetworkTransport: NetworkTransport { + + func send( + query: Query, + fetchBehavior: FetchBehavior, + requestConfiguration: RequestConfiguration + ) throws -> AsyncThrowingStream, any Error> { + return .init { + return nil + } } - var clientName: String { "Mock" } - var clientVersion: String { "Mock" } + + func send( + mutation: Mutation, + requestConfiguration: RequestConfiguration + ) throws -> AsyncThrowingStream, any Error> { + return .init { + return nil + } + } + } diff --git a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Package.swift b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Package.swift index 1c993243d..2927b69d3 100644 --- a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Package.swift +++ b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -7,7 +7,7 @@ import Foundation let package = Package( name: "PackageOne", platforms: [ - .macOS(.v10_15), + .macOS(.v12), ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. diff --git a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Tests/PackageOneTests/PackageOneTests.swift b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Tests/PackageOneTests/PackageOneTests.swift index b7813a4d5..2f46fe12d 100644 --- a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Tests/PackageOneTests/PackageOneTests.swift +++ b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageOne/Tests/PackageOneTests/PackageOneTests.swift @@ -4,10 +4,10 @@ import TestMocks import ApolloTestSupport final class PackageOneTests: XCTestCase { - func testOperation() { + func testOperation() async { let mockDog = Mock(species: "Canis familiaris") let mockQuery = Mock(dog: mockDog) - let dogQuery = DogQuery.Data.from(mockQuery) + let dogQuery = await DogQuery.Data.from(mockQuery) XCTAssertEqual(dogQuery.dog.species, "Canis familiaris") } diff --git a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Package.swift b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Package.swift index f8acb2bd7..0e7728283 100644 --- a/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Package.swift +++ b/Tests/TestCodeGenConfigurations/EmbeddedInTarget-RelativeAbsolute/PackageTwo/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "PackageTwo", platforms: [ - .macOS(.v10_14), + .macOS(.v12), ], products: [ .library( name: "PackageTwo", targets: ["PackageTwo"]), diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.pbxproj b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.pbxproj deleted file mode 100644 index 15d1f1519..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.pbxproj +++ /dev/null @@ -1,899 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 56; - objects = { - -/* Begin PBXBuildFile section */ - 1B7ED6E41035FB2A0D763AD8 /* Pods_CocoaPodsProjectTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D3E998DF0E4DCCF80C445451 /* Pods_CocoaPodsProjectTests.framework */; }; - 665BDA862AE30BE2004DD21F /* Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 665BDA852AE30BE2004DD21F /* Object.swift */; }; - 66F434EB2C12B5EC00679212 /* CustomClassroomPet.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F434EA2C12B5EC00679212 /* CustomClassroomPet.graphql.swift */; }; - 66F434ED2C12B60800679212 /* CustomSkinCovering.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F434EC2C12B60800679212 /* CustomSkinCovering.graphql.swift */; }; - 66F434EF2C12B60E00679212 /* CustomCrocodile.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F434EE2C12B60E00679212 /* CustomCrocodile.graphql.swift */; }; - 66F434F12C12B61400679212 /* CustomPetSearchFilters.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F434F02C12B61400679212 /* CustomPetSearchFilters.graphql.swift */; }; - 66F434F32C12B61900679212 /* CustomAnimal.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F434F22C12B61900679212 /* CustomAnimal.graphql.swift */; }; - 66F434F52C12B67E00679212 /* CustomCrocodile+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66F434F42C12B67E00679212 /* CustomCrocodile+Mock.graphql.swift */; }; - B51EE231D8798C8408AB9CD5 /* Pods_CocoaPodsProject.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C684794938D6314DF12AB411 /* Pods_CocoaPodsProject.framework */; }; - DE4790B02BFBD66E00939CCC /* ID.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4790AF2BFBD66E00939CCC /* ID.swift */; }; - E607A69C29FB43F80059899E /* CocoaPodsProjectApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A69B29FB43F80059899E /* CocoaPodsProjectApp.swift */; }; - E607A69E29FB43F80059899E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A69D29FB43F80059899E /* ContentView.swift */; }; - E607A6A029FB43F80059899E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E607A69F29FB43F80059899E /* Assets.xcassets */; }; - E607A6A329FB43F80059899E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E607A6A229FB43F80059899E /* Preview Assets.xcassets */; }; - E607A6AE29FB43F90059899E /* CocoaPodsProjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A6AD29FB43F90059899E /* CocoaPodsProjectTests.swift */; }; - E607A74829FB692F0059899E /* PetDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71429FB692F0059899E /* PetDetails.graphql.swift */; }; - E607A74929FB692F0059899E /* HeightInMeters.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71529FB692F0059899E /* HeightInMeters.graphql.swift */; }; - E607A74A29FB692F0059899E /* ClassroomPetDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71629FB692F0059899E /* ClassroomPetDetails.graphql.swift */; }; - E607A74B29FB692F0059899E /* DogFragment.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71729FB692F0059899E /* DogFragment.graphql.swift */; }; - E607A74D29FB692F0059899E /* WarmBloodedDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71929FB692F0059899E /* WarmBloodedDetails.graphql.swift */; }; - E607A74E29FB692F0059899E /* PetDetailsMutation.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71B29FB692F0059899E /* PetDetailsMutation.graphql.swift */; }; - E607A74F29FB692F0059899E /* AllAnimalsLocalCacheMutation.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71C29FB692F0059899E /* AllAnimalsLocalCacheMutation.graphql.swift */; }; - E607A75029FB692F0059899E /* PetSearchLocalCacheMutation.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A71D29FB692F0059899E /* PetSearchLocalCacheMutation.graphql.swift */; }; - E607A75129FB692F0059899E /* PetAdoptionMutation.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A72029FB692F0059899E /* PetAdoptionMutation.graphql.swift */; }; - E607A75229FB692F0059899E /* DogQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A72229FB692F0059899E /* DogQuery.graphql.swift */; }; - E607A75429FB692F0059899E /* ClassroomPetsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A72429FB692F0059899E /* ClassroomPetsQuery.graphql.swift */; }; - E607A75629FB692F0059899E /* AllAnimalsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A72629FB692F0059899E /* AllAnimalsQuery.graphql.swift */; }; - E607A75729FB692F0059899E /* AllAnimalsIncludeSkipQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A72729FB692F0059899E /* AllAnimalsIncludeSkipQuery.graphql.swift */; }; - E607A75829FB692F0059899E /* PetSearchQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A72829FB692F0059899E /* PetSearchQuery.graphql.swift */; }; - E607A75A29FB692F0059899E /* CustomDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A72D29FB692F0059899E /* CustomDate.swift */; }; - E607A75C29FB692F0059899E /* RelativeSize.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73029FB692F0059899E /* RelativeSize.graphql.swift */; }; - E607A75D29FB692F0059899E /* Rat.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73229FB692F0059899E /* Rat.graphql.swift */; }; - E607A75E29FB692F0059899E /* Bird.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73329FB692F0059899E /* Bird.graphql.swift */; }; - E607A75F29FB692F0059899E /* PetRock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73429FB692F0059899E /* PetRock.graphql.swift */; }; - E607A76029FB692F0059899E /* Dog.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73529FB692F0059899E /* Dog.graphql.swift */; }; - E607A76129FB69300059899E /* Height.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73629FB692F0059899E /* Height.graphql.swift */; }; - E607A76229FB69300059899E /* Mutation.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73729FB692F0059899E /* Mutation.graphql.swift */; }; - E607A76329FB69300059899E /* Query.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73829FB692F0059899E /* Query.graphql.swift */; }; - E607A76429FB69300059899E /* Cat.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73929FB692F0059899E /* Cat.graphql.swift */; }; - E607A76529FB69300059899E /* Fish.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73A29FB692F0059899E /* Fish.graphql.swift */; }; - E607A76629FB69300059899E /* Human.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73B29FB692F0059899E /* Human.graphql.swift */; }; - E607A76829FB69300059899E /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73D29FB692F0059899E /* SchemaConfiguration.swift */; }; - E607A76929FB69300059899E /* PetAdoptionInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A73F29FB692F0059899E /* PetAdoptionInput.graphql.swift */; }; - E607A76B29FB69300059899E /* MeasurementsInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A74129FB692F0059899E /* MeasurementsInput.graphql.swift */; }; - E607A76C29FB69300059899E /* SchemaMetadata.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A74229FB692F0059899E /* SchemaMetadata.graphql.swift */; }; - E607A76D29FB69300059899E /* HousePet.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A74429FB692F0059899E /* HousePet.graphql.swift */; }; - E607A76E29FB69300059899E /* Pet.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A74529FB692F0059899E /* Pet.graphql.swift */; }; - E607A77029FB69300059899E /* WarmBlooded.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A74729FB692F0059899E /* WarmBlooded.graphql.swift */; }; - E607A77F29FB69920059899E /* Human+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77229FB69920059899E /* Human+Mock.graphql.swift */; }; - E607A78029FB69920059899E /* Height+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77329FB69920059899E /* Height+Mock.graphql.swift */; }; - E607A78129FB69920059899E /* Query+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77429FB69920059899E /* Query+Mock.graphql.swift */; }; - E607A78229FB69920059899E /* PetRock+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77529FB69920059899E /* PetRock+Mock.graphql.swift */; }; - E607A78329FB69920059899E /* Bird+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77629FB69920059899E /* Bird+Mock.graphql.swift */; }; - E607A78429FB69920059899E /* MockObject+Interfaces.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77729FB69920059899E /* MockObject+Interfaces.graphql.swift */; }; - E607A78529FB69920059899E /* Rat+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77829FB69920059899E /* Rat+Mock.graphql.swift */; }; - E607A78629FB69920059899E /* MockObject+Unions.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77929FB69920059899E /* MockObject+Unions.graphql.swift */; }; - E607A78729FB69920059899E /* Cat+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77A29FB69920059899E /* Cat+Mock.graphql.swift */; }; - E607A78829FB69920059899E /* Dog+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77B29FB69920059899E /* Dog+Mock.graphql.swift */; }; - E607A78929FB69920059899E /* Fish+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77C29FB69920059899E /* Fish+Mock.graphql.swift */; }; - E607A78A29FB69920059899E /* Mutation+Mock.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E607A77D29FB69920059899E /* Mutation+Mock.graphql.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - E607A6AA29FB43F90059899E /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = E607A69029FB43F80059899E /* Project object */; - proxyType = 1; - remoteGlobalIDString = E607A69729FB43F80059899E; - remoteInfo = CocoaPodsProject; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 4C671E51FD3FF74D8B077F45 /* Pods-CocoaPodsProjectTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPodsProjectTests.release.xcconfig"; path = "Target Support Files/Pods-CocoaPodsProjectTests/Pods-CocoaPodsProjectTests.release.xcconfig"; sourceTree = ""; }; - 665BDA852AE30BE2004DD21F /* Object.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Object.swift; sourceTree = ""; }; - 66F434EA2C12B5EC00679212 /* CustomClassroomPet.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomClassroomPet.graphql.swift; sourceTree = ""; }; - 66F434EC2C12B60800679212 /* CustomSkinCovering.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomSkinCovering.graphql.swift; sourceTree = ""; }; - 66F434EE2C12B60E00679212 /* CustomCrocodile.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomCrocodile.graphql.swift; sourceTree = ""; }; - 66F434F02C12B61400679212 /* CustomPetSearchFilters.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomPetSearchFilters.graphql.swift; sourceTree = ""; }; - 66F434F22C12B61900679212 /* CustomAnimal.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomAnimal.graphql.swift; sourceTree = ""; }; - 66F434F42C12B67E00679212 /* CustomCrocodile+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CustomCrocodile+Mock.graphql.swift"; sourceTree = ""; }; - 7D6507DAD078EC7A270097F0 /* Pods-CocoaPodsProject.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPodsProject.release.xcconfig"; path = "Target Support Files/Pods-CocoaPodsProject/Pods-CocoaPodsProject.release.xcconfig"; sourceTree = ""; }; - 951B5BB1CBB3B06FF1E6F602 /* Pods-CocoaPodsProject.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPodsProject.debug.xcconfig"; path = "Target Support Files/Pods-CocoaPodsProject/Pods-CocoaPodsProject.debug.xcconfig"; sourceTree = ""; }; - C684794938D6314DF12AB411 /* Pods_CocoaPodsProject.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CocoaPodsProject.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D3E998DF0E4DCCF80C445451 /* Pods_CocoaPodsProjectTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CocoaPodsProjectTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DE4790AF2BFBD66E00939CCC /* ID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ID.swift; sourceTree = ""; }; - DF6B509D20E06102691FD98C /* Pods-CocoaPodsProjectTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CocoaPodsProjectTests.debug.xcconfig"; path = "Target Support Files/Pods-CocoaPodsProjectTests/Pods-CocoaPodsProjectTests.debug.xcconfig"; sourceTree = ""; }; - E607A69829FB43F80059899E /* CocoaPodsProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CocoaPodsProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; - E607A69B29FB43F80059899E /* CocoaPodsProjectApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaPodsProjectApp.swift; sourceTree = ""; }; - E607A69D29FB43F80059899E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - E607A69F29FB43F80059899E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - E607A6A229FB43F80059899E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - E607A6A429FB43F80059899E /* CocoaPodsProject.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = CocoaPodsProject.entitlements; sourceTree = ""; }; - E607A6A929FB43F90059899E /* CocoaPodsProjectTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CocoaPodsProjectTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - E607A6AD29FB43F90059899E /* CocoaPodsProjectTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaPodsProjectTests.swift; sourceTree = ""; }; - E607A71129FB67140059899E /* apollo-codegen-config.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "apollo-codegen-config.json"; sourceTree = ""; }; - E607A71429FB692F0059899E /* PetDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetDetails.graphql.swift; sourceTree = ""; }; - E607A71529FB692F0059899E /* HeightInMeters.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeightInMeters.graphql.swift; sourceTree = ""; }; - E607A71629FB692F0059899E /* ClassroomPetDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassroomPetDetails.graphql.swift; sourceTree = ""; }; - E607A71729FB692F0059899E /* DogFragment.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DogFragment.graphql.swift; sourceTree = ""; }; - E607A71929FB692F0059899E /* WarmBloodedDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WarmBloodedDetails.graphql.swift; sourceTree = ""; }; - E607A71B29FB692F0059899E /* PetDetailsMutation.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetDetailsMutation.graphql.swift; sourceTree = ""; }; - E607A71C29FB692F0059899E /* AllAnimalsLocalCacheMutation.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllAnimalsLocalCacheMutation.graphql.swift; sourceTree = ""; }; - E607A71D29FB692F0059899E /* PetSearchLocalCacheMutation.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetSearchLocalCacheMutation.graphql.swift; sourceTree = ""; }; - E607A72029FB692F0059899E /* PetAdoptionMutation.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetAdoptionMutation.graphql.swift; sourceTree = ""; }; - E607A72229FB692F0059899E /* DogQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DogQuery.graphql.swift; sourceTree = ""; }; - E607A72429FB692F0059899E /* ClassroomPetsQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClassroomPetsQuery.graphql.swift; sourceTree = ""; }; - E607A72629FB692F0059899E /* AllAnimalsQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllAnimalsQuery.graphql.swift; sourceTree = ""; }; - E607A72729FB692F0059899E /* AllAnimalsIncludeSkipQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllAnimalsIncludeSkipQuery.graphql.swift; sourceTree = ""; }; - E607A72829FB692F0059899E /* PetSearchQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetSearchQuery.graphql.swift; sourceTree = ""; }; - E607A72D29FB692F0059899E /* CustomDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomDate.swift; sourceTree = ""; }; - E607A73029FB692F0059899E /* RelativeSize.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelativeSize.graphql.swift; sourceTree = ""; }; - E607A73229FB692F0059899E /* Rat.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Rat.graphql.swift; sourceTree = ""; }; - E607A73329FB692F0059899E /* Bird.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bird.graphql.swift; sourceTree = ""; }; - E607A73429FB692F0059899E /* PetRock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetRock.graphql.swift; sourceTree = ""; }; - E607A73529FB692F0059899E /* Dog.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dog.graphql.swift; sourceTree = ""; }; - E607A73629FB692F0059899E /* Height.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Height.graphql.swift; sourceTree = ""; }; - E607A73729FB692F0059899E /* Mutation.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Mutation.graphql.swift; sourceTree = ""; }; - E607A73829FB692F0059899E /* Query.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.graphql.swift; sourceTree = ""; }; - E607A73929FB692F0059899E /* Cat.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cat.graphql.swift; sourceTree = ""; }; - E607A73A29FB692F0059899E /* Fish.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Fish.graphql.swift; sourceTree = ""; }; - E607A73B29FB692F0059899E /* Human.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Human.graphql.swift; sourceTree = ""; }; - E607A73D29FB692F0059899E /* SchemaConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaConfiguration.swift; sourceTree = ""; }; - E607A73F29FB692F0059899E /* PetAdoptionInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PetAdoptionInput.graphql.swift; sourceTree = ""; }; - E607A74129FB692F0059899E /* MeasurementsInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeasurementsInput.graphql.swift; sourceTree = ""; }; - E607A74229FB692F0059899E /* SchemaMetadata.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaMetadata.graphql.swift; sourceTree = ""; }; - E607A74429FB692F0059899E /* HousePet.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HousePet.graphql.swift; sourceTree = ""; }; - E607A74529FB692F0059899E /* Pet.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pet.graphql.swift; sourceTree = ""; }; - E607A74729FB692F0059899E /* WarmBlooded.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WarmBlooded.graphql.swift; sourceTree = ""; }; - E607A77229FB69920059899E /* Human+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Human+Mock.graphql.swift"; sourceTree = ""; }; - E607A77329FB69920059899E /* Height+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Height+Mock.graphql.swift"; sourceTree = ""; }; - E607A77429FB69920059899E /* Query+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Query+Mock.graphql.swift"; sourceTree = ""; }; - E607A77529FB69920059899E /* PetRock+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "PetRock+Mock.graphql.swift"; sourceTree = ""; }; - E607A77629FB69920059899E /* Bird+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Bird+Mock.graphql.swift"; sourceTree = ""; }; - E607A77729FB69920059899E /* MockObject+Interfaces.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MockObject+Interfaces.graphql.swift"; sourceTree = ""; }; - E607A77829FB69920059899E /* Rat+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Rat+Mock.graphql.swift"; sourceTree = ""; }; - E607A77929FB69920059899E /* MockObject+Unions.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MockObject+Unions.graphql.swift"; sourceTree = ""; }; - E607A77A29FB69920059899E /* Cat+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Cat+Mock.graphql.swift"; sourceTree = ""; }; - E607A77B29FB69920059899E /* Dog+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dog+Mock.graphql.swift"; sourceTree = ""; }; - E607A77C29FB69920059899E /* Fish+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Fish+Mock.graphql.swift"; sourceTree = ""; }; - E607A77D29FB69920059899E /* Mutation+Mock.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Mutation+Mock.graphql.swift"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - E607A69529FB43F80059899E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B51EE231D8798C8408AB9CD5 /* Pods_CocoaPodsProject.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E607A6A629FB43F90059899E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 1B7ED6E41035FB2A0D763AD8 /* Pods_CocoaPodsProjectTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1EE6B2F55FB6076D9E775D66 /* Pods */ = { - isa = PBXGroup; - children = ( - 951B5BB1CBB3B06FF1E6F602 /* Pods-CocoaPodsProject.debug.xcconfig */, - 7D6507DAD078EC7A270097F0 /* Pods-CocoaPodsProject.release.xcconfig */, - DF6B509D20E06102691FD98C /* Pods-CocoaPodsProjectTests.debug.xcconfig */, - 4C671E51FD3FF74D8B077F45 /* Pods-CocoaPodsProjectTests.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 8B24EAE1D0AA7CD86619645B /* Frameworks */ = { - isa = PBXGroup; - children = ( - C684794938D6314DF12AB411 /* Pods_CocoaPodsProject.framework */, - D3E998DF0E4DCCF80C445451 /* Pods_CocoaPodsProjectTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - E607A68F29FB43F80059899E = { - isa = PBXGroup; - children = ( - E607A71129FB67140059899E /* apollo-codegen-config.json */, - E607A69A29FB43F80059899E /* CocoaPodsProject */, - E607A6AC29FB43F90059899E /* CocoaPodsProjectTests */, - E607A69929FB43F80059899E /* Products */, - 1EE6B2F55FB6076D9E775D66 /* Pods */, - 8B24EAE1D0AA7CD86619645B /* Frameworks */, - ); - sourceTree = ""; - }; - E607A69929FB43F80059899E /* Products */ = { - isa = PBXGroup; - children = ( - E607A69829FB43F80059899E /* CocoaPodsProject.app */, - E607A6A929FB43F90059899E /* CocoaPodsProjectTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - E607A69A29FB43F80059899E /* CocoaPodsProject */ = { - isa = PBXGroup; - children = ( - E607A71229FB692F0059899E /* AnimalKingdomAPI */, - E607A69B29FB43F80059899E /* CocoaPodsProjectApp.swift */, - E607A69D29FB43F80059899E /* ContentView.swift */, - E607A69F29FB43F80059899E /* Assets.xcassets */, - E607A6A429FB43F80059899E /* CocoaPodsProject.entitlements */, - E607A6A129FB43F80059899E /* Preview Content */, - ); - path = CocoaPodsProject; - sourceTree = ""; - }; - E607A6A129FB43F80059899E /* Preview Content */ = { - isa = PBXGroup; - children = ( - E607A6A229FB43F80059899E /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; - E607A6AC29FB43F90059899E /* CocoaPodsProjectTests */ = { - isa = PBXGroup; - children = ( - E607A77129FB69920059899E /* AnimalKingdomAPITestMocks */, - E607A6AD29FB43F90059899E /* CocoaPodsProjectTests.swift */, - ); - path = CocoaPodsProjectTests; - sourceTree = ""; - }; - E607A71229FB692F0059899E /* AnimalKingdomAPI */ = { - isa = PBXGroup; - children = ( - E607A71329FB692F0059899E /* Fragments */, - E607A71A29FB692F0059899E /* LocalCacheMutations */, - E607A71E29FB692F0059899E /* Operations */, - E607A72929FB692F0059899E /* Schema */, - ); - path = AnimalKingdomAPI; - sourceTree = ""; - }; - E607A71329FB692F0059899E /* Fragments */ = { - isa = PBXGroup; - children = ( - E607A71429FB692F0059899E /* PetDetails.graphql.swift */, - E607A71529FB692F0059899E /* HeightInMeters.graphql.swift */, - E607A71629FB692F0059899E /* ClassroomPetDetails.graphql.swift */, - E607A71729FB692F0059899E /* DogFragment.graphql.swift */, - E607A71929FB692F0059899E /* WarmBloodedDetails.graphql.swift */, - ); - path = Fragments; - sourceTree = ""; - }; - E607A71A29FB692F0059899E /* LocalCacheMutations */ = { - isa = PBXGroup; - children = ( - E607A71B29FB692F0059899E /* PetDetailsMutation.graphql.swift */, - E607A71C29FB692F0059899E /* AllAnimalsLocalCacheMutation.graphql.swift */, - E607A71D29FB692F0059899E /* PetSearchLocalCacheMutation.graphql.swift */, - ); - path = LocalCacheMutations; - sourceTree = ""; - }; - E607A71E29FB692F0059899E /* Operations */ = { - isa = PBXGroup; - children = ( - E607A71F29FB692F0059899E /* Mutations */, - E607A72129FB692F0059899E /* Queries */, - ); - path = Operations; - sourceTree = ""; - }; - E607A71F29FB692F0059899E /* Mutations */ = { - isa = PBXGroup; - children = ( - E607A72029FB692F0059899E /* PetAdoptionMutation.graphql.swift */, - ); - path = Mutations; - sourceTree = ""; - }; - E607A72129FB692F0059899E /* Queries */ = { - isa = PBXGroup; - children = ( - E607A72229FB692F0059899E /* DogQuery.graphql.swift */, - E607A72429FB692F0059899E /* ClassroomPetsQuery.graphql.swift */, - E607A72629FB692F0059899E /* AllAnimalsQuery.graphql.swift */, - E607A72729FB692F0059899E /* AllAnimalsIncludeSkipQuery.graphql.swift */, - E607A72829FB692F0059899E /* PetSearchQuery.graphql.swift */, - ); - path = Queries; - sourceTree = ""; - }; - E607A72929FB692F0059899E /* Schema */ = { - isa = PBXGroup; - children = ( - E607A72A29FB692F0059899E /* Unions */, - E607A72C29FB692F0059899E /* CustomScalars */, - E607A72E29FB692F0059899E /* Enums */, - E607A73129FB692F0059899E /* Objects */, - E607A73D29FB692F0059899E /* SchemaConfiguration.swift */, - E607A73E29FB692F0059899E /* InputObjects */, - E607A74229FB692F0059899E /* SchemaMetadata.graphql.swift */, - E607A74329FB692F0059899E /* Interfaces */, - ); - path = Schema; - sourceTree = ""; - }; - E607A72A29FB692F0059899E /* Unions */ = { - isa = PBXGroup; - children = ( - 66F434EA2C12B5EC00679212 /* CustomClassroomPet.graphql.swift */, - ); - path = Unions; - sourceTree = ""; - }; - E607A72C29FB692F0059899E /* CustomScalars */ = { - isa = PBXGroup; - children = ( - 665BDA852AE30BE2004DD21F /* Object.swift */, - DE4790AF2BFBD66E00939CCC /* ID.swift */, - E607A72D29FB692F0059899E /* CustomDate.swift */, - ); - path = CustomScalars; - sourceTree = ""; - }; - E607A72E29FB692F0059899E /* Enums */ = { - isa = PBXGroup; - children = ( - 66F434EC2C12B60800679212 /* CustomSkinCovering.graphql.swift */, - E607A73029FB692F0059899E /* RelativeSize.graphql.swift */, - ); - path = Enums; - sourceTree = ""; - }; - E607A73129FB692F0059899E /* Objects */ = { - isa = PBXGroup; - children = ( - 66F434EE2C12B60E00679212 /* CustomCrocodile.graphql.swift */, - E607A73229FB692F0059899E /* Rat.graphql.swift */, - E607A73329FB692F0059899E /* Bird.graphql.swift */, - E607A73429FB692F0059899E /* PetRock.graphql.swift */, - E607A73529FB692F0059899E /* Dog.graphql.swift */, - E607A73629FB692F0059899E /* Height.graphql.swift */, - E607A73729FB692F0059899E /* Mutation.graphql.swift */, - E607A73829FB692F0059899E /* Query.graphql.swift */, - E607A73929FB692F0059899E /* Cat.graphql.swift */, - E607A73A29FB692F0059899E /* Fish.graphql.swift */, - E607A73B29FB692F0059899E /* Human.graphql.swift */, - ); - path = Objects; - sourceTree = ""; - }; - E607A73E29FB692F0059899E /* InputObjects */ = { - isa = PBXGroup; - children = ( - 66F434F02C12B61400679212 /* CustomPetSearchFilters.graphql.swift */, - E607A73F29FB692F0059899E /* PetAdoptionInput.graphql.swift */, - E607A74129FB692F0059899E /* MeasurementsInput.graphql.swift */, - ); - path = InputObjects; - sourceTree = ""; - }; - E607A74329FB692F0059899E /* Interfaces */ = { - isa = PBXGroup; - children = ( - 66F434F22C12B61900679212 /* CustomAnimal.graphql.swift */, - E607A74429FB692F0059899E /* HousePet.graphql.swift */, - E607A74529FB692F0059899E /* Pet.graphql.swift */, - E607A74729FB692F0059899E /* WarmBlooded.graphql.swift */, - ); - path = Interfaces; - sourceTree = ""; - }; - E607A77129FB69920059899E /* AnimalKingdomAPITestMocks */ = { - isa = PBXGroup; - children = ( - 66F434F42C12B67E00679212 /* CustomCrocodile+Mock.graphql.swift */, - E607A77229FB69920059899E /* Human+Mock.graphql.swift */, - E607A77329FB69920059899E /* Height+Mock.graphql.swift */, - E607A77429FB69920059899E /* Query+Mock.graphql.swift */, - E607A77529FB69920059899E /* PetRock+Mock.graphql.swift */, - E607A77629FB69920059899E /* Bird+Mock.graphql.swift */, - E607A77729FB69920059899E /* MockObject+Interfaces.graphql.swift */, - E607A77829FB69920059899E /* Rat+Mock.graphql.swift */, - E607A77929FB69920059899E /* MockObject+Unions.graphql.swift */, - E607A77A29FB69920059899E /* Cat+Mock.graphql.swift */, - E607A77B29FB69920059899E /* Dog+Mock.graphql.swift */, - E607A77C29FB69920059899E /* Fish+Mock.graphql.swift */, - E607A77D29FB69920059899E /* Mutation+Mock.graphql.swift */, - ); - path = AnimalKingdomAPITestMocks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - E607A69729FB43F80059899E /* CocoaPodsProject */ = { - isa = PBXNativeTarget; - buildConfigurationList = E607A6BD29FB43F90059899E /* Build configuration list for PBXNativeTarget "CocoaPodsProject" */; - buildPhases = ( - EF1528EB4B5A42F7D1ED29C4 /* [CP] Check Pods Manifest.lock */, - E607A69429FB43F80059899E /* Sources */, - E607A69529FB43F80059899E /* Frameworks */, - E607A69629FB43F80059899E /* Resources */, - AAB057D4FF869FAB271801DE /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = CocoaPodsProject; - productName = CocoaPodsProject; - productReference = E607A69829FB43F80059899E /* CocoaPodsProject.app */; - productType = "com.apple.product-type.application"; - }; - E607A6A829FB43F90059899E /* CocoaPodsProjectTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = E607A6C029FB43F90059899E /* Build configuration list for PBXNativeTarget "CocoaPodsProjectTests" */; - buildPhases = ( - 60E939E8C36DD74BFBA50236 /* [CP] Check Pods Manifest.lock */, - E607A6A529FB43F90059899E /* Sources */, - E607A6A629FB43F90059899E /* Frameworks */, - E607A6A729FB43F90059899E /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - E607A6AB29FB43F90059899E /* PBXTargetDependency */, - ); - name = CocoaPodsProjectTests; - productName = CocoaPodsProjectTests; - productReference = E607A6A929FB43F90059899E /* CocoaPodsProjectTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - E607A69029FB43F80059899E /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1420; - LastUpgradeCheck = 1430; - TargetAttributes = { - E607A69729FB43F80059899E = { - CreatedOnToolsVersion = 14.2; - }; - E607A6A829FB43F90059899E = { - CreatedOnToolsVersion = 14.2; - TestTargetID = E607A69729FB43F80059899E; - }; - }; - }; - buildConfigurationList = E607A69329FB43F80059899E /* Build configuration list for PBXProject "CocoaPodsProject" */; - compatibilityVersion = "Xcode 14.0"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = E607A68F29FB43F80059899E; - productRefGroup = E607A69929FB43F80059899E /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - E607A69729FB43F80059899E /* CocoaPodsProject */, - E607A6A829FB43F90059899E /* CocoaPodsProjectTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - E607A69629FB43F80059899E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - E607A6A329FB43F80059899E /* Preview Assets.xcassets in Resources */, - E607A6A029FB43F80059899E /* Assets.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E607A6A729FB43F90059899E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 60E939E8C36DD74BFBA50236 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CocoaPodsProjectTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - AAB057D4FF869FAB271801DE /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-CocoaPodsProject/Pods-CocoaPodsProject-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-CocoaPodsProject/Pods-CocoaPodsProject-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CocoaPodsProject/Pods-CocoaPodsProject-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - EF1528EB4B5A42F7D1ED29C4 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-CocoaPodsProject-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - E607A69429FB43F80059899E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - E607A76029FB692F0059899E /* Dog.graphql.swift in Sources */, - E607A74A29FB692F0059899E /* ClassroomPetDetails.graphql.swift in Sources */, - 66F434EB2C12B5EC00679212 /* CustomClassroomPet.graphql.swift in Sources */, - E607A75F29FB692F0059899E /* PetRock.graphql.swift in Sources */, - E607A76129FB69300059899E /* Height.graphql.swift in Sources */, - 665BDA862AE30BE2004DD21F /* Object.swift in Sources */, - E607A76C29FB69300059899E /* SchemaMetadata.graphql.swift in Sources */, - E607A76329FB69300059899E /* Query.graphql.swift in Sources */, - E607A75229FB692F0059899E /* DogQuery.graphql.swift in Sources */, - 66F434ED2C12B60800679212 /* CustomSkinCovering.graphql.swift in Sources */, - E607A75829FB692F0059899E /* PetSearchQuery.graphql.swift in Sources */, - E607A76629FB69300059899E /* Human.graphql.swift in Sources */, - 66F434EF2C12B60E00679212 /* CustomCrocodile.graphql.swift in Sources */, - E607A76229FB69300059899E /* Mutation.graphql.swift in Sources */, - E607A76E29FB69300059899E /* Pet.graphql.swift in Sources */, - E607A76929FB69300059899E /* PetAdoptionInput.graphql.swift in Sources */, - E607A75029FB692F0059899E /* PetSearchLocalCacheMutation.graphql.swift in Sources */, - E607A76429FB69300059899E /* Cat.graphql.swift in Sources */, - E607A75D29FB692F0059899E /* Rat.graphql.swift in Sources */, - E607A74B29FB692F0059899E /* DogFragment.graphql.swift in Sources */, - 66F434F12C12B61400679212 /* CustomPetSearchFilters.graphql.swift in Sources */, - E607A74E29FB692F0059899E /* PetDetailsMutation.graphql.swift in Sources */, - E607A74F29FB692F0059899E /* AllAnimalsLocalCacheMutation.graphql.swift in Sources */, - E607A75729FB692F0059899E /* AllAnimalsIncludeSkipQuery.graphql.swift in Sources */, - E607A76529FB69300059899E /* Fish.graphql.swift in Sources */, - E607A75E29FB692F0059899E /* Bird.graphql.swift in Sources */, - E607A74929FB692F0059899E /* HeightInMeters.graphql.swift in Sources */, - E607A76829FB69300059899E /* SchemaConfiguration.swift in Sources */, - E607A75629FB692F0059899E /* AllAnimalsQuery.graphql.swift in Sources */, - E607A75429FB692F0059899E /* ClassroomPetsQuery.graphql.swift in Sources */, - E607A74D29FB692F0059899E /* WarmBloodedDetails.graphql.swift in Sources */, - 66F434F32C12B61900679212 /* CustomAnimal.graphql.swift in Sources */, - E607A69E29FB43F80059899E /* ContentView.swift in Sources */, - DE4790B02BFBD66E00939CCC /* ID.swift in Sources */, - E607A76B29FB69300059899E /* MeasurementsInput.graphql.swift in Sources */, - E607A74829FB692F0059899E /* PetDetails.graphql.swift in Sources */, - E607A76D29FB69300059899E /* HousePet.graphql.swift in Sources */, - E607A75A29FB692F0059899E /* CustomDate.swift in Sources */, - E607A75C29FB692F0059899E /* RelativeSize.graphql.swift in Sources */, - E607A69C29FB43F80059899E /* CocoaPodsProjectApp.swift in Sources */, - E607A75129FB692F0059899E /* PetAdoptionMutation.graphql.swift in Sources */, - E607A77029FB69300059899E /* WarmBlooded.graphql.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E607A6A529FB43F90059899E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 66F434F52C12B67E00679212 /* CustomCrocodile+Mock.graphql.swift in Sources */, - E607A78629FB69920059899E /* MockObject+Unions.graphql.swift in Sources */, - E607A78729FB69920059899E /* Cat+Mock.graphql.swift in Sources */, - E607A78029FB69920059899E /* Height+Mock.graphql.swift in Sources */, - E607A78829FB69920059899E /* Dog+Mock.graphql.swift in Sources */, - E607A78429FB69920059899E /* MockObject+Interfaces.graphql.swift in Sources */, - E607A78529FB69920059899E /* Rat+Mock.graphql.swift in Sources */, - E607A78929FB69920059899E /* Fish+Mock.graphql.swift in Sources */, - E607A77F29FB69920059899E /* Human+Mock.graphql.swift in Sources */, - E607A6AE29FB43F90059899E /* CocoaPodsProjectTests.swift in Sources */, - E607A78229FB69920059899E /* PetRock+Mock.graphql.swift in Sources */, - E607A78A29FB69920059899E /* Mutation+Mock.graphql.swift in Sources */, - E607A78129FB69920059899E /* Query+Mock.graphql.swift in Sources */, - E607A78329FB69920059899E /* Bird+Mock.graphql.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - E607A6AB29FB43F90059899E /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = E607A69729FB43F80059899E /* CocoaPodsProject */; - targetProxy = E607A6AA29FB43F90059899E /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin XCBuildConfiguration section */ - E607A6BB29FB43F90059899E /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_TREAT_WARNINGS_AS_ERRORS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.3; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - ONLY_ACTIVE_ARCH = YES; - OTHER_SWIFT_FLAGS = "\"-enable-upcoming-feature ExistentialAny\""; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - E607A6BC29FB43F90059899E /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_TREAT_WARNINGS_AS_ERRORS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 12.3; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - OTHER_SWIFT_FLAGS = "\"-enable-upcoming-feature ExistentialAny\""; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - E607A6BE29FB43F90059899E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 951B5BB1CBB3B06FF1E6F602 /* Pods-CocoaPodsProject.debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = CocoaPodsProject/CocoaPodsProject.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_ASSET_PATHS = "\"CocoaPodsProject/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-enable-upcoming-feature ExistentialAny"; - PRODUCT_BUNDLE_IDENTIFIER = com.apollographql.CocoaPodsProject; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - E607A6BF29FB43F90059899E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7D6507DAD078EC7A270097F0 /* Pods-CocoaPodsProject.release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_ENTITLEMENTS = CocoaPodsProject/CocoaPodsProject.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - DEVELOPMENT_ASSET_PATHS = "\"CocoaPodsProject/Preview Content\""; - ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-enable-upcoming-feature ExistentialAny"; - PRODUCT_BUNDLE_IDENTIFIER = com.apollographql.CocoaPodsProject; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - E607A6C129FB43F90059899E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = DF6B509D20E06102691FD98C /* Pods-CocoaPodsProjectTests.debug.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-enable-upcoming-feature ExistentialAny"; - PRODUCT_BUNDLE_IDENTIFIER = com.apollographql.CocoaPodsProjectTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CocoaPodsProject.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CocoaPodsProject"; - }; - name = Debug; - }; - E607A6C229FB43F90059899E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 4C671E51FD3FF74D8B077F45 /* Pods-CocoaPodsProjectTests.release.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEAD_CODE_STRIPPING = YES; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - OTHER_SWIFT_FLAGS = "-enable-upcoming-feature ExistentialAny"; - PRODUCT_BUNDLE_IDENTIFIER = com.apollographql.CocoaPodsProjectTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/CocoaPodsProject.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/CocoaPodsProject"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - E607A69329FB43F80059899E /* Build configuration list for PBXProject "CocoaPodsProject" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E607A6BB29FB43F90059899E /* Debug */, - E607A6BC29FB43F90059899E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - E607A6BD29FB43F90059899E /* Build configuration list for PBXNativeTarget "CocoaPodsProject" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E607A6BE29FB43F90059899E /* Debug */, - E607A6BF29FB43F90059899E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - E607A6C029FB43F90059899E /* Build configuration list for PBXNativeTarget "CocoaPodsProjectTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - E607A6C129FB43F90059899E /* Debug */, - E607A6C229FB43F90059899E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = E607A69029FB43F80059899E /* Project object */; -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a62..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcworkspace/contents.xcworkspacedata b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 4605f73f7..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d981003..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/AnimalKingdomAPI/Schema/SchemaConfiguration.swift b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/AnimalKingdomAPI/Schema/SchemaConfiguration.swift deleted file mode 100644 index 1c3331d34..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/AnimalKingdomAPI/Schema/SchemaConfiguration.swift +++ /dev/null @@ -1,14 +0,0 @@ -// @generated -// This file was automatically generated and can be edited to -// provide custom configuration for a generated GraphQL schema. -// -// Any changes to this file will not be overwritten by future -// code generation execution. - -import Apollo - -public enum SchemaConfiguration: Apollo.SchemaConfiguration { - public static func cacheKeyInfo(for type: Apollo.Object, object: ObjectData) -> CacheKeyInfo? { - try? CacheKeyInfo(jsonValue: object["id"]) - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/AccentColor.colorset/Contents.json b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb8789700..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "colors" : [ - { - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/AppIcon.appiconset/Contents.json b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 3f00db43e..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "images" : [ - { - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/Contents.json b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/CocoaPodsProject.entitlements b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/CocoaPodsProject.entitlements deleted file mode 100644 index f2ef3ae02..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/CocoaPodsProject.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - - - diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/CocoaPodsProjectApp.swift b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/CocoaPodsProjectApp.swift deleted file mode 100644 index 846e3cb05..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/CocoaPodsProjectApp.swift +++ /dev/null @@ -1,10 +0,0 @@ -import SwiftUI - -@main -struct CocoaPodsProjectApp: App { - var body: some Scene { - WindowGroup { - ContentView() - } - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/ContentView.swift b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/ContentView.swift deleted file mode 100644 index 2a045811a..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/ContentView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import SwiftUI - -struct ContentView: View { - var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") - } - .padding() - } -} - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Preview Content/Preview Assets.xcassets/Contents.json b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProject/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProjectTests/CocoaPodsProjectTests.swift b/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProjectTests/CocoaPodsProjectTests.swift deleted file mode 100644 index c4b216992..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/CocoaPodsProjectTests/CocoaPodsProjectTests.swift +++ /dev/null @@ -1,55 +0,0 @@ -import XCTest -import ApolloTestSupport -@testable import CocoaPodsProject - -final class CocoaPodsProjectTests: XCTestCase { - - func testCacheKeyResolution() throws { - let store = ApolloStore() - - let response = GraphQLResponse( - operation: DogQuery(), - body: ["data": [ - "allAnimals": [ - [ - "__typename": "Dog", - "id": "1", - "skinCovering": "Fur", - "species": "Canine", - "houseDetails": "Single Level Ranch" - ] - ] - ]]) - - let (_, records) = try response.parseResult() - - let expectation = expectation(description: "Publish Record then Fetch") - - store.publish(records: records!) { result in - store.withinReadTransaction { transaction in - let dog = try! transaction.readObject( - ofType: DogQuery.Data.AllAnimal.self, - withKey: "Dog:1") - - XCTAssertEqual(dog.id, "1") - expectation.fulfill() - } - } - - waitForExpectations(timeout: 1.0) - } - - func test_mockObject() throws { - let mock = Mock() - - XCTAssertEqual(mock.__typename, "Dog") - } - - func test_mockUnion() throws { - let mock = Mock() - mock.classroomPets = [Mock()] - - XCTAssertEqual(mock.classroomPets!.count, 1) - } - -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/Podfile b/Tests/TestCodeGenConfigurations/Other-CocoaPods/Podfile deleted file mode 100644 index 20644adc0..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/Podfile +++ /dev/null @@ -1,17 +0,0 @@ -# Uncomment the next line to define a global platform for your project -platform :osx, '12.3' - -target 'CocoaPodsProject' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! - - # Pods for CocoaPodsProject - pod 'Apollo', :path => '../../../apollo-ios' - pod 'ApolloTestSupport', :path => '../../../apollo-ios' - - target 'CocoaPodsProjectTests' do - inherit! :search_paths - # Pods for testing - end - -end diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/apollo-codegen-config.json b/Tests/TestCodeGenConfigurations/Other-CocoaPods/apollo-codegen-config.json deleted file mode 100644 index 0978d2b81..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/apollo-codegen-config.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "schemaNamespace" : "CocoaPodsProject", - "options" : { - "schemaDocumentation" : "include", - "deprecatedEnumCases" : "include", - "apqs" : "disabled", - "cocoapodsCompatibleImportStatements": true, - "additionalInflectionRules" : [ - - ], - "conversionStrategies" : { - "enumCases" : "camelCase" - }, - "pruneGeneratedFiles" : true, - "queryStringLiteralFormat" : "multiline", - "warningsOnDeprecatedUsage" : "include", - "schemaCustomization" : { - "customTypeNames" : { - "SkinCovering" : { - "enum" : { - "cases" : { - "HAIR" : "CUSTOMHAIR" - }, - "name" : "CustomSkinCovering" - } - }, - "Animal" : "CustomAnimal", - "Crocodile" : "CustomCrocodile", - "ClassroomPet" : "CustomClassroomPet", - "Date" : "CustomDate", - "PetSearchFilters" : { - "inputObject" : { - "fields" : { - "size" : "customSize" - }, - "name" : "CustomPetSearchFilters" - } - } - } - } - }, - "input" : { - "operationSearchPaths" : [ - "../../../Sources/AnimalKingdomAPI/animalkingdom-graphql/*.graphql" - ], - "schemaSearchPaths" : [ - "../../../Sources/AnimalKingdomAPI/animalkingdom-graphql/AnimalSchema.graphqls" - ] - }, - "output" : { - "testMocks" : { - "absolute" : { - "path": "./CocoaPodsProjectTests/AnimalKingdomAPITestMocks/" - } - }, - "schemaTypes" : { - "path" : "./CocoaPodsProject/AnimalKingdomAPI", - "moduleType" : { - "other" : { - - } - } - }, - "operations" : { - "inSchemaModule" : { - - } - } - }, - "experimentalFeatures" : { - "clientControlledNullability" : true, - "legacySafelistingCompatibleOperations": false - } -} diff --git a/Tests/TestCodeGenConfigurations/Other-CocoaPods/test-project.sh b/Tests/TestCodeGenConfigurations/Other-CocoaPods/test-project.sh deleted file mode 100755 index ef229c63e..000000000 --- a/Tests/TestCodeGenConfigurations/Other-CocoaPods/test-project.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -pod install -set -o pipefail && xcodebuild test -workspace CocoaPodsProject.xcworkspace -scheme CocoaPodsProject -destination platform=macOS -quiet | xcbeautify --is-ci diff --git a/Tests/TestCodeGenConfigurations/SPMInXcodeProject/SPMInXcodeProject.xcodeproj/project.pbxproj b/Tests/TestCodeGenConfigurations/SPMInXcodeProject/SPMInXcodeProject.xcodeproj/project.pbxproj index 9bfb43351..41c8a2217 100644 --- a/Tests/TestCodeGenConfigurations/SPMInXcodeProject/SPMInXcodeProject.xcodeproj/project.pbxproj +++ b/Tests/TestCodeGenConfigurations/SPMInXcodeProject/SPMInXcodeProject.xcodeproj/project.pbxproj @@ -412,7 +412,7 @@ PRODUCT_BUNDLE_IDENTIFIER = apollograpqhl.SPMInXcodeProject; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Debug; }; @@ -438,7 +438,7 @@ PRODUCT_BUNDLE_IDENTIFIER = apollograpqhl.SPMInXcodeProject; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; }; name = Release; }; diff --git a/Tests/TestCodeGenConfigurations/SwiftPackageManager/Package.swift b/Tests/TestCodeGenConfigurations/SwiftPackageManager/Package.swift index 33c642fb0..28fc90512 100644 --- a/Tests/TestCodeGenConfigurations/SwiftPackageManager/Package.swift +++ b/Tests/TestCodeGenConfigurations/SwiftPackageManager/Package.swift @@ -1,14 +1,15 @@ -// swift-tools-version:5.9 +// swift-tools-version:6.1 import PackageDescription let package = Package( name: "TestApp", platforms: [ - .iOS(.v12), - .macOS(.v10_15), - .tvOS(.v12), - .watchOS(.v5), + .iOS(.v15), + .macOS(.v12), + .tvOS(.v15), + .watchOS(.v8), + .visionOS(.v1), ], products: [ .library(name: "TestApp", targets: ["TestApp"]), diff --git a/Tuist/ProjectDescriptionHelpers/Enums/ApolloTarget.swift b/Tuist/ProjectDescriptionHelpers/Enums/ApolloTarget.swift index e0cc02487..3b0826af2 100644 --- a/Tuist/ProjectDescriptionHelpers/Enums/ApolloTarget.swift +++ b/Tuist/ProjectDescriptionHelpers/Enums/ApolloTarget.swift @@ -104,6 +104,6 @@ public enum ApolloTarget { } public var deploymentTargets: DeploymentTargets { - return DeploymentTargets(macOS: "12.0") + return DeploymentTargets.macOS("12.0") } } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+AnimalKingdomAPI.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+AnimalKingdomAPI.swift index e4c9c3e9c..210984c04 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+AnimalKingdomAPI.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+AnimalKingdomAPI.swift @@ -1,31 +1,29 @@ import ProjectDescription extension Target { - - public static func animalKingdomFramework() -> Target { - let target: ApolloTarget = .animalKingdomAPI - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), - sources: [ - "Sources/\(target.name)/\(target.name)/Sources/**", - "Sources/\(target.name)/Resources.swift" - ], - resources: ResourceFileElements( - resources: [ - .folderReference(path: "Sources/\(target.name)/animalkingdom-graphql") - ] - ), - dependencies: [ - .package(product: "ApolloAPI") - ], - settings: .forTarget(target) - ) - } - + + public static func animalKingdomFramework() -> Target { + let target: ApolloTarget = .animalKingdomAPI + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), + sources: [ + "Sources/\(target.name)/\(target.name)/Sources/**", + "Sources/\(target.name)/Resources.swift", + ], + resources: .resources([ + .folderReference(path: "Sources/\(target.name)/animalkingdom-graphql") + ]), + dependencies: [ + .package(product: "ApolloAPI") + ], + settings: .forTarget(target) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenInternalTestHelpers.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenInternalTestHelpers.swift index 59bef90b1..7511b4b2a 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenInternalTestHelpers.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenInternalTestHelpers.swift @@ -1,32 +1,30 @@ import ProjectDescription extension Target { - - public static func apolloCodegenInternalTestHelpersFramework() -> Target { - let target: ApolloTarget = .apolloCodegenInternalTestHelpers - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - sources: [ - "Tests/\(target.name)/**", - ], - resources: ResourceFileElements( - resources: [ - .folderReference(path: "Sources/\(ApolloTarget.animalKingdomAPI.name)/animalkingdom-graphql"), - .folderReference(path: "Sources/\(ApolloTarget.starWarsAPI.name)/starwars-graphql") - ] - ), - dependencies: [ - .target(name: ApolloTarget.apolloCodegenLibWrapper.name), - .package(product: "OrderedCollections") - ], - settings: .forTarget(target) - ) - } - + + public static func apolloCodegenInternalTestHelpersFramework() -> Target { + let target: ApolloTarget = .apolloCodegenInternalTestHelpers + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + sources: [ + "Tests/\(target.name)/**" + ], + resources: .resources([ + .folderReference(path: "Sources/\(ApolloTarget.animalKingdomAPI.name)/animalkingdom-graphql"), + .folderReference(path: "Sources/\(ApolloTarget.starWarsAPI.name)/starwars-graphql"), + ]), + dependencies: [ + .target(name: ApolloTarget.apolloCodegenLibWrapper.name), + .package(product: "OrderedCollections") + ], + settings: .forTarget(target) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenLibWrapper.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenLibWrapper.swift index 08d808826..cdd5ae074 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenLibWrapper.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenLibWrapper.swift @@ -1,22 +1,22 @@ import ProjectDescription extension Target { - - public static func apolloCodegenLibWrapperFramework() -> Target { - let target: ApolloTarget = .apolloCodegenLibWrapper - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - dependencies: [ - .package(product: "ApolloCodegenLib") - ], - settings: .forTarget(target) - ) - } - + + public static func apolloCodegenLibWrapperFramework() -> Target { + let target: ApolloTarget = .apolloCodegenLibWrapper + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + dependencies: [ + .package(product: "ApolloCodegenLib") + ], + settings: .forTarget(target) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenTests.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenTests.swift index 918d8b7bf..8e14eafd6 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenTests.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloCodegenTests.swift @@ -1,53 +1,51 @@ import ProjectDescription extension Target { - - public static func apolloCodegenTests() -> Target { - let target: ApolloTarget = .apolloCodegenTests - - return Target( - name: target.name, - destinations: target.destinations, - product: .unitTests, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - sources: [ - "Tests/\(target.name)/**", - ], - resources: ResourceFileElements( - resources: [ - .glob(pattern: "Tests/\(target.name)/Resources/*.json") - ] - ), - dependencies: [ - .target(name: ApolloTarget.apolloCodegenInternalTestHelpers.name), - .target(name: ApolloTarget.apolloCodegenLibWrapper.name), - .target(name: ApolloTarget.apolloInternalTestHelpers.name), - .package(product: "OrderedCollections"), - .package(product: "Nimble") - ], - settings: .forTarget(target) - ) - } - + + public static func apolloCodegenTests() -> Target { + let target: ApolloTarget = .apolloCodegenTests + + return .target( + name: target.name, + destinations: target.destinations, + product: .unitTests, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + sources: [ + "Tests/\(target.name)/**" + ], + resources: .resources([ + .glob(pattern: "Tests/\(target.name)/Resources/*.json") + ]), + dependencies: [ + .target(name: ApolloTarget.apolloCodegenInternalTestHelpers.name), + .target(name: ApolloTarget.apolloCodegenLibWrapper.name), + .target(name: ApolloTarget.apolloInternalTestHelpers.name), + .package(product: "OrderedCollections"), + .package(product: "Nimble"), + ], + settings: .forTarget(target) + ) + } + } extension Scheme { - - public static func apolloCodegenTests() -> Scheme { - let target: ApolloTarget = .apolloCodegenTests - - return Scheme( - name: target.name, - buildAction: .buildAction(targets: [ - TargetReference(projectPath: nil, target: target.name) - ]), - testAction: .testPlans([ - ApolloTestPlan.codegenTest.path, - ApolloTestPlan.codegenCITest.path - ]) - ) - } - + + public static func apolloCodegenTests() -> Scheme { + let target: ApolloTarget = .apolloCodegenTests + + return .scheme( + name: target.name, + buildAction: .buildAction(targets: [ + TargetReference.target(target.name) + ]), + testAction: .testPlans([ + ApolloTestPlan.codegenTest.path, + ApolloTestPlan.codegenCITest.path, + ]) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloInternalTestHelpers.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloInternalTestHelpers.swift index 746693cca..f67d2f4fe 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloInternalTestHelpers.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloInternalTestHelpers.swift @@ -1,33 +1,31 @@ import ProjectDescription extension Target { - - public static func apolloInternalTestHelpersFramework() -> Target { - let target: ApolloTarget = .apolloInternalTestHelpers - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - sources: [ - "Tests/\(target.name)/**", - ], - resources: ResourceFileElements( - resources: [ - .glob(pattern: "Tests/\(target.name)/Resources/**/*.txt") - ] - ), - dependencies: [ - .target(name: ApolloTarget.apolloWrapper.name), - .package(product: "ApolloAPI"), - .package(product: "ApolloSQLite"), - .package(product: "ApolloWebSocket") - ], - settings: .forTarget(target) - ) - } - + + public static func apolloInternalTestHelpersFramework() -> Target { + let target: ApolloTarget = .apolloInternalTestHelpers + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + sources: [ + "Tests/\(target.name)/**" + ], + resources: .resources([ + .glob(pattern: "Tests/\(target.name)/Resources/**/*.txt") + ]), + dependencies: [ + .target(name: ApolloTarget.apolloWrapper.name), + .package(product: "ApolloAPI"), + .package(product: "ApolloSQLite"), + .package(product: "ApolloWebSocket"), + ], + settings: .forTarget(target) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPaginationTests.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPaginationTests.swift index c27967630..0dbc1ae6a 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPaginationTests.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPaginationTests.swift @@ -5,7 +5,7 @@ extension Target { public static func apolloPaginationTests() -> Target { let target: ApolloTarget = .apolloPaginationTests - return Target( + return .target( name: target.name, destinations: target.destinations, product: .unitTests, @@ -33,10 +33,10 @@ extension Scheme { public static func apolloPaginationTests() -> Scheme { let target: ApolloTarget = .apolloPaginationTests - return Scheme( + return .scheme( name: target.name, buildAction: .buildAction(targets: [ - TargetReference(projectPath: nil, target: target.name) + TargetReference.target(target.name) ]), testAction: .testPlans([ ApolloTestPlan.paginationTest.path, diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPerformanceTests.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPerformanceTests.swift index 03fb474b6..aabc73833 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPerformanceTests.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloPerformanceTests.swift @@ -1,54 +1,53 @@ import ProjectDescription extension Target { - - public static func apolloPerformanceTests() -> Target { - let target: ApolloTarget = .apolloPerformanceTests - - return Target( - name: target.name, - destinations: target.destinations, - product: .unitTests, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - sources: [ - "Tests/\(target.name)/**", - ], - resources: ResourceFileElements( - resources: [ - .glob(pattern: "Tests/\(target.name)/Responses/**/*.json") - ] - ), - dependencies: [ - .target(name: ApolloTarget.apolloInternalTestHelpers.name), - .target(name: ApolloTarget.animalKingdomAPI.name), - .target(name: ApolloTarget.gitHubAPI.name), - .package(product: "Apollo"), - .package(product: "Nimble") - ], - settings: .forTarget(target) - ) - } - + + public static func apolloPerformanceTests() -> Target { + let target: ApolloTarget = .apolloPerformanceTests + + return .target( + name: target.name, + destinations: target.destinations, + product: .unitTests, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + sources: [ + "Tests/\(target.name)/**" + ], + resources: .resources([ + .glob(pattern: "Tests/\(target.name)/Responses/**/*.json") + ]), + dependencies: [ + .target(name: ApolloTarget.apolloInternalTestHelpers.name), + .target(name: ApolloTarget.animalKingdomAPI.name), + .target(name: ApolloTarget.gitHubAPI.name), + .package(product: "Apollo"), + .package(product: "Nimble"), + ], + settings: .forTarget(target) + ) + } + } extension Scheme { - - public static func apolloPerformanceTests() -> Scheme { - let target: ApolloTarget = .apolloPerformanceTests - - return Scheme( - name: target.name, - buildAction: .buildAction(targets: [ - TargetReference(projectPath: nil, target: target.name) - ]), - testAction: .testPlans([ - ApolloTestPlan.performanceTest.path - ], - configuration: .performanceTesting - ) - ) - } - + + public static func apolloPerformanceTests() -> Scheme { + let target: ApolloTarget = .apolloPerformanceTests + + return .scheme( + name: target.name, + buildAction: .buildAction(targets: [ + TargetReference.target(target.name) + ]), + testAction: .testPlans( + [ + ApolloTestPlan.performanceTest.path + ], + configuration: .performanceTesting + ) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloTests.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloTests.swift index c2a473d72..9a304e3c3 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloTests.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloTests.swift @@ -1,51 +1,51 @@ import ProjectDescription extension Target { - - public static func apolloTests() -> Target { - let target: ApolloTarget = .apolloTests - - return Target( - name: target.name, - destinations: target.destinations, - product: .unitTests, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - sources: [ - "Tests/\(target.name)/**", - ], - dependencies: [ - .target(name: ApolloTarget.apolloInternalTestHelpers.name), - .target(name: ApolloTarget.starWarsAPI.name), - .target(name: ApolloTarget.uploadAPI.name), - .package(product: "Apollo"), - .package(product: "ApolloSQLite"), - .package(product: "ApolloWebSocket"), - .package(product: "ApolloTestSupport"), - .package(product: "Nimble") - ], - settings: .forTarget(target) - ) - } - + + public static func apolloTests() -> Target { + let target: ApolloTarget = .apolloTests + + return .target( + name: target.name, + destinations: target.destinations, + product: .unitTests, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + sources: [ + "Tests/\(target.name)/**" + ], + dependencies: [ + .target(name: ApolloTarget.apolloInternalTestHelpers.name), + .target(name: ApolloTarget.starWarsAPI.name), + .target(name: ApolloTarget.uploadAPI.name), + .package(product: "Apollo"), + .package(product: "ApolloSQLite"), + .package(product: "ApolloWebSocket"), + .package(product: "ApolloTestSupport"), + .package(product: "Nimble"), + ], + settings: .forTarget(target) + ) + } + } extension Scheme { - - public static func apolloTests() -> Scheme { - let target: ApolloTarget = .apolloTests - - return Scheme( - name: target.name, - buildAction: .buildAction(targets: [ - TargetReference(projectPath: nil, target: target.name) - ]), - testAction: .testPlans([ - ApolloTestPlan.unitTest.path, - ApolloTestPlan.ciTest.path - ]) - ) - } - + + public static func apolloTests() -> Scheme { + let target: ApolloTarget = .apolloTests + + return .scheme( + name: target.name, + buildAction: .buildAction(targets: [ + TargetReference.target(target.name) + ]), + testAction: .testPlans([ + ApolloTestPlan.unitTest.path, + ApolloTestPlan.ciTest.path, + ]) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloWrapper.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloWrapper.swift index 78d04f1a5..813ebd054 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloWrapper.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+ApolloWrapper.swift @@ -1,22 +1,22 @@ import ProjectDescription extension Target { - - public static func apolloWrapperFramework() -> Target { - let target: ApolloTarget = .apolloWrapper - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - dependencies: [ - .package(product: "Apollo") - ], - settings: .forTarget(target) - ) - } - + + public static func apolloWrapperFramework() -> Target { + let target: ApolloTarget = .apolloWrapper + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + dependencies: [ + .package(product: "Apollo") + ], + settings: .forTarget(target) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+CodegenCLITests.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+CodegenCLITests.swift index d107b8156..218403317 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+CodegenCLITests.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+CodegenCLITests.swift @@ -1,49 +1,49 @@ import ProjectDescription extension Target { - - public static func codegenCLITests() -> Target { - let target: ApolloTarget = .codegenCLITests - - return Target( - name: target.name, - destinations: target.destinations, - product: .unitTests, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), - sources: [ - "Tests/\(target.name)/*.swift", - "Tests/\(target.name)/Commands/**", - "Tests/\(target.name)/Matchers/**", - "Tests/\(target.name)/Support/**" - ], - dependencies: [ - .target(name: ApolloTarget.apolloInternalTestHelpers.name), - .package(product: "Apollo"), - .package(product: "CodegenCLI"), - .package(product: "Nimble"), - ], - settings: .forTarget(target) - ) - } - + + public static func codegenCLITests() -> Target { + let target: ApolloTarget = .codegenCLITests + + return .target( + name: target.name, + destinations: target.destinations, + product: .unitTests, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Tests/\(target.name)/Info.plist"), + sources: [ + "Tests/\(target.name)/*.swift", + "Tests/\(target.name)/Commands/**", + "Tests/\(target.name)/Matchers/**", + "Tests/\(target.name)/Support/**", + ], + dependencies: [ + .target(name: ApolloTarget.apolloInternalTestHelpers.name), + .package(product: "Apollo"), + .package(product: "CodegenCLI"), + .package(product: "Nimble"), + ], + settings: .forTarget(target) + ) + } + } extension Scheme { - - public static func codegenCLITests() -> Scheme { - let target: ApolloTarget = .codegenCLITests - - return Scheme( - name: target.name, - buildAction: .buildAction(targets: [ - TargetReference(projectPath: nil, target: target.name) - ]), - testAction: .testPlans([ - ApolloTestPlan.codegenCLITest.path - ]) - ) - } - + + public static func codegenCLITests() -> Scheme { + let target: ApolloTarget = .codegenCLITests + + return .scheme( + name: target.name, + buildAction: .buildAction(targets: [ + TargetReference.target(target.name) + ]), + testAction: .testPlans([ + ApolloTestPlan.codegenCLITest.path + ]) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+GitHubAPI.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+GitHubAPI.swift index e736df097..3bea97fb1 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+GitHubAPI.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+GitHubAPI.swift @@ -5,7 +5,7 @@ extension Target { public static func gitHubFramework() -> Target { let target: ApolloTarget = .gitHubAPI - return Target( + return .target( name: target.name, destinations: target.destinations, product: .framework, diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+StarWarsAPI.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+StarWarsAPI.swift index ee36c175b..dd809628c 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+StarWarsAPI.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+StarWarsAPI.swift @@ -1,30 +1,28 @@ import ProjectDescription extension Target { - - public static func starWarsFramework() -> Target { - let target: ApolloTarget = .starWarsAPI - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), - sources: [ - "Sources/\(target.name)/\(target.name)/Sources/**" - ], - resources: ResourceFileElements( - resources: [ - .folderReference(path: "Sources/\(target.name)/starwars-graphql") - ] - ), - dependencies: [ - .package(product: "ApolloAPI") - ], - settings: .forTarget(target) - ) - } - + + public static func starWarsFramework() -> Target { + let target: ApolloTarget = .starWarsAPI + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), + sources: [ + "Sources/\(target.name)/\(target.name)/Sources/**" + ], + resources: .resources([ + .folderReference(path: "Sources/\(target.name)/starwars-graphql") + ]), + dependencies: [ + .package(product: "ApolloAPI") + ], + settings: .forTarget(target) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+SubscriptionAPI.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+SubscriptionAPI.swift index 08112951b..a2e147871 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+SubscriptionAPI.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+SubscriptionAPI.swift @@ -1,30 +1,28 @@ import ProjectDescription extension Target { - - public static func subscriptionFramework() -> Target { - let target: ApolloTarget = .subscriptionAPI - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), - sources: [ - "Sources/\(target.name)/\(target.name)/Sources/**" - ], - resources: ResourceFileElements( - resources: [ - .folderReference(path: "Sources/\(target.name)/graphql") - ] - ), - dependencies: [ - .package(product: "ApolloAPI") - ], - settings: .forTarget(target) - ) - } - + + public static func subscriptionFramework() -> Target { + let target: ApolloTarget = .subscriptionAPI + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), + sources: [ + "Sources/\(target.name)/\(target.name)/Sources/**" + ], + resources: .resources([ + .folderReference(path: "Sources/\(target.name)/graphql") + ]), + dependencies: [ + .package(product: "ApolloAPI") + ], + settings: .forTarget(target) + ) + } + } diff --git a/Tuist/ProjectDescriptionHelpers/Targets/Target+UploadAPI.swift b/Tuist/ProjectDescriptionHelpers/Targets/Target+UploadAPI.swift index 7813d1f62..b3ca82b5f 100644 --- a/Tuist/ProjectDescriptionHelpers/Targets/Target+UploadAPI.swift +++ b/Tuist/ProjectDescriptionHelpers/Targets/Target+UploadAPI.swift @@ -1,25 +1,25 @@ import ProjectDescription extension Target { - - public static func uploadFramework() -> Target { - let target: ApolloTarget = .uploadAPI - - return Target( - name: target.name, - destinations: target.destinations, - product: .framework, - bundleId: "com.apollographql.\(target.name.lowercased())", - deploymentTargets: target.deploymentTargets, - infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), - sources: [ - "Sources/\(target.name)/\(target.name)/Sources/**" - ], - dependencies: [ - .package(product: "ApolloAPI") - ], - settings: .forTarget(target) - ) - } - + + public static func uploadFramework() -> Target { + let target: ApolloTarget = .uploadAPI + + return .target( + name: target.name, + destinations: target.destinations, + product: .framework, + bundleId: "com.apollographql.\(target.name.lowercased())", + deploymentTargets: target.deploymentTargets, + infoPlist: .file(path: "Sources/\(target.name)/Info.plist"), + sources: [ + "Sources/\(target.name)/\(target.name)/Sources/**" + ], + dependencies: [ + .package(product: "ApolloAPI") + ], + settings: .forTarget(target) + ) + } + } diff --git a/apollo-ios-codegen/Package.swift b/apollo-ios-codegen/Package.swift index f56552938..ee90931ac 100644 --- a/apollo-ios-codegen/Package.swift +++ b/apollo-ios-codegen/Package.swift @@ -1,7 +1,4 @@ -// swift-tools-version:5.9 -// -// The swift-tools-version declares the minimum version of Swift required to build this package. -// Swift 5.9 is available from Xcode 15.0. +// swift-tools-version:6.1 import PackageDescription @@ -87,5 +84,6 @@ let package = Package( ], swiftSettings: [.enableUpcomingFeature("ExistentialAny")] ), - ] + ], + swiftLanguageModes: [.v6, .v5] ) diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen+Errors.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen+Errors.swift index ed20dd488..e877b63ab 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen+Errors.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen+Errors.swift @@ -67,7 +67,7 @@ extension ApolloCodegen { /// Errors that may occur during code generation that are not fatal. If these errors are present, /// the generated files will likely not compile correctly. Code generation execution can continue, /// but these errors should be surfaced to the user. - public enum NonFatalError: Equatable { + public enum NonFatalError: Equatable, Sendable { case typeNameConflict(name: String, conflictingName: String, containingObject: String) var errorTypeName: String { diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift index f08338334..28d97a439 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloCodegen.swift @@ -8,12 +8,12 @@ import Utilities #if os(macOS) /// A class to facilitate running code generation -public class ApolloCodegen { +public final class ApolloCodegen: Sendable { // MARK: - Public /// OptionSet used to configure what items should be generated during code generation. - public struct ItemsToGenerate: OptionSet { + public struct ItemsToGenerate: OptionSet, Sendable { public var rawValue: Int /// Only generate your code (Operations, Fragments, Enums, etc), this option maintains the codegen functionality @@ -81,7 +81,7 @@ public class ApolloCodegen { // MARK: - Internal @dynamicMemberLookup - class ConfigurationContext { + final class ConfigurationContext: Sendable { let config: ApolloCodegenConfiguration let pluralizer: Pluralizer let rootURL: URL? @@ -654,7 +654,7 @@ public class ApolloCodegen { let filePathsToDelete = await oldGeneratedFilePaths.subtracting(fileManager.writtenFiles) for path in filePathsToDelete { - try fileManager.deleteFile(atPath: path) + try await fileManager.deleteFile(atPath: path) } } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloadConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloadConfiguration.swift index 05a63c51f..51c3628a5 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloadConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloadConfiguration.swift @@ -1,12 +1,12 @@ import Foundation /// A configuration object that defines behavior for schema download. -public struct ApolloSchemaDownloadConfiguration: Equatable, Codable { +public struct ApolloSchemaDownloadConfiguration: Equatable, Codable, Sendable { // MARK: Types /// How to attempt to download your schema - public enum DownloadMethod: Equatable, Codable { + public enum DownloadMethod: Equatable, Codable, Sendable { /// The Apollo Schema Registry, which serves as a central hub for managing your graph. case apolloRegistry(_ settings: ApolloRegistrySettings) @@ -18,7 +18,7 @@ public struct ApolloSchemaDownloadConfiguration: Equatable, Codable { includeDeprecatedInputValues: Bool = false ) - public struct ApolloRegistrySettings: Equatable, Codable { + public struct ApolloRegistrySettings: Equatable, Codable, Sendable { /// The API key to use when retrieving your schema from the Apollo Registry. public let apiKey: String /// The identifier of the graph to fetch. Can be found in Apollo Studio. @@ -68,7 +68,7 @@ public struct ApolloSchemaDownloadConfiguration: Equatable, Codable { /// The HTTP request method. This is an option on Introspection schema downloads only. /// Apollo Registry downloads are always POST requests. - public enum HTTPMethod: Equatable, CustomStringConvertible, Codable { + public enum HTTPMethod: Equatable, CustomStringConvertible, Codable, Sendable { /// Use POST for HTTP requests. This is the default for GraphQL. case POST /// Use GET for HTTP requests with the GraphQL query being sent in the query string @@ -88,7 +88,7 @@ public struct ApolloSchemaDownloadConfiguration: Equatable, Codable { /// The output format for the downloaded schema. This is an option on Introspection schema /// downloads only. For Apollo Registry schema downloads, the schema will always be output as /// an SDL document - public enum OutputFormat: String, Equatable, CustomStringConvertible, Codable { + public enum OutputFormat: String, Equatable, CustomStringConvertible, Codable, Sendable { /// A Schema Definition Language (SDL) document defining the schema as described in /// the [GraphQL Specification](https://spec.graphql.org/draft/#sec-Schema) case SDL @@ -117,7 +117,7 @@ public struct ApolloSchemaDownloadConfiguration: Equatable, Codable { } /// An HTTP header that will be sent in the schema download request. - public struct HTTPHeader: Equatable, CustomDebugStringConvertible, Codable { + public struct HTTPHeader: Equatable, Sendable, CustomDebugStringConvertible, Codable { /// The name of the header field. HTTP header field names are case insensitive. let key: String /// The value for the header field. diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift index 5ea9fa82e..1fe3a1ea0 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/ApolloSchemaDownloader.swift @@ -56,7 +56,7 @@ public struct ApolloSchemaDownloader { withRootURL rootURL: URL? = nil, session: (any NetworkSession)? = nil ) async throws { - try ApolloFileManager.default.createContainingDirectoryIfNeeded( + try await ApolloFileManager.default.createContainingDirectoryIfNeeded( forPath: configuration.outputPath ) diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+OperationManifestConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+OperationManifestConfiguration.swift index 682c9f559..2bf3303d8 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+OperationManifestConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+OperationManifestConfiguration.swift @@ -2,8 +2,8 @@ import Foundation extension ApolloCodegenConfiguration { - public struct OperationManifestConfiguration: Codable, Equatable { - + public struct OperationManifestConfiguration: Codable, Equatable, Sendable { + // MARK: - Properties /// Local path where the generated operation manifest file should be written. @@ -11,7 +11,7 @@ extension ApolloCodegenConfiguration { /// The version format to use when generating the operation manifest. Defaults to `.persistedQueries`. public let version: Version - public enum Version: String, Codable, Equatable { + public enum Version: String, Codable, Equatable, Sendable { /// Generates an operation manifest for use with persisted queries. case persistedQueries /// Generates an operation manifest in the legacy safelisting format used prior to the diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift index a6c4883c3..7dff02c47 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration+SchemaCustomization.swift @@ -2,8 +2,8 @@ import Foundation extension ApolloCodegenConfiguration { - public struct SchemaCustomization: Codable, Equatable { - + public struct SchemaCustomization: Codable, Equatable, Sendable { + // MARK: - Properties /// Dictionary with Keys representing the types being renamed/customized, and @@ -54,7 +54,7 @@ extension ApolloCodegenConfiguration { // MARK: - Enums - public enum CustomSchemaTypeName: Codable, ExpressibleByStringLiteral, Equatable { + public enum CustomSchemaTypeName: Codable, ExpressibleByStringLiteral, Equatable, Sendable { case type(name: String) case `enum`(name: String?, cases: [String: String]?) case inputObject(name: String?, fields: [String: String]?) diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift index ef798c195..be16e4f82 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenConfiguration/ApolloCodegenConfiguration.swift @@ -2,12 +2,12 @@ import Foundation import IR /// A configuration object that defines behavior for code generation. -public struct ApolloCodegenConfiguration: Codable, Equatable { +public struct ApolloCodegenConfiguration: Codable, Equatable, Sendable { // MARK: Input Types /// The input paths and files required for code generation. - public struct FileInput: Codable, Equatable { + public struct FileInput: Codable, Equatable, Sendable { /// An array of path matching pattern strings used to find GraphQL schema /// files to be included for code generation. /// @@ -158,7 +158,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { // MARK: Output Types /// The paths and files output by code generation. - public struct FileOutput: Codable, Equatable { + public struct FileOutput: Codable, Equatable, Sendable { /// The local path structure for the generated schema types files. public let schemaTypes: SchemaTypesFileOutput /// The local path structure for the generated operation object files. @@ -242,7 +242,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } /// Swift access control configuration. - public enum AccessModifier: String, Codable, Equatable { + public enum AccessModifier: String, Codable, Equatable, Sendable { /// Enable entities to be used within any source file from their defining module, and also in /// a source file from another module that imports the defining module. case `public` @@ -252,7 +252,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } /// The local path structure for the generated schema types files. - public struct SchemaTypesFileOutput: Codable, Equatable { + public struct SchemaTypesFileOutput: Codable, Equatable, Sendable { /// Local path where the generated schema types files should be stored. public let path: String /// How to package the schema types for dependency management. @@ -272,7 +272,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } /// Compatible dependency manager automation. - public enum ModuleType: Codable, Equatable { + public enum ModuleType: Codable, Equatable, Sendable { /// Generated schema types will be manually embedded in a target with the specified `name`. /// No module will be created for the generated schema types. Use `accessModifier` to control /// the visibility of generated code, defaults to `.internal`. @@ -343,7 +343,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } /// Configuation for apollo-ios dependency in SPM modules - public struct ApolloSDKDependency: Codable, Equatable { + public struct ApolloSDKDependency: Codable, Equatable, Sendable { /// URL for the SPM package dependency, not used for local dependencies. /// Defaults to 'https://github.com/apollographql/apollo-ios'. let url: String @@ -399,7 +399,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } /// Type of SPM dependency - public enum SDKVersion: Codable, Equatable { + public enum SDKVersion: Codable, Equatable, Sendable { /// Configures SPM dependency to use the exact version of apollo-ios /// that matches the code generation library version currently in use. /// Results in a dependency that looks like: @@ -513,7 +513,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } /// The local path structure for the generated operation object files. - public enum OperationsFileOutput: Codable, Equatable { + public enum OperationsFileOutput: Codable, Equatable, Sendable { /// All operation object files will be located in the module with the schema types. case inSchemaModule /// Operation object files will be co-located relative to the defining operation `.graphql` @@ -573,7 +573,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } /// The local path structure for the generated test mock object files. - public enum TestMockFileOutput: Codable, Equatable { + public enum TestMockFileOutput: Codable, Equatable, Sendable { /// Test mocks will not be generated. This is the default value. case none /// Generated test mock files will be located in the specified `path`. Use `accessModifier` to @@ -639,7 +639,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } // MARK: - Other Types - public struct OutputOptions: Codable, Equatable { + public struct OutputOptions: Codable, Equatable, Sendable { /// Any non-default rules for pluralization or singularization you wish to include. public let additionalInflectionRules: [InflectionRule] /// How deprecated enum cases from the schema should be handled. @@ -872,18 +872,18 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// Composition is used as a substitute for a boolean where context is better placed in the value /// instead of the parameter name, e.g.: `includeDeprecatedEnumCases = true` vs. /// `deprecatedEnumCases = .include`. - public enum Composition: String, Codable, Equatable { + public enum Composition: String, Codable, Equatable, Sendable { case include case exclude } /// ``ConversionStrategies`` configures rules for how to convert the names of values from the /// schema in generated code. - public struct ConversionStrategies: Codable, Equatable { + public struct ConversionStrategies: Codable, Equatable, Sendable { /// ``ApolloCodegenConfiguration/ConversionStrategies/EnumCases`` is used to specify the strategy /// used to convert the casing of enum cases in a GraphQL schema into generated Swift code. - public enum EnumCases: String, Codable, Equatable { + public enum EnumCases: String, Codable, Equatable, Sendable { /// Generates swift code using the exact name provided in the GraphQL schema /// performing no conversion. case none @@ -894,7 +894,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// ``ApolloCodegenConfiguration/ConversionStrategies/FieldAccessors`` is used to specify the /// strategy used to convert the casing of fields on GraphQL selection sets into field accessors /// on the response models in generated Swift code. - public enum FieldAccessors: String, Codable, Equatable { + public enum FieldAccessors: String, Codable, Equatable, Sendable { /// This conversion strategy will: /// - Lowercase the first letter of all fields. /// - Convert field names that are all `UPPERCASE` to all `lowercase`. @@ -907,7 +907,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// ``ApolloCodegenConfiguration/ConversionStrategies/InputObjects`` is used to specify /// the strategy used to convert the casing of input objects in a GraphQL schema into generated Swift code. - public enum InputObjects: String, Codable, Equatable { + public enum InputObjects: String, Codable, Equatable, Sendable { /// Generates swift code using the exact name provided in the GraphQL schema /// performing no conversion case none @@ -997,7 +997,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { // MARK: - OperationDocumentFormat - public struct OperationDocumentFormat: OptionSet, Codable, Equatable { + public struct OperationDocumentFormat: OptionSet, Codable, Equatable, Sendable { /// Include the GraphQL source document for the operation in the generated operation models. public static let definition = Self(rawValue: 1) /// Include the computed operation identifier hash for use with persisted queries @@ -1060,7 +1060,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// ``SelectionSetInitializers`` functions like an `OptionSet`, allowing you to combine multiple /// different instances together to indicate all the types you would like to generate /// initializers for. - public struct SelectionSetInitializers: Codable, Equatable, ExpressibleByArrayLiteral { + public struct SelectionSetInitializers: Codable, Equatable, Sendable, ExpressibleByArrayLiteral { /// Option to generate initializers for all named fragments. public static let namedFragments: SelectionSetInitializers = .init(.namedFragments) @@ -1122,7 +1122,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { /// - Note: Disabling field merging and `selectionSetInitializers` functionality are /// incompatible. If using `selectionSetInitializers`, `fieldMerging` must be set to `.all`, /// otherwise a validation error will be thrown when runnning code generation. - public struct FieldMerging: Codable, Equatable, ExpressibleByArrayLiteral { + public struct FieldMerging: Codable, Equatable, Sendable, ExpressibleByArrayLiteral { /// Merges fields and fragment accessors from the selection set's direct ancestors. public static let ancestors = FieldMerging(.ancestors) @@ -1160,7 +1160,7 @@ public struct ApolloCodegenConfiguration: Codable, Equatable { } } - public struct ExperimentalFeatures: Codable, Equatable { + public struct ExperimentalFeatures: Codable, Equatable, Sendable { /// **EXPERIMENTAL**: If enabled, the generated operations will be transformed using a method /// that attempts to maintain compatibility with the legacy behavior from diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenLogger.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenLogger.swift index 6d577b98b..3b2aa091c 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenLogger.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/CodegenLogger.swift @@ -21,8 +21,8 @@ public struct CodegenLogger { /// The `LogLevel` at which to print logs. Higher raw values than this will /// be ignored. Defaults to `debug`. - public static var level = LogLevel.debug - + nonisolated(unsafe) public static var level = LogLevel.debug + /// Logs the given string if its `logLevel` is at or above `CodegenLogger.level`, otherwise ignores it. /// /// - Parameter logString: The string to log out, as an autoclosure @@ -44,7 +44,7 @@ public struct CodegenLogger { } // Extension which allows `print` to output to a FileHandle -extension FileHandle: TextOutputStream { +extension FileHandle: @retroactive TextOutputStream { public func write(_ string: String) { guard let data = string.data(using: .utf8) else { return } self.write(data) diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/FileGenerators/FileGenerator.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/FileGenerators/FileGenerator.swift index 763d2cc02..00d492cd7 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/FileGenerators/FileGenerator.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/FileGenerators/FileGenerator.swift @@ -4,7 +4,7 @@ import GraphQLCompiler // MARK: FileGenerator (protocol and extension) /// The methods to conform to when building a code generation Swift file generator. -protocol FileGenerator { +protocol FileGenerator: Sendable { var fileName: String { get } var fileExtension: String { get } var fileSuffix: String? { get } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/FileManager+Apollo.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/FileManager+Apollo.swift index df590cf29..aa67d68f6 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/FileManager+Apollo.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/FileManager+Apollo.swift @@ -2,30 +2,21 @@ import Foundation public typealias FileAttributes = [FileAttributeKey : Any] +@globalActor public actor FileManagerActor: GlobalActor { + public static let shared = FileManagerActor() +} + +@FileManagerActor public class ApolloFileManager { - public static var `default` = ApolloFileManager(base: FileManager.default) + public nonisolated static let `default` = ApolloFileManager(base: FileManager.default) public let base: FileManager - actor WrittenFiles { - public fileprivate(set) var value: Set = [] - - func addWrittenFile(path: String) { - value.insert(path) - } - } - - private let _writtenFiles = WrittenFiles() - /// The paths for the files written to by the ``ApolloFileManager``. - public var writtenFiles: Set { - get async { - await _writtenFiles.value - } - } + public var writtenFiles: Set = [] - init(base: FileManager) { + nonisolated init(base: FileManager) { self.base = base } @@ -96,7 +87,7 @@ public class ApolloFileManager { /// If `false` the function will exit without writing the file if it already exists. /// This will not throw an error. /// Defaults to `false. - public func createFile(atPath path: String, data: Data? = nil, overwrite: Bool = true) async throws { + public func createFile(atPath path: String, data: Data? = nil, overwrite: Bool = true) throws { try createContainingDirectoryIfNeeded(forPath: path) if !overwrite && doesFileExist(atPath: path) { return } @@ -104,15 +95,15 @@ public class ApolloFileManager { guard base.createFile(atPath: path, contents: data, attributes: nil) else { throw FileManagerPathError.cannotCreateFile(at: path) } - await _writtenFiles.addWrittenFile(path: path) + writtenFiles.insert(path) } - public func renameFile(atPath oldPath: String, toPath newPath: String) async throws { + public func renameFile(atPath oldPath: String, toPath newPath: String) throws { guard doesFileExist(atPath: oldPath) else { return } try base.moveItem(atPath: oldPath, toPath: newPath) - await _writtenFiles.addWrittenFile(path: newPath) + writtenFiles.insert(newPath) } /// Creates the containing directory (including all intermediate directories) for the given file URL if necessary. @@ -152,3 +143,5 @@ public enum FileManagerPathError: Swift.Error, LocalizedError, Equatable { } } } + +extension FileManager: @retroactive @unchecked Sendable {} diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Glob.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Glob.swift index a3096d72c..bd61ce5bd 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Glob.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Glob.swift @@ -221,7 +221,7 @@ public struct Glob { } return (0.. String +public typealias OperationIdentifierProvider = @Sendable (_ operation: OperationDescriptor) async throws -> String actor OperationIdentifierFactory { @@ -49,7 +49,7 @@ actor OperationIdentifierFactory { } let DefaultOperationIdentifierProvider = -{ (operation: OperationDescriptor) -> String in +{ @Sendable (operation: OperationDescriptor) -> String in var hasher = SHA256() func updateHash(with source: inout String) { source.withUTF8({ buffer in diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Pluralizer.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Pluralizer.swift index a320abd04..2909bcae0 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Pluralizer.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Pluralizer.swift @@ -2,7 +2,7 @@ import Foundation import InflectorKit /// The types of inflection rules that can be used to customize pluralization. -public enum InflectionRule: Codable, Equatable { +public enum InflectionRule: Codable, Equatable, Sendable { /// A pluralization rule that allows taking a singular word and pluralizing it. /// - singularRegex: A regular expression representing the single version of the word @@ -24,8 +24,8 @@ public enum InflectionRule: Codable, Equatable { case uncountable(word: String) } -struct Pluralizer { - +struct Pluralizer: Sendable { + private let inflector: StringInflector init(rules: [InflectionRule] = []) { @@ -129,3 +129,5 @@ struct Pluralizer { .uncountable(word: "police"), ] } + +extension StringInflector: @retroactive @unchecked Sendable {} diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/DeferredFragmentsMetadataTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/DeferredFragmentsMetadataTemplate.swift index 1197d5d00..8d22d82c9 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/DeferredFragmentsMetadataTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/DeferredFragmentsMetadataTemplate.swift @@ -60,15 +60,15 @@ struct DeferredFragmentsMetadataTemplate { _ deferredFragmentPathTypeInfo: [DeferredPathTypeInfo] ) -> TemplateString { """ - public static var responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( + public static let responseFormat: ResponseFormat = IncrementalDeferredResponseFormat( deferredFragments: [ \(deferredFragmentPathTypeInfo.map { return """ - DeferredFragmentIdentifiers.\($0.deferCondition.label): \($0.typeName).self, + DeferredFragmentIdentifiers.\($0.deferCondition.label): \($0.typeName).self, """ }, separator: "\n") ] - ) + ) """ } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/InputObjectTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/InputObjectTemplate.swift index 637ce990f..1e1e5c01c 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/InputObjectTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/InputObjectTemplate.swift @@ -89,7 +89,13 @@ struct InputObjectTemplate: TemplateRenderer { _ fields: GraphQLInputFieldDictionary ) -> TemplateString { TemplateString(""" - \(fields.map({ "\"\($1.name.schemaName)\": \($1.render(config: config))" }), separator: ",\n") + \(fields.map({ + TemplateString(""" + "\($1.name.schemaName)": \($1.render(config: config))\( + if: !$1.type.isNullable && $1.hasDefaultValue, " ?? GraphQLNullable.none" + ) + """) + }), separator: ",\n") """) } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/MockObjectTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/MockObjectTemplate.swift index a25a4b221..f132d3628 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/MockObjectTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/MockObjectTemplate.swift @@ -54,12 +54,12 @@ struct MockObjectTemplate: TemplateRenderer { let memberAccessControl = accessControlModifier(for: .member) return """ - \(accessControlModifier(for: .parent))class \(objectName): MockObject { + \(accessControlModifier(for: .parent))final class \(objectName): MockObject { \(memberAccessControl)static let objectType: \(config.ApolloAPITargetName).Object = \(config.schemaNamespace.firstUppercased).Objects.\(objectName) \(memberAccessControl)static let _mockFields = MockFields() \(memberAccessControl)typealias MockValueCollectionType = Array> - \(memberAccessControl)struct MockFields { + \(memberAccessControl)struct MockFields: Sendable { \(fields.map { TemplateString(""" \(deprecationReason: $0.deprecationReason, config: config) diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SwiftPackageManagerModuleTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SwiftPackageManagerModuleTemplate.swift index 6a3d259c0..6e2f88e43 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SwiftPackageManagerModuleTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/SwiftPackageManagerModuleTemplate.swift @@ -40,17 +40,18 @@ struct SwiftPackageManagerModuleTemplate: TemplateRenderer { let casedSchemaNamespace = config.schemaNamespace.firstUppercased return TemplateString(""" - // swift-tools-version:5.9 + // swift-tools-version:6.1 import PackageDescription let package = Package( name: "\(casedSchemaNamespace)", platforms: [ - .iOS(.v12), - .macOS(.v10_14), - .tvOS(.v12), - .watchOS(.v5), + .iOS(.v15), + .macOS(.v12), + .tvOS(.v15), + .watchOS(.v8), + .visionOS(.v1), ], products: [ .library(name: "\(casedSchemaNamespace)", targets: ["\(casedSchemaNamespace)"]), @@ -79,7 +80,8 @@ struct SwiftPackageManagerModuleTemplate: TemplateRenderer { path: "\($0.path)" ), """}) - ] + ], + swiftLanguageModes: [.v6, .v5] ) """) diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift index cb7b01c71..e8af6eb00 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/TemplateRenderer.swift @@ -44,7 +44,7 @@ enum TemplateTarget: Equatable { /// /// All templates that output to a file should conform to this protocol, this does not include /// templates that are used by others such as `HeaderCommentTemplate` or `ImportStatementTemplate`. -protocol TemplateRenderer { +protocol TemplateRenderer: Sendable { /// Shared codegen configuration. var config: ApolloCodegen.ConfigurationContext { get } diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/URLDownloader.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/URLDownloader.swift index cce10ba57..b4fbaadc7 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/URLDownloader.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/URLDownloader.swift @@ -13,17 +13,19 @@ public protocol NetworkSession { /// to `resume`. @discardableResult func loadData( with urlRequest: URLRequest, - completionHandler: @escaping (Data?, URLResponse?, (any Error)?) -> Void + completionHandler: @Sendable @escaping (Data?, URLResponse?, (any Error)?) async -> Void ) -> URLSessionDataTask? } extension URLSession: NetworkSession { public func loadData( with urlRequest: URLRequest, - completionHandler: @escaping (Data?, URLResponse?, (any Error)?) -> Void + completionHandler: @Sendable @escaping (Data?, URLResponse?, (any Error)?) async -> Void ) -> URLSessionDataTask? { let task = dataTask(with: urlRequest) { (data, response, error) in - completionHandler(data, response, error) + Task { + await completionHandler(data, response, error) + } } task.resume() @@ -80,7 +82,7 @@ class URLDownloader { timeout: Double ) throws { let semaphore = DispatchSemaphore(value: 0) - var errorToThrow: (any Error)? = DownloadError.downloadTimedOut(after: timeout) + nonisolated(unsafe) var errorToThrow: (any Error)? = DownloadError.downloadTimedOut(after: timeout) session.loadData(with: request) { data, response, error in func finished(_ error: (any Error)? = nil) { @@ -118,7 +120,7 @@ class URLDownloader { } do { - try ApolloFileManager.default.createContainingDirectoryIfNeeded(forPath: outputURL.path) + try await ApolloFileManager.default.createContainingDirectoryIfNeeded(forPath: outputURL.path) try data.write(to: outputURL) } catch (let writeError) { diff --git a/apollo-ios-codegen/Sources/CodegenCLI/Commands/FetchSchema.swift b/apollo-ios-codegen/Sources/CodegenCLI/Commands/FetchSchema.swift index bd11e2f2b..1a9ad6a58 100644 --- a/apollo-ios-codegen/Sources/CodegenCLI/Commands/FetchSchema.swift +++ b/apollo-ios-codegen/Sources/CodegenCLI/Commands/FetchSchema.swift @@ -6,7 +6,7 @@ public struct FetchSchema: AsyncParsableCommand { // MARK: - Configuration - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( commandName: "fetch-schema", abstract: "Download a GraphQL schema from the Apollo Registry or GraphQL introspection." ) diff --git a/apollo-ios-codegen/Sources/CodegenCLI/Commands/Generate.swift b/apollo-ios-codegen/Sources/CodegenCLI/Commands/Generate.swift index 2e7f2bab3..e11ec2dab 100644 --- a/apollo-ios-codegen/Sources/CodegenCLI/Commands/Generate.swift +++ b/apollo-ios-codegen/Sources/CodegenCLI/Commands/Generate.swift @@ -6,7 +6,7 @@ public struct Generate: AsyncParsableCommand { // MARK: - Configuration - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Generate Swift source code based on a code generation configuration." ) @@ -35,7 +35,7 @@ public struct Generate: AsyncParsableCommand { ) async throws { logger.SetLoggingLevel(verbose: inputs.verbose) - try checkForCLIVersionMismatch( + try await checkForCLIVersionMismatch( with: inputs, projectRootURL: projectRootURL ) diff --git a/apollo-ios-codegen/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift b/apollo-ios-codegen/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift index d7a1e8b7e..8352687cc 100644 --- a/apollo-ios-codegen/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift +++ b/apollo-ios-codegen/Sources/CodegenCLI/Commands/GenerateOperationManifest.swift @@ -6,7 +6,7 @@ public struct GenerateOperationManifest: AsyncParsableCommand { // MARK: - Configuration - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( abstract: "Generate Persisted Queries operation manifest based on a code generation configuration." ) @@ -30,7 +30,7 @@ public struct GenerateOperationManifest: AsyncParsableCommand { let configuration = try inputs.getCodegenConfiguration(fileManager: fileManager) - try validate(configuration: configuration, projectRootURL: projectRootURL) + try await validate(configuration: configuration, projectRootURL: projectRootURL) try await generateManifest( configuration: configuration, @@ -55,8 +55,8 @@ public struct GenerateOperationManifest: AsyncParsableCommand { func validate( configuration: ApolloCodegenConfiguration, projectRootURL: URL? - ) throws { - try checkForCLIVersionMismatch(with: inputs, projectRootURL: projectRootURL) + ) async throws { + try await checkForCLIVersionMismatch(with: inputs, projectRootURL: projectRootURL) guard configuration.operationManifest != nil else { throw ValidationError(""" diff --git a/apollo-ios-codegen/Sources/CodegenCLI/Commands/Initialize.swift b/apollo-ios-codegen/Sources/CodegenCLI/Commands/Initialize.swift index c4065da6e..baf55bd58 100644 --- a/apollo-ios-codegen/Sources/CodegenCLI/Commands/Initialize.swift +++ b/apollo-ios-codegen/Sources/CodegenCLI/Commands/Initialize.swift @@ -6,7 +6,7 @@ public struct Initialize: AsyncParsableCommand { // MARK: - Configuration - public static var configuration = CommandConfiguration( + public static let configuration = CommandConfiguration( commandName: "init", abstract: "Initialize a new configuration with defaults." ) @@ -132,13 +132,15 @@ public struct Initialize: AsyncParsableCommand { fileManager: ApolloFileManager, output: OutputClosure? = nil ) async throws { - if !overwrite && fileManager.doesFileExist(atPath: path) { - throw Error( - errorDescription: """ + if !overwrite { + if await fileManager.doesFileExist(atPath: path) { + throw Error( + errorDescription: """ File already exists at \(path). Hint: use --overwrite to overwrite any existing \ file at the path. """ - ) + ) + } } try await fileManager.createFile( diff --git a/apollo-ios-codegen/Sources/CodegenCLI/Extensions/ParsableCommand+Apollo.swift b/apollo-ios-codegen/Sources/CodegenCLI/Extensions/ParsableCommand+Apollo.swift index 459a39823..25e30d757 100644 --- a/apollo-ios-codegen/Sources/CodegenCLI/Extensions/ParsableCommand+Apollo.swift +++ b/apollo-ios-codegen/Sources/CodegenCLI/Extensions/ParsableCommand+Apollo.swift @@ -14,11 +14,11 @@ extension ParsableCommand { with inputs: InputOptions, projectRootURL: URL?, ignoreVersionMismatch: Bool = false - ) throws { + ) async throws { let rootURL = projectRootURL ?? rootOutputURL(for: inputs) if case let .versionMismatch(cliVersion, apolloVersion) = - try VersionChecker.matchCLIVersionToApolloVersion(projectRootURL: rootURL) { + try await VersionChecker.matchCLIVersionToApolloVersion(projectRootURL: rootURL) { let errorMessage = """ Apollo Version Mismatch We've detected that the version of the Apollo Codegen CLI does not match the version of the diff --git a/apollo-ios-codegen/Sources/CodegenCLI/Extensions/VersionChecker.swift b/apollo-ios-codegen/Sources/CodegenCLI/Extensions/VersionChecker.swift index 87d6f637f..acbac6348 100644 --- a/apollo-ios-codegen/Sources/CodegenCLI/Extensions/VersionChecker.swift +++ b/apollo-ios-codegen/Sources/CodegenCLI/Extensions/VersionChecker.swift @@ -9,6 +9,7 @@ enum VersionChecker { case versionMismatch(cliVersion: String, apolloVersion: String) } + @FileManagerActor static func matchCLIVersionToApolloVersion(projectRootURL: URL?) throws -> VersionCheckResult { guard var packageModel = try findPackageResolvedFile(projectRootURL: projectRootURL), let apolloVersion = packageModel.apolloVersion else { @@ -23,6 +24,7 @@ enum VersionChecker { } } + @FileManagerActor private static func findPackageResolvedFile(projectRootURL: URL?) throws -> PackageResolvedModel? { let Package_resolved = "Package.resolved" let fileManager = ApolloFileManager.default diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift index bd449cb54..7c981eb6d 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift @@ -3,7 +3,7 @@ import TemplateString import OrderedCollections /// The output of the frontend compiler. -public final class CompilationResult: JavaScriptObjectDecodable { +public final class CompilationResult: Sendable, JavaScriptObjectDecodable { /// String constants used to match JavaScriptObject instances. fileprivate enum Constants { @@ -38,9 +38,10 @@ public final class CompilationResult: JavaScriptObjectDecodable { self.schemaDocumentation = schemaDocumentation } + @MainActor static func fromJSValue( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> Self { self.init( schemaRootTypes: .fromJSValue(jsValue["rootTypes"], bridge: bridge), @@ -51,7 +52,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { ) } - public final class RootTypeDefinition: JavaScriptObjectDecodable { + public final class RootTypeDefinition: Sendable, JavaScriptObjectDecodable { public let queryType: GraphQLNamedType public let mutationType: GraphQLNamedType? @@ -76,9 +77,10 @@ public final class CompilationResult: JavaScriptObjectDecodable { ].compactMap { $0 } } + @MainActor static func fromJSValue( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> RootTypeDefinition { self.init( queryType: .fromJSValue(jsValue["queryType"], bridge: bridge), @@ -112,8 +114,8 @@ public final class CompilationResult: JavaScriptObjectDecodable { public let isLocalCacheMutation: Bool public let moduleImports: OrderedSet - - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init( name: jsValue["name"], operationType: jsValue["operationType"], @@ -202,7 +204,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { static func fromJSValue( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> CompilationResult.VariableDefinition { return self.init( name: jsValue["name"], @@ -239,7 +241,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { public let overrideAsLocalCacheMutation: Bool - init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.name = jsValue["name"] self.directives = .fromJSValue(jsValue["directives"], bridge: bridge) self.type = .fromJSValue(jsValue["typeCondition"], bridge: bridge) @@ -319,7 +321,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { self.selections = selections } - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init( parentType: .fromJSValue(jsValue["parentType"], bridge: bridge), selections: .fromJSValue(jsValue["selections"], bridge: bridge) @@ -356,7 +358,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { public let deferCondition: DeferCondition? - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init( selectionSet: .fromJSValue(jsValue["selectionSet"], bridge: bridge), inclusionConditions: jsValue["inclusionConditions"], @@ -403,7 +405,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { @inlinable public var parentType: GraphQLCompositeType { fragment.type } - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init( fragment: .fromJSValue(jsValue["fragment"], bridge: bridge), inclusionConditions: jsValue["inclusionConditions"], @@ -445,7 +447,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { case inlineFragment(InlineFragment) case fragmentSpread(FragmentSpread) - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { precondition(jsValue.isObject, "Expected JavaScript object but found: \(jsValue)") let kind: String = jsValue["kind"].toString() @@ -533,7 +535,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { self.documentation = documentation } - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init( name: jsValue["name"], alias: jsValue["alias"], @@ -584,7 +586,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { public let deprecationReason: String? - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init( name: jsValue["name"], type: .fromJSValue(jsValue["type"], bridge: bridge), @@ -612,7 +614,7 @@ public final class CompilationResult: JavaScriptObjectDecodable { public let arguments: [Argument]? - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init( name: jsValue["name"], arguments: .fromJSValue(jsValue["arguments"], bridge: bridge) diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLError.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLError.swift index 50147e281..1205ef4cb 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLError.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLError.swift @@ -5,12 +5,12 @@ import JavaScriptCore /// Corresponds to [graphql-js/GraphQLError](https://graphql.org/graphql-js/error/#graphqlerror) /// You can get error details if you need them, or call `error.logLines` to get errors in a format /// that lets Xcode show inline errors. -public final class GraphQLError: JavaScriptError { +public final class GraphQLError: JavaScriptError, @unchecked Sendable { private let source: GraphQLSource? /// The source locations associated with this error. public let sourceLocations: [GraphQLSourceLocation]? - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { // When the error is a “Too many validation errors” error, there is no `source` in the error // object. This was causing a crash. Check for this to be undefined to avoid this edge case. let sourceValue = jsValue["source"] @@ -27,10 +27,11 @@ public final class GraphQLError: JavaScriptError { super.init(jsValue, bridge: bridge) } + @MainActor private static func computeSourceLocations( for source: GraphQLSource, from jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> [GraphQLSourceLocation]? { guard let locations = (jsValue["locations"]).toArray() as? [[String: Int]] else { return nil @@ -75,10 +76,10 @@ public final class GraphQLError: JavaScriptError { } /// A GraphQL schema validation error. This wraps one or more underlying validation errors. -public class GraphQLSchemaValidationError: JavaScriptError { +public class GraphQLSchemaValidationError: JavaScriptError, @unchecked Sendable { public let validationErrors: [GraphQLError] - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.validationErrors = .fromJSValue(jsValue["validationErrors"], bridge: bridge) super.init(jsValue, bridge: bridge) } diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLJSFrontend.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLJSFrontend.swift index 339ae6209..c3784e678 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLJSFrontend.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLJSFrontend.swift @@ -1,75 +1,76 @@ import Foundation -import JavaScriptCore +@preconcurrency import JavaScriptCore +@MainActor public final class GraphQLJSFrontend { private let bridge: JavaScriptBridge - private let library: JavaScriptObject - private let sourceConstructor: JavaScriptObject + private let library: any JavaScriptObject & Sendable + private let sourceConstructor: any JavaScriptObject & Sendable - public init() async throws { - let bridge = try await JavaScriptBridge() + public init() throws { + let bridge = try JavaScriptBridge() self.bridge = bridge - try await bridge.throwingJavaScriptErrorIfNeeded { + try bridge.throwingJavaScriptErrorIfNeeded { bridge in bridge.context.evaluateScript(ApolloCodegenFrontendBundle) } - self.library = await bridge + self.library = bridge .getReferenceOrInitialize(bridge.context.globalObject["ApolloCodegenFrontend"]) - await bridge.register(GraphQLSource.self, forJavaScriptClass: "Source", from: library) - await bridge.register(GraphQLError.self, from: library) - await bridge.register(GraphQLSchemaValidationError.self, from: library) - await bridge.register(GraphQLSchema.self, from: library) - await bridge.register(GraphQLScalarType.self, from: library) - await bridge.register(GraphQLEnumType.self, from: library) - await bridge.register(GraphQLInputObjectType.self, from: library) - await bridge.register(GraphQLObjectType.self, from: library) - await bridge.register(GraphQLInterfaceType.self, from: library) - await bridge.register(GraphQLUnionType.self, from: library) - - self.sourceConstructor = await bridge.getReferenceOrInitialize(library["Source"]) + bridge.register(GraphQLSource.self, forJavaScriptClass: "Source", from: library) + bridge.register(GraphQLError.self, from: library) + bridge.register(GraphQLSchemaValidationError.self, from: library) + bridge.register(GraphQLSchema.self, from: library) + bridge.register(GraphQLScalarType.self, from: library) + bridge.register(GraphQLEnumType.self, from: library) + bridge.register(GraphQLInputObjectType.self, from: library) + bridge.register(GraphQLObjectType.self, from: library) + bridge.register(GraphQLInterfaceType.self, from: library) + bridge.register(GraphQLUnionType.self, from: library) + + self.sourceConstructor = bridge.getReferenceOrInitialize(library["Source"]) } /// Load a schema by parsing an introspection result. - public func loadSchema(from sources: [GraphQLSource]) async throws -> GraphQLSchema { - return try await library.call("loadSchemaFromSources", with: sources) + public func loadSchema(from sources: [GraphQLSource]) throws -> GraphQLSchema { + return try library.call("loadSchemaFromSources", with: sources) } /// Take a loaded GQL schema and print it as SDL. - public func printSchemaAsSDL(schema: GraphQLSchema) async throws -> String { - return try await library.call("printSchemaToSDL", with: schema) + public func printSchemaAsSDL(schema: GraphQLSchema) throws -> String { + return try library.call("printSchemaToSDL", with: schema) } /// Create a `GraphQLSource` object from a string. - public func makeSource(_ body: String, filePath: String) async throws -> GraphQLSource { - return try await sourceConstructor.construct(with: body, filePath) + public func makeSource(_ body: String, filePath: String) throws -> GraphQLSource { + return try sourceConstructor.construct(with: body, filePath) } /// Create a `GraphQLSource` object by reading from a file. - public func makeSource(from fileURL: URL) async throws -> GraphQLSource { + public func makeSource(from fileURL: URL) throws -> GraphQLSource { precondition(fileURL.isFileURL) let body = try String(contentsOf: fileURL) - return try await makeSource(body, filePath: fileURL.path) + return try makeSource(body, filePath: fileURL.path) } /// Parses a GraphQL document from a source, returning a reference to the parsed AST that can be passed on to validation and compilation. /// Syntax errors will result in throwing a `GraphQLError`. - public func parseDocument(_ source: GraphQLSource) async throws -> GraphQLDocument { - return try await library.call("parseOperationDocument", with: source) + public func parseDocument(_ source: GraphQLSource) throws -> GraphQLDocument { + return try library.call("parseOperationDocument", with: source) } /// Parses a GraphQL document from a file, returning a reference to the parsed AST that can be passed on to validation and compilation. /// Syntax errors will result in throwing a `GraphQLError`. - public func parseDocument(from fileURL: URL) async throws -> GraphQLDocument { - let source = try await makeSource(from: fileURL) - return try await parseDocument(source) + public func parseDocument(from fileURL: URL) throws -> GraphQLDocument { + let source = try makeSource(from: fileURL) + return try parseDocument(source) } /// Validation and compilation take a single document, but you can merge documents, and operations and fragments will remember their source. - public func mergeDocuments(_ documents: [GraphQLDocument]) async throws -> GraphQLDocument { - return try await library.call("mergeDocuments", with: documents) + public func mergeDocuments(_ documents: [GraphQLDocument]) throws -> GraphQLDocument { + return try library.call("mergeDocuments", with: documents) } /// Validate a GraphQL document and return any validation errors as `GraphQLError`s. @@ -77,12 +78,12 @@ public final class GraphQLJSFrontend { schema: GraphQLSchema, document: GraphQLDocument, validationOptions: ValidationOptions - ) async throws -> [GraphQLError] { - return try await library.call( + ) throws -> [GraphQLError] { + return try library.call( "validateDocument", with: schema, document, - ValidationOptions.Bridged(from: validationOptions, bridge: self.bridge) + ValidationOptions.Bridged(validationOptions, bridge: self.bridge) ) } @@ -93,14 +94,14 @@ public final class GraphQLJSFrontend { experimentalLegacySafelistingCompatibleOperations: Bool = false, reduceGeneratedSchemaTypes: Bool, validationOptions: ValidationOptions - ) async throws -> CompilationResult { - return try await library.call( + ) throws -> CompilationResult { + return try library.call( "compileDocument", with: schema, document, experimentalLegacySafelistingCompatibleOperations, reduceGeneratedSchemaTypes, - ValidationOptions.Bridged(from: validationOptions, bridge: self.bridge) + ValidationOptions.Bridged(validationOptions, bridge: self.bridge) ) } } diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSchema.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSchema.swift index d16a0c0bc..b2e0cbca5 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSchema.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSchema.swift @@ -6,11 +6,12 @@ import OrderedCollections // and are partially described in https://graphql.org/graphql-js/type/ /// A GraphQL schema. -public final class GraphQLSchema: JavaScriptObject { +public final class GraphQLSchema: JavaScriptObject, @unchecked Sendable { // MARK: Methods - func getType(named typeName: String) async throws -> (GraphQLNamedType)? { - try await invokeMethod("getType", with: typeName) + @MainActor + func getType(named typeName: String) throws -> (GraphQLNamedType)? { + try invokeMethod("getType", with: typeName) } } @@ -27,13 +28,13 @@ public class GraphQLNamedType: required init( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) { self.name = .init(schemaName: jsValue["name"]) self.documentation = jsValue["description"] } - func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { } + func finalize(_ jsValue: JSValue, bridge: JavaScriptBridge) { } /// Initializer to be used for creating mock objects in tests only. init( @@ -44,9 +45,10 @@ public class GraphQLNamedType: self.documentation = documentation } + @MainActor static func initializeNewObject( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> Self { return self.init(jsValue, bridge: bridge) } @@ -64,7 +66,7 @@ public class GraphQLNamedType: } } -public final class GraphQLScalarType: GraphQLNamedType { +public final class GraphQLScalarType: GraphQLNamedType, @unchecked Sendable { public let specifiedByURL: String? public var isCustomScalar: Bool { @@ -78,7 +80,7 @@ public final class GraphQLScalarType: GraphQLNamedType { } } - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.specifiedByURL = jsValue["specifiedByUrl"] super.init(jsValue, bridge: bridge) } @@ -95,14 +97,14 @@ public final class GraphQLScalarType: GraphQLNamedType { } -public final class GraphQLEnumType: GraphQLNamedType { +public final class GraphQLEnumType: GraphQLNamedType, @unchecked Sendable { public private(set) var values: [GraphQLEnumValue]! - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { super.init(jsValue, bridge: bridge) } - override func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + override func finalize(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.values = try! bridge.invokeMethod("getValues", on: jsValue) } @@ -139,7 +141,7 @@ public struct GraphQLEnumValue: JavaScriptObjectDecodable, GraphQLNamedItem { static func fromJSValue( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> GraphQLEnumValue { self.init( name: .init(schemaName: jsValue["name"]), @@ -151,17 +153,17 @@ public struct GraphQLEnumValue: JavaScriptObjectDecodable, GraphQLNamedItem { public typealias GraphQLInputFieldDictionary = OrderedDictionary -public final class GraphQLInputObjectType: GraphQLNamedType { +public final class GraphQLInputObjectType: GraphQLNamedType, @unchecked Sendable { public private(set) var fields: GraphQLInputFieldDictionary! public let isOneOf: Bool - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.isOneOf = jsValue["isOneOf"] super.init(jsValue, bridge: bridge) } - override func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + override func finalize(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.fields = try! bridge.invokeMethod("getFields", on: jsValue) } @@ -191,7 +193,7 @@ public class GraphQLInputField: JavaScriptObjectDecodable, GraphQLNamedItem { static func fromJSValue( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> Self { self.init( name: .init(schemaName: jsValue["name"]), @@ -217,7 +219,7 @@ public class GraphQLInputField: JavaScriptObjectDecodable, GraphQLNamedItem { } } -public class GraphQLCompositeType: GraphQLNamedType { +public class GraphQLCompositeType: GraphQLNamedType, @unchecked Sendable { public override var debugDescription: String { "Type - \(name)" } @@ -233,7 +235,7 @@ public extension GraphQLInterfaceImplementingType { } } -public final class GraphQLObjectType: GraphQLCompositeType, GraphQLInterfaceImplementingType { +public final class GraphQLObjectType: GraphQLCompositeType, GraphQLInterfaceImplementingType, @unchecked Sendable { public private(set) var fields: [String: GraphQLField]! @@ -255,11 +257,11 @@ public final class GraphQLObjectType: GraphQLCompositeType, GraphQLInterfaceImpl super.init(name: name, documentation: documentation) } - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { super.init(jsValue, bridge: bridge) } - override func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + override func finalize(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.fields = try! bridge.invokeMethod("getFields", on: jsValue) self.interfaces = try! bridge.invokeMethod("getInterfaces", on: jsValue) self.keyFields = jsValue["_apolloKeyFields"] @@ -270,10 +272,10 @@ public final class GraphQLObjectType: GraphQLCompositeType, GraphQLInterfaceImpl } } -public class GraphQLAbstractType: GraphQLCompositeType { +public class GraphQLAbstractType: GraphQLCompositeType, @unchecked Sendable { } -public final class GraphQLInterfaceType: GraphQLAbstractType, GraphQLInterfaceImplementingType { +public final class GraphQLInterfaceType: GraphQLAbstractType, GraphQLInterfaceImplementingType, @unchecked Sendable { public private(set) var fields: [String: GraphQLField]! @@ -299,11 +301,11 @@ public final class GraphQLInterfaceType: GraphQLAbstractType, GraphQLInterfaceIm super.init(name: name, documentation: documentation) } - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { super.init(jsValue, bridge: bridge) } - override func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + override func finalize(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.fields = try! bridge.invokeMethod("getFields", on: jsValue) self.interfaces = try! bridge.invokeMethod("getInterfaces", on: jsValue) self.keyFields = jsValue["_apolloKeyFields"] @@ -315,10 +317,10 @@ public final class GraphQLInterfaceType: GraphQLAbstractType, GraphQLInterfaceIm } } -public final class GraphQLUnionType: GraphQLAbstractType { +public final class GraphQLUnionType: GraphQLAbstractType, @unchecked Sendable { public let types: [GraphQLObjectType] - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.types = try! bridge.invokeMethod("getTypes", on: jsValue) super.init(jsValue, bridge: bridge) } @@ -368,7 +370,7 @@ public final class GraphQLField: static func fromJSValue( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> GraphQLField { self.init( name: jsValue["name"], @@ -420,7 +422,7 @@ public struct GraphQLFieldArgument: JavaScriptObjectDecodable, Sendable, Hashabl static func fromJSValue( _ jsValue: JSValue, - bridge: isolated JavaScriptBridge + bridge: JavaScriptBridge ) -> GraphQLFieldArgument { self.init( name: jsValue["name"], diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSource.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSource.swift index 1d4bd917d..7adec5888 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSource.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLSource.swift @@ -3,12 +3,12 @@ import JavaScriptCore /// A representation of source input to GraphQL parsing. /// Corresponds to https://github.com/graphql/graphql-js/blob/master/src/language/source.js -public final class GraphQLSource: JavaScriptObject { +public final class GraphQLSource: JavaScriptObject, @unchecked Sendable { public let filePath: String public let body: String - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.filePath = jsValue["name"] self.body = jsValue["body"] super.init(jsValue, bridge: bridge) @@ -17,7 +17,7 @@ public final class GraphQLSource: JavaScriptObject { } /// Represents a location in a GraphQL source file. -public struct GraphQLSourceLocation { +public struct GraphQLSourceLocation: Sendable { let filePath: String? let lineNumber: Int @@ -32,30 +32,31 @@ public struct GraphQLSourceLocation { // `GraphQLDocument`. /// An AST node. -public class ASTNode: JavaScriptObject { +public class ASTNode: JavaScriptObject, @unchecked Sendable { public let kind: String public let source: GraphQLSource? public var filePath: String? { source?.filePath } - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.kind = jsValue["kind"] self.source = .fromJSValue(jsValue["loc"]["source"], bridge: bridge) super.init(jsValue, bridge: bridge) } - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init(jsValue, bridge: bridge) } } /// A parsed GraphQL document. -public final class GraphQLDocument: ASTNode { +public final class GraphQLDocument: ASTNode, @unchecked Sendable { public let definitions: [ASTNode] - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.definitions = .fromJSValue(jsValue["definitions"], bridge: bridge) super.init(jsValue, bridge: bridge) diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLType.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLType.swift index f890abc94..6d91a93da 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLType.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/GraphQLType.swift @@ -63,7 +63,7 @@ extension GraphQLType: CustomDebugStringConvertible { } extension GraphQLType: JavaScriptObjectDecodable { - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { precondition(jsValue.isObject, "Expected JavaScript object but found: \(jsValue)") let tag = jsValue[jsValue.context.globalObject["Symbol"]["toStringTag"]].toString() diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/JavaScriptBridge.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/JavaScriptBridge.swift index 0f5a3578b..7574e52da 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/JavaScriptBridge.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/JavaScriptBridge.swift @@ -1,4 +1,4 @@ -import JavaScriptCore +@preconcurrency import JavaScriptCore import OrderedCollections // MARK: - JavaScriptError @@ -14,13 +14,15 @@ public class JavaScriptError: JavaScriptObjectDecodable, Error, @unchecked Senda public let message: String? public let stack: String? - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + @MainActor + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { self.name = jsValue["name"] self.message = jsValue["message"] self.stack = jsValue["stack"] } - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init(jsValue, bridge: bridge) } } @@ -46,7 +48,8 @@ protocol JavaScriptReferencedObject: AnyObject, JavaScriptObjectDecodable { /// - Warning: This function should not be called directly to initialize an instance. Instead use /// `fromJSValue(_:bridge)`, which will return the existing object if it has already been /// initialized or call this and then `finalize(_: bridge:)` to initialize and store the object in the `bridge`. - init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) + @MainActor + init(_ jsValue: JSValue, bridge: JavaScriptBridge) /// This function will be after being initialized by a `JavaScriptBridge` to allow the object to /// complete setup of its values. @@ -54,15 +57,18 @@ protocol JavaScriptReferencedObject: AnyObject, JavaScriptObjectDecodable { /// Some properties of the object may be self-referential. In order to avoid infinite recursion, /// while initializing these objects, these properties must be set up after the object has been /// initialized and stored by the `JavaScriptBridge`. - func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) + @MainActor + func finalize(_ jsValue: JSValue, bridge: JavaScriptBridge) } extension JavaScriptReferencedObject { - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { bridge.getReferenceOrInitialize(jsValue) } - func finalize(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { } + @MainActor + func finalize(_ jsValue: JSValue, bridge: JavaScriptBridge) { } } // MARK: - JavaScriptCallable @@ -80,25 +86,28 @@ extension JavaScriptCallable { // MARK: Invoke Method + @MainActor func invokeMethod( _ methodName: String, with arguments: any JavaScriptValueConvertible... - ) async throws -> JSValue { - try await bridge.invokeMethod(methodName, on: jsValue, with: arguments) + ) throws -> JSValue { + try bridge.invokeMethod(methodName, on: jsValue, with: arguments) } + @MainActor func invokeMethod( _ methodName: String, with arguments: any JavaScriptValueConvertible... - ) async throws -> Decodable { - return Decodable.init(try await bridge.invokeMethod(methodName, on: jsValue, with: arguments)) + ) throws -> Decodable { + return Decodable.init(try bridge.invokeMethod(methodName, on: jsValue, with: arguments)) } + @MainActor func invokeMethod( _ methodName: String, with arguments: any JavaScriptValueConvertible... - ) async throws -> Decodable { - return await Decodable.fromJSValue( + ) throws -> Decodable { + return Decodable.fromJSValue( try bridge.invokeMethod(methodName, on: jsValue, with: arguments), bridge: self.bridge ) @@ -106,25 +115,28 @@ extension JavaScriptCallable { // MARK: Call Function + @MainActor func call( _ functionName: String, with arguments: any JavaScriptValueConvertible... - ) async throws -> JSValue { - try await bridge.call(functionName, on: jsValue, with: arguments) + ) throws -> JSValue { + try bridge.call(functionName, on: jsValue, with: arguments) } + @MainActor func call( _ functionName: String, with arguments: any JavaScriptValueConvertible... - ) async throws -> Decodable { - return Decodable.init(try await bridge.call(functionName, on: jsValue, with: arguments)) + ) throws -> Decodable { + return Decodable.init(try bridge.call(functionName, on: jsValue, with: arguments)) } + @MainActor func call( _ functionName: String, with arguments: any JavaScriptValueConvertible... - ) async throws -> Decodable { - return await Decodable.fromJSValue( + ) throws -> Decodable { + return Decodable.fromJSValue( try bridge.call(functionName, on: jsValue, with: arguments), bridge: self.bridge ) @@ -132,10 +144,11 @@ extension JavaScriptCallable { // MARK: Construct Object + @MainActor func construct( with arguments: any JavaScriptValueConvertible... - ) async throws -> Wrapper { - return try await bridge.construct(from: jsValue, with: arguments) + ) throws -> Wrapper { + return try bridge.construct(from: jsValue, with: arguments) } // MARK: Get Property @@ -148,10 +161,9 @@ extension JavaScriptCallable { return Value.init(jsValue[property]) } + @MainActor subscript(property: Any) -> Object { - get async { - return await Object.fromJSValue(jsValue[property], bridge: bridge) - } + return Object.fromJSValue(jsValue[property], bridge: bridge) } } @@ -164,11 +176,13 @@ public class JavaScriptObject: JavaScriptReferencedObject, JavaScriptCallable { let jsValue: JSValue let bridge: JavaScriptBridge - static func initializeNewObject(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func initializeNewObject(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { self.init(jsValue, bridge: bridge) } - required init(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) { + @MainActor + required init(_ jsValue: JSValue, bridge: JavaScriptBridge) { precondition(jsValue.isObject) self.jsValue = jsValue @@ -185,17 +199,14 @@ extension JavaScriptObject: CustomDebugStringConvertible { // MARK: - JavaScriptBridge -/// The JavaScript bridge is responsible for converting values to and from type-safe wrapper objects. It also ensures exceptions thrown from JavaScript wrapped and rethrown. -actor JavaScriptBridge { - - nonisolated var unownedExecutor: UnownedSerialExecutor { - MainActor.sharedUnownedExecutor - } +/// The JavaScript bridge is responsible for converting values to and from type-safe wrapper objects. It also ensures +/// exceptions thrown from JavaScript wrapped and rethrown. +@MainActor +final class JavaScriptBridge { public enum Error: Swift.Error { case failedToCreateJSContext case unrecognizedJavaScriptErrorThrown(JSValue) - } private struct WeakRef { @@ -226,7 +237,7 @@ actor JavaScriptBridge { /// JavaScript object as ineligible for garbage collection.) private var wrapperMap: [ObjectIdentifier /* JSValue */: WeakRef] = [:] - init() async throws { + init() throws { guard let context = JSContext(virtualMachine: virtualMachine) else { throw Error.failedToCreateJSContext } @@ -333,8 +344,8 @@ actor JavaScriptBridge { on jsValue: JSValue, with arguments: [any JavaScriptValueConvertible] ) throws -> JSValue { - return try throwingJavaScriptErrorIfNeeded { - jsValue.invokeMethod(methodName, withArguments: unwrap(arguments)) + return try throwingJavaScriptErrorIfNeeded { `self` in + jsValue.invokeMethod(methodName, withArguments: self.unwrap(arguments)) } } @@ -372,12 +383,12 @@ actor JavaScriptBridge { on jsValue: JSValue, with arguments: [any JavaScriptValueConvertible] ) throws -> JSValue { - return try throwingJavaScriptErrorIfNeeded { + return try throwingJavaScriptErrorIfNeeded { `self` in let function = jsValue[functionName] precondition(!function.isUndefined, "Function \(functionName) is undefined") - return function.call(withArguments: unwrap(arguments))! + return function.call(withArguments: self.unwrap(arguments))! } } @@ -416,9 +427,9 @@ actor JavaScriptBridge { from jsValue: JSValue, with arguments: [any JavaScriptValueConvertible] ) throws -> Wrapper { - return try throwingJavaScriptErrorIfNeeded { + return try throwingJavaScriptErrorIfNeeded { `self` in return Wrapper.fromJSValue( - jsValue.construct(withArguments: unwrap(arguments)), + jsValue.construct(withArguments: self.unwrap(arguments)), bridge: self) } } @@ -426,7 +437,7 @@ actor JavaScriptBridge { // MARK: Error Handling @discardableResult func throwingJavaScriptErrorIfNeeded( - body: () -> ReturnValue + body: @MainActor (JavaScriptBridge) -> ReturnValue ) throws -> ReturnValue { let previousExceptionHandler = context.exceptionHandler @@ -435,7 +446,7 @@ actor JavaScriptBridge { exception = thrownException } - let result = body() + let result = body(self) // Errors thrown from JavaScript are stored on the context and ignored by default. // To surface these to callers, we wrap them in a `JavaScriptError` and throw. @@ -467,11 +478,13 @@ actor JavaScriptBridge { /// A type that can decode itself from a JavaScript value that represents an object. protocol JavaScriptObjectDecodable { - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self } extension Optional: JavaScriptObjectDecodable where Wrapped: JavaScriptObjectDecodable { - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { if jsValue.isUndefined || jsValue.isNull { return .none } else { @@ -481,19 +494,22 @@ extension Optional: JavaScriptObjectDecodable where Wrapped: JavaScriptObjectDec } extension Array: JavaScriptObjectDecodable where Element: JavaScriptObjectDecodable { - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { jsValue.toArray { Element.fromJSValue($0, bridge: bridge) } } } extension Dictionary: JavaScriptObjectDecodable where Key == String, Value: JavaScriptObjectDecodable { - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { jsValue.toDictionary { Value.fromJSValue($0, bridge: bridge) } } } extension OrderedDictionary: JavaScriptObjectDecodable where Key == String, Value: JavaScriptObjectDecodable { - static func fromJSValue(_ jsValue: JSValue, bridge: isolated JavaScriptBridge) -> Self { + @MainActor + static func fromJSValue(_ jsValue: JSValue, bridge: JavaScriptBridge) -> Self { jsValue.toOrderedDictionary { Value.fromJSValue($0, bridge: bridge) } } } diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/ValidationOptions.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/ValidationOptions.swift index f93445ad8..202476f45 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/ValidationOptions.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/ValidationOptions.swift @@ -1,9 +1,9 @@ import Foundation import JavaScriptCore -public struct ValidationOptions { +public struct ValidationOptions: Sendable { - public struct DisallowedFieldNames { + public struct DisallowedFieldNames: Sendable { public let allFields: Set public let entity: Set public let entityList: Set @@ -41,8 +41,9 @@ public struct ValidationOptions { self.disallowedInputParameterNames = disallowedInputParameterNames } - class Bridged: JavaScriptObject { - convenience init(from options: ValidationOptions, bridge: isolated JavaScriptBridge) { + final class Bridged: JavaScriptObject { + @MainActor + convenience init(_ options: ValidationOptions, bridge: JavaScriptBridge) { let jsValue = JSValue(newObjectIn: bridge.context) jsValue?.setValue( diff --git a/apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift b/apollo-ios-codegen/Sources/IR/ComputedSelectionSet.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+ComputedSelectionSet.swift rename to apollo-ios-codegen/Sources/IR/ComputedSelectionSet.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+Definition.swift b/apollo-ios-codegen/Sources/IR/Definition.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+Definition.swift rename to apollo-ios-codegen/Sources/IR/Definition.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+DefinitionEntityStorage.swift b/apollo-ios-codegen/Sources/IR/DefinitionEntityStorage.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+DefinitionEntityStorage.swift rename to apollo-ios-codegen/Sources/IR/DefinitionEntityStorage.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+DirectSelections.swift b/apollo-ios-codegen/Sources/IR/DirectSelections.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+DirectSelections.swift rename to apollo-ios-codegen/Sources/IR/DirectSelections.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+Entity.swift b/apollo-ios-codegen/Sources/IR/Entity.swift similarity index 92% rename from apollo-ios-codegen/Sources/IR/IR+Entity.swift rename to apollo-ios-codegen/Sources/IR/Entity.swift index f3ea76dd2..da60496b2 100644 --- a/apollo-ios-codegen/Sources/IR/IR+Entity.swift +++ b/apollo-ios-codegen/Sources/IR/Entity.swift @@ -5,11 +5,11 @@ import Utilities /// /// Multiple `SelectionSet`s may select fields on the same `Entity`. All `SelectionSet`s that will /// be selected on the same object share the same `Entity`. -public class Entity { +public final class Entity: Sendable { /// Represents the location within a GraphQL definition (operation or fragment) of an `Entity`. - public struct Location: Hashable { - public enum SourceDefinition: Hashable { + public struct Location: Hashable, Sendable { + public enum SourceDefinition: Hashable, Sendable { case operation(CompilationResult.OperationDefinition) case namedFragment(CompilationResult.FragmentDefinition) @@ -21,7 +21,7 @@ public class Entity { } } - public struct FieldComponent: Hashable { + public struct FieldComponent: Hashable, Sendable { public let name: String public let type: GraphQLType @@ -71,7 +71,7 @@ public class Entity { /// The selections that are selected for the entity across all type scopes in the operation. /// Represented as a tree. - let selectionTree: EntitySelectionTree + nonisolated(unsafe) let selectionTree: EntitySelectionTree /// The location within a GraphQL definition (operation or fragment) where the `Entity` is /// located. diff --git a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift b/apollo-ios-codegen/Sources/IR/EntitySelectionTree.swift similarity index 99% rename from apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift rename to apollo-ios-codegen/Sources/IR/EntitySelectionTree.swift index aa5c2cbb0..5d751f272 100644 --- a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift +++ b/apollo-ios-codegen/Sources/IR/EntitySelectionTree.swift @@ -206,7 +206,6 @@ class EntitySelectionTree { self.child = .selections(entitySelections) } - func mergeSelections( matchingScopePath entityPathNode: LinkedList.Node, entityTypeScopePath: LinkedList.Node, diff --git a/apollo-ios-codegen/Sources/IR/IR+FieldCollector.swift b/apollo-ios-codegen/Sources/IR/FieldCollector.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+FieldCollector.swift rename to apollo-ios-codegen/Sources/IR/FieldCollector.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+Fields.swift b/apollo-ios-codegen/Sources/IR/Fields.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+Fields.swift rename to apollo-ios-codegen/Sources/IR/Fields.swift diff --git a/apollo-ios-codegen/Sources/IR/IRBuilder.swift b/apollo-ios-codegen/Sources/IR/IRBuilder.swift index e77ebfa89..ed8f76d3e 100644 --- a/apollo-ios-codegen/Sources/IR/IRBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IRBuilder.swift @@ -1,7 +1,7 @@ import OrderedCollections import GraphQLCompiler -public class IRBuilder { +public final class IRBuilder: Sendable { public let compilationResult: CompilationResult @@ -60,7 +60,7 @@ public class IRBuilder { var fragmentCache: [String: CacheEntry] = [:] - func getFragment(named name: String, builder: @escaping () async -> NamedFragment) async -> NamedFragment { + func getFragment(named name: String, builder: @Sendable @escaping () async -> NamedFragment) async -> NamedFragment { if let cachedFragment = fragmentCache[name] { switch cachedFragment { case let .ready(fragment): return fragment diff --git a/apollo-ios-codegen/Sources/IR/IR+InclusionConditions.swift b/apollo-ios-codegen/Sources/IR/InclusionConditions.swift similarity index 96% rename from apollo-ios-codegen/Sources/IR/IR+InclusionConditions.swift rename to apollo-ios-codegen/Sources/IR/InclusionConditions.swift index e6909755c..5eeb045d0 100644 --- a/apollo-ios-codegen/Sources/IR/IR+InclusionConditions.swift +++ b/apollo-ios-codegen/Sources/IR/InclusionConditions.swift @@ -4,7 +4,7 @@ import TemplateString /// A condition representing an `@include` or `@skip` directive to determine if a field /// or fragment should be included. -public struct InclusionCondition: Hashable, CustomDebugStringConvertible { +public struct InclusionCondition: Hashable, Sendable, CustomDebugStringConvertible { /// The name of variable used to determine if the inclusion condition is met. public let variable: String @@ -40,7 +40,7 @@ public struct InclusionCondition: Hashable, CustomDebugStringConvertible { } -public struct InclusionConditions: Collection, Hashable, CustomDebugStringConvertible { +public struct InclusionConditions: Collection, Hashable, Sendable, CustomDebugStringConvertible { public typealias Element = InclusionCondition @@ -174,7 +174,7 @@ public struct InclusionConditions: Collection, Hashable, CustomDebugStringConver // MARK: - AnyOf -public struct AnyOf: Hashable { +public struct AnyOf: Hashable, Sendable { public private(set) var elements: OrderedSet init(_ element: T) { diff --git a/apollo-ios-codegen/Sources/IR/IR+InlineFragmentSpread.swift b/apollo-ios-codegen/Sources/IR/InlineFragmentSpread.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+InlineFragmentSpread.swift rename to apollo-ios-codegen/Sources/IR/InlineFragmentSpread.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift b/apollo-ios-codegen/Sources/IR/MergedSelections.swift similarity index 98% rename from apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift rename to apollo-ios-codegen/Sources/IR/MergedSelections.swift index 725095a3e..588408acd 100644 --- a/apollo-ios-codegen/Sources/IR/IR+MergedSelections.swift +++ b/apollo-ios-codegen/Sources/IR/MergedSelections.swift @@ -53,7 +53,7 @@ extension MergedSelections { /// ``MergedSelections`` can compute which selections from a selection set's parents, sibling /// inline fragments, and named fragment spreads will also be included on the response object, /// given the selection set's ``SelectionSet/TypeInfo``. - public struct MergingStrategy: OptionSet, Hashable, CustomStringConvertible { + public struct MergingStrategy: OptionSet, Hashable, Sendable, CustomStringConvertible { /// Merges fields and fragment accessors from the selection set's direct ancestors. public static let ancestors = MergingStrategy(rawValue: 1 << 0) diff --git a/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift b/apollo-ios-codegen/Sources/IR/NamedFragment.swift similarity index 90% rename from apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift rename to apollo-ios-codegen/Sources/IR/NamedFragment.swift index a94a7a0a0..23bb2b811 100644 --- a/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift +++ b/apollo-ios-codegen/Sources/IR/NamedFragment.swift @@ -1,9 +1,9 @@ import GraphQLCompiler import OrderedCollections -public class NamedFragment: Definition, Hashable, CustomDebugStringConvertible { +public final class NamedFragment: Definition, Sendable, Hashable, CustomDebugStringConvertible { public let definition: CompilationResult.FragmentDefinition - public let rootField: EntityField + nonisolated(unsafe) public let rootField: EntityField /// All of the fragments that are referenced by this fragment's selection set. public let referencedFragments: OrderedSet @@ -13,7 +13,7 @@ public class NamedFragment: Definition, Hashable, CustomDebugStringConvertible { /// /// - Note: The FieldPath for an entity within a fragment will begin with a path component /// with the fragment's name and type. - public let entityStorage: DefinitionEntityStorage + nonisolated(unsafe) public let entityStorage: DefinitionEntityStorage /// `True` if any selection set, or nested selection set, within the fragment contains any /// fragment marked with the `@defer` directive. diff --git a/apollo-ios-codegen/Sources/IR/IR+NamedFragmentSpread.swift b/apollo-ios-codegen/Sources/IR/NamedFragmentSpread.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+NamedFragmentSpread.swift rename to apollo-ios-codegen/Sources/IR/NamedFragmentSpread.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+Operation.swift b/apollo-ios-codegen/Sources/IR/Operation.swift similarity index 88% rename from apollo-ios-codegen/Sources/IR/IR+Operation.swift rename to apollo-ios-codegen/Sources/IR/Operation.swift index 854a99cbc..4ca16cae5 100644 --- a/apollo-ios-codegen/Sources/IR/IR+Operation.swift +++ b/apollo-ios-codegen/Sources/IR/Operation.swift @@ -1,17 +1,17 @@ import GraphQLCompiler import OrderedCollections -public class Operation: Definition { +public final class Operation: Definition, Sendable { public let definition: CompilationResult.OperationDefinition /// The root field of the operation. This field must be the root query, mutation, or /// subscription field of the schema. - public let rootField: EntityField + nonisolated(unsafe) public let rootField: EntityField /// All of the fragments that are referenced by this operation's selection set. public let referencedFragments: OrderedSet - public let entityStorage: DefinitionEntityStorage + nonisolated(unsafe) public let entityStorage: DefinitionEntityStorage /// `True` if any selection set, or nested selection set, within the operation contains any /// fragment marked with the `@defer` directive. diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/RootFieldBuilder.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift rename to apollo-ios-codegen/Sources/IR/RootFieldBuilder.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+Schema.swift b/apollo-ios-codegen/Sources/IR/Schema.swift similarity index 96% rename from apollo-ios-codegen/Sources/IR/IR+Schema.swift rename to apollo-ios-codegen/Sources/IR/Schema.swift index f459630ce..6c31f0353 100644 --- a/apollo-ios-codegen/Sources/IR/IR+Schema.swift +++ b/apollo-ios-codegen/Sources/IR/Schema.swift @@ -3,7 +3,7 @@ import OrderedCollections import GraphQLCompiler import TemplateString -public final class Schema { +public final class Schema: Sendable { public let referencedTypes: ReferencedTypes public let documentation: String? @@ -15,7 +15,7 @@ public final class Schema { self.documentation = documentation } - public final class ReferencedTypes: CustomDebugStringConvertible { + public final class ReferencedTypes: Sendable, CustomDebugStringConvertible { public let allTypes: OrderedSet public let schemaRootTypes: CompilationResult.RootTypeDefinition diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/ScopeDescriptor.swift similarity index 98% rename from apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift rename to apollo-ios-codegen/Sources/IR/ScopeDescriptor.swift index de65040fa..6fe9ca308 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/ScopeDescriptor.swift @@ -3,7 +3,7 @@ import OrderedCollections import GraphQLCompiler import Utilities -public struct ScopeCondition: Hashable, CustomDebugStringConvertible { +public struct ScopeCondition: Hashable, Sendable, CustomDebugStringConvertible { public let type: GraphQLCompositeType? public let conditions: InclusionConditions? public let deferCondition: CompilationResult.DeferCondition? @@ -36,7 +36,7 @@ public typealias TypeScope = OrderedSet /// Defines the scope for an `IR.SelectionSet`. The "scope" indicates where in the entity the /// selection set is located, what types the `SelectionSet` implements, and what inclusion /// conditions it requires. -public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { +public struct ScopeDescriptor: Hashable, Sendable, CustomDebugStringConvertible { /// The parentType of the `SelectionSet`. /// diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopedSelectionSetHashable.swift b/apollo-ios-codegen/Sources/IR/ScopedSelectionSetHashable.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+ScopedSelectionSetHashable.swift rename to apollo-ios-codegen/Sources/IR/ScopedSelectionSetHashable.swift diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/SelectionSet.swift similarity index 98% rename from apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift rename to apollo-ios-codegen/Sources/IR/SelectionSet.swift index bda91d1ab..8f255bbb2 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/SelectionSet.swift @@ -3,7 +3,7 @@ import TemplateString import Utilities @dynamicMemberLookup -public class SelectionSet: Hashable, CustomDebugStringConvertible { +public final class SelectionSet: Hashable, CustomDebugStringConvertible { public class TypeInfo: Hashable, CustomDebugStringConvertible { /// The entity that the `selections` are being selected on. /// diff --git a/apollo-ios-codegen/Sources/TemplateString/TemplateString.swift b/apollo-ios-codegen/Sources/TemplateString/TemplateString.swift index c4bf70f92..be66d3b36 100644 --- a/apollo-ios-codegen/Sources/TemplateString/TemplateString.swift +++ b/apollo-ios-codegen/Sources/TemplateString/TemplateString.swift @@ -1,6 +1,6 @@ import Foundation -public struct TemplateString: ExpressibleByStringInterpolation, CustomStringConvertible { +public struct TemplateString: ExpressibleByStringInterpolation, CustomStringConvertible, Sendable { private let value: String private let lastLineWasRemoved: Bool diff --git a/apollo-ios-codegen/Sources/Utilities/Collection+ConcurrentCompactMap.swift b/apollo-ios-codegen/Sources/Utilities/Collection+ConcurrentCompactMap.swift index d9f98eace..464978ea5 100644 --- a/apollo-ios-codegen/Sources/Utilities/Collection+ConcurrentCompactMap.swift +++ b/apollo-ios-codegen/Sources/Utilities/Collection+ConcurrentCompactMap.swift @@ -1,6 +1,6 @@ import Foundation -extension Collection { +extension Collection where Element: Sendable { /// Returns an array containing the non-nil results of calling the given transformation /// asynchronously with each element of this collection. @@ -9,8 +9,8 @@ extension Collection { /// /// Though the transformations will be called concurrently, the returned array is guaranteed to /// retain the order of the initial sequence. - public func concurrentCompactMap( - _ transform: @escaping (Element) async throws -> ElementOfResult? + public func concurrentCompactMap( + _ transform: @Sendable @escaping (Element) async throws -> ElementOfResult? ) async throws -> [ElementOfResult] { try await withThrowingTaskGroup( of: (Int, ElementOfResult?).self, diff --git a/apollo-ios-codegen/Sources/Utilities/LinkedList.swift b/apollo-ios-codegen/Sources/Utilities/LinkedList.swift index d85d2bcc4..1d412b2d2 100644 --- a/apollo-ios-codegen/Sources/Utilities/LinkedList.swift +++ b/apollo-ios-codegen/Sources/Utilities/LinkedList.swift @@ -5,7 +5,7 @@ /// /// It is not optimized for prepending or insertion of items. public struct LinkedList: ExpressibleByArrayLiteral { - public class Node { + public class Node: @unchecked Sendable { public let value: T fileprivate let index: Int @@ -27,7 +27,7 @@ public struct LinkedList: ExpressibleByArrayLiteral { } } - final class HeadNode: Node { + final class HeadNode: Node, @unchecked Sendable { fileprivate var lastPointer: Node? var last: Node! { @@ -300,3 +300,5 @@ extension LinkedList.Node: CustomDebugStringConvertible { return string } } + +extension LinkedList: Sendable where T: Sendable {} diff --git a/apollo-ios-codegen/Sources/apollo-ios-cli/Apollo_iOS_CLI.swift b/apollo-ios-codegen/Sources/apollo-ios-cli/Apollo_iOS_CLI.swift index e4ad341a2..4cbb573f3 100644 --- a/apollo-ios-codegen/Sources/apollo-ios-cli/Apollo_iOS_CLI.swift +++ b/apollo-ios-codegen/Sources/apollo-ios-cli/Apollo_iOS_CLI.swift @@ -4,7 +4,7 @@ import CodegenCLI @main struct Apollo_iOS_CLI: AsyncParsableCommand { - static var configuration = CommandConfiguration( + static let configuration = CommandConfiguration( commandName: "apollo-ios-cli", abstract: "A command line utility for Apollo iOS code generation.", version: CodegenCLI.Constants.CLIVersion, diff --git a/apollo-ios-pagination/Package.resolved b/apollo-ios-pagination/Package.resolved index 9f896903d..fa9c40849 100644 --- a/apollo-ios-pagination/Package.resolved +++ b/apollo-ios-pagination/Package.resolved @@ -1,39 +1,22 @@ { + "originHash" : "6d60b71347b5e78326bc8fad112a36c4a1cc660c0b7c3a70beeeecd83f0fa016", "pins" : [ { "identity" : "apollo-ios", "kind" : "remoteSourceControl", "location" : "https://github.com/apollographql/apollo-ios.git", "state" : { - "revision" : "c3f6951be0e44ce75c820899fa272af66e77f5f6", - "version" : "1.3.3" + "revision" : "ca441856851e9ab7765387341dba76c0384f5991", + "version" : "2.0.0-alpha-1" } }, { - "identity" : "inflectorkit", + "identity" : "swift-atomics", "kind" : "remoteSourceControl", - "location" : "https://github.com/mattt/InflectorKit", + "location" : "https://github.com/apple/swift-atomics", "state" : { - "revision" : "d8cbcc04972690aaa5fc760a2b9ddb3e9f0decd7", - "version" : "1.0.0" - } - }, - { - "identity" : "sqlite.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stephencelis/SQLite.swift.git", - "state" : { - "revision" : "7a2e3cd27de56f6d396e84f63beefd0267b55ccb", - "version" : "0.14.1" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", - "version" : "1.2.2" + "revision" : "b601256eab081c0f92f059e12818ac1d4f178ff7", + "version" : "1.3.0" } }, { @@ -46,5 +29,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/apollo-ios-pagination/Package.swift b/apollo-ios-pagination/Package.swift index 3f63ca25b..1b2c576d6 100644 --- a/apollo-ios-pagination/Package.swift +++ b/apollo-ios-pagination/Package.swift @@ -1,7 +1,4 @@ -// swift-tools-version:5.9 -// -// The swift-tools-version declares the minimum version of Swift required to build this package. -// Swift 5.9 is available from Xcode 15.0. +// swift-tools-version:6.1 import PackageDescription @@ -20,7 +17,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/apollographql/apollo-ios.git", - .upToNextMajor(from: "1.2.0") + .exactItem("2.0.0-alpha-1") ), .package( url: "https://github.com/apple/swift-collections", @@ -35,7 +32,11 @@ let package = Package( .product(name: "ApolloAPI", package: "apollo-ios"), .product(name: "OrderedCollections", package: "swift-collections"), ], - swiftSettings: [.enableUpcomingFeature("ExistentialAny")] + swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableExperimentalFeature("StrictConcurrency") + ] ), - ] + ], + swiftLanguageModes: [.v6, .v5] ) diff --git a/apollo-ios-pagination/Sources/ApolloPagination/AsyncGraphQLQueryPager.swift b/apollo-ios-pagination/Sources/ApolloPagination/AsyncGraphQLQueryPager.swift deleted file mode 100644 index 52d07378f..000000000 --- a/apollo-ios-pagination/Sources/ApolloPagination/AsyncGraphQLQueryPager.swift +++ /dev/null @@ -1,175 +0,0 @@ -import Apollo -import ApolloAPI -import Combine -import Foundation - -/// Type-erases a query pager, transforming data from a generic type to a specific type, often a view model or array of view models. -public class AsyncGraphQLQueryPager: Publisher { - public typealias Failure = Never - public typealias Output = Result - let _subject: CurrentValueSubject = .init(nil) - var publisher: AnyPublisher { _subject.compactMap({ $0 }).eraseToAnyPublisher() } - @Atomic public var cancellables: Set = [] - public let pager: any AsyncPagerType - - public var canLoadNext: Bool { get async { await pager.canLoadNext } } - public var canLoadPrevious: Bool { get async { await pager.canLoadPrevious } } - - init, InitialQuery, PaginatedQuery>( - pager: Pager, - transform: @escaping (PaginationOutput) throws -> Model - ) { - self.pager = pager - Task { - let cancellable = await pager.subscribe { [weak self] result in - guard let self else { return } - let returnValue: Output - - switch result { - case let .success(output): - do { - let transformedModels = try transform(output) - returnValue = .success(transformedModels) - } catch { - returnValue = .failure(error) - } - case let .failure(error): - returnValue = .failure(error) - } - - _subject.send(returnValue) - } - _ = $cancellables.mutate { $0.insert(cancellable) } - } - } - - init, InitialQuery, PaginatedQuery>( - pager: Pager - ) where Model == PaginationOutput { - self.pager = pager - Task { - let cancellable = await pager.subscribe { [weak self] result in - guard let self else { return } - _subject.send(result) - } - _ = $cancellables.mutate { $0.insert(cancellable) } - } - } - - /// Initialize an `AsyncGraphQLQueryPager` that outputs a `PaginationOutput`. - /// - Parameters: - /// - client: Apollo client type - /// - initialQuery: The query to call for the first page of pagination. May be a separate type of query than the pagination query. - /// - watcherDispatchQueue: The queue that the underlying `GraphQLQueryWatcher`s respond on. Defaults to `main`. - /// - extractPageInfo: A user-input closure that instructs the pager on how to extract `P`, a `PaginationInfo` type, from the `Data` of either the `InitialQuery` or `PaginatedQuery`. - /// - pageResolver: A user-input closure that instructs the pager on how to create a new `PaginatedQuery` given a `PaginationInfo` and a `PaginationDirection`. - public convenience init< - P: PaginationInfo, - InitialQuery: GraphQLQuery, - PaginatedQuery: GraphQLQuery - >( - client: any ApolloClientProtocol, - initialQuery: InitialQuery, - watcherDispatchQueue: DispatchQueue = .main, - extractPageInfo: @escaping (PageExtractionData) -> P, - pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)? - ) where Model == PaginationOutput { - let pager = AsyncGraphQLQueryPagerCoordinator( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, - extractPageInfo: extractPageInfo, - pageResolver: pageResolver - ) - self.init(pager: pager) - } - - /// Initialize an `AsyncGraphQLQueryPager` that outputs a user-defined `Model`, the result of the `transform` argument. - /// - Parameters: - /// - client: Apollo client type - /// - initialQuery: The query to call for the first page of pagination. May be a separate type of query than the pagination query. - /// - watcherDispatchQueue: The queue that the underlying `GraphQLQueryWatcher`s respond on. Defaults to `main`. - /// - extractPageInfo: A user-input closure that instructs the pager on how to extract `P`, a `PaginationInfo` type, from the `Data` of either the `InitialQuery` or `PaginatedQuery`. - /// - pageResolver: A user-input closure that instructs the pager on how to create a new `PaginatedQuery` given a `PaginationInfo` and a `PaginationDirection`. - /// - transform: Transforms the `PaginationOutput` into a `Model` type. - public convenience init< - P: PaginationInfo, - InitialQuery: GraphQLQuery, - PaginatedQuery: GraphQLQuery - >( - client: any ApolloClientProtocol, - initialQuery: InitialQuery, - watcherDispatchQueue: DispatchQueue = .main, - extractPageInfo: @escaping (PageExtractionData?>) -> P, - pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)?, - transform: @escaping (PaginationOutput) throws -> Model - ) { - let pager = AsyncGraphQLQueryPagerCoordinator( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, - extractPageInfo: extractPageInfo, - pageResolver: pageResolver - ) - self.init(pager: pager, transform: transform) - } - - /// Subscribe to the results of the pager, with the management of the subscriber being stored internally to the `AnyGraphQLQueryPager`. - /// - Parameter completion: The closure to trigger when new values come in. - @available(*, deprecated, message: "Will be removed in a future version of ApolloPagination. Use the `Combine` publishers instead. If you need to dispatch to the main thread, make sure to use a `.receive(on: RunLoop.main)` as part of your `Combine` operation.") - public func subscribe(completion: @MainActor @escaping (Output) -> Void) { - let cancellable = publisher.sink { result in - Task { await completion(result) } - } - _ = $cancellables.mutate { $0.insert(cancellable) } - } - - /// Load the next page, if available. - /// - Parameters: - /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `returnCacheDataAndFetch`. - public func loadNext( - cachePolicy: CachePolicy = .returnCacheDataAndFetch - ) async throws { - try await pager.loadNext(cachePolicy: cachePolicy) - } - - /// Load the previous page, if available. - /// - Parameters: - /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `returnCacheDataAndFetch`. - public func loadPrevious( - cachePolicy: CachePolicy = .returnCacheDataAndFetch - ) async throws { - try await pager.loadPrevious(cachePolicy: cachePolicy) - } - - /// Loads all pages. Does not output a value until all pages have loaded. - /// - Parameters: - /// - fetchFromInitialPage: Pass true to begin loading from the initial page; otherwise pass false. Defaults to `true`. **NOTE**: Loading all pages with this value set to `false` requires that the initial page has already been loaded previously. - public func loadAll( - fetchFromInitialPage: Bool = true - ) async throws { - try await pager.loadAll(fetchFromInitialPage: fetchFromInitialPage) - } - - /// Discards pagination state and fetches the first page from scratch. - /// - Parameter cachePolicy: The apollo cache policy to trigger the first fetch with. Defaults to `fetchIgnoringCacheData`. - public func refetch(cachePolicy: CachePolicy = .fetchIgnoringCacheData) async { - await pager.refetch(cachePolicy: cachePolicy) - } - - /// Fetches the first page. - public func fetch() async { - await pager.fetch() - } - - /// Resets pagination state and cancels in-flight updates from the pager. - public func reset() async { - await pager.reset() - } - - public func receive( - subscriber: S - ) where S: Subscriber, Never == S.Failure, Result == S.Input { - publisher.subscribe(subscriber) - } -} diff --git a/apollo-ios-pagination/Sources/ApolloPagination/AsyncGraphQLQueryPagerCoordinator.swift b/apollo-ios-pagination/Sources/ApolloPagination/AsyncGraphQLQueryPagerCoordinator.swift deleted file mode 100644 index 5ef2eac68..000000000 --- a/apollo-ios-pagination/Sources/ApolloPagination/AsyncGraphQLQueryPagerCoordinator.swift +++ /dev/null @@ -1,483 +0,0 @@ -import Apollo -import ApolloAPI -import Combine -import Foundation -import OrderedCollections - -public protocol AsyncPagerType { - associatedtype InitialQuery: GraphQLQuery - associatedtype PaginatedQuery: GraphQLQuery - var canLoadNext: Bool { get async } - var canLoadPrevious: Bool { get async } - func reset() async - func loadPrevious(cachePolicy: CachePolicy) async throws - func loadNext(cachePolicy: CachePolicy) async throws - func loadAll(fetchFromInitialPage: Bool) async throws - func refetch(cachePolicy: CachePolicy) async - func fetch() async -} - -actor AsyncGraphQLQueryPagerCoordinator: AsyncPagerType { - typealias VariableValue = AnyHashable - - private let client: any ApolloClientProtocol - private var firstPageWatcher: GraphQLQueryWatcher? - private var nextPageWatchers: [GraphQLQueryWatcher] = [] - let initialQuery: InitialQuery - var isLoadingAll: Bool = false - var isFetching: Bool = false - let nextPageResolver: (any PaginationInfo) -> PaginatedQuery? - let previousPageResolver: (any PaginationInfo) -> PaginatedQuery? - let extractPageInfo: (PageExtractionData?>) -> any PaginationInfo - var nextPageInfo: (any PaginationInfo)? { nextPageTransformation() } - var previousPageInfo: (any PaginationInfo)? { previousPageTransformation() } - - var canLoadPages: (next: Bool, previous: Bool) { - (canLoadNext, canLoadPrevious) - } - - var publishers: ( - previousPageVarMap: Published, GraphQLResult>>.Publisher, - initialPageResult: Published?>.Publisher, - nextPageVarMap: Published, GraphQLResult>>.Publisher - ) { - return ($previousPageVarMap, $initialPageResult, $nextPageVarMap) - } - - typealias ResultType = Result, any Error> - - @Published var currentValue: ResultType? - private var queuedValue: ResultType? - - @Published var initialPageResult: GraphQLResult? - var latest: (previous: [GraphQLResult], initial: GraphQLResult, next: [GraphQLResult])? { - guard let initialPageResult else { return nil } - return ( - Array(previousPageVarMap.values).reversed(), - initialPageResult, - Array(nextPageVarMap.values) - ) - } - - /// Maps each query variable set to latest results from internal watchers. - @Published var nextPageVarMap: OrderedDictionary, GraphQLResult> = [:] - @Published var previousPageVarMap: OrderedDictionary, GraphQLResult> = [:] - private var tasks: Set> = [] - private var taskGroup: ThrowingTaskGroup? - private var watcherCallbackQueue: DispatchQueue - - /// Designated Initializer - /// - Parameters: - /// - client: Apollo Client - /// - initialQuery: The initial query that is being watched - /// - extractPageInfo: The `PageInfo` derived from `PageExtractionData` - /// - nextPageResolver: The resolver that can derive the query for loading more. This can be a different query than the `initialQuery`. - /// - onError: The callback when there is an error. - init( - client: any ApolloClientProtocol, - initialQuery: InitialQuery, - watcherDispatchQueue: DispatchQueue = .main, - extractPageInfo: @escaping (PageExtractionData?>) -> P, - pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)? - ) { - self.client = client - self.initialQuery = initialQuery - self.extractPageInfo = extractPageInfo - self.watcherCallbackQueue = watcherDispatchQueue - self.nextPageResolver = { page in - guard let page = page as? P else { return nil } - return pageResolver?(page, .next) - } - self.previousPageResolver = { page in - guard let page = page as? P else { return nil } - return pageResolver?(page, .previous) - } - } - - deinit { - nextPageWatchers.forEach { $0.cancel() } - firstPageWatcher?.cancel() - taskGroup?.cancelAll() - tasks.forEach { $0.cancel() } - tasks.removeAll() - } - - // MARK: - Public API - - func loadAll(fetchFromInitialPage: Bool = true) async throws { - return try await withThrowingTaskGroup(of: Void.self) { group in - taskGroup = group - func appendJobs() { - if nextPageInfo?.canLoadNext ?? false { - group.addTask { [weak self] in - try await self?.loadNext() - } - } else if previousPageInfo?.canLoadPrevious ?? false { - group.addTask { [weak self] in - try await self?.loadPrevious() - } - } - } - - // We begin by setting the initial state. The group needs some job to perform or it will perform nothing. - if fetchFromInitialPage { - // If we are fetching from an initial page, then we will want to reset state and then add a task for the initial load. - reset() - isLoadingAll = true - group.addTask { [weak self] in - await self?.fetch(cachePolicy: .fetchIgnoringCacheData) - } - } else if initialPageResult == nil { - // Otherwise, we have to make sure that we have an `initialPageResult` - throw PaginationError.missingInitialPage - } else { - isLoadingAll = true - appendJobs() - } - - // We only have one job in the group per execution. - // Calling `next()` will either throw or give the next result (irrespective of order added into the queue). - // Upon cancellation, the error is propogated to the task group and all remaining child tasks in the group are cancelled. - while try await group.next() != nil && isLoadingAll { - appendJobs() - } - - // Setup return state - isLoadingAll = false - if let queuedValue { - currentValue = queuedValue - } - queuedValue = nil - taskGroup = nil - } - } - - func loadPrevious( - cachePolicy: CachePolicy = .fetchIgnoringCacheData - ) async throws { - try await paginationFetch(direction: .previous, cachePolicy: cachePolicy) - } - - /// Loads the next page, using the currently saved pagination information to do so. - /// Thread-safe, and supports multiple subscribers calling from multiple threads. - /// **NOTE**: Requires having already called `fetch` or `refetch` prior to this call. - /// - Parameters: - /// - cachePolicy: Preferred cache policy for fetching subsequent pages. Defaults to `fetchIgnoringCacheData`. - func loadNext( - cachePolicy: CachePolicy = .fetchIgnoringCacheData - ) async throws { - try await paginationFetch(direction: .next, cachePolicy: cachePolicy) - } - - func subscribe( - onUpdate: @escaping (Result, any Error>) -> Void - ) -> AnyCancellable { - $currentValue.compactMap({ $0 }) - .flatMap { [weak self] result in - Future, any Error>?, Never> { [weak self] promise in - Task { [weak self] in - guard let self else { return } - let isLoadingAll = await self.isLoadingAll - guard !isLoadingAll else { return promise(.success(nil)) } - promise(.success(result)) - } - } - } - .sink { (result: Result, any Error>?) in - result.flatMap(onUpdate) - } - } - - /// Reloads all data, starting at the first query, resetting pagination state. - /// - Parameter cachePolicy: Preferred cache policy for first-page fetches. Defaults to `returnCacheDataAndFetch` - func refetch(cachePolicy: CachePolicy = .fetchIgnoringCacheData) async { - reset() - await fetch(cachePolicy: cachePolicy) - } - - func fetch() async { - reset() - await fetch(cachePolicy: .returnCacheDataAndFetch) - } - - /// Cancels any in-flight fetching operations and unsubscribes from the store. - func reset() { - nextPageWatchers.forEach { $0.cancel() } - nextPageWatchers = [] - firstPageWatcher?.cancel() - firstPageWatcher = nil - previousPageVarMap = [:] - nextPageVarMap = [:] - initialPageResult = nil - - // Ensure any active networking operations are halted. - taskGroup?.cancelAll() - tasks.forEach { $0.cancel() } - tasks.removeAll() - isFetching = false - isLoadingAll = false - } - - /// Whether or not we can load more information based on the current page. - var canLoadNext: Bool { - nextPageInfo?.canLoadNext ?? false - } - - var canLoadPrevious: Bool { - previousPageInfo?.canLoadPrevious ?? false - } - - // MARK: - Private - - private func fetch(cachePolicy: CachePolicy = .returnCacheDataAndFetch) async { - await execute { [weak self] publisher in - guard let self else { return } - if await self.firstPageWatcher == nil { - let watcher = GraphQLQueryWatcher(client: client, query: initialQuery, callbackQueue: await watcherCallbackQueue) { [weak self] result in - Task { [weak self] in - await self?.onFetch( - fetchType: .initial, - cachePolicy: cachePolicy, - result: result, - publisher: publisher - ) - } - } - await self.setFirstPageWatcher(watcher: watcher) - } - await self.firstPageWatcher?.refetch(cachePolicy: cachePolicy) - } - } - - private func paginationFetch( - direction: PaginationDirection, - cachePolicy: CachePolicy - ) async throws { - // Access to `isFetching` is mutually exclusive, so these checks and modifications will prevent - // other attempts to call this function in rapid succession. - if isFetching { throw PaginationError.loadInProgress } - isFetching = true - defer { isFetching = false } - - // Determine the query based on whether we are paginating forward or backwards - let pageQuery: PaginatedQuery? - switch direction { - case .previous: - guard let previousPageInfo else { throw PaginationError.missingInitialPage } - guard previousPageInfo.canLoadPrevious else { throw PaginationError.pageHasNoMoreContent } - pageQuery = previousPageResolver(previousPageInfo) - case .next: - guard let nextPageInfo else { throw PaginationError.missingInitialPage } - guard nextPageInfo.canLoadNext else { throw PaginationError.pageHasNoMoreContent } - pageQuery = nextPageResolver(nextPageInfo) - } - guard let pageQuery else { throw PaginationError.noQuery } - - await execute { [weak self] publisher in - guard let self else { return } - let watcher = await GraphQLQueryWatcher(client: self.client, query: pageQuery, callbackQueue: watcherCallbackQueue) { [weak self] result in - Task { [weak self] in - await self?.onFetch( - fetchType: .paginated(direction, pageQuery), - cachePolicy: cachePolicy, - result: result, - publisher: publisher - ) - } - } - await self.appendPaginationWatcher(watcher: watcher) - watcher.refetch(cachePolicy: cachePolicy) - } - } - - private func onFetch( - fetchType: FetchType, - cachePolicy: CachePolicy, - result: Result, any Error>, - publisher: CurrentValueSubject - ) { - switch result { - case .failure(let error): - if isLoadingAll { - queuedValue = .failure(error) - } else { - currentValue = .failure(error) - } - publisher.send(completion: .finished) - case .success(let data): - let shouldUpdate: Bool - if cachePolicy == .returnCacheDataAndFetch && data.source == .cache { - shouldUpdate = false - } else { - shouldUpdate = true - } - - var value: Result, any Error>? - var output: PaginationOutput? - var didFail = false - switch fetchType { - case .initial: - initialPageResult = data as? GraphQLResult - output = initialPageResult.flatMap { result in - .init( - previousPages: latest?.previous ?? [], - initialPage: latest?.initial, - nextPages: latest?.next ?? [], - lastUpdatedPage: .initial(result) - ) - } - if initialPageResult?.data == nil { - didFail = true - } - case .paginated(let direction, let query): - let variables: Set = query.__variables?.underlyingJsonValues ?? [] - let underlyingData = data.data as? PaginatedQuery.Data - switch direction { - case .next: - nextPageVarMap[variables] = data as? GraphQLResult - case .previous: - previousPageVarMap[variables] = data as? GraphQLResult - } - - if let latest, let paginatedResult = data as? GraphQLResult { - output = .init( - previousPages: latest.previous, - initialPage: latest.initial, - nextPages: latest.next, - lastUpdatedPage: .paginated(paginatedResult) - ) - } - if underlyingData == nil { - didFail = true - } - } - - value = output.flatMap { paginationOutput in - Result.success(paginationOutput) - } - - if let value { - if isLoadingAll { - queuedValue = value - } else { - currentValue = value - } - } - if didFail, isLoadingAll { - isLoadingAll = false - publisher.send(completion: .finished) - } else if shouldUpdate { - publisher.send(completion: .finished) - } - } - } - - private func nextPageTransformation() -> (any PaginationInfo)? { - let currentValue = try? currentValue?.get() - guard let last = nextPageVarMap.values.last?.data else { - return initialPageResult?.data.flatMap { extractPageInfo(.initial($0, currentValue)) } - } - return extractPageInfo(.paginated(last, currentValue)) - } - - private func previousPageTransformation() -> (any PaginationInfo)? { - let currentValue = try? currentValue?.get() - guard let first = previousPageVarMap.values.last?.data else { - return initialPageResult?.data.flatMap { extractPageInfo(.initial($0, currentValue)) } - } - return extractPageInfo(.paginated(first, currentValue)) - } - - private func execute(operation: @escaping (CurrentValueSubject) async throws -> Void) async { - let tasksCopy = tasks - await withCheckedContinuation { continuation in - let task = Task { - let fetchContainer = FetchContainer() - let publisher = CurrentValueSubject(()) - let subscriber = publisher.sink(receiveCompletion: { _ in - Task { await fetchContainer.cancel() } - }, receiveValue: { }) - await fetchContainer.setValues(subscriber: subscriber, continuation: continuation) - try await withTaskCancellationHandler { - try Task.checkCancellation() - try await operation(publisher) - } onCancel: { - Task { - await fetchContainer.cancel() - } - } - } - tasks.insert(task) - } - let remainder = tasks.subtracting(tasksCopy) - remainder.forEach { task in - tasks.remove(task) - } - } - - private func appendPaginationWatcher(watcher: GraphQLQueryWatcher) { - nextPageWatchers.append(watcher) - } - - private func setFirstPageWatcher(watcher: GraphQLQueryWatcher) { - firstPageWatcher = watcher - } -} - -private actor FetchContainer { - var subscriber: AnyCancellable? { - willSet { subscriber?.cancel() } - } - var continuation: CheckedContinuation? { - willSet { continuation?.resume() } - } - - init( - subscriber: AnyCancellable? = nil, - continuation: CheckedContinuation? = nil - ) { - self.subscriber = subscriber - self.continuation = continuation - } - - deinit { - continuation?.resume() - } - - func cancel() { - subscriber = nil - continuation = nil - } - - func setValues( - subscriber: AnyCancellable?, - continuation: CheckedContinuation? - ) { - self.subscriber = subscriber - self.continuation = continuation - } -} -private extension AsyncGraphQLQueryPagerCoordinator { - enum FetchType { - case initial - case paginated(PaginationDirection, PaginatedQuery) - } -} - -extension GraphQLOperation.Variables { - var underlyingJsonValues: Set { - var set = Set() - for value in values { - if let encodableValue = value._jsonEncodableValue?._jsonValue { - _ = set.insert(encodableValue) - } - } - return set - } -} - -internal extension GraphQLResult { - var updateSource: UpdateSource { - source == .cache ? .cache : .server - } -} diff --git a/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/BidirectionalPagination.swift b/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/BidirectionalPagination.swift index d1d26c8b4..ee91a30ad 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/BidirectionalPagination.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/BidirectionalPagination.swift @@ -1,6 +1,6 @@ extension CursorBasedPagination { /// A cursor based pagination strategy that can support fetching previous and next pages. - public struct Bidirectional: PaginationInfo, Hashable { + public struct Bidirectional: PaginationInfo, Hashable, Sendable { public let hasNext: Bool public let endCursor: String? public let hasPrevious: Bool diff --git a/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ForwardPagination.swift b/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ForwardPagination.swift index c39dae1ef..a76c85f87 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ForwardPagination.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ForwardPagination.swift @@ -1,6 +1,6 @@ extension CursorBasedPagination { /// A cursor based pagination strategy that supports forward pagination; fetching the next page. - public struct Forward: PaginationInfo, Hashable { + public struct Forward: PaginationInfo, Hashable, Sendable { public let hasNext: Bool public let endCursor: String? diff --git a/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ReversePagination.swift b/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ReversePagination.swift index a8435815f..421e68188 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ReversePagination.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/CursorBasedPagination/ReversePagination.swift @@ -1,6 +1,6 @@ extension CursorBasedPagination { /// A cursor-basd pagination strategy that can support fetching previous pages. - public struct Reverse: PaginationInfo, Hashable { + public struct Reverse: PaginationInfo, Hashable, Sendable { public let hasPrevious: Bool public let startCursor: String? diff --git a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager+Convenience.swift b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager+Convenience.swift index ee7193bd2..63fe5d57c 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager+Convenience.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager+Convenience.swift @@ -5,68 +5,18 @@ import Foundation // MARK: - GraphQLQueryPager Convenience Functions public extension GraphQLQueryPager { - - /// Convenience initializer for creating a pager that has a single query and does not - /// transform output responses. - convenience init( - client: any ApolloClientProtocol, - watcherDispatchQueue: DispatchQueue = .main, - initialQuery: InitialQuery, - extractPageInfo: @escaping (InitialQuery.Data) -> P, - pageResolver: @escaping (P, PaginationDirection) -> InitialQuery? - ) where Model == PaginationOutput { - self.init( - pager: GraphQLQueryPagerCoordinator( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, - extractPageInfo: pageExtraction(transform: extractPageInfo), - pageResolver: pageResolver - )) - } - - /// Convenience initializer for creating a multi-query pager that does not - /// transform output responses. - convenience init( - client: any ApolloClientProtocol, - initialQuery: InitialQuery, - watcherDispatchQueue: DispatchQueue = .main, - extractInitialPageInfo: @escaping (InitialQuery.Data) -> P, - extractNextPageInfo: @escaping (PaginatedQuery.Data) -> P, - pageResolver: @escaping (P, PaginationDirection) -> PaginatedQuery? - ) where Model == PaginationOutput { - self.init( - pager: .init( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, - extractPageInfo: pageExtraction( - initialTransfom: extractInitialPageInfo, - paginatedTransform: extractNextPageInfo - ), - pageResolver: pageResolver - ) - ) - } -} - -// MARK: - AsyncGraphQLQueryPager Convenience Functions - -public extension AsyncGraphQLQueryPager { /// Convenience initializer for creating a pager that has a single query and does not /// transform output responses. convenience init( - client: any ApolloClientProtocol, - watcherDispatchQueue: DispatchQueue = .main, + client: ApolloClient, initialQuery: InitialQuery, - extractPageInfo: @escaping (InitialQuery.Data) -> P, - pageResolver: @escaping (P, PaginationDirection) -> InitialQuery? + extractPageInfo: @escaping @Sendable (InitialQuery.Data) -> P, + pageResolver: @escaping @Sendable (P, PaginationDirection) -> InitialQuery? ) where Model == PaginationOutput { self.init( - pager: AsyncGraphQLQueryPagerCoordinator( + pager: GraphQLQueryPagerCoordinator( client: client, initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, extractPageInfo: pageExtraction(transform: extractPageInfo), pageResolver: pageResolver )) @@ -75,18 +25,16 @@ public extension AsyncGraphQLQueryPager { /// Convenience initializer for creating a multi-query pager that does not /// transform output responses. convenience init( - client: any ApolloClientProtocol, + client: ApolloClient, initialQuery: InitialQuery, - watcherDispatchQueue: DispatchQueue = .main, - extractInitialPageInfo: @escaping (InitialQuery.Data) -> P, - extractNextPageInfo: @escaping (PaginatedQuery.Data) -> P, - pageResolver: @escaping (P, PaginationDirection) -> PaginatedQuery? + extractInitialPageInfo: @escaping @Sendable (InitialQuery.Data) -> P, + extractNextPageInfo: @escaping @Sendable (PaginatedQuery.Data) -> P, + pageResolver: @escaping @Sendable (P, PaginationDirection) -> PaginatedQuery? ) where Model == PaginationOutput { self.init( pager: .init( client: client, initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, extractPageInfo: pageExtraction( initialTransfom: extractInitialPageInfo, paginatedTransform: extractNextPageInfo @@ -98,9 +46,9 @@ public extension AsyncGraphQLQueryPager { } private func pageExtraction( - initialTransfom: @escaping (InitialQuery.Data) -> P, - paginatedTransform: @escaping (PaginatedQuery.Data) -> P -) -> (PageExtractionData) -> P { + initialTransfom: @escaping @Sendable (InitialQuery.Data) -> P, + paginatedTransform: @escaping @Sendable (PaginatedQuery.Data) -> P +) -> @Sendable (PageExtractionData) -> P { { extractionData in switch extractionData { case .initial(let value, _): @@ -112,8 +60,8 @@ private func pageExtraction( - transform: @escaping (InitialQuery.Data) -> P -) -> (PageExtractionData) -> P { + transform: @escaping @Sendable (InitialQuery.Data) -> P +) -> @Sendable (PageExtractionData) -> P { { extractionData in switch extractionData { case .initial(let value, _), .paginated(let value, _): diff --git a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager.swift b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager.swift index ffc604577..e71edc952 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPager.swift @@ -1,42 +1,46 @@ import Apollo import ApolloAPI -import Combine +@preconcurrency import Combine import Foundation -/// Type-erases a query pager, transforming data from a generic type to a specific type, often a view model or array of view models. -public class GraphQLQueryPager: Publisher { +/// Type-erases a query pager, transforming data from a generic type to a specific type, often a view model or array +/// of view models. +public final class GraphQLQueryPager: Publisher, @unchecked Sendable { public typealias Failure = Never public typealias Output = Result let _subject: CurrentValueSubject = .init(nil) - var publisher: AnyPublisher { _subject.compactMap { $0 }.eraseToAnyPublisher() } - public var cancellables: Set = [] + var publisher: AnyPublisher { _subject.compactMap({ $0 }).eraseToAnyPublisher() } + @Atomic public var cancellables: Set = [] public let pager: any PagerType - public var canLoadNext: Bool { pager.canLoadNext } - public var canLoadPrevious: Bool { pager.canLoadPrevious } + public var canLoadNext: Bool { get async { await pager.canLoadNext } } + public var canLoadPrevious: Bool { get async { await pager.canLoadPrevious } } init, InitialQuery, PaginatedQuery>( pager: Pager, - transform: @escaping (PaginationOutput) throws -> Model + transform: @escaping @Sendable (PaginationOutput) throws -> Model ) { self.pager = pager - pager.subscribe { [weak self] result in - guard let self else { return } - let returnValue: Output - - switch result { - case let .success(output): - do { - let transformedModels = try transform(output) - returnValue = .success(transformedModels) - } catch { + Task { + let cancellable = await pager.subscribe { [weak self] result in + guard let self else { return } + let returnValue: Output + + switch result { + case let .success(output): + do { + let transformedModels = try transform(output) + returnValue = .success(transformedModels) + } catch { + returnValue = .failure(error) + } + case let .failure(error): returnValue = .failure(error) } - case let .failure(error): - returnValue = .failure(error) - } - _subject.send(returnValue) + _subject.send(returnValue) + } + _ = $cancellables.mutate { $0.insert(cancellable) } } } @@ -44,13 +48,16 @@ public class GraphQLQueryPager: Publisher { pager: Pager ) where Model == PaginationOutput { self.pager = pager - pager.subscribe { [weak self] result in - guard let self else { return } - _subject.send(result) + Task { + let cancellable = await pager.subscribe { [weak self] result in + guard let self else { return } + _subject.send(result) + } + _ = $cancellables.mutate { $0.insert(cancellable) } } } - /// Initialize an `GraphQLQueryPager` that outputs a `PaginationOutput`. + /// Initialize an `AsyncGraphQLQueryPager` that outputs a `PaginationOutput`. /// - Parameters: /// - client: Apollo client type /// - initialQuery: The query to call for the first page of pagination. May be a separate type of query than the pagination query. @@ -62,23 +69,21 @@ public class GraphQLQueryPager: Publisher { InitialQuery: GraphQLQuery, PaginatedQuery: GraphQLQuery >( - client: any ApolloClientProtocol, - watcherDispatchQueue: DispatchQueue = .main, + client: ApolloClient, initialQuery: InitialQuery, - extractPageInfo: @escaping (PageExtractionData) -> P, - pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)? + extractPageInfo: @escaping @Sendable (PageExtractionData) -> P, + pageResolver: (@Sendable (P, PaginationDirection) -> PaginatedQuery?)? ) where Model == PaginationOutput { let pager = GraphQLQueryPagerCoordinator( client: client, initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, extractPageInfo: extractPageInfo, pageResolver: pageResolver ) self.init(pager: pager) } - /// Initialize an `GraphQLQueryPager` that outputs a user-defined `Model`, the result of the `transform` argument. + /// Initialize an `AsyncGraphQLQueryPager` that outputs a user-defined `Model`, the result of the `transform` argument. /// - Parameters: /// - client: Apollo client type /// - initialQuery: The query to call for the first page of pagination. May be a separate type of query than the pagination query. @@ -91,102 +96,142 @@ public class GraphQLQueryPager: Publisher { InitialQuery: GraphQLQuery, PaginatedQuery: GraphQLQuery >( - client: any ApolloClientProtocol, - watcherDispatchQueue: DispatchQueue = .main, + client: ApolloClient, initialQuery: InitialQuery, - extractPageInfo: @escaping (PageExtractionData?>) -> P, - pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)?, - transform: @escaping (PaginationOutput) throws -> Model + extractPageInfo: @escaping @Sendable (PageExtractionData?>) -> P, + pageResolver: (@Sendable (P, PaginationDirection) -> PaginatedQuery?)?, + transform: @escaping @Sendable (PaginationOutput) throws -> Model ) { let pager = GraphQLQueryPagerCoordinator( client: client, initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, extractPageInfo: extractPageInfo, pageResolver: pageResolver ) self.init(pager: pager, transform: transform) } - deinit { - pager.reset() + // MARK: Load Next + + /// Load the next page, if available. + /// - Parameters: + /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `cacheAndNetwork`. + public func loadNext( + cachePolicy: CachePolicy.Query.CacheAndNetwork = .cacheAndNetwork + ) async throws { + try await self.loadNext(fetchBehavior: cachePolicy.toFetchBehavior()) } - /// Subscribe to the results of the pager, with the management of the subscriber being stored internally to the `AnyGraphQLQueryPager`. - /// - Parameter completion: The closure to trigger when new values come in. Guaranteed to run on the main thread. - @available(*, deprecated, message: "Will be removed in a future version of ApolloPagination. Use the `Combine` publishers instead. If you need to dispatch to the main thread, make sure to use a `.receive(on: RunLoop.main)` as part of your `Combine` operation.") - public func subscribe(completion: @escaping @MainActor (Output) -> Void) { - publisher.sink { result in - Task { await completion(result) } - }.store(in: &cancellables) + /// Load the next page, if available. + /// - Parameters: + /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `cacheAndNetwork`. + public func loadNext( + cachePolicy: CachePolicy.Query.CacheOnly + ) async throws { + try await self.loadNext(fetchBehavior: cachePolicy.toFetchBehavior()) } /// Load the next page, if available. /// - Parameters: - /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `returnCacheDataAndFetch`. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. Passes an optional error, of type `PaginationError`, if there was an internal error related to pagination. Does not surface network errors. Defaults to `nil`. + /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `cacheAndNetwork`. public func loadNext( - cachePolicy: CachePolicy = .returnCacheDataAndFetch, - callbackQueue: DispatchQueue = .main, - completion: ((PaginationError?) -> Void)? = nil - ) { - pager.loadNext(cachePolicy: cachePolicy, callbackQueue: callbackQueue, completion: completion) + cachePolicy: CachePolicy.Query.SingleResponse + ) async throws { + try await self.loadNext(fetchBehavior: cachePolicy.toFetchBehavior()) } + /// Load the next page, if available. + /// - Parameters: + /// - fetchBehavior: The Apollo `FetchBehavior` to use. + public func loadNext( + fetchBehavior: FetchBehavior + ) async throws { + try await pager.loadNext(fetchBehavior: fetchBehavior) + } + + // MARK: Load Previous + /// Load the previous page, if available. /// - Parameters: - /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `returnCacheDataAndFetch`. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. Passes an optional error, of type `PaginationError`, if there was an internal error related to pagination. Does not surface network errors. Defaults to `nil`. + /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `cacheAndNetwork`. public func loadPrevious( - cachePolicy: CachePolicy = .returnCacheDataAndFetch, - callbackQueue: DispatchQueue = .main, - completion: ((PaginationError?) -> Void)? = nil - ) { - pager.loadPrevious(cachePolicy: cachePolicy, callbackQueue: callbackQueue, completion: completion) + cachePolicy: CachePolicy.Query.CacheAndNetwork = .cacheAndNetwork + ) async throws { + try await self.loadPrevious(fetchBehavior: cachePolicy.toFetchBehavior()) + } + + /// Load the previous page, if available. + /// - Parameters: + /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `cacheAndNetwork`. + public func loadPrevious( + cachePolicy: CachePolicy.Query.CacheOnly + ) async throws { + try await self.loadPrevious(fetchBehavior: cachePolicy.toFetchBehavior()) } + /// Load the previous page, if available. + /// - Parameters: + /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `cacheAndNetwork`. + public func loadPrevious( + cachePolicy: CachePolicy.Query.SingleResponse + ) async throws { + try await self.loadPrevious(fetchBehavior: cachePolicy.toFetchBehavior()) + } + + /// Load the previous page, if available. + /// - Parameters: + /// - fetchBehavior: The Apollo `FetchBehavior` to use. + public func loadPrevious( + fetchBehavior: FetchBehavior + ) async throws { + try await pager.loadPrevious(fetchBehavior: fetchBehavior) + } + + // MARK: Load All + /// Loads all pages. Does not output a value until all pages have loaded. /// - Parameters: /// - fetchFromInitialPage: Pass true to begin loading from the initial page; otherwise pass false. Defaults to `true`. **NOTE**: Loading all pages with this value set to `false` requires that the initial page has already been loaded previously. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. Passes an optional error, of type `PaginationError`, if there was an internal error related to pagination. Does not surface network errors. Defaults to `nil`. public func loadAll( - fetchFromInitialPage: Bool = true, - callbackQueue: DispatchQueue = .main, - completion: ((PaginationError?) -> Void)? = nil - ) { - pager.loadAll(fetchFromInitialPage: fetchFromInitialPage, callbackQueue: callbackQueue, completion: completion) + fetchFromInitialPage: Bool = true + ) async throws { + try await pager.loadAll(fetchFromInitialPage: fetchFromInitialPage) } + // MARK: Refetch + /// Discards pagination state and fetches the first page from scratch. - /// - Parameters: - /// - cachePolicy: The apollo cache policy to trigger the first fetch with. Defaults to `fetchIgnoringCacheData`. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. - public func refetch( - cachePolicy: CachePolicy = .fetchIgnoringCacheData, - callbackQueue: DispatchQueue = .main, - completion: (() -> Void)? = nil - ) { - pager.refetch(cachePolicy: cachePolicy, callbackQueue: callbackQueue, completion: completion) + /// - Parameter cachePolicy: The apollo cache policy to trigger the first fetch with. Defaults to `networkOnly`. + public func refetch(cachePolicy: CachePolicy.Query.SingleResponse = .networkOnly) async { + await self.refetch(fetchBehavior: cachePolicy.toFetchBehavior()) + } + + /// Discards pagination state and fetches the first page from scratch. + /// - Parameter cachePolicy: The apollo cache policy to trigger the first fetch with. Defaults to `networkOnly`. + public func refetch(cachePolicy: CachePolicy.Query.CacheOnly) async { + await self.refetch(fetchBehavior: cachePolicy.toFetchBehavior()) + } + + /// Discards pagination state and fetches the first page from scratch. + /// - Parameter cachePolicy: The apollo cache policy to trigger the first fetch with. Defaults to `networkOnly`. + public func refetch(cachePolicy: CachePolicy.Query.CacheAndNetwork) async { + await self.refetch(fetchBehavior: cachePolicy.toFetchBehavior()) + } + + /// Discards pagination state and fetches the first page from scratch. + /// - Parameter fetchBehavior: The Apollo `FetchBehavior` to use. + public func refetch(fetchBehavior: FetchBehavior) async { + await pager.refetch(fetchBehavior: fetchBehavior) } /// Fetches the first page. - /// - Parameters: - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. - public func fetch( - callbackQueue: DispatchQueue = .main, - completion: (() -> Void)? = nil - ) { - pager.fetch(callbackQueue: callbackQueue, completion: completion) + public func fetch() async { + await pager.fetch() } /// Resets pagination state and cancels in-flight updates from the pager. - public func reset() { - pager.reset() + public func reset() async { + await pager.reset() } public func receive( diff --git a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerCoordinator.swift b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerCoordinator.swift index b218c092a..147bb6599 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerCoordinator.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerCoordinator.swift @@ -1,249 +1,495 @@ import Apollo -import ApolloAPI -import Combine +@_spi(Internal) import ApolloAPI +@preconcurrency import Combine import Foundation +import OrderedCollections public protocol PagerType { associatedtype InitialQuery: GraphQLQuery associatedtype PaginatedQuery: GraphQLQuery - - var canLoadNext: Bool { get } - var canLoadPrevious: Bool { get } - func reset() - func loadPrevious( - cachePolicy: CachePolicy, - callbackQueue: DispatchQueue, - completion: ((PaginationError?) -> Void)? - ) - func loadNext( - cachePolicy: CachePolicy, - callbackQueue: DispatchQueue, - completion: ((PaginationError?) -> Void)? - ) - func loadAll( - fetchFromInitialPage: Bool, - callbackQueue: DispatchQueue, - completion: ((PaginationError?) -> Void)? - ) - func refetch( - cachePolicy: CachePolicy, - callbackQueue: DispatchQueue, - completion: (() -> Void)? - ) - func fetch( - callbackQueue: DispatchQueue, - completion: (() -> Void)? - ) + var canLoadNext: Bool { get async } + var canLoadPrevious: Bool { get async } + func reset() async + func loadPrevious(fetchBehavior: FetchBehavior) async throws + func loadNext(fetchBehavior: FetchBehavior) async throws + func loadAll(fetchFromInitialPage: Bool) async throws + func refetch(fetchBehavior: FetchBehavior) async + func fetch() async } -/// Handles pagination in the queue by managing multiple query watchers. -class GraphQLQueryPagerCoordinator: PagerType { - let pager: AsyncGraphQLQueryPagerCoordinator - private var subscriptions = Subscriptions() - private var completionManager = CompletionManager() +actor GraphQLQueryPagerCoordinator: PagerType { + private let client: ApolloClient + private var firstPageWatcher: GraphQLQueryWatcher? + private var nextPageWatchers: [GraphQLQueryWatcher] = [] + let initialQuery: InitialQuery + var isLoadingAll: Bool = false + var isFetching: Bool = false + let nextPageResolver: (any PaginationInfo) -> PaginatedQuery? + let previousPageResolver: (any PaginationInfo) -> PaginatedQuery? + let extractPageInfo: (PageExtractionData?>) -> any PaginationInfo + var nextPageInfo: (any PaginationInfo)? { nextPageTransformation() } + var previousPageInfo: (any PaginationInfo)? { previousPageTransformation() } + + var canLoadPages: (next: Bool, previous: Bool) { + (canLoadNext, canLoadPrevious) + } - var publisher: AnyPublisher, any Error>, Never> { - get async { await pager.$currentValue.compactMap { $0 }.eraseToAnyPublisher() } + var publishers: ( + previousPageVarMap: Published>>.Publisher, + initialPageResult: Published?>.Publisher, + nextPageVarMap: Published>>.Publisher + ) { + return ($previousPageVarMap, $initialPageResult, $nextPageVarMap) } + typealias ResultType = Result, any Error> + + @Published var currentValue: ResultType? + private var queuedValue: ResultType? + + @Published var initialPageResult: GraphQLResponse? + var latest: (previous: [GraphQLResponse], initial: GraphQLResponse, next: [GraphQLResponse])? { + guard let initialPageResult else { return nil } + return ( + Array(previousPageVarMap.values).reversed(), + initialPageResult, + Array(nextPageVarMap.values) + ) + } + + /// Maps each query variable set to latest results from internal watchers. + @Published var nextPageVarMap: OrderedDictionary> = [:] + @Published var previousPageVarMap: OrderedDictionary> = [:] + private var tasks: Set> = [] + private nonisolated(unsafe) var taskGroup: ThrowingTaskGroup? + + /// Designated Initializer + /// - Parameters: + /// - client: Apollo Client + /// - initialQuery: The initial query that is being watched + /// - extractPageInfo: The `PageInfo` derived from `PageExtractionData` + /// - nextPageResolver: The resolver that can derive the query for loading more. This can be a different query than the `initialQuery`. + /// - onError: The callback when there is an error. init( - client: any ApolloClientProtocol, + client: ApolloClient, initialQuery: InitialQuery, - watcherDispatchQueue: DispatchQueue = .main, extractPageInfo: @escaping (PageExtractionData?>) -> P, pageResolver: ((P, PaginationDirection) -> PaginatedQuery?)? ) { - pager = .init( - client: client, - initialQuery: initialQuery, - watcherDispatchQueue: watcherDispatchQueue, - extractPageInfo: extractPageInfo, - pageResolver: pageResolver - ) - Task { [weak self] in - guard let self else { return } - let (previousPageVarMapPublisher, initialPublisher, nextPageVarMapPublisher) = await pager.publishers - let publishSubscriber = previousPageVarMapPublisher.combineLatest( - initialPublisher, - nextPageVarMapPublisher - ).sink { [weak self] _ in - guard !Task.isCancelled else { return } - Task { [weak self] in - guard let self else { return } - let (canLoadNext, canLoadPrevious) = await self.pager.canLoadPages - self.$canLoadNext.mutate { $0 = canLoadNext } - self.$canLoadPrevious.mutate { $0 = canLoadPrevious } + self.client = client + self.initialQuery = initialQuery + self.extractPageInfo = extractPageInfo + self.nextPageResolver = { page in + guard let page = page as? P else { return nil } + return pageResolver?(page, .next) + } + self.previousPageResolver = { page in + guard let page = page as? P else { return nil } + return pageResolver?(page, .previous) + } + } + + deinit { + nextPageWatchers.forEach { $0.cancel() } + firstPageWatcher?.cancel() + taskGroup?.cancelAll() + tasks.forEach { $0.cancel() } + tasks.removeAll() + } + + // MARK: - Public API + + func loadAll(fetchFromInitialPage: Bool = true) async throws { + return try await withThrowingTaskGroup(of: Void.self) { group in + taskGroup = group + func appendJobs() { + if nextPageInfo?.canLoadNext ?? false { + group.addTask { [weak self] in + try await self?.loadNext() + } + } else if previousPageInfo?.canLoadPrevious ?? false { + group.addTask { [weak self] in + try await self?.loadPrevious() + } } } - await subscriptions.store(subscription: publishSubscriber) + + // We begin by setting the initial state. The group needs some job to perform or it will perform nothing. + if fetchFromInitialPage { + // If we are fetching from an initial page, then we will want to reset state and then add a task for the initial load. + reset() + isLoadingAll = true + group.addTask { [weak self] in + await self?.fetch(fetchBehavior: .NetworkOnly) + } + } else if initialPageResult == nil { + // Otherwise, we have to make sure that we have an `initialPageResult` + throw PaginationError.missingInitialPage + } else { + isLoadingAll = true + appendJobs() + } + + // We only have one job in the group per execution. + // Calling `next()` will either throw or give the next result (irrespective of order added into the queue). + // Upon cancellation, the error is propogated to the task group and all remaining child tasks in the group are cancelled. + while try await group.next() != nil && isLoadingAll { + appendJobs() + } + + // Setup return state + isLoadingAll = false + if let queuedValue { + currentValue = queuedValue + } + queuedValue = nil + taskGroup = nil } } - /// Convenience initializer - /// - Parameter pager: An `AsyncGraphQLQueryPager`. - init(pager: AsyncGraphQLQueryPagerCoordinator) { - self.pager = pager + func loadPrevious( + fetchBehavior: FetchBehavior = .NetworkOnly + ) async throws { + try await paginationFetch(direction: .previous, fetchBehavior: fetchBehavior) } - /// Allows the caller to subscribe to new pagination results. - /// - Parameter onUpdate: A closure which provides the most recent pagination result. Execution may be on any thread. - func subscribe(onUpdate: @escaping (Result, any Error>) -> Void) { - Task { [weak self] in - guard let self else { return } - let subscription = await self.pager.subscribe(onUpdate: onUpdate) - await subscriptions.store(subscription: subscription) - } + /// Loads the next page, using the currently saved pagination information to do so. + /// Thread-safe, and supports multiple subscribers calling from multiple threads. + /// **NOTE**: Requires having already called `fetch` or `refetch` prior to this call. + /// - Parameters: + /// - fetchBehavior: Preferred fetch behavior for fetching subsequent pages. Defaults to `NetworkOnly`. + func loadNext( + fetchBehavior: FetchBehavior = .NetworkOnly + ) async throws { + try await paginationFetch(direction: .next, fetchBehavior: fetchBehavior) } - /// Whether or not we can load the next page. Initializes with a `false` value that is updated after the initial fetch. - @Atomic var canLoadNext: Bool = false - /// Whether or not we can load the previous page. Initializes with a `false` value that is updated after the initial fetch. - @Atomic var canLoadPrevious: Bool = false + func subscribe( + onUpdate: @escaping @Sendable (Result, any Error>) -> Void + ) -> AnyCancellable { + $currentValue.compactMap({ $0 }) + .flatMap { [weak self] result in + Future, any Error>?, Never> { [weak self] promise in + let wrapper = SendablePromiseWrapper(promise) + Task { [weak self] in + guard let self else { return } + let isLoadingAll = await self.isLoadingAll + guard !isLoadingAll else { return wrapper.promise(.success(nil)) } + wrapper.promise(.success(result)) + } + } + } + .sink { (result: Result, any Error>?) in + result.flatMap(onUpdate) + } + } + + /// Reloads all data, starting at the first query, resetting pagination state. + /// - Parameter fetchBehavior: Preferred fetch behavior for first-page fetches. Defaults to `NetworkOnly` + func refetch(fetchBehavior: FetchBehavior = .NetworkOnly) async { + reset() + await fetch(fetchBehavior: fetchBehavior) + } + + func fetch() async { + reset() + await fetch(fetchBehavior: .CacheAndNetwork) + } - /// Reset all pagination state and cancel all in-flight operations. + /// Cancels any in-flight fetching operations and unsubscribes from the store. func reset() { - Task { [weak self] in + nextPageWatchers.forEach { $0.cancel() } + nextPageWatchers = [] + firstPageWatcher?.cancel() + firstPageWatcher = nil + previousPageVarMap = [:] + nextPageVarMap = [:] + initialPageResult = nil + + // Ensure any active networking operations are halted. + taskGroup?.cancelAll() + tasks.forEach { $0.cancel() } + tasks.removeAll() + isFetching = false + isLoadingAll = false + } + + /// Whether or not we can load more information based on the current page. + var canLoadNext: Bool { + nextPageInfo?.canLoadNext ?? false + } + + var canLoadPrevious: Bool { + previousPageInfo?.canLoadPrevious ?? false + } + + // MARK: - Private + + private func fetch(fetchBehavior: FetchBehavior = .CacheAndNetwork) async { + await execute { [weak self] publisher in guard let self else { return } - for completion in await self.completionManager.completions { - completion.execute(error: PaginationError.cancellation) + if await self.firstPageWatcher == nil { + let watcher = await GraphQLQueryWatcher(client: client, query: initialQuery) { [weak self] result in + Task { [weak self] in + await self?.onFetch( + fetchType: .initial, + fetchBehavior: fetchBehavior, + result: result, + publisher: publisher + ) + } + } + await self.setFirstPageWatcher(watcher: watcher) } - await self.completionManager.reset() - await self.pager.reset() + await self.firstPageWatcher?.fetch(fetchBehavior: fetchBehavior) } } - /// Loads the previous page, if we can. - /// - Parameters: - /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `fetchIgnoringCacheData`. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. Passes an optional error, of type `PaginationError`, if there was an internal error related to pagination. Does not surface network errors. Defaults to `nil`. - func loadPrevious( - cachePolicy: CachePolicy = .fetchIgnoringCacheData, - callbackQueue: DispatchQueue = .main, - completion: ((PaginationError?) -> Void)? = nil - ) { - execute(callbackQueue: callbackQueue, completion: completion) { [weak self] in - try await self?.pager.loadPrevious(cachePolicy: cachePolicy) + private func paginationFetch( + direction: PaginationDirection, + fetchBehavior: FetchBehavior + ) async throws { + // Access to `isFetching` is mutually exclusive, so these checks and modifications will prevent + // other attempts to call this function in rapid succession. + if isFetching { throw PaginationError.loadInProgress } + isFetching = true + defer { isFetching = false } + + // Determine the query based on whether we are paginating forward or backwards + let pageQuery: PaginatedQuery? + switch direction { + case .previous: + guard let previousPageInfo else { throw PaginationError.missingInitialPage } + guard previousPageInfo.canLoadPrevious else { throw PaginationError.pageHasNoMoreContent } + pageQuery = previousPageResolver(previousPageInfo) + case .next: + guard let nextPageInfo else { throw PaginationError.missingInitialPage } + guard nextPageInfo.canLoadNext else { throw PaginationError.pageHasNoMoreContent } + pageQuery = nextPageResolver(nextPageInfo) } - } + guard let pageQuery else { throw PaginationError.noQuery } - /// Loads the next page, if we can. - /// - Parameters: - /// - cachePolicy: The Apollo `CachePolicy` to use. Defaults to `fetchIgnoringCacheData`. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. Passes an optional error, of type `PaginationError`, if there was an internal error related to pagination. Does not surface network errors. Defaults to `nil`. - func loadNext( - cachePolicy: CachePolicy = .fetchIgnoringCacheData, - callbackQueue: DispatchQueue = .main, - completion: ((PaginationError?) -> Void)? = nil - ) { - execute(callbackQueue: callbackQueue, completion: completion) { [weak self] in - try await self?.pager.loadNext(cachePolicy: cachePolicy) + await execute { [weak self] publisher in + guard let self else { return } + let watcher = await GraphQLQueryWatcher(client: self.client, query: pageQuery) { [weak self] result in + Task { [weak self] in + await self?.onFetch( + fetchType: .paginated(direction, pageQuery), + fetchBehavior: fetchBehavior, + result: result, + publisher: publisher + ) + } + } + await self.appendPaginationWatcher(watcher: watcher) + await watcher.fetch(fetchBehavior: fetchBehavior) } } - /// Loads all pages. - /// - Parameters: - /// - fetchFromInitialPage: Pass true to begin loading from the initial page; otherwise pass false. Defaults to `true`. **NOTE**: Loading all pages with this value set to `false` requires that the initial page has already been loaded previously. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. Passes an optional error, of type `PaginationError`, if there was an internal error related to pagination. Does not surface network errors. Defaults to `nil`. - func loadAll( - fetchFromInitialPage: Bool = true, - callbackQueue: DispatchQueue = .main, - completion: ((PaginationError?) -> Void)? = nil + private func onFetch( + fetchType: FetchType, + fetchBehavior: FetchBehavior, + result: Result, any Error>, + publisher: CurrentValueSubject ) { - execute(callbackQueue: callbackQueue, completion: completion) { [weak self] in - try await self?.pager.loadAll(fetchFromInitialPage: fetchFromInitialPage) + switch result { + case .failure(let error): + if isLoadingAll { + queuedValue = .failure(error) + } else { + currentValue = .failure(error) + } + publisher.send(completion: .finished) + case .success(let data): + let shouldUpdate: Bool + if fetchBehavior == .CacheAndNetwork && data.source == .cache { + shouldUpdate = false + } else { + shouldUpdate = true + } + + var value: Result, any Error>? + var output: PaginationOutput? + var didFail = false + switch fetchType { + case .initial: + initialPageResult = data as? GraphQLResponse + output = initialPageResult.flatMap { result in + .init( + previousPages: latest?.previous ?? [], + initialPage: latest?.initial, + nextPages: latest?.next ?? [], + lastUpdatedPage: .initial(result) + ) + } + if initialPageResult?.data == nil { + didFail = true + } + case .paginated(let direction, let query): + let variables = PageVariables(query.__variables ?? [:]) + let underlyingData = data.data as? PaginatedQuery.Data + switch direction { + case .next: + nextPageVarMap[variables] = data as? GraphQLResponse + case .previous: + previousPageVarMap[variables] = data as? GraphQLResponse + } + + if let latest, let paginatedResult = data as? GraphQLResponse { + output = .init( + previousPages: latest.previous, + initialPage: latest.initial, + nextPages: latest.next, + lastUpdatedPage: .paginated(paginatedResult) + ) + } + if underlyingData == nil { + didFail = true + } + } + + value = output.flatMap { paginationOutput in + Result.success(paginationOutput) + } + + if let value { + if isLoadingAll { + queuedValue = value + } else { + currentValue = value + } + } + if didFail, isLoadingAll { + isLoadingAll = false + publisher.send(completion: .finished) + } else if shouldUpdate { + publisher.send(completion: .finished) + } } } - /// Discards pagination state and fetches the first page from scratch. - /// - Parameters: - /// - cachePolicy: The apollo cache policy to trigger the first fetch with. Defaults to `fetchIgnoringCacheData`. - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. - func refetch( - cachePolicy: CachePolicy = .fetchIgnoringCacheData, - callbackQueue: DispatchQueue = .main, - completion: (() -> Void)? = nil - ) { - execute(callbackQueue: callbackQueue) { _ in completion?() } operation: { [weak self] in - guard let self else { return } - for completion in await self.completionManager.completions { - completion.execute(error: PaginationError.cancellation) - } - await pager.refetch(cachePolicy: cachePolicy) + private func nextPageTransformation() -> (any PaginationInfo)? { + let currentValue = try? currentValue?.get() + guard let last = nextPageVarMap.values.last?.data else { + return initialPageResult?.data.flatMap { extractPageInfo(.initial($0, currentValue)) } } + return extractPageInfo(.paginated(last, currentValue)) } - /// Fetches the first page. - /// - Parameters: - /// - callbackQueue: The `DispatchQueue` that the `completion` fires on. Defaults to `main`. - /// - completion: A completion block that will always trigger after the execution of this operation. - func fetch( - callbackQueue: DispatchQueue = .main, - completion: (() -> Void)? = nil - ) { - execute(callbackQueue: callbackQueue) { _ in completion?() } operation: { [weak self] in - await self?.pager.fetch() + private func previousPageTransformation() -> (any PaginationInfo)? { + let currentValue = try? currentValue?.get() + guard let first = previousPageVarMap.values.last?.data else { + return initialPageResult?.data.flatMap { extractPageInfo(.initial($0, currentValue)) } } + return extractPageInfo(.paginated(first, currentValue)) } - private func execute(callbackQueue: DispatchQueue, completion: ((PaginationError?) -> Void)?, operation: @escaping () async throws -> Void) { - Task<_, Never> { [weak self] in - let completionHandler = Completion(callbackQueue: callbackQueue, completion: completion) - await self?.completionManager.append(completion: completionHandler) - do { - try await operation() - await self?.completionManager.execute(completion: completionHandler, with: nil) - } catch { - await self?.completionManager.execute(completion: completionHandler, with: error as? PaginationError ?? .unknown(error)) + private func execute(operation: @escaping @Sendable (CurrentValueSubject) async throws -> Void) async { + let tasksCopy = tasks + await withCheckedContinuation { continuation in + let task = Task { + let fetchContainer = FetchContainer() + let publisher = CurrentValueSubject(()) + let subscriber = publisher.sink(receiveCompletion: { _ in + Task { await fetchContainer.cancel() } + }, receiveValue: { }) + await fetchContainer.setValues(subscriber: subscriber, continuation: continuation) + try await withTaskCancellationHandler { + try Task.checkCancellation() + try await operation(publisher) + } onCancel: { + Task { + await fetchContainer.cancel() + } + } } + tasks.insert(task) + } + let remainder = tasks.subtracting(tasksCopy) + remainder.forEach { task in + tasks.remove(task) } } -} -private actor Subscriptions { - var subscriptions: Set = [] + private func appendPaginationWatcher(watcher: GraphQLQueryWatcher) { + nextPageWatchers.append(watcher) + } - func store(subscription: AnyCancellable) { - subscriptions.insert(subscription) + private func setFirstPageWatcher(watcher: GraphQLQueryWatcher) { + firstPageWatcher = watcher } } -private class Completion { - var completion: ((PaginationError?) -> Void)? - var callbackQueue: DispatchQueue +private actor FetchContainer { + var subscriber: AnyCancellable? { + willSet { subscriber?.cancel() } + } + var continuation: CheckedContinuation? { + willSet { continuation?.resume() } + } - init(callbackQueue: DispatchQueue, completion: ((PaginationError?) -> Void)?) { - self.completion = completion - self.callbackQueue = callbackQueue + init( + subscriber: AnyCancellable? = nil, + continuation: CheckedContinuation? = nil + ) { + self.subscriber = subscriber + self.continuation = continuation } - func execute(error: PaginationError?) { - callbackQueue.async { [weak self] in - self?.completion?(error) - self?.completion = nil - } + deinit { + continuation?.resume() } -} -private actor CompletionManager { - var completions: [Completion] = [] + func cancel() { + subscriber = nil + continuation = nil + } - func append(completion: Completion) { - completions.append(completion) + func setValues( + subscriber: AnyCancellable?, + continuation: CheckedContinuation? + ) { + self.subscriber = subscriber + self.continuation = continuation + } +} +private extension GraphQLQueryPagerCoordinator { + enum FetchType { + case initial + case paginated(PaginationDirection, PaginatedQuery) } +} - func reset() { - completions.removeAll() +internal extension GraphQLResponse { + var updateSource: UpdateSource { + source == .cache ? .cache : .server + } +} + +fileprivate final class SendablePromiseWrapper: @unchecked Sendable { + fileprivate typealias Promise = (Result) -> Void + + fileprivate let promise: Promise + + fileprivate init(_ promise: @escaping Promise) { + self.promise = promise + } +} + +internal struct PageVariables: Sendable, Hashable { + + let encoded: JSONValue? + + init(_ variables: GraphQLOperation.Variables) { + self.encoded = variables._jsonEncodableValue?._jsonValue } - func execute(completion: Completion, with error: PaginationError?) { - completion.execute(error: error) + static func == (lhs: PageVariables, rhs: PageVariables) -> Bool { + AnySendableHashable.equatableCheck(lhs.encoded, rhs.encoded) } - deinit { - completions.forEach { $0.execute(error: PaginationError.cancellation) } + func hash(into hasher: inout Hasher) { + hasher.combine(encoded) } + } diff --git a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerOutput.swift b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerOutput.swift index dc70cf2d0..8f206ed97 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerOutput.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/GraphQLQueryPagerOutput.swift @@ -3,23 +3,23 @@ import ApolloAPI import Foundation /// A struct which contains the outputs of pagination -public struct PaginationOutput: Hashable { +public struct PaginationOutput: Hashable, Sendable { /// An array of previous pages, in pagination order /// Earlier pages come first in the array. - public let previousPages: [GraphQLResult] + public let previousPages: [GraphQLResponse] /// The initial page that we fetched. - public let initialPage: GraphQLResult? + public let initialPage: GraphQLResponse? /// An array of pages after the initial page. - public let nextPages: [GraphQLResult] + public let nextPages: [GraphQLResponse] public let lastUpdatedPage: QueryWrapper public init( - previousPages: [GraphQLResult], - initialPage: GraphQLResult?, - nextPages: [GraphQLResult], + previousPages: [GraphQLResponse], + initialPage: GraphQLResponse?, + nextPages: [GraphQLResponse], lastUpdatedPage: QueryWrapper ) { self.previousPages = previousPages @@ -34,9 +34,9 @@ public struct PaginationOutput) - case paginated(GraphQLResult) + public enum QueryWrapper: Hashable, Sendable { + case initial(GraphQLResponse) + case paginated(GraphQLResponse) } } @@ -76,7 +76,7 @@ extension PaginationOutput where InitialQuery == PaginatedQuery { previousPages.compactMap(\.data) + [initialPage?.data].compactMap { $0 } + nextPages.compactMap(\.data) } - public var allPages: [GraphQLResult] { + public var allPages: [GraphQLResponse] { previousPages + [initialPage].compactMap { $0 } + nextPages } } diff --git a/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/Bidirectional.swift b/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/Bidirectional.swift index 443ef6e59..4010a6c74 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/Bidirectional.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/Bidirectional.swift @@ -1,5 +1,5 @@ extension OffsetPagination { - public struct Bidirectional: PaginationInfo, Hashable { + public struct Bidirectional: PaginationInfo, Hashable, Sendable { public let offset: Int public var canLoadNext: Bool public let canLoadPrevious: Bool diff --git a/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ForwardOffset.swift b/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ForwardOffset.swift index cd193d91d..76028fef8 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ForwardOffset.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ForwardOffset.swift @@ -1,5 +1,5 @@ extension OffsetPagination { - public struct Forward: PaginationInfo, Hashable { + public struct Forward: PaginationInfo, Hashable, Sendable { public let offset: Int public let canLoadNext: Bool public var canLoadPrevious: Bool { false } diff --git a/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ReverseOffset.swift b/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ReverseOffset.swift index 0ddc07700..70d6ffeaf 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ReverseOffset.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/OffsetBasedPagination/ReverseOffset.swift @@ -1,5 +1,5 @@ extension OffsetPagination { - public struct Reverse: PaginationInfo, Hashable { + public struct Reverse: PaginationInfo, Hashable, Sendable { public let offset: Int public var canLoadNext: Bool { false } public let canLoadPrevious: Bool diff --git a/apollo-ios-pagination/Sources/ApolloPagination/PageExtractionData.swift b/apollo-ios-pagination/Sources/ApolloPagination/PageExtractionData.swift index 2b7ffdd82..23b3a00b3 100644 --- a/apollo-ios-pagination/Sources/ApolloPagination/PageExtractionData.swift +++ b/apollo-ios-pagination/Sources/ApolloPagination/PageExtractionData.swift @@ -5,3 +5,5 @@ public enum PageExtractionData String { let process = Process() - let toolPath = try context.tool(named: "xcrun").path - process.executableURL = URL(fileURLWithPath: toolPath.string) + let toolURL = try context.tool(named: "xcrun").url + process.executableURL = URL(fileURLWithPath: toolURL.absoluteString) process.arguments = ["xcodebuild", "-version"] let outputPipe = Pipe() diff --git a/apollo-ios/Sources/Apollo/ApolloStore+ReaderWriter.swift b/apollo-ios/Sources/Apollo/ApolloStore+ReaderWriter.swift deleted file mode 100644 index 3875cbcdf..000000000 --- a/apollo-ios/Sources/Apollo/ApolloStore+ReaderWriter.swift +++ /dev/null @@ -1,76 +0,0 @@ -import Foundation -import Atomics - -extension ApolloStore { - final class ReaderWriter: Sendable { - - /// Atomic counter for number of current readers. - /// While writing, this will have a value of `WRITING` - private let readerCount = ManagedAtomic(0) - - /// Value for `readerCount` while in writing state - private static let WRITING = -1 - - // MARK: - Read - - func read(_ body: () async throws -> Void) async rethrows { - await beginReading() - defer { finishReading() } - - try await body() - } - - private func beginReading() async { - var didIncrementReadCount = false - - while true { - let currentReaderCount = readerCount.load(ordering: .relaxed) - - guard currentReaderCount != Self.WRITING else { - await Task.yield() - continue - } - - (didIncrementReadCount, _) = readerCount.weakCompareExchange( - // Ensure no other thread has changed count before increment - expected: currentReaderCount, - // Increment count to add reader - desired: currentReaderCount + 1, - ordering: .acquiringAndReleasing - ) - - if didIncrementReadCount { return } - } - } - - private func finishReading() { - readerCount.wrappingDecrement(ordering: .acquiringAndReleasing) - } - - // MARK: - Write - func write(_ body: () async throws -> Void) async rethrows { - await beginWriting() - defer { finishWriting() } - try await body() - } - - private func beginWriting() async { - while true { - let (didSetWritingFlag, _) = readerCount.weakCompareExchange( - expected: 0, - desired: Self.WRITING, - ordering: .acquiringAndReleasing - ) - - if didSetWritingFlag { return } else { - await Task.yield() - } - } - } - - func finishWriting() { - readerCount.store(0, ordering: .releasing) - } - } -} - diff --git a/apollo-ios/Sources/Apollo/ApolloStore.swift b/apollo-ios/Sources/Apollo/ApolloStore.swift index 68254225a..b60ee417f 100644 --- a/apollo-ios/Sources/Apollo/ApolloStore.swift +++ b/apollo-ios/Sources/Apollo/ApolloStore.swift @@ -19,9 +19,14 @@ public protocol ApolloStoreSubscriber: AnyObject, Sendable { } /// The `ApolloStore` class acts as a local cache for normalized GraphQL results. -#warning("TODO: Docs. ReaderWriter usage; why you should not share a cache with 2 stores, etc.") +/// +/// - Warning: Using the same `NormalizedCache` with multiple `ApolloStore` instances at the same time is unsupported +/// and can result in undefined behavior, data races, and crashes. +/// The store uses an internal read/write lock to protect against concurrent write access to the `NormalizedCache`. +/// This means that the `NormalizedCache` implementation does not need to manage thread safety. If a cache is used +/// with multiple `ApolloStore` instances, no guaruntees about thread safety can be made. public final class ApolloStore: Sendable { - private let readerWriterLock = ReaderWriter() + private let readerWriterLock = AsyncReadWriteLock() /// The `NormalizedCache` itself is not thread-safe. Access to the cache by a single store is made /// thread-safe by using a `ReaderWriter`. All access to the cache must be done within the @@ -72,7 +77,7 @@ public final class ApolloStore: Sendable { /// ensure you call `unsubscribe` on this subscriber before it goes out of scope. public func subscribe(_ subscriber: any ApolloStoreSubscriber) async -> SubscriptionToken { let token = SubscriptionToken(id: ObjectIdentifier(subscriber)) - await readerWriterLock.write { + try? await readerWriterLock.write { self.subscribers[token] = subscriber } return token @@ -85,7 +90,7 @@ public final class ApolloStore: Sendable { /// To avoid retain cycles, call `unsubscribe` on all active subscribers before they go out of scope. public func unsubscribe(_ subscriptionToken: SubscriptionToken) { Task(priority: Task.currentPriority > .medium ? .medium : Task.currentPriority) { - await readerWriterLock.write { + try? await readerWriterLock.write { self.subscribers.removeValue(forKey: subscriptionToken) } } @@ -95,10 +100,10 @@ public final class ApolloStore: Sendable { /// /// - Parameters: /// - body: The body of the operation to perform. - public func withinReadTransaction( - _ body: @Sendable (ReadTransaction) async throws -> T + public func withinReadTransaction( + _ body: @Sendable @escaping (ReadTransaction) async throws -> T ) async throws -> T { - var value: T! + nonisolated(unsafe) var value: T! try await readerWriterLock.read { value = try await body(ReadTransaction(store: self)) } @@ -109,10 +114,10 @@ public final class ApolloStore: Sendable { /// /// - Parameters: /// - body: The body of the operation to perform - public func withinReadWriteTransaction( - _ body: (ReadWriteTransaction) async throws -> T - ) async rethrows -> T { - var value: T! + public func withinReadWriteTransaction( + _ body: @Sendable @escaping (ReadWriteTransaction) async throws -> T + ) async throws -> T { + nonisolated(unsafe) var value: T! try await readerWriterLock.write { value = try await body(ReadWriteTransaction(store: self)) } @@ -170,12 +175,7 @@ public final class ApolloStore: Sendable { } // MARK: - - #warning( - """ - TODO: figure out how to prevent transaction from escaping closure scope. - Maybe explicitly mark non-sendable: https://forums.swift.org/t/what-does-available-unavailable-sendable-actually-do/65218 - """ - ) + public class ReadTransaction { fileprivate let _cache: any NormalizedCache @@ -501,3 +501,6 @@ public final class ApolloStore: Sendable { } } } + +@available(*, unavailable) +extension ApolloStore.ReadTransaction: Sendable { } diff --git a/apollo-ios/Sources/Apollo/AsyncReadWriteLock.swift b/apollo-ios/Sources/Apollo/AsyncReadWriteLock.swift new file mode 100644 index 000000000..a9f5b557a --- /dev/null +++ b/apollo-ios/Sources/Apollo/AsyncReadWriteLock.swift @@ -0,0 +1,56 @@ +import Foundation + +actor AsyncReadWriteLock { + private final class ReadTask: Sendable { + let task: Task + + init(_ body: @Sendable @escaping () async throws -> Void) { + task = Task { + try await body() + } + } + } + + private var currentReadTasks: [ObjectIdentifier: ReadTask] = [:] + private var currentWriteTask: Task? + + /// Waits for all current reads/writes to be completed, then calls the provided closure while preventing + /// any other reads/writes from beginning. + /// + /// This function should be `rethrows` but the compiler doesn't understand that when passing the `block` into a Task. + /// If the `body` provided does not throw, this function will not throw. + func write(_ body: @Sendable @escaping () async throws -> Void) async throws { + while currentWriteTask != nil || !currentReadTasks.isEmpty { + await Task.yield() + continue + } + + defer { currentWriteTask = nil } + let writeTask = Task { + try await body() + } + currentWriteTask = writeTask + + try await writeTask.value + } + + /// Waits for all current writes to be completed, then calls the provided closure while preventing + /// any other writes from beginning. Other reads may be executed concurrently. + /// + /// This function should be `rethrows` but the compiler doesn't understand that when passing the `block` into a Task. + /// If the `body` provided does not throw, this function will not throw. + func read(_ body: @Sendable @escaping () async throws -> Void) async throws { + while currentWriteTask != nil { + await Task.yield() + continue + } + + let readTask = ReadTask(body) + let taskID = ObjectIdentifier(readTask) + defer { currentReadTasks[taskID] = nil } + currentReadTasks[taskID] = readTask + + try await readTask.task.value + } + +} diff --git a/apollo-ios/Sources/Apollo/Atomic.swift b/apollo-ios/Sources/Apollo/Atomic.swift index 922be16f8..52aa988c6 100644 --- a/apollo-ios/Sources/Apollo/Atomic.swift +++ b/apollo-ios/Sources/Apollo/Atomic.swift @@ -53,4 +53,14 @@ public extension Atomic where T : Numeric { _value += 1 return _value } + + /// Decrements the wrapped `Int` atomically, subtracting 1 from the value. + @discardableResult + func decrement() -> T { + lock.lock() + defer { lock.unlock() } + + _value -= 1 + return _value + } } diff --git a/apollo-ios/Sources/Apollo/ExecutionSources/NetworkResponseExecutionSource.swift b/apollo-ios/Sources/Apollo/ExecutionSources/NetworkResponseExecutionSource.swift index 321191203..b2f279995 100644 --- a/apollo-ios/Sources/Apollo/ExecutionSources/NetworkResponseExecutionSource.swift +++ b/apollo-ios/Sources/Apollo/ExecutionSources/NetworkResponseExecutionSource.swift @@ -5,7 +5,7 @@ import ApolloAPI /// A `GraphQLExecutionSource` configured to execute upon the JSON data from the network response /// for a GraphQL operation. @_spi(Execution) -public struct NetworkResponseExecutionSource: GraphQLExecutionSource, CacheKeyComputingExecutionSource { +public struct NetworkResponseExecutionSource: GraphQLExecutionSource, CacheKeyComputingExecutionSource, Sendable { public typealias RawObjectData = JSONObject public typealias FieldCollector = DefaultFieldSelectionCollector diff --git a/apollo-ios/Sources/Apollo/GraphQLExecutor.swift b/apollo-ios/Sources/Apollo/GraphQLExecutor.swift index cc1874705..cfdb5831c 100644 --- a/apollo-ios/Sources/Apollo/GraphQLExecutor.swift +++ b/apollo-ios/Sources/Apollo/GraphQLExecutor.swift @@ -167,7 +167,7 @@ public struct GraphQLExecutionError: Error, LocalizedError { } } -/// A GraphQL executor is responsible for executing a selection set and generating a result. It is +/// A GraphQL executor is responsible for executing a selection set and generating a result. It is /// initialized with a resolver closure that gets called repeatedly to resolve field values. /// /// An executor is used both to parse a response received from the server, and to read from the @@ -512,3 +512,7 @@ public final class GraphQLExecutor { .map { try accumulator.accept(childObject: $0, info: fieldInfo) } } } + +// MARK: - Sendable Conformance (Conditional) +@_spi(Execution) +extension GraphQLExecutor: Sendable where Source: Sendable {} diff --git a/apollo-ios/Sources/Apollo/Interceptors/AutomaticPersistedQueryInterceptor.swift b/apollo-ios/Sources/Apollo/Interceptors/AutomaticPersistedQueryInterceptor.swift index 6ef2d77e5..764a44c83 100644 --- a/apollo-ios/Sources/Apollo/Interceptors/AutomaticPersistedQueryInterceptor.swift +++ b/apollo-ios/Sources/Apollo/Interceptors/AutomaticPersistedQueryInterceptor.swift @@ -46,7 +46,6 @@ public struct AutomaticPersistedQueryInterceptor: GraphQLInterceptor { let isInitialResult = IsInitialResult() return await next(request).map { response in -#warning("TODO: Test if cache returns result, then server returns failed result, APQ retry still occurs") guard response.result.source == .server, await isInitialResult.get() else { return response diff --git a/apollo-ios/Sources/Apollo/NonCopyableAsyncThrowingStream.swift b/apollo-ios/Sources/Apollo/NonCopyableAsyncThrowingStream.swift index 1ae29ba3b..4152dad64 100644 --- a/apollo-ios/Sources/Apollo/NonCopyableAsyncThrowingStream.swift +++ b/apollo-ios/Sources/Apollo/NonCopyableAsyncThrowingStream.swift @@ -20,12 +20,11 @@ public struct NonCopyableAsyncThrowingStream: Sendable, ~Copy private let stream: AsyncThrowingStream -#warning("Maybe these shouldn't be public inits. Easy to create bugs when creating your own stream") - public init(stream: AsyncThrowingStream) { + public init(stream: consuming AsyncThrowingStream) { self.stream = stream } - public init(stream wrapped: sending S) where S.Element == Element { + public init(stream wrapped: consuming sending S) where S.Element == Element { self.stream = AsyncThrowingStream.executingInAsyncTask { [wrapped] continuation in for try await element in wrapped { continuation.yield(element) @@ -71,7 +70,10 @@ public struct NonCopyableAsyncThrowingStream: Sendable, ~Copy // MARK: - Error Handling - #warning("TODO: Write unit tests for this. Docs: if return nil, error is supressed and stream finishes.") + /// Calls the given `transform` if an error is thrown by the stream. + /// + /// If the `transform` returns `nil`, the error is supressed and the resulting stream will terminate without + /// emitting an error. public consuming func mapErrors( _ transform: @escaping @Sendable (any Error) async throws -> Element? ) async -> NonCopyableAsyncThrowingStream { @@ -114,7 +116,6 @@ public struct NonCopyableAsyncThrowingStream: Sendable, ~Copy } } -#warning("Do we keep this public? Helps make TaskLocalValues work, but extension on Swift standard lib type could conflict with other extensions") extension TaskLocal { @_disfavoredOverload diff --git a/apollo-ios/Sources/Apollo/RequestChain.swift b/apollo-ios/Sources/Apollo/RequestChain.swift index 6e7c28858..cd75e0b96 100644 --- a/apollo-ios/Sources/Apollo/RequestChain.swift +++ b/apollo-ios/Sources/Apollo/RequestChain.swift @@ -46,7 +46,6 @@ public struct RequestChain: Sendable { request: Request ) -> ResultStream { return doInRetryingAsyncThrowingStream(request: request) { request, continuation in - #warning("TODO: Write unit test that cache only request gets sent through interceptors still.") try await kickoffRequestInterceptors(request: request, continuation: continuation) } } @@ -122,8 +121,7 @@ public struct RequestChain: Sendable { throw ApolloClient.Error.noResults } } - - #warning("TODO: unit tests for cache read after failed network fetch") + private func execute( request: Request ) -> InterceptorResultStream { diff --git a/apollo-ios/Sources/Apollo/ResponseParsing/IncrementalResponseExecutionHandler.swift b/apollo-ios/Sources/Apollo/ResponseParsing/IncrementalResponseExecutionHandler.swift index 57f95e075..074f7acd7 100644 --- a/apollo-ios/Sources/Apollo/ResponseParsing/IncrementalResponseExecutionHandler.swift +++ b/apollo-ios/Sources/Apollo/ResponseParsing/IncrementalResponseExecutionHandler.swift @@ -48,15 +48,13 @@ extension JSONResponseParser { variables: operationVariables ) } - - #warning("Fix Docs") + /// Parses the response into a `IncrementalGraphQLResult` and a `RecordSet` depending on the cache policy. The result /// can be used to merge into a partial result and the `RecordSet` can be merged into a local cache. /// /// - Returns: A tuple of a `IncrementalGraphQLResult` and an optional `RecordSet`. /// - /// - Parameter cachePolicy: Used to determine whether a cache `RecordSet` is returned. A cache policy that does - /// not read or write to the cache will return a `nil` cache `RecordSet`. + /// - Parameter includeCacheRecords: Used to determine whether a cache `RecordSet` is returned. func execute( includeCacheRecords: Bool ) async throws -> (IncrementalGraphQLResult, RecordSet?) { diff --git a/apollo-ios/Sources/Apollo/ResponseParsing/SingleResponseExecutionHandler.swift b/apollo-ios/Sources/Apollo/ResponseParsing/SingleResponseExecutionHandler.swift index 89f5fb2c5..372f3dc36 100644 --- a/apollo-ios/Sources/Apollo/ResponseParsing/SingleResponseExecutionHandler.swift +++ b/apollo-ios/Sources/Apollo/ResponseParsing/SingleResponseExecutionHandler.swift @@ -79,8 +79,7 @@ extension JSONResponseParser { private func makeResult( data: Operation.Data?, dependentKeys: Set? - ) -> GraphQLResponse { - #warning("TODO: Do we need to make sure that there is either data or errors in the result?") + ) -> GraphQLResponse { return GraphQLResponse( data: data, extensions: base.parseExtensions(), diff --git a/apollo-ios/Sources/ApolloAPI/DataDict.swift b/apollo-ios/Sources/ApolloAPI/DataDict.swift index ef4bca825..c0ff9c688 100644 --- a/apollo-ios/Sources/ApolloAPI/DataDict.swift +++ b/apollo-ios/Sources/ApolloAPI/DataDict.swift @@ -219,3 +219,5 @@ extension Array: SelectionSetEntityValue where Element: SelectionSetEntityValue map { $0._fieldData } as DataDict.FieldValue } } + +extension NSArray: @retroactive @unchecked Sendable {} diff --git a/apollo-ios/Sources/ApolloAPI/GraphQLOperation.swift b/apollo-ios/Sources/ApolloAPI/GraphQLOperation.swift index 3208fb04a..e9876ee21 100644 --- a/apollo-ios/Sources/ApolloAPI/GraphQLOperation.swift +++ b/apollo-ios/Sources/ApolloAPI/GraphQLOperation.swift @@ -1,6 +1,6 @@ import Foundation -public enum GraphQLOperationType: Hashable { +public enum GraphQLOperationType: Sendable, Hashable { case query case mutation case subscription @@ -55,7 +55,7 @@ public struct OperationDefinition: Sendable { /// A unique identifier used as a key to map a deferred selection set type to an incremental /// response label and path. -public struct DeferredFragmentIdentifier: Hashable { +public struct DeferredFragmentIdentifier: Sendable, Hashable { public let label: String public let fieldPath: [String] @@ -150,7 +150,7 @@ public extension GraphQLSubscription { // MARK: - OperationResponseFormat /// The format expected for the network response of an operation. -public protocol OperationResponseFormat {} +public protocol OperationResponseFormat: Sendable {} /// A response format for an operation that expects a single network response. /// diff --git a/apollo-ios/Sources/ApolloAPI/LocalCacheMutation.swift b/apollo-ios/Sources/ApolloAPI/LocalCacheMutation.swift index 6ca7d50dd..9e4112fd7 100644 --- a/apollo-ios/Sources/ApolloAPI/LocalCacheMutation.swift +++ b/apollo-ios/Sources/ApolloAPI/LocalCacheMutation.swift @@ -1,4 +1,4 @@ -public protocol LocalCacheMutation: AnyObject, Hashable { +public protocol LocalCacheMutation: Hashable, Sendable { static var operationType: GraphQLOperationType { get } var __variables: GraphQLOperation.Variables? { get } diff --git a/apollo-ios/Sources/ApolloAPI/Optional+asNullable.swift b/apollo-ios/Sources/ApolloAPI/Optional+asNullable.swift index 99b4898cf..898338469 100644 --- a/apollo-ios/Sources/ApolloAPI/Optional+asNullable.swift +++ b/apollo-ios/Sources/ApolloAPI/Optional+asNullable.swift @@ -10,7 +10,12 @@ public protocol AnyOptional {} extension Optional: AnyOptional { } extension Optional where Wrapped: Sendable { -#warning("TODO: Document") + + /// Converts the optional to a `GraphQLNullable. + /// + /// - Double nested optional (ie. `Optional.some(nil)`) -> `GraphQLNullable.null`. + /// - `Optional.none` -> `GraphQLNullable.none` + /// - `Optional.some` -> `GraphQLNullable.some` @_spi(Internal) public var asNullable: GraphQLNullable { unwrapAsNullable() diff --git a/apollo-ios/Sources/ApolloAPI/ParentType.swift b/apollo-ios/Sources/ApolloAPI/ParentType.swift index 207392a12..e369a5f52 100644 --- a/apollo-ios/Sources/ApolloAPI/ParentType.swift +++ b/apollo-ios/Sources/ApolloAPI/ParentType.swift @@ -2,7 +2,7 @@ /// /// A ``SelectionSet``'s `__parentType` is the type from the schema that the selection set is /// selected against. This type can be an ``Object``, ``Interface``, or ``Union``. -public protocol ParentType { +public protocol ParentType: Sendable { /// A helper function to determine if an ``Object`` of the given type can be converted to /// the receiver type. /// diff --git a/apollo-ios/Sources/ApolloAPI/SchemaTypes/InputObject.swift b/apollo-ios/Sources/ApolloAPI/SchemaTypes/InputObject.swift index f12388742..7f63f45db 100644 --- a/apollo-ios/Sources/ApolloAPI/SchemaTypes/InputObject.swift +++ b/apollo-ios/Sources/ApolloAPI/SchemaTypes/InputObject.swift @@ -30,12 +30,6 @@ public struct InputDict: GraphQLOperationVariableValue, Hashable { public var _jsonEncodableValue: (any JSONEncodable)? { data._jsonEncodableObject } - @_disfavoredOverload - public subscript(key: String) -> T { - get { data[key] as! T } - set { data[key] = newValue } - } - public subscript(key: String) -> GraphQLNullable { get { if let value = data[key] { @@ -47,6 +41,27 @@ public struct InputDict: GraphQLOperationVariableValue, Hashable { set { data[key] = newValue } } + @_disfavoredOverload + public subscript(key: String) -> T { + get { data[key] as! T } + set { data[key] = newValue } + } + + @_disfavoredOverload + public subscript(key: String) -> T? { + get { + switch data[key] { + case let .some(value) as GraphQLNullable, + let value as T: + return value + + default: + return nil + } + } + set { data[key] = newValue ?? GraphQLNullable.none } + } + public static func == (lhs: InputDict, rhs: InputDict) -> Bool { AnyHashable(lhs.data._jsonEncodableObject._jsonValue) == AnyHashable(rhs.data._jsonEncodableObject._jsonValue) diff --git a/apollo-ios/Sources/ApolloTestSupport/Field.swift b/apollo-ios/Sources/ApolloTestSupport/Field.swift index 497957749..326bd26b7 100644 --- a/apollo-ios/Sources/ApolloTestSupport/Field.swift +++ b/apollo-ios/Sources/ApolloTestSupport/Field.swift @@ -3,7 +3,7 @@ import ApolloAPI #endif @propertyWrapper -public struct Field { +public struct Field: Sendable { let key: StaticString diff --git a/apollo-ios/Sources/ApolloTestSupport/TestMock.swift b/apollo-ios/Sources/ApolloTestSupport/TestMock.swift index 66c552aea..08f45f333 100644 --- a/apollo-ios/Sources/ApolloTestSupport/TestMock.swift +++ b/apollo-ios/Sources/ApolloTestSupport/TestMock.swift @@ -145,14 +145,14 @@ public protocol AnyMock { } public protocol MockObject: MockFieldValue { - associatedtype MockFields + associatedtype MockFields: Sendable associatedtype MockValueCollectionType = Array> static var objectType: Object { get } static var _mockFields: MockFields { get } } -public protocol MockFieldValue { +public protocol MockFieldValue: Sendable { associatedtype MockValueCollectionType: Collection } diff --git a/apollo-ios/scripts/download-cli.sh b/apollo-ios/scripts/download-cli.sh old mode 100644 new mode 100755 diff --git a/scripts/install-tuist.sh b/scripts/install-tuist.sh deleted file mode 100755 index 1efb8784d..000000000 --- a/scripts/install-tuist.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -e - -shell_join() { - local arg - printf "%s" "$1" - shift - for arg in "$@"; do - printf " " - printf "%s" "${arg// /\ }" - done -} - -ohai() { - printf "${tty_blue}==>${tty_bold} %s${tty_reset}\n" "$(shell_join "$@")" -} - -warn() { - printf "${tty_red}Warning${tty_reset}: %s\n" "$(chomp "$1")" -} - -# The line below extracts the version from the local .tuist-version file and installs that version tag from the github releases. -LATEST_VERSION=$(cat .tuist-version) -ohai "Downloading tuistenv..." -[ -f /tmp/tuistenv.zip ] && rm /tmp/tuistenv.zip -[ -f /tmp/tuistenv ] && rm /tmp/tuistenv -curl -LSsf --output /tmp/tuistenv.zip https://github.com/tuist/tuist/releases/download/${LATEST_VERSION}/tuistenv.zip -ohai "Unzipping tuistenv..." -unzip -o /tmp/tuistenv.zip -d /tmp/tuistenv > /dev/null -ohai "Installing tuistenv..." - -INSTALL_DIR="/usr/local/bin" - -sudo_if_install_dir_not_writeable() { - local command="$1" - if [ -w $INSTALL_DIR ]; then - bash -c "${command}" - else - bash -c "sudo ${command}" - fi -} - -if [[ ! -d $INSTALL_DIR ]]; then - sudo_if_install_dir_not_writeable "mkdir -p ${INSTALL_DIR}" -fi - -if [[ -f "${INSTALL_DIR}/tuist" ]]; then - sudo_if_install_dir_not_writeable "rm ${INSTALL_DIR}/tuist" -fi - -sudo_if_install_dir_not_writeable "mv /tmp/tuistenv/tuistenv \"${INSTALL_DIR}/tuist\"" -sudo_if_install_dir_not_writeable "chmod +x \"${INSTALL_DIR}/tuist\"" - -rm -rf /tmp/tuistenv -rm /tmp/tuistenv.zip - -ohai "tuistenv installed. Try running 'tuist'" -ohai "Check out the documentation at https://docs.tuist.io/"