#![cfg(all(feature = "tnc", feature = "fx25", feature = "kiss"))]
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
struct CountingAlloc;
std::thread_local! {
static ALLOCATIONS: Cell<usize> = const { Cell::new(0) };
}
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let _ = ALLOCATIONS.try_with(|c| c.set(c.get() + 1));
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let _ = ALLOCATIONS.try_with(|c| c.set(c.get() + 1));
unsafe { System.realloc(ptr, layout, new_size) }
}
}
#[global_allocator]
static GLOBAL: CountingAlloc = CountingAlloc;
fn allocations_during<R>(f: impl FnOnce() -> R) -> (usize, R) {
let before = ALLOCATIONS.with(Cell::get);
let result = f();
let after = ALLOCATIONS.with(Cell::get);
(after - before, result)
}
use yodel::SampleRate;
use yodel::ax25::{Address, UiFrame};
use yodel::fx25::{Fx25Receiver, WRAP_MAX, byte_bits, stuff_frame, wrap};
use yodel::kiss::{KissCommand, KissDeframer, KissPort, encode_into};
use yodel::tnc::{DefaultTncReceiver, TncConfig, TncTransmitter};
#[test]
fn core_mod_demod_frame_path_is_alloc_free() {
let config = TncConfig::bell_202(SampleRate::new(48_000).unwrap()).unwrap();
let tx = TncTransmitter::new(config);
let mut rx = Box::new(DefaultTncReceiver::new(config).unwrap());
let dest = Address::new(b"APRS", 0).unwrap();
let src = Address::new(b"N0CALL", 7).unwrap();
let info = b"!4903.50N/07201.75W-no alloc proof";
let mut frame_buf = [0u8; 330];
let mut recovered = [0u8; 330];
let mut recovered_len = 0usize;
let mut frames = 0usize;
let (allocs, ()) = allocations_during(|| {
let frame_len = tx
.build_frame_raw(dest, src, &[], info, &mut frame_buf)
.unwrap();
for sample in tx.frame_samples_i16(&frame_buf[..frame_len]) {
if let Some(frame) = rx.push_i16(sample) {
let bytes = frame.info();
recovered[..bytes.len()].copy_from_slice(bytes);
recovered_len = bytes.len();
frames += 1;
}
}
});
assert_eq!(allocs, 0, "core mod→demod→frame path allocated");
assert_eq!(frames, 1, "frame not recovered");
assert_eq!(
&recovered[..recovered_len],
info,
"info field not recovered byte-exact"
);
}
#[test]
fn fx25_encode_decode_is_alloc_free() {
let frame = UiFrame::new(
Address::new(b"APRS", 0).unwrap(),
Address::new(b"N0CALL", 7).unwrap(),
b">fx.25 no alloc",
);
let mut body = [0u8; 330];
let body_len = frame.build(&mut body).unwrap();
let mut stuffed = [0u8; 512];
let mut out = [0u8; WRAP_MAX];
let mut rx = Box::new(Fx25Receiver::<330>::new());
let mut recovered = [0u8; 330];
let mut recovered_len = 0usize;
let (allocs, ()) = allocations_during(|| {
let stuffed_len = stuff_frame(&body[..body_len], &mut stuffed).unwrap();
let wrapped = wrap(&stuffed[..stuffed_len], &mut out).unwrap();
for bit in byte_bits(&out[..wrapped.len()]) {
if let Some(Ok(frame)) = rx.push(bit) {
recovered[..frame.len()].copy_from_slice(frame);
recovered_len = frame.len();
}
}
});
assert_eq!(allocs, 0, "FX.25 encode/decode path allocated");
assert_eq!(&recovered[..recovered_len], &body[..body_len]);
}
#[test]
fn kiss_round_trip_is_alloc_free() {
let payload = [0xC0u8, 0xDB, 0x01, 0x7E, 0xFF, 0x00, 0xDC, 0xDD];
let mut wire = [0u8; 64];
let mut deframer = KissDeframer::<64>::new();
let mut recovered = [0u8; 64];
let mut recovered_len = 0usize;
let (allocs, ()) = allocations_during(|| {
let wire_len = encode_into(
KissPort::new(0).unwrap(),
KissCommand::Data,
&payload,
&mut wire,
)
.unwrap();
for &byte in wire.iter().take(wire_len) {
if let Some(Ok(frame)) = deframer.push(byte) {
let bytes = frame.payload();
recovered[..bytes.len()].copy_from_slice(bytes);
recovered_len = bytes.len();
}
}
});
assert_eq!(allocs, 0, "KISS round trip allocated");
assert_eq!(&recovered[..recovered_len], &payload);
}