From 2ea0796ce83ce0ed4f506d73f682f9459c86eaef Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:19:07 -0700 Subject: [PATCH 01/41] Refactor SelectionSet equality to recover type data --- .../ApolloAPI/SelectionSet+Equatable.swift | 289 ++++++++++++++++++ .../Sources/ApolloAPI/SelectionSet.swift | 8 - 2 files changed, 289 insertions(+), 8 deletions(-) create mode 100644 apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift new file mode 100644 index 000000000..1bd759d06 --- /dev/null +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -0,0 +1,289 @@ +public extension SelectionSet { + + typealias FieldValue = any Hashable + + /// Creates a hash using a narrowly scoped algorithm that only combines fields in the underlying data + /// that are relevant to the `SelectionSet`. This ensures that hashes for a fragment do not + /// consider fields that are not included in the fragment, even if they are present in the data. + func hash(into hasher: inout Hasher) { + hasher.combine(self.fieldsForEquality()) + } + + /// Checks for equality using a narrowly scoped algorithm that only compares fields in the underlying data + /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not + /// consider fields that are not included in the fragment, even if they are present in the data. + static func ==(lhs: Self, rhs: Self) -> Bool { + return Self.equatableCheck( + lhs.fieldsForEquality(), + rhs.fieldsForEquality() + ) + } + + @inlinable + internal static func equatableCheck( + _ lhs: [T: any Hashable], + _ rhs: [T: any Hashable] + ) -> Bool { + guard lhs.keys == rhs.keys else { return false } + + return lhs.allSatisfy { + guard let rhsValue = rhs[$0.key], + equatableCheck($0.value, rhsValue) else { + return false + } + return true + } + } + + @inlinable + internal static func equatableCheck( + _ lhs: T, + _ rhs: any Hashable + ) -> Bool { + lhs == rhs as? T + } + + private func fieldsForEquality() -> [String: FieldValue] { + var fields: [String: FieldValue] = [:] + if let asTypeCase = self as? any InlineFragment { + self.addFulfilledSelections(of: type(of: asTypeCase.asRootEntityType), to: &fields) + + } else { + self.addFulfilledSelections(of: type(of: self), to: &fields) + + } + + return fields + } + + private func addFulfilledSelections( + of selectionSetType: any SelectionSet.Type, + to fields: inout [String: FieldValue] + ) { + guard self.__data.fragmentIsFulfilled(selectionSetType) else { + return + } + + for selection in selectionSetType.__selections { + switch selection { + case let .field(field): + add(field: field, to: &fields) + + case let .inlineFragment(typeCase): + self.addFulfilledSelections(of: typeCase, to: &fields) + + case let .conditional(_, selections): + self.addConditionalSelections(selections, to: &fields) + + case let .fragment(fragmentType): + self.addFulfilledSelections(of: fragmentType, to: &fields) + + case let .deferred(_, fragmentType, _): + self.addFulfilledSelections(of: fragmentType, to: &fields) + } + } + } + + private func add( + field: Selection.Field, + to fields: inout [String: FieldValue] + ) { + guard let fieldData = self.__data._data[field.responseKey] else { + return + } + addData(for: field.type) + + /// This function is responsible for recovering the type data we lose by using `AnyHashable` in `DataDict`. + /// The type data is needed for equality and dealing with the nuance of `Optional` types wrapped by `AnyHashable`. + func addData(for type: Selection.Field.OutputType, inList: Bool = false) { + switch type { + case let .scalar(scalarType): + switch scalarType { + case is String.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [String?] + } else { + fields[field.responseKey] = fieldData as? String? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [String] + } else { + fields[field.responseKey] = fieldData as? String + } + } + + case is Int.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Int?] + } else { + fields[field.responseKey] = fieldData as? Int? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Int] + } else { + fields[field.responseKey] = fieldData as? Int + } + } + + case is Bool.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Bool?] + } else { + fields[field.responseKey] = fieldData as? Bool? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Bool] + } else { + fields[field.responseKey] = fieldData as? Bool + } + } + + case is Float.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Float?] + } else { + fields[field.responseKey] = fieldData as? Float? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Float] + } else { + fields[field.responseKey] = fieldData as? Float + } + } + + case is Double.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Double?] + } else { + fields[field.responseKey] = fieldData as? Double? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Double] + } else { + fields[field.responseKey] = fieldData as? Double + } + } + + default: fields[field.responseKey] = fieldData + } + + case .customScalar: + fields[field.responseKey] = fieldData + + case let .nonNull(innerType): + addData(for: innerType, inList: inList) + + case let .list(innerType): + addData(for: innerType, inList: true) + + case let .object(selectionSetType): + switch inList { + case false: + guard let objectData = fieldData as? DataDict else { + preconditionFailure("Expected object data for object field: \(field)") + } + fields[field.responseKey] = selectionSetType.init(_dataDict: objectData) + + case true: + guard let listData = fieldData as? [FieldValue] else { + preconditionFailure("Expected list data for field: \(field)") + } + + fields[field.responseKey] = convertElements(of: listData, to: selectionSetType) as FieldValue + } + } + } + } + + private func convertElements( + of list: [FieldValue], + to selectionSetType: any RootSelectionSet.Type + ) -> [FieldValue] { + if let dataDictList = list as? [DataDict] { + return dataDictList.map { selectionSetType.init(_dataDict: $0) } + } + + if let nestedList = list as? [[FieldValue]] { + return nestedList.map { self.convertElements(of: $0, to: selectionSetType) as FieldValue } + } + + preconditionFailure("Expected list data to contain objects.") + } + + private func addConditionalSelections( + _ selections: [Selection], + to fields: inout [String: FieldValue] + ) { + for selection in selections { + switch selection { + case let .inlineFragment(typeCase): + self.addFulfilledSelections(of: typeCase, to: &fields) + + case let .fragment(fragment): + self.addFulfilledSelections(of: fragment, to: &fields) + + case let .deferred(_, fragment, _): + self.addFulfilledSelections(of: fragment, to: &fields) + + case let .conditional(_, selections): + addConditionalSelections(selections, to: &fields) + + case .field: + assertionFailure("Conditional selections should not directly include fields. They should use an InlineFragment instead.") + } + } + } + +} + +extension Hasher { + + @inlinable + public mutating func combine(_ optionalJSONValue: (any Hashable)?) { + if let value = optionalJSONValue { + self.combine(1 as UInt8) + self.combine(value) + } else { + // This mimics the implementation of combining a nil optional from the Swift language core + // Source reference at: + // https://github.com/swiftlang/swift/blob/main/stdlib/public/core/Optional.swift#L590 + self.combine(0 as UInt8) + } + } + + @inlinable + public mutating func combine( + _ dictionary: [T: any Hashable] + ) { + // From Dictionary's Hashable implementation + var commutativeHash = 0 + for (key, value) in dictionary { + var elementHasher = self + elementHasher.combine(key) + elementHasher.combine(AnyHashable(value)) + commutativeHash ^= elementHasher.finalize() + } + self.combine(commutativeHash) + } + + @inlinable + public mutating func combine( + _ dictionary: [T: any Hashable]? + ) { + if let value = dictionary { + self.combine(value) + } else { + self.combine(Optional<[T: any Hashable]>.none) + } + } +} diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift index c76744ff6..bfbe2fae3 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift @@ -110,14 +110,6 @@ extension SelectionSet { return T.init(_dataDict: __data) } - @inlinable public func hash(into hasher: inout Hasher) { - hasher.combine(__data) - } - - @inlinable public static func ==(lhs: Self, rhs: Self) -> Bool { - return lhs.__data == rhs.__data - } - public var debugDescription: String { return "\(self.__data._data as AnyObject)" } From bfeb4c4455717ccb6324ec40ce8f64529d20935f Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:19:14 -0700 Subject: [PATCH 02/41] Adds equality tests --- .../SelectionSet_EqualityTests.swift | 658 ++++++++++++++++++ 1 file changed, 658 insertions(+) create mode 100644 Tests/ApolloTests/SelectionSet_EqualityTests.swift diff --git a/Tests/ApolloTests/SelectionSet_EqualityTests.swift b/Tests/ApolloTests/SelectionSet_EqualityTests.swift new file mode 100644 index 000000000..d951532b6 --- /dev/null +++ b/Tests/ApolloTests/SelectionSet_EqualityTests.swift @@ -0,0 +1,658 @@ +@testable import Apollo +import ApolloAPI +import ApolloInternalTestHelpers +import Nimble +import XCTest + +@MainActor +class SelectionSet_EqualityTests: XCTestCase { + + func test__equality__scalarString_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("stringValue", String?.self) + ]} + + public var stringValue: String? { __data["stringValue"] } + + convenience init( + stringValue: String? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "stringValue": stringValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(stringValue: "Han Solo") + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "stringValue": "Han Solo" as String // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).to(equal(dataDictHero)) + } + + func test__equality__scalarString_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("stringValue", String?.self) + ]} + + public var stringValue: String? { __data["stringValue"] } + + convenience init( + stringValue: String? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "stringValue": stringValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(stringValue: "Han Solo") + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "stringValue": "Darth Vader" as String // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarString_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("stringValue", String?.self) + ]} + + public var stringValue: String? { __data["stringValue"] } + + convenience init( + stringValue: String? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": stringValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(stringValue: nil) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "stringValue": "Han Solo" as String // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarStringList_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("stringValue", [String?]?.self) + ]} + + public var stringValue: [String?]? { __data["stringValue"] } + + convenience init( + stringValue: [String?]? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "stringValue": stringValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(stringValue: ["Han Solo"]) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "stringValue": ["Han Solo"] as [String] // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).to(equal(dataDictHero)) + } + + func test__equality__scalarStringList_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("stringValue", [String?]?.self) + ]} + + public var stringValue: [String?]? { __data["stringValue"] } + + convenience init( + stringValue: [String?]? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "stringValue": stringValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(stringValue: ["Han Solo"]) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "stringValue": ["Darth Vader"] as [String] // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarStringList_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("stringValue", [String?]?.self) + ]} + + public var stringValue: [String?]? { __data["stringValue"] } + + convenience init( + stringValue: [String?]? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": stringValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(stringValue: nil) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "stringValue": ["Han Solo"] as [String] // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarInt_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("intValue", Int?.self) + ]} + + public var intValue: Int? { __data["intValue"] } + + convenience init( + intValue: Int? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "intValue": intValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(intValue: 1) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "intValue": 1 as Int // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).to(equal(dataDictHero)) + } + + func test__equality__scalarInt_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("intValue", Int?.self) + ]} + + public var intValue: Int? { __data["intValue"] } + + convenience init( + intValue: Int? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "intValue": intValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(intValue: 1) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "intValue": 2 as Int // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarInt_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("intValue", Int?.self) + ]} + + public var intValue: Int? { __data["intValue"] } + + convenience init( + intValue: Int? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "intValue": intValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(intValue: nil) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "intValue": 2 as Int // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarBool_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("boolValue", Bool?.self) + ]} + + public var boolValue: Bool? { __data["boolValue"] } + + convenience init( + boolValue: Bool? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "boolValue": boolValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(boolValue: true) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "boolValue": true as Bool // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).to(equal(dataDictHero)) + } + + func test__equality__scalarBool_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("boolValue", Bool?.self) + ]} + + public var boolValue: Bool? { __data["boolValue"] } + + convenience init( + boolValue: Bool? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "intValue": boolValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(boolValue: true) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "boolValue": false as Bool // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarBool_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("boolValue", Bool?.self) + ]} + + public var boolValue: Bool? { __data["boolValue"] } + + convenience init( + boolValue: Bool? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "boolValue": boolValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(boolValue: nil) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "boolValue": true as Bool // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarFloat_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("floatValue", Float?.self) + ]} + + public var floatValue: Float? { __data["floatValue"] } + + convenience init( + floatValue: Float? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "floatValue": floatValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(floatValue: 1.1) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "floatValue": 1.1 as Float // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).to(equal(dataDictHero)) + } + + func test__equality__scalarFloat_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("floatValue", Float?.self) + ]} + + public var floatValue: Float? { __data["floatValue"] } + + convenience init( + floatValue: Float? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "floatValue": floatValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(floatValue: 1.1) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "floatValue": 2.2 as Float // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarFloat_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("floatValue", Float?.self) + ]} + + public var floatValue: Float? { __data["floatValue"] } + + convenience init( + floatValue: Float? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "floatValue": floatValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(floatValue: nil) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "floatValue": 2.2 as Float // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarDouble_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("doubleValue", Double?.self) + ]} + + public var doubleValue: Double? { __data["doubleValue"] } + + convenience init( + doubleValue: Double? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "doubleValue": doubleValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(doubleValue: 1.1) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "doubleValue": 1.1 as Double // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).to(equal(dataDictHero)) + } + + func test__equality__scalarDouble_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("doubleValue", Double?.self) + ]} + + public var doubleValue: Double? { __data["doubleValue"] } + + convenience init( + doubleValue: Double? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "doubleValue": doubleValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(doubleValue: 1.1) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "doubleValue": 2.2 as Double // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equality__scalarDouble_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("doubleValue", Double?.self) + ]} + + public var doubleValue: Double? { __data["doubleValue"] } + + convenience init( + doubleValue: Double? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "doubleValue": doubleValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(doubleValue: nil) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "doubleValue": 2.2 as Double // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + +} From 8a53f7922445c0488b2b0c1f7f1c77c0eb88b5dc Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Wed, 24 Sep 2025 16:41:50 -0700 Subject: [PATCH 03/41] Adds another test for type cast edge case --- .../SelectionSet_EqualityTests.swift | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Tests/ApolloTests/SelectionSet_EqualityTests.swift b/Tests/ApolloTests/SelectionSet_EqualityTests.swift index d951532b6..6bb5e12a5 100644 --- a/Tests/ApolloTests/SelectionSet_EqualityTests.swift +++ b/Tests/ApolloTests/SelectionSet_EqualityTests.swift @@ -655,4 +655,40 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } + func test__equality__scalar_givenDataDictValueOfDifferentTypeThatCannotCastToFieldType_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("fieldValue", String?.self) + ]} + + public var fieldValue: String? { __data["fieldValue"] } + + convenience init( + fieldValue: String? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": fieldValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(fieldValue: nil) // Muse be `nil` to test `as?` equality type cast behavior + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "stringValue": 2 as Int + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } + } From d417beeb7e3f34f1d0ff573fe3f3b0a3cba06dda Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:09:20 -0700 Subject: [PATCH 04/41] Fix incorrect DataDict test key names --- Tests/ApolloTests/SelectionSet_EqualityTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ApolloTests/SelectionSet_EqualityTests.swift b/Tests/ApolloTests/SelectionSet_EqualityTests.swift index 6bb5e12a5..6fa3d4a29 100644 --- a/Tests/ApolloTests/SelectionSet_EqualityTests.swift +++ b/Tests/ApolloTests/SelectionSet_EqualityTests.swift @@ -96,7 +96,7 @@ class SelectionSet_EqualityTests: XCTestCase { ) { self.init(_dataDict: DataDict(data: [ "__typename": "Hero", - "fieldValue": stringValue + "stringValue": stringValue ], fulfilledFragments: [ObjectIdentifier(Self.self)])) } } @@ -204,7 +204,7 @@ class SelectionSet_EqualityTests: XCTestCase { ) { self.init(_dataDict: DataDict(data: [ "__typename": "Hero", - "fieldValue": stringValue + "stringValue": stringValue ], fulfilledFragments: [ObjectIdentifier(Self.self)])) } } @@ -384,7 +384,7 @@ class SelectionSet_EqualityTests: XCTestCase { ) { self.init(_dataDict: DataDict(data: [ "__typename": "Hero", - "intValue": boolValue + "boolValue": boolValue ], fulfilledFragments: [ObjectIdentifier(Self.self)])) } } @@ -682,7 +682,7 @@ class SelectionSet_EqualityTests: XCTestCase { let dataDictHero = Hero(_dataDict: DataDict( data: [ "__typename": "Hero", - "stringValue": 2 as Int + "fieldValue": 2 as Int ], fulfilledFragments: [ObjectIdentifier(Hero.self)] )) From 57526e67e9b37ec6b1ca10d3a5beb3f91a1e422e Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Thu, 25 Sep 2025 13:27:24 -0700 Subject: [PATCH 05/41] Refactor type casting --- .../ApolloAPI/SelectionSet+Equatable.swift | 84 +------------------ 1 file changed, 2 insertions(+), 82 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index 1bd759d06..3b3781527 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -88,7 +88,7 @@ public extension SelectionSet { field: Selection.Field, to fields: inout [String: FieldValue] ) { - guard let fieldData = self.__data._data[field.responseKey] else { + guard let fieldData = self.__data._data[field.responseKey]?.base as? FieldValue else { return } addData(for: field.type) @@ -97,87 +97,7 @@ public extension SelectionSet { /// The type data is needed for equality and dealing with the nuance of `Optional` types wrapped by `AnyHashable`. func addData(for type: Selection.Field.OutputType, inList: Bool = false) { switch type { - case let .scalar(scalarType): - switch scalarType { - case is String.Type: - if field.type.isNullable { - if inList { - fields[field.responseKey] = fieldData as? [String?] - } else { - fields[field.responseKey] = fieldData as? String? - } - } else { - if inList { - fields[field.responseKey] = fieldData as? [String] - } else { - fields[field.responseKey] = fieldData as? String - } - } - - case is Int.Type: - if field.type.isNullable { - if inList { - fields[field.responseKey] = fieldData as? [Int?] - } else { - fields[field.responseKey] = fieldData as? Int? - } - } else { - if inList { - fields[field.responseKey] = fieldData as? [Int] - } else { - fields[field.responseKey] = fieldData as? Int - } - } - - case is Bool.Type: - if field.type.isNullable { - if inList { - fields[field.responseKey] = fieldData as? [Bool?] - } else { - fields[field.responseKey] = fieldData as? Bool? - } - } else { - if inList { - fields[field.responseKey] = fieldData as? [Bool] - } else { - fields[field.responseKey] = fieldData as? Bool - } - } - - case is Float.Type: - if field.type.isNullable { - if inList { - fields[field.responseKey] = fieldData as? [Float?] - } else { - fields[field.responseKey] = fieldData as? Float? - } - } else { - if inList { - fields[field.responseKey] = fieldData as? [Float] - } else { - fields[field.responseKey] = fieldData as? Float - } - } - - case is Double.Type: - if field.type.isNullable { - if inList { - fields[field.responseKey] = fieldData as? [Double?] - } else { - fields[field.responseKey] = fieldData as? Double? - } - } else { - if inList { - fields[field.responseKey] = fieldData as? [Double] - } else { - fields[field.responseKey] = fieldData as? Double - } - } - - default: fields[field.responseKey] = fieldData - } - - case .customScalar: + case .scalar, .customScalar: fields[field.responseKey] = fieldData case let .nonNull(innerType): From 469ef005633bfcfd19beb7d0ace32003e40d1aa2 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:25:42 -0700 Subject: [PATCH 06/41] Fix for nullable objects being null in equality checks --- .../SelectionSet_EqualityTests.swift | 64 +++++++++++++++++++ .../ApolloAPI/SelectionSet+Equatable.swift | 48 ++++++++++++-- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/Tests/ApolloTests/SelectionSet_EqualityTests.swift b/Tests/ApolloTests/SelectionSet_EqualityTests.swift index 6fa3d4a29..26d49ab0d 100644 --- a/Tests/ApolloTests/SelectionSet_EqualityTests.swift +++ b/Tests/ApolloTests/SelectionSet_EqualityTests.swift @@ -691,4 +691,68 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } + // MARK: - Null/nil tests + + func test__equatable__optionalChildObject__isNullOnBoth_returns_true() { + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("name", String?.self) + ]} + } + + // when + let selectionSet1 = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "name": NSNull() + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + let selectionSet2 = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "name": NSNull() + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(selectionSet1).to(equal(selectionSet2)) + } + + func test__equatable__optionalChildObject__isNullAndNil_returns_true() { + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("name", String?.self) + ]} + } + + // when + let selectionSet1 = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "name": NSNull() + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + let selectionSet2 = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "name": nil + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(selectionSet1).to(equal(selectionSet2)) + } + } diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index 3b3781527..d7df415c5 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -1,3 +1,5 @@ +import Foundation + public extension SelectionSet { typealias FieldValue = any Hashable @@ -88,13 +90,17 @@ public extension SelectionSet { field: Selection.Field, to fields: inout [String: FieldValue] ) { - guard let fieldData = self.__data._data[field.responseKey]?.base as? FieldValue else { - return - } + let nullableFieldData = self.__data._data[field.responseKey].asNullable + let fieldData: FieldValue + switch nullableFieldData { + case let .some(value): + fieldData = value + case .none, .null: + return + } + addData(for: field.type) - /// This function is responsible for recovering the type data we lose by using `AnyHashable` in `DataDict`. - /// The type data is needed for equality and dealing with the nuance of `Optional` types wrapped by `AnyHashable`. func addData(for type: Selection.Field.OutputType, inList: Bool = false) { switch type { case .scalar, .customScalar: @@ -207,3 +213,35 @@ extension Hasher { } } } + +fileprivate protocol AnyOptional {} + +@_spi(Internal) +extension Optional: AnyOptional { } + +fileprivate extension Optional { + + /// Converts the optional to a `GraphQLNullable. + /// + /// - Double nested optional (ie. `Optional.some(nil)`) -> `GraphQLNullable.null`. + /// - `Optional.none` -> `GraphQLNullable.none` + /// - `Optional.some` -> `GraphQLNullable.some` + var asNullable: GraphQLNullable { + unwrapAsNullable() + } + + private func unwrapAsNullable(nullIfNil: Bool = false) -> GraphQLNullable { + switch self { + case .none: return nullIfNil ? .null : .none + + case .some(let value as any AnyOptional): + return (value as! Self).unwrapAsNullable(nullIfNil: true) + + case .some(is NSNull): + return .null + + case .some(let value): + return .some(value) + } + } +} From 7ae68749518228762174b74585e0d13223e4900c Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:26:19 -0700 Subject: [PATCH 07/41] Add integration test --- .../SelectionSet_EqualityTests.swift | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Tests/ApolloTests/SelectionSet_EqualityTests.swift b/Tests/ApolloTests/SelectionSet_EqualityTests.swift index 26d49ab0d..3e4f278d6 100644 --- a/Tests/ApolloTests/SelectionSet_EqualityTests.swift +++ b/Tests/ApolloTests/SelectionSet_EqualityTests.swift @@ -691,6 +691,76 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } + // MARK: - Integration Tests + + func test__equatable__givenQueryResponseFetchedFromStore() + async throws + { + // given + class GivenSelectionSet: MockSelectionSet { + override class var __selections: [Selection] { + [ + .field("hero", Hero.self) + ] + } + var hero: Hero { __data["hero"] } + + class Hero: MockSelectionSet { + override class var __selections: [Selection] { + [ + .field("__typename", String.self), + .field("name", String.self), + .field("friend", Friend.self), + ] + } + var friend: Friend { __data["friend"] } + + class Friend: MockSelectionSet { + override class var __selections: [Selection] { + [ + .field("__typename", String.self), + .field("name", String.self), + ] + } + var name: String { __data["name"] } + } + } + } + + let store = ApolloStore(cache: InMemoryNormalizedCache()) + store.publish(records: [ + "QUERY_ROOT": ["hero": CacheReference("hero")], + "hero": [ + "__typename": "Droid", + "name": "R2-D2", + "friend": CacheReference("1000"), + ], + "1000": ["__typename": "Human", "name": "Luke Skywalker"], + ]) + + let expected = try GivenSelectionSet(data: [ + "hero": [ + "__typename": "Droid", + "name": "R2-D2", + "friend": ["__typename": "Human", "name": "Luke Skywalker"] + ] + ]) + + // when + let updateCompletedExpectation = expectation(description: "Update completed") + + store.load(MockQuery()) { result in + defer { updateCompletedExpectation.fulfill() } + + XCTAssertSuccessResult(result) + let responseData = try! result.get().data + + expect(responseData).to(equal(expected)) + } + + await fulfillment(of: [updateCompletedExpectation], timeout: 1.0) + } + // MARK: - Null/nil tests func test__equatable__optionalChildObject__isNullOnBoth_returns_true() { From f28dff69ab9ffc8662a0640e93a2ffd997c09fbd Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:41:31 -0700 Subject: [PATCH 08/41] Refactor scalar casting logic for nullable object types --- .../ApolloAPI/SelectionSet+Equatable.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index d7df415c5..d45af8ae5 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -90,14 +90,14 @@ public extension SelectionSet { field: Selection.Field, to fields: inout [String: FieldValue] ) { - let nullableFieldData = self.__data._data[field.responseKey].asNullable - let fieldData: FieldValue - switch nullableFieldData { - case let .some(value): - fieldData = value - case .none, .null: - return - } + let nullableFieldData = (self.__data._data[field.responseKey]?.base as? FieldValue).asNullable + let fieldData: FieldValue + switch nullableFieldData { + case let .some(value): + fieldData = value + case .none, .null: + return + } addData(for: field.type) From cc8a2de6c818c17ba28aba32f6f38e6f2ff5fd45 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:41:55 -0700 Subject: [PATCH 09/41] Refactor scalar casting logic for lists --- .../ApolloAPI/SelectionSet+Equatable.swift | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index d45af8ae5..a1db2123c 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -104,7 +104,15 @@ public extension SelectionSet { func addData(for type: Selection.Field.OutputType, inList: Bool = false) { switch type { case .scalar, .customScalar: - fields[field.responseKey] = fieldData + if inList { + guard let listData = fieldData as? [AnyHashable] else { + preconditionFailure("Expected list data for field: \(field)") + } + + fields[field.responseKey] = unwrapAnyHashable(list: listData) as FieldValue + } else { + fields[field.responseKey] = fieldData + } case let .nonNull(innerType): addData(for: innerType, inList: inList) @@ -146,6 +154,22 @@ public extension SelectionSet { preconditionFailure("Expected list data to contain objects.") } + private func unwrapAnyHashable( + list: [AnyHashable] + ) -> [FieldValue] { + if let nestedList = list as? [[AnyHashable]] { + return nestedList.map { self.unwrapAnyHashable(list: $0) as FieldValue } + } + + return list.map { + guard let base = $0.base as? FieldValue else { + preconditionFailure("Expected list data to contain objects.") + } + return base + } + + } + private func addConditionalSelections( _ selections: [Selection], to fields: inout [String: FieldValue] From df986c51f35fe349377918dad08c1e444430ebb4 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:42:21 -0700 Subject: [PATCH 10/41] Fix and add tests --- .../SelectionSet_EqualityTests.swift | 343 ++++++++++++++---- 1 file changed, 271 insertions(+), 72 deletions(-) diff --git a/Tests/ApolloTests/SelectionSet_EqualityTests.swift b/Tests/ApolloTests/SelectionSet_EqualityTests.swift index 3e4f278d6..18b1f5268 100644 --- a/Tests/ApolloTests/SelectionSet_EqualityTests.swift +++ b/Tests/ApolloTests/SelectionSet_EqualityTests.swift @@ -7,7 +7,9 @@ import XCTest @MainActor class SelectionSet_EqualityTests: XCTestCase { - func test__equality__scalarString_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // MARK: Scalar tests + + func test__equatable__scalarString_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -41,9 +43,10 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) } - func test__equality__scalarString_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + func test__equatable__scalarString_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -79,7 +82,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarString_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + func test__equatable__scalarString_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -115,7 +118,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarStringList_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + func test__equatable__scalarStringList_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -149,9 +152,84 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) } - func test__equality__scalarStringList_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + func test__equatable__scalarStringMultidimensionalList_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("fieldValue", [[[String?]]].self) + ]} + + public var fieldValue: [[[String?]]] { __data["fieldValue"] } + + convenience init( + fieldValue: [[[String?]]] + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": fieldValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(fieldValue: [[["Han Solo"]]]) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "fieldValue": [[["Han Solo"]]] + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) + } + + func test__equatable__scalarStringMultidimensionalList_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("fieldValue", [[[String?]]].self) + ]} + + public var fieldValue: [[[String?]]] { __data["fieldValue"] } + + convenience init( + fieldValue: [[[String?]]] + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": fieldValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let initializerHero = Hero(fieldValue: [[["Han Solo"]]]) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "fieldValue": [[["Luke Skywalker"]]] + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + XCTAssertNotEqual(initializerHero, dataDictHero) + expect(initializerHero).notTo(equal(dataDictHero)) + } + + func test__equatable__scalarStringList_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -187,7 +265,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarStringList_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + func test__equatable__scalarStringList_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -223,7 +301,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarInt_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + func test__equatable__scalarInt_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -257,9 +335,10 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) } - func test__equality__scalarInt_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + func test__equatable__scalarInt_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -295,7 +374,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarInt_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + func test__equatable__scalarInt_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -331,7 +410,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarBool_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + func test__equatable__scalarBool_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -365,9 +444,10 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) } - func test__equality__scalarBool_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + func test__equatable__scalarBool_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -403,7 +483,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarBool_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + func test__equatable__scalarBool_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -439,7 +519,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarFloat_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + func test__equatable__scalarFloat_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -473,9 +553,10 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) } - func test__equality__scalarFloat_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + func test__equatable__scalarFloat_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -511,7 +592,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarFloat_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + func test__equatable__scalarFloat_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -547,7 +628,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarDouble_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { + func test__equatable__scalarDouble_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -581,9 +662,10 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) } - func test__equality__scalarDouble_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + func test__equatable__scalarDouble_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -619,7 +701,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalarDouble_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + func test__equatable__scalarDouble_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -655,7 +737,7 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - func test__equality__scalar_givenDataDictValueOfDifferentTypeThatCannotCastToFieldType_shouldNotBeEqual() throws { + func test__equatable__scalar_givenDataDictValueOfDifferentTypeThatCannotCastToFieldType_shouldNotBeEqual() throws { // given class Hero: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -691,74 +773,119 @@ class SelectionSet_EqualityTests: XCTestCase { expect(initializerHero).notTo(equal(dataDictHero)) } - // MARK: - Integration Tests - - func test__equatable__givenQueryResponseFetchedFromStore() - async throws - { + func test__equatable__customScalar_givenOptionalityOpposedDataDictValue_sameValue_shouldBeEqual() throws { // given - class GivenSelectionSet: MockSelectionSet { - override class var __selections: [Selection] { - [ - .field("hero", Hero.self) - ] - } - var hero: Hero { __data["hero"] } + typealias GivenCustomScalar = MockCustomScalar - class Hero: MockSelectionSet { - override class var __selections: [Selection] { - [ - .field("__typename", String.self), - .field("name", String.self), - .field("friend", Friend.self), - ] - } - var friend: Friend { __data["friend"] } + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata - class Friend: MockSelectionSet { - override class var __selections: [Selection] { - [ - .field("__typename", String.self), - .field("name", String.self), - ] - } - var name: String { __data["name"] } - } + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("fieldValue", GivenCustomScalar?.self) + ]} + + public var fieldValue: GivenCustomScalar? { __data["fieldValue"] } + + convenience init( + fieldValue: GivenCustomScalar? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": fieldValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) } } - let store = ApolloStore(cache: InMemoryNormalizedCache()) - store.publish(records: [ - "QUERY_ROOT": ["hero": CacheReference("hero")], - "hero": [ - "__typename": "Droid", - "name": "R2-D2", - "friend": CacheReference("1000"), + // when + let initializerHero = Hero(fieldValue: GivenCustomScalar(value: 989561700)) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "fieldValue": GivenCustomScalar(value: 989561700) // non-optional to oppose .field selection type ], - "1000": ["__typename": "Human", "name": "Luke Skywalker"], - ]) + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) - let expected = try GivenSelectionSet(data: [ - "hero": [ - "__typename": "Droid", - "name": "R2-D2", - "friend": ["__typename": "Human", "name": "Luke Skywalker"] - ] - ]) + // then + expect(initializerHero).to(equal(dataDictHero)) + expect(initializerHero.hashValue).to(equal(dataDictHero.hashValue)) + } + + func test__equatable__customScalar_givenOptionalityOpposedDataDictValue_differentValue_shouldNotBeEqual() throws { + // given + typealias GivenCustomScalar = MockCustomScalar + + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("fieldValue", GivenCustomScalar?.self) + ]} + + public var fieldValue: GivenCustomScalar? { __data["fieldValue"] } + + convenience init( + fieldValue: GivenCustomScalar? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": fieldValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } // when - let updateCompletedExpectation = expectation(description: "Update completed") + let initializerHero = Hero(fieldValue: GivenCustomScalar(value: 989561700)) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "fieldValue": GivenCustomScalar(value: 123) // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) - store.load(MockQuery()) { result in - defer { updateCompletedExpectation.fulfill() } + // then + expect(initializerHero).notTo(equal(dataDictHero)) + } - XCTAssertSuccessResult(result) - let responseData = try! result.get().data + func test__equatable__customScalar_givenOptionalityOpposedDataDictValue_nilValue_shouldNotBeEqual() throws { + // given + typealias GivenCustomScalar = MockCustomScalar - expect(responseData).to(equal(expected)) + class Hero: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("fieldValue", GivenCustomScalar?.self) + ]} + + public var fieldValue: GivenCustomScalar? { __data["fieldValue"] } + + convenience init( + fieldValue: GivenCustomScalar? + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": "Hero", + "fieldValue": fieldValue + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } } - await fulfillment(of: [updateCompletedExpectation], timeout: 1.0) + // when + let initializerHero = Hero(fieldValue: nil) + let dataDictHero = Hero(_dataDict: DataDict( + data: [ + "__typename": "Hero", + "fieldValue": GivenCustomScalar(value: 123) // non-optional to oppose .field selection type + ], + fulfilledFragments: [ObjectIdentifier(Hero.self)] + )) + + // then + expect(initializerHero).notTo(equal(dataDictHero)) } // MARK: - Null/nil tests @@ -792,6 +919,7 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(selectionSet1).to(equal(selectionSet2)) + expect(selectionSet1.hashValue).to(equal(selectionSet2.hashValue)) } func test__equatable__optionalChildObject__isNullAndNil_returns_true() { @@ -823,6 +951,77 @@ class SelectionSet_EqualityTests: XCTestCase { // then expect(selectionSet1).to(equal(selectionSet2)) + expect(selectionSet1.hashValue).to(equal(selectionSet2.hashValue)) + } + + // MARK: - Integration Tests + + func test__equatable__givenQueryResponseFetchedFromStore() + async throws + { + // given + class GivenSelectionSet: MockSelectionSet { + override class var __selections: [Selection] { + [ + .field("hero", Hero.self) + ] + } + var hero: Hero { __data["hero"] } + + class Hero: MockSelectionSet { + override class var __selections: [Selection] { + [ + .field("__typename", String.self), + .field("name", String.self), + .field("friend", Friend.self), + ] + } + var friend: Friend { __data["friend"] } + + class Friend: MockSelectionSet { + override class var __selections: [Selection] { + [ + .field("__typename", String.self), + .field("name", String.self), + ] + } + var name: String { __data["name"] } + } + } + } + + let store = ApolloStore(cache: InMemoryNormalizedCache()) + store.publish(records: [ + "QUERY_ROOT": ["hero": CacheReference("hero")], + "hero": [ + "__typename": "Droid", + "name": "R2-D2", + "friend": CacheReference("1000"), + ], + "1000": ["__typename": "Human", "name": "Luke Skywalker"], + ]) + + let expected = try GivenSelectionSet(data: [ + "hero": [ + "__typename": "Droid", + "name": "R2-D2", + "friend": ["__typename": "Human", "name": "Luke Skywalker"] + ] + ]) + + // when + let updateCompletedExpectation = expectation(description: "Update completed") + + store.load(MockQuery()) { result in + defer { updateCompletedExpectation.fulfill() } + + XCTAssertSuccessResult(result) + let responseData = try! result.get().data + + expect(responseData).to(equal(expected)) + } + + await fulfillment(of: [updateCompletedExpectation], timeout: 1.0) } } From 893a4f57c61a355833c80bafc75de72a3c9a9f34 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 29 Sep 2025 14:42:43 -0700 Subject: [PATCH 11/41] Remove test mock infrastructure hash and equality implementations --- Tests/ApolloInternalTestHelpers/MockOperation.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Tests/ApolloInternalTestHelpers/MockOperation.swift b/Tests/ApolloInternalTestHelpers/MockOperation.swift index cfad662b1..7339fbb5c 100644 --- a/Tests/ApolloInternalTestHelpers/MockOperation.swift +++ b/Tests/ApolloInternalTestHelpers/MockOperation.swift @@ -66,14 +66,6 @@ open class AbstractMockSelectionSet: RootSelectionSet, Has public subscript(dynamicMember key: String) -> T? { __data[key] } - - public static func == (lhs: MockSelectionSet, rhs: MockSelectionSet) -> Bool { - lhs.__data == rhs.__data - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(__data) - } } public typealias MockSelectionSet = AbstractMockSelectionSet From 0974a19b443dda902878b131ee603096a09c2926 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Thu, 2 Oct 2025 14:42:54 -0700 Subject: [PATCH 12/41] debugging --- .../BidirectionalPaginationTests.swift | 18 ++++++++ .../ForwardPaginationTests.swift | 18 ++++++++ .../ReversePaginationTests.swift | 9 ++++ .../Apollo-PaginationTestPlan.xctestplan | 45 +++++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index ad2faddaf..bbd6507a1 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -126,6 +126,15 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { try XCTAssertSuccessResult(result) { output in // Assert first page is unchanged + let first = try? results.first?.get().initialPage + let last = try? results.last?.get().initialPage + print(""" + \(#function) - equality + first: \(first) + last: \(last) + data equal: \(first?.data == last?.data) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.nextPages.isEmpty) @@ -242,6 +251,15 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { try XCTAssertSuccessResult(result) { output in // Assert first page is unchanged + let first = try? results.first?.get().initialPage + let last = try? results.last?.get().initialPage + print(""" + \(#function) - equality + first: \(first) + last: \(last) + data equal: \(first?.data == last?.data) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.nextPages.isEmpty) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 2cdcea764..2735bdbe7 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -81,6 +81,15 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { try XCTAssertSuccessResult(result) { output in // Assert first page is unchanged + let first = try? results.first?.get().initialPage + let last = try? results.last?.get().initialPage + print(""" + \(#function) - equality + first: \(first) + last: \(last) + data equal: \(first?.data == last?.data) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.nextPages.isEmpty) @@ -204,6 +213,15 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { let newResult = try await XCTUnwrapping(await pager.currentValue) try XCTAssertSuccessResult(newResult) { output in // Assert first page is unchanged + let first = try? result.get().initialPage + let last = try? newResult.get().initialPage + print(""" + \(#function) - equality + first: \(first) + last: \(last) + data equal: \(first?.data == last?.data) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + """) XCTAssertEqual(try? result.get().initialPage, try? newResult.get().initialPage) XCTAssertFalse(output.nextPages.isEmpty) XCTAssertEqual(output.nextPages.count, 1) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index b6db7b2f6..aab95cb53 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -81,6 +81,15 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { try XCTAssertSuccessResult(result) { output in // Assert first page is unchanged + let first = try? results.first?.get().initialPage + let last = try? results.last?.get().initialPage + print(""" + \(#function) - equality + first: \(first) + last: \(last) + data equal: \(first?.data == last?.data) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.previousPages.isEmpty) diff --git a/Tests/TestPlans/Apollo-PaginationTestPlan.xctestplan b/Tests/TestPlans/Apollo-PaginationTestPlan.xctestplan index 8104192d8..e476245ad 100644 --- a/Tests/TestPlans/Apollo-PaginationTestPlan.xctestplan +++ b/Tests/TestPlans/Apollo-PaginationTestPlan.xctestplan @@ -14,6 +14,51 @@ "testTargets" : [ { "parallelizable" : true, + "skippedTests" : [ + "AsyncGraphQLQueryPagerCoordinatorTests", + "AsyncGraphQLQueryPagerCoordinatorTests\/test__reset__loadingState()", + "AsyncGraphQLQueryPagerCoordinatorTests\/test__reset__midflight_isFetching_isFalse()", + "AsyncGraphQLQueryPagerCoordinatorTests\/test_actor_canResetMidflight()", + "AsyncGraphQLQueryPagerCoordinatorTests\/test_canLoadMore()", + "AsyncGraphQLQueryPagerCoordinatorTests\/test_canLoadPrevious()", + "AsyncGraphQLQueryPagerTests", + "AsyncGraphQLQueryPagerTests\/test_concatenatesPages_matchingInitialAndPaginated()", + "AsyncGraphQLQueryPagerTests\/test_errors_noData()", + "AsyncGraphQLQueryPagerTests\/test_errors_noDataOnSecondPage_loadAll()", + "AsyncGraphQLQueryPagerTests\/test_errors_noData_loadAll()", + "AsyncGraphQLQueryPagerTests\/test_errors_partialSuccess()", + "AsyncGraphQLQueryPagerTests\/test_forwardInit_simple()", + "AsyncGraphQLQueryPagerTests\/test_forwardInit_simple_mapping()", + "AsyncGraphQLQueryPagerTests\/test_forwardInit_singleQuery_transform()", + "AsyncGraphQLQueryPagerTests\/test_forwardInit_singleQuery_transform_mapping()", + "AsyncGraphQLQueryPagerTests\/test_loadAll()", + "AsyncGraphQLQueryPagerTests\/test_passesBackSeparateData()", + "BidirectionalPaginationTests\/test_loadAll()", + "BidirectionalPaginationTests\/test_loadAll_async()", + "ConcurrencyTests", + "ConcurrencyTests\/test_concurrentFetches()", + "ConcurrencyTests\/test_concurrentFetchesThrowsError()", + "ConcurrencyTests\/test_concurrentFetches_nonisolated()", + "ForwardPaginationTests\/test_failingFetch_finishes()", + "ForwardPaginationTests\/test_loadAll()", + "ForwardPaginationTests\/test_paginationState()", + "ForwardPaginationTests\/test_variableMapping()", + "GraphQLQueryPagerCoordinatorTests", + "GraphQLQueryPagerCoordinatorTests\/test__reset__calls_callback()", + "GraphQLQueryPagerCoordinatorTests\/test__reset__calls_callback_deinit()", + "GraphQLQueryPagerCoordinatorTests\/test__reset__calls_callback_manyQueuedRequests()", + "GraphQLQueryPagerTests", + "GraphQLQueryPagerTests\/test_concatenatesPages_matchingInitialAndPaginated()", + "GraphQLQueryPagerTests\/test_pager_reset_calls_callback()", + "GraphQLQueryPagerTests\/test_passesBackSeparateData()", + "GraphQLQueryPagerTests\/test_reversePager_loadPrevious()", + "GraphQLQueryPagerTests\/test_transformless_init()", + "OffsetTests", + "OffsetTests\/test_concatenatesPages_matchingInitialAndPaginated()", + "ReversePaginationTests\/test_loadAll()", + "SubscribeTest", + "SubscribeTest\/test_multipleSubscribers()" + ], "target" : { "containerPath" : "container:ApolloDev.xcodeproj", "identifier" : "F13FB1AFFD21C03856AD8A03", From 26e9a2a43daa9f00dc507026a9bee5a6c910f40c Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:26:54 -0700 Subject: [PATCH 13/41] debugging --- .github/actions/build-and-run-unit-tests/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/build-and-run-unit-tests/action.yml b/.github/actions/build-and-run-unit-tests/action.yml index 27cc62aa0..3a4d6bb7f 100644 --- a/.github/actions/build-and-run-unit-tests/action.yml +++ b/.github/actions/build-and-run-unit-tests/action.yml @@ -16,4 +16,4 @@ runs: - name: Build and Test shell: bash run: | - xcodebuild clean test -resultBundlePath TestResults/ResultBundle.xcresult -derivedDataPath DerivedData -workspace "ApolloDev.xcworkspace" -scheme "${{ inputs.scheme }}" -destination "${{ inputs.destination }}" -testPlan "${{ inputs.test-plan }}" | xcbeautify \ No newline at end of file + xcodebuild clean test -resultBundlePath TestResults/ResultBundle.xcresult -derivedDataPath DerivedData -workspace "ApolloDev.xcworkspace" -scheme "${{ inputs.scheme }}" -destination "${{ inputs.destination }}" -testPlan "${{ inputs.test-plan }}" -verbose -parallel-testing-enabled NO \ No newline at end of file From 29977465d97777c668d10247b8e118a3f43b04a6 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:58:12 -0700 Subject: [PATCH 14/41] debugging --- .../BidirectionalPaginationTests.swift | 18 ++++++++++++++++-- .../ForwardPaginationTests.swift | 18 ++++++++++++++++-- .../ReversePaginationTests.swift | 7 +++++++ 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index bbd6507a1..8434dce4a 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -133,7 +133,14 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { first: \(first) last: \(last) data equal: \(first?.data == last?.data) - data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) + data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) + data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) + data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) + data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) + data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) @@ -258,7 +265,14 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { first: \(first) last: \(last) data equal: \(first?.data == last?.data) - data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) + data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) + data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) + data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) + data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) + data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 2735bdbe7..7106ea487 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -88,7 +88,14 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { first: \(first) last: \(last) data equal: \(first?.data == last?.data) - data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) + data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) + data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) + data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) + data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) + data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) @@ -220,7 +227,14 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { first: \(first) last: \(last) data equal: \(first?.data == last?.data) - data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) + data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) + data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) + data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) + data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) + data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) XCTAssertEqual(try? result.get().initialPage, try? newResult.get().initialPage) XCTAssertFalse(output.nextPages.isEmpty) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index aab95cb53..dcd3282b9 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -89,6 +89,13 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { last: \(last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) + data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) + data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) + data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) + data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) + data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) + data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) From 8d22e4688383f91d25732b6314a278e173787302 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:35:30 -0700 Subject: [PATCH 15/41] debugging --- Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift | 2 ++ Tests/ApolloPaginationTests/ForwardPaginationTests.swift | 2 ++ Tests/ApolloPaginationTests/ReversePaginationTests.swift | 1 + 3 files changed, 5 insertions(+) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index 8434dce4a..ce0410dca 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -137,6 +137,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) @@ -269,6 +270,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 7106ea487..7ec1e32f8 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -92,6 +92,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) @@ -231,6 +232,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index dcd3282b9..00dc50ce0 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -92,6 +92,7 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) + data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) From 3041d77b0dba7bae4d6b53cdc5b27faf1132b7fd Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:24:31 -0700 Subject: [PATCH 16/41] debugging --- .../BidirectionalPaginationTests.swift | 6 ++++++ Tests/ApolloPaginationTests/ForwardPaginationTests.swift | 6 ++++++ Tests/ApolloPaginationTests/ReversePaginationTests.swift | 3 +++ 3 files changed, 15 insertions(+) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index ce0410dca..3a0a6f6ad 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -138,6 +138,9 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) + data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) @@ -271,6 +274,9 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) + data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 7ec1e32f8..13be87e55 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -93,6 +93,9 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) + data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) @@ -233,6 +236,9 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) + data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 00dc50ce0..eaf6b93a6 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -93,6 +93,9 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) + data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) From bb8db912b703841ca3274d1886c05364d7433f90 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 10:59:30 -0700 Subject: [PATCH 17/41] debugging --- .../BidirectionalPaginationTests.swift | 6 ++++-- Tests/ApolloPaginationTests/ForwardPaginationTests.swift | 6 ++++-- Tests/ApolloPaginationTests/ReversePaginationTests.swift | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index 3a0a6f6ad..d20ff82ca 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -138,8 +138,9 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) - data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) @@ -274,8 +275,9 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) - data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 13be87e55..9bb35b5c2 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -93,8 +93,9 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) - data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) @@ -236,8 +237,9 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) - data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index eaf6b93a6..1e8a5131a 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -93,8 +93,9 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) - data.hero.friendsConnection equal.__data._fulfilledFragments: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) + data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) From 3badf52c5b5944a9e40a3edb36261a2f2cc2ee15 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 12:49:12 -0700 Subject: [PATCH 18/41] debugging --- .../BidirectionalPaginationTests.swift | 9 +++++---- Tests/ApolloPaginationTests/ForwardPaginationTests.swift | 8 ++++---- Tests/ApolloPaginationTests/ReversePaginationTests.swift | 4 ++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index d20ff82ca..ecc5401cf 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -128,16 +128,17 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { // Assert first page is unchanged let first = try? results.first?.get().initialPage let last = try? results.last?.get().initialPage + let _ = first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection print(""" \(#function) - equality - first: \(first) - last: \(last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) + last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) @@ -267,14 +268,14 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { let last = try? results.last?.get().initialPage print(""" \(#function) - equality - first: \(first) - last: \(last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) + last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 9bb35b5c2..b7c4cc690 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -85,14 +85,14 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { let last = try? results.last?.get().initialPage print(""" \(#function) - equality - first: \(first) - last: \(last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) + last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) @@ -229,14 +229,14 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { let last = try? newResult.get().initialPage print(""" \(#function) - equality - first: \(first) - last: \(last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) + last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 1e8a5131a..7046a582e 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -85,14 +85,14 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { let last = try? results.last?.get().initialPage print(""" \(#function) - equality - first: \(first) - last: \(last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) + first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) + last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) From 494f9c8b3ab336c8c7fdcb629c6401420897ec3f Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:15:05 -0700 Subject: [PATCH 19/41] debugging --- Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift | 2 ++ Tests/ApolloPaginationTests/ForwardPaginationTests.swift | 2 ++ Tests/ApolloPaginationTests/ReversePaginationTests.swift | 1 + 3 files changed, 5 insertions(+) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index ecc5401cf..7c19d9aa1 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -131,6 +131,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { let _ = first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection print(""" \(#function) - equality + equal: \(first == last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) @@ -268,6 +269,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { let last = try? results.last?.get().initialPage print(""" \(#function) - equality + equal: \(first == last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index b7c4cc690..1ca9a52c5 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -85,6 +85,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { let last = try? results.last?.get().initialPage print(""" \(#function) - equality + equal: \(first == last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) @@ -229,6 +230,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { let last = try? newResult.get().initialPage print(""" \(#function) - equality + equal: \(first == last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 7046a582e..7aaf33250 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -85,6 +85,7 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { let last = try? results.last?.get().initialPage print(""" \(#function) - equality + equal: \(first == last) data equal: \(first?.data == last?.data) data.hero equal: \(first?.data?.hero == last?.data?.hero) data.hero.__typename equal: \(first?.data?.hero.__typename == last?.data?.hero.__typename) From 5aa2576179c3beb75e7a5f66d8aa9b21db29d1ca Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:15:12 -0700 Subject: [PATCH 20/41] debugging --- apollo-ios/Sources/ApolloAPI/DataDict.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/DataDict.swift b/apollo-ios/Sources/ApolloAPI/DataDict.swift index e6f53873a..df093eff6 100644 --- a/apollo-ios/Sources/ApolloAPI/DataDict.swift +++ b/apollo-ios/Sources/ApolloAPI/DataDict.swift @@ -174,11 +174,11 @@ extension DataDict { /// we need to do some additional unwrapping and casting of the values to avoid crashes and other /// run time bugs. public static let _AnyHashableCanBeCoerced: Bool = { - if #available(iOS 14.5, macOS 11.3, tvOS 14.5, watchOS 7.4, *) { +// if #available(iOS 14.5, macOS 11.3, tvOS 14.5, watchOS 7.4, *) { return true - } else { - return false - } +// } else { +// return false +// } }() } From 7bea696a9882ac643ecdd0b2152abf588cd1b7db Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:30:48 -0700 Subject: [PATCH 21/41] Revert "debugging" This reverts commit 5aa2576179c3beb75e7a5f66d8aa9b21db29d1ca. --- apollo-ios/Sources/ApolloAPI/DataDict.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/DataDict.swift b/apollo-ios/Sources/ApolloAPI/DataDict.swift index df093eff6..e6f53873a 100644 --- a/apollo-ios/Sources/ApolloAPI/DataDict.swift +++ b/apollo-ios/Sources/ApolloAPI/DataDict.swift @@ -174,11 +174,11 @@ extension DataDict { /// we need to do some additional unwrapping and casting of the values to avoid crashes and other /// run time bugs. public static let _AnyHashableCanBeCoerced: Bool = { -// if #available(iOS 14.5, macOS 11.3, tvOS 14.5, watchOS 7.4, *) { + if #available(iOS 14.5, macOS 11.3, tvOS 14.5, watchOS 7.4, *) { return true -// } else { -// return false -// } + } else { + return false + } }() } From 206689a1edd06a1c8ee058765a873edde0b2f7fb Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:32:42 -0700 Subject: [PATCH 22/41] debugging --- Tests/ApolloPaginationTests/Mocks.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ApolloPaginationTests/Mocks.swift b/Tests/ApolloPaginationTests/Mocks.swift index c86056f0b..de1fa7e44 100644 --- a/Tests/ApolloPaginationTests/Mocks.swift +++ b/Tests/ApolloPaginationTests/Mocks.swift @@ -116,11 +116,11 @@ enum Mocks { class PageInfo: MockSelectionSet { override class var __selections: [Selection] {[ .field("__typename", String.self), - .field("startCursor", Optional.self), + .field("startCursor", String.self), .field("hasPreviousPage", Bool.self), ]} - var startCursor: String? { __data["startCursor"] } + var startCursor: String { __data["startCursor"] } var hasPreviousPage: Bool { __data["hasPreviousPage"] } } } From ec8b16815c99b9cc96178fbb368e9e191656a689 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:43:30 -0700 Subject: [PATCH 23/41] Revert "debugging" This reverts commit 206689a1edd06a1c8ee058765a873edde0b2f7fb. --- Tests/ApolloPaginationTests/Mocks.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ApolloPaginationTests/Mocks.swift b/Tests/ApolloPaginationTests/Mocks.swift index de1fa7e44..c86056f0b 100644 --- a/Tests/ApolloPaginationTests/Mocks.swift +++ b/Tests/ApolloPaginationTests/Mocks.swift @@ -116,11 +116,11 @@ enum Mocks { class PageInfo: MockSelectionSet { override class var __selections: [Selection] {[ .field("__typename", String.self), - .field("startCursor", String.self), + .field("startCursor", Optional.self), .field("hasPreviousPage", Bool.self), ]} - var startCursor: String { __data["startCursor"] } + var startCursor: String? { __data["startCursor"] } var hasPreviousPage: Bool { __data["hasPreviousPage"] } } } From fef4e3da4395f93f3812e0e1b516315ed1ef7973 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 13:47:21 -0700 Subject: [PATCH 24/41] debugging --- .../BidirectionalPaginationTests.swift | 10 ++++++++++ .../ApolloPaginationTests/ForwardPaginationTests.swift | 10 ++++++++++ .../ApolloPaginationTests/ReversePaginationTests.swift | 5 +++++ 3 files changed, 25 insertions(+) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index 7c19d9aa1..fb79eed98 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -149,6 +149,11 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) + for index in 0.. Date: Fri, 3 Oct 2025 14:03:49 -0700 Subject: [PATCH 25/41] debugging --- .../BidirectionalPaginationTests.swift | 10 ---------- .../ApolloPaginationTests/ForwardPaginationTests.swift | 10 ---------- .../ApolloPaginationTests/ReversePaginationTests.swift | 5 ----- 3 files changed, 25 deletions(-) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index fb79eed98..7c19d9aa1 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -149,11 +149,6 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) - for index in 0.. Date: Fri, 3 Oct 2025 14:20:35 -0700 Subject: [PATCH 26/41] debugging --- .github/workflows/ci-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 5c8f3f677..f6733ff28 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -5,7 +5,7 @@ on: types: [opened, synchronize, reopened] env: - XCODE_VERSION: "15.4" + XCODE_VERSION: "16.2" jobs: changes: From a99e646710c25f142c356dba8eff72e000a03d20 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:40:20 -0700 Subject: [PATCH 27/41] Revert "debugging" This reverts commit b7e2b103c132d0dcdf072c3aadb859cd09a16100. --- .github/workflows/ci-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index f6733ff28..5c8f3f677 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -5,7 +5,7 @@ on: types: [opened, synchronize, reopened] env: - XCODE_VERSION: "16.2" + XCODE_VERSION: "15.4" jobs: changes: From 3e6f98fe0c9e2d7497fb74dd21c3744c82a66b14 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 14:42:13 -0700 Subject: [PATCH 28/41] debugging --- .../BidirectionalPaginationTests.swift | 6 ++---- Tests/ApolloPaginationTests/ForwardPaginationTests.swift | 6 ++---- Tests/ApolloPaginationTests/ReversePaginationTests.swift | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift index 7c19d9aa1..35bb19082 100644 --- a/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift +++ b/Tests/ApolloPaginationTests/BidirectionalPaginationTests.swift @@ -138,8 +138,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) - first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) - last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) + hashes - lhs:\(first?.data?.hero.friendsConnection.hashValue) rhs:\(last?.data?.hero.friendsConnection.hashValue) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) @@ -276,8 +275,7 @@ final class BidirectionalPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) - first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) - last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) + hashes - lhs:\(first?.data?.hero.friendsConnection.hashValue) rhs:\(last?.data?.hero.friendsConnection.hashValue) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) diff --git a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift index 1ca9a52c5..4e6e1beb6 100644 --- a/Tests/ApolloPaginationTests/ForwardPaginationTests.swift +++ b/Tests/ApolloPaginationTests/ForwardPaginationTests.swift @@ -92,8 +92,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) - first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) - last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) + hashes - lhs:\(first?.data?.hero.friendsConnection.hashValue) rhs:\(last?.data?.hero.friendsConnection.hashValue) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) @@ -237,8 +236,7 @@ final class ForwardPaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) - first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) - last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) + hashes - lhs:\(first?.data?.hero.friendsConnection.hashValue) rhs:\(last?.data?.hero.friendsConnection.hashValue) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 7aaf33250..139bc3497 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -92,8 +92,7 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.id equal: \(first?.data?.hero.id == last?.data?.hero.id) data.hero.name equal: \(first?.data?.hero.name == last?.data?.hero.name) data.hero.friendsConnection equal: \(first?.data?.hero.friendsConnection == last?.data?.hero.friendsConnection) - first.data.hero.friendsConnection: \(first?.data?.hero.friendsConnection) - last.dat.hero.friendsConnection: \(last?.data?.hero.friendsConnection) + hashes - lhs:\(first?.data?.hero.friendsConnection.hashValue) rhs:\(last?.data?.hero.friendsConnection.hashValue) data.hero.friendsConnection.__data equal: \(first?.data?.hero.friendsConnection.__data == last?.data?.hero.friendsConnection.__data) data.hero.friendsConnection.__data._data equal: \(first?.data?.hero.friendsConnection.__data._data == last?.data?.hero.friendsConnection.__data._data) data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) From eceb242d530c0f347e12f0d457af07f1cba40804 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:57:42 -0700 Subject: [PATCH 29/41] debugging --- .../ApolloPaginationTests/ReversePaginationTests.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 139bc3497..f2d53e168 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -102,6 +102,16 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) + if first != last { + print(""" + first as JSON: + \(first?.asJSONDictionary()) + last as JSON: + \(last?.asJSONDictionary()) + """) + } else { + print("THEY'RE EQUAL!!!!") + } XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.previousPages.isEmpty) From a798963d49d722b691637a5ef01d1b9574dfe5d2 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:38:06 -0700 Subject: [PATCH 30/41] debugging --- .../ReversePaginationTests.swift | 10 - .../ApolloAPI/SelectionSet+Equatable.swift | 542 +++++++++--------- .../Sources/ApolloAPI/SelectionSet.swift | 8 + 3 files changed, 279 insertions(+), 281 deletions(-) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index f2d53e168..139bc3497 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -102,16 +102,6 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) - if first != last { - print(""" - first as JSON: - \(first?.asJSONDictionary()) - last as JSON: - \(last?.asJSONDictionary()) - """) - } else { - print("THEY'RE EQUAL!!!!") - } XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.previousPages.isEmpty) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index a1db2123c..3aae3a248 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -1,271 +1,271 @@ -import Foundation - -public extension SelectionSet { - - typealias FieldValue = any Hashable - - /// Creates a hash using a narrowly scoped algorithm that only combines fields in the underlying data - /// that are relevant to the `SelectionSet`. This ensures that hashes for a fragment do not - /// consider fields that are not included in the fragment, even if they are present in the data. - func hash(into hasher: inout Hasher) { - hasher.combine(self.fieldsForEquality()) - } - - /// Checks for equality using a narrowly scoped algorithm that only compares fields in the underlying data - /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not - /// consider fields that are not included in the fragment, even if they are present in the data. - static func ==(lhs: Self, rhs: Self) -> Bool { - return Self.equatableCheck( - lhs.fieldsForEquality(), - rhs.fieldsForEquality() - ) - } - - @inlinable - internal static func equatableCheck( - _ lhs: [T: any Hashable], - _ rhs: [T: any Hashable] - ) -> Bool { - guard lhs.keys == rhs.keys else { return false } - - return lhs.allSatisfy { - guard let rhsValue = rhs[$0.key], - equatableCheck($0.value, rhsValue) else { - return false - } - return true - } - } - - @inlinable - internal static func equatableCheck( - _ lhs: T, - _ rhs: any Hashable - ) -> Bool { - lhs == rhs as? T - } - - private func fieldsForEquality() -> [String: FieldValue] { - var fields: [String: FieldValue] = [:] - if let asTypeCase = self as? any InlineFragment { - self.addFulfilledSelections(of: type(of: asTypeCase.asRootEntityType), to: &fields) - - } else { - self.addFulfilledSelections(of: type(of: self), to: &fields) - - } - - return fields - } - - private func addFulfilledSelections( - of selectionSetType: any SelectionSet.Type, - to fields: inout [String: FieldValue] - ) { - guard self.__data.fragmentIsFulfilled(selectionSetType) else { - return - } - - for selection in selectionSetType.__selections { - switch selection { - case let .field(field): - add(field: field, to: &fields) - - case let .inlineFragment(typeCase): - self.addFulfilledSelections(of: typeCase, to: &fields) - - case let .conditional(_, selections): - self.addConditionalSelections(selections, to: &fields) - - case let .fragment(fragmentType): - self.addFulfilledSelections(of: fragmentType, to: &fields) - - case let .deferred(_, fragmentType, _): - self.addFulfilledSelections(of: fragmentType, to: &fields) - } - } - } - - private func add( - field: Selection.Field, - to fields: inout [String: FieldValue] - ) { - let nullableFieldData = (self.__data._data[field.responseKey]?.base as? FieldValue).asNullable - let fieldData: FieldValue - switch nullableFieldData { - case let .some(value): - fieldData = value - case .none, .null: - return - } - - addData(for: field.type) - - func addData(for type: Selection.Field.OutputType, inList: Bool = false) { - switch type { - case .scalar, .customScalar: - if inList { - guard let listData = fieldData as? [AnyHashable] else { - preconditionFailure("Expected list data for field: \(field)") - } - - fields[field.responseKey] = unwrapAnyHashable(list: listData) as FieldValue - } else { - fields[field.responseKey] = fieldData - } - - case let .nonNull(innerType): - addData(for: innerType, inList: inList) - - case let .list(innerType): - addData(for: innerType, inList: true) - - case let .object(selectionSetType): - switch inList { - case false: - guard let objectData = fieldData as? DataDict else { - preconditionFailure("Expected object data for object field: \(field)") - } - fields[field.responseKey] = selectionSetType.init(_dataDict: objectData) - - case true: - guard let listData = fieldData as? [FieldValue] else { - preconditionFailure("Expected list data for field: \(field)") - } - - fields[field.responseKey] = convertElements(of: listData, to: selectionSetType) as FieldValue - } - } - } - } - - private func convertElements( - of list: [FieldValue], - to selectionSetType: any RootSelectionSet.Type - ) -> [FieldValue] { - if let dataDictList = list as? [DataDict] { - return dataDictList.map { selectionSetType.init(_dataDict: $0) } - } - - if let nestedList = list as? [[FieldValue]] { - return nestedList.map { self.convertElements(of: $0, to: selectionSetType) as FieldValue } - } - - preconditionFailure("Expected list data to contain objects.") - } - - private func unwrapAnyHashable( - list: [AnyHashable] - ) -> [FieldValue] { - if let nestedList = list as? [[AnyHashable]] { - return nestedList.map { self.unwrapAnyHashable(list: $0) as FieldValue } - } - - return list.map { - guard let base = $0.base as? FieldValue else { - preconditionFailure("Expected list data to contain objects.") - } - return base - } - - } - - private func addConditionalSelections( - _ selections: [Selection], - to fields: inout [String: FieldValue] - ) { - for selection in selections { - switch selection { - case let .inlineFragment(typeCase): - self.addFulfilledSelections(of: typeCase, to: &fields) - - case let .fragment(fragment): - self.addFulfilledSelections(of: fragment, to: &fields) - - case let .deferred(_, fragment, _): - self.addFulfilledSelections(of: fragment, to: &fields) - - case let .conditional(_, selections): - addConditionalSelections(selections, to: &fields) - - case .field: - assertionFailure("Conditional selections should not directly include fields. They should use an InlineFragment instead.") - } - } - } - -} - -extension Hasher { - - @inlinable - public mutating func combine(_ optionalJSONValue: (any Hashable)?) { - if let value = optionalJSONValue { - self.combine(1 as UInt8) - self.combine(value) - } else { - // This mimics the implementation of combining a nil optional from the Swift language core - // Source reference at: - // https://github.com/swiftlang/swift/blob/main/stdlib/public/core/Optional.swift#L590 - self.combine(0 as UInt8) - } - } - - @inlinable - public mutating func combine( - _ dictionary: [T: any Hashable] - ) { - // From Dictionary's Hashable implementation - var commutativeHash = 0 - for (key, value) in dictionary { - var elementHasher = self - elementHasher.combine(key) - elementHasher.combine(AnyHashable(value)) - commutativeHash ^= elementHasher.finalize() - } - self.combine(commutativeHash) - } - - @inlinable - public mutating func combine( - _ dictionary: [T: any Hashable]? - ) { - if let value = dictionary { - self.combine(value) - } else { - self.combine(Optional<[T: any Hashable]>.none) - } - } -} - -fileprivate protocol AnyOptional {} - -@_spi(Internal) -extension Optional: AnyOptional { } - -fileprivate extension Optional { - - /// Converts the optional to a `GraphQLNullable. - /// - /// - Double nested optional (ie. `Optional.some(nil)`) -> `GraphQLNullable.null`. - /// - `Optional.none` -> `GraphQLNullable.none` - /// - `Optional.some` -> `GraphQLNullable.some` - var asNullable: GraphQLNullable { - unwrapAsNullable() - } - - private func unwrapAsNullable(nullIfNil: Bool = false) -> GraphQLNullable { - switch self { - case .none: return nullIfNil ? .null : .none - - case .some(let value as any AnyOptional): - return (value as! Self).unwrapAsNullable(nullIfNil: true) - - case .some(is NSNull): - return .null - - case .some(let value): - return .some(value) - } - } -} +//import Foundation +// +//public extension SelectionSet { +// +// typealias FieldValue = any Hashable +// +// /// Creates a hash using a narrowly scoped algorithm that only combines fields in the underlying data +// /// that are relevant to the `SelectionSet`. This ensures that hashes for a fragment do not +// /// consider fields that are not included in the fragment, even if they are present in the data. +// func hash(into hasher: inout Hasher) { +// hasher.combine(self.fieldsForEquality()) +// } +// +// /// Checks for equality using a narrowly scoped algorithm that only compares fields in the underlying data +// /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not +// /// consider fields that are not included in the fragment, even if they are present in the data. +// static func ==(lhs: Self, rhs: Self) -> Bool { +// return Self.equatableCheck( +// lhs.fieldsForEquality(), +// rhs.fieldsForEquality() +// ) +// } +// +// @inlinable +// internal static func equatableCheck( +// _ lhs: [T: any Hashable], +// _ rhs: [T: any Hashable] +// ) -> Bool { +// guard lhs.keys == rhs.keys else { return false } +// +// return lhs.allSatisfy { +// guard let rhsValue = rhs[$0.key], +// equatableCheck($0.value, rhsValue) else { +// return false +// } +// return true +// } +// } +// +// @inlinable +// internal static func equatableCheck( +// _ lhs: T, +// _ rhs: any Hashable +// ) -> Bool { +// lhs == rhs as? T +// } +// +// private func fieldsForEquality() -> [String: FieldValue] { +// var fields: [String: FieldValue] = [:] +// if let asTypeCase = self as? any InlineFragment { +// self.addFulfilledSelections(of: type(of: asTypeCase.asRootEntityType), to: &fields) +// +// } else { +// self.addFulfilledSelections(of: type(of: self), to: &fields) +// +// } +// +// return fields +// } +// +// private func addFulfilledSelections( +// of selectionSetType: any SelectionSet.Type, +// to fields: inout [String: FieldValue] +// ) { +// guard self.__data.fragmentIsFulfilled(selectionSetType) else { +// return +// } +// +// for selection in selectionSetType.__selections { +// switch selection { +// case let .field(field): +// add(field: field, to: &fields) +// +// case let .inlineFragment(typeCase): +// self.addFulfilledSelections(of: typeCase, to: &fields) +// +// case let .conditional(_, selections): +// self.addConditionalSelections(selections, to: &fields) +// +// case let .fragment(fragmentType): +// self.addFulfilledSelections(of: fragmentType, to: &fields) +// +// case let .deferred(_, fragmentType, _): +// self.addFulfilledSelections(of: fragmentType, to: &fields) +// } +// } +// } +// +// private func add( +// field: Selection.Field, +// to fields: inout [String: FieldValue] +// ) { +// let nullableFieldData = (self.__data._data[field.responseKey]?.base as? FieldValue).asNullable +// let fieldData: FieldValue +// switch nullableFieldData { +// case let .some(value): +// fieldData = value +// case .none, .null: +// return +// } +// +// addData(for: field.type) +// +// func addData(for type: Selection.Field.OutputType, inList: Bool = false) { +// switch type { +// case .scalar, .customScalar: +// if inList { +// guard let listData = fieldData as? [AnyHashable] else { +// preconditionFailure("Expected list data for field: \(field)") +// } +// +// fields[field.responseKey] = unwrapAnyHashable(list: listData) as FieldValue +// } else { +// fields[field.responseKey] = fieldData +// } +// +// case let .nonNull(innerType): +// addData(for: innerType, inList: inList) +// +// case let .list(innerType): +// addData(for: innerType, inList: true) +// +// case let .object(selectionSetType): +// switch inList { +// case false: +// guard let objectData = fieldData as? DataDict else { +// preconditionFailure("Expected object data for object field: \(field)") +// } +// fields[field.responseKey] = selectionSetType.init(_dataDict: objectData) +// +// case true: +// guard let listData = fieldData as? [FieldValue] else { +// preconditionFailure("Expected list data for field: \(field)") +// } +// +// fields[field.responseKey] = convertElements(of: listData, to: selectionSetType) as FieldValue +// } +// } +// } +// } +// +// private func convertElements( +// of list: [FieldValue], +// to selectionSetType: any RootSelectionSet.Type +// ) -> [FieldValue] { +// if let dataDictList = list as? [DataDict] { +// return dataDictList.map { selectionSetType.init(_dataDict: $0) } +// } +// +// if let nestedList = list as? [[FieldValue]] { +// return nestedList.map { self.convertElements(of: $0, to: selectionSetType) as FieldValue } +// } +// +// preconditionFailure("Expected list data to contain objects.") +// } +// +// private func unwrapAnyHashable( +// list: [AnyHashable] +// ) -> [FieldValue] { +// if let nestedList = list as? [[AnyHashable]] { +// return nestedList.map { self.unwrapAnyHashable(list: $0) as FieldValue } +// } +// +// return list.map { +// guard let base = $0.base as? FieldValue else { +// preconditionFailure("Expected list data to contain objects.") +// } +// return base +// } +// +// } +// +// private func addConditionalSelections( +// _ selections: [Selection], +// to fields: inout [String: FieldValue] +// ) { +// for selection in selections { +// switch selection { +// case let .inlineFragment(typeCase): +// self.addFulfilledSelections(of: typeCase, to: &fields) +// +// case let .fragment(fragment): +// self.addFulfilledSelections(of: fragment, to: &fields) +// +// case let .deferred(_, fragment, _): +// self.addFulfilledSelections(of: fragment, to: &fields) +// +// case let .conditional(_, selections): +// addConditionalSelections(selections, to: &fields) +// +// case .field: +// assertionFailure("Conditional selections should not directly include fields. They should use an InlineFragment instead.") +// } +// } +// } +// +//} +// +//extension Hasher { +// +// @inlinable +// public mutating func combine(_ optionalJSONValue: (any Hashable)?) { +// if let value = optionalJSONValue { +// self.combine(1 as UInt8) +// self.combine(value) +// } else { +// // This mimics the implementation of combining a nil optional from the Swift language core +// // Source reference at: +// // https://github.com/swiftlang/swift/blob/main/stdlib/public/core/Optional.swift#L590 +// self.combine(0 as UInt8) +// } +// } +// +// @inlinable +// public mutating func combine( +// _ dictionary: [T: any Hashable] +// ) { +// // From Dictionary's Hashable implementation +// var commutativeHash = 0 +// for (key, value) in dictionary { +// var elementHasher = self +// elementHasher.combine(key) +// elementHasher.combine(AnyHashable(value)) +// commutativeHash ^= elementHasher.finalize() +// } +// self.combine(commutativeHash) +// } +// +// @inlinable +// public mutating func combine( +// _ dictionary: [T: any Hashable]? +// ) { +// if let value = dictionary { +// self.combine(value) +// } else { +// self.combine(Optional<[T: any Hashable]>.none) +// } +// } +//} +// +//fileprivate protocol AnyOptional {} +// +//@_spi(Internal) +//extension Optional: AnyOptional { } +// +//fileprivate extension Optional { +// +// /// Converts the optional to a `GraphQLNullable. +// /// +// /// - Double nested optional (ie. `Optional.some(nil)`) -> `GraphQLNullable.null`. +// /// - `Optional.none` -> `GraphQLNullable.none` +// /// - `Optional.some` -> `GraphQLNullable.some` +// var asNullable: GraphQLNullable { +// unwrapAsNullable() +// } +// +// private func unwrapAsNullable(nullIfNil: Bool = false) -> GraphQLNullable { +// switch self { +// case .none: return nullIfNil ? .null : .none +// +// case .some(let value as any AnyOptional): +// return (value as! Self).unwrapAsNullable(nullIfNil: true) +// +// case .some(is NSNull): +// return .null +// +// case .some(let value): +// return .some(value) +// } +// } +//} diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift index bfbe2fae3..3917f9c53 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift @@ -110,6 +110,14 @@ extension SelectionSet { return T.init(_dataDict: __data) } + @inlinable public func hash(into hasher: inout Hasher) { + hasher.combine(__data) + } + + @inlinable public static func ==(lhs: Self, rhs: Self) -> Bool { + return lhs.__data == rhs.__data + } + public var debugDescription: String { return "\(self.__data._data as AnyObject)" } From 42ed35358739e4a74dc55f4f7bc9d00059ed2d08 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:54:10 -0700 Subject: [PATCH 31/41] Revert "debugging" This reverts commit a798963d49d722b691637a5ef01d1b9574dfe5d2. --- .../ReversePaginationTests.swift | 10 + .../ApolloAPI/SelectionSet+Equatable.swift | 542 +++++++++--------- .../Sources/ApolloAPI/SelectionSet.swift | 8 - 3 files changed, 281 insertions(+), 279 deletions(-) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 139bc3497..f2d53e168 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -102,6 +102,16 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) + if first != last { + print(""" + first as JSON: + \(first?.asJSONDictionary()) + last as JSON: + \(last?.asJSONDictionary()) + """) + } else { + print("THEY'RE EQUAL!!!!") + } XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.previousPages.isEmpty) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index 3aae3a248..a1db2123c 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -1,271 +1,271 @@ -//import Foundation -// -//public extension SelectionSet { -// -// typealias FieldValue = any Hashable -// -// /// Creates a hash using a narrowly scoped algorithm that only combines fields in the underlying data -// /// that are relevant to the `SelectionSet`. This ensures that hashes for a fragment do not -// /// consider fields that are not included in the fragment, even if they are present in the data. -// func hash(into hasher: inout Hasher) { -// hasher.combine(self.fieldsForEquality()) -// } -// -// /// Checks for equality using a narrowly scoped algorithm that only compares fields in the underlying data -// /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not -// /// consider fields that are not included in the fragment, even if they are present in the data. -// static func ==(lhs: Self, rhs: Self) -> Bool { -// return Self.equatableCheck( -// lhs.fieldsForEquality(), -// rhs.fieldsForEquality() -// ) -// } -// -// @inlinable -// internal static func equatableCheck( -// _ lhs: [T: any Hashable], -// _ rhs: [T: any Hashable] -// ) -> Bool { -// guard lhs.keys == rhs.keys else { return false } -// -// return lhs.allSatisfy { -// guard let rhsValue = rhs[$0.key], -// equatableCheck($0.value, rhsValue) else { -// return false -// } -// return true -// } -// } -// -// @inlinable -// internal static func equatableCheck( -// _ lhs: T, -// _ rhs: any Hashable -// ) -> Bool { -// lhs == rhs as? T -// } -// -// private func fieldsForEquality() -> [String: FieldValue] { -// var fields: [String: FieldValue] = [:] -// if let asTypeCase = self as? any InlineFragment { -// self.addFulfilledSelections(of: type(of: asTypeCase.asRootEntityType), to: &fields) -// -// } else { -// self.addFulfilledSelections(of: type(of: self), to: &fields) -// -// } -// -// return fields -// } -// -// private func addFulfilledSelections( -// of selectionSetType: any SelectionSet.Type, -// to fields: inout [String: FieldValue] -// ) { -// guard self.__data.fragmentIsFulfilled(selectionSetType) else { -// return -// } -// -// for selection in selectionSetType.__selections { -// switch selection { -// case let .field(field): -// add(field: field, to: &fields) -// -// case let .inlineFragment(typeCase): -// self.addFulfilledSelections(of: typeCase, to: &fields) -// -// case let .conditional(_, selections): -// self.addConditionalSelections(selections, to: &fields) -// -// case let .fragment(fragmentType): -// self.addFulfilledSelections(of: fragmentType, to: &fields) -// -// case let .deferred(_, fragmentType, _): -// self.addFulfilledSelections(of: fragmentType, to: &fields) -// } -// } -// } -// -// private func add( -// field: Selection.Field, -// to fields: inout [String: FieldValue] -// ) { -// let nullableFieldData = (self.__data._data[field.responseKey]?.base as? FieldValue).asNullable -// let fieldData: FieldValue -// switch nullableFieldData { -// case let .some(value): -// fieldData = value -// case .none, .null: -// return -// } -// -// addData(for: field.type) -// -// func addData(for type: Selection.Field.OutputType, inList: Bool = false) { -// switch type { -// case .scalar, .customScalar: -// if inList { -// guard let listData = fieldData as? [AnyHashable] else { -// preconditionFailure("Expected list data for field: \(field)") -// } -// -// fields[field.responseKey] = unwrapAnyHashable(list: listData) as FieldValue -// } else { -// fields[field.responseKey] = fieldData -// } -// -// case let .nonNull(innerType): -// addData(for: innerType, inList: inList) -// -// case let .list(innerType): -// addData(for: innerType, inList: true) -// -// case let .object(selectionSetType): -// switch inList { -// case false: -// guard let objectData = fieldData as? DataDict else { -// preconditionFailure("Expected object data for object field: \(field)") -// } -// fields[field.responseKey] = selectionSetType.init(_dataDict: objectData) -// -// case true: -// guard let listData = fieldData as? [FieldValue] else { -// preconditionFailure("Expected list data for field: \(field)") -// } -// -// fields[field.responseKey] = convertElements(of: listData, to: selectionSetType) as FieldValue -// } -// } -// } -// } -// -// private func convertElements( -// of list: [FieldValue], -// to selectionSetType: any RootSelectionSet.Type -// ) -> [FieldValue] { -// if let dataDictList = list as? [DataDict] { -// return dataDictList.map { selectionSetType.init(_dataDict: $0) } -// } -// -// if let nestedList = list as? [[FieldValue]] { -// return nestedList.map { self.convertElements(of: $0, to: selectionSetType) as FieldValue } -// } -// -// preconditionFailure("Expected list data to contain objects.") -// } -// -// private func unwrapAnyHashable( -// list: [AnyHashable] -// ) -> [FieldValue] { -// if let nestedList = list as? [[AnyHashable]] { -// return nestedList.map { self.unwrapAnyHashable(list: $0) as FieldValue } -// } -// -// return list.map { -// guard let base = $0.base as? FieldValue else { -// preconditionFailure("Expected list data to contain objects.") -// } -// return base -// } -// -// } -// -// private func addConditionalSelections( -// _ selections: [Selection], -// to fields: inout [String: FieldValue] -// ) { -// for selection in selections { -// switch selection { -// case let .inlineFragment(typeCase): -// self.addFulfilledSelections(of: typeCase, to: &fields) -// -// case let .fragment(fragment): -// self.addFulfilledSelections(of: fragment, to: &fields) -// -// case let .deferred(_, fragment, _): -// self.addFulfilledSelections(of: fragment, to: &fields) -// -// case let .conditional(_, selections): -// addConditionalSelections(selections, to: &fields) -// -// case .field: -// assertionFailure("Conditional selections should not directly include fields. They should use an InlineFragment instead.") -// } -// } -// } -// -//} -// -//extension Hasher { -// -// @inlinable -// public mutating func combine(_ optionalJSONValue: (any Hashable)?) { -// if let value = optionalJSONValue { -// self.combine(1 as UInt8) -// self.combine(value) -// } else { -// // This mimics the implementation of combining a nil optional from the Swift language core -// // Source reference at: -// // https://github.com/swiftlang/swift/blob/main/stdlib/public/core/Optional.swift#L590 -// self.combine(0 as UInt8) -// } -// } -// -// @inlinable -// public mutating func combine( -// _ dictionary: [T: any Hashable] -// ) { -// // From Dictionary's Hashable implementation -// var commutativeHash = 0 -// for (key, value) in dictionary { -// var elementHasher = self -// elementHasher.combine(key) -// elementHasher.combine(AnyHashable(value)) -// commutativeHash ^= elementHasher.finalize() -// } -// self.combine(commutativeHash) -// } -// -// @inlinable -// public mutating func combine( -// _ dictionary: [T: any Hashable]? -// ) { -// if let value = dictionary { -// self.combine(value) -// } else { -// self.combine(Optional<[T: any Hashable]>.none) -// } -// } -//} -// -//fileprivate protocol AnyOptional {} -// -//@_spi(Internal) -//extension Optional: AnyOptional { } -// -//fileprivate extension Optional { -// -// /// Converts the optional to a `GraphQLNullable. -// /// -// /// - Double nested optional (ie. `Optional.some(nil)`) -> `GraphQLNullable.null`. -// /// - `Optional.none` -> `GraphQLNullable.none` -// /// - `Optional.some` -> `GraphQLNullable.some` -// var asNullable: GraphQLNullable { -// unwrapAsNullable() -// } -// -// private func unwrapAsNullable(nullIfNil: Bool = false) -> GraphQLNullable { -// switch self { -// case .none: return nullIfNil ? .null : .none -// -// case .some(let value as any AnyOptional): -// return (value as! Self).unwrapAsNullable(nullIfNil: true) -// -// case .some(is NSNull): -// return .null -// -// case .some(let value): -// return .some(value) -// } -// } -//} +import Foundation + +public extension SelectionSet { + + typealias FieldValue = any Hashable + + /// Creates a hash using a narrowly scoped algorithm that only combines fields in the underlying data + /// that are relevant to the `SelectionSet`. This ensures that hashes for a fragment do not + /// consider fields that are not included in the fragment, even if they are present in the data. + func hash(into hasher: inout Hasher) { + hasher.combine(self.fieldsForEquality()) + } + + /// Checks for equality using a narrowly scoped algorithm that only compares fields in the underlying data + /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not + /// consider fields that are not included in the fragment, even if they are present in the data. + static func ==(lhs: Self, rhs: Self) -> Bool { + return Self.equatableCheck( + lhs.fieldsForEquality(), + rhs.fieldsForEquality() + ) + } + + @inlinable + internal static func equatableCheck( + _ lhs: [T: any Hashable], + _ rhs: [T: any Hashable] + ) -> Bool { + guard lhs.keys == rhs.keys else { return false } + + return lhs.allSatisfy { + guard let rhsValue = rhs[$0.key], + equatableCheck($0.value, rhsValue) else { + return false + } + return true + } + } + + @inlinable + internal static func equatableCheck( + _ lhs: T, + _ rhs: any Hashable + ) -> Bool { + lhs == rhs as? T + } + + private func fieldsForEquality() -> [String: FieldValue] { + var fields: [String: FieldValue] = [:] + if let asTypeCase = self as? any InlineFragment { + self.addFulfilledSelections(of: type(of: asTypeCase.asRootEntityType), to: &fields) + + } else { + self.addFulfilledSelections(of: type(of: self), to: &fields) + + } + + return fields + } + + private func addFulfilledSelections( + of selectionSetType: any SelectionSet.Type, + to fields: inout [String: FieldValue] + ) { + guard self.__data.fragmentIsFulfilled(selectionSetType) else { + return + } + + for selection in selectionSetType.__selections { + switch selection { + case let .field(field): + add(field: field, to: &fields) + + case let .inlineFragment(typeCase): + self.addFulfilledSelections(of: typeCase, to: &fields) + + case let .conditional(_, selections): + self.addConditionalSelections(selections, to: &fields) + + case let .fragment(fragmentType): + self.addFulfilledSelections(of: fragmentType, to: &fields) + + case let .deferred(_, fragmentType, _): + self.addFulfilledSelections(of: fragmentType, to: &fields) + } + } + } + + private func add( + field: Selection.Field, + to fields: inout [String: FieldValue] + ) { + let nullableFieldData = (self.__data._data[field.responseKey]?.base as? FieldValue).asNullable + let fieldData: FieldValue + switch nullableFieldData { + case let .some(value): + fieldData = value + case .none, .null: + return + } + + addData(for: field.type) + + func addData(for type: Selection.Field.OutputType, inList: Bool = false) { + switch type { + case .scalar, .customScalar: + if inList { + guard let listData = fieldData as? [AnyHashable] else { + preconditionFailure("Expected list data for field: \(field)") + } + + fields[field.responseKey] = unwrapAnyHashable(list: listData) as FieldValue + } else { + fields[field.responseKey] = fieldData + } + + case let .nonNull(innerType): + addData(for: innerType, inList: inList) + + case let .list(innerType): + addData(for: innerType, inList: true) + + case let .object(selectionSetType): + switch inList { + case false: + guard let objectData = fieldData as? DataDict else { + preconditionFailure("Expected object data for object field: \(field)") + } + fields[field.responseKey] = selectionSetType.init(_dataDict: objectData) + + case true: + guard let listData = fieldData as? [FieldValue] else { + preconditionFailure("Expected list data for field: \(field)") + } + + fields[field.responseKey] = convertElements(of: listData, to: selectionSetType) as FieldValue + } + } + } + } + + private func convertElements( + of list: [FieldValue], + to selectionSetType: any RootSelectionSet.Type + ) -> [FieldValue] { + if let dataDictList = list as? [DataDict] { + return dataDictList.map { selectionSetType.init(_dataDict: $0) } + } + + if let nestedList = list as? [[FieldValue]] { + return nestedList.map { self.convertElements(of: $0, to: selectionSetType) as FieldValue } + } + + preconditionFailure("Expected list data to contain objects.") + } + + private func unwrapAnyHashable( + list: [AnyHashable] + ) -> [FieldValue] { + if let nestedList = list as? [[AnyHashable]] { + return nestedList.map { self.unwrapAnyHashable(list: $0) as FieldValue } + } + + return list.map { + guard let base = $0.base as? FieldValue else { + preconditionFailure("Expected list data to contain objects.") + } + return base + } + + } + + private func addConditionalSelections( + _ selections: [Selection], + to fields: inout [String: FieldValue] + ) { + for selection in selections { + switch selection { + case let .inlineFragment(typeCase): + self.addFulfilledSelections(of: typeCase, to: &fields) + + case let .fragment(fragment): + self.addFulfilledSelections(of: fragment, to: &fields) + + case let .deferred(_, fragment, _): + self.addFulfilledSelections(of: fragment, to: &fields) + + case let .conditional(_, selections): + addConditionalSelections(selections, to: &fields) + + case .field: + assertionFailure("Conditional selections should not directly include fields. They should use an InlineFragment instead.") + } + } + } + +} + +extension Hasher { + + @inlinable + public mutating func combine(_ optionalJSONValue: (any Hashable)?) { + if let value = optionalJSONValue { + self.combine(1 as UInt8) + self.combine(value) + } else { + // This mimics the implementation of combining a nil optional from the Swift language core + // Source reference at: + // https://github.com/swiftlang/swift/blob/main/stdlib/public/core/Optional.swift#L590 + self.combine(0 as UInt8) + } + } + + @inlinable + public mutating func combine( + _ dictionary: [T: any Hashable] + ) { + // From Dictionary's Hashable implementation + var commutativeHash = 0 + for (key, value) in dictionary { + var elementHasher = self + elementHasher.combine(key) + elementHasher.combine(AnyHashable(value)) + commutativeHash ^= elementHasher.finalize() + } + self.combine(commutativeHash) + } + + @inlinable + public mutating func combine( + _ dictionary: [T: any Hashable]? + ) { + if let value = dictionary { + self.combine(value) + } else { + self.combine(Optional<[T: any Hashable]>.none) + } + } +} + +fileprivate protocol AnyOptional {} + +@_spi(Internal) +extension Optional: AnyOptional { } + +fileprivate extension Optional { + + /// Converts the optional to a `GraphQLNullable. + /// + /// - Double nested optional (ie. `Optional.some(nil)`) -> `GraphQLNullable.null`. + /// - `Optional.none` -> `GraphQLNullable.none` + /// - `Optional.some` -> `GraphQLNullable.some` + var asNullable: GraphQLNullable { + unwrapAsNullable() + } + + private func unwrapAsNullable(nullIfNil: Bool = false) -> GraphQLNullable { + switch self { + case .none: return nullIfNil ? .null : .none + + case .some(let value as any AnyOptional): + return (value as! Self).unwrapAsNullable(nullIfNil: true) + + case .some(is NSNull): + return .null + + case .some(let value): + return .some(value) + } + } +} diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift index 3917f9c53..bfbe2fae3 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet.swift @@ -110,14 +110,6 @@ extension SelectionSet { return T.init(_dataDict: __data) } - @inlinable public func hash(into hasher: inout Hasher) { - hasher.combine(__data) - } - - @inlinable public static func ==(lhs: Self, rhs: Self) -> Bool { - return lhs.__data == rhs.__data - } - public var debugDescription: String { return "\(self.__data._data as AnyObject)" } From 9876b046ffafe49f57b5274ec76d4d02377d8dfb Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Fri, 3 Oct 2025 16:54:49 -0700 Subject: [PATCH 32/41] Revert "debugging" This reverts commit eceb242d530c0f347e12f0d457af07f1cba40804. --- .../ApolloPaginationTests/ReversePaginationTests.swift | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index f2d53e168..139bc3497 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -102,16 +102,6 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) """) - if first != last { - print(""" - first as JSON: - \(first?.asJSONDictionary()) - last as JSON: - \(last?.asJSONDictionary()) - """) - } else { - print("THEY'RE EQUAL!!!!") - } XCTAssertEqual(try? results.first?.get().initialPage, try? results.last?.get().initialPage) XCTAssertFalse(output.previousPages.isEmpty) From 317fc49a72e1f749b95cca1e1e966002337ae4e5 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:06:34 -0700 Subject: [PATCH 33/41] Adds new test with failing CI model --- .../SelectionSet_EqualityTests.swift | 135 +++++++++++++++++- 1 file changed, 132 insertions(+), 3 deletions(-) diff --git a/Tests/ApolloTests/SelectionSet_EqualityTests.swift b/Tests/ApolloTests/SelectionSet_EqualityTests.swift index 18b1f5268..6aefb8acb 100644 --- a/Tests/ApolloTests/SelectionSet_EqualityTests.swift +++ b/Tests/ApolloTests/SelectionSet_EqualityTests.swift @@ -956,9 +956,7 @@ class SelectionSet_EqualityTests: XCTestCase { // MARK: - Integration Tests - func test__equatable__givenQueryResponseFetchedFromStore() - async throws - { + func test__equatable__givenQueryResponseFetchedFromStore() async throws { // given class GivenSelectionSet: MockSelectionSet { override class var __selections: [Selection] { @@ -1024,4 +1022,135 @@ class SelectionSet_EqualityTests: XCTestCase { await fulfillment(of: [updateCompletedExpectation], timeout: 1.0) } + func test__equatable__givenFailingModelFromCI_sameValue_shouldBeEqual() throws { + // given + class ReverseFriendsQuery: MockSelectionSet { + override class var __selections: [Selection] { [ + .field("hero", Hero?.self, arguments: ["id": .variable("id")]) + ]} + + var hero: Hero { __data["hero"] } + + class Hero: MockSelectionSet { + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("id", String.self), + .field("name", String.self), + .field("friendsConnection", FriendsConnection.self, arguments: [ + "first": .variable("first"), + "before": .variable("before"), + ]), + ]} + + var name: String { __data["name"] } + var id: String { __data["id"] } + var friendsConnection: FriendsConnection { __data["friendsConnection"] } + + class FriendsConnection: MockSelectionSet { + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("totalCount", Int.self), + .field("friends", [Character].self), + .field("pageInfo", PageInfo.self), + ]} + + var totalCount: Int { __data["totalCount"] } + var friends: [Character] { __data["friends"] } + var pageInfo: PageInfo { __data["pageInfo"] } + + class Character: MockSelectionSet { + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("name", String.self), + .field("id", String.self), + ]} + + var name: String { __data["name"] } + var id: String { __data["id"] } + } + + class PageInfo: MockSelectionSet { + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("startCursor", Optional.self), + .field("hasPreviousPage", Bool.self), + ]} + + var startCursor: String? { __data["startCursor"] } + var hasPreviousPage: Bool { __data["hasPreviousPage"] } + } + } + } + } + + // when + let query = MockQuery() + query.__variables = ["id": "2001", "first": 2, "before": "Y3Vyc29yMw=="] + let first = try GraphQLResponse( + operation: query, + body: ["data": [ + "hero": [ + "__typename": "Droid", + "id": "2001", + "name": "R2-D2", + "friendsConnection": [ + "__typename": "FriendsConnection", + "totalCount": 3, + "friends": [ + [ + "__typename": "Human", + "name": "Han Solo", + "id": "1002", + ], + [ + "__typename": "Human", + "name": "Leia Organa", + "id": "1003", + ] + ], + "pageInfo": [ + "__typename": "PageInfo", + "startCursor": "Y3Vyc29yMg==", + "hasPreviousPage": true + ] + ] + ] + ]]).parseResult().0.data + + let second = try GraphQLResponse( + operation: query, + body: ["data": [ + "hero": [ + "__typename": "Droid", + "id": "2001", + "name": "R2-D2", + "friendsConnection": [ + "__typename": "FriendsConnection", + "totalCount": 3, + "friends": [ + [ + "__typename": "Human", + "name": "Han Solo", + "id": "1002", + ], + [ + "__typename": "Human", + "name": "Leia Organa", + "id": "1003", + ] + ], + "pageInfo": [ + "__typename": "PageInfo", + "startCursor": "Y3Vyc29yMg==", + "hasPreviousPage": true + ] + ] + ] + ]]).parseResult().0.data + + // then + XCTAssertEqual(first, second) + expect(first).to(equal(second)) + } + } From 8cef512c7ceca8267bde03d6cd2c48e2eb57a753 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:00:11 -0700 Subject: [PATCH 34/41] more debugging --- Tests/ApolloPaginationTests/ReversePaginationTests.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 139bc3497..0f4d1fd20 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -98,6 +98,9 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) + data.hero.friendsConnection.__objectType equal: \(first?.data?.hero.friendsConnection.__objectType == last?.data?.hero.friendsConnection.__objectType) + data.hero.friendsConnection._fieldData equal: \(first?.data?.hero.friendsConnection._fieldData == last?.data?.hero.friendsConnection._fieldData) + data.hero.friendsConnection._rawData equal: \(first?.data?.hero.friendsConnection._rawData == last?.data?.hero.friendsConnection._rawData) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) data.hero.friendsConnection.friends equal: \(first?.data?.hero.friendsConnection.friends == last?.data?.hero.friendsConnection.friends) data.hero.friendsConnection.pageInfo equal: \(first?.data?.hero.friendsConnection.pageInfo == last?.data?.hero.friendsConnection.pageInfo) From 05140b811e6d246cbdd9da01cd1defe19eb8b466 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 6 Oct 2025 16:56:11 -0700 Subject: [PATCH 35/41] debugging --- Tests/ApolloPaginationTests/ReversePaginationTests.swift | 2 +- apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/ApolloPaginationTests/ReversePaginationTests.swift b/Tests/ApolloPaginationTests/ReversePaginationTests.swift index 0f4d1fd20..3a66488e1 100644 --- a/Tests/ApolloPaginationTests/ReversePaginationTests.swift +++ b/Tests/ApolloPaginationTests/ReversePaginationTests.swift @@ -98,7 +98,7 @@ final class ReversePaginationTests: XCTestCase, CacheDependentTesting { data.hero.friendsConnection.__data._fulfilledFragments equal: \(first?.data?.hero.friendsConnection.__data._fulfilledFragments == last?.data?.hero.friendsConnection.__data._fulfilledFragments) data.hero.friendsConnection.__data._deferredFragments equal: \(first?.data?.hero.friendsConnection.__data._deferredFragments == last?.data?.hero.friendsConnection.__data._deferredFragments) data.hero.friendsConnection.__typename equal: \(first?.data?.hero.friendsConnection.__typename == last?.data?.hero.friendsConnection.__typename) - data.hero.friendsConnection.__objectType equal: \(first?.data?.hero.friendsConnection.__objectType == last?.data?.hero.friendsConnection.__objectType) + data.hero.friendsConnection.__objectType equal: \(first?.data?.hero.friendsConnection.__objectType == last?.data?.hero.friendsConnection.__objectType) data.hero.friendsConnection._fieldData equal: \(first?.data?.hero.friendsConnection._fieldData == last?.data?.hero.friendsConnection._fieldData) data.hero.friendsConnection._rawData equal: \(first?.data?.hero.friendsConnection._rawData == last?.data?.hero.friendsConnection._rawData) data.hero.friendsConnection.totalCount equal: \(first?.data?.hero.friendsConnection.totalCount == last?.data?.hero.friendsConnection.totalCount) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index a1db2123c..b14bcb6f9 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -15,6 +15,7 @@ public extension SelectionSet { /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not /// consider fields that are not included in the fragment, even if they are present in the data. static func ==(lhs: Self, rhs: Self) -> Bool { + print("SelectionSet::== for \(String(describing: self))") return Self.equatableCheck( lhs.fieldsForEquality(), rhs.fieldsForEquality() From ec6ca05b38eb0523223af8f9999624a89fd9a501 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 6 Oct 2025 17:25:43 -0700 Subject: [PATCH 36/41] debugging --- .../Sources/ApolloAPI/SelectionSet+Equatable.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index b14bcb6f9..089577132 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -15,7 +15,14 @@ public extension SelectionSet { /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not /// consider fields that are not included in the fragment, even if they are present in the data. static func ==(lhs: Self, rhs: Self) -> Bool { - print("SelectionSet::== for \(String(describing: self))") + if String(describing: self) == "FriendsConnection" { + print(""" + SelectionSet::== + lhs: \(lhs.fieldsForEquality()) + rhs: \(rhs.fieldsForEquality()) + """) + } + return Self.equatableCheck( lhs.fieldsForEquality(), rhs.fieldsForEquality() From e6e63b3a06c1d6d05e3fc40be689bdc9146cae7c Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:21:19 -0700 Subject: [PATCH 37/41] debugging --- .../ApolloAPI/SelectionSet+Equatable.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index 089577132..5b25587d9 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -15,14 +15,6 @@ public extension SelectionSet { /// that are relevant to the `SelectionSet`. This ensures that equality checks for a fragment do not /// consider fields that are not included in the fragment, even if they are present in the data. static func ==(lhs: Self, rhs: Self) -> Bool { - if String(describing: self) == "FriendsConnection" { - print(""" - SelectionSet::== - lhs: \(lhs.fieldsForEquality()) - rhs: \(rhs.fieldsForEquality()) - """) - } - return Self.equatableCheck( lhs.fieldsForEquality(), rhs.fieldsForEquality() @@ -50,7 +42,15 @@ public extension SelectionSet { _ lhs: T, _ rhs: any Hashable ) -> Bool { - lhs == rhs as? T + if String(describing: self) == "FriendsConnection" && (lhs != rhs as? T) { + print(""" + equatableCheck failure: + lhs: \(lhs) + rhs: \(rhs), as? T: \(rhs as? T) + """) + } + + return lhs == rhs as? T } private func fieldsForEquality() -> [String: FieldValue] { From a3fa5e6c732f6b6649ef806c718c437339b04fde Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:49:54 -0700 Subject: [PATCH 38/41] debugging --- apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index 5b25587d9..f92cca247 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -42,9 +42,10 @@ public extension SelectionSet { _ lhs: T, _ rhs: any Hashable ) -> Bool { - if String(describing: self) == "FriendsConnection" && (lhs != rhs as? T) { + if lhs != rhs as? T { print(""" - equatableCheck failure: + equatableCheck failure on \(String(describing: self)) + T: \(String(describing: T.self)) lhs: \(lhs) rhs: \(rhs), as? T: \(rhs as? T) """) From a81348cd98901c484b48f931b7f22d7f14963d69 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:08:27 -0700 Subject: [PATCH 39/41] debugging --- apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index f92cca247..f84f72676 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -42,13 +42,15 @@ public extension SelectionSet { _ lhs: T, _ rhs: any Hashable ) -> Bool { - if lhs != rhs as? T { + if lhs != rhs as? T && String(describing: lhs) == "FriendsConnection" { print(""" equatableCheck failure on \(String(describing: self)) T: \(String(describing: T.self)) lhs: \(lhs) - rhs: \(rhs), as? T: \(rhs as? T) + rhs: \(rhs), as! T: \(rhs as! T) """) + + return lhs == rhs as! T } return lhs == rhs as? T From 40bf8128c3d2f971fb2e5ce46c5abc6da3a2481b Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:10:30 -0700 Subject: [PATCH 40/41] Revert "debugging" This reverts commit a81348cd98901c484b48f931b7f22d7f14963d69. --- apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index f84f72676..f92cca247 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -42,15 +42,13 @@ public extension SelectionSet { _ lhs: T, _ rhs: any Hashable ) -> Bool { - if lhs != rhs as? T && String(describing: lhs) == "FriendsConnection" { + if lhs != rhs as? T { print(""" equatableCheck failure on \(String(describing: self)) T: \(String(describing: T.self)) lhs: \(lhs) - rhs: \(rhs), as! T: \(rhs as! T) + rhs: \(rhs), as? T: \(rhs as? T) """) - - return lhs == rhs as! T } return lhs == rhs as? T From 1f8dae99323fcc7304caf189f7b6e360f9aa4993 Mon Sep 17 00:00:00 2001 From: Calvin Cestari <146856+calvincestari@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:25:38 -0700 Subject: [PATCH 41/41] Debugging with old long-form type casting --- .../ApolloAPI/SelectionSet+Equatable.swift | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift index f92cca247..cba1b2232 100644 --- a/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift +++ b/apollo-ios/Sources/ApolloAPI/SelectionSet+Equatable.swift @@ -112,7 +112,87 @@ public extension SelectionSet { func addData(for type: Selection.Field.OutputType, inList: Bool = false) { switch type { - case .scalar, .customScalar: +// case .scalar: + case let .scalar(scalarType): + switch scalarType { + case is String.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [String?] + } else { + fields[field.responseKey] = fieldData as? String? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [String] + } else { + fields[field.responseKey] = fieldData as? String + } + } + + case is Int.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Int?] + } else { + fields[field.responseKey] = fieldData as? Int? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Int] + } else { + fields[field.responseKey] = fieldData as? Int + } + } + + case is Bool.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Bool?] + } else { + fields[field.responseKey] = fieldData as? Bool? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Bool] + } else { + fields[field.responseKey] = fieldData as? Bool + } + } + + case is Float.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Float?] + } else { + fields[field.responseKey] = fieldData as? Float? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Float] + } else { + fields[field.responseKey] = fieldData as? Float + } + } + + case is Double.Type: + if field.type.isNullable { + if inList { + fields[field.responseKey] = fieldData as? [Double?] + } else { + fields[field.responseKey] = fieldData as? Double? + } + } else { + if inList { + fields[field.responseKey] = fieldData as? [Double] + } else { + fields[field.responseKey] = fieldData as? Double + } + } + + default: fields[field.responseKey] = fieldData + } + case .customScalar: if inList { guard let listData = fieldData as? [AnyHashable] else { preconditionFailure("Expected list data for field: \(field)")