Implement native VivoxTest validation (#95)
Some checks failed
Native code generation / deterministic (push) Failing after 2m19s
Imaging and meshing gate / native (push) Failing after 4m24s
JPEG 2000 feature / linux (push) Successful in 2m50s
Native Rust workspace compile / compile (push) Failing after 15m31s
Skia feature / linux (push) Successful in 31m51s

This commit is contained in:
2026-08-11 14:09:51 +00:00
parent 21f85a1b58
commit da4afec708
16 changed files with 2462 additions and 21 deletions

View File

@@ -20,6 +20,7 @@ libremetaverse-types = { path = "../../crates/libremetaverse-types" }
libremetaverse-utilities = { path = "../../crates/libremetaverse-utilities" }
libremetaverse-voice-vivox = { path = "../../crates/libremetaverse-voice-vivox" }
libremetaverse-voice-webrtc = { path = "../../crates/libremetaverse-voice-webrtc" }
tokio = { version = "1.47.1", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] }
[lints]
workspace = true

View File

@@ -0,0 +1,117 @@
use libremetaverse_voice_vivox::VivoxControlClient;
use std::net::Ipv4Addr;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpListener;
const EXPECTED_ACTIONS: [&str; 10] = [
"Aux.GetCaptureDevices.1",
"Aux.GetRenderDevices.1",
"Connector.Create.1",
"Account.Login.1",
"Session.Create.1",
"Session.Connect.1",
"Session.SetParticipantVolumeForMe.1",
"Session.Terminate.1",
"Account.Logout.1",
"Connector.InitiateShutdown.1",
];
fn response(id: &str, action: &str, result: &str) -> String {
format!(
"<Response requestId=\"{id}\" action=\"{action}\"><ReturnCode>0</ReturnCode><Results><StatusCode>0</StatusCode><StatusString>OK</StatusString>{result}</Results></Response>\n\n\n"
)
}
#[tokio::test]
async fn native_vivox_protocol_preserves_control_order_events_and_teardown() {
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await.unwrap();
let endpoint = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let (reader, mut writer) = stream.into_split();
let mut reader = BufReader::new(reader);
let mut actions = Vec::new();
loop {
let mut line = String::new();
assert_ne!(reader.read_line(&mut line).await.unwrap(), 0);
let line = line.trim();
if line.is_empty() {
continue;
}
let id = line
.split("requestId=\"")
.nth(1)
.and_then(|value| value.split('"').next())
.unwrap();
let action = line
.split("action=\"")
.nth(1)
.and_then(|value| value.split('"').next())
.unwrap();
actions.push(action.to_owned());
let result = match action {
"Aux.GetCaptureDevices.1" => {
"<CaptureDevices><CaptureDevice><Device>Mic</Device></CaptureDevice></CaptureDevices><CurrentCaptureDevice><Device>Mic</Device></CurrentCaptureDevice>"
}
"Aux.GetRenderDevices.1" => {
"<RenderDevices><RenderDevice><Device>Speakers</Device></RenderDevice></RenderDevices><CurrentRenderDevice><Device>Speakers</Device></CurrentRenderDevice>"
}
"Connector.Create.1" => "<ConnectorHandle>connector-secret</ConnectorHandle>",
"Account.Login.1" => "<AccountHandle>account-secret</AccountHandle>",
"Session.Create.1" => "<SessionHandle>session-secret</SessionHandle>",
"Session.Connect.1"
| "Session.SetParticipantVolumeForMe.1"
| "Session.Terminate.1"
| "Account.Logout.1"
| "Connector.InitiateShutdown.1" => "",
_ => panic!("unexpected Vivox action {action}"),
};
writer
.write_all(response(id, action, result).as_bytes())
.await
.unwrap();
if action == "Session.Connect.1" {
writer.write_all(b"<Event type=\"ParticipantPropertiesEvent\"><SessionHandle>session-secret</SessionHandle><ParticipantURI>sip:participant-token@example.test</ParticipantURI><DisplayName>Alice Resident</DisplayName><IsSpeaking>true</IsSpeaking><Volume>0</Volume><Energy>0.25</Energy></Event>\n\n\n").await.unwrap();
}
if action == "Connector.InitiateShutdown.1" {
writer.shutdown().await.unwrap();
return actions;
}
}
});
let mut client = VivoxControlClient::connect(endpoint, Duration::from_secs(2))
.await
.unwrap();
assert_eq!(client.capture_devices().await.unwrap().available, ["Mic"]);
assert_eq!(
client.render_devices().await.unwrap().available,
["Speakers"]
);
client
.create_connector("https://www.voice.example.test/api2/")
.await
.unwrap();
client.login("voice-user", "voice-password").await.unwrap();
let session = client
.create_session("sip:channel-token@example.test", "Parity Region", None)
.await
.unwrap();
client.connect_session(&session).await.unwrap();
let event = client.next_event().await.unwrap();
assert_eq!(event.kind, "ParticipantPropertiesEvent");
assert_eq!(event.display_name.as_deref(), Some("Alice Resident"));
assert_eq!(event.is_speaking, Some(true));
let participant = event.participant_uri.unwrap();
client
.set_participant_volume(&session, &participant, 0)
.await
.unwrap();
client.terminate_session(&session).await.unwrap();
client.logout().await.unwrap();
client.shutdown().await.unwrap();
client.shutdown().await.unwrap();
let actions = server.await.unwrap();
assert_eq!(actions, EXPECTED_ACTIONS);
}