Files
tines/tines-ros2/tests/topics.rs
T
kyle 7be09f8e9a Task 9: tines-ros2 — pure topic_name escaping (default, tested) + optional rclrs TinesNode (feature ros2)
Adds the ninth fleet-deployment crate. Default build is ROS2-free: topic_name
hex-escapes arbitrary tines topics into valid ROS2 names under /tines/ (deterministic,
injective) and wires Node<LogStore, BtTransport>. The rclrs-backed self-contained
TinesNode lives under #[cfg(feature="ros2")] with an off-by-default ros2 feature, so
the crate builds/tests without a ROS2 toolchain. Adds scripts/check-ros2.sh (needs a
ROS2 toolchain; not run in CI), registers the crate in workspace members, and passes
sh scripts/ci.sh.
2026-10-18 14:46:57 -07:00

75 lines
2.8 KiB
Rust

//! Integration tests for [`tines_ros2::topic_name`] (pure, default features).
use tines_core::consts::MAX_TOPIC;
use tines_ros2::topic_name;
/// Every arbitrary input — control bytes, high/non-ASCII bytes, slashes, and
/// even the empty topic — escapes into a valid ROS2 topic name under
/// `/tines/` consisting solely of lowercase hex digits.
#[test]
fn topic_name_escapes() {
let inputs: [&[u8]; 5] = [
b"door/3".as_ref(),
b"".as_ref(),
[0u8, 1, 2, 250, 128, 255].as_ref(),
"café — naïve ✓".as_bytes(),
&vec![7u8; MAX_TOPIC][..],
];
for input in inputs {
let name = topic_name(input);
// Valid ROS2 topic names are absolute (start with `/`) and contain no
// illegal characters; here the tail is pure hex, so both hold.
assert!(
name.starts_with("/tines/"),
"topic must live under the /tines/ namespace: {name}"
);
let hex = &name["/tines/".len()..];
assert_eq!(
hex.len(),
input.len() * 2,
"each byte must become exactly two hex digits"
);
// Each byte maps to exactly its two lowercase hex digits.
for (byte, pair) in input.iter().zip(hex.as_bytes().chunks(2)) {
assert_eq!(
pair,
format!("{byte:02x}").into_bytes(),
"byte {byte:02x} must escape to its lowercase hex {byte:02x}"
);
}
}
}
/// Encoding is deterministic and injective across the full range of topic
/// lengths up to [`MAX_TOPIC`]: distinct byte strings never collide.
#[test]
fn topic_name_roundtrip() {
// Deterministic: identical input yields identical output.
assert_eq!(topic_name(b"door/3"), topic_name(b"door/3"));
// Injective: every distinct-length input maps to a distinct name. Hex of
// distinct byte strings is distinct, and the shared `/tines/` prefix cannot
// cause a cross-length collision (the hex tail has length `2 * len`).
for len in 0..=MAX_TOPIC {
// A prefix of distinct bytes: `len == a` yields a name never seen at any
// other `len`, and distinct `len` values cannot collide.
let input: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
assert!(topic_name(&input).starts_with("/tines/"));
}
// Cross-check: distinct byte strings of *equal* length never collide.
let mut names = std::collections::BTreeSet::new();
for seed in 0..=255u8 {
let input: Vec<u8> = (0..MAX_TOPIC).map(|i| seed.wrapping_add(i as u8)).collect();
let name = topic_name(&input);
assert!(name.starts_with("/tines/"));
assert!(
names.insert(name),
"injectivity violated: seed {seed} collided"
);
}
assert_eq!(names.len(), 256, "256 distinct equal-length names expected");
}