summaryrefslogtreecommitdiffstats
path: root/third_party/rust/glean
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /third_party/rust/glean
parentInitial commit. (diff)
downloadfirefox-esr-upstream.tar.xz
firefox-esr-upstream.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/glean')
-rw-r--r--third_party/rust/glean/.cargo-checksum.json1
-rw-r--r--third_party/rust/glean/Cargo.toml102
-rw-r--r--third_party/rust/glean/LICENSE373
-rw-r--r--third_party/rust/glean/README.md38
-rw-r--r--third_party/rust/glean/src/common_test.rs50
-rw-r--r--third_party/rust/glean/src/configuration.rs152
-rw-r--r--third_party/rust/glean/src/core_metrics.rs41
-rw-r--r--third_party/rust/glean/src/lib.rs284
-rw-r--r--third_party/rust/glean/src/net/http_uploader.rs24
-rw-r--r--third_party/rust/glean/src/net/mod.rs221
-rw-r--r--third_party/rust/glean/src/private/event.rs223
-rw-r--r--third_party/rust/glean/src/private/mod.rs35
-rw-r--r--third_party/rust/glean/src/private/ping.rs86
-rw-r--r--third_party/rust/glean/src/system.rs106
-rw-r--r--third_party/rust/glean/src/test.rs1504
-rw-r--r--third_party/rust/glean/tests/common/mod.rs51
-rw-r--r--third_party/rust/glean/tests/init_fails.rs77
-rw-r--r--third_party/rust/glean/tests/never_init.rs66
-rw-r--r--third_party/rust/glean/tests/no_time_to_init.rs74
-rw-r--r--third_party/rust/glean/tests/overflowing_preinit.rs88
-rw-r--r--third_party/rust/glean/tests/persist_ping_lifetime.rs89
-rw-r--r--third_party/rust/glean/tests/persist_ping_lifetime_nopanic.rs37
-rw-r--r--third_party/rust/glean/tests/schema.rs211
-rw-r--r--third_party/rust/glean/tests/simple.rs77
-rwxr-xr-xthird_party/rust/glean/tests/test-shutdown-blocking.sh29
-rw-r--r--third_party/rust/glean/tests/upload_timing.rs225
26 files changed, 4264 insertions, 0 deletions
diff --git a/third_party/rust/glean/.cargo-checksum.json b/third_party/rust/glean/.cargo-checksum.json
new file mode 100644
index 0000000000..8c46cce558
--- /dev/null
+++ b/third_party/rust/glean/.cargo-checksum.json
@@ -0,0 +1 @@
+{"files":{"Cargo.toml":"bb7825ea1f64c8c1d61c3dd696263f14335902b25c99fd5ec53d87c360e535e7","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"5627cc81e6187ab6c2b4dff061af16d559edcab64ba786bac39daa69c703c595","src/common_test.rs":"de47b53dcca37985c0a2b8c02daecbf32309aa54f5a4dd9290719c2c1fd0fa55","src/configuration.rs":"883b327fdad366e414ece83f65ab6b0216ab819c7854b382acf91b702b5a9697","src/core_metrics.rs":"dd17b482613894af08b51a2cff6dc1e84a6dbd853c14a55566e6698348941ced","src/lib.rs":"7cc249fc8f674958b91e6259225e858dfcd2b9b9dbdaecfab4d0ca85ad44129a","src/net/http_uploader.rs":"43812a70d19a38e8d7a093c8076c2b6345372c3c861b0f3511428762700a65e0","src/net/mod.rs":"e36e170a8e53530f8705988eea694ed7c55f50bb0ce403c0facbfb75ce03ac7f","src/private/event.rs":"02bbebf545695812e5055741cc0b5f3c99eda2039e684e26fcdd5f087ed15fe3","src/private/mod.rs":"eb8fe4e588bb32a54617324db39319920c627e6fc23c23cf4da5c17c63e0afed","src/private/ping.rs":"cbdc57f41fc9d46e56b4dfff91ac683753d1f8b3ecd0aa9bc3419e3595b8b81b","src/system.rs":"6eae5b41c15eba9cad6dbd116abe3519ee3e1fe034e79bdd692b029829a8c384","src/test.rs":"68e046309f943aacc45af9e8bb0687c5b49da32f3a55050a3724f0be0a91c61c","tests/common/mod.rs":"37cd4c48e140c793b852ae09fb3e812da28a4412977295015bcbffd632fcf294","tests/init_fails.rs":"28fd7726e76ca1295eb0905eca0b2ec65b0accfa28432c9ff90ec8f92616fc79","tests/never_init.rs":"1f33b8ce7ca3514b57b48cc16d98408974c85cf8aa7d13257ffc2ad878ebb295","tests/no_time_to_init.rs":"e7df75b47897fbf2c860a2e1c1c225b57598b8d1a39125ca897fe8d825bf0338","tests/overflowing_preinit.rs":"7ad4b2274dd9240b53430859a4eb1d2597cf508a5a678333f3d3abbadd2ed4a7","tests/persist_ping_lifetime.rs":"81415dc1d74743f02269f0d0dfa524003147056853f080276972e64a0b761d3c","tests/persist_ping_lifetime_nopanic.rs":"18379d3ffbf4a2c8c684c04ff7a0660b86dfbbb447db2d24dfed6073cb7ddf8f","tests/schema.rs":"1b7b19aec54a24c2bdd4738cf33c16802c19c83504c4d0e6bcfc19142877acdb","tests/simple.rs":"b099034b0599bdf4650e0fa09991a8413fc5fbf397755fc06c8963d4c7c8dfa6","tests/test-shutdown-blocking.sh":"9b16a01c190c7062474dd92182298a3d9a27928c8fa990340fdd798e6cdb7ab2","tests/upload_timing.rs":"d044fce7c783133e385671ea37d674e5a1b4120cae7b07708dcd825addfa0ee3"},"package":"cb5fc2dc8615ab49bfa879d64a02565b459881b72023ff39aca75e5581825695"} \ No newline at end of file
diff --git a/third_party/rust/glean/Cargo.toml b/third_party/rust/glean/Cargo.toml
new file mode 100644
index 0000000000..3b8825f17f
--- /dev/null
+++ b/third_party/rust/glean/Cargo.toml
@@ -0,0 +1,102 @@
+# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
+#
+# When uploading crates to the registry Cargo will automatically
+# "normalize" Cargo.toml files for maximal compatibility
+# with all versions of Cargo and also rewrite `path` dependencies
+# to registry (e.g., crates.io) dependencies.
+#
+# If you are reading this file be aware that the original Cargo.toml
+# will likely look very different (and much more reasonable).
+# See Cargo.toml.orig for the original contents.
+
+[package]
+edition = "2021"
+rust-version = "1.62"
+name = "glean"
+version = "52.7.0"
+authors = [
+ "Jan-Erik Rediger <jrediger@mozilla.com>",
+ "The Glean Team <glean-team@mozilla.com>",
+]
+include = [
+ "/README.md",
+ "/LICENSE",
+ "/src",
+ "/tests",
+ "/Cargo.toml",
+]
+description = "Glean SDK Rust language bindings"
+readme = "README.md"
+keywords = [
+ "telemetry",
+ "glean",
+]
+license = "MPL-2.0"
+repository = "https://github.com/mozilla/glean"
+resolver = "1"
+
+[dependencies.chrono]
+version = "0.4.10"
+features = ["serde"]
+
+[dependencies.crossbeam-channel]
+version = "0.5"
+
+[dependencies.glean-core]
+version = "52.7.0"
+
+[dependencies.inherent]
+version = "1"
+
+[dependencies.log]
+version = "0.4.8"
+
+[dependencies.once_cell]
+version = "1.2.0"
+
+[dependencies.serde]
+version = "1.0.104"
+features = ["derive"]
+
+[dependencies.serde_json]
+version = "1.0.44"
+
+[dependencies.thiserror]
+version = "1.0.4"
+
+[dependencies.time]
+version = "0.1.40"
+
+[dependencies.uuid]
+version = "1.0"
+features = ["v4"]
+
+[dependencies.whatsys]
+version = "0.3.0"
+
+[dev-dependencies.env_logger]
+version = "0.10.0"
+features = [
+ "auto-color",
+ "humantime",
+]
+default-features = false
+
+[dev-dependencies.flate2]
+version = "1.0.19"
+
+[dev-dependencies.jsonschema-valid]
+version = "0.5.0"
+
+[dev-dependencies.tempfile]
+version = "3.1.0"
+
+[features]
+preinit_million_queue = ["glean-core/preinit_million_queue"]
+
+[badges.circle-ci]
+branch = "main"
+repository = "mozilla/glean"
+
+[badges.maintenance]
+status = "actively-developed"
diff --git a/third_party/rust/glean/LICENSE b/third_party/rust/glean/LICENSE
new file mode 100644
index 0000000000..a612ad9813
--- /dev/null
+++ b/third_party/rust/glean/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You. For
+ purposes of this definition, "control" means (a) the power, direct
+ or indirect, to cause the direction or management of such entity,
+ whether by contract or otherwise, or (b) ownership of more than
+ fifty percent (50%) of the outstanding shares or beneficial
+ ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been informed of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/third_party/rust/glean/README.md b/third_party/rust/glean/README.md
new file mode 100644
index 0000000000..4ec6bba914
--- /dev/null
+++ b/third_party/rust/glean/README.md
@@ -0,0 +1,38 @@
+# Glean
+
+The `Glean SDK` is a modern approach for a Telemetry library and is part of the [Glean project](https://docs.telemetry.mozilla.org/concepts/glean/glean.html).
+
+## `glean`
+
+This library provides a Rust language bindings on top of `glean-core`, targeted to Rust consumers.
+
+## Documentation
+
+All documentation is available online:
+
+* [The Glean SDK Book][book]
+* [API documentation][apidocs]
+
+[book]: https://mozilla.github.io/glean/
+[apidocs]: https://mozilla.github.io/glean/docs/glean/index.html
+
+## Example
+
+```rust,no_run
+use glean::{ConfigurationBuilder, Error, metrics::*};
+
+let cfg = ConfigurationBuilder::new(true, "/tmp/data", "org.mozilla.glean_core.example").build();
+glean::initialize(cfg)?;
+
+let prototype_ping = PingType::new("prototype", true, true, vec![]);
+
+glean::register_ping_type(&prototype_ping);
+
+prototype_ping.submit(None);
+```
+
+## License
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/
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..145f1a5732
--- /dev/null
+++ b/third_party/rust/glean/src/configuration.rs
@@ -0,0 +1,152 @@
+// 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>,
+}
+
+/// 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>,
+}
+
+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,
+ }
+ }
+
+ /// 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,
+ }
+ }
+
+ /// 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
+ }
+}
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..fd3c11f2a1
--- /dev/null
+++ b/third_party/rust/glean/src/core_metrics.rs
@@ -0,0 +1,41 @@
+// 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>,
+}
+
+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,
+ }
+ }
+}
+
+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,
+ 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..d6ad16bdc1
--- /dev/null
+++ b/third_party/rust/glean/src/lib.rs
@@ -0,0 +1,284 @@
+// 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, 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, 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,
+ };
+
+ 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)
+}
+
+/// 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..cc1e14f3d6
--- /dev/null
+++ b/third_party/rust/glean/src/net/mod.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/.
+
+//! 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,
+ );
+ })
+ .expect("Failed to spawn Glean's uploader thread");
+ *handle = Some(thread);
+ }
+
+ 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..d646ec3eb6
--- /dev/null
+++ b/third_party/rust/glean/src/private/event.rs
@@ -0,0 +1,223 @@
+// 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};
+
+pub use glean_core::traits::NoExtraKeys;
+
+// 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);
+ }
+}
+
+#[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<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);
+ }
+}
+
+#[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)
+ }
+}
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..85f8bef58b
--- /dev/null
+++ b/third_party/rust/glean/src/private/ping.rs
@@ -0,0 +1,86 @@
+// 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,
+ reason_codes: Vec<String>,
+ ) -> Self {
+ let inner = glean_core::metrics::PingType::new(
+ name.into(),
+ include_client_id,
+ send_if_empty,
+ 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..bca1993d0b
--- /dev/null
+++ b/third_party/rust/glean/src/test.rs
@@ -0,0 +1,1504 @@
+// 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 flate2::read::GzDecoder;
+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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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, 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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 = Configuration {
+ data_path: file_path,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+ 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(
+ Configuration {
+ data_path: tmpname.clone(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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(
+ Configuration {
+ data_path: tmpname.clone(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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(
+ Configuration {
+ data_path: tmpname.clone(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ ClientInfoMetrics::unknown(),
+ true,
+ );
+
+ let ping_name = "custom_ping_1";
+ let ping = private::PingType::new(ping_name, true, false, 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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 = Configuration {
+ data_path: tmpname.clone(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: false,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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 = Configuration {
+ data_path: tmpname.clone(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Now reset Glean and keep upload enabled: no deletion-request
+ // should be sent.
+ test_reset_glean(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ assert_eq!(0, r.len());
+}
+
+#[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(
+ Configuration {
+ data_path: tmpname.clone(),
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: None,
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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(
+ Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ },
+ 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ // We create a ping and a metric before we initialize Glean
+ let sample_ping = PingType::new("sample-ping-1", true, false, 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, vec![]);
+
+ // Create a custom configuration to use a fake uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader { sender: s })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ let _t = new_glean(Some(cfg), true);
+
+ // Create a custom ping and register it.
+ let sample_ping = PingType::new("custom1", 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 = Configuration {
+ data_path: tmpname,
+ application_id: GLOBAL_APPLICATION_ID.into(),
+ upload_enabled: true,
+ max_events: None,
+ delay_ping_lifetime_io: false,
+ server_endpoint: Some("invalid-test-host".into()),
+ uploader: Some(Box::new(FakeUploader {
+ barrier: Arc::clone(&barrier),
+ counter: Arc::clone(&call_count),
+ })),
+ use_core_mps: false,
+ trim_data_to_registered_pings: false,
+ log_level: None,
+ };
+
+ 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, 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");
+ }
+}
diff --git a/third_party/rust/glean/tests/common/mod.rs b/third_party/rust/glean/tests/common/mod.rs
new file mode 100644
index 0000000000..cc02946d2c
--- /dev/null
+++ b/third_party/rust/glean/tests/common/mod.rs
@@ -0,0 +1,51 @@
+// 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 std::{panic, process};
+
+use glean::{ClientInfoMetrics, Configuration};
+
+/// Initialize the env logger for a test environment.
+///
+/// When testing we want all logs to go to stdout/stderr by default.
+pub fn enable_test_logging() {
+ let _ = env_logger::builder().is_test(true).try_init();
+}
+
+/// Install a panic handler that exits the whole process when a panic occurs.
+///
+/// This causes the process to exit even if a thread panics.
+/// This is similar to the `panic=abort` configuration, but works in the default configuration
+/// (as used by `cargo test`).
+fn install_panic_handler() {
+ let orig_hook = panic::take_hook();
+ panic::set_hook(Box::new(move |panic_info| {
+ // invoke the default handler and exit the process
+ orig_hook(panic_info);
+ process::exit(1);
+ }));
+}
+
+/// Create a new instance of Glean.
+pub fn initialize(cfg: Configuration) {
+ // Ensure panics in threads, such as the init thread or the dispatcher, cause the process to
+ // exit.
+ //
+ // Otherwise in case of a panic in a thread the integration test will just hang.
+ // CI will terminate it after a timeout, but why stick around if we know nothing is happening?
+ install_panic_handler();
+
+ // Use some default values to make our life easier a bit.
+ let client_info = ClientInfoMetrics {
+ app_build: "1.0.0".to_string(),
+ app_display_version: "1.0.0".to_string(),
+ channel: Some("testing".to_string()),
+ };
+
+ glean::initialize(cfg, client_info);
+}
diff --git a/third_party/rust/glean/tests/init_fails.rs b/third_party/rust/glean/tests/init_fails.rs
new file mode 100644
index 0000000000..2269da89ff
--- /dev/null
+++ b/third_party/rust/glean/tests/init_fails.rs
@@ -0,0 +1,77 @@
+// 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/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+use std::{thread, time::Duration};
+
+use glean::ConfigurationBuilder;
+
+/// Some user metrics.
+mod metrics {
+ use glean::private::*;
+ use glean::{Lifetime, TimeUnit};
+ use glean_core::CommonMetricData;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| {
+ TimespanMetric::new(
+ CommonMetricData {
+ name: "initialization".into(),
+ category: "sample".into(),
+ send_in_pings: vec!["validation".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ )
+ });
+}
+
+mod pings {
+ use glean::private::PingType;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static validation: Lazy<PingType> =
+ Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![]));
+}
+
+/// Test scenario: Glean initialization fails.
+///
+/// App tries to initialize Glean, but that somehow fails.
+#[test]
+fn init_fails() {
+ common::enable_test_logging();
+
+ metrics::initialization.start();
+
+ // Create a custom configuration to use a validating uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, "")
+ .with_server_endpoint("invalid-test-host")
+ .build();
+ common::initialize(cfg);
+
+ metrics::initialization.stop();
+
+ pings::validation.submit(None);
+
+ // We don't test for data here, as that would block on the dispatcher.
+
+ // Give it a short amount of time to actually finish initialization.
+ thread::sleep(Duration::from_millis(500));
+
+ glean::shutdown();
+}
diff --git a/third_party/rust/glean/tests/never_init.rs b/third_party/rust/glean/tests/never_init.rs
new file mode 100644
index 0000000000..321662b327
--- /dev/null
+++ b/third_party/rust/glean/tests/never_init.rs
@@ -0,0 +1,66 @@
+// 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/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+/// Some user metrics.
+mod metrics {
+ use glean::private::*;
+ use glean::{Lifetime, TimeUnit};
+ use glean_core::CommonMetricData;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| {
+ TimespanMetric::new(
+ CommonMetricData {
+ name: "initialization".into(),
+ category: "sample".into(),
+ send_in_pings: vec!["validation".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ )
+ });
+}
+
+mod pings {
+ use glean::private::PingType;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static validation: Lazy<PingType> =
+ Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![]));
+}
+
+/// Test scenario: Glean is never initialized.
+///
+/// Glean is never initialized.
+/// Some data is recorded early on.
+/// And later the whole process is shutdown.
+#[test]
+fn never_initialize() {
+ common::enable_test_logging();
+
+ metrics::initialization.start();
+
+ // NOT calling `initialize` here.
+ // In apps this might happen for several reasons:
+ // 1. Process doesn't run long enough for Glean to be initialized.
+ // 2. Getting some early data used for initialize fails
+
+ pings::validation.submit(None);
+
+ // We can't test for data either, as that would panic because init was never called.
+
+ glean::shutdown();
+}
diff --git a/third_party/rust/glean/tests/no_time_to_init.rs b/third_party/rust/glean/tests/no_time_to_init.rs
new file mode 100644
index 0000000000..7d51e514d6
--- /dev/null
+++ b/third_party/rust/glean/tests/no_time_to_init.rs
@@ -0,0 +1,74 @@
+// 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/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+use glean::ConfigurationBuilder;
+
+/// Some user metrics.
+mod metrics {
+ use glean::private::*;
+ use glean::{Lifetime, TimeUnit};
+ use glean_core::CommonMetricData;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| {
+ TimespanMetric::new(
+ CommonMetricData {
+ name: "initialization".into(),
+ category: "sample".into(),
+ send_in_pings: vec!["validation".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ )
+ });
+}
+
+mod pings {
+ use glean::private::PingType;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static validation: Lazy<PingType> =
+ Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![]));
+}
+
+/// Test scenario: Glean initialization fails.
+///
+/// The app tries to initializate Glean, but that somehow fails.
+#[test]
+fn init_fails() {
+ common::enable_test_logging();
+
+ metrics::initialization.start();
+
+ // Create a custom configuration to use a validating uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, "firefox-desktop")
+ .with_server_endpoint("invalid-test-host")
+ .build();
+ common::initialize(cfg);
+
+ metrics::initialization.stop();
+
+ pings::validation.submit(None);
+
+ // We don't test for data here, as that would block on the dispatcher.
+
+ // Shut it down immediately; this might not be enough time to initialize.
+
+ glean::shutdown();
+}
diff --git a/third_party/rust/glean/tests/overflowing_preinit.rs b/third_party/rust/glean/tests/overflowing_preinit.rs
new file mode 100644
index 0000000000..6d4ec7f6ae
--- /dev/null
+++ b/third_party/rust/glean/tests/overflowing_preinit.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/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+use glean::ConfigurationBuilder;
+
+/// Some user metrics.
+mod metrics {
+ use glean::private::*;
+ use glean::Lifetime;
+ use glean_core::CommonMetricData;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static rapid_counting: Lazy<CounterMetric> = Lazy::new(|| {
+ CounterMetric::new(CommonMetricData {
+ name: "rapid_counting".into(),
+ category: "sample".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+
+ // This is a hack, but a good one:
+ //
+ // To avoid reaching into RLB internals we re-create the metric so we can look at it.
+ #[allow(non_upper_case_globals)]
+ pub static preinit_tasks_overflow: Lazy<CounterMetric> = Lazy::new(|| {
+ CounterMetric::new(CommonMetricData {
+ category: "glean.error".into(),
+ name: "preinit_tasks_overflow".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+}
+
+/// Test scenario: Lots of metric recordings before init.
+///
+/// The app starts recording metrics before Glean is initialized.
+/// Once initialized the recordings are processed and data is persisted.
+/// The pre-init dispatcher queue records how many recordings over the limit it saw.
+///
+/// This is an integration test to avoid dealing with resetting the dispatcher.
+#[test]
+fn overflowing_the_task_queue_records_telemetry() {
+ common::enable_test_logging();
+
+ // Create a custom configuration to use a validating uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, "firefox-desktop")
+ .with_server_endpoint("invalid-test-host")
+ .build();
+
+ // Insert a bunch of tasks to overflow the queue.
+ for _ in 0..1010 {
+ metrics::rapid_counting.add(1);
+ }
+
+ // Now initialize Glean
+ common::initialize(cfg);
+
+ assert_eq!(Some(1000), metrics::rapid_counting.test_get_value(None));
+
+ // The metrics counts the total number of overflowing tasks,
+ // (and the count of tasks in the queue when we overflowed: bug 1764573)
+ // this might include Glean-internal tasks.
+ let val = metrics::preinit_tasks_overflow
+ .test_get_value(None)
+ .unwrap();
+ assert!(val >= 10);
+
+ glean::shutdown();
+}
diff --git a/third_party/rust/glean/tests/persist_ping_lifetime.rs b/third_party/rust/glean/tests/persist_ping_lifetime.rs
new file mode 100644
index 0000000000..f73673f46f
--- /dev/null
+++ b/third_party/rust/glean/tests/persist_ping_lifetime.rs
@@ -0,0 +1,89 @@
+// 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/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+use glean::{ClientInfoMetrics, Configuration, ConfigurationBuilder};
+use std::path::PathBuf;
+
+/// Some user metrics.
+mod metrics {
+ use glean::private::*;
+ use glean::Lifetime;
+ use glean_core::CommonMetricData;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static boo: Lazy<BooleanMetric> = Lazy::new(|| {
+ BooleanMetric::new(CommonMetricData {
+ name: "boo".into(),
+ category: "sample".into(),
+ send_in_pings: vec!["validation".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ })
+ });
+}
+
+fn cfg_new(tmpname: PathBuf) -> Configuration {
+ ConfigurationBuilder::new(true, tmpname, "firefox-desktop")
+ .with_server_endpoint("invalid-test-host")
+ .with_delay_ping_lifetime_io(true)
+ .build()
+}
+
+/// Test scenario: Are ping-lifetime data persisted on shutdown when delayed?
+///
+/// delay_ping_lifetime_io: true has Glean put "ping"-lifetime data in-memory
+/// instead of the db. Ensure that, on orderly shutdowns, we correctly persist
+/// these in-memory data to the db.
+#[test]
+fn delayed_ping_data() {
+ common::enable_test_logging();
+
+ metrics::boo.set(true);
+
+ // Create a custom configuration to delay ping-lifetime io
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ common::initialize(cfg_new(tmpname.clone()));
+
+ assert!(
+ metrics::boo.test_get_value(None).unwrap(),
+ "Data should be present. Doesn't mean it's persisted, though."
+ );
+
+ glean::test_reset_glean(
+ cfg_new(tmpname.clone()),
+ ClientInfoMetrics::unknown(),
+ false,
+ );
+
+ assert_eq!(
+ None,
+ metrics::boo.test_get_value(None),
+ "Data should not have made it to disk on unclean shutdown."
+ );
+ metrics::boo.set(true); // Let's try again
+
+ // This time, let's shut down cleanly
+ glean::shutdown();
+
+ // Now when we init, we should get the persisted data
+ glean::test_reset_glean(cfg_new(tmpname), ClientInfoMetrics::unknown(), false);
+ assert!(
+ metrics::boo.test_get_value(None).unwrap(),
+ "Data must be persisted between clean shutdown and init!"
+ );
+
+ glean::shutdown(); // Cleanly shut down at the end of the test.
+}
diff --git a/third_party/rust/glean/tests/persist_ping_lifetime_nopanic.rs b/third_party/rust/glean/tests/persist_ping_lifetime_nopanic.rs
new file mode 100644
index 0000000000..18d54e9033
--- /dev/null
+++ b/third_party/rust/glean/tests/persist_ping_lifetime_nopanic.rs
@@ -0,0 +1,37 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+use glean::{Configuration, ConfigurationBuilder};
+use std::path::PathBuf;
+
+fn cfg_new(tmpname: PathBuf) -> Configuration {
+ ConfigurationBuilder::new(true, tmpname, "firefox-desktop")
+ .with_server_endpoint("invalid-test-host")
+ .with_delay_ping_lifetime_io(true)
+ .build()
+}
+
+/// Test scenario: `persist_ping_lifetime_data` called after shutdown.
+#[test]
+fn delayed_ping_data() {
+ common::enable_test_logging();
+
+ // Create a custom configuration to delay ping-lifetime io
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ common::initialize(cfg_new(tmpname));
+ glean::persist_ping_lifetime_data();
+
+ glean::shutdown();
+ glean::persist_ping_lifetime_data();
+}
diff --git a/third_party/rust/glean/tests/schema.rs b/third_party/rust/glean/tests/schema.rs
new file mode 100644
index 0000000000..bdcfb84185
--- /dev/null
+++ b/third_party/rust/glean/tests/schema.rs
@@ -0,0 +1,211 @@
+// 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::collections::HashMap;
+use std::io::Read;
+
+use flate2::read::GzDecoder;
+use glean_core::TextMetric;
+use jsonschema_valid::{self, schemas::Draft};
+use serde_json::Value;
+
+use glean::net::UploadResult;
+use glean::private::*;
+use glean::{
+ traits, ClientInfoMetrics, CommonMetricData, ConfigurationBuilder, HistogramType, MemoryUnit,
+ TimeUnit,
+};
+
+const SCHEMA_JSON: &str = include_str!("../../../glean.1.schema.json");
+
+fn load_schema() -> Value {
+ serde_json::from_str(SCHEMA_JSON).unwrap()
+}
+
+const GLOBAL_APPLICATION_ID: &str = "org.mozilla.glean.test.app";
+
+struct SomeExtras {
+ extra1: Option<String>,
+ extra2: Option<bool>,
+}
+
+impl traits::ExtraKeys for SomeExtras {
+ const ALLOWED_KEYS: &'static [&'static str] = &["extra1", "extra2"];
+
+ fn into_ffi_extra(self) -> HashMap<String, String> {
+ let mut map = HashMap::new();
+
+ self.extra1
+ .and_then(|val| map.insert("extra1".to_string(), val));
+ self.extra2
+ .and_then(|val| map.insert("extra2".to_string(), val.to_string()));
+
+ map
+ }
+}
+
+#[test]
+fn validate_against_schema() {
+ let _ = env_logger::builder().try_init();
+
+ let schema = load_schema();
+
+ let (s, r) = crossbeam_channel::bounded::<Vec<u8>>(1);
+
+ // Define a fake uploader that reports back the submitted payload
+ // using a crossbeam channel.
+ #[derive(Debug)]
+ pub struct ValidatingUploader {
+ sender: crossbeam_channel::Sender<Vec<u8>>,
+ }
+ impl glean::net::PingUploader for ValidatingUploader {
+ fn upload(
+ &self,
+ _url: String,
+ body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> UploadResult {
+ self.sender.send(body).unwrap();
+ UploadResult::http_status(200)
+ }
+ }
+
+ // Create a custom configuration to use a validating 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(ValidatingUploader { sender: s })
+ .build();
+
+ let client_info = ClientInfoMetrics {
+ app_build: env!("CARGO_PKG_VERSION").to_string(),
+ app_display_version: env!("CARGO_PKG_VERSION").to_string(),
+ channel: Some("testing".to_string()),
+ };
+
+ glean::initialize(cfg, client_info);
+
+ const PING_NAME: &str = "test-ping";
+
+ let common = |name: &str| CommonMetricData {
+ category: "test".into(),
+ name: name.into(),
+ send_in_pings: vec![PING_NAME.into()],
+ ..Default::default()
+ };
+
+ // Test each of the metric types, just for basic smoke testing against the
+ // schema
+
+ // TODO: 1695762 Test all of the metric types against the schema from Rust
+
+ let counter_metric = CounterMetric::new(common("counter"));
+ counter_metric.add(42);
+
+ let bool_metric = BooleanMetric::new(common("bool"));
+ bool_metric.set(true);
+
+ let string_metric = StringMetric::new(common("string"));
+ string_metric.set("test".into());
+
+ let stringlist_metric = StringListMetric::new(common("stringlist"));
+ stringlist_metric.add("one".into());
+ stringlist_metric.add("two".into());
+
+ // Let's make sure an empty array is accepted.
+ let stringlist_metric2 = StringListMetric::new(common("stringlist2"));
+ stringlist_metric2.set(vec![]);
+
+ let timespan_metric = TimespanMetric::new(common("timespan"), TimeUnit::Nanosecond);
+ timespan_metric.start();
+ timespan_metric.stop();
+
+ let timing_dist = TimingDistributionMetric::new(common("timing_dist"), TimeUnit::Nanosecond);
+ let id = timing_dist.start();
+ timing_dist.stop_and_accumulate(id);
+
+ let memory_dist = MemoryDistributionMetric::new(common("memory_dist"), MemoryUnit::Byte);
+ memory_dist.accumulate(100);
+
+ let uuid_metric = UuidMetric::new(common("uuid"));
+ // chosen by fair dic roll (`uuidgen`)
+ uuid_metric.set("3ee4db5f-ee26-4557-9a66-bc7425d7893f".into());
+
+ // We can't test the URL metric,
+ // because the regex used in the schema uses a negative lookahead,
+ // which the regex crate doesn't handle.
+ //
+ //let url_metric = UrlMetric::new(common("url"));
+ //url_metric.set("https://mozilla.github.io/glean/");
+
+ let datetime_metric = DatetimeMetric::new(common("datetime"), TimeUnit::Day);
+ datetime_metric.set(None);
+
+ let event_metric = EventMetric::<SomeExtras>::new(common("event"));
+ event_metric.record(None);
+ event_metric.record(SomeExtras {
+ extra1: Some("test".into()),
+ extra2: Some(false),
+ });
+
+ let custom_dist =
+ CustomDistributionMetric::new(common("custom_dist"), 1, 100, 100, HistogramType::Linear);
+ custom_dist.accumulate_samples(vec![50, 51]);
+
+ let quantity_metric = QuantityMetric::new(common("quantity"));
+ quantity_metric.set(0);
+
+ let rate_metric = RateMetric::new(common("rate"));
+ rate_metric.add_to_numerator(1);
+ rate_metric.add_to_denominator(1);
+
+ let numerator_metric1 = NumeratorMetric::new(common("num1"));
+ let numerator_metric2 = NumeratorMetric::new(common("num2"));
+ let denominator_metric =
+ DenominatorMetric::new(common("den"), vec![common("num1"), common("num2")]);
+
+ numerator_metric1.add_to_numerator(1);
+ numerator_metric2.add_to_numerator(2);
+ denominator_metric.add(3);
+
+ let text_metric = TextMetric::new(common("text"));
+ text_metric.set("loooooong text".repeat(100));
+
+ // Define a new ping and submit it.
+ let custom_ping = glean::private::PingType::new(PING_NAME, true, true, vec![]);
+ custom_ping.submit(None);
+
+ // Wait for the ping to arrive.
+ let raw_body = r.recv().unwrap();
+
+ // Decode the gzipped body.
+ let mut gzip_decoder = GzDecoder::new(&raw_body[..]);
+ let mut s = String::with_capacity(raw_body.len());
+
+ let data = gzip_decoder
+ .read_to_string(&mut s)
+ .ok()
+ .map(|_| &s[..])
+ .or_else(|| std::str::from_utf8(&raw_body).ok())
+ .and_then(|payload| serde_json::from_str(payload).ok())
+ .unwrap();
+
+ // Now validate against the vendored schema
+ let cfg = jsonschema_valid::Config::from_schema(&schema, Some(Draft::Draft6)).unwrap();
+ let validation = cfg.validate(&data);
+ match validation {
+ Ok(()) => {}
+ Err(errors) => {
+ let mut msg = format!("Data: {data:#?}\n Errors:\n");
+ for (idx, error) in errors.enumerate() {
+ msg.push_str(&format!("Error {}: ", idx + 1));
+ msg.push_str(&error.to_string());
+ msg.push('\n');
+ }
+ panic!("{}", msg);
+ }
+ }
+}
diff --git a/third_party/rust/glean/tests/simple.rs b/third_party/rust/glean/tests/simple.rs
new file mode 100644
index 0000000000..efc8d9a0f8
--- /dev/null
+++ b/third_party/rust/glean/tests/simple.rs
@@ -0,0 +1,77 @@
+// 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/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+use glean::ConfigurationBuilder;
+
+/// Some user metrics.
+mod metrics {
+ use glean::private::*;
+ use glean::{Lifetime, TimeUnit};
+ use glean_core::CommonMetricData;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static initialization: Lazy<TimespanMetric> = Lazy::new(|| {
+ TimespanMetric::new(
+ CommonMetricData {
+ name: "initialization".into(),
+ category: "sample".into(),
+ send_in_pings: vec!["validation".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ ..Default::default()
+ },
+ TimeUnit::Nanosecond,
+ )
+ });
+}
+
+mod pings {
+ use glean::private::PingType;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static validation: Lazy<PingType> =
+ Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![]));
+}
+
+/// Test scenario: A clean run
+///
+/// The app is initialized, in turn Glean gets initialized without problems.
+/// Some data is recorded (before and after initialization).
+/// And later the whole process is shutdown.
+#[test]
+fn simple_lifecycle() {
+ common::enable_test_logging();
+
+ metrics::initialization.start();
+
+ // Create a custom configuration to use a validating uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, "firefox-desktop")
+ .with_server_endpoint("invalid-test-host")
+ .build();
+ common::initialize(cfg);
+
+ metrics::initialization.stop();
+
+ // This would never be called outside of tests,
+ // but it's the only way we can really test it's working right now.
+ assert!(metrics::initialization.test_get_value(None).is_some());
+
+ pings::validation.submit(None);
+ assert!(metrics::initialization.test_get_value(None).is_none());
+
+ glean::shutdown();
+}
diff --git a/third_party/rust/glean/tests/test-shutdown-blocking.sh b/third_party/rust/glean/tests/test-shutdown-blocking.sh
new file mode 100755
index 0000000000..2f5d82acf0
--- /dev/null
+++ b/third_party/rust/glean/tests/test-shutdown-blocking.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+# Test harness for testing the RLB processes from the outside.
+#
+# Some behavior can only be observed when properly exiting the process running Glean,
+# e.g. when an uploader runs in another thread.
+# On exit the threads will be killed, regardless of their state.
+
+# Remove the temporary data path on all exit conditions
+cleanup() {
+ if [ -n "$datapath" ]; then
+ rm -r "$datapath"
+ fi
+}
+trap cleanup INT ABRT TERM EXIT
+
+tmp="${TMPDIR:-/tmp}"
+datapath=$(mktemp -d "${tmp}/glean_long_running.XXXX")
+
+cargo run --example long-running -- "$datapath"
+count=$(ls -1q "$datapath/pending_pings" | wc -l)
+
+if [[ "$count" -eq 0 ]]; then
+ echo "test result: ok."
+ exit 0
+else
+ echo "test result: FAILED."
+ exit 101
+fi
diff --git a/third_party/rust/glean/tests/upload_timing.rs b/third_party/rust/glean/tests/upload_timing.rs
new file mode 100644
index 0000000000..1dd073bebb
--- /dev/null
+++ b/third_party/rust/glean/tests/upload_timing.rs
@@ -0,0 +1,225 @@
+// 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/.
+
+//! This integration test should model how the RLB is used when embedded in another Rust application
+//! (e.g. FOG/Firefox Desktop).
+//!
+//! We write a single test scenario per file to avoid any state keeping across runs
+//! (different files run as different processes).
+
+mod common;
+
+use std::io::Read;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::thread;
+use std::time;
+
+use crossbeam_channel::{bounded, Sender};
+use flate2::read::GzDecoder;
+use serde_json::Value as JsonValue;
+
+use glean::net;
+use glean::ConfigurationBuilder;
+
+pub mod metrics {
+ #![allow(non_upper_case_globals)]
+
+ use glean::{
+ private::BooleanMetric, private::TimingDistributionMetric, CommonMetricData, Lifetime,
+ TimeUnit,
+ };
+
+ pub static sample_boolean: once_cell::sync::Lazy<BooleanMetric> =
+ once_cell::sync::Lazy::new(|| {
+ BooleanMetric::new(CommonMetricData {
+ name: "sample_boolean".into(),
+ category: "test.metrics".into(),
+ send_in_pings: vec!["validation".into()],
+ disabled: false,
+ lifetime: Lifetime::Ping,
+ ..Default::default()
+ })
+ });
+
+ // The following are duplicated from `glean-core/src/internal_metrics.rs`
+ // so we can use the test APIs to query them.
+
+ pub static send_success: once_cell::sync::Lazy<TimingDistributionMetric> =
+ once_cell::sync::Lazy::new(|| {
+ TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "send_success".into(),
+ category: "glean.upload".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ dynamic_label: None,
+ },
+ TimeUnit::Millisecond,
+ )
+ });
+
+ pub static send_failure: once_cell::sync::Lazy<TimingDistributionMetric> =
+ once_cell::sync::Lazy::new(|| {
+ TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "send_failure".into(),
+ category: "glean.upload".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ dynamic_label: None,
+ },
+ TimeUnit::Millisecond,
+ )
+ });
+
+ pub static shutdown_wait: once_cell::sync::Lazy<TimingDistributionMetric> =
+ once_cell::sync::Lazy::new(|| {
+ TimingDistributionMetric::new(
+ CommonMetricData {
+ name: "shutdown_wait".into(),
+ category: "glean.validation".into(),
+ send_in_pings: vec!["metrics".into()],
+ lifetime: Lifetime::Ping,
+ disabled: false,
+ dynamic_label: None,
+ },
+ TimeUnit::Millisecond,
+ )
+ });
+}
+
+mod pings {
+ use glean::private::PingType;
+ use once_cell::sync::Lazy;
+
+ #[allow(non_upper_case_globals)]
+ pub static validation: Lazy<PingType> =
+ Lazy::new(|| glean::private::PingType::new("validation", true, true, vec![]));
+}
+
+// Define a fake uploader that sleeps.
+#[derive(Debug)]
+struct FakeUploader {
+ calls: AtomicUsize,
+ sender: Sender<JsonValue>,
+}
+
+impl net::PingUploader for FakeUploader {
+ fn upload(
+ &self,
+ _url: String,
+ body: Vec<u8>,
+ _headers: Vec<(String, String)>,
+ ) -> net::UploadResult {
+ let calls = self.calls.fetch_add(1, Ordering::SeqCst);
+ let decode = |body: Vec<u8>| {
+ let mut gzip_decoder = GzDecoder::new(&body[..]);
+ let mut s = String::with_capacity(body.len());
+
+ 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()
+ };
+
+ match calls {
+ // First goes through as is.
+ 0 => net::UploadResult::http_status(200),
+ // Second briefly sleeps
+ 1 => {
+ thread::sleep(time::Duration::from_millis(100));
+ net::UploadResult::http_status(200)
+ }
+ // Third one fails
+ 2 => net::UploadResult::http_status(404),
+ // Fourth one fast again
+ 3 => {
+ self.sender.send(decode(body)).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ // Last one is the metrics ping, a-ok.
+ _ => {
+ self.sender.send(decode(body)).unwrap();
+ net::UploadResult::http_status(200)
+ }
+ }
+ }
+}
+
+/// Test scenario: Different timings for upload on success and failure.
+///
+/// The app is initialized, in turn Glean gets initialized without problems.
+/// A custom ping is submitted multiple times to trigger upload.
+/// A metrics ping is submitted to get the upload timing data.
+///
+/// And later the whole process is shutdown.
+#[test]
+fn upload_timings() {
+ common::enable_test_logging();
+
+ // Create a custom configuration to use a validating uploader.
+ let dir = tempfile::tempdir().unwrap();
+ let tmpname = dir.path().to_path_buf();
+ let (tx, rx) = bounded(1);
+
+ let cfg = ConfigurationBuilder::new(true, tmpname.clone(), "glean-upload-timing")
+ .with_server_endpoint("invalid-test-host")
+ .with_use_core_mps(false)
+ .with_uploader(FakeUploader {
+ calls: AtomicUsize::new(0),
+ sender: tx,
+ })
+ .build();
+ common::initialize(cfg);
+
+ // Wait for init to finish,
+ // otherwise we might be to quick with calling `shutdown`.
+ let _ = metrics::sample_boolean.test_get_value(None);
+
+ // fast
+ pings::validation.submit(None);
+ // slow
+ pings::validation.submit(None);
+ // failed
+ pings::validation.submit(None);
+ // fast
+ pings::validation.submit(None);
+
+ // wait for the last ping
+ let _body = rx.recv().unwrap();
+
+ assert_eq!(
+ 3,
+ metrics::send_success.test_get_value(None).unwrap().count,
+ "Successful pings: two fast, one slow"
+ );
+ assert_eq!(
+ 1,
+ metrics::send_failure.test_get_value(None).unwrap().count,
+ "One failed ping"
+ );
+
+ // This is awkward, but it's what gets us very close to just starting a new process with a
+ // fresh Glean.
+ // This also calls `glean::shutdown();` internally, waiting on the uploader.
+ let data_path = Some(tmpname.display().to_string());
+ glean_core::glean_test_destroy_glean(false, data_path);
+
+ let cfg = ConfigurationBuilder::new(true, tmpname, "glean-upload-timing")
+ .with_server_endpoint("invalid-test-host")
+ .with_use_core_mps(false)
+ .build();
+ common::initialize(cfg);
+
+ assert_eq!(
+ 1,
+ metrics::shutdown_wait.test_get_value(None).unwrap().count,
+ "Measured time waiting for shutdown exactly once"
+ );
+}