HeartbeatLoggingIntegrationTests.swift 11 KB

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