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
417 lines
14 KiB
Rust
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))));
|
|
}
|