Translate complete RLV test suite
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
//! Shared deterministic fixtures used by translated compatibility tests.
|
||||
|
||||
pub mod rlv_support;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
|
||||
491
tests/compat/src/rlv_support.rs
Normal file
491
tests/compat/src/rlv_support.rs
Normal file
@@ -0,0 +1,491 @@
|
||||
//! Recording callbacks shared by the RLV translations.
|
||||
|
||||
use libremetaverse_rlv::{
|
||||
AttachmentRequest, CameraSettings, IRlvActionCallbacks, IRlvQueryCallbacks, InventoryMap,
|
||||
RlvAttachmentPoint, RlvInventoryItem, RlvService, RlvSharedFolder,
|
||||
};
|
||||
use libremetaverse_types::Error;
|
||||
use libremetaverse_types::compat::{CancellationToken, Guid};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub const SENDER_NAME: &str = "Sender 1";
|
||||
pub const SENDER_ID: &str = "ffffffff-ffff-4fff-8fff-ffffffffffff";
|
||||
|
||||
/// Parses a UUID literal used by the pinned RLV fixtures.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics when `value` is not a 32-digit hexadecimal UUID, ignoring hyphens.
|
||||
#[must_use]
|
||||
pub fn guid(value: &str) -> Guid {
|
||||
let digits: Vec<_> = value.bytes().filter(|byte| *byte != b'-').collect();
|
||||
assert_eq!(digits.len(), 32, "valid RLV test UUID");
|
||||
let mut bytes = [0; 16];
|
||||
for (index, pair) in digits.chunks_exact(2).enumerate() {
|
||||
let digit = |byte| match byte {
|
||||
b'0'..=b'9' => byte - b'0',
|
||||
b'a'..=b'f' => byte - b'a' + 10,
|
||||
b'A'..=b'F' => byte - b'A' + 10,
|
||||
_ => panic!("valid RLV test UUID"),
|
||||
};
|
||||
bytes[index] = digit(pair[0]) << 4 | digit(pair[1]);
|
||||
}
|
||||
Guid(bytes)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AttachmentCall {
|
||||
pub item_id: Guid,
|
||||
pub point: RlvAttachmentPoint,
|
||||
pub replace: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TeleportCall {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
pub region: Option<String>,
|
||||
pub look_at: Option<Option<f32>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ActionLog {
|
||||
pub adjusted_heights: Vec<(f32, f32, f32)>,
|
||||
pub attachments: Vec<Vec<AttachmentCall>>,
|
||||
pub detachments: Vec<Vec<Guid>>,
|
||||
pub removed_outfits: Vec<Vec<Guid>>,
|
||||
pub instant_messages: Vec<(Guid, String)>,
|
||||
pub replies: Vec<(i32, String)>,
|
||||
pub camera_fovs: Vec<f32>,
|
||||
pub debug_settings: Vec<(String, String)>,
|
||||
pub environment_settings: Vec<(String, String)>,
|
||||
pub group_ids: Vec<(Guid, Option<String>)>,
|
||||
pub group_names: Vec<(String, Option<String>)>,
|
||||
pub rotations: Vec<f32>,
|
||||
pub sits: Vec<Guid>,
|
||||
pub sit_ground_count: usize,
|
||||
pub teleports: Vec<TeleportCall>,
|
||||
pub unsit_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct RecordingActions(pub Arc<Mutex<ActionLog>>);
|
||||
|
||||
type ActionFuture<'a> = Pin<Box<dyn Future<Output = Result<(), Error>> + Send + 'a>>;
|
||||
|
||||
impl IRlvActionCallbacks for RecordingActions {
|
||||
fn adjust_height(
|
||||
&self,
|
||||
distance: f32,
|
||||
factor: f32,
|
||||
delta: f32,
|
||||
_: CancellationToken,
|
||||
) -> ActionFuture<'_> {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.adjusted_heights
|
||||
.push((distance, factor, delta));
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn attach(&self, requests: Vec<AttachmentRequest>, _: CancellationToken) -> ActionFuture<'_> {
|
||||
let calls = requests
|
||||
.iter()
|
||||
.map(|request| AttachmentCall {
|
||||
item_id: request.item_id(),
|
||||
point: request.attachment_point(),
|
||||
replace: request.replace_existing_attachments(),
|
||||
})
|
||||
.collect();
|
||||
self.0.lock().unwrap().attachments.push(calls);
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn detach(&self, item_ids: Vec<Guid>, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().detachments.push(item_ids);
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn rem_outfit(&self, item_ids: Vec<Guid>, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().removed_outfits.push(item_ids);
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn send_instant_message(
|
||||
&self,
|
||||
target: Guid,
|
||||
message: String,
|
||||
_: CancellationToken,
|
||||
) -> ActionFuture<'_> {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.instant_messages
|
||||
.push((target, message));
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn send_reply(&self, channel: i32, message: String, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().replies.push((channel, message));
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn set_cam_fov(&self, value: f32, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().camera_fovs.push(value);
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn set_debug(&self, name: String, value: String, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().debug_settings.push((name, value));
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn set_env(&self, name: String, value: String, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.environment_settings
|
||||
.push((name, value));
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn set_group_with_guid_string_cancellation_token(
|
||||
&self,
|
||||
id: Guid,
|
||||
role: Option<String>,
|
||||
_: CancellationToken,
|
||||
) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().group_ids.push((id, role));
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn set_group_with_string_string_cancellation_token(
|
||||
&self,
|
||||
name: String,
|
||||
role: Option<String>,
|
||||
_: CancellationToken,
|
||||
) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().group_names.push((name, role));
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn set_rot(&self, angle: f32, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().rotations.push(angle);
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn sit(&self, target: Guid, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().sits.push(target);
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn sit_ground(&self, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().sit_ground_count += 1;
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn tp_to(
|
||||
&self,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
region: Option<String>,
|
||||
look_at: Option<Option<f32>>,
|
||||
_: CancellationToken,
|
||||
) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().teleports.push(TeleportCall {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
region,
|
||||
look_at,
|
||||
});
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn unsit(&self, _: CancellationToken) -> ActionFuture<'_> {
|
||||
self.0.lock().unwrap().unsit_count += 1;
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct QueryState {
|
||||
pub is_sitting: bool,
|
||||
pub object_exists: HashMap<Guid, bool>,
|
||||
pub active_group: Option<String>,
|
||||
pub camera_settings: Option<[f32; 6]>,
|
||||
pub debug_settings: BTreeMap<String, String>,
|
||||
pub environment_settings: BTreeMap<String, String>,
|
||||
pub inventory_map: Option<InventoryMap>,
|
||||
pub sit_id: Option<Guid>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct RecordingQueries(pub Arc<Mutex<QueryState>>);
|
||||
|
||||
type QueryFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, Error>> + Send + 'a>>;
|
||||
|
||||
impl IRlvQueryCallbacks for RecordingQueries {
|
||||
fn is_sitting(&self, _: CancellationToken) -> QueryFuture<'_, bool> {
|
||||
let value = self.0.lock().unwrap().is_sitting;
|
||||
Box::pin(async move { Ok(value) })
|
||||
}
|
||||
|
||||
fn object_exists(&self, id: Guid, _: CancellationToken) -> QueryFuture<'_, bool> {
|
||||
let value = self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.object_exists
|
||||
.get(&id)
|
||||
.copied()
|
||||
.unwrap_or(false);
|
||||
Box::pin(async move { Ok(value) })
|
||||
}
|
||||
|
||||
fn try_get_active_group_name(&self, _: CancellationToken) -> QueryFuture<'_, (bool, String)> {
|
||||
let value = self.0.lock().unwrap().active_group.clone();
|
||||
Box::pin(async move { Ok(value.map_or((false, String::new()), |name| (true, name))) })
|
||||
}
|
||||
|
||||
fn try_get_camera_settings(
|
||||
&self,
|
||||
_: CancellationToken,
|
||||
) -> QueryFuture<'_, (bool, Option<CameraSettings>)> {
|
||||
let values = self.0.lock().unwrap().camera_settings;
|
||||
Box::pin(async move {
|
||||
let Some([av_min, av_max, fov_min, fov_max, zoom_min, current_fov]) = values else {
|
||||
return Ok((false, None));
|
||||
};
|
||||
Ok((
|
||||
true,
|
||||
Some(CameraSettings::new(
|
||||
av_min,
|
||||
av_max,
|
||||
fov_min,
|
||||
fov_max,
|
||||
zoom_min,
|
||||
current_fov,
|
||||
)?),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn try_get_debug_setting_value(
|
||||
&self,
|
||||
name: String,
|
||||
_: CancellationToken,
|
||||
) -> QueryFuture<'_, (bool, String)> {
|
||||
let value = self.0.lock().unwrap().debug_settings.get(&name).cloned();
|
||||
Box::pin(async move { Ok(value.map_or((false, String::new()), |value| (true, value))) })
|
||||
}
|
||||
|
||||
fn try_get_environment_setting_value(
|
||||
&self,
|
||||
name: String,
|
||||
_: CancellationToken,
|
||||
) -> QueryFuture<'_, (bool, String)> {
|
||||
let value = self
|
||||
.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.environment_settings
|
||||
.get(&name)
|
||||
.cloned();
|
||||
Box::pin(async move { Ok(value.map_or((false, String::new()), |value| (true, value))) })
|
||||
}
|
||||
|
||||
fn try_get_inventory_map(
|
||||
&self,
|
||||
_: CancellationToken,
|
||||
) -> QueryFuture<'_, (bool, Option<InventoryMap>)> {
|
||||
let value = self.0.lock().unwrap().inventory_map.take();
|
||||
Box::pin(async move { Ok((value.is_some(), value)) })
|
||||
}
|
||||
|
||||
fn try_get_sit_id(&self, _: CancellationToken) -> QueryFuture<'_, (bool, Guid)> {
|
||||
let value = self.0.lock().unwrap().sit_id;
|
||||
Box::pin(async move { Ok(value.map_or((false, Guid([0; 16])), |id| (true, id))) })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RlvHarness {
|
||||
pub service: RlvService,
|
||||
pub actions: Arc<Mutex<ActionLog>>,
|
||||
pub queries: Arc<Mutex<QueryState>>,
|
||||
}
|
||||
|
||||
impl RlvHarness {
|
||||
/// Builds the same enabled service and two narrow callback fakes as the C# base fixture.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the standardized constructor error while the public RLV service is a shim.
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let action_callbacks = RecordingActions::default();
|
||||
let query_callbacks = RecordingQueries::default();
|
||||
let actions = Arc::clone(&action_callbacks.0);
|
||||
let queries = Arc::clone(&query_callbacks.0);
|
||||
let service = RlvService::new(Box::new(query_callbacks), Box::new(action_callbacks), true)?;
|
||||
Ok(Self {
|
||||
service,
|
||||
actions,
|
||||
queries,
|
||||
})
|
||||
}
|
||||
|
||||
/// Processes a command using the base fixture sender identity.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the public RLV service error for the command.
|
||||
pub async fn process(&self, command: &str) -> Result<bool, Error> {
|
||||
self.service
|
||||
.process_message(command.into(), guid(SENDER_ID), SENDER_NAME.into(), None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Processes a command using an explicit sender identity.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns the public RLV service error for the command.
|
||||
pub async fn process_from(
|
||||
&self,
|
||||
command: &str,
|
||||
sender_id: &str,
|
||||
sender_name: &str,
|
||||
) -> Result<bool, Error> {
|
||||
self.service
|
||||
.process_message(command.into(), guid(sender_id), sender_name.into(), None)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SampleInventoryTree {
|
||||
pub root: RlvSharedFolder,
|
||||
pub clothing: RlvSharedFolder,
|
||||
pub hats: RlvSharedFolder,
|
||||
pub sub_hats: RlvSharedFolder,
|
||||
pub accessories: RlvSharedFolder,
|
||||
pub fancy_hat: RlvInventoryItem,
|
||||
pub party_hat: RlvInventoryItem,
|
||||
pub business_pants: RlvInventoryItem,
|
||||
pub retro_pants: RlvInventoryItem,
|
||||
pub happy_shirt: RlvInventoryItem,
|
||||
pub glasses: RlvInventoryItem,
|
||||
pub watch: RlvInventoryItem,
|
||||
}
|
||||
|
||||
impl SampleInventoryTree {
|
||||
/// Builds the exact `#RLV` hierarchy shared by the upstream fixtures.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a mapped constructor or collection error.
|
||||
pub fn build() -> Result<Self, Error> {
|
||||
let root =
|
||||
RlvSharedFolder::new(guid("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"), "#RLV".into())?;
|
||||
let clothing = root.add_child(
|
||||
guid("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
||||
"Clothing".into(),
|
||||
)?;
|
||||
let hats =
|
||||
clothing.add_child(guid("dddddddd-dddd-4ddd-8ddd-dddddddddddd"), "Hats".into())?;
|
||||
let sub_hats = hats.add_child(
|
||||
guid("ffffffff-0000-4000-8000-000000000000"),
|
||||
"Sub Hats".into(),
|
||||
)?;
|
||||
root.add_child(
|
||||
guid("eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"),
|
||||
".private".into(),
|
||||
)?;
|
||||
let accessories = root.add_child(
|
||||
guid("cccccccc-cccc-4ccc-8ccc-cccccccccccc"),
|
||||
"Accessories".into(),
|
||||
)?;
|
||||
let watch = accessories.add_item(
|
||||
guid("c0000000-cccc-4ccc-8ccc-cccccccccccc"),
|
||||
"Watch".into(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let glasses = accessories.add_item(
|
||||
guid("c1111111-cccc-4ccc-8ccc-cccccccccccc"),
|
||||
"Glasses".into(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let business_pants = clothing.add_item(
|
||||
guid("b0000000-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
||||
"Business Pants (Pelvis)".into(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let happy_shirt = clothing.add_item(
|
||||
guid("b1111111-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
||||
"Happy Shirt".into(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let retro_pants = clothing.add_item(
|
||||
guid("b2222222-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
|
||||
"Retro Pants".into(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let party_hat = hats.add_item(
|
||||
guid("d0000000-dddd-4ddd-8ddd-dddddddddddd"),
|
||||
"Party Hat (Spine)".into(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
let fancy_hat = hats.add_item(
|
||||
guid("d1111111-dddd-4ddd-8ddd-dddddddddddd"),
|
||||
"Fancy Hat (chin)".into(),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
Ok(Self {
|
||||
root,
|
||||
clothing,
|
||||
hats,
|
||||
sub_hats,
|
||||
accessories,
|
||||
fancy_hat,
|
||||
party_hat,
|
||||
business_pants,
|
||||
retro_pants,
|
||||
happy_shirt,
|
||||
glasses,
|
||||
watch,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user