335 lines
13 KiB
Rust
335 lines
13 KiB
Rust
use libremetaverse_imaging::{
|
|
J2kEncodeOptions, ManagedImage, ManagedImageImageChannels, RustJ2kCodec,
|
|
};
|
|
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 MASTER: &str = "11111111-2222-3333-4444-555555555555";
|
|
const RECIPIENT: &str = "22222222-3333-4444-5555-666666666666";
|
|
const ROOT: &str = "10000000-0000-0000-0000-000000000001";
|
|
const OUTFITS: &str = "10000000-0000-0000-0000-000000000002";
|
|
const TRASH: &str = "10000000-0000-0000-0000-000000000003";
|
|
const LOST: &str = "10000000-0000-0000-0000-000000000004";
|
|
const NOTES: &str = "10000000-0000-0000-0000-000000000005";
|
|
const SCRIPTS: &str = "10000000-0000-0000-0000-000000000006";
|
|
const TEXTURES: &str = "10000000-0000-0000-0000-000000000007";
|
|
const OBJECTS: &str = "10000000-0000-0000-0000-000000000008";
|
|
const DELETE_ME: &str = "10000000-0000-0000-0000-000000000009";
|
|
const GIVE_ITEM: &str = "20000000-0000-0000-0000-000000000001";
|
|
const NOTE_ITEM: &str = "20000000-0000-0000-0000-000000000002";
|
|
const SCRIPT_ITEM: &str = "20000000-0000-0000-0000-000000000003";
|
|
const OUTFIT_ITEM: &str = "20000000-0000-0000-0000-000000000004";
|
|
const NOTE_ASSET: &str = "30000000-0000-0000-0000-000000000001";
|
|
const SCRIPT_ASSET: &str = "30000000-0000-0000-0000-000000000002";
|
|
const OBJECT_ASSET: &str = "30000000-0000-0000-0000-000000000003";
|
|
const TEXTURE_ASSET: &str = "30000000-0000-0000-0000-000000000004";
|
|
const AVATAR: &str = "40000000-0000-0000-0000-000000000001";
|
|
const ATTACHMENT: &str = "40000000-0000-0000-0000-000000000002";
|
|
const TASK: &str = "50000000-0000-0000-0000-000000000001";
|
|
const TASK_SCRIPT: &str = "50000000-0000-0000-0000-000000000002";
|
|
const SESSION: &str = "60000000-0000-0000-0000-000000000001";
|
|
const CREATED_NOTE_ITEM: &str = "00000000-0000-0000-0200-00000000e01e";
|
|
const UPLOADED_TEXTURE_ASSET: &str = "00000000-0000-0000-0500-0000000050a5";
|
|
|
|
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-inventory-{name}-{}-{id}",
|
|
std::process::id()
|
|
));
|
|
fs::create_dir(&path).expect("create test directory");
|
|
Self(path)
|
|
}
|
|
|
|
fn write(&self, name: &str, contents: impl AsRef<[u8]>) -> PathBuf {
|
|
let path = self.0.join(name);
|
|
fs::write(&path, contents).expect("write fixture");
|
|
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 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 notecard(body: &str) -> Vec<u8> {
|
|
format!(
|
|
"Linden text version 2\n{{\nLLEmbeddedItems version 1\n{{\ncount 0\n}}\nText length {}\n{}\n}}\n",
|
|
body.len(),
|
|
body
|
|
)
|
|
.into_bytes()
|
|
}
|
|
|
|
fn one_pixel_jp2() -> Vec<u8> {
|
|
let mut image =
|
|
ManagedImage::new(1, 1, ManagedImageImageChannels::COLOR).expect("create managed image");
|
|
image.red[0] = 10;
|
|
image.green[0] = 20;
|
|
image.blue[0] = 30;
|
|
RustJ2kCodec::encode(&image, J2kEncodeOptions::default()).expect("encode JPEG2000 fixture")
|
|
}
|
|
|
|
fn one_pixel_tga() -> Vec<u8> {
|
|
let mut bytes = vec![0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 32, 0x28];
|
|
bytes.extend_from_slice(&[30, 20, 10, 255]);
|
|
bytes
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::too_many_lines)]
|
|
fn fake_grid_exercises_every_owned_inventory_appearance_and_asset_command() {
|
|
let directory = TestDir::new("commands");
|
|
let note_source = directory.write("new note.txt", "created note body");
|
|
let script_source = directory.write("new script.lsl", "default { state_entry() {} }");
|
|
let image_source = directory.write("image.tga", one_pixel_tga());
|
|
let nested_script = directory.write("nested.tc", "balance\nls -l\n");
|
|
let download = directory.path("downloaded-note.asset");
|
|
let xfer = directory.path("object.xfer");
|
|
let backup = directory.path("backup");
|
|
let outfit = directory.path("outfit");
|
|
let jp2 = one_pixel_jp2();
|
|
let script = directory.write(
|
|
"terminal.tsv",
|
|
format!(
|
|
"!client\t{CLIENT}\tAlice\tBot\tMaster Resident\t{MASTER}\tfalse\n\
|
|
!inventory-root\t{CLIENT}\t{ROOT}\t{CLIENT}\tMy Inventory\n\
|
|
!inventory-folder\t{CLIENT}\t{OUTFITS}\t{ROOT}\toutfit\tOutfits\n\
|
|
!inventory-folder\t{CLIENT}\t{TRASH}\t{ROOT}\ttrash\tTrash\n\
|
|
!inventory-folder\t{CLIENT}\t{LOST}\t{ROOT}\tlostandfound\tLost And Found\n\
|
|
!inventory-folder\t{CLIENT}\t{NOTES}\t{ROOT}\tnotecard\tNotecards\n\
|
|
!inventory-folder\t{CLIENT}\t{SCRIPTS}\t{ROOT}\tscript\tScripts\n\
|
|
!inventory-folder\t{CLIENT}\t{TEXTURES}\t{ROOT}\ttexture\tTextures\n\
|
|
!inventory-folder\t{CLIENT}\t{OBJECTS}\t{ROOT}\tobject\tObjects\n\
|
|
!inventory-folder\t{CLIENT}\t{DELETE_ME}\t{ROOT}\tnone\tDelete Me\n\
|
|
!inventory-item\t{CLIENT}\t{GIVE_ITEM}\t{ROOT}\t{OBJECT_ASSET}\tobject\tobject\t0008e000\tGiveable\tTransfer me\n\
|
|
!inventory-item\t{CLIENT}\t{NOTE_ITEM}\t{NOTES}\t{NOTE_ASSET}\tnotecard\tnotecard\t0008e000\tFixture Note\tA note\n\
|
|
!inventory-item\t{CLIENT}\t{SCRIPT_ITEM}\t{SCRIPTS}\t{SCRIPT_ASSET}\tlsltext\tlsl\t0008e000\tFixture Script\tA script\n\
|
|
!inventory-item\t{CLIENT}\t{OUTFIT_ITEM}\t{OUTFITS}\t{TEXTURE_ASSET}\tclothing\twearable\t0008e000\tShirt\tOutfit shirt\n\
|
|
!asset\t{CLIENT}\t{NOTE_ASSET}\tnotecard\t{}\n\
|
|
!asset\t{CLIENT}\t{SCRIPT_ASSET}\tlsltext\t{}\n\
|
|
!asset\t{CLIENT}\t{OBJECT_ASSET}\tobject\t{}\n\
|
|
!asset\t{CLIENT}\t{TEXTURE_ASSET}\ttexture\t{}\n\
|
|
!balance\t{CLIENT}\t75\n\
|
|
!avatar\t{CLIENT}\t{AVATAR}\tTarget Resident\tHead={TEXTURE_ASSET}\n\
|
|
!appearance-cache\t{CLIENT}\t{AVATAR}\n\
|
|
!attachment\t{CLIENT}\tChest\t42\t{ATTACHMENT}\t<1,2,3>\n\
|
|
!task\t{CLIENT}\t{TASK}\t99\n\
|
|
!task-item\t{CLIENT}\t{TASK}\t{TASK_SCRIPT}\tlsltext\ttrue\tRunning Script\tTask script\n\
|
|
appearance rebake --confirm\n\
|
|
attachments\n\
|
|
avatarinfo Target Resident\n\
|
|
clone Target Resident --confirm\n\
|
|
wear /Outfits --confirm\n\
|
|
backuptext to \"{}\"\n\
|
|
backuptext status\n\
|
|
backuptext abort\n\
|
|
balance\n\
|
|
cd Outfits\n\
|
|
ls\n\
|
|
cd /\n\
|
|
ls -l\n\
|
|
i\n\
|
|
give {RECIPIENT} Giveable --confirm\n\
|
|
download {NOTE_ASSET} notecard \"{}\"\n\
|
|
xfer {OBJECT_ASSET} \"{}\"\n\
|
|
viewnote {NOTE_ITEM}\n\
|
|
objectinventory {TASK}\n\
|
|
taskrunning {TASK}\n\
|
|
taskrunning {TASK} \"Running Script\" false --confirm\n\
|
|
taskrunning {TASK}\n\
|
|
createnotecard \"{}\" {GIVE_ITEM} --confirm\n\
|
|
viewnote {CREATED_NOTE_ITEM}\n\
|
|
uploadscript \"{}\" --confirm\n\
|
|
uploadimage TestTexture 30000 \"{}\" --confirm\n\
|
|
!avatar\t{CLIENT}\t{AVATAR}\tTarget Resident\tUploaded={UPLOADED_TEXTURE_ASSET}\n\
|
|
dumpoutfit {AVATAR} \"{}\"\n\
|
|
script \"{}\"\n\
|
|
deletefolder \"Delete Me\" --confirm\n\
|
|
emptylostandfound --confirm\n\
|
|
emptytrash --confirm\n\
|
|
i\n\
|
|
!im\t{CLIENT}\t{MASTER}\tMaster Resident\tagent\tfalse\tgiveall --confirm\t{SESSION}\n\
|
|
balance\n\
|
|
quit\n",
|
|
hex(¬ecard("fixture note body")),
|
|
hex(b"default { state_entry() {} }"),
|
|
hex(b"fake object asset"),
|
|
hex(&jp2),
|
|
path_text(&backup),
|
|
path_text(&download),
|
|
path_text(&xfer),
|
|
path_text(¬e_source),
|
|
path_text(&script_source),
|
|
path_text(&image_source),
|
|
path_text(&outfit),
|
|
path_text(&nested_script),
|
|
),
|
|
);
|
|
|
|
let output = run(&[
|
|
"--allow-live-mutations",
|
|
"--allow-spending",
|
|
"--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 [
|
|
"Appearance sequence started",
|
|
"Found 1 attachments",
|
|
"Head: 30000000-0000-0000-0000-000000000004",
|
|
"Cloned Target Resident",
|
|
"Starting to change outfit to /Outfits",
|
|
"backuptext completed: found 2, transferred 2, errors 0",
|
|
"Last backup: found 2, transferred 2, errors 0",
|
|
"No backup is currently running",
|
|
"Balance is L$: 75",
|
|
"Current folder: Outfits",
|
|
"Shirt",
|
|
"Returned 12 items.",
|
|
"Gave Giveable (Object)",
|
|
"Raw Notecard Data: fixture note body",
|
|
"[Item] Name: Running Script Desc: Task script Type: LSLText",
|
|
"IsRunning: true",
|
|
"Setting true => false",
|
|
"IsRunning: false",
|
|
"Notecard successfully created",
|
|
"Raw Notecard Data: created note body",
|
|
"compilation requested",
|
|
"Texture upload succeeded",
|
|
"Downloaded Uploaded",
|
|
"Finished executing 2 commands",
|
|
"Moved folder Delete Me to Trash",
|
|
"Lost And Found Emptied",
|
|
"Trash Emptied",
|
|
"Returned 14 items.",
|
|
"Gave $75 to 11111111-2222-3333-4444-555555555555",
|
|
"Balance is L$: 0",
|
|
"CALL appearance-set rebake=true",
|
|
"CALL appearance-clone 40000000-0000-0000-0000-000000000001",
|
|
"CALL appearance-wear 20000000-0000-0000-0000-000000000004",
|
|
"CALL inventory-give 20000000-0000-0000-0000-000000000001",
|
|
"CALL xfer 30000000-0000-0000-0000-000000000003 Object",
|
|
"CALL task-script-running 50000000-0000-0000-0000-000000000001 50000000-0000-0000-0000-000000000002 false",
|
|
"CALL inventory-empty LostAndFound",
|
|
"CALL inventory-empty Trash",
|
|
"active-tasks=0 shutdown=true",
|
|
] {
|
|
assert!(text.contains(expected), "missing {expected}:\n{text}");
|
|
}
|
|
assert_eq!(
|
|
fs::read(&download).expect("download file"),
|
|
notecard("fixture note body")
|
|
);
|
|
assert_eq!(fs::read(&xfer).expect("xfer file"), b"fake object asset");
|
|
assert_eq!(
|
|
fs::read(backup.join("Notecards/Fixture Note.txt")).expect("backup notecard"),
|
|
notecard("fixture note body")
|
|
);
|
|
assert_eq!(
|
|
fs::read(backup.join("Scripts/Fixture Script.lsl")).expect("backup script"),
|
|
b"default { state_entry() {} }"
|
|
);
|
|
assert_eq!(
|
|
RustJ2kCodec::decode_bytes(
|
|
&fs::read(outfit.join(format!("{UPLOADED_TEXTURE_ASSET}.jp2")))
|
|
.expect("uploaded outfit jp2"),
|
|
libremetaverse_imaging::J2kDecodeOptions::default(),
|
|
)
|
|
.expect("decode uploaded outfit texture")
|
|
.width,
|
|
1
|
|
);
|
|
assert!(
|
|
outfit
|
|
.join(format!("{UPLOADED_TEXTURE_ASSET}.tga"))
|
|
.exists()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mutations_require_global_opt_in_and_per_command_confirmation() {
|
|
let directory = TestDir::new("guards");
|
|
let script = directory.write(
|
|
"terminal.tsv",
|
|
format!(
|
|
"!client\t{CLIENT}\tAlice\tBot\n\
|
|
!inventory-root\t{CLIENT}\t{ROOT}\t{CLIENT}\tMy Inventory\n\
|
|
!inventory-folder\t{CLIENT}\t{TRASH}\t{ROOT}\ttrash\tTrash\n\
|
|
emptytrash --confirm\n\
|
|
quit\n"
|
|
),
|
|
);
|
|
let output = run(&["--fake-script", path_text(&script)]);
|
|
assert!(output.status.success());
|
|
assert!(stdout(&output).contains("Live mutation blocked; restart with --allow-live-mutations"));
|
|
assert!(!stdout(&output).contains("CALL inventory-empty"));
|
|
|
|
let no_confirm = directory.write(
|
|
"no-confirm.tsv",
|
|
format!(
|
|
"!client\t{CLIENT}\tAlice\tBot\n\
|
|
!inventory-root\t{CLIENT}\t{ROOT}\t{CLIENT}\tMy Inventory\n\
|
|
!inventory-folder\t{CLIENT}\t{TRASH}\t{ROOT}\ttrash\tTrash\n\
|
|
emptytrash\n\
|
|
quit\n"
|
|
),
|
|
);
|
|
let output = run(&[
|
|
"--allow-live-mutations",
|
|
"--fake-script",
|
|
path_text(&no_confirm),
|
|
]);
|
|
assert!(output.status.success());
|
|
assert!(stdout(&output).contains("Operation requires an explicit --confirm argument"));
|
|
assert!(!stdout(&output).contains("CALL inventory-empty"));
|
|
}
|