Translate HTTP and network tests

This commit is contained in:
2026-08-08 18:30:51 +02:00
parent de6e611ef0
commit 6d4d9adaf4
13 changed files with 1793 additions and 1051 deletions

30
tests/compat/build.rs Normal file
View File

@@ -0,0 +1,30 @@
use std::path::Path;
const KEYS: [&str; 3] = ["GRID_USER", "GRID_PASSWORD", "GRID_LOGIN_URL"];
fn dotenv_has(path: &Path, key: &str) -> bool {
std::fs::read_to_string(path).is_ok_and(|contents| {
contents.lines().any(|line| {
let line = line.trim().strip_prefix("export ").unwrap_or(line.trim());
line.split_once('=').is_some_and(|(name, value)| {
name.trim() == key && !value.trim().trim_matches(['\'', '"']).is_empty()
})
})
})
}
fn main() {
let dotenv = Path::new(&std::env::var("CARGO_MANIFEST_DIR").expect("manifest directory"))
.join("../..")
.join(".env");
println!("cargo:rustc-check-cfg=cfg(live_grid_credentials)");
println!("cargo:rerun-if-changed={}", dotenv.display());
for key in KEYS {
println!("cargo:rerun-if-env-changed={key}");
}
if KEYS.iter().all(|key| {
std::env::var(key).is_ok_and(|value| !value.trim().is_empty()) || dotenv_has(&dotenv, key)
}) {
println!("cargo:rustc-cfg=live_grid_credentials");
}
}

View File

