opentelemetry-rust/opentelemetry-stackdriver/tests/generate.rs

262 lines
7.9 KiB
Rust

use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use futures_util::stream::FuturesUnordered;
use futures_util::stream::StreamExt;
use walkdir::WalkDir;
/// Download the latest protobuf schemas from the Google APIs GitHub repository.
///
/// This test is ignored by default, but can be run with `cargo test sync_schemas -- --ignored`.
#[tokio::test]
#[ignore]
async fn sync_schemas() {
let client = reqwest::Client::new();
let cache = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("proto/google");
let schemas = PREREQUISITE_SCHEMAS
.iter()
.chain(GENERATE_FROM_SCHEMAS.iter());
let mut futures = FuturesUnordered::new();
for path in schemas.copied() {
let filename = cache.join(path);
let client = client.clone();
futures.push(async move {
let url = format!("{BASE_URI}/{path}");
let rsp = client.get(url).send().await.unwrap();
let body = rsp.text().await.unwrap();
fs::create_dir_all(filename.parent().unwrap()).unwrap();
fs::write(filename, body).unwrap();
});
}
while futures.next().await.is_some() {}
}
/// Use the protobuf schemas downloaded by the `sync_schemas` test to generate code.
///
/// This test will fail if the code currently in the repository is different from the
/// newly generated code, and will update it in place in that case.
#[test]
fn generated_code_is_fresh() {
// Generate code into a temporary directory.
let schemas = GENERATE_FROM_SCHEMAS
.iter()
.map(|s| format!("google/{s}"))
.collect::<Vec<_>>();
let tmp_dir = tempfile::tempdir().unwrap();
fs::create_dir_all(&tmp_dir).unwrap();
tonic_build::configure()
.build_client(true)
.build_server(false)
.out_dir(&tmp_dir)
.compile(&schemas, &["proto"])
.unwrap();
// Next, wrangle the generated file names into a directory hierarchy.
let (mut modules, mut renames) = (Vec::new(), Vec::new());
for entry in fs::read_dir(&tmp_dir).unwrap() {
let path = entry.unwrap().path();
// Tonic now uses prettyplease instead of rustfmt, which causes a
// number of differences in the generated code.
Command::new("rustfmt")
.arg("--edition=2021")
.arg(&path)
.output()
.unwrap();
let file_name_str = path.file_name().and_then(|s| s.to_str()).unwrap();
let (base, _) = file_name_str
.strip_prefix("google.")
.unwrap()
.rsplit_once('.')
.unwrap();
let new = match base.rsplit_once('.') {
Some((dir, fname)) => {
let mut module = dir.split('.').map(|s| s.to_owned()).collect::<Vec<_>>();
module.push(fname.to_owned());
modules.push(module);
tmp_dir
.path()
.join(dir.replace('.', "/").replace("r#", ""))
.join(format!("{}.rs", fname.replace("r#", "")))
}
None => {
let new = tmp_dir
.path()
.join(format!("{}.rs", base.replace("r#", "")));
modules.push(vec![base.to_owned()]);
new
}
};
renames.push((path, new));
}
// Rename the files into place after iterating over the old version.
for (old, new) in renames {
fs::create_dir_all(new.parent().unwrap()).unwrap();
fs::rename(old, new).unwrap();
}
// Build the module root and write it to `mod.rs`.
modules.sort_unstable();
let mut previous: &[String] = &[];
let (mut root, mut level) = (String::new(), 0);
for module in &modules {
// Find out how many modules to close and what modules to open.
let parent = &module[..module.len() - 1];
let (mut close, mut open) = (0, vec![]);
let components = Ord::max(previous.len(), parent.len());
for i in 0..components {
let (prev, cur) = (previous.get(i), parent.get(i));
if prev == cur && close == 0 && open.is_empty() {
continue;
}
match (prev, cur) {
(Some(_), Some(new)) => {
close += 1;
open.push(new);
}
(Some(_), None) => close += 1,
(None, Some(new)) => open.push(new),
(None, None) => unreachable!(),
}
}
// Close modules.
let closed = close > 0;
while close > 0 {
for _ in 0..((level - 1) * 4) {
root.push(' ');
}
root.push_str("}\n");
close -= 1;
level -= 1;
}
if closed {
root.push('\n');
}
// Open modules.
let mut opened = false;
for component in &open {
if !opened && !closed {
root.push('\n');
opened = true;
}
for _ in 0..(level * 4) {
root.push(' ');
}
root.push_str("pub mod ");
root.push_str(component);
root.push_str(" {\n");
level += 1;
}
// Write a module declaration for this actual module.
for _ in 0..(level * 4) {
root.push(' ');
}
root.push_str("pub mod ");
root.push_str(module.last().unwrap());
root.push_str(";\n");
previous = parent;
}
while level > 0 {
level -= 1;
for _ in 0..(level * 4) {
root.push(' ');
}
root.push_str("}\n");
}
fs::write(tmp_dir.path().join("mod.rs"), root).unwrap();
// Move on to actually comparing the old and new versions.
let versions = [SOURCE_DIR, tmp_dir.path().to_str().unwrap()]
.iter()
.map(|path| {
let mut files = HashMap::new();
for entry in WalkDir::new(path) {
let entry = match entry {
Ok(e) => e,
Err(_) => continue,
};
let is_file = entry.file_type().is_file();
let rs = entry.path().extension() == Some(OsStr::new("rs"));
if !is_file || !rs {
continue;
}
let file = entry.path();
let name = file.strip_prefix(path).unwrap();
files.insert(name.to_owned(), fs::read_to_string(file).unwrap());
}
files
})
.collect::<Vec<_>>();
// Compare the old version and new version and fail the test if they're different.
let mut keys = versions[0].keys().collect::<Vec<_>>();
keys.extend(versions[1].keys());
keys.sort_unstable();
keys.dedup();
if versions[0] != versions[1] {
let _ = fs::remove_dir_all(SOURCE_DIR);
fs::rename(tmp_dir, SOURCE_DIR).unwrap();
panic!("generated code in the repository is outdated, updating...");
}
}
/// Schema files used as input for the generated code.
const GENERATE_FROM_SCHEMAS: &[&str] = &[
"devtools/cloudtrace/v2/tracing.proto",
"devtools/cloudtrace/v2/trace.proto",
"logging/type/http_request.proto",
"logging/v2/log_entry.proto",
"logging/v2/logging.proto",
"rpc/status.proto",
];
/// Schema files that are dependencies of the `GENERATED_SCHEMAS`.
const PREREQUISITE_SCHEMAS: &[&str] = &[
"api/annotations.proto",
"api/resource.proto",
"api/monitored_resource.proto",
"api/field_behavior.proto",
"api/http.proto",
"api/client.proto",
"logging/type/log_severity.proto",
"api/label.proto",
"api/launch_stage.proto",
"logging/v2/logging_config.proto",
];
const BASE_URI: &str = "https://raw.githubusercontent.com/googleapis/googleapis/master/google";
const SOURCE_DIR: &str = "src/proto";