Revision control
Copy as Markdown
// 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
use std::collections::HashMap;
use std::io::Read;
use flate2::read::GzDecoder;
use jsonschema_valid::schemas::Draft;
use serde_json::Value;
use glean::net::{PingUploadRequest, 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, ping_request: PingUploadRequest) -> UploadResult {
self.sender.send(ping_request.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()),
locale: Some("xx-XX".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"));
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,
true,
true,
true,
vec![],
vec![],
true,
);
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);
}
}
}