HeartbeatLoggingIntegrationTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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. class HeartbeatLoggingIntegrationTests: XCTestCase {
  17. // 2021-11-01 @ 00:00:00 (EST)
  18. let date = Date(timeIntervalSince1970: 1_635_739_200)
  19. override func setUpWithError() throws {
  20. try HeartbeatLoggingTestUtils.removeUnderlyingHeartbeatStorageContainers()
  21. }
  22. override func tearDownWithError() throws {
  23. try HeartbeatLoggingTestUtils.removeUnderlyingHeartbeatStorageContainers()
  24. }
  25. /// This test may flake if it is executed during the transition from one day to the next.
  26. func testLogAndFlush() throws {
  27. // Given
  28. let heartbeatController = HeartbeatController(id: #function)
  29. let expectedDate = HeartbeatsPayload.dateFormatter.string(from: Date())
  30. // When
  31. heartbeatController.log("dummy_agent")
  32. let payload = heartbeatController.flush()
  33. // Then
  34. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  35. payload.headerValue(),
  36. """
  37. {
  38. "version": 2,
  39. "heartbeats": [
  40. {
  41. "agent": "dummy_agent",
  42. "dates": ["\(expectedDate)"]
  43. }
  44. ]
  45. }
  46. """
  47. )
  48. }
  49. func testLogAndFlushAsync() throws {
  50. // Given
  51. let heartbeatController = HeartbeatController(id: #function)
  52. let expectedDate = HeartbeatsPayload.dateFormatter.string(from: Date())
  53. let expectation = self.expectation(description: #function)
  54. // When
  55. heartbeatController.log("dummy_agent")
  56. heartbeatController.flushAsync { payload in
  57. // Then
  58. do {
  59. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  60. payload.headerValue(),
  61. """
  62. {
  63. "version": 2,
  64. "heartbeats": [
  65. {
  66. "agent": "dummy_agent",
  67. "dates": ["\(expectedDate)"]
  68. }
  69. ]
  70. }
  71. """
  72. )
  73. expectation.fulfill()
  74. } catch {
  75. XCTFail("Unexpected error: \(error)")
  76. }
  77. }
  78. waitForExpectations(timeout: 1.0)
  79. }
  80. /// This test may flake if it is executed during the transition from one day to the next.
  81. func testDoNotLogMoreThanOnceInACalendarDay() throws {
  82. // Given
  83. let heartbeatController = HeartbeatController(id: #function)
  84. heartbeatController.log("dummy_agent")
  85. heartbeatController.flush()
  86. // When
  87. heartbeatController.log("dummy_agent")
  88. // Then
  89. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
  90. }
  91. /// This test may flake if it is executed during the transition from one day to the next.
  92. func testFlushHeartbeatFromToday() throws {
  93. // Given
  94. let heartbeatController = HeartbeatController(id: #function)
  95. let expectedDate = HeartbeatsPayload.dateFormatter.string(from: Date())
  96. // When
  97. heartbeatController.log("dummy_agent")
  98. let payload = heartbeatController.flushHeartbeatFromToday()
  99. // Then
  100. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  101. payload.headerValue(),
  102. """
  103. {
  104. "version": 2,
  105. "heartbeats": [
  106. {
  107. "agent": "dummy_agent",
  108. "dates": ["\(expectedDate)"]
  109. }
  110. ]
  111. }
  112. """
  113. )
  114. }
  115. func testMultipleControllersWithTheSameIDUseTheSameStorageInstance() throws {
  116. // Given
  117. let heartbeatController1 = HeartbeatController(id: #function, dateProvider: { self.date })
  118. let heartbeatController2 = HeartbeatController(id: #function, dateProvider: { self.date })
  119. // When
  120. heartbeatController1.log("dummy_agent")
  121. // Then
  122. let payload = heartbeatController2.flush()
  123. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  124. payload.headerValue(),
  125. """
  126. {
  127. "version": 2,
  128. "heartbeats": [
  129. {
  130. "agent": "dummy_agent",
  131. "dates": ["2021-11-01"]
  132. }
  133. ]
  134. }
  135. """
  136. )
  137. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController1)
  138. }
  139. func testLogAndFlushConcurrencyStressTest() throws {
  140. // Given
  141. let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date })
  142. // When
  143. DispatchQueue.concurrentPerform(iterations: 100) { _ in
  144. heartbeatController.log("dummy_agent")
  145. }
  146. var payloads: [HeartbeatsPayload] = []
  147. DispatchQueue.concurrentPerform(iterations: 100) { _ in
  148. let payload = heartbeatController.flush()
  149. payloads.append(payload)
  150. }
  151. // Then
  152. let nonEmptyPayloads = payloads.filter { payload in
  153. // Filter out non-empty payloads.
  154. !payload.userAgentPayloads.isEmpty
  155. }
  156. XCTAssertEqual(nonEmptyPayloads.count, 1)
  157. let payload = try XCTUnwrap(nonEmptyPayloads.first)
  158. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  159. payload.headerValue(),
  160. """
  161. {
  162. "version": 2,
  163. "heartbeats": [
  164. {
  165. "agent": "dummy_agent",
  166. "dates": ["2021-11-01"]
  167. }
  168. ]
  169. }
  170. """
  171. )
  172. }
  173. func testLogAndFlushHeartbeatFromTodayConcurrencyStressTest() throws {
  174. // Given
  175. let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date })
  176. // When
  177. DispatchQueue.concurrentPerform(iterations: 100) { _ in
  178. heartbeatController.log("dummy_agent")
  179. }
  180. var payloads: [HeartbeatsPayload] = []
  181. DispatchQueue.concurrentPerform(iterations: 100) { _ in
  182. let payload = heartbeatController.flushHeartbeatFromToday()
  183. payloads.append(payload)
  184. }
  185. // Then
  186. let nonEmptyPayloads = payloads.filter { payload in
  187. // Filter out non-empty payloads.
  188. !payload.userAgentPayloads.isEmpty
  189. }
  190. XCTAssertEqual(nonEmptyPayloads.count, 1)
  191. let payload = try XCTUnwrap(nonEmptyPayloads.first)
  192. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  193. payload.headerValue(),
  194. """
  195. {
  196. "version": 2,
  197. "heartbeats": [
  198. {
  199. "agent": "dummy_agent",
  200. "dates": ["2021-11-01"],
  201. }
  202. ]
  203. }
  204. """
  205. )
  206. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
  207. }
  208. func testLogRepeatedly_WithoutFlushing_LimitsOnWrite() throws {
  209. // Given
  210. let testDate = AdjustableDate(date: date)
  211. let heartbeatController = HeartbeatController(id: #function, dateProvider: { testDate.date })
  212. // When
  213. // Iterate over 35 days and log a heartbeat each day.
  214. // - 30: The heartbeat logging library can store a max of 30 heartbeats. See
  215. // `HeartbeatController`'s `heartbeatsStorageCapacity` property.
  216. // - 5: Because of the above limit, expect 5 heartbeats to be overwritten.
  217. for day in 1 ... 35 {
  218. // A different user agent is logged based on the current iteration. There
  219. // is no particular reason for when each user agent is used– the goal is
  220. // to achieve a payload with multiple user agent groupings.
  221. if day < 5 {
  222. heartbeatController.log("dummy_agent_1")
  223. } else if day < 13 {
  224. heartbeatController.log("dummy_agent_2")
  225. } else {
  226. heartbeatController.log("dummy_agent_3")
  227. }
  228. testDate.date.addTimeInterval(60 * 60 * 24) // Advance the test date by 1 day.
  229. }
  230. // Then
  231. let payload = heartbeatController.flush()
  232. // The first 5 days of heartbeats (associated with `dummy_agent_1`) should
  233. // have been overwritten.
  234. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  235. payload.headerValue(),
  236. """
  237. {
  238. "version": 2,
  239. "heartbeats": [
  240. {
  241. "agent": "dummy_agent_2",
  242. "dates": [
  243. "2021-11-06",
  244. "2021-11-07",
  245. "2021-11-08",
  246. "2021-11-09",
  247. "2021-11-10",
  248. "2021-11-11",
  249. "2021-11-12"
  250. ]
  251. },
  252. {
  253. "agent": "dummy_agent_3",
  254. "dates": [
  255. "2021-12-01",
  256. "2021-12-02",
  257. "2021-12-03",
  258. "2021-12-04",
  259. "2021-12-05",
  260. "2021-11-13",
  261. "2021-11-14",
  262. "2021-11-15",
  263. "2021-11-16",
  264. "2021-11-17",
  265. "2021-11-18",
  266. "2021-11-19",
  267. "2021-11-20",
  268. "2021-11-21",
  269. "2021-11-22",
  270. "2021-11-23",
  271. "2021-11-24",
  272. "2021-11-25",
  273. "2021-11-26",
  274. "2021-11-27",
  275. "2021-11-28",
  276. "2021-11-29",
  277. "2021-11-30"
  278. ]
  279. }
  280. ]
  281. }
  282. """
  283. )
  284. }
  285. func testLogAndFlush_AfterUnderlyingStorageIsDeleted_CreatesNewStorage() throws {
  286. // Given
  287. let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date })
  288. heartbeatController.log("dummy_agent")
  289. _ = XCTWaiter.wait(for: [expectation(description: "Wait for async log.")], timeout: 0.1)
  290. // When
  291. XCTAssertNoThrow(try HeartbeatLoggingTestUtils.removeUnderlyingHeartbeatStorageContainers())
  292. // Then
  293. // 1. Assert controller flushes empty payload.
  294. assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
  295. // 2. Assert controller can log and flush non-empty payload.
  296. heartbeatController.log("dummy_agent")
  297. let payload = heartbeatController.flush()
  298. try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
  299. payload.headerValue(),
  300. """
  301. {
  302. "version": 2,
  303. "heartbeats": [
  304. {
  305. "agent": "dummy_agent",
  306. "dates": ["2021-11-01"]
  307. }
  308. ]
  309. }
  310. """
  311. )
  312. }
  313. func testInitializingControllerDoesNotModifyUnderlyingStorage() throws {
  314. // Given
  315. let id = #function
  316. // When
  317. _ = HeartbeatController(id: id)
  318. // Then
  319. #if os(tvOS)
  320. XCTAssertNil(
  321. UserDefaults(suiteName: HeartbeatLoggingTestUtils.Constants.heartbeatUserDefaultsSuiteName)?
  322. .object(forKey: "heartbeats-\(id)"),
  323. "Specified user defaults suite should be empty."
  324. )
  325. #else
  326. let heartbeatsDirectoryURL = FileManager.default
  327. .applicationSupportDirectory
  328. .appendingPathComponent(
  329. HeartbeatLoggingTestUtils.Constants.heartbeatFileStorageDirectoryPath,
  330. isDirectory: true
  331. )
  332. XCTAssertFalse(
  333. FileManager.default.fileExists(atPath: heartbeatsDirectoryURL.path),
  334. "Specified file path should not exist."
  335. )
  336. #endif
  337. }
  338. func testUnderlyingStorageLocationForRegressions() throws {
  339. // Given
  340. let id = #function
  341. let controller = HeartbeatController(id: id)
  342. // When
  343. controller.log("dummy_agent")
  344. _ = XCTWaiter.wait(for: [expectation(description: "Wait for async log.")], timeout: 0.1)
  345. // Then
  346. #if os(tvOS)
  347. XCTAssertNotNil(
  348. UserDefaults(suiteName: HeartbeatLoggingTestUtils.Constants.heartbeatUserDefaultsSuiteName)?
  349. .object(forKey: "heartbeats-\(id)"),
  350. "Data should not be nil."
  351. )
  352. #else
  353. let heartbeatsFileURL = FileManager.default
  354. .applicationSupportDirectory
  355. .appendingPathComponent(
  356. HeartbeatLoggingTestUtils.Constants.heartbeatFileStorageDirectoryPath,
  357. isDirectory: true
  358. )
  359. .appendingPathComponent(
  360. "heartbeats-\(id)", isDirectory: false
  361. )
  362. XCTAssertNotNil(try Data(contentsOf: heartbeatsFileURL), "Data should not be nil.")
  363. #endif
  364. }
  365. #if !os(tvOS)
  366. // Do not run on tvOS because tvOS uses UserDefaults to store heartbeats.
  367. func testControllerCreatesHeartbeatStorageWithSanitizedFileName() throws {
  368. // Given
  369. let appID = "1:123456789000:ios:abcdefghijklmnop"
  370. let sanitizedAppID = appID.replacingOccurrences(of: ":", with: "_")
  371. let controller = HeartbeatController(id: appID)
  372. // When
  373. // - Trigger the controller to write to the file system.
  374. controller.log("dummy_agent")
  375. _ = XCTWaiter.wait(for: [expectation(description: "Wait for async log.")], timeout: 0.1)
  376. // Then
  377. let heartbeatsDirectoryURL = FileManager.default
  378. .applicationSupportDirectory
  379. .appendingPathComponent(
  380. HeartbeatLoggingTestUtils.Constants.heartbeatFileStorageDirectoryPath,
  381. isDirectory: true
  382. )
  383. let directoryContents = try FileManager.default
  384. .contentsOfDirectory(atPath: heartbeatsDirectoryURL.path)
  385. XCTAssertEqual(directoryContents, ["heartbeats-\(sanitizedAppID)"])
  386. }
  387. #endif // !os(tvOS)
  388. }