TestHelper.swift 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. /*
  2. * Copyright 2025 Google LLC
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import FirebaseCore
  17. import FirebaseFirestore
  18. import Foundation
  19. import XCTest
  20. public enum TestHelper {
  21. public static func compare(snapshot: Pipeline.Snapshot,
  22. expectedCount: Int,
  23. file: StaticString = #file,
  24. line: UInt = #line) {
  25. XCTAssertEqual(
  26. snapshot.results.count,
  27. expectedCount,
  28. "Snapshot results count mismatch",
  29. file: file,
  30. line: line
  31. )
  32. }
  33. static func compare(snapshot: Pipeline.Snapshot,
  34. expectedIDs: [String],
  35. enforceOrder: Bool,
  36. file: StaticString = #file,
  37. line: UInt = #line) {
  38. let results = snapshot.results
  39. XCTAssertEqual(
  40. results.count,
  41. expectedIDs.count,
  42. "Snapshot document IDs count mismatch. Expected \(expectedIDs.count), got \(results.count). Actual IDs: \(results.map { $0.id })",
  43. file: file,
  44. line: line
  45. )
  46. if enforceOrder {
  47. let actualIDs = results.map { $0.id! }
  48. XCTAssertEqual(
  49. actualIDs,
  50. expectedIDs,
  51. "Snapshot document IDs mismatch. Expected: \(expectedIDs.sorted()), got: \(actualIDs)",
  52. file: file,
  53. line: line
  54. )
  55. } else {
  56. let actualIDs = results.map { $0.id! }.sorted()
  57. XCTAssertEqual(
  58. actualIDs,
  59. expectedIDs.sorted(),
  60. "Snapshot document IDs mismatch. Expected (sorted): \(expectedIDs.sorted()), got (sorted): \(actualIDs)",
  61. file: file,
  62. line: line
  63. )
  64. }
  65. }
  66. static func compare(snapshot: Pipeline.Snapshot,
  67. expected: [[String: Sendable?]],
  68. enforceOrder: Bool,
  69. file: StaticString = #file,
  70. line: UInt = #line) {
  71. guard snapshot.results.count == expected.count else {
  72. XCTFail("Mismatch in expected results count and actual results count.")
  73. return
  74. }
  75. if enforceOrder {
  76. for i in 0 ..< expected.count {
  77. compare(pipelineResult: snapshot.results[i], expected: expected[i])
  78. }
  79. } else {
  80. let result = snapshot.results.map { $0.data }
  81. XCTAssertTrue(areArraysOfDictionariesEqualRegardlessOfOrder(result, expected),
  82. "PipelineSnapshot mismatch. Expected \(expected), got \(result)")
  83. }
  84. }
  85. static func compare(pipelineResult result: PipelineResult,
  86. expected: [String: Sendable?],
  87. file: StaticString = #file,
  88. line: UInt = #line) {
  89. XCTAssertTrue(areDictionariesEqual(result.data, expected),
  90. "Document data mismatch. Expected \(expected), got \(result.data)")
  91. }
  92. // MARK: - Internal helper
  93. private static func isNilOrNSNull(_ value: Sendable?) -> Bool {
  94. // First, use a `guard` to safely unwrap the optional.
  95. // If it's nil, we can immediately return true.
  96. guard let unwrappedValue = value else {
  97. return true
  98. }
  99. // If it wasn't nil, we now check if the unwrapped value is the NSNull object.
  100. return unwrappedValue is NSNull
  101. }
  102. // A custom function to compare two values of type 'Sendable'
  103. private static func areEqual(_ value1: Sendable?, _ value2: Sendable?) -> Bool {
  104. if isNilOrNSNull(value1) || isNilOrNSNull(value2) {
  105. return isNilOrNSNull(value1) && isNilOrNSNull(value2)
  106. }
  107. switch (value1!, value2!) {
  108. case let (v1 as [String: Sendable?], v2 as [String: Sendable?]):
  109. return areDictionariesEqual(v1, v2)
  110. case let (v1 as [Sendable?], v2 as [Sendable?]):
  111. return areArraysEqual(v1, v2)
  112. case let (v1 as Timestamp, v2 as Timestamp):
  113. return v1 == v2
  114. case let (v1 as Date, v2 as Timestamp):
  115. // Firestore converts Dates to Timestamps
  116. return Timestamp(date: v1) == v2
  117. case let (v1 as GeoPoint, v2 as GeoPoint):
  118. return v1.latitude == v2.latitude && v1.longitude == v2.longitude
  119. case let (v1 as DocumentReference, v2 as DocumentReference):
  120. return v1.path == v2.path
  121. case let (v1 as VectorValue, v2 as VectorValue):
  122. return v1.array == v2.array
  123. case let (v1 as Data, v2 as Data):
  124. return v1 == v2
  125. case let (v1 as Int, v2 as Int):
  126. return v1 == v2
  127. case let (v1 as Double, v2 as Double):
  128. let doubleEpsilon = 0.000001
  129. return abs(v1 - v2) <= doubleEpsilon
  130. case let (v1 as Float, v2 as Float):
  131. let floatEpsilon: Float = 0.00001
  132. return abs(v1 - v2) <= floatEpsilon
  133. case let (v1 as String, v2 as String):
  134. return v1 == v2
  135. case let (v1 as Bool, v2 as Bool):
  136. return v1 == v2
  137. case let (v1 as UInt8, v2 as UInt8):
  138. return v1 == v2
  139. default:
  140. // Fallback for any other types, might need more specific checks
  141. return false
  142. }
  143. }
  144. // A function to compare two dictionaries
  145. private static func areDictionariesEqual(_ dict1: [String: Sendable?],
  146. _ dict2: [String: Sendable?]) -> Bool {
  147. guard dict1.count == dict2.count else { return false }
  148. for (key, value1) in dict1 {
  149. guard let value2 = dict2[key], areEqual(value1, value2) else {
  150. print("""
  151. Dictionary value mismatch for key: '\(key)'
  152. Actual value: '\(String(describing: value1))' (from dict1)
  153. Expected value: '\(String(describing: dict2[key]))' (from dict2)
  154. Full actual value: \(String(describing: dict1))
  155. Full expected value: \(String(describing: dict2))
  156. """)
  157. return false
  158. }
  159. }
  160. return true
  161. }
  162. private static func areArraysEqual(_ array1: [Sendable?], _ array2: [Sendable?]) -> Bool {
  163. guard array1.count == array2.count else { return false }
  164. for (index, value1) in array1.enumerated() {
  165. let value2 = array2[index]
  166. if !areEqual(value1, value2) {
  167. print("""
  168. Array value mismatch.
  169. Actual array value: '\(String(describing: value1))'
  170. Expected array value: '\(String(describing: value2))'
  171. """)
  172. return false
  173. }
  174. }
  175. return true
  176. }
  177. private static func areArraysOfDictionariesEqualRegardlessOfOrder(_ array1: [[String: Sendable?]],
  178. _ array2: [[String: Sendable?]])
  179. -> Bool {
  180. // 1. Check if the arrays have the same number of dictionaries.
  181. guard array1.count == array2.count else {
  182. return false
  183. }
  184. // Create a mutable copy of array2 to remove matched dictionaries
  185. var mutableArray2 = array2
  186. // Iterate through each dictionary in array1
  187. for dict1 in array1 {
  188. var foundMatch = false
  189. // Try to find an equivalent dictionary in mutableArray2
  190. if let index = mutableArray2.firstIndex(where: { dict2 in
  191. areDictionariesEqual(dict1, dict2) // Use our deep comparison function
  192. }) {
  193. // If a match is found, remove it from mutableArray2 to handle duplicates
  194. mutableArray2.remove(at: index)
  195. foundMatch = true
  196. }
  197. // If no match was found for the current dictionary from array1, arrays are not equal
  198. if !foundMatch {
  199. return false
  200. }
  201. }
  202. // If we've iterated through all of array1 and mutableArray2 is empty,
  203. // it means all dictionaries found a unique match.
  204. return mutableArray2.isEmpty
  205. }
  206. }