summaryrefslogtreecommitdiffstats
path: root/third_party/rust/glean-core/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/glean-core/tests
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/glean-core/tests')
-rw-r--r--third_party/rust/glean-core/tests/boolean.rs91
-rw-r--r--third_party/rust/glean-core/tests/common/mod.rs142
-rw-r--r--third_party/rust/glean-core/tests/counter.rs177
-rw-r--r--third_party/rust/glean-core/tests/custom_distribution.rs437
-rw-r--r--third_party/rust/glean-core/tests/datetime.rs187
-rw-r--r--third_party/rust/glean-core/tests/event.rs290
-rw-r--r--third_party/rust/glean-core/tests/jwe.rs113
-rw-r--r--third_party/rust/glean-core/tests/labeled.rs395
-rw-r--r--third_party/rust/glean-core/tests/memory_distribution.rs193
-rw-r--r--third_party/rust/glean-core/tests/metrics.rs37
-rw-r--r--third_party/rust/glean-core/tests/ping.rs103
-rw-r--r--third_party/rust/glean-core/tests/ping_maker.rs210
-rw-r--r--third_party/rust/glean-core/tests/quantity.rs118
-rw-r--r--third_party/rust/glean-core/tests/storage.rs105
-rw-r--r--third_party/rust/glean-core/tests/string.rs121
-rw-r--r--third_party/rust/glean-core/tests/string_list.rs249
-rw-r--r--third_party/rust/glean-core/tests/timespan.rs353
-rw-r--r--third_party/rust/glean-core/tests/timing_distribution.rs336
-rw-r--r--third_party/rust/glean-core/tests/uuid.rs114
19 files changed, 3771 insertions, 0 deletions
diff --git a/third_party/rust/glean-core/tests/boolean.rs b/third_party/rust/glean-core/tests/boolean.rs
new file mode 100644
index 0000000000..c640048d6e
--- /dev/null
+++ b/third_party/rust/glean-core/tests/boolean.rs
@@ -0,0 +1,91 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{CommonMetricData, Lifetime};
+
+// SKIPPED from glean-ac: string deserializer should correctly parse integers
+// This test doesn't really apply to rkv
+
+#[test]
+fn boolean_serializer_should_correctly_serialize_boolean() {
+ let (mut tempdir, _) = tempdir();
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let metric = BooleanMetric::new(CommonMetricData {
+ name: "boolean_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+
+ metric.set(&glean, true);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"boolean": {"telemetry.boolean_metric": true}}),
+ snapshot
+ );
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _t) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"boolean": {"telemetry.boolean_metric": true}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn set_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = BooleanMetric::new(CommonMetricData {
+ name: "boolean_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set(&glean, true);
+
+ // Check that the data was correctly set in each store.
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!({"boolean": {"telemetry.boolean_metric": true}}),
+ snapshot
+ );
+ }
+}
+
+// SKIPPED from glean-ac: booleans are serialized in the correct JSON format
+// Completely redundant with other tests.
diff --git a/third_party/rust/glean-core/tests/common/mod.rs b/third_party/rust/glean-core/tests/common/mod.rs
new file mode 100644
index 0000000000..1d96e617ae
--- /dev/null
+++ b/third_party/rust/glean-core/tests/common/mod.rs
@@ -0,0 +1,142 @@
+// 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/.
+
+// #[allow(dead_code)] is required on this module as a workaround for
+// https://github.com/rust-lang/rust/issues/46379
+#![allow(dead_code)]
+use glean_core::{Glean, Result};
+
+use std::fs::{read_dir, File};
+use std::io::{BufRead, BufReader};
+use std::path::Path;
+
+use chrono::offset::TimeZone;
+use iso8601::Date::YMD;
+use serde_json::Value as JsonValue;
+
+use ctor::ctor;
+
+/// Initialize the logger for all tests without individual tests requiring to call the init code.
+/// Log output can be controlled via the environment variable `RUST_LOG` for the `glean_core` crate,
+/// e.g.:
+///
+/// ```
+/// export RUST_LOG=glean_core=debug
+/// ```
+#[ctor]
+fn enable_test_logging() {
+ // When testing we want all logs to go to stdout/stderr by default,
+ // without requiring each individual test to activate it.
+ // This only applies to glean-core tests, users of the main library still need to call
+ // `glean_enable_logging` of the FFI component (automatically done by the platform wrappers).
+ let _ = env_logger::builder().is_test(true).try_init();
+}
+
+pub fn tempdir() -> (tempfile::TempDir, String) {
+ let t = tempfile::tempdir().unwrap();
+ let name = t.path().display().to_string();
+ (t, name)
+}
+
+pub const GLOBAL_APPLICATION_ID: &str = "org.mozilla.glean.test.app";
+
+// Creates a new instance of Glean with a temporary directory.
+// We need to keep the `TempDir` alive, so that it's not deleted before we stop using it.
+pub fn new_glean(tempdir: Option<tempfile::TempDir>) -> (Glean, tempfile::TempDir) {
+ let dir = match tempdir {
+ Some(tempdir) => tempdir,
+ None => tempfile::tempdir().unwrap(),
+ };
+ let tmpname = dir.path().display().to_string();
+
+ let cfg = glean_core::Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ language_binding_name: "Rust".into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ };
+ let glean = Glean::new(cfg).unwrap();
+
+ (glean, dir)
+}
+
+/// Converts an iso8601::DateTime to a chrono::DateTime<FixedOffset>
+pub fn iso8601_to_chrono(datetime: &iso8601::DateTime) -> chrono::DateTime<chrono::FixedOffset> {
+ if let YMD { year, month, day } = datetime.date {
+ return chrono::FixedOffset::east(datetime.time.tz_offset_hours * 3600)
+ .ymd(year, month, day)
+ .and_hms_milli(
+ datetime.time.hour,
+ datetime.time.minute,
+ datetime.time.second,
+ datetime.time.millisecond,
+ );
+ };
+ panic!("Unsupported datetime format");
+}
+
+/// Gets a vector of the currently queued pings.
+///
+/// # Arguments
+///
+/// * `data_path` - Glean's data path, as returned from Glean::get_data_path()
+///
+/// # Returns
+///
+/// A vector of all queued pings.
+///
+/// Each entry is a pair `(url, json_data, metadata)`,
+/// where `url` is the endpoint the ping will go to, `json_data` is the JSON payload
+/// and metadata is optional persisted data related to the ping.
+pub fn get_queued_pings(data_path: &Path) -> Result<Vec<(String, JsonValue, Option<JsonValue>)>> {
+ get_pings(&data_path.join("pending_pings"))
+}
+
+/// Gets a vector of the currently queued `deletion-request` pings.
+///
+/// # Arguments
+///
+/// * `data_path` - Glean's data path, as returned from Glean::get_data_path()
+///
+/// # Returns
+///
+/// A vector of all queued pings.
+///
+/// Each entry is a pair `(url, json_data, metadata)`,
+/// where `url` is the endpoint the ping will go to, `json_data` is the JSON payload
+/// and metadata is optional persisted data related to the ping.
+pub fn get_deletion_pings(data_path: &Path) -> Result<Vec<(String, JsonValue, Option<JsonValue>)>> {
+ get_pings(&data_path.join("deletion_request"))
+}
+
+fn get_pings(pings_dir: &Path) -> Result<Vec<(String, JsonValue, Option<JsonValue>)>> {
+ let entries = read_dir(pings_dir)?;
+ Ok(entries
+ .filter_map(|entry| entry.ok())
+ .filter(|entry| match entry.file_type() {
+ Ok(file_type) => file_type.is_file(),
+ Err(_) => false,
+ })
+ .filter_map(|entry| File::open(entry.path()).ok())
+ .filter_map(|file| {
+ let mut lines = BufReader::new(file).lines();
+ if let (Some(Ok(url)), Some(Ok(body)), Ok(metadata)) =
+ (lines.next(), lines.next(), lines.next().transpose())
+ {
+ let parsed_metadata = metadata.map(|m| {
+ serde_json::from_str::<JsonValue>(&m).expect("metadata should be valid JSON")
+ });
+ if let Ok(parsed_body) = serde_json::from_str::<JsonValue>(&body) {
+ Some((url, parsed_body, parsed_metadata))
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ .collect())
+}
diff --git a/third_party/rust/glean-core/tests/counter.rs b/third_party/rust/glean-core/tests/counter.rs
new file mode 100644
index 0000000000..ccada50fb0
--- /dev/null
+++ b/third_party/rust/glean-core/tests/counter.rs
@@ -0,0 +1,177 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+use glean_core::{CommonMetricData, Lifetime};
+
+// Tests ported from glean-ac
+
+// SKIPPED from glean-ac: counter deserializer should correctly parse integers
+// This test doesn't really apply to rkv
+
+#[test]
+fn counter_serializer_should_correctly_serialize_counters() {
+ let (mut tempdir, _) = tempdir();
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let metric = CounterMetric::new(CommonMetricData {
+ name: "counter_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+
+ metric.add(&glean, 1);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"counter": {"telemetry.counter_metric": 1}}),
+ snapshot
+ );
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _t) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"counter": {"telemetry.counter_metric": 1}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn set_value_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = CounterMetric::new(CommonMetricData {
+ name: "counter_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.add(&glean, 1);
+
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!({"counter": {"telemetry.counter_metric": 1}}),
+ snapshot
+ );
+ }
+}
+
+// SKIPPED from glean-ac: counters are serialized in the correct JSON format
+// Completely redundant with other tests.
+
+#[test]
+fn counters_must_not_increment_when_passed_zero_or_negative() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = CounterMetric::new(CommonMetricData {
+ name: "counter_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Application,
+ ..Default::default()
+ });
+
+ // Attempt to increment the counter with zero
+ metric.add(&glean, 0);
+ // Check that nothing was recorded
+ assert!(metric.test_get_value(&glean, "store1").is_none());
+
+ // Attempt to increment the counter with negative
+ metric.add(&glean, -1);
+ // Check that nothing was recorded
+ assert!(metric.test_get_value(&glean, "store1").is_none());
+
+ // Attempt increment counter properly
+ metric.add(&glean, 1);
+ // Check that nothing was recorded
+ assert_eq!(1, metric.test_get_value(&glean, "store1").unwrap());
+
+ // Make sure that the errors have been recorded
+ assert_eq!(
+ Ok(2),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue, None)
+ );
+}
+
+// New tests for glean-core below
+
+#[test]
+fn transformation_works() {
+ let (glean, _t) = new_glean(None);
+
+ let counter: CounterMetric = CounterMetric::new(CommonMetricData {
+ name: "transformation".into(),
+ category: "local".into(),
+ send_in_pings: vec!["store1".into(), "store2".into()],
+ ..Default::default()
+ });
+
+ counter.add(&glean, 2);
+
+ assert_eq!(2, counter.test_get_value(&glean, "store1").unwrap());
+ assert_eq!(2, counter.test_get_value(&glean, "store2").unwrap());
+
+ // Clearing just one store
+ let _ = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ counter.add(&glean, 2);
+
+ assert_eq!(2, counter.test_get_value(&glean, "store1").unwrap());
+ assert_eq!(4, counter.test_get_value(&glean, "store2").unwrap());
+}
+
+#[test]
+fn saturates_at_boundary() {
+ let (glean, _t) = new_glean(None);
+
+ let counter: CounterMetric = CounterMetric::new(CommonMetricData {
+ name: "transformation".into(),
+ category: "local".into(),
+ send_in_pings: vec!["store1".into()],
+ ..Default::default()
+ });
+
+ counter.add(&glean, 2);
+ counter.add(&glean, i32::max_value());
+
+ assert_eq!(
+ i32::max_value(),
+ counter.test_get_value(&glean, "store1").unwrap()
+ );
+}
diff --git a/third_party/rust/glean-core/tests/custom_distribution.rs b/third_party/rust/glean-core/tests/custom_distribution.rs
new file mode 100644
index 0000000000..e3a27cb60c
--- /dev/null
+++ b/third_party/rust/glean-core/tests/custom_distribution.rs
@@ -0,0 +1,437 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+use glean_core::{CommonMetricData, Lifetime};
+
+// Tests ported from glean-ac
+
+mod linear {
+ use super::*;
+
+ #[test]
+ fn serializer_should_correctly_serialize_custom_distribution() {
+ let (mut tempdir, _) = tempdir();
+
+ {
+ let (glean, dir) = new_glean(Some(tempdir));
+ tempdir = dir;
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 100,
+ HistogramType::Linear,
+ );
+
+ metric.accumulate_samples_signed(&glean, vec![50]);
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ assert_eq!(snapshot.sum, 50);
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!(50),
+ snapshot["custom_distribution"]["telemetry.distribution"]["sum"]
+ );
+ }
+ }
+
+ #[test]
+ fn set_value_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 100,
+ HistogramType::Linear,
+ );
+
+ metric.accumulate_samples_signed(&glean, vec![50]);
+
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!(50),
+ snapshot["custom_distribution"]["telemetry.distribution"]["sum"]
+ );
+ assert_eq!(
+ json!(1),
+ snapshot["custom_distribution"]["telemetry.distribution"]["values"]["50"]
+ );
+ }
+ }
+
+ // SKIPPED from glean-ac: memory distributions must not accumulate negative values
+ // This test doesn't apply to Rust, because we're using unsigned integers.
+
+ #[test]
+ fn the_accumulate_samples_api_correctly_stores_memory_values() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 100,
+ HistogramType::Linear,
+ );
+
+ // Accumulate the samples. We intentionally do not report
+ // negative values to not trigger error reporting.
+ metric.accumulate_samples_signed(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum of samples.
+ assert_eq!(snapshot.sum, 6);
+
+ // We should get a sample in 3 buckets.
+ // These numbers are a bit magic, but they correspond to
+ // `hist.sample_to_bucket_minimum(i * kb)` for `i = 1..=3`.
+ assert_eq!(1, snapshot.values[&1]);
+ assert_eq!(1, snapshot.values[&2]);
+ assert_eq!(1, snapshot.values[&3]);
+
+ // No errors should be reported.
+ assert!(test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn the_accumulate_samples_api_correctly_handles_negative_values() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 100,
+ HistogramType::Linear,
+ );
+
+ // Accumulate the samples.
+ metric.accumulate_samples_signed(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum of samples.
+ assert_eq!(snapshot.sum, 6);
+
+ // We should get a sample in 3 buckets.
+ // These numbers are a bit magic, but they correspond to
+ // `hist.sample_to_bucket_minimum(i * kb)` for `i = 1..=3`.
+ assert_eq!(1, snapshot.values[&1]);
+ assert_eq!(1, snapshot.values[&2]);
+ assert_eq!(1, snapshot.values[&3]);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ );
+ }
+
+ #[test]
+ fn json_snapshotting_works() {
+ let (glean, _t) = new_glean(None);
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 100,
+ HistogramType::Linear,
+ );
+
+ metric.accumulate_samples_signed(&glean, vec![50]);
+
+ let snapshot = metric.test_get_value_as_json_string(&glean, "store1");
+ assert!(snapshot.is_some());
+ }
+}
+
+mod exponential {
+ use super::*;
+
+ #[test]
+ fn serializer_should_correctly_serialize_custom_distribution() {
+ let (mut tempdir, _) = tempdir();
+
+ {
+ let (glean, dir) = new_glean(Some(tempdir));
+ tempdir = dir;
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 10,
+ HistogramType::Exponential,
+ );
+
+ metric.accumulate_samples_signed(&glean, vec![50]);
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ assert_eq!(snapshot.sum, 50);
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!(50),
+ snapshot["custom_distribution"]["telemetry.distribution"]["sum"]
+ );
+ }
+ }
+
+ #[test]
+ fn set_value_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 10,
+ HistogramType::Exponential,
+ );
+
+ metric.accumulate_samples_signed(&glean, vec![50]);
+
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!(50),
+ snapshot["custom_distribution"]["telemetry.distribution"]["sum"]
+ );
+ assert_eq!(
+ json!(1),
+ snapshot["custom_distribution"]["telemetry.distribution"]["values"]["29"]
+ );
+ }
+ }
+
+ // SKIPPED from glean-ac: memory distributions must not accumulate negative values
+ // This test doesn't apply to Rust, because we're using unsigned integers.
+
+ #[test]
+ fn the_accumulate_samples_api_correctly_stores_memory_values() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 10,
+ HistogramType::Exponential,
+ );
+
+ // Accumulate the samples. We intentionally do not report
+ // negative values to not trigger error reporting.
+ metric.accumulate_samples_signed(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum of samples.
+ assert_eq!(snapshot.sum, 6);
+
+ // We should get a sample in 3 buckets.
+ // These numbers are a bit magic, but they correspond to
+ // `hist.sample_to_bucket_minimum(i * kb)` for `i = 1..=3`.
+ assert_eq!(1, snapshot.values[&1]);
+ assert_eq!(1, snapshot.values[&2]);
+ assert_eq!(1, snapshot.values[&3]);
+
+ // No errors should be reported.
+ assert!(test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ .is_err());
+ }
+
+ #[test]
+ fn the_accumulate_samples_api_correctly_handles_negative_values() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 10,
+ HistogramType::Exponential,
+ );
+
+ // Accumulate the samples.
+ metric.accumulate_samples_signed(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum of samples.
+ assert_eq!(snapshot.sum, 6);
+
+ // We should get a sample in 3 buckets.
+ // These numbers are a bit magic, but they correspond to
+ // `hist.sample_to_bucket_minimum(i * kb)` for `i = 1..=3`.
+ assert_eq!(1, snapshot.values[&1]);
+ assert_eq!(1, snapshot.values[&2]);
+ assert_eq!(1, snapshot.values[&3]);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ );
+ }
+
+ #[test]
+ fn json_snapshotting_works() {
+ let (glean, _t) = new_glean(None);
+ let metric = CustomDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ 1,
+ 100,
+ 10,
+ HistogramType::Exponential,
+ );
+
+ metric.accumulate_samples_signed(&glean, vec![50]);
+
+ let snapshot = metric.test_get_value_as_json_string(&glean, "store1");
+ assert!(snapshot.is_some());
+ }
+}
diff --git a/third_party/rust/glean-core/tests/datetime.rs b/third_party/rust/glean-core/tests/datetime.rs
new file mode 100644
index 0000000000..b67d01c3a3
--- /dev/null
+++ b/third_party/rust/glean-core/tests/datetime.rs
@@ -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 https://mozilla.org/MPL/2.0/.
+
+mod common;
+use crate::common::*;
+
+use chrono::prelude::*;
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{CommonMetricData, Lifetime};
+
+// SKIPPED from glean-ac: datetime deserializer should correctly parse integers
+// This test doesn't really apply to rkv
+
+#[test]
+fn datetime_serializer_should_correctly_serialize_datetime() {
+ let expected_value = "1983-04-13T12:09+00:00";
+ let (mut tempdir, _) = tempdir();
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let metric = DatetimeMetric::new(
+ CommonMetricData {
+ name: "datetime_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ },
+ TimeUnit::Minute,
+ );
+
+ // `1983-04-13T12:09:14.274+00:00` will be truncated to Minute resolution.
+ let dt = FixedOffset::east(0)
+ .ymd(1983, 4, 13)
+ .and_hms_milli(12, 9, 14, 274);
+ metric.set(&glean, Some(dt));
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"datetime": {"telemetry.datetime_metric": expected_value}}),
+ snapshot
+ );
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"datetime": {"telemetry.datetime_metric": expected_value}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn set_value_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = DatetimeMetric::new(
+ CommonMetricData {
+ name: "datetime_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ // `1983-04-13T12:09:14.274+00:00` will be truncated to Minute resolution.
+ let dt = FixedOffset::east(0)
+ .ymd(1983, 4, 13)
+ .and_hms_nano(12, 9, 14, 1_560_274);
+ metric.set(&glean, Some(dt));
+
+ for store_name in store_names {
+ assert_eq!(
+ "1983-04-13T12:09:14.001560274+00:00",
+ metric
+ .test_get_value_as_string(&glean, &store_name)
+ .unwrap()
+ );
+ }
+}
+
+// SKIPPED from glean-ac: getSnapshot() returns null if nothing is recorded in the store
+// This test doesn't really apply to rkv
+
+// SKIPPED from glean-ac: getSnapshot() correctly clears the stores
+// This test doesn't really apply to rkv
+
+#[test]
+fn test_that_truncation_works() {
+ let (glean, _t) = new_glean(None);
+
+ // `1985-07-03T12:09:14.000560274+01:00`
+ let high_res_datetime = FixedOffset::east(3600)
+ .ymd(1985, 7, 3)
+ .and_hms_nano(12, 9, 14, 1_560_274);
+ let store_name = "store1";
+
+ // Create an helper struct for defining the truncation cases.
+ struct TestCase {
+ case_name: &'static str,
+ desired_resolution: TimeUnit,
+ expected_result: &'static str,
+ }
+
+ // Define the single test cases.
+ let test_cases = vec![
+ TestCase {
+ case_name: "nano",
+ desired_resolution: TimeUnit::Nanosecond,
+ expected_result: "1985-07-03T12:09:14.001560274+01:00",
+ },
+ TestCase {
+ case_name: "micro",
+ desired_resolution: TimeUnit::Microsecond,
+ expected_result: "1985-07-03T12:09:14.001560+01:00",
+ },
+ TestCase {
+ case_name: "milli",
+ desired_resolution: TimeUnit::Millisecond,
+ expected_result: "1985-07-03T12:09:14.001+01:00",
+ },
+ TestCase {
+ case_name: "second",
+ desired_resolution: TimeUnit::Second,
+ expected_result: "1985-07-03T12:09:14+01:00",
+ },
+ TestCase {
+ case_name: "minute",
+ desired_resolution: TimeUnit::Minute,
+ expected_result: "1985-07-03T12:09+01:00",
+ },
+ TestCase {
+ case_name: "hour",
+ desired_resolution: TimeUnit::Hour,
+ expected_result: "1985-07-03T12+01:00",
+ },
+ TestCase {
+ case_name: "day",
+ desired_resolution: TimeUnit::Day,
+ expected_result: "1985-07-03+01:00",
+ },
+ ];
+
+ // Execute them all.
+ for t in test_cases {
+ let metric = DatetimeMetric::new(
+ CommonMetricData {
+ name: format!("datetime_metric_{}", t.case_name),
+ category: "telemetry".into(),
+ send_in_pings: vec![store_name.into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ },
+ t.desired_resolution,
+ );
+ metric.set(&glean, Some(high_res_datetime));
+
+ assert_eq!(
+ t.expected_result,
+ metric
+ .test_get_value_as_string(&glean, &store_name)
+ .unwrap()
+ );
+ }
+}
diff --git a/third_party/rust/glean-core/tests/event.rs b/third_party/rust/glean-core/tests/event.rs
new file mode 100644
index 0000000000..14cf0d0c8c
--- /dev/null
+++ b/third_party/rust/glean-core/tests/event.rs
@@ -0,0 +1,290 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use std::collections::HashMap;
+use std::fs;
+
+use glean_core::metrics::*;
+use glean_core::{CommonMetricData, Lifetime};
+
+#[test]
+fn record_properly_records_without_optional_arguments() {
+ let store_names = vec!["store1".into(), "store2".into()];
+
+ let (glean, _t) = new_glean(None);
+
+ let metric = EventMetric::new(
+ CommonMetricData {
+ name: "test_event_no_optional".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec![],
+ );
+
+ metric.record(&glean, 1000, None);
+
+ for store_name in store_names {
+ let events = metric.test_get_value(&glean, &store_name).unwrap();
+ assert_eq!(1, events.len());
+ assert_eq!("telemetry", events[0].category);
+ assert_eq!("test_event_no_optional", events[0].name);
+ assert!(events[0].extra.is_none());
+ }
+}
+
+#[test]
+fn record_properly_records_with_optional_arguments() {
+ let (glean, _t) = new_glean(None);
+
+ let store_names = vec!["store1".into(), "store2".into()];
+
+ let metric = EventMetric::new(
+ CommonMetricData {
+ name: "test_event_no_optional".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec!["key1".into(), "key2".into()],
+ );
+
+ let extra: HashMap<i32, String> = [(0, "value1".into()), (1, "value2".into())]
+ .iter()
+ .cloned()
+ .collect();
+
+ metric.record(&glean, 1000, extra);
+
+ for store_name in store_names {
+ let events = metric.test_get_value(&glean, &store_name).unwrap();
+ let event = events[0].clone();
+ assert_eq!(1, events.len());
+ assert_eq!("telemetry", event.category);
+ assert_eq!("test_event_no_optional", event.name);
+ let extra = event.extra.unwrap();
+ assert_eq!(2, extra.len());
+ assert_eq!("value1", extra["key1"]);
+ assert_eq!("value2", extra["key2"]);
+ }
+}
+
+// SKIPPED record() computes the correct time between events
+// Timing is now handled in the language-specific part.
+
+#[test]
+fn snapshot_returns_none_if_nothing_is_recorded_in_the_store() {
+ let (glean, _t) = new_glean(None);
+
+ assert!(glean
+ .event_storage()
+ .snapshot_as_json("store1", false)
+ .is_none())
+}
+
+#[test]
+fn snapshot_correctly_clears_the_stores() {
+ let (glean, _t) = new_glean(None);
+
+ let store_names = vec!["store1".into(), "store2".into()];
+
+ let metric = EventMetric::new(
+ CommonMetricData {
+ name: "test_event_clear".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec![],
+ );
+
+ metric.record(&glean, 1000, None);
+
+ let snapshot = glean.event_storage().snapshot_as_json("store1", true);
+ assert!(snapshot.is_some());
+
+ assert!(glean
+ .event_storage()
+ .snapshot_as_json("store1", false)
+ .is_none());
+
+ let files: Vec<fs::DirEntry> = fs::read_dir(&glean.event_storage().path)
+ .unwrap()
+ .filter_map(|x| x.ok())
+ .collect();
+ assert_eq!(1, files.len());
+ assert_eq!("store2", files[0].file_name());
+
+ let snapshot2 = glean.event_storage().snapshot_as_json("store2", false);
+ for s in vec![snapshot, snapshot2] {
+ assert!(s.is_some());
+ let s = s.unwrap();
+ assert_eq!(1, s.as_array().unwrap().len());
+ assert_eq!("telemetry", s[0]["category"]);
+ assert_eq!("test_event_clear", s[0]["name"]);
+ println!("{:?}", s[0].get("extra"));
+ assert!(s[0].get("extra").is_none());
+ }
+}
+
+// SKIPPED: Events are serialized in the correct JSON format (no extra)
+// SKIPPED: Events are serialized in the correct JSON format (with extra)
+// This test won't work as-is since Rust doesn't maintain the insertion order in
+// a JSON object, therefore you can't check the JSON output directly against a
+// string. This check is redundant with other tests, anyway, and checking against
+// the schema is much more useful.
+
+#[test]
+fn test_sending_of_event_ping_when_it_fills_up() {
+ let (mut glean, _t) = new_glean(None);
+
+ let store_names: Vec<String> = vec!["events".into()];
+
+ for store_name in &store_names {
+ glean.register_ping_type(&PingType::new(store_name.clone(), true, false, vec![]));
+ }
+
+ let click = EventMetric::new(
+ CommonMetricData {
+ name: "click".into(),
+ category: "ui".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec!["test_event_number".into()],
+ );
+
+ // We send 510 events. We expect to get the first 500 in the ping and 10
+ // remaining afterward
+ for i in 0..510 {
+ let mut extra: HashMap<i32, String> = HashMap::new();
+ extra.insert(0, i.to_string());
+ click.record(&glean, i, extra);
+ }
+
+ assert_eq!(10, click.test_get_value(&glean, "events").unwrap().len());
+
+ let (url, json, _) = &get_queued_pings(glean.get_data_path()).unwrap()[0];
+ assert!(url.starts_with(format!("/submit/{}/events/", glean.get_application_id()).as_str()));
+ assert_eq!(500, json["events"].as_array().unwrap().len());
+ assert_eq!(
+ "max_capacity",
+ json["ping_info"].as_object().unwrap()["reason"]
+ .as_str()
+ .unwrap()
+ );
+
+ for i in 0..500 {
+ let event = &json["events"].as_array().unwrap()[i];
+ assert_eq!(i.to_string(), event["extra"]["test_event_number"]);
+ }
+
+ let snapshot = glean
+ .event_storage()
+ .snapshot_as_json("events", false)
+ .unwrap();
+ assert_eq!(10, snapshot.as_array().unwrap().len());
+ for i in 0..10 {
+ let event = &snapshot.as_array().unwrap()[i];
+ assert_eq!((i + 500).to_string(), event["extra"]["test_event_number"]);
+ }
+}
+
+#[test]
+fn extra_keys_must_be_recorded_and_truncated_if_needed() {
+ let (glean, _t) = new_glean(None);
+
+ let store_names: Vec<String> = vec!["store1".into()];
+
+ let test_event = EventMetric::new(
+ CommonMetricData {
+ name: "testEvent".into(),
+ category: "ui".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec!["extra1".into(), "truncatedExtra".into()],
+ );
+
+ let test_value = "LeanGleanByFrank";
+ let mut extra: HashMap<i32, String> = HashMap::new();
+ extra.insert(0, test_value.to_string());
+ extra.insert(1, test_value.to_string().repeat(10));
+
+ test_event.record(&glean, 0, extra);
+
+ let snapshot = glean
+ .event_storage()
+ .snapshot_as_json("store1", false)
+ .unwrap();
+ assert_eq!(1, snapshot.as_array().unwrap().len());
+ let event = &snapshot.as_array().unwrap()[0];
+ assert_eq!("ui", event["category"]);
+ assert_eq!("testEvent", event["name"]);
+ assert_eq!(2, event["extra"].as_object().unwrap().len());
+ assert_eq!(test_value, event["extra"]["extra1"]);
+ assert_eq!(
+ test_value.to_string().repeat(10)[0..100],
+ event["extra"]["truncatedExtra"]
+ );
+}
+
+#[test]
+fn snapshot_sorts_the_timestamps() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = EventMetric::new(
+ CommonMetricData {
+ name: "test_event_clear".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec![],
+ );
+
+ metric.record(&glean, 1000, None);
+ metric.record(&glean, 100, None);
+ metric.record(&glean, 10000, None);
+
+ let snapshot = glean
+ .event_storage()
+ .snapshot_as_json("store1", true)
+ .unwrap();
+
+ assert_eq!(
+ 0,
+ snapshot.as_array().unwrap()[0]["timestamp"]
+ .as_i64()
+ .unwrap()
+ );
+ assert_eq!(
+ 900,
+ snapshot.as_array().unwrap()[1]["timestamp"]
+ .as_i64()
+ .unwrap()
+ );
+ assert_eq!(
+ 9900,
+ snapshot.as_array().unwrap()[2]["timestamp"]
+ .as_i64()
+ .unwrap()
+ );
+}
diff --git a/third_party/rust/glean-core/tests/jwe.rs b/third_party/rust/glean-core/tests/jwe.rs
new file mode 100644
index 0000000000..d6ddef4872
--- /dev/null
+++ b/third_party/rust/glean-core/tests/jwe.rs
@@ -0,0 +1,113 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{CommonMetricData, Lifetime};
+
+const HEADER: &str = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ";
+const KEY: &str = "OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg";
+const INIT_VECTOR: &str = "48V1_ALb6US04U3b";
+const CIPHER_TEXT: &str =
+ "5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A";
+const AUTH_TAG: &str = "XFBoMYUZodetZdvTiFvSkQ";
+const JWE: &str = "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZHQ00ifQ.OKOawDo13gRp2ojaHV7LFpZcgV7T6DVZKTyKOMTYUmKoTCVJRgckCL9kiMT03JGeipsEdY3mx_etLbbWSrFr05kLzcSr4qKAq7YN7e9jwQRb23nfa6c9d-StnImGyFDbSv04uVuxIp5Zms1gNxKKK2Da14B8S4rzVRltdYwam_lDp5XnZAYpQdb76FdIKLaVmqgfwX7XWRxv2322i-vDxRfqNzo_tETKzpVLzfiwQyeyPGLBIO56YJ7eObdv0je81860ppamavo35UgoRdbYaBcoh9QcfylQr66oc6vFWXRcZ_ZT2LawVCWTIy3brGPi6UklfCpIMfIjf7iGdXKHzg.48V1_ALb6US04U3b.5eym8TW_c8SuK0ltJ3rpYIzOeDQz7TALvtu6UG9oMo4vpzs9tX_EFShS8iB7j6jiSdiwkIr3ajwQzaBtQD_A.XFBoMYUZodetZdvTiFvSkQ";
+
+#[test]
+fn jwe_metric_is_generated_and_stored() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = JweMetric::new(CommonMetricData {
+ name: "jwe_metric".into(),
+ category: "local".into(),
+ send_in_pings: vec!["core".into()],
+ ..Default::default()
+ });
+
+ metric.set_with_compact_representation(&glean, JWE);
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "core", false)
+ .unwrap();
+
+ assert_eq!(
+ json!({"jwe": {"local.jwe_metric": metric.test_get_value(&glean, "core") }}),
+ snapshot
+ );
+}
+
+#[test]
+fn set_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = JweMetric::new(CommonMetricData {
+ name: "jwe_metric".into(),
+ category: "local".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set_with_compact_representation(&glean, JWE);
+
+ // Check that the data was correctly set in each store.
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, false)
+ .unwrap();
+
+ assert_eq!(
+ json!({"jwe": {"local.jwe_metric": metric.test_get_value(&glean, &store_name) }}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn get_test_value_returns_the_period_delimited_string() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = JweMetric::new(CommonMetricData {
+ name: "jwe_metric".into(),
+ category: "local".into(),
+ send_in_pings: vec!["core".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set_with_compact_representation(&glean, JWE);
+
+ assert_eq!(metric.test_get_value(&glean, "core").unwrap(), JWE);
+}
+
+#[test]
+fn get_test_value_as_json_string_returns_the_expected_repr() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = JweMetric::new(CommonMetricData {
+ name: "jwe_metric".into(),
+ category: "local".into(),
+ send_in_pings: vec!["core".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set_with_compact_representation(&glean, JWE);
+
+ let expected_json = format!("{{\"header\":\"{}\",\"key\":\"{}\",\"init_vector\":\"{}\",\"cipher_text\":\"{}\",\"auth_tag\":\"{}\"}}", HEADER, KEY, INIT_VECTOR, CIPHER_TEXT, AUTH_TAG);
+ assert_eq!(
+ metric
+ .test_get_value_as_json_string(&glean, "core")
+ .unwrap(),
+ expected_json
+ );
+}
diff --git a/third_party/rust/glean-core/tests/labeled.rs b/third_party/rust/glean-core/tests/labeled.rs
new file mode 100644
index 0000000000..386a86d521
--- /dev/null
+++ b/third_party/rust/glean-core/tests/labeled.rs
@@ -0,0 +1,395 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{CommonMetricData, Lifetime};
+
+#[test]
+fn can_create_labeled_counter_metric() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ CounterMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ Some(vec!["label1".into()]),
+ );
+
+ let metric = labeled.get("label1");
+ metric.add(&glean, 1);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_counter": {
+ "telemetry.labeled_metric": { "label1": 1 }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn can_create_labeled_string_metric() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ StringMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ Some(vec!["label1".into()]),
+ );
+
+ let metric = labeled.get("label1");
+ metric.set(&glean, "text");
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_string": {
+ "telemetry.labeled_metric": { "label1": "text" }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn can_create_labeled_bool_metric() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ BooleanMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ Some(vec!["label1".into()]),
+ );
+
+ let metric = labeled.get("label1");
+ metric.set(&glean, true);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_boolean": {
+ "telemetry.labeled_metric": { "label1": true }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn can_use_multiple_labels() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ CounterMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ None,
+ );
+
+ let metric = labeled.get("label1");
+ metric.add(&glean, 1);
+
+ let metric = labeled.get("label2");
+ metric.add(&glean, 2);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_counter": {
+ "telemetry.labeled_metric": {
+ "label1": 1,
+ "label2": 2,
+ }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn labels_are_checked_against_static_list() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ CounterMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ Some(vec!["label1".into(), "label2".into()]),
+ );
+
+ let metric = labeled.get("label1");
+ metric.add(&glean, 1);
+
+ let metric = labeled.get("label2");
+ metric.add(&glean, 2);
+
+ // All non-registed labels get mapped to the `other` label
+ let metric = labeled.get("label3");
+ metric.add(&glean, 3);
+ let metric = labeled.get("label4");
+ metric.add(&glean, 4);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_counter": {
+ "telemetry.labeled_metric": {
+ "label1": 1,
+ "label2": 2,
+ "__other__": 7,
+ }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn dynamic_labels_too_long() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ CounterMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ None,
+ );
+
+ let metric = labeled.get("this_string_has_more_than_thirty_characters");
+ metric.add(&glean, 1);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_counter": {
+ "glean.error.invalid_label": { "telemetry.labeled_metric": 1 },
+ "telemetry.labeled_metric": {
+ "__other__": 1,
+ }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn dynamic_labels_regex_mismatch() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ CounterMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ None,
+ );
+
+ let labels_not_validating = vec![
+ "notSnakeCase",
+ "",
+ "with/slash",
+ "1.not_fine",
+ "this.$isnotfine",
+ "-.not_fine",
+ "this.is_not_fine.2",
+ ];
+ let num_non_validating = labels_not_validating.len();
+
+ for label in &labels_not_validating {
+ labeled.get(label).add(&glean, 1);
+ }
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_counter": {
+ "glean.error.invalid_label": { "telemetry.labeled_metric": num_non_validating },
+ "telemetry.labeled_metric": {
+ "__other__": num_non_validating,
+ }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn dynamic_labels_regex_allowed() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledMetric::new(
+ CounterMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ None,
+ );
+
+ let labels_validating = vec![
+ "this.is.fine",
+ "this_is_fine_too",
+ "this.is_still_fine",
+ "thisisfine",
+ "_.is_fine",
+ "this.is-fine",
+ "this-is-fine",
+ ];
+
+ for label in &labels_validating {
+ labeled.get(label).add(&glean, 1);
+ }
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({
+ "labeled_counter": {
+ "telemetry.labeled_metric": {
+ "this.is.fine": 1,
+ "this_is_fine_too": 1,
+ "this.is_still_fine": 1,
+ "thisisfine": 1,
+ "_.is_fine": 1,
+ "this.is-fine": 1,
+ "this-is-fine": 1
+ }
+ }
+ }),
+ snapshot
+ );
+}
+
+#[test]
+fn seen_labels_get_reloaded_from_disk() {
+ let (mut tempdir, _) = tempdir();
+
+ let (glean, dir) = new_glean(Some(tempdir));
+ tempdir = dir;
+
+ let labeled = LabeledMetric::new(
+ CounterMetric::new(CommonMetricData {
+ name: "labeled_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ }),
+ None,
+ );
+
+ // Store some data into labeled metrics
+ {
+ // Set the maximum number of labels
+ for i in 1..=16 {
+ let label = format!("label{}", i);
+ labeled.get(&label).add(&glean, i);
+ }
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", false)
+ .unwrap();
+
+ // Check that the data is there
+ for i in 1..=16 {
+ let label = format!("label{}", i);
+ assert_eq!(
+ i,
+ snapshot["labeled_counter"]["telemetry.labeled_metric"][&label]
+ );
+ }
+
+ drop(glean);
+ }
+
+ // Force a reload
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+
+ // Try to store another label
+ labeled.get("new_label").add(&glean, 40);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", false)
+ .unwrap();
+
+ // Check that the old data is still there
+ for i in 1..=16 {
+ let label = format!("label{}", i);
+ assert_eq!(
+ i,
+ snapshot["labeled_counter"]["telemetry.labeled_metric"][&label]
+ );
+ }
+
+ // The new label lands in the __other__ bucket, due to too many labels
+ assert_eq!(
+ 40,
+ snapshot["labeled_counter"]["telemetry.labeled_metric"]["__other__"]
+ );
+ }
+}
diff --git a/third_party/rust/glean-core/tests/memory_distribution.rs b/third_party/rust/glean-core/tests/memory_distribution.rs
new file mode 100644
index 0000000000..8c7c620fa8
--- /dev/null
+++ b/third_party/rust/glean-core/tests/memory_distribution.rs
@@ -0,0 +1,193 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+use glean_core::{CommonMetricData, Lifetime};
+
+// Tests ported from glean-ac
+
+#[test]
+fn serializer_should_correctly_serialize_memory_distribution() {
+ let (mut tempdir, _) = tempdir();
+
+ let memory_unit = MemoryUnit::Kilobyte;
+ let kb = 1024;
+
+ {
+ let (glean, dir) = new_glean(Some(tempdir));
+ tempdir = dir;
+
+ let metric = MemoryDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ memory_unit,
+ );
+
+ metric.accumulate(&glean, 100_000);
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ assert_eq!(snapshot.sum, 100_000 * kb);
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!(100_000 * kb),
+ snapshot["memory_distribution"]["telemetry.distribution"]["sum"]
+ );
+ }
+}
+
+#[test]
+fn set_value_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = MemoryDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ MemoryUnit::Byte,
+ );
+
+ metric.accumulate(&glean, 100_000);
+
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!(100_000),
+ snapshot["memory_distribution"]["telemetry.distribution"]["sum"]
+ );
+ assert_eq!(
+ json!(1),
+ snapshot["memory_distribution"]["telemetry.distribution"]["values"]["96785"]
+ );
+ }
+}
+
+// SKIPPED from glean-ac: memory distributions must not accumulate negative values
+// This test doesn't apply to Rust, because we're using unsigned integers.
+
+#[test]
+fn the_accumulate_samples_api_correctly_stores_memory_values() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = MemoryDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ MemoryUnit::Kilobyte,
+ );
+
+ // Accumulate the samples. We intentionally do not report
+ // negative values to not trigger error reporting.
+ metric.accumulate_samples_signed(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ let kb = 1024;
+
+ // Check that we got the right sum of samples.
+ assert_eq!(snapshot.sum, 6 * kb);
+
+ // We should get a sample in 3 buckets.
+ // These numbers are a bit magic, but they correspond to
+ // `hist.sample_to_bucket_minimum(i * kb)` for `i = 1..=3`.
+ assert_eq!(1, snapshot.values[&1023]);
+ assert_eq!(1, snapshot.values[&2047]);
+ assert_eq!(1, snapshot.values[&3024]);
+
+ // No errors should be reported.
+ assert!(test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ .is_err());
+}
+
+#[test]
+fn the_accumulate_samples_api_correctly_handles_negative_values() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = MemoryDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ MemoryUnit::Kilobyte,
+ );
+
+ // Accumulate the samples.
+ metric.accumulate_samples_signed(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ let kb = 1024;
+
+ // Check that we got the right sum of samples.
+ assert_eq!(snapshot.sum, 6 * kb);
+
+ // We should get a sample in 3 buckets.
+ // These numbers are a bit magic, but they correspond to
+ // `hist.sample_to_bucket_minimum(i * kb)` for `i = 1..=3`.
+ assert_eq!(1, snapshot.values[&1023]);
+ assert_eq!(1, snapshot.values[&2047]);
+ assert_eq!(1, snapshot.values[&3024]);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ );
+}
diff --git a/third_party/rust/glean-core/tests/metrics.rs b/third_party/rust/glean-core/tests/metrics.rs
new file mode 100644
index 0000000000..3e906fe90e
--- /dev/null
+++ b/third_party/rust/glean-core/tests/metrics.rs
@@ -0,0 +1,37 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use glean_core::metrics::*;
+use glean_core::CommonMetricData;
+
+#[test]
+fn stores_strings() {
+ let (glean, _t) = new_glean(None);
+ let metric = StringMetric::new(CommonMetricData::new("local", "string", "baseline"));
+
+ assert_eq!(None, metric.test_get_value(&glean, "baseline"));
+
+ metric.set(&glean, "telemetry");
+ assert_eq!(
+ "telemetry",
+ metric.test_get_value(&glean, "baseline").unwrap()
+ );
+}
+
+#[test]
+fn stores_counters() {
+ let (glean, _t) = new_glean(None);
+ let metric = CounterMetric::new(CommonMetricData::new("local", "counter", "baseline"));
+
+ assert_eq!(None, metric.test_get_value(&glean, "baseline"));
+
+ metric.add(&glean, 1);
+ assert_eq!(1, metric.test_get_value(&glean, "baseline").unwrap());
+
+ metric.add(&glean, 2);
+ assert_eq!(3, metric.test_get_value(&glean, "baseline").unwrap());
+}
diff --git a/third_party/rust/glean-core/tests/ping.rs b/third_party/rust/glean-core/tests/ping.rs
new file mode 100644
index 0000000000..765297aea5
--- /dev/null
+++ b/third_party/rust/glean-core/tests/ping.rs
@@ -0,0 +1,103 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use glean_core::metrics::*;
+use glean_core::CommonMetricData;
+
+#[test]
+fn write_ping_to_disk() {
+ let (mut glean, _temp) = new_glean(None);
+
+ let ping = PingType::new("metrics", true, false, vec![]);
+ glean.register_ping_type(&ping);
+
+ // We need to store a metric as an empty ping is not stored.
+ let counter = CounterMetric::new(CommonMetricData {
+ name: "counter".into(),
+ category: "local".into(),
+ send_in_pings: vec!["metrics".into()],
+ ..Default::default()
+ });
+ counter.add(&glean, 1);
+
+ assert!(ping.submit(&glean, None).unwrap());
+
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn disabling_upload_clears_pending_pings() {
+ let (mut glean, _) = new_glean(None);
+
+ let ping = PingType::new("metrics", true, false, vec![]);
+ glean.register_ping_type(&ping);
+
+ // We need to store a metric as an empty ping is not stored.
+ let counter = CounterMetric::new(CommonMetricData {
+ name: "counter".into(),
+ category: "local".into(),
+ send_in_pings: vec!["metrics".into()],
+ ..Default::default()
+ });
+
+ counter.add(&glean, 1);
+ assert!(ping.submit(&glean, None).unwrap());
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+ // At this point no deletion_request ping should exist
+ // (that is: it's directory should not exist at all)
+ assert!(get_deletion_pings(glean.get_data_path()).is_err());
+
+ glean.set_upload_enabled(false);
+ assert_eq!(0, get_queued_pings(glean.get_data_path()).unwrap().len());
+ // Disabling upload generates a deletion ping
+ assert_eq!(1, get_deletion_pings(glean.get_data_path()).unwrap().len());
+
+ glean.set_upload_enabled(true);
+ assert_eq!(0, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ counter.add(&glean, 1);
+ assert!(ping.submit(&glean, None).unwrap());
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn deletion_request_only_when_toggled_from_on_to_off() {
+ let (mut glean, _) = new_glean(None);
+
+ // Disabling upload generates a deletion ping
+ glean.set_upload_enabled(false);
+ assert_eq!(1, get_deletion_pings(glean.get_data_path()).unwrap().len());
+
+ // Re-setting it to `false` should not generate an additional ping.
+ // As we didn't clear the pending ping, that's the only one that sticks around.
+ glean.set_upload_enabled(false);
+ assert_eq!(1, get_deletion_pings(glean.get_data_path()).unwrap().len());
+
+ // Toggling back to true won't generate a ping either.
+ glean.set_upload_enabled(true);
+ assert_eq!(1, get_deletion_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn empty_pings_with_flag_are_sent() {
+ let (mut glean, _) = new_glean(None);
+
+ let ping1 = PingType::new("custom-ping1", true, true, vec![]);
+ glean.register_ping_type(&ping1);
+ let ping2 = PingType::new("custom-ping2", true, false, vec![]);
+ glean.register_ping_type(&ping2);
+
+ // No data is stored in either of the custom pings
+
+ // Sending this should succeed.
+ assert_eq!(true, ping1.submit(&glean, None).unwrap());
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ // Sending this should fail.
+ assert_eq!(false, ping2.submit(&glean, None).unwrap());
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
diff --git a/third_party/rust/glean-core/tests/ping_maker.rs b/third_party/rust/glean-core/tests/ping_maker.rs
new file mode 100644
index 0000000000..436e38e711
--- /dev/null
+++ b/third_party/rust/glean-core/tests/ping_maker.rs
@@ -0,0 +1,210 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use glean_core::metrics::*;
+use glean_core::ping::PingMaker;
+use glean_core::{CommonMetricData, Glean, Lifetime};
+
+fn set_up_basic_ping() -> (Glean, PingMaker, PingType, tempfile::TempDir) {
+ let (tempdir, _) = tempdir();
+ let (mut glean, t) = new_glean(Some(tempdir));
+ let ping_maker = PingMaker::new();
+ let ping_type = PingType::new("store1", true, false, vec![]);
+ glean.register_ping_type(&ping_type);
+
+ // Record something, so the ping will have data
+ let metric = BooleanMetric::new(CommonMetricData {
+ name: "boolean_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+ metric.set(&glean, true);
+
+ (glean, ping_maker, ping_type, t)
+}
+
+#[test]
+fn ping_info_must_contain_a_nonempty_start_and_end_time() {
+ let (glean, ping_maker, ping_type, _t) = set_up_basic_ping();
+
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let ping_info = content["ping_info"].as_object().unwrap();
+
+ let start_time_str = ping_info["start_time"].as_str().unwrap();
+ let start_time_date = iso8601_to_chrono(&iso8601::datetime(start_time_str).unwrap());
+
+ let end_time_str = ping_info["end_time"].as_str().unwrap();
+ let end_time_date = iso8601_to_chrono(&iso8601::datetime(end_time_str).unwrap());
+
+ assert!(start_time_date <= end_time_date);
+}
+
+#[test]
+fn get_ping_info_must_report_all_the_required_fields() {
+ let (glean, ping_maker, ping_type, _t) = set_up_basic_ping();
+
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let ping_info = content["ping_info"].as_object().unwrap();
+
+ assert!(ping_info.get("start_time").is_some());
+ assert!(ping_info.get("end_time").is_some());
+ assert!(ping_info.get("seq").is_some());
+}
+
+#[test]
+fn get_client_info_must_report_all_the_available_data() {
+ let (glean, ping_maker, ping_type, _t) = set_up_basic_ping();
+
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let client_info = content["client_info"].as_object().unwrap();
+
+ client_info["telemetry_sdk_build"].as_str().unwrap();
+}
+
+// SKIPPED from glean-ac: collect() must report a valid ping with the data from the engines
+// This test doesn't really make sense with rkv
+
+#[test]
+fn collect_must_report_none_when_no_data_is_stored() {
+ // NOTE: This is a behavior change from glean-ac which returned an empty
+ // string in this case. As this is an implementation detail and not part of
+ // the public API, it's safe to change this.
+
+ let (mut glean, ping_maker, ping_type, _t) = set_up_basic_ping();
+
+ let unknown_ping_type = PingType::new("unknown", true, false, vec![]);
+ glean.register_ping_type(&ping_type);
+
+ assert!(ping_maker
+ .collect(&glean, &unknown_ping_type, None)
+ .is_none());
+}
+
+#[test]
+fn seq_number_must_be_sequential() {
+ let (glean, ping_maker, _ping_type, _t) = set_up_basic_ping();
+
+ let metric = BooleanMetric::new(CommonMetricData {
+ name: "boolean_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store2".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+ metric.set(&glean, true);
+
+ for i in 0..=1 {
+ for ping_name in ["store1", "store2"].iter() {
+ let ping_type = PingType::new(*ping_name, true, false, vec![]);
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let seq_num = content["ping_info"]["seq"].as_i64().unwrap();
+ // Ensure sequence numbers in different stores are independent of
+ // each other
+ assert_eq!(i, seq_num);
+ }
+ }
+
+ // Test that ping sequence numbers increase independently.
+ {
+ let ping_type = PingType::new("store1", true, false, vec![]);
+
+ // 3rd ping of store1
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let seq_num = content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(2, seq_num);
+
+ // 4th ping of store1
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let seq_num = content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(3, seq_num);
+ }
+
+ {
+ let ping_type = PingType::new("store2", true, false, vec![]);
+
+ // 3rd ping of store2
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let seq_num = content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(2, seq_num);
+ }
+
+ {
+ let ping_type = PingType::new("store1", true, false, vec![]);
+
+ // 5th ping of store1
+ let content = ping_maker.collect(&glean, &ping_type, None).unwrap();
+ let seq_num = content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(4, seq_num);
+ }
+}
+
+#[test]
+fn clear_pending_pings() {
+ let (mut glean, _) = new_glean(None);
+ let ping_maker = PingMaker::new();
+ let ping_type = PingType::new("store1", true, false, vec![]);
+ glean.register_ping_type(&ping_type);
+
+ // Record something, so the ping will have data
+ let metric = BooleanMetric::new(CommonMetricData {
+ name: "boolean_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+ metric.set(&glean, true);
+
+ assert!(glean.submit_ping(&ping_type, None).is_ok());
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ assert!(ping_maker
+ .clear_pending_pings(glean.get_data_path())
+ .is_ok());
+ assert_eq!(0, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn no_pings_submitted_if_upload_disabled() {
+ // Regression test, bug 1603571
+
+ let (mut glean, _) = new_glean(None);
+ let ping_type = PingType::new("store1", true, true, vec![]);
+ glean.register_ping_type(&ping_type);
+
+ assert!(glean.submit_ping(&ping_type, None).is_ok());
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ // Disable upload, then try to sumbit
+ glean.set_upload_enabled(false);
+
+ assert!(glean.submit_ping(&ping_type, None).is_ok());
+ assert_eq!(0, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ // Test again through the direct call
+ assert!(ping_type.submit(&glean, None).is_ok());
+ assert_eq!(0, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn metadata_is_correctly_added_when_necessary() {
+ let (mut glean, _) = new_glean(None);
+ glean.set_debug_view_tag("valid-tag");
+ let ping_type = PingType::new("store1", true, true, vec![]);
+ glean.register_ping_type(&ping_type);
+
+ assert!(glean.submit_ping(&ping_type, None).is_ok());
+
+ let (_, _, metadata) = &get_queued_pings(glean.get_data_path()).unwrap()[0];
+ let headers = metadata.as_ref().unwrap().get("headers").unwrap();
+ assert_eq!(headers.get("X-Debug-ID").unwrap(), "valid-tag");
+}
diff --git a/third_party/rust/glean-core/tests/quantity.rs b/third_party/rust/glean-core/tests/quantity.rs
new file mode 100644
index 0000000000..644281521f
--- /dev/null
+++ b/third_party/rust/glean-core/tests/quantity.rs
@@ -0,0 +1,118 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+use glean_core::{CommonMetricData, Lifetime};
+
+// Tests ported from glean-ac
+
+// SKIPPED from glean-ac: quantity deserializer should correctly parse integers
+// This test doesn't really apply to rkv
+
+#[test]
+fn quantity_serializer_should_correctly_serialize_quantities() {
+ let (mut tempdir, _) = tempdir();
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let metric = QuantityMetric::new(CommonMetricData {
+ name: "quantity_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+
+ metric.set(&glean, 1);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"quantity": {"telemetry.quantity_metric": 1}}),
+ snapshot
+ );
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"quantity": {"telemetry.quantity_metric": 1}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn set_value_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = QuantityMetric::new(CommonMetricData {
+ name: "quantity_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set(&glean, 1);
+
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!({"quantity": {"telemetry.quantity_metric": 1}}),
+ snapshot
+ );
+ }
+}
+
+// SKIPPED from glean-ac: quantities are serialized in the correct JSON format
+// Completely redundant with other tests.
+
+#[test]
+fn quantities_must_not_set_when_passed_negative() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = QuantityMetric::new(CommonMetricData {
+ name: "quantity_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Application,
+ ..Default::default()
+ });
+
+ // Attempt to set the quantity with negative
+ metric.set(&glean, -1);
+ // Check that nothing was recorded
+ assert!(metric.test_get_value(&glean, "store1").is_none());
+
+ // Make sure that the errors have been recorded
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue, None)
+ );
+}
diff --git a/third_party/rust/glean-core/tests/storage.rs b/third_party/rust/glean-core/tests/storage.rs
new file mode 100644
index 0000000000..9ffab11f0c
--- /dev/null
+++ b/third_party/rust/glean-core/tests/storage.rs
@@ -0,0 +1,105 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{CommonMetricData, Lifetime};
+
+#[test]
+fn snapshot_returns_none_if_nothing_is_recorded_in_the_store() {
+ let (glean, _t) = new_glean(None);
+ assert!(StorageManager
+ .snapshot(glean.storage(), "unknown_store", true)
+ .is_none())
+}
+
+#[test]
+fn can_snapshot() {
+ let (glean, _t) = new_glean(None);
+
+ let local_metric = StringMetric::new(CommonMetricData {
+ name: "can_snapshot_local_metric".into(),
+ category: "local".into(),
+ send_in_pings: vec!["store".into()],
+ ..Default::default()
+ });
+
+ local_metric.set(&glean, "snapshot 42");
+
+ assert!(StorageManager
+ .snapshot(glean.storage(), "store", true)
+ .is_some())
+}
+
+#[test]
+fn snapshot_correctly_clears_the_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = CounterMetric::new(CommonMetricData {
+ name: "metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names,
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.add(&glean, 1);
+
+ // Get the snapshot from "store1" and clear it.
+ let snapshot = StorageManager.snapshot(glean.storage(), "store1", true);
+ assert!(snapshot.is_some());
+ // Check that getting a new snapshot for "store1" returns an empty store.
+ assert!(StorageManager
+ .snapshot(glean.storage(), "store1", false)
+ .is_none());
+ // Check that we get the right data from both the stores. Clearing "store1" must
+ // not clear "store2" as well.
+ let snapshot2 = StorageManager.snapshot(glean.storage(), "store2", true);
+ assert!(snapshot2.is_some());
+}
+
+#[test]
+fn storage_is_thread_safe() {
+ use std::sync::{Arc, Barrier, Mutex};
+ use std::thread;
+
+ let (glean, _t) = new_glean(None);
+ let glean = Arc::new(Mutex::new(glean));
+
+ let threadsafe_metric = CounterMetric::new(CommonMetricData {
+ name: "threadsafe".into(),
+ category: "global".into(),
+ send_in_pings: vec!["core".into(), "metrics".into()],
+ ..Default::default()
+ });
+ let threadsafe_metric = Arc::new(threadsafe_metric);
+
+ let barrier = Arc::new(Barrier::new(2));
+ let c = barrier.clone();
+ let threadsafe_metric_clone = threadsafe_metric.clone();
+ let glean_clone = glean.clone();
+ let child = thread::spawn(move || {
+ threadsafe_metric_clone.add(&*glean_clone.lock().unwrap(), 1);
+ c.wait();
+ threadsafe_metric_clone.add(&*glean_clone.lock().unwrap(), 1);
+ });
+
+ threadsafe_metric.add(&*glean.lock().unwrap(), 1);
+ barrier.wait();
+ threadsafe_metric.add(&*glean.lock().unwrap(), 1);
+
+ child.join().unwrap();
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.lock().unwrap().storage(), "core", true)
+ .unwrap();
+ assert_eq!(json!({"counter": { "global.threadsafe": 4 }}), snapshot);
+}
diff --git a/third_party/rust/glean-core/tests/string.rs b/third_party/rust/glean-core/tests/string.rs
new file mode 100644
index 0000000000..f5a1858cd7
--- /dev/null
+++ b/third_party/rust/glean-core/tests/string.rs
@@ -0,0 +1,121 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+use glean_core::{CommonMetricData, Lifetime};
+
+// SKIPPED from glean-ac: string deserializer should correctly parse integers
+// This test doesn't really apply to rkv
+
+#[test]
+fn string_serializer_should_correctly_serialize_strings() {
+ let (mut tempdir, _) = tempdir();
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let metric = StringMetric::new(CommonMetricData {
+ name: "string_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+
+ metric.set(&glean, "test_string_value");
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"string": {"telemetry.string_metric": "test_string_value"}}),
+ snapshot
+ );
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"string": {"telemetry.string_metric": "test_string_value"}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn set_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = StringMetric::new(CommonMetricData {
+ name: "string_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set(&glean, "test_string_value");
+
+ // Check that the data was correctly set in each store.
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!({"string": {"telemetry.string_metric": "test_string_value"}}),
+ snapshot
+ );
+ }
+}
+
+// SKIPPED from glean-ac: strings are serialized in the correct JSON format
+// Completely redundant with other tests.
+
+#[test]
+fn long_string_values_are_truncated() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = StringMetric::new(CommonMetricData {
+ name: "string_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ let test_sting = "01234567890".repeat(20);
+ metric.set(&glean, test_sting.clone());
+
+ // Check that data was truncated
+ assert_eq!(
+ test_sting[..100],
+ metric.test_get_value(&glean, "store1").unwrap()
+ );
+
+ // Make sure that the errors have been recorded
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow, None)
+ );
+}
diff --git a/third_party/rust/glean-core/tests/string_list.rs b/third_party/rust/glean-core/tests/string_list.rs
new file mode 100644
index 0000000000..e2355d5df5
--- /dev/null
+++ b/third_party/rust/glean-core/tests/string_list.rs
@@ -0,0 +1,249 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, CommonMetricData, ErrorType, Lifetime};
+
+#[test]
+fn list_can_store_multiple_items() {
+ let (glean, _t) = new_glean(None);
+
+ let list: StringListMetric = StringListMetric::new(CommonMetricData {
+ name: "list".into(),
+ category: "local".into(),
+ send_in_pings: vec!["core".into()],
+ ..Default::default()
+ });
+
+ list.add(&glean, "first");
+ assert_eq!(list.test_get_value(&glean, "core").unwrap(), vec!["first"]);
+
+ list.add(&glean, "second");
+ assert_eq!(
+ list.test_get_value(&glean, "core").unwrap(),
+ vec!["first", "second"]
+ );
+
+ list.set(&glean, vec!["third".into()]);
+ assert_eq!(list.test_get_value(&glean, "core").unwrap(), vec!["third"]);
+
+ list.add(&glean, "fourth");
+ assert_eq!(
+ list.test_get_value(&glean, "core").unwrap(),
+ vec!["third", "fourth"]
+ );
+}
+
+#[test]
+fn stringlist_serializer_should_correctly_serialize_stringlists() {
+ let (mut tempdir, _) = tempdir();
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let metric = StringListMetric::new(CommonMetricData {
+ name: "string_list_metric".into(),
+ category: "telemetry.test".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+ metric.set(&glean, vec!["test_string_1".into(), "test_string_2".into()]);
+ }
+
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"string_list": {"telemetry.test.string_list_metric": ["test_string_1", "test_string_2"]}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn set_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let metric = StringListMetric::new(CommonMetricData {
+ name: "string_list_metric".into(),
+ category: "telemetry.test".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set(&glean, vec!["test_string_1".into(), "test_string_2".into()]);
+
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!({"string_list": {"telemetry.test.string_list_metric": ["test_string_1", "test_string_2"]}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn long_string_values_are_truncated() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = StringListMetric::new(CommonMetricData {
+ name: "string_list_metric".into(),
+ category: "telemetry.test".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ let test_string = "0123456789".repeat(20);
+ metric.add(&glean, test_string.clone());
+
+ // Ensure the string was truncated to the proper length.
+ assert_eq!(
+ vec![test_string[..50].to_string()],
+ metric.test_get_value(&glean, "store1").unwrap()
+ );
+
+ // Ensure the error has been recorded.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow, None)
+ );
+
+ metric.set(&glean, vec![test_string.clone()]);
+
+ // Ensure the string was truncated to the proper length.
+ assert_eq!(
+ vec![test_string[..50].to_string()],
+ metric.test_get_value(&glean, "store1").unwrap()
+ );
+
+ // Ensure the error has been recorded.
+ assert_eq!(
+ Ok(2),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow, None)
+ );
+}
+
+#[test]
+fn disabled_string_lists_dont_record() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = StringListMetric::new(CommonMetricData {
+ name: "string_list_metric".into(),
+ category: "telemetry.test".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: true,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.add(&glean, "test_string".repeat(20));
+
+ // Ensure the string was not added.
+ assert_eq!(None, metric.test_get_value(&glean, "store1"));
+
+ metric.set(&glean, vec!["test_string_2".repeat(20)]);
+
+ // Ensure the stringlist was not set.
+ assert_eq!(None, metric.test_get_value(&glean, "store1"));
+
+ // Ensure no error was recorded.
+ assert!(
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue, None).is_err()
+ );
+}
+
+#[test]
+fn string_lists_dont_exceed_max_items() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = StringListMetric::new(CommonMetricData {
+ name: "string_list_metric".into(),
+ category: "telemetry.test".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ for _n in 1..21 {
+ metric.add(&glean, "test_string");
+ }
+
+ let expected: Vec<String> = "test_string "
+ .repeat(20)
+ .split_whitespace()
+ .map(|s| s.to_string())
+ .collect();
+ assert_eq!(expected, metric.test_get_value(&glean, "store1").unwrap());
+
+ // Ensure the 21st string wasn't added.
+ metric.add(&glean, "test_string");
+ assert_eq!(expected, metric.test_get_value(&glean, "store1").unwrap());
+
+ // Ensure we recorded the error.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue, None)
+ );
+
+ // Try to set it to a list that's too long. Ensure it cuts off at 20 elements.
+ let too_many: Vec<String> = "test_string "
+ .repeat(21)
+ .split_whitespace()
+ .map(|s| s.to_string())
+ .collect();
+ metric.set(&glean, too_many);
+ assert_eq!(expected, metric.test_get_value(&glean, "store1").unwrap());
+
+ assert_eq!(
+ Ok(2),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue, None)
+ );
+}
+
+#[test]
+fn set_does_not_record_error_when_receiving_empty_list() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = StringListMetric::new(CommonMetricData {
+ name: "string_list_metric".into(),
+ category: "telemetry.test".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set(&glean, vec![]);
+
+ // Ensure the empty list was added
+ assert_eq!(Some(vec![]), metric.test_get_value(&glean, "store1"));
+
+ // Ensure we didn't record an error.
+ assert!(
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue, None).is_err()
+ );
+}
diff --git a/third_party/rust/glean-core/tests/timespan.rs b/third_party/rust/glean-core/tests/timespan.rs
new file mode 100644
index 0000000000..60855729af
--- /dev/null
+++ b/third_party/rust/glean-core/tests/timespan.rs
@@ -0,0 +1,353 @@
+// 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::time::Duration;
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+use glean_core::{CommonMetricData, Lifetime};
+
+// Tests ported from glean-ac
+
+#[test]
+fn serializer_should_correctly_serialize_timespans() {
+ let (mut tempdir, _) = tempdir();
+
+ let duration = 60;
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let mut metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ metric.set_start(&glean, 0);
+ metric.set_stop(&glean, duration);
+
+ let val = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+ assert_eq!(duration, val, "Recorded timespan should be positive.");
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!({"timespan": {"telemetry.timespan_metric": { "value": duration, "time_unit": "nanosecond" }}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn single_elapsed_time_must_be_recorded() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let duration = 60;
+
+ metric.set_start(&glean, 0);
+ metric.set_stop(&glean, duration);
+
+ let val = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+ assert_eq!(duration, val, "Recorded timespan should be positive.");
+}
+
+// SKIPPED from glean-ac: multiple elapsed times must be correctly accumulated.
+// replaced by below after API change.
+
+#[test]
+fn second_timer_run_is_skipped() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let duration = 60;
+ metric.set_start(&glean, 0);
+ metric.set_stop(&glean, duration);
+
+ // No error should be recorded here: we had no prior value stored.
+ assert!(
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidState, None).is_err()
+ );
+
+ let first_value = metric.test_get_value(&glean, "store1").unwrap();
+ assert_eq!(duration, first_value);
+
+ metric.set_start(&glean, 0);
+ metric.set_stop(&glean, duration * 2);
+
+ let second_value = metric.test_get_value(&glean, "store1").unwrap();
+ assert_eq!(second_value, first_value);
+
+ // Make sure that the error has been recorded: we had a stored value, the
+ // new measurement was dropped.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidState, None)
+ );
+}
+
+#[test]
+fn recorded_time_conforms_to_resolution() {
+ let (glean, _t) = new_glean(None);
+
+ let mut ns_metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_ns".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let mut minute_metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_m".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Minute,
+ );
+
+ let duration = 60;
+ ns_metric.set_start(&glean, 0);
+ ns_metric.set_stop(&glean, duration);
+
+ let ns_value = ns_metric.test_get_value(&glean, "store1").unwrap();
+ assert_eq!(duration, ns_value);
+
+ // 1 minute in nanoseconds
+ let duration_minute = 60 * 1_000_000_000;
+ minute_metric.set_start(&glean, 0);
+ minute_metric.set_stop(&glean, duration_minute);
+
+ let minute_value = minute_metric.test_get_value(&glean, "store1").unwrap();
+ assert_eq!(1, minute_value);
+}
+
+// SKIPPED from glean-ac: accumulated short-lived timespans should not be discarded
+
+#[test]
+fn cancel_does_not_store() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ metric.set_start(&glean, 0);
+ metric.cancel();
+
+ assert_eq!(None, metric.test_get_value(&glean, "store1"));
+}
+
+#[test]
+fn nothing_stored_before_stop() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let duration = 60;
+
+ metric.set_start(&glean, 0);
+
+ assert_eq!(None, metric.test_get_value(&glean, "store1"));
+
+ metric.set_stop(&glean, duration);
+ assert_eq!(duration, metric.test_get_value(&glean, "store1").unwrap());
+}
+
+#[test]
+fn set_raw_time() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let time = Duration::from_secs(1);
+ metric.set_raw(&glean, time);
+
+ let time_in_ns = time.as_nanos() as u64;
+ assert_eq!(Some(time_in_ns), metric.test_get_value(&glean, "store1"));
+}
+
+#[test]
+fn set_raw_time_does_nothing_when_timer_running() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let time = Duration::from_secs(42);
+
+ metric.set_start(&glean, 0);
+ metric.set_raw(&glean, time);
+ metric.set_stop(&glean, 60);
+
+ // We expect the start/stop value, not the raw value.
+ assert_eq!(Some(60), metric.test_get_value(&glean, "store1"));
+
+ // Make sure that the error has been recorded
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidState, None)
+ );
+}
+
+#[test]
+fn timespan_is_not_tracked_across_upload_toggle() {
+ let (mut glean, _t) = new_glean(None);
+
+ let mut metric = TimespanMetric::new(
+ CommonMetricData {
+ name: "timespan_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ // Timer is started.
+ metric.set_start(&glean, 0);
+ // User disables telemetry upload.
+ glean.set_upload_enabled(false);
+ // App code eventually stops the timer.
+ // We should clear internal state as upload is disabled.
+ metric.set_stop(&glean, 40);
+
+ // App code eventually starts the timer again.
+ // Upload is disabled, so this should not have any effect.
+ metric.set_start(&glean, 100);
+ // User enables telemetry upload again.
+ glean.set_upload_enabled(true);
+ // App code eventually stops the timer.
+ // None should be running.
+ metric.set_stop(&glean, 200);
+
+ // Nothing should have been recorded.
+ assert_eq!(None, metric.test_get_value(&glean, "store1"));
+
+ // Make sure that the error has been recorded
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidState, None)
+ );
+}
+
+#[test]
+fn time_cannot_go_backwards() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric: TimespanMetric = TimespanMetric::new(
+ CommonMetricData {
+ name: "raw_timespan".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ },
+ TimeUnit::Millisecond,
+ );
+
+ // Time cannot go backwards.
+ metric.set_start(&glean, 10);
+ metric.set_stop(&glean, 0);
+ assert!(metric.test_get_value(&glean, "test1").is_none());
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue, None),
+ );
+}
diff --git a/third_party/rust/glean-core/tests/timing_distribution.rs b/third_party/rust/glean-core/tests/timing_distribution.rs
new file mode 100644
index 0000000000..e338fc70c0
--- /dev/null
+++ b/third_party/rust/glean-core/tests/timing_distribution.rs
@@ -0,0 +1,336 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use std::time::Duration;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+use glean_core::{CommonMetricData, Lifetime};
+
+// Tests ported from glean-ac
+
+#[test]
+fn serializer_should_correctly_serialize_timing_distribution() {
+ let (mut tempdir, _) = tempdir();
+
+ let duration = 60;
+ let time_unit = TimeUnit::Nanosecond;
+
+ {
+ let (glean, dir) = new_glean(Some(tempdir));
+ tempdir = dir;
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ time_unit,
+ );
+
+ let id = metric.set_start(0);
+ metric.set_stop_and_accumulate(&glean, id, duration);
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ assert_eq!(snapshot.sum, duration);
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ assert_eq!(
+ json!(duration),
+ snapshot["timing_distribution"]["telemetry.distribution"]["sum"]
+ );
+ }
+}
+
+#[test]
+fn set_value_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+
+ let duration = 1;
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let id = metric.set_start(0);
+ metric.set_stop_and_accumulate(&glean, id, duration);
+
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!(duration),
+ snapshot["timing_distribution"]["telemetry.distribution"]["sum"]
+ );
+ assert_eq!(
+ json!(1),
+ snapshot["timing_distribution"]["telemetry.distribution"]["values"]["1"]
+ );
+ }
+}
+
+#[test]
+fn timing_distributions_must_not_accumulate_negative_values() {
+ let (glean, _t) = new_glean(None);
+
+ let duration = 60;
+ let time_unit = TimeUnit::Nanosecond;
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ time_unit,
+ );
+
+ // Flip around the timestamps, this should result in a negative value which should be
+ // discarded.
+ let id = metric.set_start(duration);
+ metric.set_stop_and_accumulate(&glean, id, 0);
+
+ assert!(metric.test_get_value(&glean, "store1").is_none());
+
+ // Make sure that the errors have been recorded
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ );
+}
+
+#[test]
+fn the_accumulate_samples_api_correctly_stores_timing_values() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Second,
+ );
+
+ // Accumulate the samples. We intentionally do not report
+ // negative values to not trigger error reporting.
+ metric.accumulate_samples_signed(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ let seconds_to_nanos = 1000 * 1000 * 1000;
+
+ // Check that we got the right sum and number of samples.
+ assert_eq!(snapshot.sum, 6 * seconds_to_nanos);
+
+ // We should get a sample in 3 buckets.
+ // These numbers are a bit magic, but they correspond to
+ // `hist.sample_to_bucket_minimum(i * seconds_to_nanos)` for `i = 1..=3`.
+ assert_eq!(1, snapshot.values[&984_625_593]);
+ assert_eq!(1, snapshot.values[&1_969_251_187]);
+ assert_eq!(1, snapshot.values[&2_784_941_737]);
+
+ // No errors should be reported.
+ assert!(test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ .is_err());
+}
+
+#[test]
+fn the_accumulate_samples_api_correctly_handles_negative_values() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ // Accumulate the samples.
+ metric.accumulate_samples_signed(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum and number of samples.
+ assert_eq!(snapshot.sum, 6);
+
+ // We should get a sample in each of the first 3 buckets.
+ assert_eq!(1, snapshot.values[&1]);
+ assert_eq!(1, snapshot.values[&2]);
+ assert_eq!(1, snapshot.values[&3]);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidValue,
+ Some("store1")
+ )
+ );
+}
+
+#[test]
+fn the_accumulate_samples_api_correctly_handles_overflowing_values() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ // The MAX_SAMPLE_TIME is the same from `metrics/timing_distribution.rs`.
+ const MAX_SAMPLE_TIME: u64 = 1000 * 1000 * 1000 * 60 * 10;
+ let overflowing_val = MAX_SAMPLE_TIME as i64 + 1;
+ // Accumulate the samples.
+ metric.accumulate_samples_signed(&glean, [overflowing_val, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Overflowing values are truncated to MAX_SAMPLE_TIME and recorded.
+ assert_eq!(snapshot.sum, MAX_SAMPLE_TIME + 6);
+
+ // We should get a sample in each of the first 3 buckets.
+ assert_eq!(1, snapshot.values[&1]);
+ assert_eq!(1, snapshot.values[&2]);
+ assert_eq!(1, snapshot.values[&3]);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidOverflow,
+ Some("store1")
+ )
+ );
+}
+
+#[test]
+fn large_nanoseconds_values() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "distribution".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ let time = Duration::from_secs(10).as_nanos() as u64;
+ assert!(time > u64::from(u32::max_value()));
+
+ let id = metric.set_start(0);
+ metric.set_stop_and_accumulate(&glean, id, time);
+
+ let val = metric
+ .test_get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum and number of samples.
+ assert_eq!(val.sum, time);
+}
+
+#[test]
+fn stopping_non_existing_id_records_an_error() {
+ let (glean, _t) = new_glean(None);
+
+ let mut metric = TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "non_existing_id".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ );
+
+ metric.set_stop_and_accumulate(&glean, 3785, 60);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(
+ &glean,
+ metric.meta(),
+ ErrorType::InvalidState,
+ Some("store1")
+ )
+ );
+}
diff --git a/third_party/rust/glean-core/tests/uuid.rs b/third_party/rust/glean-core/tests/uuid.rs
new file mode 100644
index 0000000000..1317790e6c
--- /dev/null
+++ b/third_party/rust/glean-core/tests/uuid.rs
@@ -0,0 +1,114 @@
+// 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/.
+
+mod common;
+use crate::common::*;
+
+use serde_json::json;
+
+use glean_core::metrics::*;
+use glean_core::storage::StorageManager;
+use glean_core::{CommonMetricData, Lifetime};
+
+#[test]
+fn uuid_is_generated_and_stored() {
+ let (mut glean, _t) = new_glean(None);
+
+ let uuid: UuidMetric = UuidMetric::new(CommonMetricData {
+ name: "uuid".into(),
+ category: "local".into(),
+ send_in_pings: vec!["core".into()],
+ ..Default::default()
+ });
+
+ uuid.generate_and_set(&glean);
+ let snapshot = glean.snapshot("core", false);
+ assert!(
+ snapshot.contains(r#""local.uuid": ""#),
+ format!("Snapshot 1: {}", snapshot)
+ );
+
+ uuid.generate_and_set(&glean);
+ let snapshot = glean.snapshot("core", false);
+ assert!(
+ snapshot.contains(r#""local.uuid": ""#),
+ format!("Snapshot 2: {}", snapshot)
+ );
+}
+
+#[test]
+fn uuid_serializer_should_correctly_serialize_uuids() {
+ let value = uuid::Uuid::new_v4();
+
+ let (mut tempdir, _) = tempdir();
+
+ {
+ // We give tempdir to the `new_glean` function...
+ let (glean, dir) = new_glean(Some(tempdir));
+ // And then we get it back once that function returns.
+ tempdir = dir;
+
+ let metric = UuidMetric::new(CommonMetricData {
+ name: "uuid_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+
+ metric.set(&glean, value);
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"uuid": {"telemetry.uuid_metric": value.to_string()}}),
+ snapshot
+ );
+ }
+
+ // Make a new Glean instance here, which should force reloading of the data from disk
+ // so we can ensure it persisted, because it has User lifetime
+ {
+ let (glean, _) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"uuid": {"telemetry.uuid_metric": value.to_string()}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn set_properly_sets_the_value_in_all_stores() {
+ let (glean, _t) = new_glean(None);
+ let store_names: Vec<String> = vec!["store1".into(), "store2".into()];
+ let value = uuid::Uuid::new_v4();
+
+ let metric = UuidMetric::new(CommonMetricData {
+ name: "uuid_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set(&glean, value);
+
+ // Check that the data was correctly set in each store.
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ json!({"uuid": {"telemetry.uuid_metric": value.to_string()}}),
+ snapshot
+ );
+ }
+}