Bug 1791789 - Fix for iOS startup crash caused by Glean (#2206)

This commit is contained in:
Travis Long 2022-09-29 22:07:24 -05:00 коммит произвёл GitHub
Родитель 232ac2a1d2
Коммит 679691540c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
5 изменённых файлов: 272 добавлений и 245 удалений

Просмотреть файл

@ -23,6 +23,7 @@
1FB8F8382326EABD00618E47 /* ConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB8F8372326EABD00618E47 /* ConfigurationTests.swift */; };
1FD4527523395B4500F4C7E8 /* UuidMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD4527423395B4500F4C7E8 /* UuidMetric.swift */; };
1FD4527723395EEB00F4C7E8 /* UuidMetricTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD4527623395EEB00F4C7E8 /* UuidMetricTests.swift */; };
60691AEB28DD0BF200BDF31A /* BaselinePingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60691AEA28DD0BF200BDF31A /* BaselinePingTests.swift */; };
AC06529C26E032E300D92D5E /* QuantityMetric.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC06529B26E032E300D92D5E /* QuantityMetric.swift */; };
AC06529E26E034BF00D92D5E /* QuantityMetricTypeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC06529D26E034BF00D92D5E /* QuantityMetricTypeTest.swift */; };
AC1DB401237EF0ED005A0F8A /* Glean.h in Headers */ = {isa = PBXBuildFile; fileRef = BF3DE3942243A2F20018E23F /* Glean.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -95,6 +96,7 @@
1FB8F8372326EABD00618E47 /* ConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationTests.swift; sourceTree = "<group>"; };
1FD4527423395B4500F4C7E8 /* UuidMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UuidMetric.swift; sourceTree = "<group>"; };
1FD4527623395EEB00F4C7E8 /* UuidMetricTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UuidMetricTests.swift; sourceTree = "<group>"; };
60691AEA28DD0BF200BDF31A /* BaselinePingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BaselinePingTests.swift; path = Net/BaselinePingTests.swift; sourceTree = "<group>"; };
AC06529B26E032E300D92D5E /* QuantityMetric.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuantityMetric.swift; sourceTree = "<group>"; };
AC06529D26E034BF00D92D5E /* QuantityMetricTypeTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuantityMetricTypeTest.swift; sourceTree = "<group>"; };
BF10007F23548B0500064051 /* MemoryDistributionMetric.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryDistributionMetric.swift; sourceTree = "<group>"; };
@ -292,6 +294,7 @@
BF3DE39E2243A2F20018E23F /* GleanTests */ = {
isa = PBXGroup;
children = (
60691AEA28DD0BF200BDF31A /* BaselinePingTests.swift */,
1F39E7B1239F0741009B13B3 /* Debug */,
1FB8F8392326EBA500618E47 /* Config */,
BF43A8CB232A613100545310 /* Metrics */,
@ -546,7 +549,7 @@
};
CD0CADA527E216F40015A997 /* Run UniFFI bindgen */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 12;
files = (
);
inputFileListPaths = (
@ -611,6 +614,7 @@
files = (
1F39E7B3239F0777009B13B3 /* GleanDebugUtilityTests.swift in Sources */,
BFAED50A2369752400DF293D /* StringListMetricTests.swift in Sources */,
60691AEB28DD0BF200BDF31A /* BaselinePingTests.swift in Sources */,
BF890561232BC227003CA2BA /* StringMetricTests.swift in Sources */,
CD0F7CC226F0F28900EDA6A4 /* UrlMetricTests.swift in Sources */,
BFCBD6AB246D55CC0032096D /* TestUtils.swift in Sources */,

Просмотреть файл

@ -150,36 +150,33 @@ public class HttpPingUploader {
func process() {
// Limits are enforced by glean-core to avoid an inifinite loop here.
// Whenever a limit is reached, this binding will receive `.done` and step out.
while true {
let task = gleanGetUploadTask()
let task = gleanGetUploadTask()
switch task {
case let .upload(request):
var body = Data(capacity: request.body.count)
body.append(contentsOf: request.body)
self.upload(path: request.path, data: body, headers: request.headers) { result in
let action = gleanProcessPingUploadResponse(request.documentId, result)
switch action {
case .next:
// launch a new iteration.
Dispatchers.shared.launchAsync {
HttpPingUploader(configuration: self.config, testingMode: self.testingMode).process()
}
case .end:
return
switch task {
case let .upload(request):
var body = Data(capacity: request.body.count)
body.append(contentsOf: request.body)
self.upload(path: request.path, data: body, headers: request.headers) { result in
let action = gleanProcessPingUploadResponse(request.documentId, result)
switch action {
case .next:
// launch a new iteration.
Dispatchers.shared.launchAsync {
HttpPingUploader(configuration: self.config, testingMode: self.testingMode).process()
}
case .end:
return
}
// we don't want to launch multiple uploads at once.
// if the upload finishes, we going to launch the next one.
return
case .wait(let time):
sleep(UInt32(time) / 1000)
continue
case .done:
return
}
case .wait(let time):
sleep(UInt32(time) / 1000)
// launch a new iteration.
Dispatchers.shared.launchAsync {
HttpPingUploader(configuration: self.config, testingMode: self.testingMode).process()
}
case .done:
return
}
}
}

Просмотреть файл

@ -9,7 +9,6 @@ import XCTest
private typealias GleanInternalMetrics = GleanMetrics.GleanInternalMetrics
// swiftlint:disable type_body_length
class GleanTests: XCTestCase {
var expectation: XCTestExpectation?
@ -103,224 +102,6 @@ class GleanTests: XCTestCase {
)
}
func testSendingOfForegroundBaselinePing() {
stubServerReceive { _, json in
// Check for the "dirty_startup" flag
let pingInfo = json?["ping_info"] as? [String: Any]
XCTAssertEqual("active", pingInfo?["reason"] as? String)
// We may get error metrics in foreground pings,
// so 'metrics' may exist.
let metrics = json?["metrics"] as? [String: Any]
if metrics != nil {
// Since we are only expecting error metrics,
// let's check that this is all we got (plus the `validation.first_run_hour`).
XCTAssertEqual(metrics?.count, 2, "metrics has more keys than expected: \(JSONStringify(metrics!))")
let labeledCounters = metrics?["labeled_counter"] as? [String: Any]
labeledCounters!.forEach { key, _ in
XCTAssertTrue(
key.starts(with: "glean.error") || key.starts(with: "glean.validation"),
"Should only see glean.* counters, saw \(key)"
)
}
}
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
// Set up the expectation that will be fulfilled by the stub above
expectation = expectation(description: "Baseline Ping Received")
// Set the last time the "metrics" ping was sent to now. This is required for us to not
// send a metrics pings the first time we initialize Glean and to keep it from interfering
// with these tests.
let now = Date()
Glean.shared.metricsPingScheduler!.updateSentDate(now)
// Resetting Glean doesn't trigger lifecycle events in tests so we must call the method
// invoked by the lifecycle observer directly.
Glean.shared.handleForegroundEvent()
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
/*
FIXME: This test causes crashes in subsequent tests,
probably because of some race condition triggered by restarting Glean.
func testSendingOfBaselinePingWithDirtyFlag() {
// Set the dirty flag
gleanSetDirtyFlag(true)
// Set up the test stub based on the default telemetry endpoint
stubServerReceive { pingType, json in
XCTAssertEqual("baseline", pingType)
XCTAssert(json != nil)
// Check for the "dirty_startup" flag
let pingInfo = json!["ping_info"] as! [String: Any]
let reason = pingInfo["reason"] as! String
if reason == "active" {
// Skip initial "active" ping.
// Glean is initialized ahead of this test and thus we might get one.
return
}
XCTAssertEqual("dirty_startup", reason, "Expected a dirty_startup, got \(reason)")
// 'metrics' will exist and include exactly one valid metric.
// No errors should be reported.
let metrics = json!["metrics"] as? [String: Any]
if metrics != nil {
let datetimes = metrics!["datetime"] as! [String: Any]
XCTAssertTrue(datetimes.keys.contains("glean.validation.first_run_hour"),
"Datetime should have first_run_hour: \(datetimes)")
if metrics!.count > 1 {
// Since we are only expecting error metrics,
// let's check that this is all we got (plus the `validation.first_run_hour`).
XCTAssertEqual(metrics?.count, 2, "metrics has more keys than expected: \(JSONStringify(metrics!))")
let labeledCounters = metrics?["labeled_counter"] as? [String: Any]
labeledCounters!.forEach { key, _ in
XCTAssertTrue(
key.starts(with: "glean.error") || key.starts(with: "glean.validation"),
"Should only see glean.* counters, saw \(key)"
)
}
}
}
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
// Set up the expectation that will be fulfilled by the stub above
expectation = expectation(description: "Baseline Ping Received")
// Set the last time the "metrics" ping was sent to now. This is required for us to not
// send a metrics pings the first time we initialize Glean and to keep it from interfering
// with these tests.
let now = Date()
Glean.shared.metricsPingScheduler.updateSentDate(now)
// Restart Glean and don't clear the stores and then await the expectation
Glean.shared.resetGlean(clearStores: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
*/
func testSendingDeletionPingIfDisabledOutsideOfRun() {
stubServerReceive { pingType, _ in
// Since we are starting Glean with upload disabled, the only ping we
// should see is the deletion request ping
XCTAssertEqual("deletion-request", pingType)
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
// Set up the expectation that will be fulfilled by the stub above
expectation = expectation(description: "Deletion Request Received")
// Now reset Glean with uploadEnabled = false and not clearing the stores to
// trigger the deletion request ping. Since `uploadEnabled` is `false`, only
// the deletion-request ping should be generated.
Glean.shared.resetGlean(clearStores: false, uploadEnabled: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
func testNotSendingDeletionRequestIfUnchangedOutsideOfRun() {
XCTAssert(Glean.shared.isInitialized(), "Glean should be initialized")
// Set up the test stub based on the default telemetry endpoint
stubServerReceive { _, _ in
XCTFail("Should not have recieved any ping")
}
// Set up the expectation that will NOT be fulfilled by the stub above. If it is
// then it will trigger an assertion due to the `assertForOverFulfill` property.
expectation = expectation(description: "Deletion Request Received")
// So we can wait for expectations below, we will go ahead and fulfill the
// expectation. We want to assert if the ping is triggered and over fulfills it
// from the stub above.
expectation?.fulfill()
// Reset Glean with uploadEnabled = false
Glean.shared.resetGlean(clearStores: true, uploadEnabled: false)
// Now reset Glean with uploadEnabled = false again without clearing the stores to
// make sure we don't trigger the deletion request ping. If it does, then we will
// have overfulfilled the expectation which will trigger a test assertion.
Glean.shared.resetGlean(clearStores: false, uploadEnabled: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
// swiftlint:disable force_cast
func testSendingOfStartupBaselinePingWithAppLifetimeMetric() {
// Set the dirty flag.
gleanSetDirtyFlag(true)
let stringMetric = StringMetricType(CommonMetricData(
category: "telemetry",
name: "app_lifetime",
sendInPings: ["baseline"],
lifetime: .application,
disabled: false
))
stringMetric.set("HELLOOOOO!")
// Set up the test stub based on the default telemetry endpoint
stubServerReceive { pingType, json in
XCTAssertEqual("baseline", pingType)
XCTAssert(json != nil)
// Check for the "dirty_startup" flag
let pingInfo = json!["ping_info"] as! [String: Any]
let reason = pingInfo["reason"] as! String
if reason == "active" {
// Skip initial "active" ping.
// Glean is initialized ahead of this test and thus we might get one.
return
}
XCTAssertEqual("dirty_startup", reason)
// Ensure there is only the expected locale string metric
let metrics = json?["metrics"] as? [String: Any]
let strings = metrics?["string"] as? [String: Any]
let metric = strings?["telemetry.app_lifetime"] as? String
XCTAssertEqual("HELLOOOOO!", metric)
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
expectation = expectation(description: "baseline ping received")
// Restart glean and don't clear the stores.
// This should trigger a baseline ping with a "dirty_startup" reason.
Glean.shared.resetGlean(clearStores: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
// swiftlint:enable force_cast
func testGleanIsNotInitializedFromOtherProcesses() {
// Check to see if Glean is initialized
XCTAssert(Glean.shared.isInitialized())

Просмотреть файл

@ -0,0 +1,187 @@
/* 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 OHHTTPStubs
import OHHTTPStubsSwift
import XCTest
final class BaselinePingTests: XCTestCase {
var expectation: XCTestExpectation?
override func setUp() {
resetGleanDiscardingInitialPings(testCase: self, tag: "GleanTests")
}
override func tearDown() {
Glean.shared.testDestroyGleanHandle()
expectation = nil
tearDownStubs()
}
func testSendingOfForegroundBaselinePing() {
stubServerReceive { _, json in
// Check for the "dirty_startup" flag
let pingInfo = json?["ping_info"] as? [String: Any]
XCTAssertEqual("active", pingInfo?["reason"] as? String)
// We may get error metrics in foreground pings,
// so 'metrics' may exist.
let metrics = json?["metrics"] as? [String: Any]
if metrics != nil {
// Since we are only expecting error metrics,
// let's check that this is all we got (plus the `validation.first_run_hour`).
XCTAssertEqual(metrics?.count, 2, "metrics has more keys than expected: \(JSONStringify(metrics!))")
let labeledCounters = metrics?["labeled_counter"] as? [String: Any]
labeledCounters!.forEach { key, _ in
XCTAssertTrue(
key.starts(with: "glean.error") || key.starts(with: "glean.validation"),
"Should only see glean.* counters, saw \(key)"
)
}
}
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
// Set up the expectation that will be fulfilled by the stub above
expectation = expectation(description: "Baseline Ping Received")
// Set the last time the "metrics" ping was sent to now. This is required for us to not
// send a metrics pings the first time we initialize Glean and to keep it from interfering
// with these tests.
let now = Date()
Glean.shared.metricsPingScheduler!.updateSentDate(now)
// Resetting Glean doesn't trigger lifecycle events in tests so we must call the method
// invoked by the lifecycle observer directly.
Glean.shared.handleForegroundEvent()
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
/*
FIXME: This test causes crashes in subsequent tests,
probably because of some race condition triggered by restarting Glean.
func testSendingOfBaselinePingWithDirtyFlag() {
// Set the dirty flag
gleanSetDirtyFlag(true)
// Set up the test stub based on the default telemetry endpoint
stubServerReceive { pingType, json in
XCTAssertEqual("baseline", pingType)
XCTAssert(json != nil)
// Check for the "dirty_startup" flag
let pingInfo = json!["ping_info"] as! [String: Any]
let reason = pingInfo["reason"] as! String
if reason == "active" {
// Skip initial "active" ping.
// Glean is initialized ahead of this test and thus we might get one.
return
}
XCTAssertEqual("dirty_startup", reason, "Expected a dirty_startup, got \(reason)")
// 'metrics' will exist and include exactly one valid metric.
// No errors should be reported.
let metrics = json!["metrics"] as? [String: Any]
if metrics != nil {
let datetimes = metrics!["datetime"] as! [String: Any]
XCTAssertTrue(datetimes.keys.contains("glean.validation.first_run_hour"),
"Datetime should have first_run_hour: \(datetimes)")
if metrics!.count > 1 {
// Since we are only expecting error metrics,
// let's check that this is all we got (plus the `validation.first_run_hour`).
XCTAssertEqual(metrics?.count, 2, "metrics has more keys than expected: \(JSONStringify(metrics!))")
let labeledCounters = metrics?["labeled_counter"] as? [String: Any]
labeledCounters!.forEach { key, _ in
XCTAssertTrue(
key.starts(with: "glean.error") || key.starts(with: "glean.validation"),
"Should only see glean.* counters, saw \(key)"
)
}
}
}
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
// Set up the expectation that will be fulfilled by the stub above
expectation = expectation(description: "Baseline Ping Received")
// Set the last time the "metrics" ping was sent to now. This is required for us to not
// send a metrics pings the first time we initialize Glean and to keep it from interfering
// with these tests.
let now = Date()
Glean.shared.metricsPingScheduler.updateSentDate(now)
// Restart Glean and don't clear the stores and then await the expectation
Glean.shared.resetGlean(clearStores: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
*/
// swiftlint:disable force_cast
func testSendingOfStartupBaselinePingWithAppLifetimeMetric() {
// Set the dirty flag.
gleanSetDirtyFlag(true)
let stringMetric = StringMetricType(CommonMetricData(
category: "telemetry",
name: "app_lifetime",
sendInPings: ["baseline"],
lifetime: .application,
disabled: false
))
stringMetric.set("HELLOOOOO!")
// Set up the test stub based on the default telemetry endpoint
stubServerReceive { pingType, json in
XCTAssertEqual("baseline", pingType)
XCTAssert(json != nil)
// Check for the "dirty_startup" flag
let pingInfo = json!["ping_info"] as! [String: Any]
let reason = pingInfo["reason"] as! String
if reason == "active" {
// Skip initial "active" ping.
// Glean is initialized ahead of this test and thus we might get one.
return
}
XCTAssertEqual("dirty_startup", reason)
// Ensure there is only the expected locale string metric
let metrics = json?["metrics"] as? [String: Any]
let strings = metrics?["string"] as? [String: Any]
let metric = strings?["telemetry.app_lifetime"] as? String
XCTAssertEqual("HELLOOOOO!", metric)
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
expectation = expectation(description: "baseline ping received")
// Restart glean and don't clear the stores.
// This should trigger a baseline ping with a "dirty_startup" reason.
Glean.shared.resetGlean(clearStores: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
// swiftlint:enable force_cast
}

Просмотреть файл

@ -27,11 +27,69 @@ class DeletionRequestPingTests: XCTestCase {
}
override func tearDown() {
Glean.shared.testDestroyGleanHandle()
lastPingJson = nil
expectation = nil
tearDownStubs()
}
func testSendingDeletionPingIfDisabledOutsideOfRun() {
resetGleanDiscardingInitialPings(testCase: self, tag: "GleanTests")
stubServerReceive { pingType, _ in
// Since we are starting Glean with upload disabled, the only ping we
// should see is the deletion request ping
XCTAssertEqual("deletion-request", pingType)
DispatchQueue.main.async {
// let the response get processed before we mark the expectation fulfilled
self.expectation?.fulfill()
}
}
// Set up the expectation that will be fulfilled by the stub above
expectation = expectation(description: "Deletion Request Received")
// Now reset Glean with uploadEnabled = false and not clearing the stores to
// trigger the deletion request ping. Since `uploadEnabled` is `false`, only
// the deletion-request ping should be generated.
Glean.shared.resetGlean(clearStores: false, uploadEnabled: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
func testNotSendingDeletionRequestIfUnchangedOutsideOfRun() {
resetGleanDiscardingInitialPings(testCase: self, tag: "GleanTests")
XCTAssert(Glean.shared.isInitialized(), "Glean should be initialized")
// Set up the test stub based on the default telemetry endpoint
stubServerReceive { _, _ in
XCTFail("Should not have recieved any ping")
}
// Set up the expectation that will NOT be fulfilled by the stub above. If it is
// then it will trigger an assertion due to the `assertForOverFulfill` property.
expectation = expectation(description: "Deletion Request Received")
// So we can wait for expectations below, we will go ahead and fulfill the
// expectation. We want to assert if the ping is triggered and over fulfills it
// from the stub above.
expectation?.fulfill()
// Reset Glean with uploadEnabled = false
Glean.shared.resetGlean(clearStores: true, uploadEnabled: false)
// Now reset Glean with uploadEnabled = false again without clearing the stores to
// make sure we don't trigger the deletion request ping. If it does, then we will
// have overfulfilled the expectation which will trigger a test assertion.
Glean.shared.resetGlean(clearStores: false, uploadEnabled: false)
waitForExpectations(timeout: 5.0) { error in
XCTAssertNil(error, "Test timed out waiting for upload: \(error!)")
}
}
func testDeletionRequestPingsAreSentWhenUploadDisabled() {
resetGleanDiscardingInitialPings(testCase: self, tag: "DeletionRequestPingTests")