Files
MetaCrate/programs/tests/osd_inspector_cli.rs
Chili Palmer f93a27f469
Some checks failed
Native code generation / deterministic (push) Successful in 18m30s
Imaging and meshing gate / native (push) Successful in 5m41s
JPEG 2000 feature / linux (push) Successful in 2m53s
Skia feature / linux (push) Has been cancelled
Native Rust workspace compile / compile (push) Has been cancelled
Implement OSDInspector offline tool (#85)
2026-08-11 06:45:22 +00:00

417 lines
14 KiB
Rust

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<u8>); 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))));
}