summaryrefslogtreecommitdiffstats
path: root/third_party/rust/glean/src
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/glean/src')
-rw-r--r--third_party/rust/glean/src/common_test.rs50
-rw-r--r--third_party/rust/glean/src/configuration.rs187
-rw-r--r--third_party/rust/glean/src/core_metrics.rs45
-rw-r--r--third_party/rust/glean/src/lib.rs299
-rw-r--r--third_party/rust/glean/src/net/http_uploader.rs24
-rw-r--r--third_party/rust/glean/src/net/mod.rs238
-rw-r--r--third_party/rust/glean/src/private/event.rs221
-rw-r--r--third_party/rust/glean/src/private/mod.rs35
-rw-r--r--third_party/rust/glean/src/private/ping.rs88
-rw-r--r--third_party/rust/glean/src/system.rs106
-rw-r--r--third_party/rust/glean/src/test.rs1453
11 files changed, 2746 insertions, 0 deletions
diff --git a/third_party/rust/glean/src/common_test.rs b/third_party/rust/glean/src/common_test.rs
new file mode 100644
index 0000000000..fdb7cfadbf
--- /dev/null
+++ b/third_party/rust/glean/src/common_test.rs
@@ -0,0 +1,50 @@
+// 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 crate::ClientInfoMetrics;
+use crate::{Configuration, ConfigurationBuilder};
+use std::sync::{Mutex, MutexGuard};
+
+use once_cell::sync::Lazy;
+
+pub(crate) const GLOBAL_APPLICATION_ID: &str = "org.mozilla.rlb.test";
+
+// Because Glean uses a global-singleton, we need to run the tests one-by-one to
+// avoid different tests stomping over each other.
+// This is only an issue because we're resetting Glean, this cannot happen in normal
+// use of the RLB.
+//
+// We use a global lock to force synchronization of all tests, even if run multi-threaded.
+// This allows us to run without `--test-threads 1`.`
+pub(crate) fn lock_test() -> MutexGuard<'static, ()> {
+ static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
+
+ // This is going to be called from all the tests: make sure
+ // to enable logging.
+ env_logger::try_init().ok();
+
+ let lock = GLOBAL_LOCK.lock().unwrap();
+
+ lock
+}
+
+// Create 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(crate) fn new_glean(
+ configuration: Option<Configuration>,
+ clear_stores: bool,
+) -> tempfile::TempDir {
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = match configuration {
+ Some(c) => c,
+ None => ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ };
+
+ crate::test_reset_glean(cfg, ClientInfoMetrics::unknown(), clear_stores);
+ dir
+}
diff --git a/third_party/rust/glean/src/configuration.rs b/third_party/rust/glean/src/configuration.rs
new file mode 100644
index 0000000000..42360e96e0
--- /dev/null
+++ b/third_party/rust/glean/src/configuration.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/.
+
+use log::LevelFilter;
+
+use crate::net::PingUploader;
+
+use std::path::PathBuf;
+
+/// The default server pings are sent to.
+pub(crate) const DEFAULT_GLEAN_ENDPOINT: &str = "https://incoming.telemetry.mozilla.org";
+
+/// The Glean configuration.
+///
+/// Optional values will be filled in with default values.
+#[derive(Debug)]
+pub struct Configuration {
+ /// Whether upload should be enabled.
+ pub upload_enabled: bool,
+ /// Path to a directory to store all data in.
+ pub data_path: PathBuf,
+ /// The application ID (will be sanitized during initialization).
+ pub application_id: String,
+ /// The maximum number of events to store before sending a ping containing events.
+ pub max_events: Option<usize>,
+ /// Whether Glean should delay persistence of data from metrics with ping lifetime.
+ pub delay_ping_lifetime_io: bool,
+ /// The server pings are sent to.
+ pub server_endpoint: Option<String>,
+ /// The instance of the uploader used to send pings.
+ pub uploader: Option<Box<dyn PingUploader + 'static>>,
+ /// Whether Glean should schedule "metrics" pings for you.
+ pub use_core_mps: bool,
+ /// Whether Glean should limit its storage to only that of registered pings.
+ /// Unless you know that all your and your libraries' pings are appropriately registered
+ /// _before_ init, you shouldn't use this.
+ pub trim_data_to_registered_pings: bool,
+ /// The internal logging level.
+ pub log_level: Option<LevelFilter>,
+ /// The rate pings may be uploaded before they are throttled.
+ pub rate_limit: Option<crate::PingRateLimit>,
+ /// (Experimental) Whether to add a wallclock timestamp to all events.
+ pub enable_event_timestamps: bool,
+ /// An experimentation identifier derived by the application to be sent with all pings, it should
+ /// be noted that this has an underlying StringMetric and so should conform to the limitations that
+ /// StringMetric places on length, etc.
+ pub experimentation_id: Option<String>,
+}
+
+/// Configuration builder.
+///
+/// Let's you build a configuration from the required fields
+/// and let you set optional fields individually.
+#[derive(Debug)]
+pub struct Builder {
+ /// Required: Whether upload should be enabled.
+ pub upload_enabled: bool,
+ /// Required: Path to a directory to store all data in.
+ pub data_path: PathBuf,
+ /// Required: The application ID (will be sanitized during initialization).
+ pub application_id: String,
+ /// Optional: The maximum number of events to store before sending a ping containing events.
+ /// Default: `None`
+ pub max_events: Option<usize>,
+ /// Optional: Whether Glean should delay persistence of data from metrics with ping lifetime.
+ /// Default: `false`
+ pub delay_ping_lifetime_io: bool,
+ /// Optional: The server pings are sent to.
+ /// Default: `None`
+ pub server_endpoint: Option<String>,
+ /// Optional: The instance of the uploader used to send pings.
+ /// Default: `None`
+ pub uploader: Option<Box<dyn PingUploader + 'static>>,
+ /// Optional: Whether Glean should schedule "metrics" pings for you.
+ /// Default: `false`
+ pub use_core_mps: bool,
+ /// Optional: Whether Glean should limit its storage to only that of registered pings.
+ /// Unless you know that all your and your libraries' pings are appropriately registered
+ /// _before_ init, you shouldn't use this.
+ /// Default: `false`
+ pub trim_data_to_registered_pings: bool,
+ /// Optional: The internal logging level.
+ /// Default: `None`
+ pub log_level: Option<LevelFilter>,
+ /// Optional: The internal ping upload rate limit.
+ /// Default: `None`
+ pub rate_limit: Option<crate::PingRateLimit>,
+ /// (Experimental) Whether to add a wallclock timestamp to all events.
+ pub enable_event_timestamps: bool,
+ /// An experimentation identifier derived by the application to be sent with all pings, it should
+ /// be noted that this has an underlying StringMetric and so should conform to the limitations that
+ /// StringMetric places on length, etc.
+ pub experimentation_id: Option<String>,
+}
+
+impl Builder {
+ /// A new configuration builder.
+ pub fn new<P: Into<PathBuf>, S: Into<String>>(
+ upload_enabled: bool,
+ data_path: P,
+ application_id: S,
+ ) -> Self {
+ Self {
+ upload_enabled,
+ data_path: data_path.into(),
+ application_id: application_id.into(),
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: None,
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ rate_limit: None,
+ enable_event_timestamps: false,
+ experimentation_id: None,
+ }
+ }
+
+ /// Generate the full configuration.
+ pub fn build(self) -> Configuration {
+ Configuration {
+ upload_enabled: self.upload_enabled,
+ data_path: self.data_path,
+ application_id: self.application_id,
+ max_events: self.max_events,
+ delay_ping_lifetime_io: self.delay_ping_lifetime_io,
+ server_endpoint: self.server_endpoint,
+ uploader: self.uploader,
+ use_core_mps: self.use_core_mps,
+ trim_data_to_registered_pings: self.trim_data_to_registered_pings,
+ log_level: self.log_level,
+ rate_limit: self.rate_limit,
+ enable_event_timestamps: self.enable_event_timestamps,
+ experimentation_id: self.experimentation_id,
+ }
+ }
+
+ /// Set the maximum number of events to store before sending a ping containing events.
+ pub fn with_max_events(mut self, max_events: usize) -> Self {
+ self.max_events = Some(max_events);
+ self
+ }
+
+ /// Set whether Glean should delay persistence of data from metrics with ping lifetime.
+ pub fn with_delay_ping_lifetime_io(mut self, value: bool) -> Self {
+ self.delay_ping_lifetime_io = value;
+ self
+ }
+
+ /// Set the server pings are sent to.
+ pub fn with_server_endpoint<S: Into<String>>(mut self, server_endpoint: S) -> Self {
+ self.server_endpoint = Some(server_endpoint.into());
+ self
+ }
+
+ /// Set the instance of the uploader used to send pings.
+ pub fn with_uploader<U: PingUploader + 'static>(mut self, uploader: U) -> Self {
+ self.uploader = Some(Box::new(uploader));
+ self
+ }
+
+ /// Set whether Glean should schedule "metrics" pings for you.
+ pub fn with_use_core_mps(mut self, value: bool) -> Self {
+ self.use_core_mps = value;
+ self
+ }
+
+ /// Set whether Glean should limit its storage to only that of registered pings.
+ pub fn with_trim_data_to_registered_pings(mut self, value: bool) -> Self {
+ self.trim_data_to_registered_pings = value;
+ self
+ }
+
+ /// Set whether to add a wallclock timestamp to all events (experimental).
+ pub fn with_event_timestamps(mut self, value: bool) -> Self {
+ self.enable_event_timestamps = value;
+ self
+ }
+
+ /// Set whether to add a wallclock timestamp to all events (experimental).
+ pub fn with_experimentation_id(mut self, value: String) -> Self {
+ self.experimentation_id = Some(value);
+ self
+ }
+}
diff --git a/third_party/rust/glean/src/core_metrics.rs b/third_party/rust/glean/src/core_metrics.rs
new file mode 100644
index 0000000000..70c11fea87
--- /dev/null
+++ b/third_party/rust/glean/src/core_metrics.rs
@@ -0,0 +1,45 @@
+// 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 crate::system;
+
+/// Metrics included in every ping as `client_info`.
+#[derive(Debug)]
+pub struct ClientInfoMetrics {
+ /// The build identifier generated by the CI system (e.g. "1234/A").
+ pub app_build: String,
+ /// The user visible version string (e.g. "1.0.3").
+ pub app_display_version: String,
+ /// The product-provided release channel (e.g. "beta").
+ pub channel: Option<String>,
+ /// The locale of the application during initialization (e.g. "es-ES").
+ pub locale: Option<String>,
+}
+
+impl ClientInfoMetrics {
+ /// Creates the client info with dummy values for all.
+ pub fn unknown() -> Self {
+ ClientInfoMetrics {
+ app_build: "Unknown".to_string(),
+ app_display_version: "Unknown".to_string(),
+ channel: None,
+ locale: None,
+ }
+ }
+}
+
+impl From<ClientInfoMetrics> for glean_core::ClientInfoMetrics {
+ fn from(metrics: ClientInfoMetrics) -> Self {
+ glean_core::ClientInfoMetrics {
+ app_build: metrics.app_build,
+ app_display_version: metrics.app_display_version,
+ channel: metrics.channel,
+ locale: metrics.locale,
+ os_version: system::get_os_version(),
+ windows_build_number: system::get_windows_build_number(),
+ architecture: system::ARCH.to_string(),
+ ..Default::default()
+ }
+ }
+}
diff --git a/third_party/rust/glean/src/lib.rs b/third_party/rust/glean/src/lib.rs
new file mode 100644
index 0000000000..538b8c590d
--- /dev/null
+++ b/third_party/rust/glean/src/lib.rs
@@ -0,0 +1,299 @@
+// 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(clippy::uninlined_format_args)]
+#![deny(rustdoc::broken_intra_doc_links)]
+#![deny(missing_docs)]
+
+//! Glean is a modern approach for recording and sending Telemetry data.
+//!
+//! It's in use at Mozilla.
+//!
+//! All documentation can be found online:
+//!
+//! ## [The Glean SDK Book](https://mozilla.github.io/glean)
+//!
+//! ## Example
+//!
+//! Initialize Glean, register a ping and then send it.
+//!
+//! ```rust,no_run
+//! # use glean::{ConfigurationBuilder, ClientInfoMetrics, Error, private::*};
+//! let cfg = ConfigurationBuilder::new(true, "/tmp/data", "org.mozilla.glean_core.example").build();
+//! glean::initialize(cfg, ClientInfoMetrics::unknown());
+//!
+//! let prototype_ping = PingType::new("prototype", true, true, true, vec!());
+//!
+//! prototype_ping.submit(None);
+//! ```
+
+use std::collections::HashMap;
+use std::path::Path;
+
+use configuration::DEFAULT_GLEAN_ENDPOINT;
+pub use configuration::{Builder as ConfigurationBuilder, Configuration};
+pub use core_metrics::ClientInfoMetrics;
+pub use glean_core::{
+ metrics::{Datetime, DistributionData, MemoryUnit, Rate, RecordedEvent, TimeUnit, TimerId},
+ traits, CommonMetricData, Error, ErrorType, Glean, HistogramType, Lifetime, PingRateLimit,
+ RecordedExperiment, Result,
+};
+
+mod configuration;
+mod core_metrics;
+pub mod net;
+pub mod private;
+mod system;
+
+#[cfg(test)]
+mod common_test;
+
+const LANGUAGE_BINDING_NAME: &str = "Rust";
+
+/// Creates and initializes a new Glean object.
+///
+/// See [`glean_core::Glean::new`] for more information.
+///
+/// # Arguments
+///
+/// * `cfg` - the [`Configuration`] options to initialize with.
+/// * `client_info` - the [`ClientInfoMetrics`] values used to set Glean
+/// core metrics.
+pub fn initialize(cfg: Configuration, client_info: ClientInfoMetrics) {
+ initialize_internal(cfg, client_info);
+}
+
+struct GleanEvents {
+ /// An instance of the upload manager
+ upload_manager: net::UploadManager,
+}
+
+impl glean_core::OnGleanEvents for GleanEvents {
+ fn initialize_finished(&self) {
+ // intentionally left empty
+ }
+
+ fn trigger_upload(&self) -> Result<(), glean_core::CallbackError> {
+ self.upload_manager.trigger_upload();
+ Ok(())
+ }
+
+ fn start_metrics_ping_scheduler(&self) -> bool {
+ // We rely on the glean-core MPS.
+ // We always trigger an upload as it might have submitted a ping.
+ true
+ }
+
+ fn cancel_uploads(&self) -> Result<(), glean_core::CallbackError> {
+ // intentionally left empty
+ Ok(())
+ }
+
+ fn shutdown(&self) -> Result<(), glean_core::CallbackError> {
+ self.upload_manager.shutdown();
+ Ok(())
+ }
+}
+
+fn initialize_internal(cfg: Configuration, client_info: ClientInfoMetrics) -> Option<()> {
+ // Initialize the ping uploader.
+ let upload_manager = net::UploadManager::new(
+ cfg.server_endpoint
+ .unwrap_or_else(|| DEFAULT_GLEAN_ENDPOINT.to_string()),
+ cfg.uploader
+ .unwrap_or_else(|| Box::new(net::HttpUploader) as Box<dyn net::PingUploader>),
+ );
+
+ // Now make this the global object available to others.
+ let callbacks = Box::new(GleanEvents { upload_manager });
+
+ let core_cfg = glean_core::InternalConfiguration {
+ upload_enabled: cfg.upload_enabled,
+ data_path: cfg.data_path.display().to_string(),
+ application_id: cfg.application_id.clone(),
+ language_binding_name: LANGUAGE_BINDING_NAME.into(),
+ max_events: cfg.max_events.map(|m| m as u32),
+ delay_ping_lifetime_io: cfg.delay_ping_lifetime_io,
+ app_build: client_info.app_build.clone(),
+ use_core_mps: cfg.use_core_mps,
+ trim_data_to_registered_pings: cfg.trim_data_to_registered_pings,
+ log_level: cfg.log_level,
+ rate_limit: cfg.rate_limit,
+ enable_event_timestamps: cfg.enable_event_timestamps,
+ experimentation_id: cfg.experimentation_id,
+ };
+
+ glean_core::glean_initialize(core_cfg, client_info.into(), callbacks);
+ Some(())
+}
+
+/// Shuts down Glean in an orderly fashion.
+pub fn shutdown() {
+ glean_core::shutdown()
+}
+
+/// Sets whether upload is enabled or not.
+///
+/// See [`glean_core::Glean::set_upload_enabled`].
+pub fn set_upload_enabled(enabled: bool) {
+ glean_core::glean_set_upload_enabled(enabled)
+}
+
+/// Collects and submits a ping for eventual uploading by name.
+///
+/// Note that this needs to be public in order for RLB consumers to
+/// use Glean debugging facilities.
+///
+/// See [`glean_core::Glean.submit_ping_by_name`].
+pub fn submit_ping_by_name(ping: &str, reason: Option<&str>) {
+ let ping = ping.to_string();
+ let reason = reason.map(|s| s.to_string());
+ glean_core::glean_submit_ping_by_name(ping, reason)
+}
+
+/// Indicate that an experiment is running. Glean will then add an
+/// experiment annotation to the environment which is sent with pings. This
+/// infomration is not persisted between runs.
+///
+/// See [`glean_core::Glean::set_experiment_active`].
+pub fn set_experiment_active(
+ experiment_id: String,
+ branch: String,
+ extra: Option<HashMap<String, String>>,
+) {
+ glean_core::glean_set_experiment_active(experiment_id, branch, extra.unwrap_or_default())
+}
+
+/// Indicate that an experiment is no longer running.
+///
+/// See [`glean_core::Glean::set_experiment_inactive`].
+pub fn set_experiment_inactive(experiment_id: String) {
+ glean_core::glean_set_experiment_inactive(experiment_id)
+}
+
+/// Dynamically set the experimentation identifier, as opposed to setting it through the configuration
+/// during initialization.
+pub fn set_experimentation_id(experimentation_id: String) {
+ glean_core::glean_set_experimentation_id(experimentation_id);
+}
+
+/// TEST ONLY FUNCTION.
+/// Gets stored experimentation id.
+pub fn test_get_experimentation_id() -> Option<String> {
+ glean_core::glean_test_get_experimentation_id()
+}
+
+/// Set the remote configuration values for the metrics' disabled property
+///
+/// See [`glean_core::Glean::set_metrics_enabled_config`].
+pub fn glean_set_metrics_enabled_config(json: String) {
+ glean_core::glean_set_metrics_enabled_config(json)
+}
+
+/// Performs the collection/cleanup operations required by becoming active.
+///
+/// This functions generates a baseline ping with reason `active`
+/// and then sets the dirty bit.
+/// This should be called whenever the consuming product becomes active (e.g.
+/// getting to foreground).
+pub fn handle_client_active() {
+ glean_core::glean_handle_client_active()
+}
+
+/// Performs the collection/cleanup operations required by becoming inactive.
+///
+/// This functions generates a baseline and an events ping with reason
+/// `inactive` and then clears the dirty bit.
+/// This should be called whenever the consuming product becomes inactive (e.g.
+/// getting to background).
+pub fn handle_client_inactive() {
+ glean_core::glean_handle_client_inactive()
+}
+
+/// TEST ONLY FUNCTION.
+/// Checks if an experiment is currently active.
+pub fn test_is_experiment_active(experiment_id: String) -> bool {
+ glean_core::glean_test_get_experiment_data(experiment_id).is_some()
+}
+
+/// TEST ONLY FUNCTION.
+/// Returns the [`RecordedExperiment`] for the given `experiment_id` or panics if
+/// the id isn't found.
+pub fn test_get_experiment_data(experiment_id: String) -> Option<RecordedExperiment> {
+ glean_core::glean_test_get_experiment_data(experiment_id)
+}
+
+/// Destroy the global Glean state.
+pub(crate) fn destroy_glean(clear_stores: bool, data_path: &Path) {
+ let data_path = data_path.display().to_string();
+ glean_core::glean_test_destroy_glean(clear_stores, Some(data_path))
+}
+
+/// TEST ONLY FUNCTION.
+/// Resets the Glean state and triggers init again.
+pub fn test_reset_glean(cfg: Configuration, client_info: ClientInfoMetrics, clear_stores: bool) {
+ destroy_glean(clear_stores, &cfg.data_path);
+ initialize_internal(cfg, client_info);
+ glean_core::join_init();
+}
+
+/// Sets a debug view tag.
+///
+/// When the debug view tag is set, pings are sent with a `X-Debug-ID` header with the
+/// value of the tag and are sent to the ["Ping Debug Viewer"](https://mozilla.github.io/glean/book/dev/core/internal/debug-pings.html).
+///
+/// # Arguments
+///
+/// * `tag` - A valid HTTP header value. Must match the regex: "[a-zA-Z0-9-]{1,20}".
+///
+/// # Returns
+///
+/// This will return `false` in case `tag` is not a valid tag and `true` otherwise.
+/// If called before Glean is initialized it will always return `true`.
+pub fn set_debug_view_tag(tag: &str) -> bool {
+ glean_core::glean_set_debug_view_tag(tag.to_string())
+}
+
+/// Sets the log pings debug option.
+///
+/// When the log pings debug option is `true`,
+/// we log the payload of all succesfully assembled pings.
+///
+/// # Arguments
+///
+/// * `value` - The value of the log pings option
+pub fn set_log_pings(value: bool) {
+ glean_core::glean_set_log_pings(value)
+}
+
+/// Sets source tags.
+///
+/// Overrides any existing source tags.
+/// Source tags will show in the destination datasets, after ingestion.
+///
+/// **Note** If one or more tags are invalid, all tags are ignored.
+///
+/// # Arguments
+///
+/// * `tags` - A vector of at most 5 valid HTTP header values. Individual
+/// tags must match the regex: "[a-zA-Z0-9-]{1,20}".
+pub fn set_source_tags(tags: Vec<String>) {
+ glean_core::glean_set_source_tags(tags);
+}
+
+/// Returns a timestamp corresponding to "now" with millisecond precision.
+pub fn get_timestamp_ms() -> u64 {
+ glean_core::get_timestamp_ms()
+}
+
+/// Asks the database to persist ping-lifetime data to disk. Probably expensive to call.
+/// Only has effect when Glean is configured with `delay_ping_lifetime_io: true`.
+/// If Glean hasn't been initialized this will dispatch and return Ok(()),
+/// otherwise it will block until the persist is done and return its Result.
+pub fn persist_ping_lifetime_data() {
+ glean_core::persist_ping_lifetime_data();
+}
+
+#[cfg(test)]
+mod test;
diff --git a/third_party/rust/glean/src/net/http_uploader.rs b/third_party/rust/glean/src/net/http_uploader.rs
new file mode 100644
index 0000000000..4646fe61b4
--- /dev/null
+++ b/third_party/rust/glean/src/net/http_uploader.rs
@@ -0,0 +1,24 @@
+// 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 crate::net::{PingUploader, UploadResult};
+
+/// A simple mechanism to upload pings over HTTPS.
+#[derive(Debug)]
+pub struct HttpUploader;
+
+impl PingUploader for HttpUploader {
+ /// Uploads a ping to a server.
+ ///
+ /// # Arguments
+ ///
+ /// * `url` - the URL path to upload the data to.
+ /// * `body` - the serialized text data to send.
+ /// * `headers` - a vector of tuples containing the headers to send with
+ /// the request, i.e. (Name, Value).
+ fn upload(&self, url: String, _body: Vec<u8>, _headers: Vec<(String, String)>) -> UploadResult {
+ log::debug!("TODO bug 1675468: submitting to {:?}", url);
+ UploadResult::http_status(200)
+ }
+}
diff --git a/third_party/rust/glean/src/net/mod.rs b/third_party/rust/glean/src/net/mod.rs
new file mode 100644
index 0000000000..5571d30e67
--- /dev/null
+++ b/third_party/rust/glean/src/net/mod.rs
@@ -0,0 +1,238 @@
+// 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/.
+
+//! Handling the Glean upload logic.
+//!
+//! This doesn't perform the actual upload but rather handles
+//! retries, upload limitations and error tracking.
+
+use std::sync::{atomic::Ordering, Arc, Mutex};
+use std::thread::{self, JoinHandle};
+use std::time::Duration;
+
+use glean_core::upload::PingUploadTask;
+pub use glean_core::upload::{PingRequest, UploadResult, UploadTaskAction};
+
+pub use http_uploader::*;
+use thread_state::{AtomicState, State};
+
+mod http_uploader;
+
+/// A description of a component used to upload pings.
+pub trait PingUploader: std::fmt::Debug + Send + Sync {
+ /// Uploads a ping to a server.
+ ///
+ /// # Arguments
+ ///
+ /// * `url` - the URL path to upload the data to.
+ /// * `body` - the serialized text data to send.
+ /// * `headers` - a vector of tuples containing the headers to send with
+ /// the request, i.e. (Name, Value).
+ fn upload(&self, url: String, body: Vec<u8>, headers: Vec<(String, String)>) -> UploadResult;
+}
+
+/// The logic for uploading pings: this leaves the actual upload mechanism as
+/// a detail of the user-provided object implementing [`PingUploader`].
+#[derive(Debug)]
+pub(crate) struct UploadManager {
+ inner: Arc<Inner>,
+}
+
+#[derive(Debug)]
+struct Inner {
+ server_endpoint: String,
+ uploader: Box<dyn PingUploader + 'static>,
+ thread_running: AtomicState,
+ handle: Mutex<Option<JoinHandle<()>>>,
+}
+
+impl UploadManager {
+ /// Create a new instance of the upload manager.
+ ///
+ /// # Arguments
+ ///
+ /// * `server_endpoint` - the server pings are sent to.
+ /// * `new_uploader` - the instance of the uploader used to send pings.
+ pub(crate) fn new(
+ server_endpoint: String,
+ new_uploader: Box<dyn PingUploader + 'static>,
+ ) -> Self {
+ Self {
+ inner: Arc::new(Inner {
+ server_endpoint,
+ uploader: new_uploader,
+ thread_running: AtomicState::new(State::Stopped),
+ handle: Mutex::new(None),
+ }),
+ }
+ }
+
+ /// Signals Glean to upload pings at the next best opportunity.
+ pub(crate) fn trigger_upload(&self) {
+ // If no other upload proces is running, we're the one starting it.
+ // Need atomic compare/exchange to avoid any further races
+ // or we can end up with 2+ uploader threads.
+ if self
+ .inner
+ .thread_running
+ .compare_exchange(
+ State::Stopped,
+ State::Running,
+ Ordering::SeqCst,
+ Ordering::SeqCst,
+ )
+ .is_err()
+ {
+ return;
+ }
+
+ let inner = Arc::clone(&self.inner);
+
+ // Need to lock before we start so that noone thinks we're not running.
+ let mut handle = self.inner.handle.lock().unwrap();
+ let thread = thread::Builder::new()
+ .name("glean.upload".into())
+ .spawn(move || {
+ log::trace!("Started glean.upload thread");
+ loop {
+ let incoming_task = glean_core::glean_get_upload_task();
+
+ match incoming_task {
+ PingUploadTask::Upload { request } => {
+ log::trace!("Received upload task with request {:?}", request);
+ let doc_id = request.document_id.clone();
+ let upload_url = format!("{}{}", inner.server_endpoint, request.path);
+ let headers: Vec<(String, String)> =
+ request.headers.into_iter().collect();
+ let result = inner.uploader.upload(upload_url, request.body, headers);
+ // Process the upload response.
+ match glean_core::glean_process_ping_upload_response(doc_id, result) {
+ UploadTaskAction::Next => (),
+ UploadTaskAction::End => break,
+ }
+
+ let status = inner.thread_running.load(Ordering::SeqCst);
+ // asked to shut down. let's do it.
+ if status == State::ShuttingDown {
+ break;
+ }
+ }
+ PingUploadTask::Wait { time } => {
+ log::trace!("Instructed to wait for {:?}ms", time);
+ thread::sleep(Duration::from_millis(time));
+ }
+ PingUploadTask::Done { .. } => {
+ log::trace!("Received PingUploadTask::Done. Exiting.");
+ // Nothing to do here, break out of the loop.
+ break;
+ }
+ }
+ }
+
+ // Clear the running flag to signal that this thread is done,
+ // but only if there's no shutdown thread.
+ let _ = inner.thread_running.compare_exchange(
+ State::Running,
+ State::Stopped,
+ Ordering::SeqCst,
+ Ordering::SeqCst,
+ );
+ });
+
+ match thread {
+ Ok(thread) => *handle = Some(thread),
+ Err(err) => {
+ log::warn!("Failed to spawn Glean's uploader thread. This will be retried on next upload. Error: {err}");
+ // Swapping back the thread state. We're the ones setting it to `Running`, so
+ // should be able to set it back.
+ let state_change = self.inner.thread_running.compare_exchange(
+ State::Running,
+ State::Stopped,
+ Ordering::SeqCst,
+ Ordering::SeqCst,
+ );
+
+ if state_change.is_err() {
+ log::warn!("Failed to swap back thread state. Someone else jumped in and changed the state.");
+ }
+ }
+ };
+ }
+
+ pub(crate) fn shutdown(&self) {
+ // mark as shutting down.
+ self.inner
+ .thread_running
+ .store(State::ShuttingDown, Ordering::SeqCst);
+
+ // take the thread handle out.
+ let mut handle = self.inner.handle.lock().unwrap();
+ let thread = handle.take();
+
+ if let Some(thread) = thread {
+ thread
+ .join()
+ .expect("couldn't join on the uploader thread.");
+ }
+ }
+}
+
+mod thread_state {
+ use std::sync::atomic::{AtomicU8, Ordering};
+
+ #[derive(Debug, PartialEq)]
+ #[repr(u8)]
+ pub enum State {
+ Stopped = 0,
+ Running = 1,
+ ShuttingDown = 2,
+ }
+
+ #[derive(Debug)]
+ pub struct AtomicState(AtomicU8);
+
+ impl AtomicState {
+ const fn to_u8(val: State) -> u8 {
+ val as u8
+ }
+
+ fn from_u8(val: u8) -> State {
+ #![allow(non_upper_case_globals)]
+ const U8_Stopped: u8 = State::Stopped as u8;
+ const U8_Running: u8 = State::Running as u8;
+ const U8_ShuttingDown: u8 = State::ShuttingDown as u8;
+ match val {
+ U8_Stopped => State::Stopped,
+ U8_Running => State::Running,
+ U8_ShuttingDown => State::ShuttingDown,
+ _ => panic!("Invalid enum discriminant"),
+ }
+ }
+
+ pub const fn new(v: State) -> AtomicState {
+ AtomicState(AtomicU8::new(Self::to_u8(v)))
+ }
+
+ pub fn load(&self, order: Ordering) -> State {
+ Self::from_u8(self.0.load(order))
+ }
+
+ pub fn store(&self, val: State, order: Ordering) {
+ self.0.store(Self::to_u8(val), order)
+ }
+
+ pub fn compare_exchange(
+ &self,
+ current: State,
+ new: State,
+ success: Ordering,
+ failure: Ordering,
+ ) -> Result<State, State> {
+ self.0
+ .compare_exchange(Self::to_u8(current), Self::to_u8(new), success, failure)
+ .map(Self::from_u8)
+ .map_err(Self::from_u8)
+ }
+ }
+}
diff --git a/third_party/rust/glean/src/private/event.rs b/third_party/rust/glean/src/private/event.rs
new file mode 100644
index 0000000000..8b1abca84f
--- /dev/null
+++ b/third_party/rust/glean/src/private/event.rs
@@ -0,0 +1,221 @@
+// 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 inherent::inherent;
+use std::{collections::HashMap, marker::PhantomData};
+
+use glean_core::traits;
+
+use crate::{ErrorType, RecordedEvent};
+
+// We need to wrap the glean-core type: otherwise if we try to implement
+// the trait for the metric in `glean_core::metrics` we hit error[E0117]:
+// only traits defined in the current crate can be implemented for arbitrary
+// types.
+
+/// Developer-facing API for recording event metrics.
+///
+/// Instances of this class type are automatically generated by the parsers
+/// at build time, allowing developers to record values that were previously
+/// registered in the metrics.yaml file.
+#[derive(Clone)]
+pub struct EventMetric<K> {
+ pub(crate) inner: glean_core::metrics::EventMetric,
+ extra_keys: PhantomData<K>,
+}
+
+impl<K: traits::ExtraKeys> EventMetric<K> {
+ /// The public constructor used by automatically generated metrics.
+ pub fn new(meta: glean_core::CommonMetricData) -> Self {
+ let allowed_extra_keys = K::ALLOWED_KEYS.iter().map(|s| s.to_string()).collect();
+ let inner = glean_core::metrics::EventMetric::new(meta, allowed_extra_keys);
+ Self {
+ inner,
+ extra_keys: PhantomData,
+ }
+ }
+
+ /// The public constructor used by runtime-defined metrics.
+ pub fn with_runtime_extra_keys(
+ meta: glean_core::CommonMetricData,
+ allowed_extra_keys: Vec<String>,
+ ) -> Self {
+ let inner = glean_core::metrics::EventMetric::new(meta, allowed_extra_keys);
+ Self {
+ inner,
+ extra_keys: PhantomData,
+ }
+ }
+
+ /// Record a new event with a provided timestamp.
+ ///
+ /// It's the caller's responsibility to ensure the timestamp comes from the same clock source.
+ /// Use [`glean::get_timestamp_ms`](crate::get_timestamp_ms) to get a valid timestamp.
+ pub fn record_with_time(&self, timestamp: u64, extra: HashMap<String, String>) {
+ self.inner.record_with_time(timestamp, extra);
+ }
+}
+
+#[inherent]
+impl<K: traits::ExtraKeys> traits::Event for EventMetric<K> {
+ type Extra = K;
+
+ pub fn record<M: Into<Option<<Self as traits::Event>::Extra>>>(&self, extra: M) {
+ let extra = extra
+ .into()
+ .map(|e| e.into_ffi_extra())
+ .unwrap_or_else(HashMap::new);
+ self.inner.record(extra);
+ }
+
+ pub fn test_get_value<'a, S: Into<Option<&'a str>>>(
+ &self,
+ ping_name: S,
+ ) -> Option<Vec<RecordedEvent>> {
+ let ping_name = ping_name.into().map(|s| s.to_string());
+ self.inner.test_get_value(ping_name)
+ }
+
+ pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
+ self.inner.test_get_num_recorded_errors(error)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::common_test::{lock_test, new_glean};
+ use crate::CommonMetricData;
+
+ #[test]
+ fn no_extra_keys() {
+ let _lock = lock_test();
+ let _t = new_glean(None, true);
+
+ let metric: EventMetric<traits::NoExtraKeys> = EventMetric::new(CommonMetricData {
+ name: "event".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ });
+
+ metric.record(None);
+ metric.record(None);
+
+ let data = metric.test_get_value(None).expect("no event recorded");
+ assert_eq!(2, data.len());
+ assert!(data[0].timestamp <= data[1].timestamp);
+ }
+
+ #[test]
+ fn with_extra_keys() {
+ let _lock = lock_test();
+ let _t = new_glean(None, true);
+
+ #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)]
+ struct SomeExtra {
+ key1: Option<String>,
+ key2: Option<String>,
+ }
+
+ impl glean_core::traits::ExtraKeys for SomeExtra {
+ const ALLOWED_KEYS: &'static [&'static str] = &["key1", "key2"];
+
+ fn into_ffi_extra(self) -> HashMap<String, String> {
+ let mut map = HashMap::new();
+ self.key1.and_then(|key1| map.insert("key1".into(), key1));
+ self.key2.and_then(|key2| map.insert("key2".into(), key2));
+ map
+ }
+ }
+
+ let metric: EventMetric<SomeExtra> = EventMetric::new(CommonMetricData {
+ name: "event".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ });
+
+ let map1 = SomeExtra {
+ key1: Some("1".into()),
+ ..Default::default()
+ };
+ metric.record(map1);
+
+ let map2 = SomeExtra {
+ key1: Some("1".into()),
+ key2: Some("2".into()),
+ };
+ metric.record(map2);
+
+ metric.record(None);
+
+ let data = metric.test_get_value(None).expect("no event recorded");
+ assert_eq!(3, data.len());
+ assert!(data[0].timestamp <= data[1].timestamp);
+ assert!(data[1].timestamp <= data[2].timestamp);
+
+ let mut map = HashMap::new();
+ map.insert("key1".into(), "1".into());
+ assert_eq!(Some(map), data[0].extra);
+
+ let mut map = HashMap::new();
+ map.insert("key1".into(), "1".into());
+ map.insert("key2".into(), "2".into());
+ assert_eq!(Some(map), data[1].extra);
+
+ assert_eq!(None, data[2].extra);
+ }
+
+ #[test]
+ fn with_runtime_extra_keys() {
+ let _lock = lock_test();
+ let _t = new_glean(None, true);
+
+ #[derive(Default, Debug, Clone, Hash, Eq, PartialEq)]
+ struct RuntimeExtra {}
+
+ impl glean_core::traits::ExtraKeys for RuntimeExtra {
+ const ALLOWED_KEYS: &'static [&'static str] = &[];
+
+ fn into_ffi_extra(self) -> HashMap<String, String> {
+ HashMap::new()
+ }
+ }
+
+ let metric: EventMetric<RuntimeExtra> = EventMetric::with_runtime_extra_keys(
+ CommonMetricData {
+ name: "event".into(),
+ category: "test".into(),
+ send_in_pings: vec!["test1".into()],
+ ..Default::default()
+ },
+ vec!["key1".into(), "key2".into()],
+ );
+
+ let map1 = HashMap::from([("key1".into(), "1".into())]);
+ metric.record_with_time(0, map1);
+
+ let map2 = HashMap::from([("key1".into(), "1".into()), ("key2".into(), "2".into())]);
+ metric.record_with_time(1, map2);
+
+ metric.record_with_time(2, HashMap::new());
+
+ let data = metric.test_get_value(None).expect("no event recorded");
+ assert_eq!(3, data.len());
+ assert!(data[0].timestamp <= data[1].timestamp);
+ assert!(data[1].timestamp <= data[2].timestamp);
+
+ let mut map = HashMap::new();
+ map.insert("key1".into(), "1".into());
+ assert_eq!(Some(map), data[0].extra);
+
+ let mut map = HashMap::new();
+ map.insert("key1".into(), "1".into());
+ map.insert("key2".into(), "2".into());
+ assert_eq!(Some(map), data[1].extra);
+
+ assert_eq!(None, data[2].extra);
+ }
+}
diff --git a/third_party/rust/glean/src/private/mod.rs b/third_party/rust/glean/src/private/mod.rs
new file mode 100644
index 0000000000..8a5c304193
--- /dev/null
+++ b/third_party/rust/glean/src/private/mod.rs
@@ -0,0 +1,35 @@
+// 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/.
+
+//! The different metric types supported by the Glean SDK to handle data.
+
+mod event;
+mod ping;
+
+pub use event::EventMetric;
+pub use glean_core::BooleanMetric;
+pub use glean_core::CounterMetric;
+pub use glean_core::CustomDistributionMetric;
+pub use glean_core::DenominatorMetric;
+pub use glean_core::MemoryDistributionMetric;
+pub use glean_core::NumeratorMetric;
+pub use glean_core::QuantityMetric;
+pub use glean_core::RateMetric;
+pub use glean_core::RecordedExperiment;
+pub use glean_core::StringListMetric;
+pub use glean_core::StringMetric;
+pub use glean_core::TextMetric;
+pub use glean_core::TimespanMetric;
+pub use glean_core::TimingDistributionMetric;
+pub use glean_core::UrlMetric;
+pub use glean_core::UuidMetric;
+pub use glean_core::{AllowLabeled, LabeledMetric};
+pub use glean_core::{Datetime, DatetimeMetric};
+pub use ping::PingType;
+
+// Re-export types that are used by the glean_parser-generated code.
+#[doc(hidden)]
+pub mod __export {
+ pub use once_cell::sync::Lazy;
+}
diff --git a/third_party/rust/glean/src/private/ping.rs b/third_party/rust/glean/src/private/ping.rs
new file mode 100644
index 0000000000..c9c68a10a2
--- /dev/null
+++ b/third_party/rust/glean/src/private/ping.rs
@@ -0,0 +1,88 @@
+// 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::sync::{Arc, Mutex};
+
+type BoxedCallback = Box<dyn FnOnce(Option<&str>) + Send + 'static>;
+
+/// A ping is a bundle of related metrics, gathered in a payload to be transmitted.
+///
+/// The ping payload will be encoded in JSON format and contains shared information data.
+#[derive(Clone)]
+pub struct PingType {
+ pub(crate) inner: glean_core::metrics::PingType,
+
+ /// **Test-only API**
+ ///
+ /// A function to be called right before a ping is submitted.
+ test_callback: Arc<Mutex<Option<BoxedCallback>>>,
+}
+
+impl PingType {
+ /// Creates a new ping type.
+ ///
+ /// # Arguments
+ ///
+ /// * `name` - The name of the ping.
+ /// * `include_client_id` - Whether to include the client ID in the assembled ping when.
+ /// * `send_if_empty` - Whether the ping should be sent empty or not.
+ /// * `reason_codes` - The valid reason codes for this ping.
+ pub fn new<A: Into<String>>(
+ name: A,
+ include_client_id: bool,
+ send_if_empty: bool,
+ precise_timestamps: bool,
+ reason_codes: Vec<String>,
+ ) -> Self {
+ let inner = glean_core::metrics::PingType::new(
+ name.into(),
+ include_client_id,
+ send_if_empty,
+ precise_timestamps,
+ reason_codes,
+ );
+
+ Self {
+ inner,
+ test_callback: Arc::new(Default::default()),
+ }
+ }
+
+ /// Submits the ping for eventual uploading.
+ ///
+ /// The ping content is assembled as soon as possible, but upload is not
+ /// guaranteed to happen immediately, as that depends on the upload policies.
+ ///
+ /// If the ping currently contains no content, it will not be sent,
+ /// unless it is configured to be sent if empty.
+ ///
+ /// # Arguments
+ ///
+ /// * `reason` - the reason the ping was triggered. Included in the
+ /// `ping_info.reason` part of the payload.
+ pub fn submit(&self, reason: Option<&str>) {
+ let mut cb = self.test_callback.lock().unwrap();
+ let cb = cb.take();
+ if let Some(cb) = cb {
+ cb(reason)
+ }
+
+ self.inner.submit(reason.map(|s| s.to_string()))
+ }
+
+ /// **Test-only API**
+ ///
+ /// Attach a callback to be called right before a new ping is submitted.
+ /// The provided function is called exactly once before submitting a ping.
+ ///
+ /// Note: The callback will be called on any call to submit.
+ /// A ping might not be sent afterwards, e.g. if the ping is otherwise empty (and
+ /// `send_if_empty` is `false`).
+ pub fn test_before_next_submit(&self, cb: impl FnOnce(Option<&str>) + Send + 'static) {
+ let mut test_callback = self.test_callback.lock().unwrap();
+
+ let cb = Box::new(cb);
+ *test_callback = Some(cb);
+ }
+}
diff --git a/third_party/rust/glean/src/system.rs b/third_party/rust/glean/src/system.rs
new file mode 100644
index 0000000000..4816f2552a
--- /dev/null
+++ b/third_party/rust/glean/src/system.rs
@@ -0,0 +1,106 @@
+// Copyright (c) 2017 The Rust Project Developers
+// Copyright (c) 2018-2020 The Rust Secure Code Working Group
+// Licensed under the MIT License.
+// Original license:
+// https://github.com/rustsec/rustsec/blob/2a080f173ad9d8ac7fa260f0a3a6aebf0000de06/platforms/LICENSE-MIT
+//
+// Permission is hereby granted, free of charge, to any
+// person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the
+// Software without restriction, including without
+// limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice
+// shall be included in all copies or substantial portions
+// of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+//! Detect and expose `target_arch` as a constant
+
+#[cfg(target_arch = "aarch64")]
+/// `target_arch` when building this crate: `aarch64`
+pub const ARCH: &str = "aarch64";
+
+#[cfg(target_arch = "arm")]
+/// `target_arch` when building this crate: `arm`
+pub const ARCH: &str = "arm";
+
+#[cfg(target_arch = "x86")]
+/// `target_arch` when building this crate: `x86`
+pub const ARCH: &str = "x86";
+
+#[cfg(target_arch = "x86_64")]
+/// `target_arch` when building this crate: `x86_64`
+pub const ARCH: &str = "x86_64";
+
+#[cfg(not(any(
+ target_arch = "aarch64",
+ target_arch = "arm",
+ target_arch = "x86",
+ target_arch = "x86_64"
+)))]
+/// `target_arch` when building this crate: unknown!
+pub const ARCH: &str = "Unknown";
+
+#[cfg(any(target_os = "macos", target_os = "windows"))]
+/// Returns Darwin kernel version for MacOS, or NT Kernel version for Windows
+pub fn get_os_version() -> String {
+ whatsys::kernel_version().unwrap_or_else(|| "Unknown".to_owned())
+}
+
+#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
+/// Returns "Unknown" for platforms other than Linux, MacOS or Windows
+pub fn get_os_version() -> String {
+ "Unknown".to_owned()
+}
+
+#[cfg(target_os = "linux")]
+/// Returns Linux kernel version, in the format of <Major>.<Minor> e.g. 5.8
+pub fn get_os_version() -> String {
+ parse_linux_os_string(whatsys::kernel_version().unwrap_or_else(|| "Unknown".to_owned()))
+}
+
+#[cfg(target_os = "windows")]
+/// Returns the Windows build number, e.g. 22000
+pub fn get_windows_build_number() -> Option<i64> {
+ match whatsys::windows_build_number() {
+ // Cast to i64 to work with QuantityMetric type
+ Some(i) => Some(i as i64),
+ _ => None,
+ }
+}
+
+#[cfg(not(target_os = "windows"))]
+/// Returns None, for non-Windows operating systems
+pub fn get_windows_build_number() -> Option<i64> {
+ None
+}
+
+#[cfg(target_os = "linux")]
+fn parse_linux_os_string(os_str: String) -> String {
+ os_str.split('.').take(2).collect::<Vec<&str>>().join(".")
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn parse_fixed_linux_os_string() {
+ let alpine_os_string = "4.12.0-rc6-g48ec1f0-dirty".to_owned();
+ assert_eq!(parse_linux_os_string(alpine_os_string), "4.12");
+ let centos_os_string = "3.10.0-514.16.1.el7.x86_64".to_owned();
+ assert_eq!(parse_linux_os_string(centos_os_string), "3.10");
+ let ubuntu_os_string = "5.8.0-44-generic".to_owned();
+ assert_eq!(parse_linux_os_string(ubuntu_os_string), "5.8");
+}
diff --git a/third_party/rust/glean/src/test.rs b/third_party/rust/glean/src/test.rs
new file mode 100644
index 0000000000..cb41b49b66
--- /dev/null
+++ b/third_party/rust/glean/src/test.rs
@@ -0,0 +1,1453 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+use std::io::Read;
+use std::sync::{Arc, Barrier, Mutex};
+use std::thread::{self, ThreadId};
+use std::time::{Duration, Instant};
+
+use crossbeam_channel::RecvTimeoutError;
+use flate2::read::GzDecoder;
+use glean_core::glean_test_get_experimentation_id;
+use serde_json::Value as JsonValue;
+
+use crate::private::PingType;
+use crate::private::{BooleanMetric, CounterMetric, EventMetric, StringMetric, TextMetric};
+
+use super::*;
+use crate::common_test::{lock_test, new_glean, GLOBAL_APPLICATION_ID};
+
+#[test]
+fn send_a_ping() {
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Define a new ping and submit it.
+ const PING_NAME: &str = "test-ping";
+ let custom_ping = private::PingType::new(PING_NAME, true, true, true, vec![]);
+ custom_ping.submit(None);
+
+ // Wait for the ping to arrive.
+ let url = r.recv().unwrap();
+ assert!(url.contains(PING_NAME));
+}
+
+#[test]
+fn disabling_upload_disables_metrics_recording() {
+ let _lock = lock_test();
+
+ let _t = new_glean(None, true);
+
+ let metric = BooleanMetric::new(CommonMetricData {
+ name: "bool_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ crate::set_upload_enabled(false);
+
+ assert!(metric.test_get_value(Some("store1".into())).is_none())
+}
+
+#[test]
+fn test_experiments_recording() {
+ let _lock = lock_test();
+
+ let _t = new_glean(None, true);
+
+ set_experiment_active("experiment_test".to_string(), "branch_a".to_string(), None);
+ let mut extra = HashMap::new();
+ extra.insert("test_key".to_string(), "value".to_string());
+ set_experiment_active(
+ "experiment_api".to_string(),
+ "branch_b".to_string(),
+ Some(extra),
+ );
+ assert!(test_is_experiment_active("experiment_test".to_string()));
+ assert!(test_is_experiment_active("experiment_api".to_string()));
+ set_experiment_inactive("experiment_test".to_string());
+ assert!(!test_is_experiment_active("experiment_test".to_string()));
+ assert!(test_is_experiment_active("experiment_api".to_string()));
+ let stored_data = test_get_experiment_data("experiment_api".to_string()).unwrap();
+ assert_eq!("branch_b", stored_data.branch);
+ assert_eq!("value", stored_data.extra.unwrap()["test_key"]);
+}
+
+#[test]
+fn test_experiments_recording_before_glean_inits() {
+ let _lock = lock_test();
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ destroy_glean(true, &tmpname);
+
+ set_experiment_active(
+ "experiment_set_preinit".to_string(),
+ "branch_a".to_string(),
+ None,
+ );
+ set_experiment_active(
+ "experiment_preinit_disabled".to_string(),
+ "branch_a".to_string(),
+ None,
+ );
+ set_experiment_inactive("experiment_preinit_disabled".to_string());
+
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ assert!(test_is_experiment_active(
+ "experiment_set_preinit".to_string()
+ ));
+ assert!(!test_is_experiment_active(
+ "experiment_preinit_disabled".to_string()
+ ));
+}
+
+#[test]
+fn test_experimentation_id_recording() {
+ let _lock = lock_test();
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ destroy_glean(true, &tmpname);
+
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_experimentation_id("alpha-beta-gamma-delta".to_string())
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ let exp_id = glean_test_get_experimentation_id().expect("Experimentation id must not be None");
+ assert_eq!(
+ "alpha-beta-gamma-delta".to_string(),
+ exp_id,
+ "Experimentation id must match"
+ );
+}
+
+#[test]
+fn sending_of_foreground_background_pings() {
+ let _lock = lock_test();
+
+ let click: EventMetric<traits::NoExtraKeys> = private::EventMetric::new(CommonMetricData {
+ name: "click".into(),
+ category: "ui".into(),
+ send_in_pings: vec!["events".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ });
+
+ // Define a fake uploader that reports back the submission headers
+ // using a crossbeam channel.
+ let (s, r) = crossbeam_channel::bounded::<String>(3);
+
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Simulate becoming active.
+ handle_client_active();
+
+ // We expect a baseline ping to be generated here (reason: 'active').
+ let url = r.recv().unwrap();
+ assert!(url.contains("baseline"));
+
+ // Recording an event so that an "events" ping will contain data.
+ click.record(None);
+
+ // Simulate becoming inactive
+ handle_client_inactive();
+
+ // Wait for the pings to arrive.
+ let mut expected_pings = vec!["baseline", "events"];
+ for _ in 0..2 {
+ let url = r.recv().unwrap();
+ // If the url contains the expected reason, remove it from the list.
+ expected_pings.retain(|&name| !url.contains(name));
+ }
+ // We received all the expected pings.
+ assert_eq!(0, expected_pings.len());
+
+ // Simulate becoming active again.
+ handle_client_active();
+
+ // We expect a baseline ping to be generated here (reason: 'active').
+ let url = r.recv().unwrap();
+ assert!(url.contains("baseline"));
+}
+
+#[test]
+fn sending_of_startup_baseline_ping() {
+ let _lock = lock_test();
+
+ // Create an instance of Glean and then flip the dirty
+ // bit to true.
+ let data_dir = new_glean(None, true);
+
+ glean_core::glean_set_dirty_flag(true);
+
+ // Restart glean and wait for a baseline ping to be generated.
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let tmpname = data_dir.path().to_path_buf();
+
+ // Now reset Glean: it should still send a baseline ping with reason
+ // dirty_startup when starting, because of the dirty bit being set.
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ // Wait for the ping to arrive.
+ let url = r.recv().unwrap();
+ assert!(url.contains("baseline"));
+}
+
+#[test]
+fn no_dirty_baseline_on_clean_shutdowns() {
+ let _lock = lock_test();
+
+ // Create an instance of Glean, wait for init and then flip the dirty
+ // bit to true.
+ let data_dir = new_glean(None, true);
+
+ glean_core::glean_set_dirty_flag(true);
+
+ crate::shutdown();
+
+ // Restart glean and wait for a baseline ping to be generated.
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let tmpname = data_dir.path().to_path_buf();
+
+ // Now reset Glean: it should not send a baseline ping, because
+ // we cleared the dirty bit.
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ // We don't expect a startup ping.
+ assert_eq!(r.try_recv(), Err(crossbeam_channel::TryRecvError::Empty));
+}
+
+#[test]
+fn initialize_must_not_crash_if_data_dir_is_messed_up() {
+ let _lock = lock_test();
+
+ let dir = tempfile::tempdir().unwrap();
+ let tmpdirname = dir.path();
+ // Create a file in the temporary dir and use that as the
+ // name of the Glean data dir.
+ let file_path = tmpdirname.to_path_buf().join("notadir");
+ std::fs::write(file_path.clone(), "test").expect("The test Glean dir file must be created");
+
+ let cfg = ConfigurationBuilder::new(true, file_path, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build();
+
+ test_reset_glean(cfg, ClientInfoMetrics::unknown(), false);
+
+ // We don't need to sleep here.
+ // The `test_reset_glean` already waited on the initialize task.
+}
+
+#[test]
+fn queued_recorded_metrics_correctly_record_during_init() {
+ let _lock = lock_test();
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ destroy_glean(true, &tmpname);
+
+ let metric = CounterMetric::new(CommonMetricData {
+ name: "counter_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["store1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ // This will queue 3 tasks that will add to the metric value once Glean is initialized
+ for _ in 0..3 {
+ metric.add(1);
+ }
+
+ // TODO: To be fixed in bug 1677150.
+ // Ensure that no value has been stored yet since the tasks have only been queued
+ // and not executed yet
+
+ // Calling `new_glean` here will cause Glean to be initialized and should cause the queued
+ // tasks recording metrics to execute
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build();
+ let _t = new_glean(Some(cfg), false);
+
+ // Verify that the callback was executed by testing for the correct value
+ assert!(metric.test_get_value(None).is_some(), "Value must exist");
+ assert_eq!(3, metric.test_get_value(None).unwrap(), "Value must match");
+}
+
+#[test]
+fn initializing_twice_is_a_noop() {
+ let _lock = lock_test();
+
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ ClientInfoMetrics::unknown(),
+ true,
+ );
+
+ // Glean was initialized and it waited for a full initialization to finish.
+ // We now just want to try to initialize again.
+ // This will bail out early.
+
+ crate::initialize(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ ClientInfoMetrics::unknown(),
+ );
+
+ // We don't need to sleep here.
+ // The `test_reset_glean` already waited on the initialize task,
+ // and the 2nd initialize will bail out early.
+ //
+ // All we tested is that this didn't crash.
+}
+
+#[test]
+fn dont_handle_events_when_uninitialized() {
+ let _lock = lock_test();
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ ClientInfoMetrics::unknown(),
+ true,
+ );
+
+ // Ensure there's at least one event recorded,
+ // otherwise the ping is not sent.
+ let click: EventMetric<traits::NoExtraKeys> = private::EventMetric::new(CommonMetricData {
+ name: "click".into(),
+ category: "ui".into(),
+ send_in_pings: vec!["events".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ });
+ click.record(None);
+ // Wait for the dispatcher.
+ assert_ne!(None, click.test_get_value(None));
+
+ // Now destroy Glean. We test submission when not initialized.
+ destroy_glean(false, &tmpname);
+
+ // We reach into `glean_core` to test this,
+ // only there we can synchronously submit and get a return value.
+ assert!(!glean_core::glean_submit_ping_by_name_sync(
+ "events".to_string(),
+ None
+ ));
+}
+
+// TODO: Should probably move into glean-core.
+#[test]
+fn the_app_channel_must_be_correctly_set_if_requested() {
+ let _lock = lock_test();
+
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ // Internal metric, replicated here for testing.
+ let app_channel = StringMetric::new(CommonMetricData {
+ name: "app_channel".into(),
+ category: "".into(),
+ send_in_pings: vec!["glean_client_info".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ });
+
+ // No app_channel reported.
+ let client_info = ClientInfoMetrics {
+ channel: None,
+ ..ClientInfoMetrics::unknown()
+ };
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ client_info,
+ true,
+ );
+ assert!(app_channel.test_get_value(None).is_none());
+
+ // Custom app_channel reported.
+ let client_info = ClientInfoMetrics {
+ channel: Some("testing".into()),
+ ..ClientInfoMetrics::unknown()
+ };
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ client_info,
+ true,
+ );
+ assert_eq!("testing", app_channel.test_get_value(None).unwrap());
+}
+
+#[test]
+fn ping_collection_must_happen_after_concurrently_scheduled_metrics_recordings() {
+ // Given the following block of code:
+ //
+ // Metric.A.set("SomeTestValue")
+ // Glean.submitPings(listOf("custom-ping-1"))
+ //
+ // This test ensures that "custom-ping-1" contains "metric.a" with a value of "SomeTestValue"
+ // when the ping is collected.
+
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<(String, JsonValue)>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ // Decode the gzipped body.
+ let mut gzip_decoder = GzDecoder::new(&body[..]);
+ let mut s = String::with_capacity(body.len());
+
+ let data = gzip_decoder
+ .read_to_string(&mut s)
+ .ok()
+ .map(|_| &s[..])
+ .or_else(|| std::str::from_utf8(&body).ok())
+ .and_then(|payload| serde_json::from_str(payload).ok())
+ .unwrap();
+ self.sender.send((url, data)).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build(),
+ ClientInfoMetrics::unknown(),
+ true,
+ );
+
+ let ping_name = "custom_ping_1";
+ let ping = private::PingType::new(ping_name, true, false, true, vec![]);
+ let metric = private::StringMetric::new(CommonMetricData {
+ name: "string_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec![ping_name.into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ });
+
+ let test_value = "SomeTestValue";
+ metric.set(test_value.to_string());
+ ping.submit(None);
+
+ // Wait for the ping to arrive.
+ let (url, body) = r.recv().unwrap();
+ assert!(url.contains(ping_name));
+
+ assert_eq!(
+ test_value,
+ body["metrics"]["string"]["telemetry.string_metric"]
+ );
+}
+
+#[test]
+fn basic_metrics_should_be_cleared_when_disabling_uploading() {
+ let _lock = lock_test();
+
+ let _t = new_glean(None, false);
+
+ let metric = private::StringMetric::new(CommonMetricData {
+ name: "string_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["default".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ });
+
+ assert!(metric.test_get_value(None).is_none());
+
+ metric.set("TEST VALUE".into());
+ assert!(metric.test_get_value(None).is_some());
+
+ set_upload_enabled(false);
+ assert!(metric.test_get_value(None).is_none());
+ metric.set("TEST VALUE".into());
+ assert!(metric.test_get_value(None).is_none());
+
+ set_upload_enabled(true);
+ assert!(metric.test_get_value(None).is_none());
+ metric.set("TEST VALUE".into());
+ assert_eq!("TEST VALUE", metric.test_get_value(None).unwrap());
+}
+
+// TODO: Should probably move into glean-core.
+#[test]
+fn core_metrics_should_be_cleared_and_restored_when_disabling_and_enabling_uploading() {
+ let _lock = lock_test();
+
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ // No app_channel reported.
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ ClientInfoMetrics::unknown(),
+ true,
+ );
+
+ // Internal metric, replicated here for testing.
+ let os_version = StringMetric::new(CommonMetricData {
+ name: "os_version".into(),
+ category: "".into(),
+ send_in_pings: vec!["glean_client_info".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ });
+
+ assert!(os_version.test_get_value(None).is_some());
+
+ set_upload_enabled(false);
+ assert!(os_version.test_get_value(None).is_none());
+
+ set_upload_enabled(true);
+ assert!(os_version.test_get_value(None).is_some());
+}
+
+#[test]
+fn sending_deletion_ping_if_disabled_outside_of_run() {
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Now reset Glean and disable upload: it should still send a deletion request
+ // ping even though we're just starting.
+ test_reset_glean(
+ ConfigurationBuilder::new(false, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ // Wait for the ping to arrive.
+ let url = r.recv().unwrap();
+ assert!(url.contains("deletion-request"));
+}
+
+#[test]
+fn no_sending_of_deletion_ping_if_unchanged_outside_of_run() {
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Now reset Glean and keep upload enabled: no deletion-request
+ // should be sent.
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ assert_eq!(0, r.len());
+}
+
+#[test]
+fn deletion_request_ping_contains_experimentation_id() {
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded::<JsonValue>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<JsonValue>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ _url: String,
+ body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ let mut gzip_decoder = GzDecoder::new(&body[..]);
+ let mut body_str = String::with_capacity(body.len());
+ let data: JsonValue = gzip_decoder
+ .read_to_string(&mut body_str)
+ .ok()
+ .map(|_| &body_str[..])
+ .or_else(|| std::str::from_utf8(&body).ok())
+ .and_then(|payload| serde_json::from_str(payload).ok())
+ .unwrap();
+ self.sender.send(data).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_experimentation_id("alpha-beta-gamma-delta".to_string())
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Now reset Glean and disable upload: it should still send a deletion request
+ // ping even though we're just starting.
+ test_reset_glean(
+ ConfigurationBuilder::new(false, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .with_experimentation_id("alpha-beta-gamma-delta".to_string())
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ // Wait for the ping to arrive and check the experimentation id matches
+ let url = r.recv().unwrap();
+ let metrics = url.get("metrics").unwrap();
+ let strings = metrics.get("string").unwrap();
+ assert_eq!(
+ "alpha-beta-gamma-delta",
+ strings
+ .get("glean.client.annotation.experimentation_id")
+ .unwrap()
+ );
+}
+
+#[test]
+fn test_sending_of_startup_baseline_ping_with_application_lifetime_metric() {
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<(String, JsonValue)>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ // Decode the gzipped body.
+ let mut gzip_decoder = GzDecoder::new(&body[..]);
+ let mut s = String::with_capacity(body.len());
+
+ let data = gzip_decoder
+ .read_to_string(&mut s)
+ .ok()
+ .map(|_| &s[..])
+ .or_else(|| std::str::from_utf8(&body).ok())
+ .and_then(|payload| serde_json::from_str(payload).ok())
+ .unwrap();
+ self.sender.send((url, data)).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname.clone(), GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .build(),
+ ClientInfoMetrics::unknown(),
+ true,
+ );
+
+ // Reaching into the core.
+ glean_core::glean_set_dirty_flag(true);
+
+ let metric = private::StringMetric::new(CommonMetricData {
+ name: "app_lifetime".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["baseline".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ ..Default::default()
+ });
+ let test_value = "HELLOOOOO!";
+ metric.set(test_value.into());
+ assert_eq!(test_value, metric.test_get_value(None).unwrap());
+
+ // Restart glean and don't clear the stores.
+ test_reset_glean(
+ ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build(),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ let (url, body) = r.recv().unwrap();
+ assert!(url.contains("/baseline/"));
+
+ // We set the dirty bit above.
+ assert_eq!("dirty_startup", body["ping_info"]["reason"]);
+ assert_eq!(
+ test_value,
+ body["metrics"]["string"]["telemetry.app_lifetime"]
+ );
+}
+
+#[test]
+fn setting_debug_view_tag_before_initialization_should_not_crash() {
+ let _lock = lock_test();
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ destroy_glean(true, &tmpname);
+
+ // Define a fake uploader that reports back the submission headers
+ // using a crossbeam channel.
+ let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1);
+
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<Vec<(String, String)>>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ _url: String,
+ _body: Vec<u8>,
+ headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(headers).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Attempt to set a debug view tag before Glean is initialized.
+ set_debug_view_tag("valid-tag");
+
+ // Create a custom configuration to use a fake uploader.
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Submit a baseline ping.
+ submit_ping_by_name("baseline", Some("inactive"));
+
+ // Wait for the ping to arrive.
+ let headers = r.recv().unwrap();
+ assert_eq!(
+ "valid-tag",
+ headers.iter().find(|&kv| kv.0 == "X-Debug-ID").unwrap().1
+ );
+}
+
+#[test]
+fn setting_source_tags_before_initialization_should_not_crash() {
+ let _lock = lock_test();
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ destroy_glean(true, &tmpname);
+ //assert!(!was_initialize_called());
+
+ // Define a fake uploader that reports back the submission headers
+ // using a crossbeam channel.
+ let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1);
+
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<Vec<(String, String)>>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ _url: String,
+ _body: Vec<u8>,
+ headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(headers).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Attempt to set source tags before Glean is initialized.
+ set_source_tags(vec!["valid-tag1".to_string(), "valid-tag2".to_string()]);
+
+ // Create a custom configuration to use a fake uploader.
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Submit a baseline ping.
+ submit_ping_by_name("baseline", Some("inactive"));
+
+ // Wait for the ping to arrive.
+ let headers = r.recv().unwrap();
+ assert_eq!(
+ "valid-tag1,valid-tag2",
+ headers
+ .iter()
+ .find(|&kv| kv.0 == "X-Source-Tags")
+ .unwrap()
+ .1
+ );
+}
+
+#[test]
+fn setting_source_tags_after_initialization_should_not_crash() {
+ let _lock = lock_test();
+
+ // Define a fake uploader that reports back the submission headers
+ // using a crossbeam channel.
+ let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1);
+
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<Vec<(String, String)>>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ _url: String,
+ _body: Vec<u8>,
+ headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(headers).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Attempt to set source tags after `Glean.initialize` is called,
+ // but before Glean is fully initialized.
+ //assert!(was_initialize_called());
+ set_source_tags(vec!["valid-tag1".to_string(), "valid-tag2".to_string()]);
+
+ // Submit a baseline ping.
+ submit_ping_by_name("baseline", Some("inactive"));
+
+ // Wait for the ping to arrive.
+ let headers = r.recv().unwrap();
+ assert_eq!(
+ "valid-tag1,valid-tag2",
+ headers
+ .iter()
+ .find(|&kv| kv.0 == "X-Source-Tags")
+ .unwrap()
+ .1
+ );
+}
+
+#[test]
+fn flipping_upload_enabled_respects_order_of_events() {
+ // NOTES(janerik):
+ // I'm reasonably sure this test is excercising the right code paths
+ // and from the log output it does the right thing:
+ //
+ // * It fully initializes with the assumption uploadEnabled=true
+ // * It then disables upload
+ // * Then it submits the custom ping, which rightfully is ignored because uploadEnabled=false.
+ //
+ // The test passes.
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ // We create a ping and a metric before we initialize Glean
+ let sample_ping = PingType::new("sample-ping-1", true, false, true, vec![]);
+ let metric = private::StringMetric::new(CommonMetricData {
+ name: "string_metric".into(),
+ category: "telemetry".into(),
+ send_in_pings: vec!["sample-ping-1".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ });
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Glean might still be initializing. Disable upload.
+ set_upload_enabled(false);
+
+ // Set data and try to submit a custom ping.
+ metric.set("some-test-value".into());
+ sample_ping.submit(None);
+
+ // Wait for the ping to arrive.
+ let url = r.recv().unwrap();
+ assert!(url.contains("deletion-request"));
+}
+
+#[test]
+fn registering_pings_before_init_must_work() {
+ let _lock = lock_test();
+
+ // Define a fake uploader that reports back the submission headers
+ // using a crossbeam channel.
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom ping and attempt its registration.
+ let sample_ping = PingType::new("pre-register", true, true, true, vec![]);
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Submit a baseline ping.
+ sample_ping.submit(None);
+
+ // Wait for the ping to arrive.
+ let url = r.recv().unwrap();
+ assert!(url.contains("pre-register"));
+}
+
+#[test]
+fn test_a_ping_before_submission() {
+ let _lock = lock_test();
+
+ // Define a fake uploader that reports back the submission headers
+ // using a crossbeam channel.
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader { sender: s })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Create a custom ping and register it.
+ let sample_ping = PingType::new("custom1", true, true, true, vec![]);
+
+ let metric = CounterMetric::new(CommonMetricData {
+ name: "counter_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["custom1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: None,
+ });
+
+ metric.add(1);
+
+ sample_ping.test_before_next_submit(move |reason| {
+ assert_eq!(None, reason);
+ assert_eq!(1, metric.test_get_value(None).unwrap());
+ });
+
+ // Submit a baseline ping.
+ sample_ping.submit(None);
+
+ // Wait for the ping to arrive.
+ let url = r.recv().unwrap();
+ assert!(url.contains("custom1"));
+}
+
+#[test]
+fn test_boolean_get_num_errors() {
+ let _lock = lock_test();
+
+ let _t = new_glean(None, false);
+
+ let metric = BooleanMetric::new(CommonMetricData {
+ name: "counter_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["custom1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: Some(str::to_string("asdf")),
+ });
+
+ // Check specifically for an invalid label
+ let result = metric.test_get_num_recorded_errors(ErrorType::InvalidLabel);
+
+ assert_eq!(result, 0);
+}
+
+#[test]
+fn test_text_can_hold_long_string() {
+ let _lock = lock_test();
+
+ let _t = new_glean(None, false);
+
+ let metric = TextMetric::new(CommonMetricData {
+ name: "text_metric".into(),
+ category: "test".into(),
+ send_in_pings: vec!["custom1".into()],
+ lifetime: Lifetime::Application,
+ disabled: false,
+ dynamic_label: Some(str::to_string("text")),
+ });
+
+ // 216 characters, which would overflow StringMetric
+ metric.set("I've seen things you people wouldn't believe. Attack ships on fire off the shoulder of Orion. I watched C-beams glitter in the dark near the Tannhäuser Gate. All those moments will be lost in time, like tears in rain".into());
+
+ let result = metric.test_get_num_recorded_errors(ErrorType::InvalidValue);
+ assert_eq!(result, 0);
+
+ let result = metric.test_get_num_recorded_errors(ErrorType::InvalidOverflow);
+ assert_eq!(result, 0);
+}
+
+#[test]
+fn signaling_done() {
+ let _lock = lock_test();
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ barrier: Arc<Barrier>,
+ counter: Arc<Mutex<HashMap<ThreadId, u32>>>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ _url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ let mut map = self.counter.lock().unwrap();
+ *map.entry(thread::current().id()).or_insert(0) += 1;
+
+ // Wait for the sync.
+ self.barrier.wait();
+
+ // Signal that this uploader thread is done.
+ net::UploadResult::done()
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ // We use a barrier to sync this test thread with the uploader thread.
+ let barrier = Arc::new(Barrier::new(2));
+ // We count how many times `upload` was invoked per thread.
+ let call_count = Arc::new(Mutex::default());
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader {
+ barrier: Arc::clone(&barrier),
+ counter: Arc::clone(&call_count),
+ })
+ .build();
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Define a new ping and submit it.
+ const PING_NAME: &str = "test-ping";
+ let custom_ping = private::PingType::new(PING_NAME, true, true, true, vec![]);
+ custom_ping.submit(None);
+ custom_ping.submit(None);
+
+ // Sync up with the upload thread.
+ barrier.wait();
+
+ // Submit another ping and wait for it to do work.
+ custom_ping.submit(None);
+
+ // Sync up with the upload thread again.
+ // This will not be the same thread as the one before (hopefully).
+ barrier.wait();
+
+ // No one's ever gonna wait for the uploader thread (the RLB doesn't store the handle to it),
+ // so all we can do is hope it finishes within time.
+ std::thread::sleep(std::time::Duration::from_millis(100));
+
+ let map = call_count.lock().unwrap();
+ assert_eq!(2, map.len(), "should have launched 2 uploader threads");
+ for &count in map.values() {
+ assert_eq!(1, count, "each thread should call upload only once");
+ }
+}
+
+#[test]
+fn configure_ping_throttling() {
+ let _lock = lock_test();
+
+ let (s, r) = crossbeam_channel::bounded::<String>(1);
+
+ // Define a fake uploader that reports back the submission URL
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct FakeUploader {
+ sender: crossbeam_channel::Sender<String>,
+ done: Arc<std::sync::atomic::AtomicBool>,
+ }
+ impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ url: String,
+ _body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ if self.done.load(std::sync::atomic::Ordering::SeqCst) {
+ // If we've outlived the test, just lie.
+ return net::UploadResult::http_status(200);
+ }
+ self.sender.send(url).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
+ let mut cfg = ConfigurationBuilder::new(true, tmpname, GLOBAL_APPLICATION_ID)
+ .with_server_endpoint("invalid-test-host")
+ .with_uploader(FakeUploader {
+ sender: s,
+ done: Arc::clone(&done),
+ })
+ .build();
+ let pings_per_interval = 10;
+ cfg.rate_limit = Some(crate::PingRateLimit {
+ seconds_per_interval: 1,
+ pings_per_interval,
+ });
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Define a new ping.
+ const PING_NAME: &str = "test-ping";
+ let custom_ping = private::PingType::new(PING_NAME, true, true, true, vec![]);
+
+ // Submit and receive it `pings_per_interval` times.
+ for _ in 0..pings_per_interval {
+ custom_ping.submit(None);
+
+ // Wait for the ping to arrive.
+ let url = r.recv().unwrap();
+ assert!(url.contains(PING_NAME));
+ }
+
+ // Submit one ping more than the rate limit permits.
+ custom_ping.submit(None);
+
+ // We'd expect it to be received within 250ms if it weren't throttled.
+ let now = Instant::now();
+ assert_eq!(
+ r.recv_deadline(now + Duration::from_millis(250)),
+ Err(RecvTimeoutError::Timeout)
+ );
+
+ // We still have to deal with that eleventh ping.
+ // When it eventually processes after the throttle interval, this'll tell
+ // it that it's done.
+ done.store(true, std::sync::atomic::Ordering::SeqCst);
+ // Unfortunately, we'll still be stuck waiting the full
+ // `seconds_per_interval` before running the next test, since shutting down
+ // will wait for the queue to clear.
+}