HeartbeatStorageTests.swift 14 KB

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