#![allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::expect_used,
clippy::disallowed_methods,
clippy::disallowed_types,
missing_docs
)]
use std::{
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
};
use obs_build::{Config, DescriptorSource};
fn fixture_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
}
fn workspace_proto_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("obs-proto")
.join("proto")
}
fn run_protoc_for_fixture(out_root: &std::path::Path) -> PathBuf {
let fds_path = out_root.join("fds.bin");
let protoc = std::env::var("PROTOC").unwrap_or_else(|_| "protoc".to_string());
let status = std::process::Command::new(&protoc)
.arg(format!("--proto_path={}", fixture_dir().display()))
.arg(format!("--proto_path={}", workspace_proto_dir().display()))
.arg("--include_imports")
.arg(format!("--descriptor_set_out={}", fds_path.display()))
.arg("myapp/v1/events.proto")
.status()
.expect("invoke protoc");
assert!(status.success(), "protoc failed for fixture");
fds_path
}
#[test]
fn test_should_emit_four_files_per_compile() {
let tmp = tempdir();
let fds = run_protoc_for_fixture(tmp.path());
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds))
.out_dir(tmp.path())
.compile()
.expect("codegen succeeds");
for fname in ["schemas.rs", "builders.rs", "lints.rs", "arrow_schema.rs"] {
let path = tmp.path().join("obs").join(fname);
assert!(path.exists(), "expected {fname} to be written: {path:?}");
let body = std::fs::read_to_string(&path).unwrap();
assert!(!body.is_empty(), "{fname} should not be empty");
assert!(
body.contains("@generated by obs-build"),
"{fname} missing generation marker"
);
}
}
#[test]
fn test_codegen_output_should_be_byte_identical_across_runs() {
let tmp_a = tempdir();
let tmp_b = tempdir();
let fds_a = run_protoc_for_fixture(tmp_a.path());
let fds_b = run_protoc_for_fixture(tmp_b.path());
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds_a))
.out_dir(tmp_a.path())
.compile()
.unwrap();
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds_b))
.out_dir(tmp_b.path())
.compile()
.unwrap();
for fname in ["schemas.rs", "builders.rs", "lints.rs", "arrow_schema.rs"] {
let body_a = std::fs::read(tmp_a.path().join("obs").join(fname)).unwrap();
let body_b = std::fs::read(tmp_b.path().join("obs").join(fname)).unwrap();
assert_eq!(body_a, body_b, "non-deterministic output for {fname}");
}
}
#[test]
fn test_schemas_rs_should_emit_one_static_per_event() {
let tmp = tempdir();
let fds = run_protoc_for_fixture(tmp.path());
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds))
.out_dir(tmp.path())
.compile()
.unwrap();
let body = std::fs::read_to_string(tmp.path().join("obs").join("schemas.rs")).unwrap();
assert!(body.contains("ObsRequestCompletedSchema"));
assert!(body.contains("ObsHelloEmittedSchema"));
assert!(body.contains("__OBS_SCHEMA_OBS_REQUEST_COMPLETED"));
assert!(body.contains("__OBS_SCHEMA_OBS_HELLO_EMITTED"));
assert!(body.contains("\"myapp.v1.ObsRequestCompleted\""));
assert!(body.contains("\"myapp.v1.ObsHelloEmitted\""));
assert!(body.contains("u64"));
}
#[test]
fn test_builders_rs_should_emit_one_builder_per_event() {
let tmp = tempdir();
let fds = run_protoc_for_fixture(tmp.path());
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds))
.out_dir(tmp.path())
.compile()
.unwrap();
let body = std::fs::read_to_string(tmp.path().join("obs").join("builders.rs")).unwrap();
assert!(body.contains("pub struct ObsRequestCompletedBuilder"));
assert!(body.contains("pub struct ObsHelloEmittedBuilder"));
assert!(body.contains("pub fn route("));
assert!(body.contains("pub fn latency_ms("));
assert!(body.contains("pub fn who("));
assert!(body.contains("static __CALLSITE"));
}
#[test]
fn test_lints_rs_should_assert_l001_l011_per_event() {
let tmp = tempdir();
let fds = run_protoc_for_fixture(tmp.path());
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds))
.out_dir(tmp.path())
.compile()
.unwrap();
let body = std::fs::read_to_string(tmp.path().join("obs").join("lints.rs")).unwrap();
assert!(body.contains("@generated by obs-build"));
assert!(
body.contains("obs L004"),
"expected L004 panic for fixture's MEASUREMENT-without-metric field, got:\n{body}"
);
assert!(body.contains("latency_ms"));
}
#[test]
fn test_generated_project_metrics_should_use_field_relative_instruments() {
let tmp = tempdir();
let fds = run_protoc_for_fixture(tmp.path());
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds))
.out_dir(tmp.path())
.compile()
.unwrap();
let body = std::fs::read_to_string(tmp.path().join("obs").join("schemas.rs")).unwrap();
assert!(
body.contains("record_counter(\"latency_ms\"")
|| body.contains("record_gauge_u64(\"latency_ms\"")
|| body.contains("record_histogram(\"latency_ms\""),
"expected generated metric instrument to be field-relative, got:\n{body}"
);
assert!(
!body.contains("record_counter(\"myapp.v1.ObsRequestCompleted.latency_ms\"")
&& !body.contains("record_gauge_u64(\"myapp.v1.ObsRequestCompleted.latency_ms\"")
&& !body.contains("record_histogram(\"myapp.v1.ObsRequestCompleted.latency_ms\""),
"generated metric instrument should not be event-qualified"
);
}
#[test]
fn test_arrow_schema_rs_should_list_every_event_full_name() {
let tmp = tempdir();
let fds = run_protoc_for_fixture(tmp.path());
Config::new()
.descriptor_source(DescriptorSource::Precompiled(fds))
.out_dir(tmp.path())
.compile()
.unwrap();
let body = std::fs::read_to_string(tmp.path().join("obs").join("arrow_schema.rs")).unwrap();
assert!(body.contains("payload_struct_for"));
assert!(body.contains("all_payload_full_names"));
assert!(body.contains("\"myapp.v1.ObsRequestCompleted\""));
assert!(body.contains("\"myapp.v1.ObsHelloEmitted\""));
}
struct TempDir(PathBuf);
impl TempDir {
fn path(&self) -> &std::path::Path {
&self.0
}
}
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
static TEMPDIR_SEQ: AtomicU64 = AtomicU64::new(0);
fn tempdir() -> TempDir {
let mut path = std::env::temp_dir();
let seq = TEMPDIR_SEQ.fetch_add(1, Ordering::Relaxed);
path.push(format!("obs_build_test_{}_{}", std::process::id(), seq));
std::fs::create_dir_all(&path).unwrap();
TempDir(path)
}