use std::net::UdpSocket;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
#[tokio::test]
async fn test_osc_loopback_float() {
let receiver = UdpSocket::bind("127.0.0.1:0").expect("Failed to bind receiver socket");
let recv_addr = receiver.local_addr().unwrap();
receiver.set_read_timeout(Some(Duration::from_secs(2))).ok();
let sender = UdpSocket::bind("127.0.0.1:0").expect("Failed to bind sender socket");
let msg = rosc::OscMessage {
addr: "/test/value".to_string(),
args: vec![rosc::OscType::Float(0.75)],
};
let packet = rosc::OscPacket::Message(msg);
let encoded = rosc::encoder::encode(&packet).expect("Failed to encode OSC message");
sender
.send_to(&encoded, recv_addr)
.expect("Failed to send OSC message");
let mut buf = [0u8; 1024];
let (len, _) = receiver.recv_from(&mut buf).expect("Failed to receive");
let (_, decoded_packet) = rosc::decoder::decode_udp(&buf[..len]).expect("Failed to decode OSC");
if let rosc::OscPacket::Message(m) = decoded_packet {
assert_eq!(m.addr, "/test/value");
if let Some(rosc::OscType::Float(v)) = m.args.first() {
assert!(
(*v - 0.75).abs() < 0.001,
"Float value mismatch: {} != 0.75",
v
);
} else {
panic!("Expected Float argument, got: {:?}", m.args);
}
} else {
panic!("Expected Message, got Bundle");
}
}
#[tokio::test]
async fn test_osc_loopback_int() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
let recv_addr = receiver.local_addr().unwrap();
receiver.set_read_timeout(Some(Duration::from_secs(2))).ok();
let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
let msg = rosc::OscMessage {
addr: "/midi/cc/1".to_string(),
args: vec![rosc::OscType::Int(127)],
};
let packet = rosc::OscPacket::Message(msg);
let encoded = rosc::encoder::encode(&packet).unwrap();
sender.send_to(&encoded, recv_addr).unwrap();
let mut buf = [0u8; 1024];
let (len, _) = receiver.recv_from(&mut buf).expect("Failed to receive");
let (_, decoded_packet) = rosc::decoder::decode_udp(&buf[..len]).expect("Failed to decode");
if let rosc::OscPacket::Message(m) = decoded_packet {
if let Some(rosc::OscType::Int(v)) = m.args.first() {
assert_eq!(*v, 127, "Int value mismatch");
} else {
panic!("Expected Int argument");
}
} else {
panic!("Expected Message");
}
}
#[tokio::test]
async fn test_osc_loopback_string() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
let recv_addr = receiver.local_addr().unwrap();
receiver.set_read_timeout(Some(Duration::from_secs(2))).ok();
let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
let msg = rosc::OscMessage {
addr: "/status/message".to_string(),
args: vec![rosc::OscType::String("Hello CLASP!".to_string())],
};
let packet = rosc::OscPacket::Message(msg);
let encoded = rosc::encoder::encode(&packet).unwrap();
sender.send_to(&encoded, recv_addr).unwrap();
let mut buf = [0u8; 1024];
let (len, _) = receiver.recv_from(&mut buf).expect("Failed to receive");
let (_, decoded_packet) = rosc::decoder::decode_udp(&buf[..len]).expect("Failed to decode");
if let rosc::OscPacket::Message(m) = decoded_packet {
if let Some(rosc::OscType::String(s)) = m.args.first() {
assert_eq!(s, "Hello CLASP!", "String value mismatch");
} else {
panic!("Expected String argument");
}
} else {
panic!("Expected Message");
}
}
#[tokio::test]
async fn test_osc_loopback_multiple_args() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
let recv_addr = receiver.local_addr().unwrap();
receiver.set_read_timeout(Some(Duration::from_secs(2))).ok();
let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
let msg = rosc::OscMessage {
addr: "/color/rgb".to_string(),
args: vec![
rosc::OscType::Float(1.0), rosc::OscType::Float(0.5), rosc::OscType::Float(0.25), ],
};
let packet = rosc::OscPacket::Message(msg);
let encoded = rosc::encoder::encode(&packet).unwrap();
sender.send_to(&encoded, recv_addr).unwrap();
let mut buf = [0u8; 1024];
let (len, _) = receiver.recv_from(&mut buf).expect("Failed to receive");
let (_, decoded_packet) = rosc::decoder::decode_udp(&buf[..len]).expect("Failed to decode");
if let rosc::OscPacket::Message(m) = decoded_packet {
assert_eq!(m.args.len(), 3, "Expected 3 arguments");
if let (
Some(rosc::OscType::Float(r)),
Some(rosc::OscType::Float(g)),
Some(rosc::OscType::Float(b)),
) = (m.args.first(), m.args.get(1), m.args.get(2))
{
assert!((*r - 1.0).abs() < 0.001, "R value mismatch");
assert!((*g - 0.5).abs() < 0.001, "G value mismatch");
assert!((*b - 0.25).abs() < 0.001, "B value mismatch");
} else {
panic!("Expected 3 Float arguments");
}
} else {
panic!("Expected Message");
}
}
#[tokio::test]
async fn test_osc_loopback_bundle() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
let recv_addr = receiver.local_addr().unwrap();
receiver.set_read_timeout(Some(Duration::from_secs(2))).ok();
let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
let bundle = rosc::OscBundle {
timetag: rosc::OscTime {
seconds: 0,
fractional: 1,
},
content: vec![
rosc::OscPacket::Message(rosc::OscMessage {
addr: "/fader/1".to_string(),
args: vec![rosc::OscType::Float(0.5)],
}),
rosc::OscPacket::Message(rosc::OscMessage {
addr: "/fader/2".to_string(),
args: vec![rosc::OscType::Float(0.75)],
}),
],
};
let packet = rosc::OscPacket::Bundle(bundle);
let encoded = rosc::encoder::encode(&packet).unwrap();
sender.send_to(&encoded, recv_addr).unwrap();
let mut buf = [0u8; 1024];
let (len, _) = receiver.recv_from(&mut buf).expect("Failed to receive");
let (_, decoded_packet) = rosc::decoder::decode_udp(&buf[..len]).expect("Failed to decode");
if let rosc::OscPacket::Bundle(b) = decoded_packet {
assert_eq!(b.content.len(), 2, "Bundle should contain 2 messages");
} else {
panic!("Expected Bundle, got Message");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_osc_high_frequency() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
let recv_addr = receiver.local_addr().unwrap();
receiver
.set_read_timeout(Some(Duration::from_millis(100)))
.ok();
receiver.set_nonblocking(true).ok();
let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
let msg_count = 1000;
let received = Arc::new(AtomicU32::new(0));
let received_clone = received.clone();
let done = Arc::new(AtomicBool::new(false));
let done_clone = done.clone();
let recv_handle = thread::spawn(move || {
let mut buf = [0u8; 1024];
while !done_clone.load(Ordering::Relaxed) {
if let Ok((len, _)) = receiver.recv_from(&mut buf) {
if rosc::decoder::decode_udp(&buf[..len]).is_ok() {
received_clone.fetch_add(1, Ordering::Relaxed);
}
}
}
});
for i in 0..msg_count {
let msg = rosc::OscMessage {
addr: format!("/rapid/{}", i % 10),
args: vec![rosc::OscType::Float(i as f32 / msg_count as f32)],
};
let packet = rosc::OscPacket::Message(msg);
let encoded = rosc::encoder::encode(&packet).unwrap();
let _ = sender.send_to(&encoded, recv_addr);
}
thread::sleep(Duration::from_millis(200));
done.store(true, Ordering::Relaxed);
let _ = recv_handle.join();
let count = received.load(Ordering::Relaxed);
assert!(
count >= (msg_count as u32 * 9 / 10),
"Only {}/{} messages received",
count,
msg_count
);
}
#[tokio::test]
async fn test_artnet_packet_format() {
let mut art_dmx = vec![
b'A', b'r', b't', b'-', b'N', b'e', b't', 0x00, 0x00, 0x50, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, ];
for i in 0..512u16 {
art_dmx.push((i % 256) as u8);
}
assert_eq!(&art_dmx[0..8], b"Art-Net\0", "Invalid Art-Net header");
let opcode = u16::from_le_bytes([art_dmx[8], art_dmx[9]]);
assert_eq!(opcode, 0x5000, "Invalid opcode: 0x{:04X}", opcode);
let dmx_length = u16::from_be_bytes([art_dmx[16], art_dmx[17]]);
assert_eq!(dmx_length, 512, "Invalid DMX length: {}", dmx_length);
}
#[tokio::test]
async fn test_artnet_loopback() {
let server = UdpSocket::bind("127.0.0.1:0").unwrap();
let server_addr = server.local_addr().unwrap();
server.set_read_timeout(Some(Duration::from_secs(2))).ok();
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
client.set_read_timeout(Some(Duration::from_secs(2))).ok();
let mut art_dmx = vec![
b'A', b'r', b't', b'-', b'N', b'e', b't', 0x00, 0x00, 0x50, 0x00, 0x0E, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, ];
art_dmx.extend_from_slice(&[0, 36, 73, 109, 146, 182, 219, 255]);
client.send_to(&art_dmx, server_addr).unwrap();
let mut buf = [0u8; 1024];
let (len, from) = server.recv_from(&mut buf).expect("Server receive failed");
assert_eq!(
len,
art_dmx.len(),
"Size mismatch: {} vs {}",
len,
art_dmx.len()
);
assert_eq!(&buf[0..8], b"Art-Net\0", "Corrupted Art-Net header");
let seq = buf[12];
assert_eq!(seq, 1, "Wrong sequence: {}", seq);
server.send_to(&buf[..len], from).unwrap();
let (echo_len, _) = client.recv_from(&mut buf).expect("Echo receive failed");
assert_eq!(echo_len, len, "Echo length mismatch");
assert_eq!(buf[12], 1, "Echo sequence mismatch");
}
#[tokio::test]
async fn test_artnet_poll_reply() {
let server = UdpSocket::bind("127.0.0.1:0").unwrap();
let server_addr = server.local_addr().unwrap();
server.set_read_timeout(Some(Duration::from_secs(2))).ok();
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
client.set_read_timeout(Some(Duration::from_secs(2))).ok();
let art_poll = [
b'A', b'r', b't', b'-', b'N', b'e', b't', 0x00, 0x00, 0x20, 0x00, 0x0E, 0x00, 0x00, ];
client.send_to(&art_poll, server_addr).unwrap();
let mut buf = [0u8; 1024];
let (len, from) = server.recv_from(&mut buf).expect("Poll receive failed");
assert!(len >= 14, "ArtPoll too short");
assert_eq!(&buf[0..8], b"Art-Net\0", "Invalid ArtPoll header");
let opcode = u16::from_le_bytes([buf[8], buf[9]]);
assert_eq!(opcode, 0x2000, "Wrong opcode: 0x{:04X}", opcode);
let mut reply = vec![
b'A', b'r', b't', b'-', b'N', b'e', b't', 0x00, 0x00,
0x21, ];
reply.extend_from_slice(&[0u8; 229]);
server.send_to(&reply, from).unwrap();
let (_reply_len, _) = client.recv_from(&mut buf).expect("Reply receive failed");
let reply_opcode = u16::from_le_bytes([buf[8], buf[9]]);
assert_eq!(
reply_opcode, 0x2100,
"Wrong reply opcode: 0x{:04X}",
reply_opcode
);
}
#[tokio::test]
async fn test_artnet_multiple_universes() {
let receiver = UdpSocket::bind("127.0.0.1:0").unwrap();
let recv_addr = receiver.local_addr().unwrap();
receiver.set_read_timeout(Some(Duration::from_secs(2))).ok();
let sender = UdpSocket::bind("127.0.0.1:0").unwrap();
let universes = [0u8, 1, 2, 3];
for &uni in &universes {
let art_dmx = vec![
b'A', b'r', b't', b'-', b'N', b'e', b't', 0x00, 0x00, 0x50, 0x00, 0x0E,
uni, 0x00, uni, 0x00, 0x00, 0x04, uni, uni, uni, uni, ];
sender.send_to(&art_dmx, recv_addr).unwrap();
}
let mut received_universes = Vec::new();
let mut buf = [0u8; 256];
for _ in 0..4 {
if let Ok((len, _)) = receiver.recv_from(&mut buf) {
if len >= 18 && &buf[0..8] == b"Art-Net\0" {
let uni = buf[14]; received_universes.push(uni);
}
}
}
received_universes.sort();
assert_eq!(
received_universes,
universes.to_vec(),
"Missing universes, got: {:?}",
received_universes
);
}
#[tokio::test]
async fn test_midi_virtual_port_available() {
let midi_in = match midir::MidiInput::new("CLASP Protocol Test") {
Ok(m) => m,
Err(_) => {
return;
}
};
let ports = midi_in.ports();
let port_names: Vec<String> = ports
.iter()
.filter_map(|p| midi_in.port_name(p).ok())
.collect();
let virtual_ports: Vec<&String> = port_names
.iter()
.filter(|name| {
let lower = name.to_lowercase();
lower.contains("iac") || lower.contains("virtual") || lower.contains("loop") || lower.contains("midi through") || lower.contains("virmidi") })
.collect();
assert!(
port_names.is_empty() || !port_names.is_empty(),
"MIDI subsystem check complete"
);
if !virtual_ports.is_empty() {
println!("Found virtual ports: {:?}", virtual_ports);
} else if !port_names.is_empty() {
println!(
"Found {} MIDI ports (no virtual): {:?}",
port_names.len(),
port_names
);
} else {
println!("No MIDI ports (OK in CI/headless environments)");
}
}
#[tokio::test]
async fn test_midi_message_encoding() {
let note_on = [0x90, 60, 100];
assert_eq!(note_on[0] & 0xF0, 0x90, "Note On status byte wrong");
assert_eq!(note_on[0] & 0x0F, 0, "Channel should be 0");
let cc = [0xB0, 1, 64];
assert_eq!(cc[0] & 0xF0, 0xB0, "CC status byte wrong");
let pc = [0xC0, 42];
assert_eq!(pc[0] & 0xF0, 0xC0, "PC status byte wrong");
let pb_lsb = 8192 & 0x7F;
let pb_msb = (8192 >> 7) & 0x7F;
let pitch_bend = [0xE0, pb_lsb as u8, pb_msb as u8];
assert_eq!(pitch_bend[0] & 0xF0, 0xE0, "Pitch Bend status byte wrong");
let sysex = [0xF0, 0x7E, 0x00, 0x06, 0x01, 0xF7];
assert_eq!(sysex[0], 0xF0, "SysEx start byte wrong");
assert_eq!(sysex[sysex.len() - 1], 0xF7, "SysEx end byte wrong");
}
#[tokio::test]
async fn test_midi_channel_mapping() {
for ch in 0u8..16 {
let note_on = 0x90 | ch;
let extracted_channel = note_on & 0x0F;
assert_eq!(
extracted_channel,
ch,
"Channel {} extraction failed",
ch + 1
);
}
let status_types = [
(0x80, "Note Off"),
(0x90, "Note On"),
(0xA0, "Aftertouch"),
(0xB0, "CC"),
(0xC0, "Program Change"),
(0xD0, "Channel Pressure"),
(0xE0, "Pitch Bend"),
];
for (status, name_str) in status_types {
for ch in 0u8..16 {
let byte = status | ch;
let msg_type = byte & 0xF0;
assert_eq!(
msg_type,
status,
"{} status extraction failed on ch {}",
name_str,
ch + 1
);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_midi_loopback_if_available() {
let midi_in = match midir::MidiInput::new("CLASP Test In") {
Ok(m) => m,
Err(_) => {
return;
}
};
let midi_out = match midir::MidiOutput::new("CLASP Test Out") {
Ok(m) => m,
Err(_) => {
return;
}
};
let in_ports = midi_in.ports();
let out_ports = midi_out.ports();
let in_names: Vec<String> = in_ports
.iter()
.filter_map(|p| midi_in.port_name(p).ok())
.collect();
let out_names: Vec<String> = out_ports
.iter()
.filter_map(|p| midi_out.port_name(p).ok())
.collect();
let mut loopback_in = None;
let mut loopback_out = None;
for (i, name) in in_names.iter().enumerate() {
let lower = name.to_lowercase();
if lower.contains("iac")
|| lower.contains("virtual")
|| lower.contains("loop")
|| lower.contains("through")
{
loopback_in = Some(i);
break;
}
}
for (i, name) in out_names.iter().enumerate() {
let lower = name.to_lowercase();
if lower.contains("iac")
|| lower.contains("virtual")
|| lower.contains("loop")
|| lower.contains("through")
{
loopback_out = Some(i);
break;
}
}
match (loopback_in, loopback_out) {
(Some(in_idx), Some(out_idx)) => {
let received = Arc::new(AtomicBool::new(false));
let received_clone = received.clone();
let received_value = Arc::new(AtomicU32::new(0));
let received_value_clone = received_value.clone();
let _conn_in = midi_in.connect(
&in_ports[in_idx],
"test-in",
move |_stamp, message, _| {
if message.len() >= 3 && (message[0] & 0xF0) == 0xB0 {
received_clone.store(true, Ordering::SeqCst);
received_value_clone.store(message[2] as u32, Ordering::SeqCst);
}
},
(),
);
let mut conn_out = match midi_out.connect(&out_ports[out_idx], "test-out") {
Ok(c) => c,
Err(e) => {
panic!("Output connect failed: {}", e);
}
};
let test_value = 42u8;
conn_out
.send(&[0xB0, 1, test_value])
.expect("Failed to send MIDI message");
thread::sleep(Duration::from_millis(100));
if received.load(Ordering::SeqCst) {
let value = received_value.load(Ordering::SeqCst);
assert_eq!(
value, test_value as u32,
"Wrong value: {} vs {}",
value, test_value
);
} else {
println!("No MIDI message received (loopback may need configuration)");
}
}
_ => {
println!(
"No virtual MIDI loopback available (in: {:?}, out: {:?})",
in_names, out_names
);
}
}
}