summaryrefslogtreecommitdiffstats
path: root/third_party/rust/glean-core/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /third_party/rust/glean-core/tests
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
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.rs148
-rw-r--r--third_party/rust/glean-core/tests/counter.rs177
-rw-r--r--third_party/rust/glean-core/tests/custom_distribution.rs419
-rw-r--r--third_party/rust/glean-core/tests/datetime.rs187
-rw-r--r--third_party/rust/glean-core/tests/event.rs514
-rw-r--r--third_party/rust/glean-core/tests/labeled.rs518
-rw-r--r--third_party/rust/glean-core/tests/memory_distribution.rs194
-rw-r--r--third_party/rust/glean-core/tests/ping.rs249
-rw-r--r--third_party/rust/glean-core/tests/ping_maker.rs301
-rw-r--r--third_party/rust/glean-core/tests/quantity.rs118
-rw-r--r--third_party/rust/glean-core/tests/rate.rs134
-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.rs245
-rw-r--r--third_party/rust/glean-core/tests/text.rs115
-rw-r--r--third_party/rust/glean-core/tests/timespan.rs351
-rw-r--r--third_party/rust/glean-core/tests/timing_distribution.rs431
-rw-r--r--third_party/rust/glean-core/tests/uuid.rs114
19 files changed, 4532 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..0215dd75ec
--- /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_sync(&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_sync(&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..ebc4f14045
--- /dev/null
+++ b/third_party/rust/glean-core/tests/common/mod.rs
@@ -0,0 +1,148 @@
+// 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 cfg = glean_core::InternalConfiguration {
+ data_path: dir.path().display().to_string(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ language_binding_name: "Rust".into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ app_build: "Unknown".into(),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ rate_limit: None,
+ enable_event_timestamps: false,
+ experimentation_id: None,
+ };
+ 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..bdcd232773
--- /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_sync(&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_sync(&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_sync(&glean, 0);
+ // Check that nothing was recorded
+ assert!(metric.get_value(&glean, Some("store1")).is_none());
+
+ // Attempt to increment the counter with negative
+ metric.add_sync(&glean, -1);
+ // Check that nothing was recorded
+ assert!(metric.get_value(&glean, Some("store1")).is_none());
+
+ // Attempt increment counter properly
+ metric.add_sync(&glean, 1);
+ // Check that nothing was recorded
+ assert_eq!(1, metric.get_value(&glean, Some("store1")).unwrap());
+
+ // Make sure that the error has been recorded
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue)
+ );
+}
+
+// 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_sync(&glean, 2);
+
+ assert_eq!(2, counter.get_value(&glean, Some("store1")).unwrap());
+ assert_eq!(2, counter.get_value(&glean, Some("store2")).unwrap());
+
+ // Clearing just one store
+ let _ = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ counter.add_sync(&glean, 2);
+
+ assert_eq!(2, counter.get_value(&glean, Some("store1")).unwrap());
+ assert_eq!(4, counter.get_value(&glean, Some("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_sync(&glean, 2);
+ counter.add_sync(&glean, i32::max_value());
+
+ assert_eq!(
+ i32::max_value(),
+ counter.get_value(&glean, Some("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..43c69fb26d
--- /dev/null
+++ b/third_party/rust/glean-core/tests/custom_distribution.rs
@@ -0,0 +1,419 @@
+// 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_sync(&glean, vec![50]);
+
+ let snapshot = metric
+ .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, _t) = 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_sync(&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_sync(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .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).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_sync(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .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)
+ );
+ }
+
+ #[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_sync(&glean, vec![50]);
+
+ let snapshot = metric.get_value(&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_sync(&glean, vec![50]);
+
+ let snapshot = metric
+ .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, _t) = 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_sync(&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_sync(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .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).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_sync(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .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)
+ );
+ }
+
+ #[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_sync(&glean, vec![50]);
+
+ let snapshot = metric.get_value(&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..7e7938690f
--- /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_sync(&glean, Some(dt.into()));
+
+ 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, _t) = 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_sync(&glean, Some(dt.into()));
+
+ for store_name in store_names {
+ assert_eq!(
+ "1983-04-13T12:09:14.001560274+00:00",
+ metric
+ .get_value_as_string(&glean, Some(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_sync(&glean, Some(high_res_datetime.into()));
+
+ assert_eq!(
+ t.expected_result,
+ metric
+ .get_value_as_string(&glean, Some(store_name.into()))
+ .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..ed8f7d807f
--- /dev/null
+++ b/third_party/rust/glean-core/tests/event.rs
@@ -0,0 +1,514 @@
+// 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::{
+ get_timestamp_ms, test_get_num_recorded_errors, CommonMetricData, ErrorType, 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_sync(&glean, 1000, HashMap::new(), 0);
+
+ for store_name in store_names {
+ let events = metric.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 = [
+ ("key1".into(), "value1".into()),
+ ("key2".into(), "value2".into()),
+ ]
+ .iter()
+ .cloned()
+ .collect();
+
+ metric.record_sync(&glean, 1000, extra, 0);
+
+ for store_name in store_names {
+ let events = metric.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(&glean, "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_sync(&glean, 1000, HashMap::new(), 0);
+
+ let snapshot = glean
+ .event_storage()
+ .snapshot_as_json(&glean, "store1", true);
+ assert!(snapshot.is_some());
+
+ assert!(glean
+ .event_storage()
+ .snapshot_as_json(&glean, "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(&glean, "store2", false);
+ for s in [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,
+ true,
+ vec!["max_capacity".to_string()],
+ ));
+ }
+
+ 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::new();
+ extra.insert("test_event_number".to_string(), i.to_string());
+ click.record_sync(&glean, i, extra, 0);
+ }
+
+ assert_eq!(10, click.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(&glean, "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 test_value_long = test_value.to_string().repeat(32);
+ // max length for extra values.
+ let test_value_cap = 500;
+ assert!(
+ test_value_long.len() > test_value_cap,
+ "test value is not long enough"
+ );
+ let mut extra = HashMap::new();
+ extra.insert("extra1".into(), test_value.to_string());
+ extra.insert("truncatedExtra".into(), test_value_long.clone());
+
+ test_event.record_sync(&glean, 0, extra, 0);
+
+ let snapshot = glean
+ .event_storage()
+ .snapshot_as_json(&glean, "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_long[0..test_value_cap],
+ 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_sync(&glean, 1000, HashMap::new(), 0);
+ metric.record_sync(&glean, 100, HashMap::new(), 0);
+ metric.record_sync(&glean, 10000, HashMap::new(), 0);
+
+ let snapshot = glean
+ .event_storage()
+ .snapshot_as_json(&glean, "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()
+ );
+}
+
+#[test]
+fn ensure_custom_ping_events_dont_overflow() {
+ let (glean, _dir) = new_glean(None);
+
+ let store_name = "store-name";
+ let event_meta = CommonMetricData {
+ name: "name".into(),
+ category: "category".into(),
+ send_in_pings: vec![store_name.into()],
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ };
+ let event = EventMetric::new(event_meta.clone(), vec![]);
+
+ assert!(test_get_num_recorded_errors(
+ &glean,
+ &(event_meta.clone()).into(),
+ ErrorType::InvalidOverflow
+ )
+ .is_err());
+
+ for _ in 0..500 {
+ event.record_sync(&glean, 0, HashMap::new(), 0);
+ }
+ assert!(test_get_num_recorded_errors(
+ &glean,
+ &(event_meta.clone()).into(),
+ ErrorType::InvalidOverflow
+ )
+ .is_err());
+
+ // That should top us right up to the limit. Now for going over.
+ event.record_sync(&glean, 0, HashMap::new(), 0);
+ assert!(
+ test_get_num_recorded_errors(&glean, &event_meta.into(), ErrorType::InvalidOverflow)
+ .is_err()
+ );
+ assert_eq!(501, event.get_value(&glean, store_name).unwrap().len());
+}
+
+/// Ensure that events from multiple runs serialize properly.
+///
+/// Records an event once each in two separate sessions,
+/// ensuring they both show (and an inserted `glean.restarted` event)
+/// in the serialized result.
+#[test]
+fn ensure_custom_ping_events_from_multiple_runs_work() {
+ let (mut tempdir, _) = tempdir();
+
+ let store_name = "store-name";
+ let event = EventMetric::new(
+ CommonMetricData {
+ name: "name".into(),
+ category: "category".into(),
+ send_in_pings: vec![store_name.into()],
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec![],
+ );
+
+ {
+ let (glean, dir) = new_glean(Some(tempdir));
+ // We don't need a full init. Just to deal with on-disk events:
+ assert!(!glean
+ .event_storage()
+ .flush_pending_events_on_startup(&glean, false));
+ tempdir = dir;
+
+ event.record_sync(&glean, 10, HashMap::new(), 0);
+ }
+
+ {
+ let (glean, _dir) = new_glean(Some(tempdir));
+ // We don't need a full init. Just to deal with on-disk events:
+ assert!(!glean
+ .event_storage()
+ .flush_pending_events_on_startup(&glean, false));
+
+ // Gotta use get_timestamp_ms or this event won't happen "after" the injected
+ // glean.restarted event from `flush_pending_events_on_startup`.
+ event.record_sync(&glean, get_timestamp_ms(), HashMap::new(), 0);
+
+ let json = glean
+ .event_storage()
+ .snapshot_as_json(&glean, store_name, false)
+ .unwrap();
+ assert_eq!(json.as_array().unwrap().len(), 3);
+ assert_eq!(json[0]["category"], "category");
+ assert_eq!(json[0]["name"], "name");
+ assert_eq!(json[1]["category"], "glean");
+ assert_eq!(json[1]["name"], "restarted");
+ assert_eq!(json[2]["category"], "category");
+ assert_eq!(json[2]["name"], "name");
+ }
+}
+
+/// Ensure events in an unregistered, non-"events" (ie Custom) ping are trimmed on a subsequent init
+/// when we pass `true ` for `trim_data_to_registered_pings` in `on_ready_to_submit_pings`.
+#[test]
+fn event_storage_trimming() {
+ let (mut tempdir, _) = tempdir();
+
+ let store_name = "store-name";
+ let store_name_2 = "store-name-2";
+ let event = EventMetric::new(
+ CommonMetricData {
+ name: "name".into(),
+ category: "category".into(),
+ send_in_pings: vec![store_name.into(), store_name_2.into()],
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec![],
+ );
+ // First, record the event in the two pings.
+ // Successfully records just fine because nothing's checking on record that these pings
+ // exist and are registered.
+ {
+ let (glean, dir) = new_glean(Some(tempdir));
+ tempdir = dir;
+ event.record_sync(&glean, 10, HashMap::new(), 0);
+
+ assert_eq!(1, event.get_value(&glean, store_name).unwrap().len());
+ assert_eq!(1, event.get_value(&glean, store_name_2).unwrap().len());
+ }
+ // Second, construct (but don't init) Glean over again.
+ // Register exactly one of the two pings.
+ // Then process the part of init that does the trimming (`on_ready_to_submit_pings`).
+ // This ought to load the data from the registered ping and trim the data from the unregistered one.
+ {
+ let (mut glean, _dir) = new_glean(Some(tempdir));
+ // In Rust, pings are registered via construction.
+ // But that's done asynchronously, so we do it synchronously here:
+ glean.register_ping_type(&PingType::new(
+ store_name.to_string(),
+ true,
+ false,
+ true,
+ vec![],
+ ));
+
+ glean.on_ready_to_submit_pings(true);
+
+ assert_eq!(1, event.get_value(&glean, store_name).unwrap().len());
+ assert!(event.get_value(&glean, store_name_2).is_none());
+ }
+}
+
+#[test]
+fn with_event_timestamps() {
+ use glean_core::{Glean, InternalConfiguration};
+
+ let dir = tempfile::tempdir().unwrap();
+ let cfg = InternalConfiguration {
+ data_path: dir.path().display().to_string(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ language_binding_name: "Rust".into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ app_build: "Unknown".into(),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ rate_limit: None,
+ enable_event_timestamps: true,
+ experimentation_id: None, // Enabling event timestamps
+ };
+ let glean = Glean::new(cfg).unwrap();
+
+ let store_name = "store-name";
+ let event = EventMetric::new(
+ CommonMetricData {
+ name: "name".into(),
+ category: "category".into(),
+ send_in_pings: vec![store_name.into()],
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ vec![],
+ );
+
+ event.record_sync(&glean, get_timestamp_ms(), HashMap::new(), 12345);
+
+ let json = glean
+ .event_storage()
+ .snapshot_as_json(&glean, store_name, false)
+ .unwrap();
+ assert_eq!(json.as_array().unwrap().len(), 1);
+ assert_eq!(json[0]["category"], "category");
+ assert_eq!(json[0]["name"], "name");
+
+ let glean_timestamp = json[0]["extra"]["glean_timestamp"]
+ .as_str()
+ .expect("glean_timestamp should exist");
+ let glean_timestamp: i64 = glean_timestamp
+ .parse()
+ .expect("glean_timestamp should be an integer");
+ assert_eq!(12345, glean_timestamp);
+}
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..63a7f2ccc2
--- /dev/null
+++ b/third_party/rust/glean-core/tests/labeled.rs
@@ -0,0 +1,518 @@
+// 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};
+
+#[test]
+fn can_create_labeled_counter_metric() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledCounter::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_sync(&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 = LabeledString::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_sync(&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 = LabeledBoolean::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_sync(&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 = LabeledCounter::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_sync(&glean, 1);
+
+ let metric = labeled.get("label2");
+ metric.add_sync(&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 can_record_error_for_submetric() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledString::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_sync(&glean, "01234567890".repeat(20));
+
+ // Make sure that the errors have been recorded
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
+ );
+}
+
+#[test]
+fn labels_are_checked_against_static_list() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledCounter::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_sync(&glean, 1);
+
+ let metric = labeled.get("label2");
+ metric.add_sync(&glean, 2);
+
+ // All non-registed labels get mapped to the `other` label
+ let metric = labeled.get("label3");
+ metric.add_sync(&glean, 3);
+ let metric = labeled.get("label4");
+ metric.add_sync(&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 = LabeledCounter::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("1".repeat(72));
+ metric.add_sync(&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 = LabeledCounter::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!["non-ASCII�"];
+ let num_non_validating = labels_not_validating.len();
+
+ for label in &labels_not_validating {
+ labeled.get(label).add_sync(&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 = LabeledCounter::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_sync(&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 = LabeledCounter::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_sync(&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, _t) = new_glean(Some(tempdir));
+
+ // Try to store another label
+ labeled.get("new_label").add_sync(&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__"]
+ );
+ }
+}
+
+#[test]
+fn caching_metrics_with_dynamic_labels() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledCounter::new(
+ CommonMetricData {
+ name: "cached_labels".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ None,
+ );
+
+ // Create multiple metric instances and cache them for later use.
+ let metrics = (1..=20)
+ .map(|i| {
+ let label = format!("label{i}");
+ labeled.get(label)
+ })
+ .collect::<Vec<_>>();
+
+ // Only now use them.
+ for metric in metrics {
+ metric.add_sync(&glean, 1);
+ }
+
+ // The maximum number of labels we store is 16.
+ // So we should have put 4 metrics in the __other__ bucket.
+ let other = labeled.get("__other__");
+ assert_eq!(Some(4), other.get_value(&glean, Some("store1")));
+}
+
+#[test]
+fn caching_metrics_with_dynamic_labels_across_pings() {
+ let (glean, _t) = new_glean(None);
+ let labeled = LabeledCounter::new(
+ CommonMetricData {
+ name: "cached_labels2".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ },
+ None,
+ );
+
+ // Create multiple metric instances and cache them for later use.
+ let metrics = (1..=20)
+ .map(|i| {
+ let label = format!("label{i}");
+ labeled.get(label)
+ })
+ .collect::<Vec<_>>();
+
+ // Only now use them.
+ for metric in &metrics {
+ metric.add_sync(&glean, 1);
+ }
+
+ // The maximum number of labels we store is 16.
+ // So we should have put 4 metrics in the __other__ bucket.
+ let other = labeled.get("__other__");
+ assert_eq!(Some(4), other.get_value(&glean, Some("store1")));
+
+ // Snapshot (so we can inspect the JSON)
+ // and clear out storage (the same way submitting a ping would)
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ // We didn't send the 20th label
+ assert_eq!(
+ json!(null),
+ snapshot["labeled_counter"]["telemetry.cached_labels2"]["label20"]
+ );
+
+ // We now set the ones that ended up in `__other__` before.
+ // Note: indexing is zero-based,
+ // but we later check the names, so let's offset it by 1.
+ metrics[16].add_sync(&glean, 17);
+ metrics[17].add_sync(&glean, 18);
+ metrics[18].add_sync(&glean, 19);
+ metrics[19].add_sync(&glean, 20);
+
+ assert_eq!(Some(17), metrics[16].get_value(&glean, Some("store1")));
+ assert_eq!(Some(18), metrics[17].get_value(&glean, Some("store1")));
+ assert_eq!(Some(19), metrics[18].get_value(&glean, Some("store1")));
+ assert_eq!(Some(20), metrics[19].get_value(&glean, Some("store1")));
+ assert_eq!(None, other.get_value(&glean, Some("store1")));
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ let cached_labels = &snapshot["labeled_counter"]["telemetry.cached_labels2"];
+ assert_eq!(json!(17), cached_labels["label17"]);
+ assert_eq!(json!(18), cached_labels["label18"]);
+ assert_eq!(json!(19), cached_labels["label19"]);
+ assert_eq!(json!(20), cached_labels["label20"]);
+ assert_eq!(json!(null), cached_labels["__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..83ca84ddd5
--- /dev/null
+++ b/third_party/rust/glean-core/tests/memory_distribution.rs
@@ -0,0 +1,194 @@
+// 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_sync(&glean, 100_000);
+
+ let snapshot = metric
+ .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, _t) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+
+ // We check the exact format to catch changes to the serialization.
+ let expected = json!({
+ "sum": 100_000 * kb,
+ "values": {
+ "99108124": 1,
+ "103496016": 0,
+ }
+ });
+ assert_eq!(
+ expected,
+ snapshot["memory_distribution"]["telemetry.distribution"]
+ );
+ }
+}
+
+#[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_sync(&glean, 100_000);
+
+ // We check the exact format to catch changes to the serialization.
+ let expected = json!({
+ "sum": 100_000,
+ "values": {
+ "96785": 1,
+ "101070": 0,
+ }
+ });
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ expected,
+ snapshot["memory_distribution"]["telemetry.distribution"]
+ );
+ }
+}
+
+// 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_sync(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .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).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_sync(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .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)
+ );
+}
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..0ee3736168
--- /dev/null
+++ b/third_party/rust/glean-core/tests/ping.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 std::collections::HashMap;
+
+use glean_core::metrics::*;
+use glean_core::CommonMetricData;
+use glean_core::Lifetime;
+
+#[test]
+fn write_ping_to_disk() {
+ let (mut glean, _temp) = new_glean(None);
+
+ let ping = PingType::new("metrics", true, false, true, 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_sync(&glean, 1);
+
+ assert!(ping.submit_sync(&glean, None));
+
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn disabling_upload_clears_pending_pings() {
+ let (mut glean, _t) = new_glean(None);
+
+ let ping = PingType::new("metrics", true, false, true, 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_sync(&glean, 1);
+ assert!(ping.submit_sync(&glean, None));
+ 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
+ let dpings = get_deletion_pings(glean.get_data_path()).unwrap();
+ assert_eq!(1, dpings.len());
+ let payload = &dpings[0].1;
+ assert_eq!(
+ "set_upload_enabled",
+ payload["ping_info"].as_object().unwrap()["reason"]
+ .as_str()
+ .unwrap()
+ );
+
+ glean.set_upload_enabled(true);
+ assert_eq!(0, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ counter.add_sync(&glean, 1);
+ assert!(ping.submit_sync(&glean, None));
+ 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, _t) = new_glean(None);
+
+ // Disabling upload generates a deletion ping
+ glean.set_upload_enabled(false);
+ let dpings = get_deletion_pings(glean.get_data_path()).unwrap();
+ assert_eq!(1, dpings.len());
+ let payload = &dpings[0].1;
+ assert_eq!(
+ "set_upload_enabled",
+ payload["ping_info"].as_object().unwrap()["reason"]
+ .as_str()
+ .unwrap()
+ );
+
+ // 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, _t) = new_glean(None);
+
+ let ping1 = PingType::new("custom-ping1", true, true, true, vec![]);
+ glean.register_ping_type(&ping1);
+ let ping2 = PingType::new("custom-ping2", true, false, true, vec![]);
+ glean.register_ping_type(&ping2);
+
+ // No data is stored in either of the custom pings
+
+ // Sending this should succeed.
+ assert!(ping1.submit_sync(&glean, None));
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ // Sending this should fail.
+ assert!(!ping2.submit_sync(&glean, None));
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn test_pings_submitted_metric() {
+ let (mut glean, _temp) = new_glean(None);
+
+ // Reconstructed here so we can test it without reaching into the library
+ // internals.
+ let pings_submitted = LabeledCounter::new(
+ CommonMetricData {
+ name: "pings_submitted".into(),
+ category: "glean.validation".into(),
+ send_in_pings: vec!["metrics".into(), "baseline".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ dynamic_label: None,
+ },
+ None,
+ );
+
+ let metrics_ping = PingType::new("metrics", true, false, true, vec![]);
+ glean.register_ping_type(&metrics_ping);
+
+ let baseline_ping = PingType::new("baseline", true, false, true, vec![]);
+ glean.register_ping_type(&baseline_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_sync(&glean, 1);
+
+ assert!(metrics_ping.submit_sync(&glean, None));
+
+ // Check recording in the metrics ping
+ assert_eq!(
+ Some(1),
+ pings_submitted.get("metrics").get_value(&glean, "metrics")
+ );
+ assert_eq!(
+ None,
+ pings_submitted.get("baseline").get_value(&glean, "metrics")
+ );
+
+ // Check recording in the baseline ping
+ assert_eq!(
+ Some(1),
+ pings_submitted.get("metrics").get_value(&glean, "baseline")
+ );
+ assert_eq!(
+ None,
+ pings_submitted
+ .get("baseline")
+ .get_value(&glean, "baseline")
+ );
+
+ // Trigger 2 baseline pings.
+ // This should record a count of 2 baseline pings in the metrics ping, but
+ // it resets each time on the baseline ping, so we should only ever get 1
+ // baseline ping recorded in the baseline ping itsef.
+ assert!(baseline_ping.submit_sync(&glean, None));
+ assert!(baseline_ping.submit_sync(&glean, None));
+
+ // Check recording in the metrics ping
+ assert_eq!(
+ Some(1),
+ pings_submitted
+ .get("metrics")
+ .get_value(&glean, Some("metrics"))
+ );
+ assert_eq!(
+ Some(2),
+ pings_submitted
+ .get("baseline")
+ .get_value(&glean, Some("metrics"))
+ );
+
+ // Check recording in the baseline ping
+ assert_eq!(
+ None,
+ pings_submitted
+ .get("metrics")
+ .get_value(&glean, Some("baseline"))
+ );
+ assert_eq!(
+ Some(1),
+ pings_submitted
+ .get("baseline")
+ .get_value(&glean, Some("baseline"))
+ );
+}
+
+#[test]
+fn events_ping_with_metric_but_no_events_is_not_sent() {
+ let (mut glean, _t) = new_glean(None);
+
+ let events_ping = PingType::new("events", true, true, true, vec![]);
+ glean.register_ping_type(&events_ping);
+ let counter = CounterMetric::new(CommonMetricData {
+ name: "counter".into(),
+ category: "local".into(),
+ send_in_pings: vec!["events".into()],
+ ..Default::default()
+ });
+ counter.add_sync(&glean, 1);
+
+ // Sending this should fail.
+ assert!(!events_ping.submit_sync(&glean, None));
+ assert!(get_queued_pings(glean.get_data_path()).is_err());
+
+ let event = EventMetric::new(
+ CommonMetricData {
+ name: "name".into(),
+ category: "category".into(),
+ send_in_pings: vec!["events".into()],
+ ..Default::default()
+ },
+ vec![],
+ );
+ event.record_sync(&glean, 0, HashMap::new(), 0);
+
+ // Sending this should now succeed.
+ assert!(events_ping.submit_sync(&glean, None));
+ 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..29b6bccaca
--- /dev/null
+++ b/third_party/rust/glean-core/tests/ping_maker.rs
@@ -0,0 +1,301 @@
+// 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, true, 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_sync(&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 ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let ping_info = ping.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 ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let ping_info = ping.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 ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let client_info = ping.content["client_info"].as_object().unwrap();
+
+ client_info["telemetry_sdk_build"].as_str().unwrap();
+}
+
+#[test]
+fn test_metrics_must_report_experimentation_id() {
+ let (tempdir, _) = tempdir();
+ let mut glean = Glean::new(glean_core::InternalConfiguration {
+ data_path: tempdir.path().display().to_string(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ language_binding_name: "Rust".into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ app_build: "Unknown".into(),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ rate_limit: None,
+ enable_event_timestamps: false,
+ experimentation_id: Some("test-experimentation-id".to_string()),
+ })
+ .unwrap();
+ let ping_maker = PingMaker::new();
+ let ping_type = PingType::new("store1", true, false, true, 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_sync(&glean, true);
+
+ let ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let metrics = ping.content["metrics"].as_object().unwrap();
+ println!("TLDEBUG Metrics:\n{:?}", metrics);
+
+ let strings = metrics["string"].as_object().unwrap();
+ assert_eq!(
+ strings["glean.client.annotation.experimentation_id"]
+ .as_str()
+ .unwrap(),
+ "test-experimentation-id",
+ "experimentation ids must match"
+ );
+}
+
+#[test]
+fn experimentation_id_is_removed_if_send_if_empty_is_false() {
+ // Initialize Glean with an experimentation id, it should be removed if the ping is empty
+ // and send_if_empty is false.
+ let (tempdir, _) = tempdir();
+ let mut glean = Glean::new(glean_core::InternalConfiguration {
+ data_path: tempdir.path().display().to_string(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ language_binding_name: "Rust".into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ app_build: "Unknown".into(),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ rate_limit: None,
+ enable_event_timestamps: false,
+ experimentation_id: Some("test-experimentation-id".to_string()),
+ })
+ .unwrap();
+ let ping_maker = PingMaker::new();
+
+ let unknown_ping_type = PingType::new("unknown", true, false, true, vec![]);
+ glean.register_ping_type(&unknown_ping_type);
+
+ assert!(ping_maker
+ .collect(&glean, &unknown_ping_type, None, "", "")
+ .is_none());
+}
+
+#[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, true, 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_sync(&glean, true);
+
+ for i in 0..=1 {
+ for ping_name in ["store1", "store2"].iter() {
+ let ping_type = PingType::new(*ping_name, true, false, true, vec![]);
+ let ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let seq_num = ping.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, true, vec![]);
+
+ // 3rd ping of store1
+ let ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let seq_num = ping.content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(2, seq_num);
+
+ // 4th ping of store1
+ let ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let seq_num = ping.content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(3, seq_num);
+ }
+
+ {
+ let ping_type = PingType::new("store2", true, false, true, vec![]);
+
+ // 3rd ping of store2
+ let ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let seq_num = ping.content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(2, seq_num);
+ }
+
+ {
+ let ping_type = PingType::new("store1", true, false, true, vec![]);
+
+ // 5th ping of store1
+ let ping = ping_maker
+ .collect(&glean, &ping_type, None, "", "")
+ .unwrap();
+ let seq_num = ping.content["ping_info"]["seq"].as_i64().unwrap();
+ assert_eq!(4, seq_num);
+ }
+}
+
+#[test]
+fn clear_pending_pings() {
+ let (mut glean, _t) = new_glean(None);
+ let ping_maker = PingMaker::new();
+ let ping_type = PingType::new("store1", true, false, true, 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_sync(&glean, true);
+
+ assert!(ping_type.submit_sync(&glean, None));
+ 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, _t) = new_glean(None);
+ let ping_type = PingType::new("store1", true, true, true, vec![]);
+ glean.register_ping_type(&ping_type);
+
+ assert!(ping_type.submit_sync(&glean, None));
+ assert_eq!(1, get_queued_pings(glean.get_data_path()).unwrap().len());
+
+ // Disable upload, then try to sumbit
+ glean.set_upload_enabled(false);
+
+ // Test again through the direct call
+ assert!(!ping_type.submit_sync(&glean, None));
+ assert_eq!(0, get_queued_pings(glean.get_data_path()).unwrap().len());
+}
+
+#[test]
+fn metadata_is_correctly_added_when_necessary() {
+ let (mut glean, _t) = new_glean(None);
+ glean.set_debug_view_tag("valid-tag");
+ let ping_type = PingType::new("store1", true, true, true, vec![]);
+ glean.register_ping_type(&ping_type);
+
+ assert!(ping_type.submit_sync(&glean, None));
+
+ 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..b16c5fd992
--- /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_sync(&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, _t) = 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_sync(&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_sync(&glean, -1);
+ // Check that nothing was recorded
+ assert!(metric.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)
+ );
+}
diff --git a/third_party/rust/glean-core/tests/rate.rs b/third_party/rust/glean-core/tests/rate.rs
new file mode 100644
index 0000000000..f81e10cb53
--- /dev/null
+++ b/third_party/rust/glean-core/tests/rate.rs
@@ -0,0 +1,134 @@
+// 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;
+use glean_core::{test_get_num_recorded_errors, ErrorType};
+
+#[test]
+fn rate_smoke() {
+ let (glean, _t) = new_glean(None);
+
+ let metric: RateMetric = RateMetric::new(CommonMetricData {
+ name: "rate".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ });
+
+ // Adding 0 doesn't error.
+ metric.add_to_numerator_sync(&glean, 0);
+ metric.add_to_denominator_sync(&glean, 0);
+
+ assert!(test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).is_err());
+
+ // Adding a negative value errors.
+ metric.add_to_numerator_sync(&glean, -1);
+ metric.add_to_denominator_sync(&glean, -1);
+
+ assert_eq!(
+ Ok(2),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue),
+ );
+
+ // Getting the value returns 0s if that's all we have.
+ assert_eq!(metric.get_value(&glean, None), Some((0, 0).into()));
+
+ // And normal values of course work.
+ metric.add_to_numerator_sync(&glean, 22);
+ metric.add_to_denominator_sync(&glean, 7);
+
+ assert_eq!(metric.get_value(&glean, None), Some((22, 7).into()));
+}
+
+#[test]
+fn numerator_smoke() {
+ let (glean, _t) = new_glean(None);
+
+ let metric: NumeratorMetric = NumeratorMetric::new(CommonMetricData {
+ name: "rate".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ });
+
+ // Adding 0 doesn't error.
+ metric.add_to_numerator_sync(&glean, 0);
+
+ assert!(test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).is_err());
+
+ // Adding a negative value errors.
+ metric.add_to_numerator_sync(&glean, -1);
+
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue),
+ );
+
+ // Getting the value returns 0s if that's all we have.
+ let data = metric.get_value(&glean, None).unwrap();
+ assert_eq!(0, data.numerator);
+ assert_eq!(0, data.denominator);
+
+ // And normal values of course work.
+ metric.add_to_numerator_sync(&glean, 22);
+
+ let data = metric.get_value(&glean, None).unwrap();
+ assert_eq!(22, data.numerator);
+ assert_eq!(0, data.denominator);
+}
+
+#[test]
+fn denominator_smoke() {
+ let (glean, _t) = new_glean(None);
+
+ let meta1 = CommonMetricData {
+ name: "rate1".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ };
+
+ let meta2 = CommonMetricData {
+ name: "rate2".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ };
+
+ // This acts like a normal counter.
+ let denom: DenominatorMetric = DenominatorMetric::new(
+ CommonMetricData {
+ name: "counter".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ },
+ vec![meta1.clone(), meta2.clone()],
+ );
+
+ let num1 = NumeratorMetric::new(meta1);
+ let num2 = NumeratorMetric::new(meta2);
+
+ num1.add_to_numerator_sync(&glean, 3);
+ num2.add_to_numerator_sync(&glean, 5);
+
+ denom.add_sync(&glean, 7);
+
+ // no errors.
+ assert!(test_get_num_recorded_errors(&glean, num1.meta(), ErrorType::InvalidValue).is_err());
+ assert!(test_get_num_recorded_errors(&glean, num2.meta(), ErrorType::InvalidValue).is_err());
+
+ // Getting the value returns 0s if that's all we have.
+ let data = num1.get_value(&glean, None).unwrap();
+ assert_eq!(3, data.numerator);
+ assert_eq!(7, data.denominator);
+
+ let data = num2.get_value(&glean, None).unwrap();
+ assert_eq!(5, data.numerator);
+ assert_eq!(7, data.denominator);
+}
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..238fe6855c
--- /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_sync(&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_sync(&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_sync(&glean_clone.lock().unwrap(), 1);
+ c.wait();
+ threadsafe_metric_clone.add_sync(&glean_clone.lock().unwrap(), 1);
+ });
+
+ threadsafe_metric.add_sync(&glean.lock().unwrap(), 1);
+ barrier.wait();
+ threadsafe_metric.add_sync(&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..3ccfa8f494
--- /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_sync(&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, _t) = 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_sync(&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_sync(&glean, test_sting.clone());
+
+ // Check that data was truncated
+ assert_eq!(
+ test_sting[..100],
+ metric.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)
+ );
+}
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..7dd5acfbb5
--- /dev/null
+++ b/third_party/rust/glean-core/tests/string_list.rs
@@ -0,0 +1,245 @@
+// 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_sync(&glean, "first");
+ assert_eq!(list.get_value(&glean, "core").unwrap(), vec!["first"]);
+
+ list.add_sync(&glean, "second");
+ assert_eq!(
+ list.get_value(&glean, "core").unwrap(),
+ vec!["first", "second"]
+ );
+
+ list.set_sync(&glean, vec!["third".into()]);
+ assert_eq!(list.get_value(&glean, "core").unwrap(), vec!["third"]);
+
+ list.add_sync(&glean, "fourth");
+ assert_eq!(
+ list.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_sync(&glean, vec!["test_string_1".into(), "test_string_2".into()]);
+ }
+
+ {
+ let (glean, _t) = 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_sync(&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_sync(&glean, test_string.clone());
+
+ // Ensure the string was truncated to the proper length.
+ assert_eq!(
+ vec![test_string[..100].to_string()],
+ metric.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)
+ );
+
+ metric.set_sync(&glean, vec![test_string.clone()]);
+
+ // Ensure the string was truncated to the proper length.
+ assert_eq!(
+ vec![test_string[..100].to_string()],
+ metric.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)
+ );
+}
+
+#[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_sync(&glean, "test_string".repeat(20));
+
+ // Ensure the string was not added.
+ assert_eq!(None, metric.get_value(&glean, "store1"));
+
+ metric.set_sync(&glean, vec!["test_string_2".repeat(20)]);
+
+ // Ensure the stringlist was not set.
+ assert_eq!(None, metric.get_value(&glean, "store1"));
+
+ // Ensure no error was recorded.
+ assert!(test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).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..101 {
+ metric.add_sync(&glean, "test_string");
+ }
+
+ let expected: Vec<String> = "test_string "
+ .repeat(100)
+ .split_whitespace()
+ .map(|s| s.to_string())
+ .collect();
+ assert_eq!(expected, metric.get_value(&glean, "store1").unwrap());
+
+ // Ensure the 101st string wasn't added.
+ metric.add_sync(&glean, "test_string");
+ assert_eq!(expected, metric.get_value(&glean, "store1").unwrap());
+
+ // Ensure we recorded the error.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue)
+ );
+
+ // Try to set it to a list that's too long. Ensure it cuts off at 100 elements.
+ let too_many: Vec<String> = "test_string "
+ .repeat(101)
+ .split_whitespace()
+ .map(|s| s.to_string())
+ .collect();
+ metric.set_sync(&glean, too_many);
+ assert_eq!(expected, metric.get_value(&glean, "store1").unwrap());
+
+ assert_eq!(
+ Ok(2),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue)
+ );
+}
+
+#[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_sync(&glean, vec![]);
+
+ // Ensure the empty list was added
+ assert_eq!(Some(vec![]), metric.get_value(&glean, "store1"));
+
+ // Ensure we didn't record an error.
+ assert!(test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).is_err());
+}
diff --git a/third_party/rust/glean-core/tests/text.rs b/third_party/rust/glean-core/tests/text.rs
new file mode 100644
index 0000000000..2592e45899
--- /dev/null
+++ b/third_party/rust/glean-core/tests/text.rs
@@ -0,0 +1,115 @@
+// 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};
+
+#[test]
+fn text_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 = TextMetric::new(CommonMetricData {
+ name: "text_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::User,
+ ..Default::default()
+ });
+
+ metric.set_sync(&glean, "test_text_value");
+
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"text": {"telemetry.text_metric": "test_text_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, _t) = new_glean(Some(tempdir));
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), "store1", true)
+ .unwrap();
+ assert_eq!(
+ json!({"text": {"telemetry.text_metric": "test_text_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 = TextMetric::new(CommonMetricData {
+ name: "text_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: store_names.clone(),
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ metric.set_sync(&glean, "test_text_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!({"text": {"telemetry.text_metric": "test_text_value"}}),
+ snapshot
+ );
+ }
+}
+
+#[test]
+fn long_text_values_are_truncated() {
+ let (glean, _t) = new_glean(None);
+
+ let metric = TextMetric::new(CommonMetricData {
+ name: "text_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["store1".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ });
+
+ let test_sting = "01234567890".repeat(200 * 1024);
+ metric.set_sync(&glean, test_sting.clone());
+
+ // Check that data was truncated
+ assert_eq!(
+ test_sting[..(200 * 1024)],
+ metric.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)
+ );
+}
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..c964c450d4
--- /dev/null
+++ b/third_party/rust/glean-core/tests/timespan.rs
@@ -0,0 +1,351 @@
+// 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 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
+ .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, _t) = 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 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
+ .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 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).is_err());
+
+ let first_value = metric.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.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)
+ );
+}
+
+#[test]
+fn recorded_time_conforms_to_resolution() {
+ let (glean, _t) = new_glean(None);
+
+ let 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 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.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.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 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.get_value(&glean, "store1"));
+}
+
+#[test]
+fn nothing_stored_before_stop() {
+ 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 duration = 60;
+
+ metric.set_start(&glean, 0);
+
+ assert_eq!(None, metric.get_value(&glean, "store1"));
+
+ metric.set_stop(&glean, duration);
+ assert_eq!(duration, metric.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_sync(&glean, time);
+
+ let time_in_ns = time.as_nanos() as u64;
+ assert_eq!(Some(time_in_ns), metric.get_value(&glean, "store1"));
+}
+
+#[test]
+fn set_raw_time_does_nothing_when_timer_running() {
+ 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(42);
+
+ metric.set_start(&glean, 0);
+ metric.set_raw_sync(&glean, time);
+ metric.set_stop(&glean, 60);
+
+ // We expect the start/stop value, not the raw value.
+ assert_eq!(Some(60), metric.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)
+ );
+}
+
+#[test]
+fn timespan_is_not_tracked_across_upload_toggle() {
+ let (mut 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,
+ );
+
+ // 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.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)
+ );
+}
+
+#[test]
+fn time_cannot_go_backwards() {
+ let (glean, _t) = new_glean(None);
+
+ let 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.get_value(&glean, "test1").is_none());
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue),
+ );
+}
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..96f7fae5af
--- /dev/null
+++ b/third_party/rust/glean-core/tests/timing_distribution.rs
@@ -0,0 +1,431 @@
+// 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 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 = 4u64.into();
+ metric.set_start(id, 0);
+ metric.set_stop_and_accumulate(&glean, id, duration);
+
+ let snapshot = metric
+ .get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ assert_eq!(snapshot.count, 1);
+ assert_eq!(snapshot.sum, duration as i64);
+ }
+
+ // 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();
+
+ // We check the exact format to catch changes to the serialization.
+ let expected = json!({
+ "sum": duration,
+ "values": {
+ "58": 1,
+ "64": 0,
+ }
+ });
+ assert_eq!(
+ expected,
+ snapshot["timing_distribution"]["telemetry.distribution"]
+ );
+ }
+}
+
+#[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 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 = 4u64.into();
+ metric.set_start(id, 0);
+ metric.set_stop_and_accumulate(&glean, id, duration);
+
+ // We check the exact format to catch changes to the serialization.
+ let expected = json!({
+ "sum": 1,
+ "values": {
+ "1": 1,
+ "2": 0,
+ }
+ });
+ for store_name in store_names {
+ let snapshot = StorageManager
+ .snapshot_as_json(glean.storage(), &store_name, true)
+ .unwrap();
+
+ assert_eq!(
+ expected,
+ snapshot["timing_distribution"]["telemetry.distribution"]
+ );
+ }
+}
+
+#[test]
+fn timing_distributions_must_not_accumulate_negative_values() {
+ let (glean, _t) = new_glean(None);
+
+ let duration = 60;
+ let time_unit = TimeUnit::Nanosecond;
+
+ let 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 = 4u64.into();
+ metric.set_start(id, duration);
+ metric.set_stop_and_accumulate(&glean, id, 0);
+
+ assert!(metric.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)
+ );
+}
+
+#[test]
+fn the_accumulate_samples_api_correctly_stores_timing_values() {
+ let (glean, _t) = new_glean(None);
+
+ let 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_sync(&glean, [1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ let seconds_to_nanos = 1000 * 1000 * 1000;
+
+ // Check that we got the right sum.
+ assert_eq!(snapshot.sum, 6 * seconds_to_nanos);
+
+ // Check that we got the right number of samples.
+ assert_eq!(snapshot.count, 3);
+
+ // 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[&984625593]);
+ assert_eq!(1, snapshot.values[&1969251187]);
+ assert_eq!(1, snapshot.values[&2784941737]);
+
+ // No errors should be reported.
+ assert!(test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).is_err());
+}
+
+#[test]
+fn the_accumulate_samples_api_correctly_handles_negative_values() {
+ let (glean, _t) = new_glean(None);
+
+ let 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_sync(&glean, [-1, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum.
+ assert_eq!(snapshot.sum, 6);
+
+ // Check that we got the right number of samples.
+ assert_eq!(snapshot.count, 3);
+
+ // 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)
+ );
+}
+
+#[test]
+fn the_accumulate_samples_api_correctly_handles_overflowing_values() {
+ let (glean, _t) = new_glean(None);
+
+ let 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_sync(&glean, [overflowing_val, 1, 2, 3].to_vec());
+
+ let snapshot = metric
+ .get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Overflowing values are truncated to MAX_SAMPLE_TIME and recorded.
+ assert_eq!(snapshot.sum as u64, MAX_SAMPLE_TIME + 6);
+
+ // Check that we got the right number of samples.
+ assert_eq!(snapshot.count, 4);
+
+ // 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)
+ );
+}
+
+#[test]
+fn large_nanoseconds_values() {
+ let (glean, _t) = new_glean(None);
+
+ let 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 = 4u64.into();
+ metric.set_start(id, 0);
+ metric.set_stop_and_accumulate(&glean, id, time);
+
+ let val = metric
+ .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 as i64);
+}
+
+#[test]
+fn stopping_non_existing_id_records_an_error() {
+ let (glean, _t) = new_glean(None);
+
+ let 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,
+ );
+
+ let id = 3785u64.into();
+ metric.set_stop_and_accumulate(&glean, id, 60);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidState)
+ );
+}
+
+#[test]
+fn the_accumulate_raw_samples_api_correctly_stores_timing_values() {
+ let (glean, _t) = new_glean(None);
+
+ let 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,
+ );
+
+ let seconds_to_nanos = 1000 * 1000 * 1000;
+ metric.accumulate_raw_samples_nanos_sync(
+ &glean,
+ [seconds_to_nanos, 2 * seconds_to_nanos, 3 * seconds_to_nanos].as_ref(),
+ );
+
+ let snapshot = metric
+ .get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum.
+ assert_eq!(snapshot.sum, 6 * seconds_to_nanos as i64);
+
+ // Check that we got the right number of samples.
+ assert_eq!(snapshot.count, 3);
+
+ // 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[&984625593]);
+ assert_eq!(1, snapshot.values[&1969251187]);
+ assert_eq!(1, snapshot.values[&2784941737]);
+
+ // No errors should be reported.
+ assert!(test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidState).is_err());
+}
+
+#[test]
+fn raw_samples_api_error_cases() {
+ let (glean, _t) = new_glean(None);
+
+ let 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,
+ );
+
+ // 10minutes in nanoseconds
+ let max_sample_time = 1000 * 1000 * 1000 * 60 * 10;
+
+ metric.accumulate_raw_samples_nanos_sync(
+ &glean,
+ &[
+ 0, /* rounded up to 1 */
+ 1, /* valid */
+ max_sample_time + 1, /* larger then the maximum, will record an error and the maximum */
+ ],
+ );
+
+ let snapshot = metric
+ .get_value(&glean, "store1")
+ .expect("Value should be stored");
+
+ // Check that we got the right sum.
+ assert_eq!(snapshot.sum, 2 + max_sample_time as i64);
+
+ // Check that we got the right number of samples.
+ assert_eq!(snapshot.count, 3);
+
+ // 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, max_sample_time}`.
+ assert_eq!(2, snapshot.values[&1]);
+ assert_eq!(1, snapshot.values[&599512966122]);
+
+ // 1 error should be reported.
+ assert_eq!(
+ Ok(1),
+ test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow)
+ );
+}
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..c9d5015a72
--- /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_sync(&glean);
+ let snapshot = glean.snapshot("core", false);
+ assert!(
+ snapshot.contains(r#""local.uuid": ""#),
+ "Snapshot 1: {snapshot}"
+ );
+
+ uuid.generate_and_set_sync(&glean);
+ let snapshot = glean.snapshot("core", false);
+ assert!(
+ snapshot.contains(r#""local.uuid": ""#),
+ "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_from_uuid_sync(&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, _t) = 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_from_uuid_sync(&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
+ );
+ }
+}