HeartbeatLoggingIntegrationTests.swift 13 KB

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