HeartbeatLoggingIntegrationTests.swift 13 KB

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