Implement remaining TestClient service commands (#94)
This commit is contained in:
@@ -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 |
|
||||
| `inventory-explorer` | InventoryExplorer | Implemented with live inventory and deterministic AIS fixtures |
|
||||
| `irc-gateway` | IRCGateway | Implemented with live IRC/grid transports and deterministic offline scripts |
|
||||
| `test-client` | TestClient | Native shell, registry, system, communication, inventory, appearance, asset, movement, object, parcel, estate, and grid groups implemented; remaining command group tracked by #94 |
|
||||
| `test-client` | TestClient | All non-voice command groups implemented with live and deterministic fake-grid backends; voice adapters tracked by #95 and #96 |
|
||||
| `vivox-test` | VivoxTest | Pending milestone 11 issue #95 |
|
||||
| `webrtc-test` | WebRtcTest | Pending milestone 11 issue #96 |
|
||||
|
||||
@@ -184,6 +184,13 @@ transfer, and grid map/layer/location/wind commands. `textures on` and
|
||||
primitive textures. Detailed command behavior and limits are in
|
||||
[`docs/test-client.md`](../docs/test-client.md).
|
||||
|
||||
The final non-voice wave adds nearby-agent and bot inspection, complete profile
|
||||
cloning, friend mapping, group membership and role operations, paged directory
|
||||
searches, event details, animation control, object touch, and region/network
|
||||
statistics. Live profile cloning combines the modern AgentProfile capability
|
||||
with the legacy interests reply so text, images, URL, interests, picks, and
|
||||
public groups are all synchronized.
|
||||
|
||||
The native `load` command reads a portable command-alias manifest instead of a
|
||||
CLR assembly. Each non-comment line is tab-separated
|
||||
`name<TAB>description<TAB>template`; `{args}` or `$*` inserts the quoted command
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Native `TestClient` shell, registry, and first-wave command groups.
|
||||
|
||||
mod inventory;
|
||||
mod services;
|
||||
mod world;
|
||||
|
||||
use crate::commands::TEST_CLIENT_COMMANDS;
|
||||
@@ -135,6 +136,34 @@ pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
|
||||
"ShowEffectsCommand",
|
||||
"SleepCommand",
|
||||
"WaitForLoginCommand",
|
||||
"BotsCommand",
|
||||
"CloneProfileCommand",
|
||||
"GenericMessageCommand",
|
||||
"PlayAnimationCommand",
|
||||
"TouchCommand",
|
||||
"WhoCommand",
|
||||
"Key2NameCommand",
|
||||
"SearchClassifiedsCommand",
|
||||
"SearchEventsCommand",
|
||||
"SearchGroupsCommand",
|
||||
"SearchLandCommand",
|
||||
"SearchPeopleCommand",
|
||||
"SearchPlacesCommand",
|
||||
"ShowEventDetailsCommand",
|
||||
"FriendsCommand",
|
||||
"MapFriendCommand",
|
||||
"ActivateGroupCommand",
|
||||
"GroupMembersCommand",
|
||||
"GroupRolesCommand",
|
||||
"GroupsCommand",
|
||||
"InviteGroupCommand",
|
||||
"JoinGroupCommand",
|
||||
"LeaveGroupCommand",
|
||||
"DilationCommand",
|
||||
"NetstatsCommand",
|
||||
"RegionInfoCommand",
|
||||
"StatsCommand",
|
||||
"UptimeCommand",
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
@@ -153,7 +182,7 @@ pub fn pending_test_client_commands() -> Vec<&'static str> {
|
||||
version,
|
||||
about = "Run the native LibreMetaverse multi-client command shell",
|
||||
long_about = None,
|
||||
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, appearance, asset, movement, object, parcel, estate, and grid groups. Run `help` or `help COMMAND` inside the shell for the complete command registry."
|
||||
after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, and whisper. All non-voice inventory, appearance, asset, movement, object, parcel, estate, grid, social, directory, and statistics commands are also native. Run `help` or `help COMMAND` inside the shell for the complete command registry."
|
||||
)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct Cli {
|
||||
@@ -296,7 +325,7 @@ enum ClientEvent {
|
||||
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 + inventory::Backend + world::Backend {
|
||||
trait ClientBackend: Send + Sync + inventory::Backend + services::Backend + world::Backend {
|
||||
fn id(&self) -> UUID;
|
||||
fn name(&self) -> String;
|
||||
fn connected(&self) -> bool;
|
||||
@@ -332,6 +361,7 @@ struct LiveBackend {
|
||||
subscriptions: Mutex<Vec<Subscription>>,
|
||||
inventory_state: Mutex<inventory::State>,
|
||||
world_state: Mutex<world::State>,
|
||||
services_state: Mutex<services::State>,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -393,6 +423,7 @@ impl LiveBackend {
|
||||
subscriptions: Mutex::new(Vec::new()),
|
||||
inventory_state: Mutex::new(inventory::State::new(account_policy)),
|
||||
world_state: Mutex::new(world::State::new(account_policy, get_textures)),
|
||||
services_state: Mutex::new(services::State::new(account_policy)),
|
||||
closed: AtomicBool::new(false),
|
||||
});
|
||||
backend.install(&events, &dropped);
|
||||
@@ -512,6 +543,7 @@ impl LiveBackend {
|
||||
},
|
||||
)));
|
||||
world::install_live(self, &mut subscriptions);
|
||||
services::install_live(self, &mut subscriptions);
|
||||
*lock(&self.subscriptions) = subscriptions;
|
||||
}
|
||||
}
|
||||
@@ -680,6 +712,7 @@ struct FakeBackend {
|
||||
group_members: Arc<Mutex<HashSet<UUID>>>,
|
||||
inventory_state: Mutex<inventory::State>,
|
||||
world_state: Mutex<world::State>,
|
||||
services_state: Mutex<services::State>,
|
||||
}
|
||||
|
||||
impl FakeBackend {
|
||||
@@ -700,6 +733,7 @@ impl FakeBackend {
|
||||
group_members,
|
||||
inventory_state: Mutex::new(inventory::State::new(policy)),
|
||||
world_state: Mutex::new(world::State::new(policy, get_textures)),
|
||||
services_state: Mutex::new(services::State::new(policy)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -820,10 +854,13 @@ impl ProgramOutput {
|
||||
enum CommandCategory {
|
||||
Appearance,
|
||||
Communication,
|
||||
Friends,
|
||||
Groups,
|
||||
Inventory,
|
||||
Movement,
|
||||
Objects,
|
||||
Parcel,
|
||||
Search,
|
||||
Simulator,
|
||||
TestClient,
|
||||
Other,
|
||||
@@ -838,6 +875,7 @@ enum CommandHandler {
|
||||
Im,
|
||||
ImGroup,
|
||||
Inventory(inventory::Command),
|
||||
Services(services::Command),
|
||||
World(world::Command),
|
||||
Load,
|
||||
Login,
|
||||
@@ -999,6 +1037,7 @@ fn built_in_commands() -> HashMap<String, CommandDefinition> {
|
||||
.into_iter()
|
||||
.chain(communication_commands())
|
||||
.chain(inventory::commands())
|
||||
.chain(services::commands())
|
||||
.chain(world::commands())
|
||||
.map(|(name, description, category, handler)| {
|
||||
(
|
||||
@@ -1780,6 +1819,9 @@ async fn execute_client_command(
|
||||
CommandHandler::Inventory(command) => {
|
||||
inventory::execute(client.backend.as_ref(), *command, args, from, cancellation).await
|
||||
}
|
||||
CommandHandler::Services(command) => {
|
||||
services::execute(client.backend.as_ref(), *command, args, cancellation).await
|
||||
}
|
||||
CommandHandler::World(command) => {
|
||||
world::execute(client.backend.as_ref(), *command, args, cancellation).await
|
||||
}
|
||||
@@ -2367,6 +2409,10 @@ async fn run_fake_event_directive(
|
||||
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
|
||||
return Ok(());
|
||||
}
|
||||
if let Some(result) = services::apply_fake_directive(manager, fields) {
|
||||
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
|
||||
return Ok(());
|
||||
}
|
||||
match fields {
|
||||
["chat", client, source, name, message] => {
|
||||
let event = ClientEvent::Chat {
|
||||
@@ -2670,6 +2716,7 @@ mod tests {
|
||||
assert!(TEST_CLIENT_COMMANDS.contains(command));
|
||||
assert!(!pending.contains(command));
|
||||
}
|
||||
assert_eq!(pending, ["ParcelVoiceInfo", "VoiceAcountCommand"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
2673
programs/src/test_client/services.rs
Normal file
2673
programs/src/test_client/services.rs
Normal file
File diff suppressed because it is too large
Load Diff
215
programs/tests/test_client_services_cli.rs
Normal file
215
programs/tests/test_client_services_cli.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
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 AVATAR: &str = "11111111-2222-3333-4444-555555555555";
|
||||
const BOT: &str = "22222222-3333-4444-5555-666666666666";
|
||||
const GROUP: &str = "33333333-4444-5555-6666-777777777777";
|
||||
const MEMBER: &str = "44444444-5555-6666-7777-888888888888";
|
||||
const ROLE: &str = "55555555-6666-7777-8888-999999999999";
|
||||
const CLASSIFIED: &str = "66666666-7777-8888-9999-aaaaaaaaaaaa";
|
||||
const LAND: &str = "77777777-8888-9999-aaaa-bbbbbbbbbbbb";
|
||||
const SIM: &str = "88888888-9999-aaaa-bbbb-cccccccccccc";
|
||||
const OBJECT: &str = "99999999-aaaa-bbbb-cccc-dddddddddddd";
|
||||
const DANCE1: &str = "b68a3d7c-de9e-fc87-eec8-543d787e5b0d";
|
||||
|
||||
static TEMP_ID: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestDir(PathBuf);
|
||||
impl TestDir {
|
||||
fn new() -> Self {
|
||||
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"metacrate-test-client-services-{}-{id}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir(&path).expect("create test directory");
|
||||
Self(path)
|
||||
}
|
||||
fn write(&self, contents: &str) -> PathBuf {
|
||||
let path = self.0.join("session.tsv");
|
||||
fs::write(&path, contents).expect("write script");
|
||||
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")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn full_offline_session_exercises_every_remaining_non_voice_command() {
|
||||
let directory = TestDir::new();
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\n\
|
||||
!service-avatar\t{CLIENT}\t{AVATAR}\tActive Resident\tBuilders\t128\t129\t25\t10\ttrue\n\
|
||||
!service-avatar\t{CLIENT}\t{BOT}\tQuiet Bot\tNone\t130\t131\t25\t11\tfalse\n\
|
||||
!service-friend\t{CLIENT}\t{AVATAR}\tActive Resident\ttrue\t4294967298000\t128\t129\t25\n\
|
||||
!service-group\t{CLIENT}\t{GROUP}\tBuilders Guild\n\
|
||||
!service-group-result\t{CLIENT}\t{GROUP}\tBuilders Guild\t42\n\
|
||||
!service-group-member\t{CLIENT}\t{GROUP}\t{MEMBER}\n\
|
||||
!service-group-role\t{CLIENT}\t{GROUP}\t{ROLE}\tOfficer\tArchitect\n\
|
||||
!service-person\t{CLIENT}\t{AVATAR}\tActive\tResident\ttrue\n\
|
||||
!service-classified\t{CLIENT}\t{CLASSIFIED}\tBuilder Services\t25\n\
|
||||
!service-event\t{CLIENT}\t7\tBuilding Class\tToday\n\
|
||||
!service-event-info\t{CLIENT}\t7\tBuilding Class\tTest Region\t1000\t2000\t25\n\
|
||||
!service-land\t{CLIENT}\t{LAND}\tBuilder Parcel\t1024\t500\n\
|
||||
!service-place\t{CLIENT}\tBuilder Plaza\tTest Region\t128\t128\t25\n\
|
||||
!service-sim\t{CLIENT}\tTest Region\t{SIM}\t4294967298000\t0.98\n\
|
||||
!service-utilization\t{CLIENT}\ttrue\n\
|
||||
!service-utilization-row\t{CLIENT}\tpacket\tAgentUpdate\t2\t3\t2048\t4096\n\
|
||||
!service-utilization-row\t{CLIENT}\tcapability\tEventQueueGet\t1\t4\t512\t8192\n\
|
||||
who\n\
|
||||
bots\n\
|
||||
friends\n\
|
||||
mapfriend {AVATAR}\n\
|
||||
key2name {AVATAR}\n\
|
||||
key2name {GROUP}\n\
|
||||
searchclassifieds Builder\n\
|
||||
searchevents Building\n\
|
||||
searchgroups Builders\n\
|
||||
searchland mainland 1000 512\n\
|
||||
searchpeople Active\n\
|
||||
searchplaces Builder\n\
|
||||
showevent 7\n\
|
||||
groups\n\
|
||||
groupmembers Builders Guild\n\
|
||||
grouproles Builders Guild\n\
|
||||
play list\n\
|
||||
play show\n\
|
||||
dilation\n\
|
||||
netstats\n\
|
||||
regioninfo\n\
|
||||
stats\n\
|
||||
uptime\n\
|
||||
cloneprofile {AVATAR} --confirm\n\
|
||||
sendgeneric test-method alpha beta --confirm\n\
|
||||
play DANCE1 --confirm\n\
|
||||
touch {OBJECT} --confirm\n\
|
||||
activategroup Builders Guild --confirm\n\
|
||||
invitegroup {AVATAR} {GROUP} {ROLE} --confirm\n\
|
||||
joingroup Builders Guild --confirm\n\
|
||||
leavegroup Builders Guild --confirm\n\
|
||||
quit\n"
|
||||
));
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--allow-spending",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
let stderr = String::from_utf8(output.stderr).expect("UTF-8 stderr");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stdout:\n{stdout}\nstderr:\n{stderr}"
|
||||
);
|
||||
assert!(stderr.is_empty(), "{stderr}");
|
||||
for expected in [
|
||||
"Active Resident (Group: Builders",
|
||||
"Quiet Bot (Group: None",
|
||||
"Is Probably a bot",
|
||||
"has 1 friends:",
|
||||
"Found Friend 11111111-2222-3333-4444-555555555555",
|
||||
"Avatar: Active Resident",
|
||||
"Group: Builders Guild",
|
||||
"returned 1 classified ads",
|
||||
"matched 1 Events",
|
||||
"matched 1 Groups",
|
||||
"1 results",
|
||||
"matched 1 People",
|
||||
"returned 1 results",
|
||||
"<redacted-url> Region/232/208/0",
|
||||
"got 1 groups",
|
||||
"MemberCount 1",
|
||||
"RoleCount 1",
|
||||
"AFRAID",
|
||||
DANCE1,
|
||||
"Dilation is 0.98",
|
||||
"Capabilities Totals",
|
||||
"Packet Totals",
|
||||
"UUID: 88888888-9999-aaaa-bbbb-cccccccccccc",
|
||||
"Packets in the queue:",
|
||||
"I am Alice Bot, Up Since:",
|
||||
"Synchronized our profile",
|
||||
"Sent generic message with method test-method",
|
||||
"Touched object",
|
||||
"Active group is now Builders Guild",
|
||||
"invited",
|
||||
"Joined the group Builders Guild",
|
||||
"has left the group Builders Guild",
|
||||
"CALL profile-clone",
|
||||
"CALL generic-message",
|
||||
"CALL animation-start",
|
||||
"CALL object-touch",
|
||||
"CALL group-activate",
|
||||
"CALL group-invite",
|
||||
"CALL group-join",
|
||||
"CALL group-leave",
|
||||
] {
|
||||
assert!(stdout.contains(expected), "missing {expected:?}:\n{stdout}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn social_mutations_require_startup_and_per_command_confirmation() {
|
||||
let directory = TestDir::new();
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\nplay {DANCE1} --confirm\nquit\n"
|
||||
));
|
||||
let output = run(&["--fake-script", path_text(&script)]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
assert!(output.status.success());
|
||||
assert!(stdout.contains("Live mutations are disabled"), "{stdout}");
|
||||
assert!(!stdout.contains("CALL animation-start"), "{stdout}");
|
||||
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\ntouch {OBJECT}\nquit\n"
|
||||
));
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
assert!(output.status.success());
|
||||
assert!(stdout.contains("append --confirm"), "{stdout}");
|
||||
assert!(!stdout.contains("CALL object-touch"), "{stdout}");
|
||||
|
||||
let script = directory.write(&format!(
|
||||
"!client\t{CLIENT}\tAlice\tBot\njoingroup uuid {GROUP} --confirm\nquit\n"
|
||||
));
|
||||
let output = run(&[
|
||||
"--allow-live-mutations",
|
||||
"--fake-script",
|
||||
path_text(&script),
|
||||
]);
|
||||
let stdout = String::from_utf8(output.stdout).expect("UTF-8 stdout");
|
||||
assert!(output.status.success());
|
||||
assert!(stdout.contains("restart with --allow-spending"), "{stdout}");
|
||||
assert!(!stdout.contains("CALL group-join"), "{stdout}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_inventory_leaves_only_explicit_voice_adapters() {
|
||||
assert_eq!(
|
||||
libremetaverse_programs::test_client::pending_test_client_commands(),
|
||||
["ParcelVoiceInfo", "VoiceAcountCommand"]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user