Implement TestClient world commands (#93)
Some checks failed
Native code generation / deterministic (push) Failing after 31s
Imaging and meshing gate / native (push) Successful in 5m30s
JPEG 2000 feature / linux (push) Successful in 2m47s
Native Rust workspace compile / compile (push) Successful in 22m56s
Skia feature / linux (push) Successful in 31m28s
Some checks failed
Native code generation / deterministic (push) Failing after 31s
Imaging and meshing gate / native (push) Successful in 5m30s
JPEG 2000 feature / linux (push) Successful in 2m47s
Native Rust workspace compile / compile (push) Successful in 22m56s
Skia feature / linux (push) Successful in 31m28s
This commit is contained in:
327
programs/tests/test_client_world_cli.rs
Normal file
327
programs/tests/test_client_world_cli.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
use libremetaverse_imaging::{J2kCodec, J2kEncodeOptions, ManagedImage, ManagedImageImageChannels};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
const CLIENT: &str = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
|
||||
const TARGET: &str = "11111111-2222-3333-4444-555555555555";
|
||||
const ROOT: &str = "90000000-0000-0000-0000-000000000001";
|
||||
const CHILD: &str = "90000000-0000-0000-0000-000000000002";
|
||||
const REMOVE: &str = "90000000-0000-0000-0000-000000000003";
|
||||
const TEXTURE: &str = "80000000-0000-0000-0000-000000000001";
|
||||
const COVENANT: &str = "70000000-0000-0000-0000-000000000001";
|
||||
const LAYER: &str = "60000000-0000-0000-0000-000000000001";
|
||||
const LANDMARK: &str = "50000000-0000-0000-0000-000000000001";
|
||||
const HANDLE: &str = "4294967298000";
|
||||
|
||||
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-test-client-world-{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)
|
||||
}
|
||||
|
||||
fn write(&self, name: &str, contents: impl AsRef<[u8]>) -> PathBuf {
|
||||
let path = self.path(name);
|
||||
fs::write(&path, contents).expect("write fixture");
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn path_text(path: &Path) -> &str {
|
||||
path.to_str().expect("UTF-8 test path")
|
||||
}
|
||||
|
||||
fn run(args: &[&str]) -> Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_test-client"))
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("run test-client")
|
||||
}
|
||||
|
||||
fn stdout(output: &Output) -> &str {
|
||||
std::str::from_utf8(&output.stdout).expect("UTF-8 stdout")
|
||||
}
|
||||
|
||||
fn stderr(output: &Output) -> &str {
|
||||
std::str::from_utf8(&output.stderr).expect("UTF-8 stderr")
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
const DIGITS: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
output.push(char::from(DIGITS[usize::from(byte >> 4)]));
|
||||
output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn one_pixel_jp2() -> Vec<u8> {
|
||||
let mut image =
|
||||
ManagedImage::new(1, 1, ManagedImageImageChannels::COLOR).expect("create image");
|
||||
image.red[0] = 10;
|
||||
image.green[0] = 20;
|
||||
image.blue[0] = 30;
|
||||
J2kCodec::encode(&image, J2kEncodeOptions::default()).expect("encode JPEG2000")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn fake_grid_exercises_every_owned_world_command() {
|
||||
let directory = TestDir::new("commands");
|
||||
let terrain_source = directory.write("terrain.raw", [9, 8, 7, 6]);
|
||||
let terrain_download = directory.path("downloaded.raw");
|
||||
let texture_download = directory.path("download.jp2");
|
||||
let export_one = directory.path("export-one.xml");
|
||||
let export_two = directory.path("export-two.xml");
|
||||
let texture = one_pixel_jp2();
|
||||
let script = directory.write(
|
||||
"terminal.tsv",
|
||||
format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\n\
|
||||
!person\tTarget Resident\t{TARGET}\n\
|
||||
!world-region\t{CLIENT}\t{HANDLE}\tTest Region\t128\t128\t25\t1.5\t-2.5\n\
|
||||
!world-avatar\t{CLIENT}\t{TARGET}\tTarget Resident\t132\t132\t25\n\
|
||||
!world-prim\t{CLIENT}\t{ROOT}\t100\t0\t{CLIENT}\tCube Root\tExport root\t130\t130\t25\t{TEXTURE}\ttrue\n\
|
||||
!world-prim\t{CLIENT}\t{CHILD}\t101\t100\t{CLIENT}\tCube Child\tExport child\t1\t0\t0\t{TEXTURE}\tfalse\n\
|
||||
!world-prim\t{CLIENT}\t{REMOVE}\t102\t0\t{CLIENT}\tRemove Me\tDerez target\t131\t130\t25\t{TEXTURE}\tfalse\n\
|
||||
!world-parcel\t{CLIENT}\t1\t{CLIENT}\tHome Parcel\tFixture parcel\t2.5\t4096\t3\t117\n\
|
||||
!world-parcel-owner\t{CLIENT}\t1\t{CLIENT}\t3\n\
|
||||
!world-parcel-object\t{CLIENT}\t1\t{CLIENT}\t100,101,102\n\
|
||||
!world-grid-region\t{CLIENT}\tTest Region\t{HANDLE}\t1000\t2000\t3\n\
|
||||
!world-layer\t{CLIENT}\t{LAYER}\t0\t0\t1024\t1024\n\
|
||||
!world-agent-location\t{CLIENT}\t{HANDLE}\t3\t128\t128\n\
|
||||
!world-estate\t{CLIENT}\tFixture Estate\t{CLIENT}\t{COVENANT}\t12345\tBe kind.\t0102\n\
|
||||
!world-asset\t{CLIENT}\t{TEXTURE}\t{}\n\
|
||||
!world-syntax\t{CLIENT}\tdefault,state_entry,llSay\n\
|
||||
downloadterrain 1000 \"{}\" --confirm\n\
|
||||
getestatecovenant 1\n\
|
||||
uploadterrain \"{}\" --confirm\n\
|
||||
agentlocations\n\
|
||||
findsim Test Region\n\
|
||||
gridlayer\n\
|
||||
gridmap\n\
|
||||
parceldetails 1\n\
|
||||
parcelinfo\n\
|
||||
primowners 1\n\
|
||||
selectobjects 1 {CLIENT}\n\
|
||||
syntaxid\n\
|
||||
wind\n\
|
||||
back 0 --confirm\n\
|
||||
crossregion north fly --confirm\n\
|
||||
crouch start --confirm\n\
|
||||
fly start --confirm\n\
|
||||
flyto 130 130 25 1 --confirm\n\
|
||||
follow Target Resident --confirm\n\
|
||||
follow off --confirm\n\
|
||||
forward 0 --confirm\n\
|
||||
gohome --confirm\n\
|
||||
goto Test Region/128/128/25 --confirm\n\
|
||||
goto_landmark {LANDMARK} --confirm\n\
|
||||
jump --confirm\n\
|
||||
left 0 --confirm\n\
|
||||
location\n\
|
||||
moveto 10 11 12 --confirm\n\
|
||||
right 0 --confirm\n\
|
||||
sethome --confirm\n\
|
||||
sit --confirm\n\
|
||||
siton {ROOT} --confirm\n\
|
||||
stand --confirm\n\
|
||||
turnto 140 141 25 --confirm\n\
|
||||
changeperms {ROOT} copy mod xfer --confirm\n\
|
||||
derez {REMOVE} --confirm\n\
|
||||
downloadtexture {TEXTURE} 0 \"{}\"\n\
|
||||
export {ROOT} \"{}\"\n\
|
||||
export {ROOT} \"{}\"\n\
|
||||
exportparticles {ROOT}\n\
|
||||
findobjects 4096 Cube\n\
|
||||
findtexture 0 {TEXTURE}\n\
|
||||
import \"{}\" usegroup --confirm\n\
|
||||
primcount\n\
|
||||
priminfo {ROOT}\n\
|
||||
primregex Cube\n\
|
||||
textures on\n\
|
||||
textures off\n\
|
||||
tree Oak --confirm\n\
|
||||
quit\n",
|
||||
hex(&texture),
|
||||
path_text(&terrain_download),
|
||||
path_text(&terrain_source),
|
||||
path_text(&texture_download),
|
||||
path_text(&export_one),
|
||||
path_text(&export_two),
|
||||
path_text(&export_one),
|
||||
),
|
||||
);
|
||||
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--allow-spending",
|
||||
"--allow-estate-actions",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
assert!(output.stderr.is_empty(), "{}", stderr(&output));
|
||||
let text = stdout(&output);
|
||||
for expected in [
|
||||
"Terrain file Test Region (2 bytes) downloaded successfully",
|
||||
"Estate name: Fixture Estate",
|
||||
"Terrain raw file uploaded and applied",
|
||||
"3 avatar(s) at 128,128",
|
||||
"Test Region: handle=4294967298000",
|
||||
"Received 1 layer chunks",
|
||||
"Received 1 grid regions",
|
||||
"Name = Home Parcel",
|
||||
"Downloaded 1 Parcels in Test Region",
|
||||
"Owner: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee Count: 3",
|
||||
"Found a total of 3 Objects",
|
||||
"llSay",
|
||||
"Local wind speed is Vector2",
|
||||
"Following Target Resident",
|
||||
"CurrentSim: 'Test Region'",
|
||||
"Set permissions to PermissionMask",
|
||||
"Removing Remove Me",
|
||||
"Saved",
|
||||
"Exported 2 prims",
|
||||
"llParticleSystem",
|
||||
"Done searching; found 2 objects",
|
||||
"Done searching; found 2 faces",
|
||||
"Import complete.",
|
||||
"Tracking a total of",
|
||||
"ID: 90000000-0000-0000-0000-000000000001",
|
||||
"Texture downloading is on",
|
||||
"Texture downloading is off",
|
||||
"CALL estate-upload-terrain terrain.raw",
|
||||
"CALL world-query download-terrain",
|
||||
"CALL world-query estate-covenant",
|
||||
"CALL world-query agent-locations",
|
||||
"CALL world-query find-region",
|
||||
"CALL world-query grid-layer",
|
||||
"CALL world-query grid-map",
|
||||
"CALL world-query parcel-owners",
|
||||
"CALL world-query parcel-objects",
|
||||
"CALL movement-back duration-ms=0",
|
||||
"CALL movement-cross Vector3",
|
||||
"CALL movement-crouch true",
|
||||
"CALL movement-fly true",
|
||||
"CALL movement-flyto Vector3",
|
||||
"CALL movement-follow 11111111-2222-3333-4444-555555555555",
|
||||
"CALL movement-follow off",
|
||||
"CALL movement-forward duration-ms=0",
|
||||
"CALL teleport-home",
|
||||
"CALL teleport-region Test Region",
|
||||
"CALL teleport-landmark 50000000-0000-0000-0000-000000000001",
|
||||
"CALL movement-jump",
|
||||
"CALL movement-left duration-ms=0",
|
||||
"CALL movement-autopilot [1010.0, 2011.0, 12.0]",
|
||||
"CALL movement-right duration-ms=0",
|
||||
"CALL movement-set-home",
|
||||
"CALL movement-sit 90000000-0000-0000-0000-000000000001",
|
||||
"CALL movement-stand",
|
||||
"CALL movement-turn Vector3",
|
||||
"CALL object-permissions 90000000-0000-0000-0000-000000000001",
|
||||
"CALL object-derez 90000000-0000-0000-0000-000000000003",
|
||||
"CALL texture-download 80000000-0000-0000-0000-000000000001",
|
||||
"CALL object-import count=2 use-group=true",
|
||||
"CALL texture-stream enabled=true",
|
||||
"CALL texture-stream enabled=false",
|
||||
"CALL object-tree Oak",
|
||||
] {
|
||||
assert!(text.contains(expected), "missing {expected:?} in:\n{text}");
|
||||
}
|
||||
assert_eq!(
|
||||
text.matches("CALL world-query parcels").count(),
|
||||
2,
|
||||
"{text}"
|
||||
);
|
||||
assert_eq!(text.matches("CALL movement-sit ").count(), 2, "{text}");
|
||||
assert_eq!(
|
||||
text.matches("CALL texture-download 80000000-0000-0000-0000-000000000001")
|
||||
.count(),
|
||||
3,
|
||||
"{text}"
|
||||
);
|
||||
assert_eq!(fs::read(&terrain_download).expect("terrain output"), [1, 2]);
|
||||
assert_eq!(
|
||||
fs::read(&texture_download).expect("texture output"),
|
||||
texture
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(&export_one).expect("first export"),
|
||||
fs::read(&export_two).expect("second export"),
|
||||
"exports must be deterministic"
|
||||
);
|
||||
assert!(directory.path(&format!("{TEXTURE}.jp2")).is_file());
|
||||
assert!(directory.path(&format!("{TEXTURE}.tga")).is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn privileged_world_mutations_require_every_gate_and_confirmation() {
|
||||
let directory = TestDir::new("guards");
|
||||
let terrain = directory.write("terrain.raw", [1, 2, 3]);
|
||||
let script = directory.write(
|
||||
"guards.tsv",
|
||||
format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\n\
|
||||
!world-region\t{CLIENT}\t{HANDLE}\tTest Region\t128\t128\t25\t0\t0\n\
|
||||
!world-estate\t{CLIENT}\tFixture Estate\t{CLIENT}\t{COVENANT}\t1\tCovenant\t0102\n\
|
||||
forward 1 --confirm\n\
|
||||
uploadterrain \"{}\" --confirm\n\
|
||||
quit\n",
|
||||
path_text(&terrain),
|
||||
),
|
||||
);
|
||||
let output = run(&["--fake-script", path_text(&script)]);
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
let text = stdout(&output);
|
||||
assert!(text.contains("Live world mutation blocked"), "{text}");
|
||||
assert!(!text.contains("CALL movement-forward"), "{text}");
|
||||
assert!(!text.contains("CALL estate-upload-terrain"), "{text}");
|
||||
|
||||
let script = directory.write(
|
||||
"confirm.tsv",
|
||||
format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\n\
|
||||
!world-region\t{CLIENT}\t{HANDLE}\tTest Region\t128\t128\t25\t0\t0\n\
|
||||
!world-estate\t{CLIENT}\tFixture Estate\t{CLIENT}\t{COVENANT}\t1\tCovenant\t0102\n\
|
||||
uploadterrain \"{}\"\n\
|
||||
quit\n",
|
||||
path_text(&terrain),
|
||||
),
|
||||
);
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--allow-spending",
|
||||
"--allow-estate-actions",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
assert!(output.status.success(), "{}", stderr(&output));
|
||||
let text = stdout(&output);
|
||||
assert!(text.contains("explicit --confirm"), "{text}");
|
||||
assert!(!text.contains("CALL estate-upload-terrain"), "{text}");
|
||||
}
|
||||
Reference in New Issue
Block a user