HeartbeatStorageTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. // Copyright 2021 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. @testable import FirebaseCoreInternal
  15. import XCTest
  16. extension HeartbeatsBundle {
  17. static func testHeartbeatBundle() -> HeartbeatsBundle {
  18. var heartbeatBundle = HeartbeatsBundle(capacity: 1)
  19. let date = Date(timeIntervalSince1970: 1_635_739_200) // 2021-11-01 @ 00:00:00 (EST)
  20. heartbeatBundle.append(Heartbeat(agent: "dummy_agent", date: date))
  21. return heartbeatBundle
  22. }
  23. }
  24. class HeartbeatStorageTests: XCTestCase {
  25. // MARK: - Instance Management
  26. func testGettingInstance_WithSameID_ReturnsSameInstance() {
  27. // Given
  28. let heartbeatStorage1 = HeartbeatStorage.getInstance(id: "sparky")
  29. // When
  30. let heartbeatStorage2 = HeartbeatStorage.getInstance(id: "sparky")
  31. // Then
  32. XCTAssert(
  33. heartbeatStorage1 === heartbeatStorage2,
  34. "Instances should reference the same object."
  35. )
  36. addTeardownBlock { [weak heartbeatStorage1, weak heartbeatStorage2] in
  37. XCTAssertNil(heartbeatStorage1)
  38. XCTAssertNil(heartbeatStorage2)
  39. }
  40. }
  41. func testGettingInstance_WithDifferentID_ReturnsDifferentInstances() {
  42. // Given
  43. let heartbeatStorage1 = HeartbeatStorage.getInstance(id: "sparky_jr")
  44. // When
  45. let heartbeatStorage2 = HeartbeatStorage.getInstance(id: "sparky_sr")
  46. // Then
  47. XCTAssert(
  48. heartbeatStorage1 !== heartbeatStorage2,
  49. "Instances should NOT reference the same object."
  50. )
  51. addTeardownBlock { [weak heartbeatStorage1, weak heartbeatStorage2] in
  52. XCTAssertNil(heartbeatStorage1)
  53. XCTAssertNil(heartbeatStorage2)
  54. }
  55. }
  56. func testCachedInstancesCannotBeRetainedWeakly() {
  57. // Given
  58. var strongHeartbeatStorage: HeartbeatStorage? = .getInstance(id: "sparky")
  59. weak var weakHeartbeatStorage: HeartbeatStorage? = .getInstance(id: "sparky")
  60. XCTAssert(
  61. strongHeartbeatStorage === weakHeartbeatStorage,
  62. "Instances should reference the same object."
  63. )
  64. // When
  65. strongHeartbeatStorage = nil
  66. // Then
  67. XCTAssertNil(strongHeartbeatStorage)
  68. XCTAssertNil(weakHeartbeatStorage)
  69. }
  70. func testCachedInstancesAreRemovedUponDeinitAndCanBeRetainedStrongly() {
  71. // Given
  72. var heartbeatStorage1: HeartbeatStorage? = .getInstance(id: "sparky")
  73. var heartbeatStorage2: HeartbeatStorage? = .getInstance(id: "sparky")
  74. XCTAssert(
  75. heartbeatStorage1 === heartbeatStorage2,
  76. "Instances should reference the same object."
  77. )
  78. // When
  79. heartbeatStorage1 = nil
  80. XCTAssertNil(heartbeatStorage1)
  81. XCTAssertNotNil(heartbeatStorage2)
  82. // Then
  83. heartbeatStorage2 = nil
  84. XCTAssertNil(heartbeatStorage2)
  85. }
  86. // MARK: - HeartbeatStorageProtocol
  87. func testReadAndWrite_ReadsOldValueAndWritesNewValue() throws {
  88. // Given
  89. let expectation = expectation(description: #function)
  90. let heartbeatStorage = HeartbeatStorage(id: #function, storage: StorageFake())
  91. // When
  92. heartbeatStorage.readAndWriteAsync { heartbeatsBundle in
  93. // Assert that heartbeat storage is empty.
  94. XCTAssertNil(heartbeatsBundle)
  95. // Write new value.
  96. return HeartbeatsBundle.testHeartbeatBundle()
  97. }
  98. heartbeatStorage.readAndWriteAsync { heartbeatsBundle in
  99. expectation.fulfill()
  100. // Assert old value is read.
  101. XCTAssertEqual(
  102. heartbeatsBundle?.makeHeartbeatsPayload(),
  103. HeartbeatsBundle.testHeartbeatBundle().makeHeartbeatsPayload()
  104. )
  105. // Write some new value.
  106. return heartbeatsBundle
  107. }
  108. // Then
  109. wait(for: [expectation], timeout: 0.5)
  110. }
  111. func testReadAndWrite_WhenLoadFails_PassesNilToBlock() throws {
  112. // Given
  113. let expectation = expectation(description: #function)
  114. let heartbeatStorage = HeartbeatStorage(id: #function, storage: StorageFake())
  115. // When
  116. heartbeatStorage.readAndWriteAsync { heartbeatsBundle in
  117. expectation.fulfill()
  118. XCTAssertNil(heartbeatsBundle)
  119. return heartbeatsBundle
  120. }
  121. // Then
  122. wait(for: [expectation], timeout: 0.5)
  123. }
  124. func testReadAndWrite_WhenSaveFails_DoesNotAttemptRecovery() throws {
  125. // Given
  126. let expectation = expectation(description: #function)
  127. expectation.expectedFulfillmentCount = 4
  128. let storageFake = StorageFake()
  129. let heartbeatStorage = HeartbeatStorage(id: #function, storage: storageFake)
  130. // When
  131. storageFake.onWrite = { _ in
  132. expectation.fulfill() // Fulfilled 2 times.
  133. throw StorageError.writeError
  134. }
  135. heartbeatStorage.readAndWriteAsync { heartbeatsBundle in
  136. expectation.fulfill()
  137. return HeartbeatsBundle.testHeartbeatBundle()
  138. }
  139. // Then
  140. heartbeatStorage.readAndWriteAsync { heartbeatsBundle in
  141. expectation.fulfill()
  142. XCTAssertNotEqual(
  143. heartbeatsBundle?.makeHeartbeatsPayload(),
  144. HeartbeatsBundle.testHeartbeatBundle().makeHeartbeatsPayload(),
  145. "They should not be equal because the previous save failed."
  146. )
  147. return HeartbeatsBundle.testHeartbeatBundle()
  148. }
  149. wait(for: [expectation], timeout: 0.5)
  150. }
  151. func testGetAndSet_ReturnsOldValueAndSetsNewValue() throws {
  152. // Given
  153. let expectation = expectation(description: #function)
  154. let heartbeatStorage = HeartbeatStorage(id: #function, storage: StorageFake())
  155. var dummyHeartbeatsBundle = HeartbeatsBundle(capacity: 1)
  156. dummyHeartbeatsBundle.append(Heartbeat(agent: "dummy_agent", date: Date()))
  157. // When
  158. XCTAssertNoThrow(
  159. try heartbeatStorage.getAndSet { heartbeatsBundle in
  160. // Assert that heartbeat storage is empty.
  161. XCTAssertNil(heartbeatsBundle)
  162. // Write new value.
  163. return dummyHeartbeatsBundle
  164. }
  165. )
  166. // Then
  167. XCTAssertNoThrow(
  168. try heartbeatStorage.getAndSet { heartbeatsBundle in
  169. expectation.fulfill()
  170. // Assert old value is read.
  171. XCTAssertEqual(
  172. heartbeatsBundle?.makeHeartbeatsPayload(),
  173. dummyHeartbeatsBundle.makeHeartbeatsPayload()
  174. )
  175. // Write some new value.
  176. return heartbeatsBundle
  177. }
  178. )
  179. wait(for: [expectation], timeout: 0.5)
  180. }
  181. func testGetAndSetAsync_ReturnsOldValueAndSetsNewValue() throws {
  182. // Given
  183. let heartbeatStorage = HeartbeatStorage(id: #function, storage: StorageFake())
  184. // When
  185. let expectation1 = expectation(description: #function + "_1")
  186. heartbeatStorage.getAndSetAsync { heartbeatsBundle in
  187. // Assert that heartbeat storage is empty.
  188. XCTAssertNil(heartbeatsBundle)
  189. // Write new value.
  190. return HeartbeatsBundle.testHeartbeatBundle()
  191. } completion: { result in
  192. switch result {
  193. case .success: break
  194. case let .failure(error): XCTFail("Error: \(error)")
  195. }
  196. expectation1.fulfill()
  197. }
  198. // Then
  199. let expectation2 = expectation(description: #function + "_2")
  200. XCTAssertNoThrow(
  201. try heartbeatStorage.getAndSet { heartbeatsBundle in
  202. // Assert old value is read.
  203. XCTAssertEqual(
  204. heartbeatsBundle?.makeHeartbeatsPayload(),
  205. HeartbeatsBundle.testHeartbeatBundle().makeHeartbeatsPayload()
  206. )
  207. // Write some new value.
  208. expectation2.fulfill()
  209. return heartbeatsBundle
  210. }
  211. )
  212. wait(for: [expectation1, expectation2], timeout: 0.5, enforceOrder: true)
  213. }
  214. func testGetAndSet_WhenLoadFails_PassesNilToBlockAndReturnsNil() throws {
  215. // Given
  216. let expectation = expectation(description: #function)
  217. expectation.expectedFulfillmentCount = 2
  218. let storageFake = StorageFake()
  219. let heartbeatStorage = HeartbeatStorage(id: #function, storage: storageFake)
  220. // When
  221. storageFake.onRead = {
  222. expectation.fulfill()
  223. return try XCTUnwrap("BAD_DATA".data(using: .utf8))
  224. }
  225. // Then
  226. try heartbeatStorage.getAndSet { heartbeatsBundle in
  227. expectation.fulfill()
  228. XCTAssertNil(heartbeatsBundle)
  229. return heartbeatsBundle
  230. }
  231. wait(for: [expectation], timeout: 0.5)
  232. }
  233. func testGetAndSetAsync_WhenLoadFails_PassesNilToBlockAndReturnsNil() throws {
  234. // Given
  235. let readExpectation = expectation(description: #function + "_1")
  236. let transformExpectation = expectation(description: #function + "_2")
  237. let completionExpectation = expectation(description: #function + "_3")
  238. let storageFake = StorageFake()
  239. let heartbeatStorage = HeartbeatStorage(id: #function, storage: storageFake)
  240. // When
  241. storageFake.onRead = {
  242. readExpectation.fulfill()
  243. return try XCTUnwrap("BAD_DATA".data(using: .utf8))
  244. }
  245. // Then
  246. heartbeatStorage.getAndSetAsync { heartbeatsBundle in
  247. XCTAssertNil(heartbeatsBundle)
  248. transformExpectation.fulfill()
  249. return heartbeatsBundle
  250. } completion: { result in
  251. switch result {
  252. case .success: break
  253. case let .failure(error): XCTFail("Error: \(error)")
  254. }
  255. completionExpectation.fulfill()
  256. }
  257. wait(
  258. for: [readExpectation, transformExpectation, completionExpectation],
  259. timeout: 0.5,
  260. enforceOrder: true
  261. )
  262. }
  263. func testGetAndSet_WhenSaveFails_ThrowsError() throws {
  264. // Given
  265. let expectation = expectation(description: #function)
  266. let storageFake = StorageFake()
  267. let heartbeatStorage = HeartbeatStorage(id: #function, storage: storageFake)
  268. // When
  269. storageFake.onWrite = { _ in
  270. expectation.fulfill()
  271. throw StorageError.writeError
  272. }
  273. // Then
  274. XCTAssertThrowsError(try heartbeatStorage.getAndSet { $0 })
  275. wait(for: [expectation], timeout: 0.5)
  276. }
  277. func testGetAndSetAsync_WhenSaveFails_ThrowsError() throws {
  278. // Given
  279. let transformExpectation = expectation(description: #function + "_1")
  280. let writeExpectation = expectation(description: #function + "_2")
  281. let completionExpectation = expectation(description: #function + "_3")
  282. let storageFake = StorageFake()
  283. let heartbeatStorage = HeartbeatStorage(id: #function, storage: storageFake)
  284. // When
  285. storageFake.onWrite = { _ in
  286. writeExpectation.fulfill()
  287. throw StorageError.writeError
  288. }
  289. // Then
  290. heartbeatStorage.getAndSetAsync { heartbeatsBundle in
  291. transformExpectation.fulfill()
  292. XCTAssertNil(heartbeatsBundle)
  293. return heartbeatsBundle
  294. } completion: { result in
  295. switch result {
  296. case .success: XCTFail("Error: unexpected success")
  297. case .failure: break
  298. }
  299. completionExpectation.fulfill()
  300. }
  301. wait(
  302. for: [transformExpectation, writeExpectation, completionExpectation],
  303. timeout: 0.5,
  304. enforceOrder: true
  305. )
  306. }
  307. func testOperationsAreSyncrononizedSerially() throws {
  308. // Given
  309. let heartbeatStorage = HeartbeatStorage(id: #function, storage: StorageFake())
  310. // When
  311. let expectations: [XCTestExpectation] = try (0 ... 1000).map { i in
  312. let expectation = expectation(description: "\(#function)_\(i)")
  313. let transform: @Sendable (HeartbeatsBundle?) -> HeartbeatsBundle? = { heartbeatsBundle in
  314. expectation.fulfill()
  315. return heartbeatsBundle
  316. }
  317. switch Int.random(in: 1 ... 3) {
  318. case 1:
  319. heartbeatStorage.readAndWriteAsync(using: transform)
  320. case 2:
  321. XCTAssertNoThrow(try heartbeatStorage.getAndSet(using: transform))
  322. case 3:
  323. let getAndSet = self.expectation(description: "GetAndSetAsync_\(i)")
  324. heartbeatStorage.getAndSetAsync(using: transform) { result in
  325. switch result {
  326. case .success: break
  327. case let .failure(error):
  328. XCTFail("Unexpected: Error occurred in getAndSet_\(i), \(error)")
  329. }
  330. getAndSet.fulfill()
  331. }
  332. wait(for: [getAndSet], timeout: 1.0)
  333. default:
  334. XCTFail("Unexpected: Random number is out of range.")
  335. }
  336. return expectation
  337. }
  338. // Then
  339. wait(for: expectations, timeout: 1.0, enforceOrder: true)
  340. }
  341. func testForMemoryLeakInInstanceManager() {
  342. // This unchecked Sendable class is used to avoid passing a non-sendable
  343. // type '[WeakContainer<HeartbeatStorage>]' to a `@Sendable` closure
  344. // (`DispatchQueue.global().async { ... }`).
  345. final class WeakRefs: @unchecked Sendable {
  346. // Lock is used to synchronize `weakRefs` during concurrent access.
  347. private(set) var weakRefs =
  348. UnfairLock<[WeakContainer<HeartbeatStorage>]>([])
  349. func append(_ weakRef: WeakContainer<HeartbeatStorage>) {
  350. weakRefs.withLock {
  351. $0.append(weakRef)
  352. }
  353. }
  354. }
  355. // Given
  356. let id = "testID"
  357. let weakRefs = WeakRefs()
  358. // When
  359. // Simulate concurrent access. This will help expose race conditions that could cause a crash.
  360. let group = DispatchGroup()
  361. for _ in 0 ..< 100 {
  362. group.enter()
  363. DispatchQueue.global().async {
  364. let instance = HeartbeatStorage.getInstance(id: id)
  365. weakRefs.append(WeakContainer(object: instance))
  366. group.leave()
  367. }
  368. }
  369. group.wait()
  370. // Then
  371. // The `weakRefs` array's references should all be nil; otherwise, something is being
  372. // unexpectedly strongly retained.
  373. weakRefs.weakRefs.withLock { refs in
  374. for weakRef in refs {
  375. XCTAssertNil(weakRef.object, "Potential memory leak detected.")
  376. }
  377. }
  378. }
  379. }
  380. private final class StorageFake: Storage, @unchecked Sendable {
  381. // The unchecked Sendable conformance is used to prevent warnings for the below var, which
  382. // violates the class's Sendable conformance. Ignoring this violation should be okay for
  383. // testing purposes.
  384. var fakeFile: Data?
  385. var onRead: (() throws -> Data)?
  386. var onWrite: ((Data?) throws -> Void)?
  387. func read() throws -> Data {
  388. if let onRead {
  389. return try onRead()
  390. } else if let data = fakeFile {
  391. return data
  392. } else {
  393. throw StorageError.readError
  394. }
  395. }
  396. func write(_ data: Data?) throws {
  397. if let onWrite {
  398. return try onWrite(data)
  399. } else {
  400. fakeFile = data
  401. }
  402. }
  403. }