use libremetaverse::Primitive; use libremetaverse::structured_data::{OSD, OSDParser}; use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; use std::sync::atomic::{AtomicU64, Ordering}; const JSON_FIXTURE: &[u8] = include_bytes!("../../tests/fixtures/structured_data/json_reference.json"); const EXIT_USAGE: i32 = 2; const EXIT_IO: i32 = 3; const EXIT_INVALID_OSD: i32 = 4; const EXIT_INVALID_PRIMITIVE: i32 = 5; static TEMP_ID: AtomicU64 = AtomicU64::new(0); struct TestDir(PathBuf); impl TestDir { fn new(test_name: &str) -> Self { let id = TEMP_ID.fetch_add(1, Ordering::Relaxed); let path = std::env::temp_dir().join(format!( "metacrate-osd-inspector-{test_name}-{}-{id}", std::process::id() )); fs::create_dir(&path).expect("create isolated test directory"); Self(path) } fn path(&self, name: &str) -> PathBuf { self.0.join(name) } } impl Drop for TestDir { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } fn run(args: &[&str], stdin: Option<&[u8]>) -> Output { let mut command = Command::new(env!("CARGO_BIN_EXE_osd-inspector")); command .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()); if stdin.is_some() { command.stdin(Stdio::piped()); } else { command.stdin(Stdio::null()); } let mut child = command.spawn().expect("spawn osd-inspector"); if let Some(bytes) = stdin { child .stdin .take() .expect("piped stdin") .write_all(bytes) .expect("write command stdin"); } child.wait_with_output().expect("collect command output") } fn utf8(bytes: &[u8]) -> &str { std::str::from_utf8(bytes).expect("command output is UTF-8") } fn assert_exit(output: &Output, expected: i32) { assert_eq!( output.status.code(), Some(expected), "stdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); } fn write_fixture(directory: &TestDir) -> PathBuf { let path = directory.path("reference.json"); fs::write(&path, JSON_FIXTURE).expect("write JSON fixture"); path } fn path_text(path: &Path) -> &str { path.to_str().expect("test path is UTF-8") } #[test] fn help_usage_and_upstream_aliases_are_stable() { let no_args = run(&[], None); assert_exit(&no_args, EXIT_USAGE); assert!(utf8(&no_args.stderr).contains("Usage: osd-inspector")); let help = run(&["--help"], None); assert!(help.status.success()); let help = utf8(&help.stdout); for command in [ "inspect", "convert", "validate", "prim-to-osd", "osd-to-prim", ] { assert!(help.contains(command), "help omitted {command}"); } let directory = TestDir::new("aliases"); let input = write_fixture(&directory); let input = path_text(&input); for (command, expected) in [ ("i", "Structure:"), ("inspect", "Map (4 keys)"), ("v", "Valid OSD file"), ("validate", "Keys: 4"), ] { let output = run(&[command, input], None); assert!( output.status.success(), "{command}: {}", utf8(&output.stderr) ); assert!(utf8(&output.stdout).contains(expected)); } } #[test] fn inspect_is_deterministic_and_validate_accepts_standard_input() { let directory = TestDir::new("inspect"); let input = write_fixture(&directory); let output = run(&["inspect", path_text(&input)], None); assert!(output.status.success(), "{}", utf8(&output.stderr)); let output = utf8(&output.stdout); assert!(output.contains("Type: Map")); assert!(output.contains(&format!("Size: {} bytes", JSON_FIXTURE.len()))); let array = output.find(" array:").expect("array key"); let false_value = output.find(" false:").expect("false key"); let true_value = output.find(" true:").expect("true key"); let zero = output.find(" zero:").expect("zero key"); assert!(array < false_value && false_value < true_value && true_value < zero); let output = run(&["validate", "-"], Some(JSON_FIXTURE)); assert!(output.status.success(), "{}", utf8(&output.stderr)); assert_eq!( utf8(&output.stdout), "Valid OSD file\n Type: Map\n Keys: 4\n" ); } #[test] fn convert_emits_library_golden_bytes_for_every_format() { let directory = TestDir::new("convert"); let input = write_fixture(&directory); let expected = OSDParser::deserialize_json_with_string( std::str::from_utf8(JSON_FIXTURE) .expect("compatibility fixture is UTF-8") .to_owned(), ) .expect("parse checked-in compatibility fixture"); let cases: [(&str, &str, Vec); 4] = [ ( "json", "converted.json", OSDParser::serialize_json_string(expected.clone(), Some(true)) .expect("serialize golden JSON") .into_bytes(), ), ( "xml", "converted.xml", OSDParser::serialize_llsd_xml_bytes(expected.clone()).expect("serialize golden XML"), ), ( "binary", "converted.bin", OSDParser::serialize_llsd_binary_with_osd(expected.clone()) .expect("serialize golden binary"), ), ( "notation", "converted.notation", OSDParser::serialize_llsd_notation(expected) .expect("serialize golden notation") .into_bytes(), ), ]; for (index, (format, filename, golden)) in cases.into_iter().enumerate() { let destination = directory.path(filename); let command = if index == 0 { "convert" } else { "c" }; let output = run( &[command, path_text(&input), format, path_text(&destination)], None, ); assert!( output.status.success(), "{format}: {}", utf8(&output.stderr) ); assert_eq!(fs::read(&destination).expect("read converted file"), golden); assert!(utf8(&output.stdout).contains(&format!("to {format}:"))); let output = run(&["validate", path_text(&destination)], None); assert!( output.status.success(), "could not read back {format}: {}", utf8(&output.stderr) ); assert!(utf8(&output.stdout).contains("Type: Map")); } } #[test] fn every_documented_output_format_alias_is_accepted() { let directory = TestDir::new("format-aliases"); let input = write_fixture(&directory); let expected = OSDParser::deserialize_json_with_string( std::str::from_utf8(JSON_FIXTURE) .expect("compatibility fixture is UTF-8") .to_owned(), ) .expect("parse checked-in compatibility fixture"); let aliases = [ ( "j", OSDParser::serialize_json_string(expected.clone(), Some(true)) .expect("serialize JSON") .into_bytes(), ), ( "x", OSDParser::serialize_llsd_xml_bytes(expected.clone()).expect("serialize XML"), ), ( "bin", OSDParser::serialize_llsd_binary_with_osd(expected.clone()).expect("serialize binary"), ), ( "b", OSDParser::serialize_llsd_binary_with_osd(expected.clone()).expect("serialize binary"), ), ( "llsd", OSDParser::serialize_llsd_notation(expected.clone()) .expect("serialize notation") .into_bytes(), ), ( "n", OSDParser::serialize_llsd_notation(expected) .expect("serialize notation") .into_bytes(), ), ]; for (alias, golden) in aliases { let output = run(&["c", path_text(&input), alias, "-"], None); assert!( output.status.success(), "format alias {alias}: {}", utf8(&output.stderr) ); assert_eq!(output.stdout, golden, "format alias {alias}"); } } #[test] fn conversion_supports_binary_standard_output_and_format_detection() { let expected = OSDParser::deserialize_json_with_string( std::str::from_utf8(JSON_FIXTURE) .expect("compatibility fixture is UTF-8") .to_owned(), ) .expect("parse checked-in compatibility fixture"); let output = run(&["convert", "-", "binary", "-"], Some(JSON_FIXTURE)); assert!(output.status.success(), "{}", utf8(&output.stderr)); assert!(output.stderr.is_empty()); assert_eq!( OSDParser::deserialize_llsd_binary_with_bytes(output.stdout) .expect("parse binary standard output"), expected ); let binary = OSDParser::serialize_llsd_binary_with_osd(expected.clone()) .expect("serialize binary fixture"); let output = run(&["validate", "-"], Some(&binary)); assert!(output.status.success(), "{}", utf8(&output.stderr)); assert!(utf8(&output.stdout).contains("Type: Map")); let notation = OSDParser::serialize_llsd_notation(expected).expect("serialize notation fixture"); let output = run(&["validate", "-"], Some(notation.as_bytes())); assert!(output.status.success(), "{}", utf8(&output.stderr)); assert!(utf8(&output.stdout).contains("Type: Map")); } #[test] #[allow(clippy::float_cmp)] // Exact integral coordinates are part of the golden sample. fn sample_primitive_round_trips_through_the_public_native_api() { let output = run(&["prim-to-osd"], None); assert!(output.status.success(), "{}", utf8(&output.stderr)); let osd = OSDParser::deserialize_json_with_string( String::from_utf8(output.stdout.clone()).expect("sample command emits UTF-8"), ) .expect("sample command emits JSON"); let primitive = Primitive::from_osd(osd).expect("sample is native Primitive OSD"); assert_eq!(primitive.local_id, 12_345); assert_eq!(primitive.position.x, 128.0); assert_eq!(primitive.position.y, 128.0); assert_eq!(primitive.position.z, 25.0); assert_eq!( primitive .properties .as_ref() .map(|value| value.name.as_str()), Some("Example Cube") ); let directory = TestDir::new("primitive"); let path = directory.path("cube.json"); fs::write(&path, output.stdout).expect("write primitive JSON"); let output = run(&["osd-to-prim", path_text(&path)], None); assert!(output.status.success(), "{}", utf8(&output.stderr)); let summary = utf8(&output.stdout); for expected in [ "Successfully parsed Primitive:", "Name: Example Cube", "Position: <128, 128, 25>", "Scale: <1, 1, 1>", "Material: Wood", "PCode: Prim", ] { assert!( summary.contains(expected), "summary omitted {expected}:\n{summary}" ); } } #[test] fn failures_use_stderr_and_documented_exit_codes() { let directory = TestDir::new("failures"); let malformed = directory.path("malformed.json"); fs::write(&malformed, b"{\"unterminated\": [1, 2}").expect("write malformed fixture"); let output = run(&["validate", path_text(&malformed)], None); assert_exit(&output, EXIT_INVALID_OSD); assert!(output.stdout.is_empty()); assert!(utf8(&output.stderr).contains("invalid OSD input")); let primitive = directory.path("not-a-primitive.json"); fs::write(&primitive, b"[1,2,3]").expect("write non-primitive fixture"); let output = run(&["osd-to-prim", path_text(&primitive)], None); assert_exit(&output, EXIT_INVALID_PRIMITIVE); assert!(utf8(&output.stderr).contains("not a Primitive map")); let missing = directory.path("missing.json"); let output = run(&["inspect", path_text(&missing)], None); assert_exit(&output, EXIT_IO); assert!(utf8(&output.stderr).contains("opening")); let input = write_fixture(&directory); let output = run( &["--max-input-bytes", "4", "validate", path_text(&input)], None, ); assert_exit(&output, EXIT_INVALID_OSD); assert!(utf8(&output.stderr).contains("4-byte limit")); let output = run( &[ "convert", path_text(&input), "json", path_text(&directory.0), ], None, ); assert_exit(&output, EXIT_IO); assert!(utf8(&output.stderr).contains("creating")); } #[test] fn scalar_json_is_not_confused_with_llsd_notation() { for (input, expected_type) in [ (b"true".as_slice(), "Boolean"), (b"42", "Integer"), (br#""text""#, "String"), (b"null", "Unknown"), ] { let output = run(&["validate", "-"], Some(input)); assert!(output.status.success(), "{}", utf8(&output.stderr)); assert!(utf8(&output.stdout).contains(&format!("Type: {expected_type}"))); } for invalid in [b"".as_slice(), b" \t\r\n"] { let output = run(&["validate", "-"], Some(invalid)); assert_exit(&output, EXIT_INVALID_OSD); } } #[test] fn inspected_fixture_retains_expected_value_types() { let expected = OSDParser::deserialize_json_with_string( std::str::from_utf8(JSON_FIXTURE) .expect("compatibility fixture is UTF-8") .to_owned(), ) .expect("parse checked-in compatibility fixture"); let OSD::Map(values) = expected else { panic!("compatibility fixture must remain a map"); }; assert!(matches!(values.get("array"), Some(OSD::Array(_)))); assert!(matches!(values.get("false"), Some(OSD::Boolean(false)))); assert!(matches!(values.get("true"), Some(OSD::Boolean(true)))); assert!(matches!(values.get("zero"), Some(OSD::Integer(0)))); }