diff options
Diffstat (limited to 'third_party/rust/glean/src/test.rs')
-rw-r--r-- | third_party/rust/glean/src/test.rs | 1453 |
1 files changed, 1453 insertions, 0 deletions
diff --git a/third_party/rust/glean/src/test.rs b/third_party/rust/glean/src/test.rs new file mode 100644 index 0000000000..cb41b49b66 --- /dev/null +++ b/third_party/rust/glean/src/test.rs @@ -0,0 +1,1453 @@ +// 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 https://mozilla.org/MPL/2.0/. + +use std::io::Read; +use std::sync::{Arc, Barrier, Mutex}; +use std::thread::{self, ThreadId}; +use std::time::{Duration, Instant}; + +use crossbeam_channel::RecvTimeoutError; +use flate2::read::GzDecoder; +use glean_core::glean_test_get_experimentation_id; +use serde_json::Value as JsonValue; + +use crate::private::PingType; +use crate::private::{BooleanMetric, CounterMetric, EventMetric, StringMetric, TextMetric}; + +use super::*; +use crate::common_test::{lock_test, new_glean, GLOBAL_APPLICATION_ID}; + +#[test] +fn send_a_ping() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Define a new ping and submit it. + const PING_NAME: &str = "test-ping"; + let custom_ping = private::PingType::new(PING_NAME, true, true, true, vec![]); + custom_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains(PING_NAME)); +} + +#[test] +fn disabling_upload_disables_metrics_recording() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric = BooleanMetric::new(CommonMetricData { + name: "bool_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + crate::set_upload_enabled(false); + + assert!(metric.test_get_value(Some("store1".into())).is_none()) +} + +#[test] +fn test_experiments_recording() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + set_experiment_active("experiment_test".to_string(), "branch_a".to_string(), None); + let mut extra = HashMap::new(); + extra.insert("test_key".to_string(), "value".to_string()); + set_experiment_active( + "experiment_api".to_string(), + "branch_b".to_string(), + Some(extra), + ); + assert!(test_is_experiment_active("experiment_test".to_string())); + assert!(test_is_experiment_active("experiment_api".to_string())); + set_experiment_inactive("experiment_test".to_string()); + assert!(!test_is_experiment_active("experiment_test".to_string())); + assert!(test_is_experiment_active("experiment_api".to_string())); + let stored_data = test_get_experiment_data("experiment_api".to_string()).unwrap(); + assert_eq!("branch_b", stored_data.branch); + assert_eq!("value", stored_data.extra.unwrap()["test_key"]); +} + +#[test] +fn test_experiments_recording_before_glean_inits() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + + set_experiment_active( + "experiment_set_preinit".to_string(), + "branch_a".to_string(), + None, + ); + set_experiment_active( + "experiment_preinit_disabled".to_string(), + "branch_a".to_string(), + None, + ); + set_experiment_inactive("experiment_preinit_disabled".to_string()); + + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + assert!(test_is_experiment_active( + "experiment_set_preinit".to_string() + )); + assert!(!test_is_experiment_active( + "experiment_preinit_disabled".to_string() + )); +} + +#[test] +fn test_experimentation_id_recording() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_experimentation_id("alpha-beta-gamma-delta".to_string()) + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + let exp_id = glean_test_get_experimentation_id().expect("Experimentation id must not be None"); + assert_eq!( + "alpha-beta-gamma-delta".to_string(), + exp_id, + "Experimentation id must match" + ); +} + +#[test] +fn sending_of_foreground_background_pings() { + let _lock = lock_test(); + + let click: EventMetric<traits::NoExtraKeys> = private::EventMetric::new(CommonMetricData { + name: "click".into(), + category: "ui".into(), + send_in_pings: vec!["events".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<String>(3); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Simulate becoming active. + handle_client_active(); + + // We expect a baseline ping to be generated here (reason: 'active'). + let url = r.recv().unwrap(); + assert!(url.contains("baseline")); + + // Recording an event so that an "events" ping will contain data. + click.record(None); + + // Simulate becoming inactive + handle_client_inactive(); + + // Wait for the pings to arrive. + let mut expected_pings = vec!["baseline", "events"]; + for _ in 0..2 { + let url = r.recv().unwrap(); + // If the url contains the expected reason, remove it from the list. + expected_pings.retain(|&name| !url.contains(name)); + } + // We received all the expected pings. + assert_eq!(0, expected_pings.len()); + + // Simulate becoming active again. + handle_client_active(); + + // We expect a baseline ping to be generated here (reason: 'active'). + let url = r.recv().unwrap(); + assert!(url.contains("baseline")); +} + +#[test] +fn sending_of_startup_baseline_ping() { + let _lock = lock_test(); + + // Create an instance of Glean and then flip the dirty + // bit to true. + let data_dir = new_glean(None, true); + + glean_core::glean_set_dirty_flag(true); + + // Restart glean and wait for a baseline ping to be generated. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let tmpname = data_dir.path().to_path_buf(); + + // Now reset Glean: it should still send a baseline ping with reason + // dirty_startup when starting, because of the dirty bit being set. + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("baseline")); +} + +#[test] +fn no_dirty_baseline_on_clean_shutdowns() { + let _lock = lock_test(); + + // Create an instance of Glean, wait for init and then flip the dirty + // bit to true. + let data_dir = new_glean(None, true); + + glean_core::glean_set_dirty_flag(true); + + crate::shutdown(); + + // Restart glean and wait for a baseline ping to be generated. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let tmpname = data_dir.path().to_path_buf(); + + // Now reset Glean: it should not send a baseline ping, because + // we cleared the dirty bit. + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + // We don't expect a startup ping. + assert_eq!(r.try_recv(), Err(crossbeam_channel::TryRecvError::Empty)); +} + +#[test] +fn initialize_must_not_crash_if_data_dir_is_messed_up() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpdirname = dir.path(); + // Create a file in the temporary dir and use that as the + // name of the Glean data dir. + let file_path = tmpdirname.to_path_buf().join("notadir"); + std::fs::write(file_path.clone(), "test").expect("The test Glean dir file must be created"); + + let cfg = ConfigurationBuilder::new(true, file_path, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(); + + test_reset_glean(cfg, ClientInfoMetrics::unknown(), false); + + // We don't need to sleep here. + // The `test_reset_glean` already waited on the initialize task. +} + +#[test] +fn queued_recorded_metrics_correctly_record_during_init() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + + let metric = CounterMetric::new(CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + // This will queue 3 tasks that will add to the metric value once Glean is initialized + for _ in 0..3 { + metric.add(1); + } + + // TODO: To be fixed in bug 1677150. + // Ensure that no value has been stored yet since the tasks have only been queued + // and not executed yet + + // Calling `new_glean` here will cause Glean to be initialized and should cause the queued + // tasks recording metrics to execute + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(); + let _t = new_glean(Some(cfg), false); + + // Verify that the callback was executed by testing for the correct value + assert!(metric.test_get_value(None).is_some(), "Value must exist"); + assert_eq!(3, metric.test_get_value(None).unwrap(), "Value must match"); +} + +#[test] +fn initializing_twice_is_a_noop() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + test_reset_glean( + ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + ClientInfoMetrics::unknown(), + true, + ); + + // Glean was initialized and it waited for a full initialization to finish. + // We now just want to try to initialize again. + // This will bail out early. + + crate::initialize( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + ClientInfoMetrics::unknown(), + ); + + // We don't need to sleep here. + // The `test_reset_glean` already waited on the initialize task, + // and the 2nd initialize will bail out early. + // + // All we tested is that this didn't crash. +} + +#[test] +fn dont_handle_events_when_uninitialized() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + test_reset_glean( + ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + ClientInfoMetrics::unknown(), + true, + ); + + // Ensure there's at least one event recorded, + // otherwise the ping is not sent. + let click: EventMetric<traits::NoExtraKeys> = private::EventMetric::new(CommonMetricData { + name: "click".into(), + category: "ui".into(), + send_in_pings: vec!["events".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + click.record(None); + // Wait for the dispatcher. + assert_ne!(None, click.test_get_value(None)); + + // Now destroy Glean. We test submission when not initialized. + destroy_glean(false, &tmpname); + + // We reach into `glean_core` to test this, + // only there we can synchronously submit and get a return value. + assert!(!glean_core::glean_submit_ping_by_name_sync( + "events".to_string(), + None + )); +} + +// TODO: Should probably move into glean-core. +#[test] +fn the_app_channel_must_be_correctly_set_if_requested() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + // Internal metric, replicated here for testing. + let app_channel = StringMetric::new(CommonMetricData { + name: "app_channel".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }); + + // No app_channel reported. + let client_info = ClientInfoMetrics { + channel: None, + ..ClientInfoMetrics::unknown() + }; + test_reset_glean( + ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + client_info, + true, + ); + assert!(app_channel.test_get_value(None).is_none()); + + // Custom app_channel reported. + let client_info = ClientInfoMetrics { + channel: Some("testing".into()), + ..ClientInfoMetrics::unknown() + }; + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + client_info, + true, + ); + assert_eq!("testing", app_channel.test_get_value(None).unwrap()); +} + +#[test] +fn ping_collection_must_happen_after_concurrently_scheduled_metrics_recordings() { + // Given the following block of code: + // + // Metric.A.set("SomeTestValue") + // Glean.submitPings(listOf("custom-ping-1")) + // + // This test ensures that "custom-ping-1" contains "metric.a" with a value of "SomeTestValue" + // when the ping is collected. + + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<(String, JsonValue)>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + // Decode the gzipped body. + let mut gzip_decoder = GzDecoder::new(&body[..]); + let mut s = String::with_capacity(body.len()); + + let data = gzip_decoder + .read_to_string(&mut s) + .ok() + .map(|_| &s[..]) + .or_else(|| std::str::from_utf8(&body).ok()) + .and_then(|payload| serde_json::from_str(payload).ok()) + .unwrap(); + self.sender.send((url, data)).unwrap(); + net::UploadResult::http_status(200) + } + } + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(), + ClientInfoMetrics::unknown(), + true, + ); + + let ping_name = "custom_ping_1"; + let ping = private::PingType::new(ping_name, true, false, true, vec![]); + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec![ping_name.into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + let test_value = "SomeTestValue"; + metric.set(test_value.to_string()); + ping.submit(None); + + // Wait for the ping to arrive. + let (url, body) = r.recv().unwrap(); + assert!(url.contains(ping_name)); + + assert_eq!( + test_value, + body["metrics"]["string"]["telemetry.string_metric"] + ); +} + +#[test] +fn basic_metrics_should_be_cleared_when_disabling_uploading() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["default".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + assert!(metric.test_get_value(None).is_none()); + + metric.set("TEST VALUE".into()); + assert!(metric.test_get_value(None).is_some()); + + set_upload_enabled(false); + assert!(metric.test_get_value(None).is_none()); + metric.set("TEST VALUE".into()); + assert!(metric.test_get_value(None).is_none()); + + set_upload_enabled(true); + assert!(metric.test_get_value(None).is_none()); + metric.set("TEST VALUE".into()); + assert_eq!("TEST VALUE", metric.test_get_value(None).unwrap()); +} + +// TODO: Should probably move into glean-core. +#[test] +fn core_metrics_should_be_cleared_and_restored_when_disabling_and_enabling_uploading() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + // No app_channel reported. + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + ClientInfoMetrics::unknown(), + true, + ); + + // Internal metric, replicated here for testing. + let os_version = StringMetric::new(CommonMetricData { + name: "os_version".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }); + + assert!(os_version.test_get_value(None).is_some()); + + set_upload_enabled(false); + assert!(os_version.test_get_value(None).is_none()); + + set_upload_enabled(true); + assert!(os_version.test_get_value(None).is_some()); +} + +#[test] +fn sending_deletion_ping_if_disabled_outside_of_run() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(); + + let _t = new_glean(Some(cfg), true); + + // Now reset Glean and disable upload: it should still send a deletion request + // ping even though we're just starting. + test_reset_glean( + ConfigurationBuilder::new(false, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("deletion-request")); +} + +#[test] +fn no_sending_of_deletion_ping_if_unchanged_outside_of_run() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(); + + let _t = new_glean(Some(cfg), true); + + // Now reset Glean and keep upload enabled: no deletion-request + // should be sent. + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + assert_eq!(0, r.len()); +} + +#[test] +fn deletion_request_ping_contains_experimentation_id() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<JsonValue>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<JsonValue>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + let mut gzip_decoder = GzDecoder::new(&body[..]); + let mut body_str = String::with_capacity(body.len()); + let data: JsonValue = gzip_decoder + .read_to_string(&mut body_str) + .ok() + .map(|_| &body_str[..]) + .or_else(|| std::str::from_utf8(&body).ok()) + .and_then(|payload| serde_json::from_str(payload).ok()) + .unwrap(); + self.sender.send(data).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_experimentation_id("alpha-beta-gamma-delta".to_string()) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Now reset Glean and disable upload: it should still send a deletion request + // ping even though we're just starting. + test_reset_glean( + ConfigurationBuilder::new(false, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .with_experimentation_id("alpha-beta-gamma-delta".to_string()) + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + // Wait for the ping to arrive and check the experimentation id matches + let url = r.recv().unwrap(); + let metrics = url.get("metrics").unwrap(); + let strings = metrics.get("string").unwrap(); + assert_eq!( + "alpha-beta-gamma-delta", + strings + .get("glean.client.annotation.experimentation_id") + .unwrap() + ); +} + +#[test] +fn test_sending_of_startup_baseline_ping_with_application_lifetime_metric() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<(String, JsonValue)>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + // Decode the gzipped body. + let mut gzip_decoder = GzDecoder::new(&body[..]); + let mut s = String::with_capacity(body.len()); + + let data = gzip_decoder + .read_to_string(&mut s) + .ok() + .map(|_| &s[..]) + .or_else(|| std::str::from_utf8(&body).ok()) + .and_then(|payload| serde_json::from_str(payload).ok()) + .unwrap(); + self.sender.send((url, data)).unwrap(); + net::UploadResult::http_status(200) + } + } + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + test_reset_glean( + ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .build(), + ClientInfoMetrics::unknown(), + true, + ); + + // Reaching into the core. + glean_core::glean_set_dirty_flag(true); + + let metric = private::StringMetric::new(CommonMetricData { + name: "app_lifetime".into(), + category: "telemetry".into(), + send_in_pings: vec!["baseline".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }); + let test_value = "HELLOOOOO!"; + metric.set(test_value.into()); + assert_eq!(test_value, metric.test_get_value(None).unwrap()); + + // Restart glean and don't clear the stores. + test_reset_glean( + ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(), + ClientInfoMetrics::unknown(), + false, + ); + + let (url, body) = r.recv().unwrap(); + assert!(url.contains("/baseline/")); + + // We set the dirty bit above. + assert_eq!("dirty_startup", body["ping_info"]["reason"]); + assert_eq!( + test_value, + body["metrics"]["string"]["telemetry.app_lifetime"] + ); +} + +#[test] +fn setting_debug_view_tag_before_initialization_should_not_crash() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Attempt to set a debug view tag before Glean is initialized. + set_debug_view_tag("valid-tag"); + + // Create a custom configuration to use a fake uploader. + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("inactive")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag", + headers.iter().find(|&kv| kv.0 == "X-Debug-ID").unwrap().1 + ); +} + +#[test] +fn setting_source_tags_before_initialization_should_not_crash() { + let _lock = lock_test(); + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + destroy_glean(true, &tmpname); + //assert!(!was_initialize_called()); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Attempt to set source tags before Glean is initialized. + set_source_tags(vec!["valid-tag1".to_string(), "valid-tag2".to_string()]); + + // Create a custom configuration to use a fake uploader. + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("inactive")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag1,valid-tag2", + headers + .iter() + .find(|&kv| kv.0 == "X-Source-Tags") + .unwrap() + .1 + ); +} + +#[test] +fn setting_source_tags_after_initialization_should_not_crash() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Attempt to set source tags after `Glean.initialize` is called, + // but before Glean is fully initialized. + //assert!(was_initialize_called()); + set_source_tags(vec!["valid-tag1".to_string(), "valid-tag2".to_string()]); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("inactive")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag1,valid-tag2", + headers + .iter() + .find(|&kv| kv.0 == "X-Source-Tags") + .unwrap() + .1 + ); +} + +#[test] +fn flipping_upload_enabled_respects_order_of_events() { + // NOTES(janerik): + // I'm reasonably sure this test is excercising the right code paths + // and from the log output it does the right thing: + // + // * It fully initializes with the assumption uploadEnabled=true + // * It then disables upload + // * Then it submits the custom ping, which rightfully is ignored because uploadEnabled=false. + // + // The test passes. + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + // We create a ping and a metric before we initialize Glean + let sample_ping = PingType::new("sample-ping-1", true, false, true, vec![]); + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["sample-ping-1".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + let _t = new_glean(Some(cfg), true); + + // Glean might still be initializing. Disable upload. + set_upload_enabled(false); + + // Set data and try to submit a custom ping. + metric.set("some-test-value".into()); + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("deletion-request")); +} + +#[test] +fn registering_pings_before_init_must_work() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom ping and attempt its registration. + let sample_ping = PingType::new("pre-register", true, true, true, vec![]); + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Submit a baseline ping. + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("pre-register")); +} + +#[test] +fn test_a_ping_before_submission() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { sender: s }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Create a custom ping and register it. + let sample_ping = PingType::new("custom1", true, true, true, vec![]); + + let metric = CounterMetric::new(CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["custom1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + metric.add(1); + + sample_ping.test_before_next_submit(move |reason| { + assert_eq!(None, reason); + assert_eq!(1, metric.test_get_value(None).unwrap()); + }); + + // Submit a baseline ping. + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("custom1")); +} + +#[test] +fn test_boolean_get_num_errors() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + let metric = BooleanMetric::new(CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["custom1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: Some(str::to_string("asdf")), + }); + + // Check specifically for an invalid label + let result = metric.test_get_num_recorded_errors(ErrorType::InvalidLabel); + + assert_eq!(result, 0); +} + +#[test] +fn test_text_can_hold_long_string() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + let metric = TextMetric::new(CommonMetricData { + name: "text_metric".into(), + category: "test".into(), + send_in_pings: vec!["custom1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: Some(str::to_string("text")), + }); + + // 216 characters, which would overflow StringMetric + metric.set("I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain".into()); + + let result = metric.test_get_num_recorded_errors(ErrorType::InvalidValue); + assert_eq!(result, 0); + + let result = metric.test_get_num_recorded_errors(ErrorType::InvalidOverflow); + assert_eq!(result, 0); +} + +#[test] +fn signaling_done() { + let _lock = lock_test(); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + barrier: Arc<Barrier>, + counter: Arc<Mutex<HashMap<ThreadId, u32>>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + let mut map = self.counter.lock().unwrap(); + *map.entry(thread::current().id()).or_insert(0) += 1; + + // Wait for the sync. + self.barrier.wait(); + + // Signal that this uploader thread is done. + net::UploadResult::done() + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + // We use a barrier to sync this test thread with the uploader thread. + let barrier = Arc::new(Barrier::new(2)); + // We count how many times `upload` was invoked per thread. + let call_count = Arc::new(Mutex::default()); + + let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { + barrier: Arc::clone(&barrier), + counter: Arc::clone(&call_count), + }) + .build(); + + let _t = new_glean(Some(cfg), true); + + // Define a new ping and submit it. + const PING_NAME: &str = "test-ping"; + let custom_ping = private::PingType::new(PING_NAME, true, true, true, vec![]); + custom_ping.submit(None); + custom_ping.submit(None); + + // Sync up with the upload thread. + barrier.wait(); + + // Submit another ping and wait for it to do work. + custom_ping.submit(None); + + // Sync up with the upload thread again. + // This will not be the same thread as the one before (hopefully). + barrier.wait(); + + // No one's ever gonna wait for the uploader thread (the RLB doesn't store the handle to it), + // so all we can do is hope it finishes within time. + std::thread::sleep(std::time::Duration::from_millis(100)); + + let map = call_count.lock().unwrap(); + assert_eq!(2, map.len(), "should have launched 2 uploader threads"); + for &count in map.values() { + assert_eq!(1, count, "each thread should call upload only once"); + } +} + +#[test] +fn configure_ping_throttling() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + done: Arc<std::sync::atomic::AtomicBool>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + if self.done.load(std::sync::atomic::Ordering::SeqCst) { + // If we've outlived the test, just lie. + return net::UploadResult::http_status(200); + } + self.sender.send(url).unwrap(); + net::UploadResult::http_status(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().to_path_buf(); + + let done = Arc::new(std::sync::atomic::AtomicBool::new(false)); + let mut cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID) + .with_server_endpoint("invalid-test-host") + .with_uploader(FakeUploader { + sender: s, + done: Arc::clone(&done), + }) + .build(); + let pings_per_interval = 10; + cfg.rate_limit = Some(crate::PingRateLimit { + seconds_per_interval: 1, + pings_per_interval, + }); + + let _t = new_glean(Some(cfg), true); + + // Define a new ping. + const PING_NAME: &str = "test-ping"; + let custom_ping = private::PingType::new(PING_NAME, true, true, true, vec![]); + + // Submit and receive it `pings_per_interval` times. + for _ in 0..pings_per_interval { + custom_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains(PING_NAME)); + } + + // Submit one ping more than the rate limit permits. + custom_ping.submit(None); + + // We'd expect it to be received within 250ms if it weren't throttled. + let now = Instant::now(); + assert_eq!( + r.recv_deadline(now + Duration::from_millis(250)), + Err(RecvTimeoutError::Timeout) + ); + + // We still have to deal with that eleventh ping. + // When it eventually processes after the throttle interval, this'll tell + // it that it's done. + done.store(true, std::sync::atomic::Ordering::SeqCst); + // Unfortunately, we'll still be stuck waiting the full + // `seconds_per_interval` before running the next test, since shutting down + // will wait for the queue to clear. +} |