Implement native PrimInspector
All checks were successful
Native Rust workspace compile / compile (push) Successful in 21m40s
All checks were successful
Native Rust workspace compile / compile (push) Successful in 21m40s
This commit is contained in:
260
programs/tests/prim_inspector_cli.rs
Normal file
260
programs/tests/prim_inspector_cli.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
const EXIT_USAGE: i32 = 2;
|
||||
const EXIT_INPUT: i32 = 3;
|
||||
const EXIT_OUTPUT: i32 = 5;
|
||||
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestDir(PathBuf);
|
||||
|
||||
impl TestDir {
|
||||
fn new(name: &str) -> Self {
|
||||
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"metacrate-prim-inspector-{name}-{}-{id}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&path).expect("create 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]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_prim-inspector"))
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("run prim-inspector")
|
||||
}
|
||||
|
||||
fn text(bytes: &[u8]) -> &str {
|
||||
std::str::from_utf8(bytes).expect("UTF-8 output")
|
||||
}
|
||||
|
||||
fn path_text(path: &Path) -> &str {
|
||||
path.to_str().expect("UTF-8 path")
|
||||
}
|
||||
|
||||
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 fixture(directory: &TestDir) -> PathBuf {
|
||||
let path = directory.path("objects.json");
|
||||
fs::write(
|
||||
&path,
|
||||
r#"{
|
||||
"region":"Fixture Region",
|
||||
"agent_position":[0,0,0],
|
||||
"objects":[
|
||||
{
|
||||
"id":"11111111-1111-1111-1111-111111111111",
|
||||
"localid":30,
|
||||
"name":"Far Lamp",
|
||||
"description":"A bright fixture",
|
||||
"position":[6,8,0],
|
||||
"rotation":[0,0,0,1],
|
||||
"scale":[1,2,3],
|
||||
"parentid":7,
|
||||
"material":3,
|
||||
"pcode":9,
|
||||
"physical":true,
|
||||
"light":{"color":[0.2,0.4,0.6,1],"intensity":0.75,"radius":12,"cutoff":0.5,"falloff":0.25},
|
||||
"flex":{"simulate_lod":2,"gravity":-0.5,"air_friction":1,"wind_sensitivity":0.2,"tension":0.8,"user_force":[0,0,0]},
|
||||
"owner":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
|
||||
"creator":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
|
||||
"sale_type":"copy",
|
||||
"sale_price":25,
|
||||
"hover_text":"Welcome visitor",
|
||||
"property_state":"ready"
|
||||
},
|
||||
{
|
||||
"id":"22222222-2222-2222-2222-222222222222",
|
||||
"localid":20,
|
||||
"name":"Near Lamp",
|
||||
"description":"Hydrated later",
|
||||
"position":[3,4,0],
|
||||
"rotation":[0,0,0,1],
|
||||
"scale":[1,1,1],
|
||||
"sculpt":{"texture":"cccccccc-cccc-cccc-cccc-cccccccccccc","type":5},
|
||||
"owner":"dddddddd-dddd-dddd-dddd-dddddddddddd",
|
||||
"creator":"eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
|
||||
"property_state":"delayed"
|
||||
},
|
||||
{
|
||||
"id":"33333333-3333-3333-3333-333333333333",
|
||||
"localid":10,
|
||||
"name":"Lost Lamp",
|
||||
"position":[1,0,0],
|
||||
"rotation":[0,0,0,1],
|
||||
"scale":[1,1,1],
|
||||
"property_state":"missing"
|
||||
},
|
||||
{
|
||||
"id":"44444444-4444-4444-4444-444444444444",
|
||||
"localid":40,
|
||||
"name":"Chair",
|
||||
"position":[-1,0,0],
|
||||
"rotation":[0,0,0,1],
|
||||
"scale":[1,1,1]
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.expect("write fixture");
|
||||
path
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_preserves_upstream_arguments_and_documents_bounded_controls() {
|
||||
let output = run(&["--help"]);
|
||||
assert!(output.status.success());
|
||||
let help = text(&output.stdout);
|
||||
for marker in [
|
||||
"[FIRSTNAME]",
|
||||
"[LASTNAME]",
|
||||
"[PASSWORD]",
|
||||
"[SEARCH_TERM]",
|
||||
"--fake-grid",
|
||||
"--search",
|
||||
"--limit",
|
||||
"--discovery-delay-ms",
|
||||
"--property-timeout-ms",
|
||||
"--max-output-bytes",
|
||||
] {
|
||||
assert!(help.contains(marker), "help omitted {marker}:\n{help}");
|
||||
}
|
||||
|
||||
let output = run(&[]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(text(&output.stderr).contains("Usage: prim-inspector"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fake_grid_golden_output_hydrates_orders_filters_and_formats_details() {
|
||||
let directory = TestDir::new("golden");
|
||||
let fixture = fixture(&directory);
|
||||
let output = run(&[
|
||||
"--fake-grid",
|
||||
path_text(&fixture),
|
||||
"--search",
|
||||
"Lamp",
|
||||
"--limit",
|
||||
"2",
|
||||
"--property-timeout-ms",
|
||||
"17",
|
||||
]);
|
||||
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||
assert!(output.stderr.is_empty());
|
||||
let output = text(&output.stdout);
|
||||
|
||||
for expected in [
|
||||
"Fake grid connected",
|
||||
"Region: Fixture Region",
|
||||
"Agent position: <0.000, 0.000, 0.000>",
|
||||
"Found 4 primitives",
|
||||
"CALL select-object local_id=20",
|
||||
"PROPERTY received local_id=20",
|
||||
"CALL select-object local_id=10",
|
||||
"Property timeout after 17 ms: 1 primitives remain unknown",
|
||||
"Inspecting 2 nearest matching primitives",
|
||||
"Object: Near Lamp (ID: 22222222-2222-2222-2222-222222222222)",
|
||||
"Distance: 5.00 m",
|
||||
"Sculpt Type: Mesh",
|
||||
"Sculpt Texture: cccccccc-cccc-cccc-cccc-cccccccccccc",
|
||||
"Owner: dddddddd-dddd-dddd-dddd-dddddddddddd",
|
||||
"Description: Hydrated later",
|
||||
"Object: Far Lamp (ID: 11111111-1111-1111-1111-111111111111)",
|
||||
"Distance: 10.00 m",
|
||||
"Parent ID: 7",
|
||||
"Material:",
|
||||
"PCode: Prim",
|
||||
"Light Intensity: 0.750",
|
||||
"Light Radius: 12.000",
|
||||
"Flexible Softness: 2",
|
||||
"Flexible Gravity: -0.500",
|
||||
"For Sale: Copy for L$25",
|
||||
"Hover Text: Welcome visitor",
|
||||
"Fake grid logout complete; active_tasks=0 open_sockets=0",
|
||||
] {
|
||||
assert!(
|
||||
output.contains(expected),
|
||||
"output omitted {expected}:\n{output}"
|
||||
);
|
||||
}
|
||||
assert!(output.find("Object: Near Lamp").unwrap() < output.find("Object: Far Lamp").unwrap());
|
||||
assert!(!output.contains("Object: Chair"));
|
||||
assert!(!output.contains("Object: Lost Lamp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nearest_limit_has_deterministic_distance_and_local_id_tie_breaking() {
|
||||
let directory = TestDir::new("ordering");
|
||||
let fixture = fixture(&directory);
|
||||
let output = run(&["--fake-grid", path_text(&fixture), "--limit", "3"]);
|
||||
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||
let output = text(&output.stdout);
|
||||
let lost = output.find("Object: Unknown").unwrap();
|
||||
let chair = output.find("Object: Chair").unwrap();
|
||||
let near = output.find("Object: Near Lamp").unwrap();
|
||||
assert!(lost < chair && chair < near, "unexpected order:\n{output}");
|
||||
assert!(!output.contains("Object: Far Lamp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_limits_and_live_credentials_fail_without_partial_or_secret_output() {
|
||||
let directory = TestDir::new("invalid");
|
||||
let malformed = directory.path("malformed.json");
|
||||
fs::write(&malformed, "{not-json}").expect("write malformed fixture");
|
||||
let output = run(&["--fake-grid", path_text(&malformed)]);
|
||||
assert_exit(&output, EXIT_INPUT);
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(text(&output.stderr).contains("could not parse LLSD JSON"));
|
||||
|
||||
let fixture = fixture(&directory);
|
||||
let output = run(&[
|
||||
"--fake-grid",
|
||||
path_text(&fixture),
|
||||
"--max-output-bytes",
|
||||
"10",
|
||||
]);
|
||||
assert_exit(&output, EXIT_OUTPUT);
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(text(&output.stderr).contains("output byte limit"));
|
||||
|
||||
let password = "do-not-print-this-password";
|
||||
let output = run(&[
|
||||
"First",
|
||||
"Last",
|
||||
password,
|
||||
"--fake-grid",
|
||||
path_text(&fixture),
|
||||
]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
assert!(!text(&output.stdout).contains(password));
|
||||
assert!(!text(&output.stderr).contains(password));
|
||||
|
||||
let output = run(&["--fake-grid", path_text(&fixture), "--limit", "0"]);
|
||||
assert_exit(&output, EXIT_USAGE);
|
||||
}
|
||||
Reference in New Issue
Block a user