Complete first release candidate audit (#107)
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled

This commit is contained in:
2026-08-12 14:44:28 +00:00
parent dceb394378
commit c9a1170a27
140 changed files with 82175 additions and 27179 deletions

View File

@@ -10,6 +10,15 @@ use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Wake, Waker};
use std::time::Duration;
const OPENSIM_LOGIN_OPTIONS: [&str; 6] = [
"avatar_picker_url",
"classified_fee",
"currency",
"destination_guide_url",
"profile-server-url",
"search",
];
/// Runs one test future without choosing an async runtime for the library.
pub fn block_on<F: Future>(future: F) -> F::Output {
struct ThreadWake(std::thread::Thread);
@@ -31,6 +40,121 @@ pub fn block_on<F: Future>(future: F) -> F::Output {
}
}
/// Tokio runtime kept alive for the complete lifetime of a live-grid fixture.
///
/// Network login starts UDP, capability, timer, and cancellation tasks. Keeping
/// this multi-thread runtime in the fixture lets those tasks continue running
/// while a synchronous compatibility assertion observes grid state.
pub struct LiveTestRuntime(tokio::runtime::Runtime);
impl LiveTestRuntime {
/// Creates a live-I/O runtime with all Tokio drivers enabled.
///
/// # Panics
///
/// Panics if worker threads or the runtime's I/O driver cannot be created.
#[must_use]
pub fn new() -> Self {
Self(
tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("create live-grid Tokio runtime"),
)
}
/// Runs one live-grid future while retaining the runtime for background I/O.
pub fn block_on<F: Future>(&self, future: F) -> F::Output {
self.0.block_on(future)
}
}
impl Default for LiveTestRuntime {
fn default() -> Self {
Self::new()
}
}
/// Authenticates a test client against the exact `OpenSim` endpoint in `.env`.
///
/// The helper adds the `OpenSim` response options used by the dedicated smoke
/// program and retries only the server's explicit stale-session response. It
/// does not substitute a Second Life endpoint, region, or start location.
///
/// # Panics
///
/// Panics if parameters cannot be constructed or the dedicated account cannot
/// authenticate within 90 seconds.
pub fn login_live_grid(
runtime: &LiveTestRuntime,
client: &mut libremetaverse::GridClient,
channel: &str,
version: &str,
) -> libremetaverse::NetworkManager {
client.settings().timing().login_timeout = 30_000;
client.settings().timing().logout_timeout = 5_000;
// OpenSim sends inventory bootstrap and initial scene packets as part of
// login. These lazily constructed services must install their consumers
// before credentials cross the wire or that one-shot state is lost.
let _ = client.self_();
let _ = client.objects();
let _ = client.inventory();
let _ = client.assets();
let _ = client.appearance();
let _ = client.avatars();
let _ = client.grid();
let _ = client.estate();
let network = client.network();
let (first, last, password, login_url) = live_grid_credentials();
let deadline = std::time::Instant::now() + Duration::from_secs(90);
loop {
let mut login = network
.default_login_params(
first.clone(),
last.clone(),
password.clone(),
channel.to_owned(),
version.to_owned(),
)
.expect("NetworkManager DefaultLoginParams");
login.uri.clone_from(&login_url);
for option in OPENSIM_LOGIN_OPTIONS {
if !login.options.iter().any(|existing| existing == option) {
login.options.push(option.to_owned());
}
}
let logged_in = runtime
.block_on(network.login_with_login_params_cancellation_token(login, None))
.expect("NetworkManager LoginAsync");
if logged_in {
assert!(network.connected(), "client is not connected to the grid");
let simulator = network
.current_sim()
.expect("CurrentSim is null after successful OpenSim login");
client
.self_()
.complete_agent_movement(simulator)
.expect("complete OpenSim agent movement after login");
return network;
}
let message = network.login_message();
let stale_session = message.to_ascii_lowercase().contains("already logged in");
assert!(
stale_session && std::time::Instant::now() < deadline,
"OpenSim login failed: {message}"
);
std::thread::sleep(Duration::from_secs(5));
}
}
/// Performs the protocol logout while the live runtime is still active.
pub fn logout_live_grid(runtime: &LiveTestRuntime, client: &mut libremetaverse::GridClient) {
let network = client.network();
let _ = runtime.block_on(network.logout_with_cancellation_token(None));
let _ = client.dispose_with_method();
}
/// Loads the live-grid identity from the process environment or workspace `.env`.
///
/// # Panics

View File

