Translate HTTP and network tests
This commit is contained in:
30
tests/compat/build.rs
Normal file
30
tests/compat/build.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
600
tests/compat/tests/network_semantics.rs
Normal file
600
tests/compat/tests/network_semantics.rs
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user