glean-core/ios/GleanTests/Metrics/EventMetricTests.swift (290 lines of code) (raw):

/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ @testable import Glean import XCTest // The event extra properties. // This would be generated by the glean_parser usually. struct ClickExtras: EventExtras { var objectId: String? var other: String? func toExtraRecord() -> [String: String] { var record = [String: String]() if let objectId = self.objectId { record["object_id"] = objectId } if let other = self.other { record["other"] = other } return record } } // The event extra properties. // This would be generated by the glean_parser usually. struct TestExtras: EventExtras { var testName: String? func toExtraRecord() -> [String: String] { var record = [String: String]() if let testName = self.testName { record["test_name"] = testName } return record } } // The event extra properties. // This would be generated by the glean_parser usually. struct SomeExtras: EventExtras { var someExtra: String? func toExtraRecord() -> [String: String] { var record = [String: String]() if let someExtra = self.someExtra { record["some_extra"] = someExtra } return record } } class TestEventListener: GleanEventListener { let listenerTag = "TestEventListener" var lastSeenId: String = "" var count: Int64 = 0 func onEventRecorded(_ id: String) { self.lastSeenId = id self.count += 1 } } class EventMetricTypeTests: XCTestCase { var expectation: XCTestExpectation? var lastPingJson: [String: Any]? private func setupHttpResponseStub() { stubServerReceive { pingType, json in if pingType != "events" { // Skip non-events pings here. // This might include the initial "active" baseline ping. return } XCTAssert(json != nil) self.lastPingJson = json // Fulfill test's expectation once we parsed the incoming data. DispatchQueue.main.async { // Let the response get processed before we mark the expectation fulfilled self.expectation?.fulfill() } } } override func setUp() { resetGleanDiscardingInitialPings(testCase: self, tag: "EventMetricTypeTests") } override func tearDown() { lastPingJson = nil expectation = nil tearDownStubs() } func testEventSavesToStorage() { // Note: We specify both `Keys` and `Extras` here to ease testing. // In user code only _one_ will be specified and the other will be its `NoExtra` variant, // thus only allowing either the old API or the new one. let metric = EventMetricType<ClickExtras>(CommonMetricData( category: "ui", name: "click", sendInPings: ["store1"], lifetime: .ping, disabled: false ), ["object_id", "other"] ) XCTAssertNil(metric.testGetValue()) // Newer API metric.record(ClickExtras(objectId: "buttonA", other: "foo")) // Some extra keys can be left undefined. metric.record(ClickExtras(objectId: "buttonA")) /* SKIPPED: resetting system clock to return fixed time value */ // Old API, this is available only because we manually implemented the enum. // Generated code will have only one of the APIs available. metric.record(ClickExtras(objectId: "buttonB", other: "bar")) let events = metric.testGetValue()! XCTAssertEqual(3, events.count) XCTAssertEqual("ui", events[0].category) XCTAssertEqual("click", events[0].name) XCTAssertEqual("buttonA", events[0].extra?["object_id"]) XCTAssertEqual("foo", events[0].extra?["other"]) XCTAssertEqual("ui", events[1].category) XCTAssertEqual("click", events[1].name) XCTAssertEqual("buttonA", events[1].extra?["object_id"]) XCTAssertEqual(nil, events[1].extra?["other"]) XCTAssertEqual("ui", events[2].category) XCTAssertEqual("click", events[2].name) XCTAssertEqual("buttonB", events[2].extra?["object_id"]) XCTAssertEqual("bar", events[2].extra?["other"]) XCTAssertLessThanOrEqual(events[0].timestamp, events[1].timestamp, "The sequence of events must be preserved") } func testEventRecordedWithEmptyCategory() { let metric = EventMetricType<ClickExtras>(CommonMetricData( category: "", name: "click", sendInPings: ["store1"], lifetime: .ping, disabled: false ), ["object_id"]) XCTAssertNil(metric.testGetValue()) metric.record(ClickExtras(objectId: "buttonA")) /* SKIPPED: resetting system clock to return fixed time value */ metric.record(ClickExtras(objectId: "buttonB")) let events = metric.testGetValue()! XCTAssertEqual(2, events.count) XCTAssertEqual("click", events[0].name) XCTAssertEqual("click", events[1].name) XCTAssertLessThanOrEqual(events[0].timestamp, events[1].timestamp, "The sequence of events must be preserved") } func testEventNotRecordedWhenDisabled() { let metric = EventMetricType<NoExtras>(CommonMetricData( category: "ui", name: "click", sendInPings: ["store1"], lifetime: .ping, disabled: true ), nil) // Attempt to store the event. metric.record() // Check that nothing was recorded. XCTAssertNil(metric.testGetValue(), "Events must not be recorded if they are disabled") } func testEventGetValueReturnsNilIfNothingIsStored() { let metric = EventMetricType<NoExtras>(CommonMetricData( category: "ui", name: "click", sendInPings: ["store1"], lifetime: .ping, disabled: false ), nil) XCTAssertNil(metric.testGetValue()) } func testEventSavesToSecondaryPings() { let metric = EventMetricType<ClickExtras>(CommonMetricData( category: "ui", name: "click", sendInPings: ["store1", "store2"], lifetime: .ping, disabled: false ), ["object_id"]) XCTAssertNil(metric.testGetValue()) metric.record(ClickExtras(objectId: "buttonA")) /* SKIPPED: resetting system clock to return fixed time value */ metric.record(ClickExtras(objectId: "buttonB")) let events = metric.testGetValue("store2")! XCTAssertEqual(2, events.count) XCTAssertEqual("ui", events[0].category) XCTAssertEqual("click", events[0].name) XCTAssertEqual("ui", events[1].category) XCTAssertEqual("click", events[1].name) XCTAssertLessThanOrEqual(events[0].timestamp, events[1].timestamp, "The sequence of events must be preserved") } func testEventNotRecordWhenUploadDisabled() { let metric = EventMetricType<TestExtras>(CommonMetricData( category: "ui", name: "click", sendInPings: ["store1", "store2"], lifetime: .ping, disabled: false ), ["test_name"]) Glean.shared.setCollectionEnabled(true) metric.record(TestExtras(testName: "event1")) let snapshot1 = metric.testGetValue()! XCTAssertEqual(1, snapshot1.count) Glean.shared.setCollectionEnabled(false) metric.record(TestExtras(testName: "event2")) XCTAssertNil(metric.testGetValue()) Glean.shared.setCollectionEnabled(true) metric.record(TestExtras(testName: "event3")) let snapshot3 = metric.testGetValue()! XCTAssertEqual(1, snapshot3.count) } func testFlushQueuedEventsOnStartup() { setupHttpResponseStub() expectation = expectation(description: "Completed upload") let event = EventMetricType<SomeExtras>(CommonMetricData( category: "telemetry", name: "test_event", sendInPings: ["events"], lifetime: .ping, disabled: false ), ["some_extra"]) event.record(SomeExtras(someExtra: "bar")) Glean.shared.resetGlean(clearStores: false) waitForExpectations(timeout: 5.0) { error in XCTAssertNil(error, "Test timed out waiting for upload: \(error!)") } let events = lastPingJson?["events"] as? [Any] XCTAssertNotNil(events) XCTAssertEqual(1, events?.count) } private func getExtraValue(from event: Any?, for key: String) -> String { let event = event! as! [String: Any] let extras = event["extra"] as! [String: Any] return extras[key] as! String } func testFlushQueuedEventsOnStartupDroppingPreinitEvents() { setupHttpResponseStub() expectation = expectation(description: "Completed upload") let event = EventMetricType<SomeExtras>(CommonMetricData( category: "telemetry", name: "test_event", sendInPings: ["events"], lifetime: .ping, disabled: false ), ["some_extra"]) event.record(SomeExtras(someExtra: "run1")) XCTAssertEqual(1, event.testGetValue()!.count) Glean.shared.testDestroyGleanHandle(false) event.record(SomeExtras(someExtra: "pre-init")) Glean.shared.resetGlean(clearStores: false) event.record(SomeExtras(someExtra: "post-init")) waitForExpectations(timeout: 5.0) { error in XCTAssertNil(error, "Test timed out waiting for upload: \(error!)") } let events = lastPingJson?["events"] as? [Any] XCTAssertNotNil(events) XCTAssertEqual(1, events?.count) XCTAssertEqual("run1", getExtraValue(from: events![0], for: "some_extra")) setupHttpResponseStub() expectation = expectation(description: "Completed upload") Glean.shared.submitPingByName("events") waitForExpectations(timeout: 5.0) { error in XCTAssertNil(error, "Test timed out waiting for upload: \(error!)") } let events2 = lastPingJson?["events"] as? [Any] XCTAssertNotNil(events2) XCTAssertEqual(2, events2?.count) XCTAssertEqual("pre-init", getExtraValue(from: events2![0], for: "some_extra")) XCTAssertEqual("post-init", getExtraValue(from: events2![1], for: "some_extra")) } func testEventLongExtraRecordsError() { let metric = EventMetricType<TestExtras>(CommonMetricData( category: "ui", name: "click", sendInPings: ["store1", "store2"], lifetime: .ping, disabled: false ), ["test_name"]) metric.record(TestExtras(testName: String(repeating: "0123456789", count: 51))) XCTAssertEqual(1, metric.testGetNumRecordedErrors(.invalidOverflow)) } func testEventListener() { let event1 = EventMetricType<SomeExtras>(CommonMetricData( category: "telemetry", name: "test_event1", sendInPings: ["events"], lifetime: .ping, disabled: false ), ["some_extra"]) let event2 = EventMetricType<SomeExtras>(CommonMetricData( category: "telemetry", name: "test_event2", sendInPings: ["events"], lifetime: .ping, disabled: false ), ["some_extra"]) let event3 = EventMetricType<SomeExtras>(CommonMetricData( category: "telemetry", name: "test_event3", sendInPings: ["events"], lifetime: .ping, disabled: false ), ["some_extra"]) Glean.shared.resetGlean(clearStores: false) let listener = TestEventListener() // Register the listener Glean.shared.registerEventListener(tag: listener.listenerTag, listener: listener) // Ensure events are being reported via the callback event1.record(SomeExtras(someExtra: "uno")) XCTAssertEqual(1, listener.count) XCTAssertEqual("telemetry.test_event1", listener.lastSeenId) event2.record(SomeExtras(someExtra: "dos")) XCTAssertEqual(2, listener.count) XCTAssertEqual("telemetry.test_event2", listener.lastSeenId) event3.record(SomeExtras(someExtra: "tres")) XCTAssertEqual(3, listener.count) XCTAssertEqual("telemetry.test_event3", listener.lastSeenId) // Unregister the listener Glean.shared.unregisterEventListener(tag: listener.listenerTag) // Ensure events are no longer reported via the callback event1.record(SomeExtras(someExtra: "uno")) XCTAssertEqual(3, listener.count) XCTAssertEqual("telemetry.test_event3", listener.lastSeenId) event2.record(SomeExtras(someExtra: "dos")) XCTAssertEqual(3, listener.count) XCTAssertEqual("telemetry.test_event3", listener.lastSeenId) event3.record(SomeExtras(someExtra: "tres")) XCTAssertEqual(3, listener.count) XCTAssertEqual("telemetry.test_event3", listener.lastSeenId) } }