Implement native InventoryExplorer
All checks were successful
Native Rust workspace compile / compile (push) Successful in 21m44s
All checks were successful
Native Rust workspace compile / compile (push) Successful in 21m44s
This commit is contained in:
@@ -485,8 +485,8 @@ documented in the
|
|||||||
|
|
||||||
### Milestone 11
|
### Milestone 11
|
||||||
|
|
||||||
The native `osd-inspector`, `simple-bot`, `packet-dump`, and `prim-inspector`
|
The native `osd-inspector`, `simple-bot`, `packet-dump`, `prim-inspector`, and
|
||||||
programs are complete.
|
`inventory-explorer` programs are complete.
|
||||||
`osd-inspector` validates and converts bounded LLSD and performs deterministic
|
`osd-inspector` validates and converts bounded LLSD and performs deterministic
|
||||||
Primitive-to-OSD round trips. `simple-bot` provides native async login, IM and
|
Primitive-to-OSD round trips. `simple-bot` provides native async login, IM and
|
||||||
local-chat commands, movement and animation calls, bounded fake-grid
|
local-chat commands, movement and animation calls, bounded fake-grid
|
||||||
@@ -495,6 +495,8 @@ adds filtered, bounded incoming/outgoing capture with native wire validation,
|
|||||||
sanitized optional raw bytes, and deterministic fake datagrams. `prim-inspector`
|
sanitized optional raw bytes, and deterministic fake datagrams. `prim-inspector`
|
||||||
discovers real simulator primitives, hydrates missing object properties, orders
|
discovers real simulator primitives, hydrates missing object properties, orders
|
||||||
nearest matches, and formats construction, feature, and ownership details with
|
nearest matches, and formats construction, feature, and ownership details with
|
||||||
bounded deterministic fake-grid coverage. Their command
|
bounded deterministic fake-grid coverage. `inventory-explorer` uses the native
|
||||||
|
manager/store and AIS parser for bounded browsing, statistics, typed search,
|
||||||
|
link-aware hierarchy traversal, and deterministic export. Their command
|
||||||
surfaces, limits, isolated CLI tests, and the status of every remaining program
|
surfaces, limits, isolated CLI tests, and the status of every remaining program
|
||||||
are documented in the [`native programs guide`](programs/README.md).
|
are documented in the [`native programs guide`](programs/README.md).
|
||||||
|
|||||||
@@ -214,6 +214,11 @@ pub trait InventoryObjectClass: std::any::Any {
|
|||||||
|
|
||||||
/// Returns the shared `InventoryBase` portion of the concrete object.
|
/// Returns the shared `InventoryBase` portion of the concrete object.
|
||||||
fn inventory_base(&self) -> &InventoryBase;
|
fn inventory_base(&self) -> &InventoryBase;
|
||||||
|
|
||||||
|
/// Returns the shared `InventoryItem` portion for item-derived objects.
|
||||||
|
fn inventory_item(&self) -> Option<&InventoryItem> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rust object-safe view of concrete classes derived from C# `InventoryItem`.
|
/// Rust object-safe view of concrete classes derived from C# `InventoryItem`.
|
||||||
@@ -233,6 +238,24 @@ macro_rules! inventory_object_class {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! inventory_item_class {
|
||||||
|
($type:ty, $item:ident $(.$nested:ident)*) => {
|
||||||
|
impl InventoryObjectClass for $type {
|
||||||
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inventory_base(&self) -> &InventoryBase {
|
||||||
|
&self.$item$(.$nested)*.base
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inventory_item(&self) -> Option<&InventoryItem> {
|
||||||
|
Some(&self.$item$(.$nested)*)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
impl InventoryObjectClass for InventoryBase {
|
impl InventoryObjectClass for InventoryBase {
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
@@ -244,22 +267,34 @@ impl InventoryObjectClass for InventoryBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inventory_object_class!(InventoryFolder, base);
|
inventory_object_class!(InventoryFolder, base);
|
||||||
inventory_object_class!(InventoryItem, base);
|
impl InventoryObjectClass for InventoryItem {
|
||||||
inventory_object_class!(InventoryAnimation, base.base);
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
inventory_object_class!(InventoryAttachment, base.base);
|
self
|
||||||
inventory_object_class!(InventoryCallingCard, base.base);
|
}
|
||||||
inventory_object_class!(InventoryCategory, base.base);
|
|
||||||
inventory_object_class!(InventoryGesture, base.base);
|
fn inventory_base(&self) -> &InventoryBase {
|
||||||
inventory_object_class!(InventoryLSL, base.base);
|
&self.base
|
||||||
inventory_object_class!(InventoryLandmark, base.base);
|
}
|
||||||
inventory_object_class!(InventoryMaterial, base.base);
|
|
||||||
inventory_object_class!(InventoryNotecard, base.base);
|
fn inventory_item(&self) -> Option<&InventoryItem> {
|
||||||
inventory_object_class!(InventoryObject, base.base);
|
Some(self)
|
||||||
inventory_object_class!(InventorySettings, base.base);
|
}
|
||||||
inventory_object_class!(InventorySnapshot, base.base);
|
}
|
||||||
inventory_object_class!(InventorySound, base.base);
|
inventory_item_class!(InventoryAnimation, base);
|
||||||
inventory_object_class!(InventoryTexture, base.base);
|
inventory_item_class!(InventoryAttachment, base);
|
||||||
inventory_object_class!(InventoryWearable, base.base);
|
inventory_item_class!(InventoryCallingCard, base);
|
||||||
|
inventory_item_class!(InventoryCategory, base);
|
||||||
|
inventory_item_class!(InventoryGesture, base);
|
||||||
|
inventory_item_class!(InventoryLSL, base);
|
||||||
|
inventory_item_class!(InventoryLandmark, base);
|
||||||
|
inventory_item_class!(InventoryMaterial, base);
|
||||||
|
inventory_item_class!(InventoryNotecard, base);
|
||||||
|
inventory_item_class!(InventoryObject, base);
|
||||||
|
inventory_item_class!(InventorySettings, base);
|
||||||
|
inventory_item_class!(InventorySnapshot, base);
|
||||||
|
inventory_item_class!(InventorySound, base);
|
||||||
|
inventory_item_class!(InventoryTexture, base);
|
||||||
|
inventory_item_class!(InventoryWearable, base);
|
||||||
|
|
||||||
impl InventoryItemClass for InventoryItem {}
|
impl InventoryItemClass for InventoryItem {}
|
||||||
impl InventoryItemClass for InventoryAnimation {}
|
impl InventoryItemClass for InventoryAnimation {}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ here so a source entry is never mistaken for a completed port.
|
|||||||
| `simple-bot` | SimpleBot | Implemented with live and deterministic fake-grid modes |
|
| `simple-bot` | SimpleBot | Implemented with live and deterministic fake-grid modes |
|
||||||
| `packet-dump` | PacketDump | Implemented with live and deterministic fake-grid capture |
|
| `packet-dump` | PacketDump | Implemented with live and deterministic fake-grid capture |
|
||||||
| `prim-inspector` | PrimInspector | Implemented with live and deterministic fake-grid discovery |
|
| `prim-inspector` | PrimInspector | Implemented with live and deterministic fake-grid discovery |
|
||||||
| `inventory-explorer` | InventoryExplorer | Pending milestone 11 issue #89 |
|
| `inventory-explorer` | InventoryExplorer | Implemented with live inventory and deterministic AIS fixtures |
|
||||||
| `irc-gateway` | IRCGateway | Pending milestone 11 issue #90 |
|
| `irc-gateway` | IRCGateway | Pending milestone 11 issue #90 |
|
||||||
| `test-client` | TestClient | Pending milestone 11 issues #91–#94 |
|
| `test-client` | TestClient | Pending milestone 11 issues #91–#94 |
|
||||||
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
||||||
@@ -189,3 +189,52 @@ Run the isolated CLI golden tests and the related translated primitive cases:
|
|||||||
cargo test -p libremetaverse-programs --test prim_inspector_cli --locked
|
cargo test -p libremetaverse-programs --test prim_inspector_cli --locked
|
||||||
cargo test --manifest-path tests/compat/Cargo.toml --test world_object_semantics --locked
|
cargo test --manifest-path tests/compat/Cargo.toml --test world_object_semantics --locked
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## InventoryExplorer
|
||||||
|
|
||||||
|
`inventory-explorer` preserves the source program's live login and operation
|
||||||
|
options:
|
||||||
|
|
||||||
|
```text
|
||||||
|
inventory-explorer FIRSTNAME LASTNAME PASSWORD [--stats]
|
||||||
|
[--search TERM [--type TYPE]] [--export FILE]
|
||||||
|
```
|
||||||
|
|
||||||
|
Credentials may instead come from `GRID_FIRST_NAME`, `GRID_LAST_NAME`, and
|
||||||
|
`GRID_PASSWORD`; `GRID_LOGIN_URL` or `--login-uri` selects the endpoint. Login
|
||||||
|
and inventory readiness each have a 30-second deadline. The native client waits
|
||||||
|
for a populated root in the real `InventoryManager::store`, handles Ctrl-C and
|
||||||
|
disconnects, removes event handlers, logs out, and disposes the client on every
|
||||||
|
post-construction path.
|
||||||
|
|
||||||
|
With no operation option, the program prints the sorted top-level folders and
|
||||||
|
items. `--stats` traverses the hierarchy and reports item and root-folder counts
|
||||||
|
plus the ten most common asset types. `--search` performs a case-insensitive
|
||||||
|
name search across folders and items, reports parent folders and UUIDs, and
|
||||||
|
shows link targets. `--type` accepts every native `AssetType` name and narrows
|
||||||
|
item results. Search output defaults to 50 results and is capped at 1,000.
|
||||||
|
|
||||||
|
`--export FILE` writes a deterministic `deterministic-v1` hierarchy with
|
||||||
|
folders before items, stable case-insensitive name and UUID ordering, asset
|
||||||
|
types, link targets, and UUIDs. The destination must be an explicit non-symlink
|
||||||
|
path of at most 4,096 characters. The in-memory export is validated before the
|
||||||
|
file is written and defaults to a 16 MiB limit. Traversal defaults to 100,000
|
||||||
|
entries and 64 folder levels, detects cycles, and exposes configurable bounded
|
||||||
|
limits. Standard output is buffered and limited to 4 MiB, and inventory/status
|
||||||
|
text redacts URLs and credential-like assignments.
|
||||||
|
|
||||||
|
Offline validation uses `--fake-ais FILE`. The file is an LLSD JSON AIS response
|
||||||
|
with a nonzero `root_id` and ordinary `_embedded.categories`, `.items`, and
|
||||||
|
`.links` maps. It is capped at 8 MiB and parsed by the public native
|
||||||
|
`InventoryAISClient`; parsed folders, items, and links are then installed
|
||||||
|
through the real manager-owned `Inventory` store. This exercises AIS typing,
|
||||||
|
derived inventory items, link targets, parent indexes, and folder contents
|
||||||
|
without contacting a grid or embedding credentials.
|
||||||
|
|
||||||
|
Run the isolated CLI/golden suite and its related translated inventory gates:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test -p libremetaverse-programs --test inventory_explorer_cli --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test inventory_store_semantics --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test inventory_ais_semantics --locked
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
fn main() -> std::process::ExitCode {
|
fn main() -> std::process::ExitCode {
|
||||||
libremetaverse_programs::pending_program("InventoryExplorer")
|
libremetaverse_programs::inventory_explorer::main_entry()
|
||||||
}
|
}
|
||||||
|
|||||||
1059
programs/src/inventory_explorer.rs
Normal file
1059
programs/src/inventory_explorer.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
//! Rust targets corresponding to the upstream example and tool projects.
|
//! Rust targets corresponding to the upstream example and tool projects.
|
||||||
|
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
|
pub mod inventory_explorer;
|
||||||
pub mod osd_inspector;
|
pub mod osd_inspector;
|
||||||
pub mod packet_dump;
|
pub mod packet_dump;
|
||||||
pub mod prim_inspector;
|
pub mod prim_inspector;
|
||||||
|
|||||||
289
programs/tests/inventory_explorer_cli.rs
Normal file
289
programs/tests/inventory_explorer_cli.rs
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
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-inventory-explorer-{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_inventory-explorer"))
|
||||||
|
.args(args)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.expect("run inventory-explorer")
|
||||||
|
}
|
||||||
|
|
||||||
|
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("inventory.json");
|
||||||
|
fs::write(
|
||||||
|
&path,
|
||||||
|
r#"{
|
||||||
|
"root_id":"10000000-0000-0000-0000-000000000000",
|
||||||
|
"_embedded":{
|
||||||
|
"categories":{
|
||||||
|
"root":{"category_id":"10000000-0000-0000-0000-000000000000","parent_id":"00000000-0000-0000-0000-000000000000","name":"My Inventory","type_default":8,"version":4,"descendents":5},
|
||||||
|
"objects":{"category_id":"20000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","name":"Objects","type_default":6,"version":2,"descendents":2},
|
||||||
|
"scripts":{"category_id":"30000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","name":"Scripts","type_default":10,"version":2,"descendents":1},
|
||||||
|
"empty":{"category_id":"40000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","name":"Empty","type_default":-1,"version":1,"descendents":0}
|
||||||
|
},
|
||||||
|
"items":{
|
||||||
|
"sword":{"item_id":"50000000-0000-0000-0000-000000000000","parent_id":"20000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"51000000-0000-0000-0000-000000000000","name":"Sword","desc":"A test object","type":"object","inv_type":"object","created_at":1},
|
||||||
|
"texture":{"item_id":"60000000-0000-0000-0000-000000000000","parent_id":"20000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"61000000-0000-0000-0000-000000000000","name":"Sword Texture","desc":"Texture","type":"texture","inv_type":"texture","created_at":2},
|
||||||
|
"script":{"item_id":"70000000-0000-0000-0000-000000000000","parent_id":"30000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"71000000-0000-0000-0000-000000000000","name":"Sword Script","desc":"Script","type":"lsltext","inv_type":"script","created_at":3},
|
||||||
|
"readme":{"item_id":"80000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","asset_id":"81000000-0000-0000-0000-000000000000","name":"Read Me","desc":"Notes","type":"notecard","inv_type":"notecard","created_at":4}
|
||||||
|
},
|
||||||
|
"links":{
|
||||||
|
"shortcut":{"item_id":"90000000-0000-0000-0000-000000000000","parent_id":"10000000-0000-0000-0000-000000000000","agent_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","linked_id":"50000000-0000-0000-0000-000000000000","name":"Sword Shortcut","desc":"Link","type":"object","inv_type":"texture","created_at":5}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("write AIS fixture");
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn help_preserves_upstream_options_and_documents_bounded_fake_mode() {
|
||||||
|
let output = run(&["--help"]);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let help = text(&output.stdout);
|
||||||
|
for marker in [
|
||||||
|
"[FIRSTNAME]",
|
||||||
|
"[LASTNAME]",
|
||||||
|
"[PASSWORD]",
|
||||||
|
"--stats",
|
||||||
|
"--search <TERM>",
|
||||||
|
"--type <TYPE>",
|
||||||
|
"--export <FILE>",
|
||||||
|
"--fake-ais <FILE>",
|
||||||
|
"--max-entries",
|
||||||
|
"--max-export-bytes",
|
||||||
|
] {
|
||||||
|
assert!(help.contains(marker), "help omitted {marker}:\n{help}");
|
||||||
|
}
|
||||||
|
let output = run(&[]);
|
||||||
|
assert_exit(&output, EXIT_USAGE);
|
||||||
|
assert!(text(&output.stderr).contains("Usage: inventory-explorer"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fake_ais_top_level_and_statistics_use_real_hierarchical_store() {
|
||||||
|
let directory = TestDir::new("browse-stats");
|
||||||
|
let fixture = fixture(&directory);
|
||||||
|
let output = run(&["--fake-ais", path_text(&fixture)]);
|
||||||
|
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||||
|
let output = text(&output.stdout);
|
||||||
|
for expected in [
|
||||||
|
"CALL AIS fetch root=10000000-0000-0000-0000-000000000000",
|
||||||
|
"Fake AIS inventory loaded: 9 entries",
|
||||||
|
"=== Inventory Tree (Top Level) ===",
|
||||||
|
"Folder: Empty (0 entries)",
|
||||||
|
"Folder: Objects (2 entries)",
|
||||||
|
"Folder: Scripts (1 entries)",
|
||||||
|
"Item: Read Me (Notecard)",
|
||||||
|
"Link: Sword Shortcut (Link -> 50000000-0000-0000-0000-000000000000)",
|
||||||
|
"Fake grid logout complete; active_tasks=0 open_sockets=0",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
output.contains(expected),
|
||||||
|
"output omitted {expected}:\n{output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(output.find("Folder: Empty").unwrap() < output.find("Folder: Objects").unwrap());
|
||||||
|
|
||||||
|
let output = run(&["--fake-ais", path_text(&fixture), "--stats"]);
|
||||||
|
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||||
|
let output = text(&output.stdout);
|
||||||
|
for expected in [
|
||||||
|
"=== Inventory Statistics ===",
|
||||||
|
"Total Items: 5",
|
||||||
|
"Root Folders: 3",
|
||||||
|
"Link: 1 items",
|
||||||
|
"LSLText: 1 items",
|
||||||
|
"Notecard: 1 items",
|
||||||
|
"Object: 1 items",
|
||||||
|
"Texture: 1 items",
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
output.contains(expected),
|
||||||
|
"stats omitted {expected}:\n{output}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn search_filter_limit_parent_and_link_details_are_deterministic() {
|
||||||
|
let directory = TestDir::new("search");
|
||||||
|
let fixture = fixture(&directory);
|
||||||
|
let output = run(&[
|
||||||
|
"--fake-ais",
|
||||||
|
path_text(&fixture),
|
||||||
|
"--search",
|
||||||
|
"sWoRd",
|
||||||
|
"--type",
|
||||||
|
"TeXtUrE",
|
||||||
|
]);
|
||||||
|
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||||
|
let output = text(&output.stdout);
|
||||||
|
assert!(output.contains("Found 1 matching entries"));
|
||||||
|
assert!(output.contains("Sword Texture\nType: Texture\nFolder: Objects"));
|
||||||
|
assert!(!output.contains("Sword Script\n"));
|
||||||
|
assert!(!output.contains("Sword Shortcut\n"));
|
||||||
|
|
||||||
|
let output = run(&[
|
||||||
|
"--fake-ais",
|
||||||
|
path_text(&fixture),
|
||||||
|
"--search",
|
||||||
|
"Sword",
|
||||||
|
"--max-results",
|
||||||
|
"2",
|
||||||
|
]);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let output = text(&output.stdout);
|
||||||
|
assert!(output.contains("Found 4 matching entries"));
|
||||||
|
assert!(output.contains("... and 2 more results"));
|
||||||
|
assert!(output.contains("Sword\nType: Object"));
|
||||||
|
assert!(output.contains("Sword Script\nType: LSLText"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn export_is_hierarchical_deterministic_and_bounded() {
|
||||||
|
let directory = TestDir::new("export");
|
||||||
|
let fixture = fixture(&directory);
|
||||||
|
let first = directory.path("first.txt");
|
||||||
|
let second = directory.path("second.txt");
|
||||||
|
for destination in [&first, &second] {
|
||||||
|
let output = run(&[
|
||||||
|
"--fake-ais",
|
||||||
|
path_text(&fixture),
|
||||||
|
"--export",
|
||||||
|
path_text(destination),
|
||||||
|
]);
|
||||||
|
assert!(output.status.success(), "{}", text(&output.stderr));
|
||||||
|
assert!(text(&output.stdout).contains("Exported 8 entries"));
|
||||||
|
}
|
||||||
|
let first_bytes = fs::read(&first).expect("read first export");
|
||||||
|
let second_bytes = fs::read(&second).expect("read second export");
|
||||||
|
assert_eq!(first_bytes, second_bytes);
|
||||||
|
let export = text(&first_bytes);
|
||||||
|
assert_eq!(
|
||||||
|
export,
|
||||||
|
concat!(
|
||||||
|
"LibreMetaverse Inventory Export\n",
|
||||||
|
"Format: deterministic-v1\n",
|
||||||
|
"Entries: 8\n",
|
||||||
|
"\n",
|
||||||
|
"[Folder] Empty\n",
|
||||||
|
"[Folder] Objects\n",
|
||||||
|
" Sword (Object) - 50000000-0000-0000-0000-000000000000\n",
|
||||||
|
" Sword Texture (Texture) - 60000000-0000-0000-0000-000000000000\n",
|
||||||
|
"[Folder] Scripts\n",
|
||||||
|
" Sword Script (LSLText) - 70000000-0000-0000-0000-000000000000\n",
|
||||||
|
"Read Me (Notecard) - 80000000-0000-0000-0000-000000000000\n",
|
||||||
|
"Sword Shortcut (Link -> 50000000-0000-0000-0000-000000000000) - ",
|
||||||
|
"90000000-0000-0000-0000-000000000000\n",
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let limited = directory.path("limited.txt");
|
||||||
|
let output = run(&[
|
||||||
|
"--fake-ais",
|
||||||
|
path_text(&fixture),
|
||||||
|
"--export",
|
||||||
|
path_text(&limited),
|
||||||
|
"--max-export-bytes",
|
||||||
|
"10",
|
||||||
|
]);
|
||||||
|
assert_exit(&output, EXIT_OUTPUT);
|
||||||
|
assert!(!limited.exists());
|
||||||
|
assert!(text(&output.stderr).contains("export exceeds the byte limit"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_fixtures_options_filesystem_and_credentials_fail_explicitly() {
|
||||||
|
let directory = TestDir::new("invalid");
|
||||||
|
let malformed = directory.path("malformed.json");
|
||||||
|
fs::write(&malformed, "{bad-json}").expect("write malformed fixture");
|
||||||
|
let output = run(&["--fake-ais", 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-ais", path_text(&fixture), "--type", "Texture"]);
|
||||||
|
assert_exit(&output, EXIT_USAGE);
|
||||||
|
assert!(text(&output.stderr).contains("--type requires --search"));
|
||||||
|
|
||||||
|
let output = run(&[
|
||||||
|
"--fake-ais",
|
||||||
|
path_text(&fixture),
|
||||||
|
"--search",
|
||||||
|
"Sword",
|
||||||
|
"--type",
|
||||||
|
"Money",
|
||||||
|
]);
|
||||||
|
assert_exit(&output, EXIT_USAGE);
|
||||||
|
assert!(text(&output.stderr).contains("unknown asset type"));
|
||||||
|
|
||||||
|
let password = "do-not-print-this-password";
|
||||||
|
let output = run(&["First", "Last", password, "--fake-ais", path_text(&fixture)]);
|
||||||
|
assert_exit(&output, EXIT_USAGE);
|
||||||
|
assert!(!text(&output.stdout).contains(password));
|
||||||
|
assert!(!text(&output.stderr).contains(password));
|
||||||
|
|
||||||
|
let output = run(&[
|
||||||
|
"--fake-ais",
|
||||||
|
path_text(&fixture),
|
||||||
|
"--export",
|
||||||
|
path_text(&directory.0),
|
||||||
|
]);
|
||||||
|
assert_exit(&output, EXIT_OUTPUT);
|
||||||
|
assert!(text(&output.stderr).contains("writing inventory export"));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user