@@ -1,9 +1,9 @@
// Exact Rust translations of LibreMetaverse.Tests/AppearanceLiveTests.cs.
// Source SHA-256: 9f4ec256d11290cbad70e209d2f9043d4c6afb18255796c09c4cb26415f971a6
use libremetaverse::GridClient;
use libremetaverse::appearance::CurrentOutfitFolder;
use libremetaverse::{GridClient, NetworkManager};
use libremetaverse_compat_tests::{block_on, live_grid_credentials};
use libremetaverse_compat_tests::{LiveTestRuntime, login_live_grid, logout_live_grid};
use libremetaverse_types::{UUID, WearableType};
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -11,47 +11,36 @@ use std::time::{Duration, Instant};
struct LiveAppearance {
client: Arc<GridClient>,
cof: CurrentOutfitFolder,
runtime: LiveTestRuntime,
}
impl LiveAppearance {
fn login() -> Self {
let runtime = LiveTestRuntime::new();
let mut client = GridClient::new().expect("GridClient constructor");
client.settings().timing().login_timeout = 30_000;
let network = client.network();
let (first, last, password, login_url) = live_grid_credentials();
let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
.expect("NetworkManager StartLocation");
let mut login = network
.default_login_params(
first,
last,
password,
"Unit Test Framework".into(),
"admin@radegast.life".into(),
)
.expect("NetworkManager DefaultLoginParams");
login.start = start;
login.uri = login_url;
let logged_in = block_on(network.login_with_login_params_cancellation_token(login, None))
.expect("NetworkManager LoginAsync");
assert!(
logged_in,
"client failed to login: {}",
network.login_message()
login_live_grid(
&runtime,
&mut client,
"Unit Test Framework",
"admin@radegast.life",
);
assert!(network.connected(), "client is not connected to the grid");
let client = Arc::new(client);
let cof = CurrentOutfitFolder::new(Some(Arc::clone(&client)))
.expect("CurrentOutfitFolder constructor");
Self { client, cof }
Self {
client,
cof,
runtime,
}
}
}
impl Drop for LiveAppearance {
fn drop(&mut self) {
let _ = self.cof.dispose();
let _ = self.client.network().logout_with_method();
let _ = self.client.dispose_with_method();
if let Some(client) = Arc::get_mut(&mut self.client) {
logout_live_grid(&self.runtime, client);
}
}
}
@@ -63,7 +52,9 @@ impl Drop for LiveAppearance {
)]
fn cof_is_initialized_after_login() {
let live = LiveAppearance::login();
block_on(live.cof.get_current_outfit_links(None)).unwrap();
live.runtime
.block_on(live.cof.get_current_outfit_links(None))
.unwrap();
let folder = live.cof.cof().expect("COF folder is null");
assert_ne!(folder.base.uuid(), UUID::zero());
}
@@ -76,7 +67,9 @@ fn cof_is_initialized_after_login() {
)]
fn cof_has_expected_folder_name() {
let live = LiveAppearance::login();
block_on(live.cof.get_current_outfit_links(None)).unwrap();
live.runtime
.block_on(live.cof.get_current_outfit_links(None))
.unwrap();
let folder = live.cof.cof().expect("COF folder is null");
assert!(folder.base.name().eq_ignore_ascii_case("Current Outfit"));
}
@@ -89,7 +82,10 @@ fn cof_has_expected_folder_name() {
)]
fn get_current_outfit_links_returns_links() {
let live = LiveAppearance::login();
let links = block_on(live.cof.get_current_outfit_links(None)).unwrap();
let links = live
.runtime
.block_on(live.cof.get_current_outfit_links(None))
.unwrap();
for link in links {
assert!(link.is_link().unwrap(), "returned COF item is not a link");
}
@@ -103,12 +99,14 @@ fn get_current_outfit_links_returns_links() {
)]
fn get_current_outfit_links_contains_at_least_one_link() {
let live = LiveAppearance::login();
let links = block_on(live.cof.get_current_outfit_links(None)).unwrap();
if links.is_empty() {
eprintln!("COF returned zero links; equip at least a default shape for this assertion");
return;
}
assert!(!links.is_empty());
let links = live
.runtime
.block_on(live.cof.get_current_outfit_links(None))
.unwrap();
assert!(
!links.is_empty(),
"COF returned zero links; the dedicated account must wear at least its default shape"
);
}
// parity-case: LibreMetaverse.Tests/AppearanceLiveTests.cs::AppearanceLiveTests.GetWornAt_Shape_ReturnsAtLeastOne::test abc01e346371910610312ff9352ee05c0d317a7c677c0fa8cb398d3b0522c3bf ignored-live
@@ -119,12 +117,14 @@ fn get_current_outfit_links_contains_at_least_one_link() {
)]
fn get_worn_at_shape_returns_at_least_one() {
let live = LiveAppearance::login();
let worn = block_on(live.cof.get_worn_at(WearableType::Shape, None)).unwrap();
if worn.is_empty() {
eprintln!("GetWornAtAsync(Shape) returned no items for this account");
return;
}
assert!(!worn.is_empty());
let worn = live
.runtime
.block_on(live.cof.get_worn_at(WearableType::Shape, None))
.unwrap();
assert!(
!worn.is_empty(),
"GetWornAtAsync(Shape) returned no items for the dedicated account"
);
}
// parity-case: LibreMetaverse.Tests/AppearanceLiveTests.cs::AppearanceLiveTests.LastUpdateReceivedCOFVersion_AdvancesAfterLogin::test 5371df8b02df9da20b413f4155f63967184dad23f57708beb56a2c6559e78520 ignored-live
@@ -144,11 +144,10 @@ fn last_update_received_cof_version_advances_after_login() {
}
std::thread::sleep(Duration::from_millis(500));
}
if version == -1 {
eprintln!("the simulator did not send AvatarAppearance for self within 25 seconds");
return;
}
assert!(version > -1);
assert!(
version > -1,
"the OpenSim simulator did not send AvatarAppearance for self within 25 seconds"
);
}
// parity-case: LibreMetaverse.Tests/AppearanceLiveTests.cs::AppearanceLiveTests.RequestOwnAvatarTextures_DoesNotThrow_AndMaintainsVersionGuard::test ee8420690fc9714411450beb5be5755b82ddf7ed0d69e9da88b3184339bea744 ignored-live

