diff options
Diffstat (limited to 'third_party/rust/glean/tests/schema.rs')
-rw-r--r-- | third_party/rust/glean/tests/schema.rs | 211 |
1 files changed, 211 insertions, 0 deletions
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); + } + } +} |