All checks were successful
Native Rust workspace compile / compile (push) Successful in 21m40s
261 lines
7.7 KiB
Rust
261 lines
7.7 KiB
Rust
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);
|
|
}
|