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