@@ -175,9 +175,6 @@
// pending-parity-case: LibreMetaverse.Tests/CurrentOutfitFolderTests.cs::CompositeCurrentOutfitPolicyTests.ReportItemChangeAsync_WithNullLists_DoesNotThrowFromComposite::test c512c54f78c46fb4774d9b5c9c1057627592e1c25267811cddf3d687d3d1ec4d
// pending-parity-case: LibreMetaverse.Tests/CurrentOutfitFolderTests.cs::CompositeCurrentOutfitPolicyTests.AddRemovePolicy_ConcurrentAccess_DoesNotThrow::test a73a01065867d90305f491e41d9ea61bc27b80c026672ed920368877bfb0fe14
// pending-parity-case: LibreMetaverse.Tests/CurrentOutfitFolderTests.cs::CompositeCurrentOutfitPolicyTests.CanAttach_WhileModifyingPolicies_DoesNotThrow::test 697999feeb776fae9a47cb9678c0ae0234d3b3831ce40c0e6f50fe622a5cc2b7
// pending-parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_SingleDownload_CompletesSuccessfully::test f8feb54d38f79624ddf1d368e0599259ff9d474c08ea0249a8f244460a1f533f
// pending-parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_DeduplicatesRequests_SameUri_OneHttpCall::test dc3ffee1f6171833a8364750cfac2c6d3bfa812b404129e0425066ea212f2337
// pending-parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_Cancellation_PropagatesToTask::test 9eb3791b1def609d6f45be0fff8ae67ea04332045c0c419c6c2e7d118aeb6271
// pending-parity-case: LibreMetaverse.Tests/EnvironmentManagerTests.cs::EnvironmentManagerTests.GetParcelEnvironment_UsesParcelidQueryParameter::test 5c2f2b0fa2a86955975c621b2415a92ff3da283215e669956e14bf5ee2a4c1f4
// pending-parity-case: LibreMetaverse.Tests/EnvironmentManagerTests.cs::EnvironmentManagerTests.SetParcelEnvironment_SendsParcelidInQueryStringNotBody::test f633e2d78e4c6a0fff42917ccfe8de68e4f6133a11e7eef7f9b757c3d1622f0f
// pending-parity-case: LibreMetaverse.Tests/EnvironmentManagerTests.cs::EnvironmentManagerTests.SetRegionEnvironment_OmitsQueryStringEntirely::test bade6e06765506ad7e500ac3e5c9f168605ce25fef86647819d285a68b39f109
@@ -219,8 +216,6 @@
// pending-parity-case: LibreMetaverse.Tests/GltfDocumentTests.cs::GltfDocumentTests.Load_AutoDetectsGlbVsJson::test 6d07930be2302e504ef9ece3fef10f47b855d33ae6c8bff152e82df0beb2a474
// pending-parity-case: LibreMetaverse.Tests/GltfDocumentTests.cs::GltfDocumentTests.ParseNode_TrsDefaults::test 0f0fbe116d20276c2aee917b940bded9de277ed0b3e3b659844c5b3532755259
// pending-parity-case: LibreMetaverse.Tests/GltfDocumentTests.cs::GltfDocumentTests.ParseNode_ExplicitMatrix::test 197ba2ee3a66cbb7d927acea13ce9dc84e62af8a500d939957749039118755c8
// pending-parity-case: LibreMetaverse.Tests/GridClientTests.cs::GridClientTests.GetGridRegion::test b0b72fc63fe829984167a479b763784c7daae8691399f3f3d2feb824090ad59f
// pending-parity-case: LibreMetaverse.Tests/HttpCapsClientNonHttpLocationTests.cs::HttpCapsClientNonHttpLocationTests.PutAsync_ResponseWithNonHttpLocationHeader_DoesNotThrow::test 9dbcf4023212af6f2294fa048068fe2f4d9e7e3f976d8eefb1dacbc9f6899aa8
// pending-parity-case: LibreMetaverse.Tests/InventoryAISClientTests.cs::InventoryAISClientTests.ParseLinksFromEmbedded_ObjectAsset_ResultsInAttachmentInventoryTypeAndParsesFields::test 3ae365ed34f12d036dcd44c94f8568d163cfeec2a58b4975975b77b418770131
// pending-parity-case: LibreMetaverse.Tests/InventoryAISClientTests.cs::InventoryAISClientTests.ParseItemsFromEmbedded_ItemsKey_ParsesConcreteItems::test d9e7c64106639071e1ec6ada7346bfa050a31183becaac79efefcd4755fe2855
// pending-parity-case: LibreMetaverse.Tests/InventoryAISClientTests.cs::InventoryAISClientTests.ParseLinksFromEmbedded_MeshAsset_TreatedAsAttachment::test f807110d577c654128dd8d42e618c610c260c4f3367cf2795776564409f876a2
@@ -344,8 +339,6 @@
// pending-parity-case: LibreMetaverse.Tests/MisclassifiedLinkRegressionTests.cs::MisclassifiedLinkRegressionTests.InventoryStore_Indexer_ReturnsAttachment_WhenTargetIsAttachment::test f58debff8f66b7b417215eb6879f96935647b66cc619fdb00e6815c9ce5e7c67
// pending-parity-case: LibreMetaverse.Tests/MisclassifiedLinkRegressionTests.cs::MisclassifiedLinkRegressionTests.InventoryStore_Indexer_ReturnsObject_WhenTargetIsObject::test b8137c994019103977b0b85d8c148e12ae8f9051334ca487db7c88bcbe707d40
// pending-parity-case: LibreMetaverse.Tests/MisclassifiedLinkRegressionTests.cs::MisclassifiedLinkRegressionTests.InventoryStore_NotContains_WhenTargetAbsent::test 6900e220c33ec135d02c55e047c7052a095d5dd9d71da4a857ae70bc151c472b
// pending-parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.DetectObjects::test 8b72f1ca30feda2fad0812faf3a2f83d691d3cca9268054b3cc3bf89ad786e87
// pending-parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.CapsQueue::test cd5af4e7d69df3e938540711877d167fe28efb9af8af7518a62b8f406e46cc18
// pending-parity-case: LibreMetaverse.Tests/OarFileTerrainTests.cs::OarFileTerrainTests.LoadTerrain_StandardRegion_Loads256x256::test c4e3a994baf06b0106a6614e9c3efbcc27e45ff1725f5470d2db6ded1fff6014
// pending-parity-case: LibreMetaverse.Tests/OarFileTerrainTests.cs::OarFileTerrainTests.LoadTerrain_Varregion_Loads512x512::test 4820338f10acbb4987e26992d7a7957be58ab6bc548674bf18943ef312962894
// pending-parity-case: LibreMetaverse.Tests/OarFileTerrainTests.cs::OarFileTerrainTests.LoadTerrain_WrongByteCount_FailsGracefully::test ff6fb4feb2773c93397d412d0ecf6aef1f3636e823dabb729a7a6cbe94d1513f

