| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- // Copyright 2021 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- import XCTest
- @testable import FirebaseCoreInternal
- class HeartbeatLoggingIntegrationTests: XCTestCase {
- // 2021-11-01 @ 00:00:00 (EST)
- let date = Date(timeIntervalSince1970: 1_635_739_200)
- override func setUpWithError() throws {
- try HeartbeatLoggingTestUtils.removeUnderlyingHeartbeatStorageContainers()
- }
- override func tearDownWithError() throws {
- try HeartbeatLoggingTestUtils.removeUnderlyingHeartbeatStorageContainers()
- }
- /// This test may flake if it is executed during the transition from one day to the next.
- func testLogAndFlush() throws {
- // Given
- let heartbeatController = HeartbeatController(id: #function)
- let expectedDate = HeartbeatsPayload.dateFormatter.string(from: Date())
- // When
- heartbeatController.log("dummy_agent")
- let payload = heartbeatController.flush()
- // Then
- try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
- payload.headerValue(),
- """
- {
- "version": 2,
- "heartbeats": [
- {
- "agent": "dummy_agent",
- "dates": ["\(expectedDate)"]
- }
- ]
- }
- """
- )
- }
- /// This test may flake if it is executed during the transition from one day to the next.
- func testDoNotLogMoreThanOnceInACalendarDay() throws {
- // Given
- let heartbeatController = HeartbeatController(id: #function)
- heartbeatController.log("dummy_agent")
- heartbeatController.flush()
- // When
- heartbeatController.log("dummy_agent")
- // Then
- assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
- }
- /// This test may flake if it is executed during the transition from one day to the next.
- func testFlushHeartbeatFromToday() throws {
- // Given
- let heartbeatController = HeartbeatController(id: #function)
- let expectedDate = HeartbeatsPayload.dateFormatter.string(from: Date())
- // When
- heartbeatController.log("dummy_agent")
- let payload = heartbeatController.flushHeartbeatFromToday()
- // Then
- try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
- payload.headerValue(),
- """
- {
- "version": 2,
- "heartbeats": [
- {
- "agent": "dummy_agent",
- "dates": ["\(expectedDate)"]
- }
- ]
- }
- """
- )
- }
- func testMultipleControllersWithTheSameIDUseTheSameStorageInstance() throws {
- // Given
- let heartbeatController1 = HeartbeatController(id: #function, dateProvider: { self.date })
- let heartbeatController2 = HeartbeatController(id: #function, dateProvider: { self.date })
- // When
- heartbeatController1.log("dummy_agent")
- // Then
- let payload = heartbeatController2.flush()
- try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
- payload.headerValue(),
- """
- {
- "version": 2,
- "heartbeats": [
- {
- "agent": "dummy_agent",
- "dates": ["2021-11-01"]
- }
- ]
- }
- """
- )
- assertHeartbeatControllerFlushesEmptyPayload(heartbeatController1)
- }
- func testLogAndFlushConcurrencyStressTest() throws {
- // Given
- let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date })
- // When
- DispatchQueue.concurrentPerform(iterations: 100) { _ in
- heartbeatController.log("dummy_agent")
- }
- var payloads: [HeartbeatsPayload] = []
- DispatchQueue.concurrentPerform(iterations: 100) { _ in
- let payload = heartbeatController.flush()
- payloads.append(payload)
- }
- // Then
- let nonEmptyPayloads = payloads.filter { payload in
- // Filter out non-empty payloads.
- !payload.userAgentPayloads.isEmpty
- }
- XCTAssertEqual(nonEmptyPayloads.count, 1)
- let payload = try XCTUnwrap(nonEmptyPayloads.first)
- try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
- payload.headerValue(),
- """
- {
- "version": 2,
- "heartbeats": [
- {
- "agent": "dummy_agent",
- "dates": ["2021-11-01"]
- }
- ]
- }
- """
- )
- }
- func testLogAndFlushHeartbeatFromTodayConcurrencyStressTest() throws {
- // Given
- let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date })
- // When
- DispatchQueue.concurrentPerform(iterations: 100) { _ in
- heartbeatController.log("dummy_agent")
- }
- var payloads: [HeartbeatsPayload] = []
- DispatchQueue.concurrentPerform(iterations: 100) { _ in
- let payload = heartbeatController.flushHeartbeatFromToday()
- payloads.append(payload)
- }
- // Then
- let nonEmptyPayloads = payloads.filter { payload in
- // Filter out non-empty payloads.
- !payload.userAgentPayloads.isEmpty
- }
- XCTAssertEqual(nonEmptyPayloads.count, 1)
- let payload = try XCTUnwrap(nonEmptyPayloads.first)
- try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
- payload.headerValue(),
- """
- {
- "version": 2,
- "heartbeats": [
- {
- "agent": "dummy_agent",
- "dates": ["2021-11-01"],
- }
- ]
- }
- """
- )
- assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
- }
- func testLogRepeatedly_WithoutFlushing_LimitsOnWrite() throws {
- // Given
- var testdate = date
- let heartbeatController = HeartbeatController(id: #function, dateProvider: { testdate })
- // When
- // Iterate over 35 days and log a heartbeat each day.
- // - 30: The heartbeat logging library can store a max of 30 heartbeats. See
- // `HeartbeatController`'s `heartbeatsStorageCapacity` property.
- // - 5: Because of the above limit, expect 5 heartbeats to be overwritten.
- for day in 1 ... 35 {
- // A different user agent is logged based on the current iteration. There
- // is no particular reason for when each user agent is used– the goal is
- // to achieve a payload with multiple user agent groupings.
- if day < 5 {
- heartbeatController.log("dummy_agent_1")
- } else if day < 13 {
- heartbeatController.log("dummy_agent_2")
- } else {
- heartbeatController.log("dummy_agent_3")
- }
- testdate.addTimeInterval(60 * 60 * 24) // Advance the test date by 1 day.
- }
- // Then
- let payload = heartbeatController.flush()
- // The first 5 days of heartbeats (associated with `dummy_agent_1`) should
- // have been overwritten.
- try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
- payload.headerValue(),
- """
- {
- "version": 2,
- "heartbeats": [
- {
- "agent": "dummy_agent_2",
- "dates": [
- "2021-11-06",
- "2021-11-07",
- "2021-11-08",
- "2021-11-09",
- "2021-11-10",
- "2021-11-11",
- "2021-11-12"
- ]
- },
- {
- "agent": "dummy_agent_3",
- "dates": [
- "2021-12-01",
- "2021-12-02",
- "2021-12-03",
- "2021-12-04",
- "2021-12-05",
- "2021-11-13",
- "2021-11-14",
- "2021-11-15",
- "2021-11-16",
- "2021-11-17",
- "2021-11-18",
- "2021-11-19",
- "2021-11-20",
- "2021-11-21",
- "2021-11-22",
- "2021-11-23",
- "2021-11-24",
- "2021-11-25",
- "2021-11-26",
- "2021-11-27",
- "2021-11-28",
- "2021-11-29",
- "2021-11-30"
- ]
- }
- ]
- }
- """
- )
- }
- func testLogAndFlush_AfterUnderlyingStorageIsDeleted_CreatesNewStorage() throws {
- // Given
- let heartbeatController = HeartbeatController(id: #function, dateProvider: { self.date })
- heartbeatController.log("dummy_agent")
- _ = XCTWaiter.wait(for: [expectation(description: "Wait for async log.")], timeout: 0.1)
- // When
- XCTAssertNoThrow(try HeartbeatLoggingTestUtils.removeUnderlyingHeartbeatStorageContainers())
- // Then
- // 1. Assert controller flushes empty payload.
- assertHeartbeatControllerFlushesEmptyPayload(heartbeatController)
- // 2. Assert controller can log and flush non-empty payload.
- heartbeatController.log("dummy_agent")
- let payload = heartbeatController.flush()
- try HeartbeatLoggingTestUtils.assertEqualPayloadStrings(
- payload.headerValue(),
- """
- {
- "version": 2,
- "heartbeats": [
- {
- "agent": "dummy_agent",
- "dates": ["2021-11-01"]
- }
- ]
- }
- """
- )
- }
- func testInitializingControllerDoesNotModifyUnderlyingStorage() throws {
- // Given
- let id = #function
- // When
- _ = HeartbeatController(id: id)
- // Then
- #if os(tvOS)
- XCTAssertNil(
- UserDefaults(suiteName: HeartbeatLoggingTestUtils.Constants.heartbeatUserDefaultsSuiteName)?
- .object(forKey: "heartbeats-\(id)"),
- "Specified user defaults suite should be empty."
- )
- #else
- let heartbeatsDirectoryURL = FileManager.default
- .applicationSupportDirectory
- .appendingPathComponent(
- HeartbeatLoggingTestUtils.Constants.heartbeatFileStorageDirectoryPath,
- isDirectory: true
- )
- XCTAssertFalse(
- FileManager.default.fileExists(atPath: heartbeatsDirectoryURL.path),
- "Specified file path should not exist."
- )
- #endif
- }
- func testUnderlyingStorageLocationForRegressions() throws {
- // Given
- let id = #function
- let controller = HeartbeatController(id: id)
- // When
- controller.log("dummy_agent")
- _ = XCTWaiter.wait(for: [expectation(description: "Wait for async log.")], timeout: 0.1)
- // Then
- #if os(tvOS)
- XCTAssertNotNil(
- UserDefaults(suiteName: HeartbeatLoggingTestUtils.Constants.heartbeatUserDefaultsSuiteName)?
- .object(forKey: "heartbeats-\(id)"),
- "Data should not be nil."
- )
- #else
- let heartbeatsFileURL = FileManager.default
- .applicationSupportDirectory
- .appendingPathComponent(
- HeartbeatLoggingTestUtils.Constants.heartbeatFileStorageDirectoryPath,
- isDirectory: true
- )
- .appendingPathComponent(
- "heartbeats-\(id)", isDirectory: false
- )
- XCTAssertNotNil(try Data(contentsOf: heartbeatsFileURL), "Data should not be nil.")
- #endif
- }
- #if !os(tvOS)
- // Do not run on tvOS because tvOS uses UserDefaults to store heartbeats.
- func testControllerCreatesHeartbeatStorageWithSanitizedFileName() throws {
- // Given
- let appID = "1:123456789000:ios:abcdefghijklmnop"
- let sanitizedAppID = appID.replacingOccurrences(of: ":", with: "_")
- let controller = HeartbeatController(id: appID)
- // When
- // - Trigger the controller to write to the file system.
- controller.log("dummy_agent")
- _ = XCTWaiter.wait(for: [expectation(description: "Wait for async log.")], timeout: 0.1)
- // Then
- let heartbeatsDirectoryURL = FileManager.default
- .applicationSupportDirectory
- .appendingPathComponent(
- HeartbeatLoggingTestUtils.Constants.heartbeatFileStorageDirectoryPath,
- isDirectory: true
- )
- let directoryContents = try FileManager.default
- .contentsOfDirectory(atPath: heartbeatsDirectoryURL.path)
- XCTAssertEqual(directoryContents, ["heartbeats-\(sanitizedAppID)"])
- }
- #endif // !os(tvOS)
- }
|