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.
75 lines
2.8 KiB
Rust
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");
|
|
}
|