View File

@@ -0,0 +1,600 @@
#![allow(clippy::needless_pass_by_value)]
// Exact fixture provenance at LibreMetaverse 2aa70bb68513b39795da5d13c88f31b86e85a3ba:
// DownloadManagerTests.cs 07f31d4db35335602bc8d0a70ef440d38ce7a59f2c7765a184bc33f03b6c6015
// GridClientTests.cs adfee7ccb929270d5e88dccdb354c686bd008a167a3a1028b536664813c6d1
// HttpCapsClientNonHttpLocationTests.cs 92cac055067808883132d7415dc87d17e6ad05a62b85cd454e552879f568016f
// NetworkTests.cs 325586ce3cf9af1dbcb8dd36fc2be5647e524f4baeeb260520e44e66172142dc
use libremetaverse::http::DownloadManager;
use libremetaverse::packets::PacketType;
use libremetaverse::{GridClient, GridLayerType, HttpCapsClient, NetworkManager};
use libremetaverse_compat_tests::block_on;
use libremetaverse_types::compat::{
CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, Uri,
};
use std::collections::BTreeMap;
use std::future::Future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::{Duration, Instant};
#[derive(Clone)]
struct FakeHandler {
requests: Arc<Mutex<Vec<HttpRequest>>>,
}
struct Delay {
state: Arc<Mutex<(bool, Option<Waker>)>>,
}
impl Delay {
fn new(duration: Duration) -> Self {
let state = Arc::new(Mutex::new((false, None::<Waker>)));
let worker_state = Arc::clone(&state);
thread::spawn(move || {
thread::sleep(duration);
let waker = {
let mut state = worker_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.0 = true;
state.1.take()
};
if let Some(waker) = waker {
waker.wake();
}
});
Self { state }
}
}
impl Future for Delay {
type Output = ();
fn poll(self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state.0 {
Poll::Ready(())
} else {
state.1 = Some(context.waker().clone());
Poll::Pending
}
}
}
impl FakeHandler {
fn new(data: &[u8], delay: Duration) -> (Self, HttpMessageHandler) {
let requests = Arc::new(Mutex::new(Vec::new()));
let recorded = Arc::clone(&requests);
let response = HttpResponse {
status_code: 200,
headers: BTreeMap::from([("Content-Length".into(), data.len().to_string())]),
content_type: None,
body: data.to_vec(),
};
let handler = HttpMessageHandler::new(move |request, _cancellation_token| {
let recorded = Arc::clone(&recorded);
let response = response.clone();
async move {
recorded
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(request);
if !delay.is_zero() {
Delay::new(delay).await;
}
response
}
});
(Self { requests }, handler)
}
fn send_count(&self) -> usize {
self.requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len()
}
fn only_request(&self) -> HttpRequest {
let requests = self
.requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(requests.len(), 1, "expected exactly one HTTP request");
requests[0].clone()
}
}
fn block_on_timeout<F>(future: F, timeout: Duration) -> F::Output
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let (sender, receiver) = mpsc::sync_channel(1);
let worker = thread::spawn(move || {
let _ = sender.send(block_on(future));
});
let output = receiver.recv_timeout(timeout).expect("operation timed out");
worker.join().expect("operation worker panicked");
output
}
async fn join<F1, F2>(first: F1, second: F2) -> (F1::Output, F2::Output)
where
F1: Future,
F2: Future,
{
let mut first = std::pin::pin!(first);
let mut second = std::pin::pin!(second);
let mut first_output = None;
let mut second_output = None;
std::future::poll_fn(|context| {
if first_output.is_none()
&& let Poll::Ready(output) = first.as_mut().poll(context)
{
first_output = Some(output);
}
if second_output.is_none()
&& let Poll::Ready(output) = second.as_mut().poll(context)
{
second_output = Some(output);
}
if first_output.is_some() && second_output.is_some() {
Poll::Ready((
first_output.take().expect("first future output"),
second_output.take().expect("second future output"),
))
} else {
Poll::Pending
}
})
.await
}
fn credential(name: &str) -> String {
if let Ok(value) = std::env::var(name)
&& !value.trim().is_empty()
{
return value;
}
let dotenv = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join(".env");
let contents = std::fs::read_to_string(dotenv).expect("read workspace .env");
contents
.lines()
.find_map(|line| {
let line = line.trim().strip_prefix("export ").unwrap_or(line.trim());
let (key, value) = line.split_once('=')?;
(key.trim() == name).then(|| value.trim().trim_matches(['\'', '"']).to_owned())
})
.filter(|value| !value.is_empty())
.unwrap_or_else(|| panic!("live test requires {name}"))
}
fn live_credentials() -> (String, String, String, String) {
let full_name = credential("GRID_USER");
let password = credential("GRID_PASSWORD");
let login_url = credential("GRID_LOGIN_URL");
let mut names = full_name.split(' ');
let first = names.next().expect("live first name").to_owned();
let last = names.next().expect("live last name").to_owned();
(first, last, password, login_url)
}
// parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_SingleDownload_CompletesSuccessfully::test f8feb54d38f79624ddf1d368e0599259ff9d474c08ea0249a8f244460a1f533f translated
#[test]
fn queue_download_single_completes_successfully() {
let bytes = b"hello world".to_vec();
let (handler, message_handler) = FakeHandler::new(&bytes, Duration::ZERO);
let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
let mut client = GridClient::new().expect("GridClient constructor");
client.set_http_caps_client(http_caps);
let downloads = Arc::new(DownloadManager::new(client).expect("DownloadManager constructor"));
let worker_downloads = Arc::clone(&downloads);
let result = block_on_timeout(
async move {
worker_downloads
.queue_download_with_uri_string_i_progress_cancellation_token_int32(
Uri("http://example.test/one".into()),
None,
None,
Some(CancellationToken::default()),
Some(1),
)
.await
},
Duration::from_secs(5),
);
let dispose_result = downloads.dispose();
let (response, data) = result.expect("QueueDownloadAsync");
assert_eq!(data, bytes);
assert!(response.is_success_status_code());
assert_eq!(response.headers["Content-Length"], bytes.len().to_string());
let request = handler.only_request();
assert_eq!(request.method, "GET");
assert_eq!(request.uri, Uri("http://example.test/one".into()));
dispose_result.expect("DownloadManager Dispose");
}
// parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_DeduplicatesRequests_SameUri_OneHttpCall::test dc3ffee1f6171833a8364750cfac2c6d3bfa812b404129e0425066ea212f2337 translated
#[test]
fn queue_download_deduplicates_same_uri() {
let bytes = b"duplicate payload".to_vec();
let (handler, message_handler) = FakeHandler::new(&bytes, Duration::from_millis(200));
let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
let mut client = GridClient::new().expect("GridClient constructor");
client.set_http_caps_client(http_caps);
let downloads = Arc::new(DownloadManager::new(client).expect("DownloadManager constructor"));
let uri = Uri("http://example.test/dup".into());
let worker_downloads = Arc::clone(&downloads);
let (first_result, second_result) = block_on_timeout(
async move {
let first = worker_downloads
.queue_download_with_uri_string_i_progress_cancellation_token_int32(
uri.clone(),
None,
None,
Some(CancellationToken::default()),
Some(1),
);
let second = worker_downloads
.queue_download_with_uri_string_i_progress_cancellation_token_int32(
uri,
None,
None,
Some(CancellationToken::default()),
Some(1),
);
join(first, second).await
},
Duration::from_secs(5),
);
let dispose_result = downloads.dispose();
let (_, first_data) = first_result.expect("first download");
let (_, second_data) = second_result.expect("second download");
assert_eq!(first_data, bytes);
assert_eq!(second_data, bytes);
assert_eq!(handler.send_count(), 1);
let request = handler.only_request();
assert_eq!(request.method, "GET");
assert_eq!(request.uri, Uri("http://example.test/dup".into()));
dispose_result.expect("DownloadManager Dispose");
}
// parity-case: LibreMetaverse.Tests/DownloadManagerTests.cs::DownloadManagerTests.QueueDownloadAsync_Cancellation_PropagatesToTask::test 9eb3791b1def609d6f45be0fff8ae67ea04332045c0c419c6c2e7d118aeb6271 translated
#[test]
fn queue_download_cancellation_propagates() {
let bytes = b"will cancel".to_vec();
let (handler, message_handler) = FakeHandler::new(&bytes, Duration::ZERO);
let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
let mut client = GridClient::new().expect("GridClient constructor");
client.set_http_caps_client(http_caps);
let downloads = DownloadManager::new(client).expect("DownloadManager constructor");
let cancellation = CancellationTokenSource::new();
cancellation.cancel();
let result = block_on(
downloads.queue_download_with_uri_string_i_progress_cancellation_token_int32(
Uri("http://example.test/cancel".into()),
None,
None,
Some(cancellation.token()),
Some(1),
),
);
let dispose_result = downloads.dispose();
assert!(
matches!(&result, Err(libremetaverse::Error::Cancelled)),
"expected Cancelled, got {result:?}"
);
assert_eq!(handler.send_count(), 0);
dispose_result.expect("DownloadManager Dispose");
}
// parity-case: LibreMetaverse.Tests/HttpCapsClientNonHttpLocationTests.cs::HttpCapsClientNonHttpLocationTests.PutAsync_ResponseWithNonHttpLocationHeader_DoesNotThrow::test 9dbcf4023212af6f2294fa048068fe2f4d9e7e3f976d8eefb1dacbc9f6899aa8 translated
#[test]
fn put_response_with_non_http_location_does_not_throw() {
const SLCAPS: &str = "slcaps://11111111-1111-1111-1111-111111111111/category/x";
let body = b"<llsd><map/></llsd>";
let response = HttpResponse {
status_code: 201,
headers: BTreeMap::from([
("Location".into(), SLCAPS.into()),
("Content-Location".into(), SLCAPS.into()),
("Content-Length".into(), body.len().to_string()),
]),
content_type: Some("application/llsd+xml".into()),
body: body.to_vec(),
};
let requests = Arc::new(Mutex::new(Vec::new()));
let recorded = Arc::clone(&requests);
let message_handler = HttpMessageHandler::new(move |request, _cancellation_token| {
let recorded = Arc::clone(&recorded);
let response = response.clone();
async move {
recorded
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(request);
response
}
});
let mut client = GridClient::new().expect("GridClient constructor");
let http_caps = HttpCapsClient::new(message_handler).expect("HttpCapsClient constructor");
client.set_http_caps_client(http_caps);
let (reply, reply_body) = block_on(
client
.http_caps_client()
.put_with_uri_string_bytes_cancellation_token_i_progress(
Uri("http://127.0.0.1/".into()),
"application/llsd+xml".into(),
b"test".to_vec(),
CancellationToken::default(),
None,
),
)
.expect("HttpCapsClient PutAsync");
assert_eq!(reply.status_code, 201);
assert_eq!(reply.headers["Location"], SLCAPS);
assert_eq!(reply.headers["Content-Location"], SLCAPS);
assert_eq!(reply.content_type.as_deref(), Some("application/llsd+xml"));
assert_eq!(reply_body, body);
let request = {
let requests = requests
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
assert_eq!(requests.len(), 1, "expected exactly one HTTP request");
requests[0].clone()
};
assert_eq!(request.method, "PUT");
assert_eq!(request.uri, Uri("http://127.0.0.1/".into()));
assert_eq!(
request.content_type.as_deref(),
Some("application/llsd+xml")
);
assert_eq!(request.body, b"test");
}
// parity-case: LibreMetaverse.Tests/GridClientTests.cs::GridClientTests.GetGridRegion::test b0b72fc63fe829984167a479b763784c7daae8691399f3f3d2feb824090ad59f ignored-live
#[test]
#[cfg_attr(
not(live_grid_credentials),
ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn get_grid_region_live() {
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()
);
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
);
}
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");
}
network.logout_with_method().expect("NetworkManager Logout");
let _ = client.dispose_with_method();
}
// parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.DetectObjects::test 8b72f1ca30feda2fad0812faf3a2f83d691d3cca9268054b3cc3bf89ad786e87 ignored-live
#[test]
#[cfg_attr(
not(live_grid_credentials),
ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn detect_objects_live() {
let mut client = GridClient::new().expect("GridClient constructor");
client.settings().timing().login_timeout = 30 * 1_000;
client.self_().movement.set_fly(true);
let detected = Arc::new(AtomicBool::new(false));
let callback_detected = Arc::clone(&detected);
let network = Arc::new(client.network());
network
.register_callback_with_packet_type_event_handler(
PacketType::ObjectUpdate,
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()
);
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
);
}
let started = Instant::now();
while !detected.load(Ordering::SeqCst) {
assert!(
started.elapsed() <= Duration::from_secs(20),
"timeout waiting for an ObjectUpdate packet after 20 seconds"
);
thread::sleep(Duration::from_millis(100));
}
assert!(
detected.load(Ordering::SeqCst),
"successfully detected objects"
);
network.logout_with_method().expect("NetworkManager Logout");
let _ = client.dispose_with_method();
}
// parity-case: LibreMetaverse.Tests/NetworkTests.cs::NetworkTests.CapsQueue::test cd5af4e7d69df3e938540711877d167fe28efb9af8af7518a62b8f406e46cc18 ignored-live
#[test]
#[cfg_attr(
not(live_grid_credentials),
ignore = "requires GRID_USER, GRID_PASSWORD, and GRID_LOGIN_URL in the environment or workspace .env"
)]
fn caps_queue_live() {
let mut client = GridClient::new().expect("GridClient constructor");
client.settings().timing().login_timeout = 30 * 1_000;
client.self_().movement.set_fly(true);
let detected = Arc::new(AtomicBool::new(false));
let callback_detected = Arc::clone(&detected);
let network = Arc::new(client.network());
network
.register_callback_with_packet_type_event_handler(
PacketType::ObjectUpdate,
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()
);
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
);
}
let already_running = network
.current_sim()
.and_then(|simulator| simulator.caps)
.is_some_and(|caps| caps.is_event_queue_running());
let event_queue_running = if already_running {
true
} else {
let (sender, receiver) = mpsc::sync_channel(1);
let _subscription = network.subscribe_event_queue_running(Arc::new(move |_event| {
let _ = sender.send(());
}));
receiver
.recv_timeout(Duration::from_secs(10))
.expect("timeout waiting for event queue to start");
true
};
assert!(
event_queue_running,
"CAPS Event Queue is not running and failed to start"
);
network.logout_with_method().expect("NetworkManager Logout");
let _ = client.dispose_with_method();
}