View File

@@ -18,7 +18,25 @@ use std::any::TypeId;
use std::sync::Arc;
struct RecordingTextureProvider;
impl IBakingTextureProvider for RecordingTextureProvider {}
impl IBakingTextureProvider for RecordingTextureProvider {
fn request_texture(
&self,
_texture_id: UUID,
_cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
) -> std::pin::Pin<
Box<
dyn std::future::Future<
Output = Result<
Option<libremetaverse::assets::AssetTexture>,
libremetaverse::Error,
>,
> + Send
+ '_,
>,
> {
Box::pin(async { Ok(None) })
}
}
// parity-case: LibreMetaverse.Tests/AppearanceManagerTests.cs::AppearanceManagerTests.WearableTypeToAssetType_BodypartsAndClothing_ReturnsExpected::test 6d0f0d09a6da8121ba828159a75ca1c1ce54329ec48060a6786aa8f807c13ec0 translated
#[test]

View File

@@ -233,6 +233,7 @@ fn collada_triangle_converts_to_native_model_asset() {
}
#[test]
#[allow(clippy::float_cmp)] // Parsed fixture values are exact decimal constants.
fn collada_applies_units_effects_and_instance_material_bindings() {
let root = temporary_directory("collada-material");
let file = root.join("material.dae");
@@ -274,8 +275,8 @@ fn collada_texture_path_cannot_escape_document_root() {
let file = root.join("traversal.dae");
fs::write(
&file,
r##"<COLLADA><library_images><image id="escape"><init_from>../secret.jp2</init_from></image></library_images>
<library_materials><material id="mat"><diffuse><texture texture="escape"/></diffuse></material></library_materials></COLLADA>"##,
r#"<COLLADA><library_images><image id="escape"><init_from>../secret.jp2</init_from></image></library_images>
<library_materials><material id="mat"><diffuse><texture texture="escape"/></diffuse></material></library_materials></COLLADA>"#,
)
.expect("write traversal fixture");
assert!(

View File

@@ -8,14 +8,6 @@ use libremetaverse::{
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription};
use libremetaverse_types::{AssetType, AttachmentPoint, UUID};
fn member_id<T>(result: Result<T, libremetaverse::Error>) -> &'static str {
result
.err()
.expect("failure-only shim unexpectedly returned a value")
.csharp_member()
.expect("failure-only shim error")
}
fn compile_bot_and_animation_calls(
agent: &AgentManager,
chat_type: ChatType,
@@ -95,7 +87,7 @@ fn avatar_facing_flows_have_typed_callable_signatures() {
}
#[test]
fn avatar_facing_constructors_preserve_native_defaults_and_catalog_errors() {
fn avatar_facing_constructors_preserve_native_defaults() {
let download = AssetDownload::new().expect("AssetDownload constructor");
assert_eq!(download.asset_id, UUID::zero());
assert_eq!(download.next_packet, 0);
@@ -111,10 +103,9 @@ fn avatar_facing_constructors_preserve_native_defaults_and_catalog_errors() {
assert_eq!(wearable.item_id, UUID::zero());
assert!(wearable.asset.is_none());
assert_eq!(
member_id(InventoryException::new_with_constructor()),
"M:LibreMetaverse.InventoryException.#ctor"
);
let inventory_error =
InventoryException::new_with_constructor().expect("InventoryException constructor");
assert_eq!(inventory_error.to_string(), "inventory operation failed");
}
#[test]

View File

@@ -6,14 +6,6 @@ use libremetaverse_utilities::ConnectionManager;
use libremetaverse_voice_vivox::{TCPPipe, VoiceManager as VivoxVoiceManager};
use libremetaverse_voice_webrtc::{LibreMetaverseVoiceLogger, VoiceManager as WebRtcVoiceManager};
fn member_id<T>(result: Result<T, libremetaverse_types::Error>) -> &'static str {
result
.err()
.expect("failure-only shim unexpectedly returned a value")
.csharp_member()
.expect("failure-only shim error")
}
async fn compile_rlv_calls(
service: &RlvService,
restrictions: &RlvRestrictionManager,
@@ -65,15 +57,11 @@ fn extension_flows_have_typed_callable_signatures() {
}
#[test]
fn extension_constructors_have_expected_native_or_failure_behavior() {
fn extension_constructors_have_expected_native_behavior() {
assert!(YyParser::new().is_ok());
assert!(RlvActionCallbacksDefault::new().is_ok());
assert_eq!(
member_id(TCPPipe::new()),
"M:LibreMetaverse.Voice.Vivox.TCPPipe.#ctor"
);
assert_eq!(
member_id(LibreMetaverseVoiceLogger::new()),
"M:LibreMetaverse.Voice.WebRTC.LibreMetaverseVoiceLogger.#ctor"
);
let pipe = TCPPipe::new().expect("TCPPipe constructor");
assert!(!pipe.connected());
let logger = LibreMetaverseVoiceLogger::new().expect("voice logger constructor");
assert!(logger.records().is_empty());
}

View File

@@ -4,62 +4,122 @@
// Source SHA-256: 1d65ebbaf8fd22427371e2a7f1ef76a4f9a70df0bea7fa53f8f5d3fd3044fa53
use libremetaverse::{
GridClient, InventoryFolder, InventoryItem, InventoryObjectClass, InventorySortOrder,
NetworkManager,
Error, GridClient, InventoryFolder, InventoryItem, InventoryObjectClass, InventorySortOrder,
PermissionMask,
};
use libremetaverse_compat_tests::{block_on, live_grid_credentials};
use libremetaverse_types::UUID;
use libremetaverse_compat_tests::{LiveTestRuntime, login_live_grid, logout_live_grid};
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource};
use libremetaverse_types::{AssetType, FolderType, InventoryType, UUID};
use std::sync::Arc;
use std::time::Duration;
struct LiveInventory {
client: GridClient,
runtime: LiveTestRuntime,
disposable_items: Vec<UUID>,
}
impl LiveInventory {
fn login() -> Self {
let runtime = LiveTestRuntime::new();
let mut client = GridClient::new().expect("GridClient constructor");
client.settings().timing().login_timeout = 30_000;
let network = client.network();
let (first, last, password, login_url) = live_grid_credentials();
let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
.expect("NetworkManager StartLocation");
let mut login = network
.default_login_params(
first,
last,
password,
"Unit Test Framework".into(),
"admin@radegast.life".into(),
)
.expect("NetworkManager DefaultLoginParams");
login.start = start;
login.uri = login_url;
let logged_in = block_on(network.login_with_login_params_cancellation_token(login, None))
.expect("NetworkManager LoginAsync");
assert!(
logged_in,
"client failed to login: {}",
network.login_message()
login_live_grid(
&runtime,
&mut client,
"Unit Test Framework",
"admin@radegast.life",
);
assert!(network.connected(), "client is not connected to the grid");
assert!(
network.current_sim().is_some(),
"CurrentSim is null after successful login"
);
Self { client }
Self {
client,
runtime,
disposable_items: Vec::new(),
}
}
fn agent_id(&mut self) -> UUID {
self.client.self_().agent_id()
}
fn create_disposable_texture(&mut self) -> (UUID, InventoryItem) {
let manager = self.client.inventory();
let folder = manager
.find_folder_for_type_with_asset_type(AssetType::Texture)
.expect("find OpenSim texture folder");
assert_ne!(folder, UUID::zero(), "OpenSim has no texture folder");
let marker = UUID::random().expect("inventory fixture UUID");
let name = format!("MetaCrate Live Test {marker}");
let agent_id = self.agent_id();
for attempt in 1..=3 {
let result = self.runtime.block_on(
manager.create_item_with_uuid_string_string_asset_type_uuid_inventory_type_permission_mask_cancellation_token(
folder,
name.clone(),
"Disposable OpenSim compatibility fixture".into(),
AssetType::Texture,
UUID::zero(),
InventoryType::TEXTURE,
PermissionMask::ALL,
Some(cancellation_after(Duration::from_secs(30))),
),
);
match result {
Ok(Some(item)) => {
self.disposable_items.push(item.base.uuid());
return (folder, item);
}
Ok(None) | Err(Error::Cancelled) => {}
Err(error) => panic!("create disposable inventory item: {error}"),
}
// A UDP callback can be lost even though OpenSim committed the
// request. Reconcile against the server before retrying so a lost
// callback does not create duplicate fixtures.
if let Ok(contents) = self.runtime.block_on(manager.folder_contents(
folder,
agent_id,
true,
true,
InventorySortOrder::BY_NAME,
Some(cancellation_after(Duration::from_secs(30))),
None,
)) && let Some(item) = items(&contents)
.find(|item| item.base.name() == name)
.cloned()
{
self.disposable_items.push(item.base.uuid());
return (folder, item);
}
assert!(
attempt < 3,
"OpenSim did not create or return the disposable item"
);
}
unreachable!("bounded inventory fixture retry loop")
}
}
impl Drop for LiveInventory {
fn drop(&mut self) {
let _ = self.client.network().logout_with_method();
let _ = self.client.dispose_with_method();
if let Ok(trash) = self
.client
.inventory()
.find_folder_for_type_with_folder_type(FolderType::Trash)
{
for item in self.disposable_items.drain(..) {
self.runtime
.block_on(
self.client
.inventory()
.move_item_with_uuid_uuid_cancellation_token_1cf693fd(
item,
trash,
Some(CancellationToken::default()),
),
)
.expect("move disposable inventory item to trash");
}
}
logout_live_grid(&self.runtime, &mut self.client);
}
}
@@ -80,9 +140,7 @@ fn folders(entries: &[Box<dyn InventoryObjectClass>]) -> impl Iterator<Item = &I
}
fn items(entries: &[Box<dyn InventoryObjectClass>]) -> impl Iterator<Item = &InventoryItem> {
entries
.iter()
.filter_map(|entry| entry.as_any().downcast_ref::<InventoryItem>())
entries.iter().filter_map(|entry| entry.inventory_item())
}
// parity-case: LibreMetaverse.Tests/InventoryFetchTests.cs::InventoryFetchTests.InventorySkeletonIsPopulatedAfterLogin::test ea1508e3c676639f00bb8a4c4c72d2ee668603bfbff40ee0b93d7f4beb607f75 ignored-live
@@ -123,20 +181,22 @@ fn inventory_root_contains_folders() {
.root_folder()
.expect("inventory root folder");
assert_ne!(root.base.uuid(), UUID::zero());
let contents = block_on(live.client.inventory().folder_contents(
root.base.uuid(),
agent_id,
true,
true,
InventorySortOrder::BY_NAME,
Some(cancellation_after(Duration::from_secs(30))),
None,
))
.expect("FolderContentsAsync");
if contents.is_empty() {
eprintln!("inventory root fetch returned no results; transient server issue");
return;
}
let contents = live
.runtime
.block_on(live.client.inventory().folder_contents(
root.base.uuid(),
agent_id,
true,
true,
InventorySortOrder::BY_NAME,
Some(cancellation_after(Duration::from_secs(30))),
None,
))
.expect("FolderContentsAsync");
assert!(
!contents.is_empty(),
"inventory root fetch returned no results"
);
assert!(folders(&contents).next().is_some());
}
@@ -148,49 +208,37 @@ fn inventory_root_contains_folders() {
)]
fn inventory_folder_contains_items() {
let mut live = LiveInventory::login();
let (_folder, created) = live.create_disposable_texture();
let created_id = created.base.uuid();
let agent_id = live.agent_id();
let store = live.client.inventory().store().expect("inventory store");
let root = store.root_folder().expect("inventory root folder");
assert_ne!(root.base.uuid(), UUID::zero());
let skeleton = store
.get_contents_with_inventory_folder(&root)
.expect("inventory skeleton");
let subfolders: Vec<_> = folders(&skeleton).collect();
assert!(!subfolders.is_empty(), "no inventory sub-folders");
let token = cancellation_after(Duration::from_secs(30));
let mut found = None;
for folder in subfolders {
if token.is_cancellation_requested() {
break;
}
let Ok(contents) = block_on(live.client.inventory().folder_contents(
folder.base.uuid(),
// OpenSim's CreateInventoryItem reply does not necessarily populate the
// viewer-side store. Fetch the server-selected parent explicitly so this
// case validates the grid's folder-contents service rather than cache
// timing inherited from the Second Life test account.
let actual_parent = created.base.parent_uuid();
assert_ne!(
actual_parent,
UUID::zero(),
"created item has no parent folder"
);
let contents = live
.runtime
.block_on(live.client.inventory().folder_contents(
actual_parent,
agent_id,
true,
true,
InventorySortOrder::BY_NAME,
Some(token.clone()),
Some(cancellation_after(Duration::from_secs(30))),
None,
)) else {
continue;
};
let found_items: Vec<_> = items(&contents).collect();
if let Some(first) = found_items.first() {
found = Some((
folder.base.name(),
found_items.len(),
first.base.name(),
first.asset_type(),
));
break;
}
}
let Some((folder_name, count, first_name, first_type)) = found else {
eprintln!("no items found in any inventory sub-folder");
return;
};
assert!(count > 0, "folder {folder_name} has no items");
eprintln!("found {count} items in {folder_name}; first: {first_name} ({first_type:?})");
))
.expect("fetch server-selected inventory folder contents");
let fetched = items(&contents)
.find(|item| item.base.uuid() == created_id)
.expect("server-created item was not cached in its parent folder");
assert_eq!(fetched.base.parent_uuid(), actual_parent);
assert_eq!(fetched.asset_type(), AssetType::Texture);
assert_eq!(fetched.inventory_type(), InventoryType::TEXTURE);
}
// parity-case: LibreMetaverse.Tests/InventoryFetchTests.cs::InventoryFetchTests.InventoryItemCanBeFetchedById::test 7fe89086754a673ab69814202b48aab9add30dd63ee98a953bdedd54d6788af2 ignored-live
@@ -202,46 +250,19 @@ fn inventory_folder_contains_items() {
fn inventory_item_can_be_fetched_by_id() {
let mut live = LiveInventory::login();
let agent_id = live.agent_id();
let store = live.client.inventory().store().expect("inventory store");
let root = store.root_folder().expect("inventory root folder");
let skeleton = store
.get_contents_with_inventory_folder(&root)
.expect("inventory skeleton");
let subfolders: Vec<_> = folders(&skeleton).collect();
assert!(!subfolders.is_empty(), "no inventory sub-folders");
let token = cancellation_after(Duration::from_secs(30));
let mut target = None;
for folder in subfolders {
if token.is_cancellation_requested() {
break;
}
let Ok(contents) = block_on(live.client.inventory().folder_contents(
folder.base.uuid(),
agent_id,
true,
true,
InventorySortOrder::BY_NAME,
Some(token.clone()),
None,
)) else {
continue;
};
if let Some(item) = items(&contents).next() {
target = Some((item.base.uuid(), item.base.name()));
break;
}
}
let Some((target_id, target_name)) = target else {
eprintln!("no inventory item found to fetch by UUID");
return;
};
let fetched = block_on(live.client.inventory().fetch_item_http(
target_id,
agent_id,
Some(token),
))
.expect("FetchItemHttpAsync")
.expect("fetched inventory item");
let (_, created) = live.create_disposable_texture();
let target_id = created.base.uuid();
let target_name = created.base.name();
let fetched = live
.runtime
.block_on(
live.client
.inventory()
.fetch_item_http(target_id, agent_id, Some(token)),
)
.expect("FetchItemHttpAsync")
.expect("fetched inventory item");
assert_eq!(fetched.base.uuid(), target_id);
assert_eq!(fetched.base.name(), target_name);
}
@@ -289,20 +310,22 @@ fn library_folder_items_can_be_fetched() {
.map(|folder| folder.base.owner_id())
.find(|owner| *owner != UUID::zero())
.expect("library owner UUID");
let contents = block_on(live.client.inventory().folder_contents(
root.base.uuid(),
owner,
true,
true,
InventorySortOrder::BY_NAME,
Some(cancellation_after(Duration::from_secs(30))),
None,
))
.expect("FolderContentsAsync");
if contents.is_empty() {
eprintln!("library root fetch returned no results; transient server issue");
return;
}
let contents = live
.runtime
.block_on(live.client.inventory().folder_contents(
root.base.uuid(),
owner,
true,
true,
InventorySortOrder::BY_NAME,
Some(cancellation_after(Duration::from_secs(30))),
None,
))
.expect("FolderContentsAsync");
assert!(
!contents.is_empty(),
"library root fetch returned no results"
);
assert!(folders(&contents).next().is_some());
}
@@ -327,31 +350,37 @@ fn library_sub_folder_contains_items() {
.find(|owner| *owner != UUID::zero())
.expect("library owner UUID");
let token = cancellation_after(Duration::from_secs(30));
let mut found = None;
let mut fetched_folders = 0_usize;
for folder in subfolders {
if token.is_cancellation_requested() {
break;
}
let Ok(contents) = block_on(live.client.inventory().folder_contents(
folder.base.uuid(),
owner,
true,
true,
InventorySortOrder::BY_NAME,
Some(token.clone()),
None,
)) else {
let Ok(contents) = live
.runtime
.block_on(live.client.inventory().folder_contents(
folder.base.uuid(),
owner,
true,
true,
InventorySortOrder::BY_NAME,
Some(token.clone()),
None,
))
else {
continue;
};
let count = items(&contents).count();
if count > 0 {
found = Some((folder.base.name(), count));
break;
fetched_folders += 1;
for entry in contents {
assert_eq!(
entry.inventory_base().parent_uuid(),
folder.base.uuid(),
"OpenSim returned a library child with the wrong parent"
);
assert_ne!(entry.inventory_base().uuid(), UUID::zero());
}
}
let Some((folder_name, count)) = found else {
eprintln!("no items found in any library sub-folder");
return;
};
assert!(count > 0, "folder {folder_name} has no items");
assert!(
fetched_folders > 0,
"no OpenSim library sub-folder could be fetched"
);
}

View File

@@ -8,8 +8,8 @@
use libremetaverse::http::DownloadManager;
use libremetaverse::packets::PacketType;
use libremetaverse::{GridClient, GridLayerType, HttpCapsClient, NetworkManager};
use libremetaverse_compat_tests::{block_on, live_grid_credentials};
use libremetaverse::{GridClient, GridLayerType, HttpCapsClient};
use libremetaverse_compat_tests::{LiveTestRuntime, block_on, login_live_grid, logout_live_grid};
use libremetaverse_types::compat::{
CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
@@ -158,10 +158,6 @@ where
.await
}
fn live_credentials() -> (String, String, String, String) {
live_grid_credentials()
}
// parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_SingleDownload_CompletesSuccessfully::test f8feb54d38f79624ddf1d368e0599259ff9d474c08ea0249a8f244460a1f533f translated
#[test]
fn queue_download_single_completes_successfully() {
@@ -349,61 +345,41 @@ fn put_response_with_non_http_location_does_not_throw() {
ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn get_grid_region_live() {
let runtime = LiveTestRuntime::new();
let mut client = GridClient::new().expect("GridClient constructor");
client.self_().movement.set_fly(true);
let network = client.network();
let (first, last, password, login_url) = live_credentials();
let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
.expect("NetworkManager StartLocation");
let mut login = network
.default_login_params(
first,
last,
password,
"Unit Test Framework".into(),
"contact@radegast.life".into(),
)
.expect("NetworkManager DefaultLoginParams");
login.start = start;
login.uri = login_url;
let logged_in = block_on(network.login_with_login_params_cancellation_token(login, None))
.expect("NetworkManager LoginAsync");
assert!(
logged_in,
"client failed to log in: {}",
network.login_message()
let network = login_live_grid(
&runtime,
&mut client,
"Unit Test Framework",
"contact@radegast.life",
);
assert!(network.connected(), "client is not connected to the grid");
let simulator = network
.current_sim()
.expect("CurrentSim is null after successful login");
if simulator.name.is_empty() {
eprintln!("CurrentSim.Name is empty after login, proceeding with tests");
} else if !simulator.name.eq_ignore_ascii_case("hooper") {
eprintln!(
"logged in to region '{}' instead of 'Hooper', proceeding with tests",
simulator.name
);
}
assert!(
!simulator.name.is_empty(),
"OpenSim region handshake returned an empty simulator name"
);
let current_region = simulator.name.clone();
let region = block_on(
client
.grid()
.get_grid_region_with_string_grid_layer_type_cancellation_token(
"Hippo Hollow".into(),
GridLayerType::Terrain,
None,
),
)
.expect("GridManager GetGridRegionAsync");
if let Some(Some(region)) = region {
assert_eq!(region.name.to_lowercase(), "hippo hollow");
} else {
eprintln!("Grid region 'Hippo Hollow' not found; skipping assertion");
}
let region = runtime
.block_on(
client
.grid()
.get_grid_region_with_string_grid_layer_type_cancellation_token(
current_region.clone(),
GridLayerType::Terrain,
None,
),
)
.expect("GridManager GetGridRegionAsync");
let region = region
.flatten()
.expect("OpenSim grid search did not return the current region");
assert!(region.name.eq_ignore_ascii_case(&current_region));
network.logout_with_method().expect("NetworkManager Logout");
let _ = client.dispose_with_method();
logout_live_grid(&runtime, &mut client);
}
// parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.DetectObjects::test 8b72f1ca30feda2fad0812faf3a2f83d691d3cca9268054b3cc3bf89ad786e87 ignored-live
@@ -413,6 +389,7 @@ fn get_grid_region_live() {
ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn detect_objects_live() {
let runtime = LiveTestRuntime::new();
let mut client = GridClient::new().expect("GridClient constructor");
client.settings().timing().login_timeout = 30 * 1_000;
client.self_().movement.set_fly(true);
@@ -425,48 +402,20 @@ fn detect_objects_live() {
Arc::new(move |_event| callback_detected.store(true, Ordering::SeqCst)),
)
.expect("register ObjectUpdate callback");
let (first, last, password, login_url) = live_credentials();
let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
.expect("NetworkManager StartLocation");
let mut login = network
.default_login_params(
first,
last,
password,
"Unit Test Framework".into(),
"admin@radegast.life".into(),
)
.expect("NetworkManager DefaultLoginParams");
login.start = start;
login.uri = login_url;
let login_network = Arc::clone(&network);
let logged_in = block_on_timeout(
async move {
login_network
.login_with_login_params_cancellation_token(login, None)
.await
},
Duration::from_secs(45),
)
.expect("NetworkManager LoginAsync");
assert!(
logged_in,
"client failed to log in: {}",
network.login_message()
login_live_grid(
&runtime,
&mut client,
"Unit Test Framework",
"admin@radegast.life",
);
assert!(network.connected(), "client is not connected to the grid");
thread::sleep(Duration::from_secs(1));
let simulator = network
.current_sim()
.expect("CurrentSim is null after successful login");
if simulator.name.is_empty() {
eprintln!("CurrentSim.Name is empty, but proceeding with tests");
} else if !simulator.name.eq_ignore_ascii_case("hooper") {
eprintln!(
"logged in to region '{}' instead of 'Hooper', but proceeding with tests",
simulator.name
);
}
assert!(
!simulator.name.is_empty(),
"OpenSim region handshake returned an empty simulator name"
);
let started = Instant::now();
while !detected.load(Ordering::SeqCst) {
@@ -481,8 +430,7 @@ fn detect_objects_live() {
"successfully detected objects"
);
network.logout_with_method().expect("NetworkManager Logout");
let _ = client.dispose_with_method();
logout_live_grid(&runtime, &mut client);
}
// parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.CapsQueue::test cd5af4e7d69df3e938540711877d167fe28efb9af8af7518a62b8f406e46cc18 ignored-live
@@ -492,6 +440,7 @@ fn detect_objects_live() {
ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn caps_queue_live() {
let runtime = LiveTestRuntime::new();
let mut client = GridClient::new().expect("GridClient constructor");
client.settings().timing().login_timeout = 30 * 1_000;
client.self_().movement.set_fly(true);
@@ -504,48 +453,20 @@ fn caps_queue_live() {
Arc::new(move |_event| callback_detected.store(true, Ordering::SeqCst)),
)
.expect("register ObjectUpdate callback");
let (first, last, password, login_url) = live_credentials();
let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
.expect("NetworkManager StartLocation");
let mut login = network
.default_login_params(
first,
last,
password,
"Unit Test Framework".into(),
"admin@radegast.life".into(),
)
.expect("NetworkManager DefaultLoginParams");
login.start = start;
login.uri = login_url;
let login_network = Arc::clone(&network);
let logged_in = block_on_timeout(
async move {
login_network
.login_with_login_params_cancellation_token(login, None)
.await
},
Duration::from_secs(45),
)
.expect("NetworkManager LoginAsync");
assert!(
logged_in,
"client failed to log in: {}",
network.login_message()
login_live_grid(
&runtime,
&mut client,
"Unit Test Framework",
"admin@radegast.life",
);
assert!(network.connected(), "client is not connected to the grid");
thread::sleep(Duration::from_secs(1));
let simulator = network
.current_sim()
.expect("CurrentSim is null after successful login");
if simulator.name.is_empty() {
eprintln!("CurrentSim.Name is empty, but proceeding with tests");
} else if !simulator.name.eq_ignore_ascii_case("hooper") {
eprintln!(
"logged in to region '{}' instead of 'Hooper', but proceeding with tests",
simulator.name
);
}
assert!(
!simulator.name.is_empty(),
"OpenSim region handshake returned an empty simulator name"
);
let already_running = network
.current_sim()
@@ -568,6 +489,5 @@ fn caps_queue_live() {
"CAPS Event Queue is not running and failed to start"
);
network.logout_with_method().expect("NetworkManager Logout");
let _ = client.dispose_with_method();
logout_live_grid(&runtime, &mut client);
}

View File

@@ -302,7 +302,7 @@ fn get_region_restart_schedule_weekly_parses_days_and_time() {
schedule.days(),
RegionRestartDays(RegionRestartDays::TUESDAY.0 | RegionRestartDays::THURSDAY.0)
);
assert_eq!(schedule.time(), Duration::from_secs(2 * 60 * 60));
assert_eq!(schedule.time(), Duration::from_hours(2));
}
// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.GetRegionRestartScheduleAsync_DailySchedule_ReturnsAllDays::test cd966b6e3ba470c91ac958bf6c71aeb34f4dab060ad5334cb6060e1954b5c6d0 translated
@@ -322,7 +322,7 @@ fn get_region_restart_schedule_daily_returns_all_days() {
.expect("restart schedule");
assert!(schedule.is_daily());
assert_eq!(schedule.days(), RegionRestartDays::ALL);
assert_eq!(schedule.time(), Duration::from_secs(60 * 60));
assert_eq!(schedule.time(), Duration::from_hours(1));
}
// parity-case: LibreMetaverse.Tests/RegionScheduleTests.cs::RegionScheduleTests.GetRegionRestartScheduleAsync_NoRestartConfigured_ReturnsNull::test 59a4b050a79659ccaf0fff13b750d634d803ed7b9e4940cbad676c1bdc04a5fc translated
@@ -377,10 +377,9 @@ fn set_region_restart_schedule_weekly_posts_uppercase_days_and_seconds() {
);
assert!(
block_on(
client.estate().set_region_restart_schedule(
schedule(false, days, Duration::from_secs(5400)),
None
)
client
.estate()
.set_region_restart_schedule(schedule(false, days, Duration::from_mins(90)), None)
)
.unwrap()
);
@@ -401,11 +400,7 @@ fn set_region_restart_schedule_daily_omits_days() {
);
assert!(
block_on(client.estate().set_region_restart_schedule(
schedule(
true,
RegionRestartDays::NONE,
Duration::from_secs(3 * 60 * 60)
),
schedule(true, RegionRestartDays::NONE, Duration::from_hours(3)),
None
))
.unwrap()

View File

@@ -1,58 +1,39 @@
// Exact Rust translation of LibreMetaverse.Tests/EstateToolsTests.cs.
// Source SHA-256: 2f1867304e21e72d20fbeed98e7705528797947eb3115f84f80aa6adc2f3f88e
use libremetaverse::{GridClient, NetworkManager};
use libremetaverse_compat_tests::{block_on, live_grid_credentials};
use libremetaverse::GridClient;
use libremetaverse_compat_tests::{LiveTestRuntime, login_live_grid, logout_live_grid};
use libremetaverse_types::UUID;
use std::sync::mpsc;
use std::time::Duration;
struct LiveEstate {
client: GridClient,
runtime: LiveTestRuntime,
}
impl LiveEstate {
fn login() -> Self {
let runtime = LiveTestRuntime::new();
let mut client = GridClient::new().expect("GridClient constructor");
client.settings().timing().login_timeout = 30_000;
let network = client.network();
let (first, last, password, login_url) = live_grid_credentials();
let start = NetworkManager::start_location("Hooper".into(), 179, 18, 32)
.expect("NetworkManager StartLocation");
let mut login = network
.default_login_params(
first,
last,
password,
"Unit Test Framework".into(),
"admin@radegast.life".into(),
)
.expect("NetworkManager DefaultLoginParams");
login.start = start;
login.uri = login_url;
let logged_in = block_on(network.login_with_login_params_cancellation_token(login, None))
.expect("NetworkManager LoginAsync");
assert!(
logged_in,
"client failed to login: {}",
network.login_message()
let network = login_live_grid(
&runtime,
&mut client,
"Unit Test Framework",
"admin@radegast.life",
);
assert!(network.connected(), "client is not connected to the grid");
let current = network.current_sim().expect("CurrentSim after login");
if !current.name.is_empty() && !current.name.eq_ignore_ascii_case("Hooper") {
eprintln!(
"logged in to {} instead of Hooper; covenant request remains read-only",
current.name
);
}
Self { client }
assert!(
!current.name.is_empty(),
"OpenSim region handshake returned an empty simulator name"
);
Self { client, runtime }
}
}
impl Drop for LiveEstate {
fn drop(&mut self) {
let _ = self.client.network().logout_with_method();
let _ = self.client.dispose_with_method();
logout_live_grid(&self.runtime, &mut self.client);
}
}
@@ -72,10 +53,9 @@ fn request_covenant() {
estate
.request_covenant()
.expect("EstateTools RequestCovenant");
let Ok((name, covenant)) = receiver.recv_timeout(Duration::from_secs(10)) else {
eprintln!("timeout waiting for estate covenant reply after 10 seconds");
return;
};
let (name, covenant) = receiver
.recv_timeout(Duration::from_secs(10))
.expect("timeout waiting for estate covenant reply after 10 seconds");
assert!(!name.is_empty());
assert_ne!(covenant, UUID::zero());
}