Bootstrap DD3 Rust port workspace with host-first compatibility tests
This commit is contained in:
8
crates/xtask/Cargo.toml
Normal file
8
crates/xtask/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
regex = "1"
|
||||
234
crates/xtask/src/main.rs
Normal file
234
crates/xtask/src/main.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use regex::Regex;
|
||||
|
||||
const BASELINE_COMMIT: &str = "a3c61f9b929fbc55bfb502b443fba2f98023b3f1";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let cmd = args.next().unwrap_or_default();
|
||||
|
||||
match cmd.as_str() {
|
||||
"sync-fixtures" => sync_fixtures(),
|
||||
"check-manufacturer" => check_manufacturer(),
|
||||
"verify-fixture-sources" => verify_fixture_sources(),
|
||||
_ => {
|
||||
eprintln!("usage: cargo run -p xtask -- <sync-fixtures|check-manufacturer|verify-fixture-sources>");
|
||||
Err(anyhow!("unknown command"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn repo_root() -> Result<PathBuf> {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let root = manifest_dir
|
||||
.parent()
|
||||
.and_then(|p| p.parent())
|
||||
.ok_or_else(|| anyhow!("failed to resolve workspace root"))?;
|
||||
Ok(root.to_path_buf())
|
||||
}
|
||||
|
||||
fn parse_cpp_array(src: &str, name: &str) -> Result<Vec<u8>> {
|
||||
let re = Regex::new(&format!(
|
||||
r"(?s)static\s+const\s+uint8_t\s+{}\[\]\s*=\s*\{{(?P<body>.*?)\}};",
|
||||
name
|
||||
))?;
|
||||
let caps = re
|
||||
.captures(src)
|
||||
.ok_or_else(|| anyhow!("array {name} not found"))?;
|
||||
let body = caps.name("body").unwrap().as_str();
|
||||
let item_re = Regex::new(r"0x([0-9A-Fa-f]{1,2})")?;
|
||||
let mut out = Vec::new();
|
||||
for cap in item_re.captures_iter(body) {
|
||||
let byte = u8::from_str_radix(&cap[1], 16)?;
|
||||
out.push(byte);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn canonicalize_full_30_vector(mut bytes: Vec<u8>) -> Vec<u8> {
|
||||
// Upstream pinned baseline vector in test_payload_codec.cpp is truncated by two
|
||||
// final p3 signed-delta varints (expected +205, -195 => 0x9A 0x03 0x85 0x03).
|
||||
// Keep raw provenance separately and write canonical bytes for host tests.
|
||||
if bytes.len() == 183 {
|
||||
bytes.extend_from_slice(&[0x9A, 0x03, 0x85, 0x03]);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn crc16_ccitt(data: &[u8]) -> u16 {
|
||||
let mut crc: u16 = 0xFFFF;
|
||||
for byte in data {
|
||||
crc ^= (*byte as u16) << 8;
|
||||
for _ in 0..8 {
|
||||
if (crc & 0x8000) != 0 {
|
||||
crc = (crc << 1) ^ 0x1021;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
crc
|
||||
}
|
||||
|
||||
fn write_bin(path: &Path, bytes: &[u8]) -> Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, bytes)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_fixtures() -> Result<()> {
|
||||
let root = repo_root()?;
|
||||
|
||||
let payload_test = root.join("vendor/dd3-cpp/test/test_payload_codec/test_payload_codec.cpp");
|
||||
let payload_src = fs::read_to_string(&payload_test).with_context(|| {
|
||||
format!("failed reading {}", payload_test.display())
|
||||
})?;
|
||||
|
||||
let sync_empty = parse_cpp_array(&payload_src, "VECTOR_SYNC_EMPTY")?;
|
||||
let sparse_5 = parse_cpp_array(&payload_src, "VECTOR_SPARSE_5")?;
|
||||
let full_30_raw = parse_cpp_array(&payload_src, "VECTOR_FULL_30")?;
|
||||
let full_30 = canonicalize_full_30_vector(full_30_raw.clone());
|
||||
|
||||
write_bin(&root.join("fixtures/protocol/payload_v3/sync_empty.bin"), &sync_empty)?;
|
||||
write_bin(&root.join("fixtures/protocol/payload_v3/sparse_5.bin"), &sparse_5)?;
|
||||
write_bin(&root.join("fixtures/protocol/payload_v3/full_30.bin"), &full_30)?;
|
||||
write_bin(
|
||||
&root.join("fixtures/protocol/payload_v3/full_30_upstream_raw.bin"),
|
||||
&full_30_raw,
|
||||
)?;
|
||||
|
||||
let frame_payload = [0x01u8, 0x02, 0xA5];
|
||||
let mut frame = vec![0x00, 0xF1, 0x9C];
|
||||
frame.extend_from_slice(&frame_payload);
|
||||
let crc = crc16_ccitt(&frame);
|
||||
frame.extend_from_slice(&crc.to_be_bytes());
|
||||
write_bin(&root.join("fixtures/protocol/frames/batchup_f19c_payload_0102a5.bin"), &frame)?;
|
||||
|
||||
let mut frame_bad = frame.clone();
|
||||
let last = frame_bad.len() - 1;
|
||||
frame_bad[last] ^= 0x01;
|
||||
write_bin(&root.join("fixtures/protocol/frames/batchup_f19c_payload_0102a5_bad_crc.bin"), &frame_bad)?;
|
||||
|
||||
let mut chunk_ok = Vec::new();
|
||||
// record format: [batch_id_le:2][idx:1][count:1][total_len_le:2][chunk_len:1][chunk_data]
|
||||
chunk_ok.extend_from_slice(&77u16.to_le_bytes());
|
||||
chunk_ok.extend_from_slice(&[0, 3]);
|
||||
chunk_ok.extend_from_slice(&7u16.to_le_bytes());
|
||||
chunk_ok.extend_from_slice(&[3, 1, 2, 3]);
|
||||
chunk_ok.extend_from_slice(&77u16.to_le_bytes());
|
||||
chunk_ok.extend_from_slice(&[1, 3]);
|
||||
chunk_ok.extend_from_slice(&7u16.to_le_bytes());
|
||||
chunk_ok.extend_from_slice(&[2, 4, 5]);
|
||||
chunk_ok.extend_from_slice(&77u16.to_le_bytes());
|
||||
chunk_ok.extend_from_slice(&[2, 3]);
|
||||
chunk_ok.extend_from_slice(&7u16.to_le_bytes());
|
||||
chunk_ok.extend_from_slice(&[2, 6, 7]);
|
||||
write_bin(&root.join("fixtures/protocol/chunks/in_order_ok.bin"), &chunk_ok)?;
|
||||
|
||||
let mut chunk_missing = Vec::new();
|
||||
chunk_missing.extend_from_slice(&10u16.to_le_bytes());
|
||||
chunk_missing.extend_from_slice(&[0, 3]);
|
||||
chunk_missing.extend_from_slice(&6u16.to_le_bytes());
|
||||
chunk_missing.extend_from_slice(&[2, 9, 8]);
|
||||
chunk_missing.extend_from_slice(&10u16.to_le_bytes());
|
||||
chunk_missing.extend_from_slice(&[2, 3]);
|
||||
chunk_missing.extend_from_slice(&6u16.to_le_bytes());
|
||||
chunk_missing.extend_from_slice(&[2, 7, 6]);
|
||||
write_bin(&root.join("fixtures/protocol/chunks/missing_chunk.bin"), &chunk_missing)?;
|
||||
|
||||
let mut chunk_wrong_total = Vec::new();
|
||||
chunk_wrong_total.extend_from_slice(&55u16.to_le_bytes());
|
||||
chunk_wrong_total.extend_from_slice(&[0, 2]);
|
||||
chunk_wrong_total.extend_from_slice(&5u16.to_le_bytes());
|
||||
chunk_wrong_total.extend_from_slice(&[3, 1, 2, 3]);
|
||||
chunk_wrong_total.extend_from_slice(&55u16.to_le_bytes());
|
||||
chunk_wrong_total.extend_from_slice(&[1, 2]);
|
||||
chunk_wrong_total.extend_from_slice(&5u16.to_le_bytes());
|
||||
chunk_wrong_total.extend_from_slice(&[3, 4, 5, 6]);
|
||||
write_bin(&root.join("fixtures/protocol/chunks/wrong_total_len.bin"), &chunk_wrong_total)?;
|
||||
|
||||
let sources = format!(
|
||||
"# Fixture Sources\n\n- Baseline repository: C3MA/DD3-LoRa-Bridge-MultiSender\n- Baseline branch: lora-refactor\n- Baseline commit: {BASELINE_COMMIT}\n- Payload vectors: vendor/dd3-cpp/test/test_payload_codec/test_payload_codec.cpp\n- Payload note: VECTOR_FULL_30 in pinned commit is 183-byte upstream raw (stored as full_30_upstream_raw.bin); canonical full_30.bin appends final two p3 deltas `9A 03 85 03` to satisfy baseline codec semantics.\n- Frame/chunk vectors: derived from vendor/dd3-cpp/test/test_lora_transport/test_lora_transport.cpp semantics\n"
|
||||
);
|
||||
fs::write(root.join("fixtures/protocol/SOURCES.md"), sources)?;
|
||||
|
||||
println!("fixtures synced");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_manufacturer() -> Result<()> {
|
||||
let root = repo_root()?;
|
||||
let mut offenders = Vec::new();
|
||||
|
||||
fn walk(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(&path, files)?;
|
||||
} else if path.extension().and_then(|x| x.to_str()) == Some("rs") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let mut files = Vec::new();
|
||||
walk(&root.join("crates"), &mut files)?;
|
||||
|
||||
for file in files {
|
||||
let txt = fs::read_to_string(&file)?;
|
||||
if txt.contains("\"AcidBurns\"") {
|
||||
let file_norm = file.to_string_lossy().replace('\\', "/");
|
||||
let allow = file_norm.ends_with("/crates/dd3_contracts/src/lib.rs")
|
||||
|| file_norm.ends_with("/crates/dd3_contracts/tests/contracts_tests.rs");
|
||||
if !allow {
|
||||
offenders.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if offenders.is_empty() {
|
||||
println!("manufacturer drift check passed");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"unexpected hardcoded manufacturer literal(s): {:?}",
|
||||
offenders
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_fixture_sources() -> Result<()> {
|
||||
let root = repo_root()?;
|
||||
let path = root.join("fixtures/protocol/SOURCES.md");
|
||||
let txt = fs::read_to_string(&path)
|
||||
.with_context(|| format!("missing {}", path.display()))?;
|
||||
|
||||
if !txt.contains(BASELINE_COMMIT) {
|
||||
return Err(anyhow!("SOURCES.md does not contain baseline commit"));
|
||||
}
|
||||
|
||||
let required = [
|
||||
"fixtures/protocol/payload_v3/sync_empty.bin",
|
||||
"fixtures/protocol/payload_v3/sparse_5.bin",
|
||||
"fixtures/protocol/payload_v3/full_30.bin",
|
||||
"fixtures/protocol/frames/batchup_f19c_payload_0102a5.bin",
|
||||
"fixtures/protocol/chunks/in_order_ok.bin",
|
||||
];
|
||||
|
||||
for rel in required {
|
||||
let full = root.join(rel);
|
||||
if !full.exists() {
|
||||
return Err(anyhow!("missing fixture {rel}"));
|
||||
}
|
||||
}
|
||||
|
||||
println!("fixture source metadata verified");
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user