Implement TestClient inventory and appearance commands (#92)
Some checks failed
Native code generation / deterministic (push) Successful in 18m25s
Imaging and meshing gate / native (push) Failing after 4m23s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 12m37s
Skia feature / linux (push) Has been cancelled
Some checks failed
Native code generation / deterministic (push) Successful in 18m25s
Imaging and meshing gate / native (push) Failing after 4m23s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 12m37s
Skia feature / linux (push) Has been cancelled
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -1053,6 +1053,8 @@ version = "0.0.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"libremetaverse",
|
"libremetaverse",
|
||||||
|
"libremetaverse-imaging",
|
||||||
|
"libremetaverse-imaging-skia",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ publish = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
libremetaverse = { path = "../crates/libremetaverse", default-features = false }
|
libremetaverse = { path = "../crates/libremetaverse", default-features = false }
|
||||||
|
libremetaverse-imaging = { path = "../crates/libremetaverse-imaging", features = ["jpeg2000"] }
|
||||||
|
libremetaverse-imaging-skia = { path = "../crates/libremetaverse-imaging-skia", features = ["skia"] }
|
||||||
tokio = { version = "1.47", features = ["io-std", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
tokio = { version = "1.47", features = ["io-std", "io-util", "macros", "net", "rt-multi-thread", "signal", "sync", "time"] }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ here so a source entry is never mistaken for a completed port.
|
|||||||
| `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 | Implemented with live inventory and deterministic AIS fixtures |
|
| `inventory-explorer` | InventoryExplorer | Implemented with live inventory and deterministic AIS fixtures |
|
||||||
| `irc-gateway` | IRCGateway | Implemented with live IRC/grid transports and deterministic offline scripts |
|
| `irc-gateway` | IRCGateway | Implemented with live IRC/grid transports and deterministic offline scripts |
|
||||||
| `test-client` | TestClient | Native shell, registry, system, and communication groups implemented; remaining command groups tracked by #92–#94 |
|
| `test-client` | TestClient | Native shell, registry, system, communication, inventory, appearance, and asset groups implemented; remaining command groups tracked by #93–#94 |
|
||||||
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
||||||
| `webrtc-test` | WebRtcTest | Pending milestone 11 issue #96 |
|
| `webrtc-test` | WebRtcTest | Pending milestone 11 issue #96 |
|
||||||
|
|
||||||
@@ -165,12 +165,16 @@ broadcast command routing.
|
|||||||
The implemented command groups are `@`, `debug`, `echomaster`, `help`, `im`,
|
The implemented command groups are `@`, `debug`, `echomaster`, `help`, `im`,
|
||||||
`imgroup`, `load`, `login`, `logpacket`, `logout`, `md5`, `quit`, `say`,
|
`imgroup`, `load`, `login`, `logpacket`, `logout`, `md5`, `quit`, `say`,
|
||||||
`setmaster`, `setmasterkey`, `shout`, `showeffects`, `sleep`, `waitforlogin`, and
|
`setmaster`, `setmasterkey`, `shout`, `showeffects`, `sleep`, `waitforlogin`, and
|
||||||
`whisper`. Chat and instant-message bodies are bounded to the grid protocol
|
`whisper`. The inventory wave also implements `appearance`, `attachments`,
|
||||||
limit. Group commands require both `--groupcommands` and current group
|
`avatarinfo`, `clone`, `wear`, `backuptext`, `balance`, `cd`, `createnotecard`,
|
||||||
membership. Master chat can be echoed, master teleport lures are accepted, and
|
`deletefolder`, `download`, `dumpoutfit`, `emptylostandfound`, `emptytrash`,
|
||||||
remote `login` and `md5` command text is redacted from transcripts. Packet logs
|
`giveall`, `give`, `i`, `ls`, `objectinventory`, `script`, `taskrunning`,
|
||||||
contain only timestamped packet type, simulator name, and byte count; they are
|
`uploadimage`, `uploadscript`, `viewnote`, and `xfer`. Chat and instant-message
|
||||||
limited to 10,000 records and 16 MiB.
|
bodies are bounded to the grid protocol limit. Group commands require both
|
||||||
|
`--groupcommands` and current group membership. Master chat can be echoed,
|
||||||
|
master teleport lures are accepted, and remote `login` and `md5` command text
|
||||||
|
is redacted from transcripts. Packet logs contain only timestamped packet type,
|
||||||
|
simulator name, and byte count; they are limited to 10,000 records and 16 MiB.
|
||||||
|
|
||||||
The native `load` command reads a portable command-alias manifest instead of a
|
The native `load` command reads a portable command-alias manifest instead of a
|
||||||
CLR assembly. Each non-comment line is tab-separated
|
CLR assembly. Each non-comment line is tab-separated
|
||||||
@@ -178,6 +182,16 @@ CLR assembly. Each non-comment line is tab-separated
|
|||||||
arguments. Manifests are limited to 1 MiB and 128 commands, with an alias
|
arguments. Manifests are limited to 1 MiB and 128 commands, with an alias
|
||||||
expansion depth of eight.
|
expansion depth of eight.
|
||||||
|
|
||||||
|
Inventory and appearance mutations require both the global
|
||||||
|
`--allow-live-mutations` switch and a per-command `--confirm` argument. Uploads
|
||||||
|
and L$ transfers additionally require `--allow-spending`. Downloads and
|
||||||
|
backups reject parent traversal, individual assets are limited to 64 MiB,
|
||||||
|
backups are limited to 10,000 files and 512 MiB, and image uploads decode
|
||||||
|
portable TGA, JPEG, PNG, WebP, or JPEG2000 input before producing the grid's
|
||||||
|
JPEG2000 texture format. Live commands use the public inventory, appearance,
|
||||||
|
asset, agent, directory, and task-inventory managers; Xfer downloads wait for
|
||||||
|
their correlated UDP completion event.
|
||||||
|
|
||||||
Use `--fake-script FILE` for deterministic, credential-free terminal and grid
|
Use `--fake-script FILE` for deterministic, credential-free terminal and grid
|
||||||
validation. Normal lines are dispatched exactly like interactive input. Fake
|
validation. Normal lines are dispatched exactly like interactive input. Fake
|
||||||
grid records begin with `!` and use tab-separated fields:
|
grid records begin with `!` and use tab-separated fields:
|
||||||
@@ -192,6 +206,16 @@ grid records begin with `!` and use tab-separated fields:
|
|||||||
!packet<TAB>client-uuid<TAB>packet-type<TAB>simulator<TAB>bytes
|
!packet<TAB>client-uuid<TAB>packet-type<TAB>simulator<TAB>bytes
|
||||||
!effect<TAB>client-uuid<TAB>summary
|
!effect<TAB>client-uuid<TAB>summary
|
||||||
!disconnect<TAB>client-uuid<TAB>reason
|
!disconnect<TAB>client-uuid<TAB>reason
|
||||||
|
!inventory-root<TAB>client-uuid<TAB>root-uuid<TAB>owner-uuid<TAB>name
|
||||||
|
!inventory-folder<TAB>client-uuid<TAB>folder-uuid<TAB>parent-uuid<TAB>folder-type<TAB>name
|
||||||
|
!inventory-item<TAB>client-uuid<TAB>item-uuid<TAB>parent-uuid<TAB>asset-uuid<TAB>asset-type<TAB>inventory-type<TAB>permissions-hex<TAB>name<TAB>description
|
||||||
|
!asset<TAB>client-uuid<TAB>asset-uuid<TAB>asset-type<TAB>hex-bytes
|
||||||
|
!balance<TAB>client-uuid<TAB>amount
|
||||||
|
!avatar<TAB>client-uuid<TAB>avatar-uuid<TAB>name<TAB>texture-label=texture-uuid,...
|
||||||
|
!appearance-cache<TAB>client-uuid<TAB>avatar-uuid
|
||||||
|
!attachment<TAB>client-uuid<TAB>point<TAB>local-id<TAB>primitive-uuid<TAB>offset
|
||||||
|
!task<TAB>client-uuid<TAB>object-uuid<TAB>local-id
|
||||||
|
!task-item<TAB>client-uuid<TAB>object-uuid<TAB>item-uuid<TAB>asset-type<TAB>true|false|none<TAB>name<TAB>description
|
||||||
!cancel
|
!cancel
|
||||||
!shutdown
|
!shutdown
|
||||||
```
|
```
|
||||||
@@ -203,10 +227,16 @@ framework and compatibility checks with:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test -p libremetaverse-programs --test test_client_cli --locked
|
cargo test -p libremetaverse-programs --test test_client_cli --locked
|
||||||
|
cargo test -p libremetaverse-programs --test test_client_inventory_cli --locked
|
||||||
cargo test -p libremetaverse-programs test_client::tests --locked
|
cargo test -p libremetaverse-programs test_client::tests --locked
|
||||||
cargo test --manifest-path tests/compat/Cargo.toml --test core_runtime_shims --locked
|
cargo test --manifest-path tests/compat/Cargo.toml --test appearance_semantics --locked
|
||||||
cargo test --manifest-path tests/compat/Cargo.toml --test social_message_semantics --locked
|
cargo test --manifest-path tests/compat/Cargo.toml --test appearance_visual --locked
|
||||||
cargo test --manifest-path tests/compat/Cargo.toml --test network_semantics --locked
|
cargo test --manifest-path tests/compat/Cargo.toml --test asset_capability --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test asset_document --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test inventory_ais --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test inventory_manager --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test inventory_store --locked
|
||||||
|
cargo test --manifest-path tests/compat/Cargo.toml --test task_inventory --locked
|
||||||
```
|
```
|
||||||
|
|
||||||
## PacketDump
|
## PacketDump
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
//! Native `TestClient` shell, registry, and first-wave command groups.
|
//! Native `TestClient` shell, registry, and first-wave command groups.
|
||||||
|
|
||||||
|
mod inventory;
|
||||||
|
|
||||||
use crate::commands::TEST_CLIENT_COMMANDS;
|
use crate::commands::TEST_CLIENT_COMMANDS;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use libremetaverse::packets::PacketType;
|
use libremetaverse::packets::PacketType;
|
||||||
@@ -41,6 +43,31 @@ const MAX_LOADED_COMMANDS: usize = 128;
|
|||||||
const MAX_ALIAS_DEPTH: usize = 8;
|
const MAX_ALIAS_DEPTH: usize = 8;
|
||||||
|
|
||||||
pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
|
pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
|
||||||
|
"AppearanceCommand",
|
||||||
|
"AttachmentsCommand",
|
||||||
|
"AvatarInfoCommand",
|
||||||
|
"CloneCommand",
|
||||||
|
"WearCommand",
|
||||||
|
"BackupCommand",
|
||||||
|
"BalanceCommand",
|
||||||
|
"ChangeDirectoryCommand",
|
||||||
|
"CreateNotecardCommand",
|
||||||
|
"DeleteFolderCommand",
|
||||||
|
"DownloadCommand",
|
||||||
|
"DumpOutfitCommand",
|
||||||
|
"EmptyLostAndFound",
|
||||||
|
"EmptyTrashCommand",
|
||||||
|
"GiveAllCommand",
|
||||||
|
"GiveItemCommand",
|
||||||
|
"InventoryCommand",
|
||||||
|
"ListContentsCommand",
|
||||||
|
"ObjectInventoryCommand",
|
||||||
|
"ScriptCommand",
|
||||||
|
"TaskRunningCommand",
|
||||||
|
"UploadImageCommand",
|
||||||
|
"UploadScriptCommand",
|
||||||
|
"ViewNotecardCommand",
|
||||||
|
"XferCommand",
|
||||||
"EchoMasterCommand",
|
"EchoMasterCommand",
|
||||||
"IMCommand",
|
"IMCommand",
|
||||||
"IMGroupCommand",
|
"IMGroupCommand",
|
||||||
@@ -79,8 +106,9 @@ pub fn pending_test_client_commands() -> Vec<&'static str> {
|
|||||||
version,
|
version,
|
||||||
about = "Run the native LibreMetaverse multi-client command shell",
|
about = "Run the native LibreMetaverse multi-client command shell",
|
||||||
long_about = None,
|
long_about = None,
|
||||||
after_help = "Owned commands: @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, whisper"
|
after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, whisper, plus the inventory/assets and appearance groups. Run `help` or `help COMMAND` inside the shell for the complete command registry."
|
||||||
)]
|
)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// First name for one initial account.
|
/// First name for one initial account.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
@@ -124,6 +152,14 @@ struct Cli {
|
|||||||
/// Maximum time allowed for each live login.
|
/// Maximum time allowed for each live login.
|
||||||
#[arg(long, default_value_t = 30, value_name = "SECONDS", value_parser = clap::value_parser!(u64).range(1..=300))]
|
#[arg(long, default_value_t = 30, value_name = "SECONDS", value_parser = clap::value_parser!(u64).range(1..=300))]
|
||||||
login_timeout_seconds: u64,
|
login_timeout_seconds: u64,
|
||||||
|
|
||||||
|
/// Permit commands that mutate live inventory or appearance state.
|
||||||
|
#[arg(long)]
|
||||||
|
allow_live_mutations: bool,
|
||||||
|
|
||||||
|
/// Permit commands that spend or transfer L$; each command also requires confirmation.
|
||||||
|
#[arg(long)]
|
||||||
|
allow_spending: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -207,8 +243,9 @@ enum ClientEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||||
|
type InventoryFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
|
||||||
|
|
||||||
trait ClientBackend: Send + Sync {
|
trait ClientBackend: Send + Sync + inventory::Backend {
|
||||||
fn id(&self) -> UUID;
|
fn id(&self) -> UUID;
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
fn connected(&self) -> bool;
|
fn connected(&self) -> bool;
|
||||||
@@ -242,6 +279,7 @@ struct LiveBackend {
|
|||||||
id: UUID,
|
id: UUID,
|
||||||
name: String,
|
name: String,
|
||||||
subscriptions: Mutex<Vec<Subscription>>,
|
subscriptions: Mutex<Vec<Subscription>>,
|
||||||
|
inventory_state: Mutex<inventory::State>,
|
||||||
closed: AtomicBool,
|
closed: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +289,7 @@ impl LiveBackend {
|
|||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
events: mpsc::Sender<ClientEvent>,
|
events: mpsc::Sender<ClientEvent>,
|
||||||
dropped: Arc<AtomicUsize>,
|
dropped: Arc<AtomicUsize>,
|
||||||
|
account_policy: inventory::MutationPolicy,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
) -> Result<Arc<Self>, ProgramError> {
|
) -> Result<Arc<Self>, ProgramError> {
|
||||||
let mut client = GridClient::new()
|
let mut client = GridClient::new()
|
||||||
@@ -299,6 +338,7 @@ impl LiveBackend {
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
subscriptions: Mutex::new(Vec::new()),
|
subscriptions: Mutex::new(Vec::new()),
|
||||||
|
inventory_state: Mutex::new(inventory::State::new(account_policy)),
|
||||||
closed: AtomicBool::new(false),
|
closed: AtomicBool::new(false),
|
||||||
});
|
});
|
||||||
backend.install(&events, &dropped);
|
backend.install(&events, &dropped);
|
||||||
@@ -309,6 +349,7 @@ impl LiveBackend {
|
|||||||
action(lock(&self.client).self_())
|
action(lock(&self.client).self_())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
fn install(self: &Arc<Self>, events: &mpsc::Sender<ClientEvent>, dropped: &Arc<AtomicUsize>) {
|
fn install(self: &Arc<Self>, events: &mpsc::Sender<ClientEvent>, dropped: &Arc<AtomicUsize>) {
|
||||||
let mut subscriptions = Vec::new();
|
let mut subscriptions = Vec::new();
|
||||||
let sender = events.clone();
|
let sender = events.clone();
|
||||||
@@ -390,6 +431,18 @@ impl LiveBackend {
|
|||||||
}),
|
}),
|
||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
|
let appearance_backend = Arc::downgrade(self);
|
||||||
|
subscriptions.push(self.network.subscribe_packet(
|
||||||
|
PacketType::AvatarAppearance,
|
||||||
|
Arc::new(move |event: PacketReceivedEventArgs| {
|
||||||
|
if let (Some(backend), Some(bytes)) =
|
||||||
|
(appearance_backend.upgrade(), event.raw_data())
|
||||||
|
{
|
||||||
|
inventory::capture_appearance(&backend, &bytes);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
false,
|
||||||
|
));
|
||||||
let sender = events.clone();
|
let sender = events.clone();
|
||||||
let dropped_disconnect = Arc::clone(dropped);
|
let dropped_disconnect = Arc::clone(dropped);
|
||||||
subscriptions.push(self.network.subscribe_disconnected(Arc::new(
|
subscriptions.push(self.network.subscribe_disconnected(Arc::new(
|
||||||
@@ -570,6 +623,7 @@ struct FakeBackend {
|
|||||||
calls: Mutex<Vec<String>>,
|
calls: Mutex<Vec<String>>,
|
||||||
people: Arc<Mutex<HashMap<String, UUID>>>,
|
people: Arc<Mutex<HashMap<String, UUID>>>,
|
||||||
group_members: Arc<Mutex<HashSet<UUID>>>,
|
group_members: Arc<Mutex<HashSet<UUID>>>,
|
||||||
|
inventory_state: Mutex<inventory::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FakeBackend {
|
impl FakeBackend {
|
||||||
@@ -578,6 +632,7 @@ impl FakeBackend {
|
|||||||
name: String,
|
name: String,
|
||||||
people: Arc<Mutex<HashMap<String, UUID>>>,
|
people: Arc<Mutex<HashMap<String, UUID>>>,
|
||||||
group_members: Arc<Mutex<HashSet<UUID>>>,
|
group_members: Arc<Mutex<HashSet<UUID>>>,
|
||||||
|
policy: inventory::MutationPolicy,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@@ -586,6 +641,7 @@ impl FakeBackend {
|
|||||||
calls: Mutex::new(Vec::new()),
|
calls: Mutex::new(Vec::new()),
|
||||||
people,
|
people,
|
||||||
group_members,
|
group_members,
|
||||||
|
inventory_state: Mutex::new(inventory::State::new(policy)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +760,9 @@ impl ProgramOutput {
|
|||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
enum CommandCategory {
|
enum CommandCategory {
|
||||||
|
Appearance,
|
||||||
Communication,
|
Communication,
|
||||||
|
Inventory,
|
||||||
TestClient,
|
TestClient,
|
||||||
Other,
|
Other,
|
||||||
}
|
}
|
||||||
@@ -717,6 +775,7 @@ enum CommandHandler {
|
|||||||
Help,
|
Help,
|
||||||
Im,
|
Im,
|
||||||
ImGroup,
|
ImGroup,
|
||||||
|
Inventory(inventory::Command),
|
||||||
Load,
|
Load,
|
||||||
Login,
|
Login,
|
||||||
LogPacket,
|
LogPacket,
|
||||||
@@ -876,6 +935,7 @@ fn built_in_commands() -> HashMap<String, CommandDefinition> {
|
|||||||
framework_commands()
|
framework_commands()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(communication_commands())
|
.chain(communication_commands())
|
||||||
|
.chain(inventory::commands())
|
||||||
.map(|(name, description, category, handler)| {
|
.map(|(name, description, category, handler)| {
|
||||||
(
|
(
|
||||||
name.into(),
|
name.into(),
|
||||||
@@ -1020,6 +1080,7 @@ struct ClientManager {
|
|||||||
default_master_name: String,
|
default_master_name: String,
|
||||||
default_master_key: UUID,
|
default_master_key: UUID,
|
||||||
default_group_commands: bool,
|
default_group_commands: bool,
|
||||||
|
default_mutation_policy: inventory::MutationPolicy,
|
||||||
cancellation: CancellationTokenSource,
|
cancellation: CancellationTokenSource,
|
||||||
output: Arc<ProgramOutput>,
|
output: Arc<ProgramOutput>,
|
||||||
events_tx: mpsc::Sender<ClientEvent>,
|
events_tx: mpsc::Sender<ClientEvent>,
|
||||||
@@ -1033,6 +1094,7 @@ struct ClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientManager {
|
impl ClientManager {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn new(
|
fn new(
|
||||||
mode: RunMode,
|
mode: RunMode,
|
||||||
login_uri: Option<String>,
|
login_uri: Option<String>,
|
||||||
@@ -1040,6 +1102,7 @@ impl ClientManager {
|
|||||||
master_name: String,
|
master_name: String,
|
||||||
master_key: UUID,
|
master_key: UUID,
|
||||||
group_commands: bool,
|
group_commands: bool,
|
||||||
|
mutation_policy: inventory::MutationPolicy,
|
||||||
output: Arc<ProgramOutput>,
|
output: Arc<ProgramOutput>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let (events_tx, events_rx) = mpsc::channel(EVENT_QUEUE_CAPACITY);
|
let (events_tx, events_rx) = mpsc::channel(EVENT_QUEUE_CAPACITY);
|
||||||
@@ -1055,6 +1118,7 @@ impl ClientManager {
|
|||||||
default_master_name: master_name,
|
default_master_name: master_name,
|
||||||
default_master_key: master_key,
|
default_master_key: master_key,
|
||||||
default_group_commands: group_commands,
|
default_group_commands: group_commands,
|
||||||
|
default_mutation_policy: mutation_policy,
|
||||||
cancellation: CancellationTokenSource::new(),
|
cancellation: CancellationTokenSource::new(),
|
||||||
output,
|
output,
|
||||||
events_tx,
|
events_tx,
|
||||||
@@ -1114,6 +1178,7 @@ impl ClientManager {
|
|||||||
name,
|
name,
|
||||||
Arc::clone(&self.fake_people),
|
Arc::clone(&self.fake_people),
|
||||||
Arc::clone(&self.fake_group_members),
|
Arc::clone(&self.fake_group_members),
|
||||||
|
self.default_mutation_policy,
|
||||||
));
|
));
|
||||||
self.add_client(ManagedClient {
|
self.add_client(ManagedClient {
|
||||||
backend,
|
backend,
|
||||||
@@ -1140,6 +1205,7 @@ impl ClientManager {
|
|||||||
self.login_timeout,
|
self.login_timeout,
|
||||||
self.events_tx.clone(),
|
self.events_tx.clone(),
|
||||||
Arc::clone(&self.dropped_events),
|
Arc::clone(&self.dropped_events),
|
||||||
|
self.default_mutation_policy,
|
||||||
self.cancellation.token(),
|
self.cancellation.token(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1153,6 +1219,7 @@ impl ClientManager {
|
|||||||
display_name,
|
display_name,
|
||||||
Arc::clone(&self.fake_people),
|
Arc::clone(&self.fake_people),
|
||||||
Arc::clone(&self.fake_group_members),
|
Arc::clone(&self.fake_group_members),
|
||||||
|
self.default_mutation_policy,
|
||||||
))),
|
))),
|
||||||
Err(_) => Err(ProgramError::Client(
|
Err(_) => Err(ProgramError::Client(
|
||||||
"could not make fake client UUID".into(),
|
"could not make fake client UUID".into(),
|
||||||
@@ -1297,6 +1364,11 @@ impl ClientManager {
|
|||||||
self.output.line(result);
|
self.output.line(result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
CommandHandler::Inventory(inventory::Command::Script) => {
|
||||||
|
let result = self.execute_script(args).await;
|
||||||
|
self.output.line(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
CommandHandler::At | CommandHandler::LoadedAlias(_) => return,
|
CommandHandler::At | CommandHandler::LoadedAlias(_) => return,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1411,6 +1483,25 @@ impl ClientManager {
|
|||||||
format!("Logging is set to {level}")
|
format!("Logging is set to {level}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn execute_script(&mut self, args: &[String]) -> String {
|
||||||
|
if args.len() != 1 {
|
||||||
|
return "Usage: script [filename]".into();
|
||||||
|
}
|
||||||
|
let lines = match read_bounded_lines(Path::new(&args[0])) {
|
||||||
|
Ok(lines) => lines,
|
||||||
|
Err(error) => return error.to_string(),
|
||||||
|
};
|
||||||
|
let count = lines.len();
|
||||||
|
for line in lines {
|
||||||
|
if !self.running {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Box::pin(self.dispatch_line(line.trim(), UUID::zero())).await;
|
||||||
|
drain_events(self).await;
|
||||||
|
}
|
||||||
|
format!("Finished executing {count} commands")
|
||||||
|
}
|
||||||
|
|
||||||
fn load_commands(&mut self, args: &[String]) -> String {
|
fn load_commands(&mut self, args: &[String]) -> String {
|
||||||
if args.len() != 1 {
|
if args.len() != 1 {
|
||||||
return "Usage: load [native-command-manifest]".into();
|
return "Usage: load [native-command-manifest]".into();
|
||||||
@@ -1605,7 +1696,7 @@ async fn execute_client_command(
|
|||||||
client: &mut ManagedClient,
|
client: &mut ManagedClient,
|
||||||
handler: &CommandHandler,
|
handler: &CommandHandler,
|
||||||
args: &[String],
|
args: &[String],
|
||||||
_from: UUID,
|
from: UUID,
|
||||||
cancellation: CancellationToken,
|
cancellation: CancellationToken,
|
||||||
) -> String {
|
) -> String {
|
||||||
match handler {
|
match handler {
|
||||||
@@ -1616,6 +1707,9 @@ async fn execute_client_command(
|
|||||||
execute_communication_command(client, handler, args, cancellation).await
|
execute_communication_command(client, handler, args, cancellation).await
|
||||||
}
|
}
|
||||||
CommandHandler::LogPacket => configure_packet_log(client, args),
|
CommandHandler::LogPacket => configure_packet_log(client, args),
|
||||||
|
CommandHandler::Inventory(command) => {
|
||||||
|
inventory::execute(client.backend.as_ref(), *command, args, from, cancellation).await
|
||||||
|
}
|
||||||
CommandHandler::Md5 => {
|
CommandHandler::Md5 => {
|
||||||
if args.len() != 1 {
|
if args.len() != 1 {
|
||||||
return "Usage: md5 [password]".into();
|
return "Usage: md5 [password]".into();
|
||||||
@@ -1861,6 +1955,8 @@ pub fn main_entry() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn run(cli: Cli) -> Result<(), ProgramError> {
|
async fn run(cli: Cli) -> Result<(), ProgramError> {
|
||||||
|
let mutation_policy =
|
||||||
|
inventory::MutationPolicy::new(cli.allow_live_mutations, cli.allow_spending);
|
||||||
let master_key = match cli.masterkey.as_deref() {
|
let master_key = match cli.masterkey.as_deref() {
|
||||||
Some(value) => UUID::new_with_string(value.into())
|
Some(value) => UUID::new_with_string(value.into())
|
||||||
.map_err(|_| ProgramError::Usage("--masterkey must be a UUID"))?,
|
.map_err(|_| ProgramError::Usage("--masterkey must be a UUID"))?,
|
||||||
@@ -1881,6 +1977,7 @@ async fn run(cli: Cli) -> Result<(), ProgramError> {
|
|||||||
cli.master.unwrap_or_default(),
|
cli.master.unwrap_or_default(),
|
||||||
master_key,
|
master_key,
|
||||||
cli.groupcommands,
|
cli.groupcommands,
|
||||||
|
mutation_policy,
|
||||||
Arc::clone(&output),
|
Arc::clone(&output),
|
||||||
);
|
);
|
||||||
run_fake_script(&mut manager, path).await?;
|
run_fake_script(&mut manager, path).await?;
|
||||||
@@ -1913,6 +2010,7 @@ async fn run(cli: Cli) -> Result<(), ProgramError> {
|
|||||||
cli.master.unwrap_or_default(),
|
cli.master.unwrap_or_default(),
|
||||||
master_key,
|
master_key,
|
||||||
cli.groupcommands,
|
cli.groupcommands,
|
||||||
|
mutation_policy,
|
||||||
Arc::clone(&output),
|
Arc::clone(&output),
|
||||||
);
|
);
|
||||||
for account in accounts {
|
for account in accounts {
|
||||||
@@ -2085,7 +2183,7 @@ async fn run_interactive(manager: &mut ClientManager) -> Result<(), ProgramError
|
|||||||
async fn drain_events(manager: &mut ClientManager) {
|
async fn drain_events(manager: &mut ClientManager) {
|
||||||
while let Ok(event) = manager.events_rx.try_recv() {
|
while let Ok(event) = manager.events_rx.try_recv() {
|
||||||
if let Some((line, from)) = manager.process_event(event) {
|
if let Some((line, from)) = manager.process_event(event) {
|
||||||
manager.dispatch_line(&line, from).await;
|
Box::pin(manager.dispatch_line(&line, from)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2183,6 +2281,10 @@ async fn run_fake_event_directive(
|
|||||||
fields: &[&str],
|
fields: &[&str],
|
||||||
line: usize,
|
line: usize,
|
||||||
) -> Result<(), ProgramError> {
|
) -> Result<(), ProgramError> {
|
||||||
|
if let Some(result) = inventory::apply_fake_directive(manager, fields) {
|
||||||
|
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
match fields {
|
match fields {
|
||||||
["chat", client, source, name, message] => {
|
["chat", client, source, name, message] => {
|
||||||
let event = ClientEvent::Chat {
|
let event = ClientEvent::Chat {
|
||||||
@@ -2254,7 +2356,7 @@ async fn run_fake_event_directive(
|
|||||||
_ => {
|
_ => {
|
||||||
return invalid(
|
return invalid(
|
||||||
line,
|
line,
|
||||||
"unknown fake directive; expected client, person, group-member, chat, im, packet, effect, disconnect, cancel, or shutdown",
|
"unknown fake directive; see --help and programs/README.md for supported records",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2894
programs/src/test_client/inventory.rs
Normal file
2894
programs/src/test_client/inventory.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,8 @@ fn help_documents_upstream_framework_and_owned_commands() {
|
|||||||
"--scriptfile",
|
"--scriptfile",
|
||||||
"--nogui",
|
"--nogui",
|
||||||
"--fake-script",
|
"--fake-script",
|
||||||
|
"--allow-live-mutations",
|
||||||
|
"--allow-spending",
|
||||||
"echomaster",
|
"echomaster",
|
||||||
"imgroup",
|
"imgroup",
|
||||||
"logpacket",
|
"logpacket",
|
||||||
|
|||||||
332
programs/tests/test_client_inventory_cli.rs
Normal file
332
programs/tests/test_client_inventory_cli.rs
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
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 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;
|
||||||
|
J2kCodec::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!(
|
||||||
|
J2kCodec::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"));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user