// 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, extra2: Option, } impl traits::ExtraKeys for SomeExtras { const ALLOWED_KEYS: &'static [&'static str] = &["extra1", "extra2"]; fn into_ffi_extra(self) -> HashMap { 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::>(1); // Define a fake uploader that reports back the submitted payload // using a crossbeam channel. #[derive(Debug)] pub struct ValidatingUploader { sender: crossbeam_channel::Sender>, } impl glean::net::PingUploader for ValidatingUploader { fn upload( &self, _url: String, body: Vec, _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::::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); } } }