manifest decode & profile validate optimization

This commit is contained in:
2026-02-25 11:16:02 +08:00
parent 2a6a963ecd
commit 1cc3351bef
35 changed files with 4418 additions and 225 deletions

View File

@@ -0,0 +1,320 @@
use rpki::data_model::manifest::ManifestObject;
use std::path::{Path, PathBuf};
use std::time::Instant;
fn default_samples_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/benchmark/selected_der")
}
fn read_samples(dir: &Path) -> Vec<Sample> {
let mut out = Vec::new();
let rd = std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {e}", dir.display()));
for ent in rd.flatten() {
let path = ent.path();
if path.extension().and_then(|s| s.to_str()) != Some("mft") {
continue;
}
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown")
.to_string();
out.push(Sample { name, path });
}
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
#[derive(Clone, Debug)]
struct Sample {
name: String,
path: PathBuf,
}
fn env_u64(name: &str, default: u64) -> u64 {
std::env::var(name)
.ok()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(default)
}
fn env_u64_opt(name: &str) -> Option<u64> {
std::env::var(name)
.ok()
.and_then(|s| s.parse::<u64>().ok())
}
fn env_bool(name: &str) -> bool {
matches!(
std::env::var(name).as_deref(),
Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES")
)
}
fn env_string(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|s| !s.trim().is_empty())
}
fn escape_md(s: &str) -> String {
s.replace('|', "\\|").replace('\n', " ")
}
fn create_parent_dirs(path: &Path) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap_or_else(|e| {
panic!("create_dir_all {}: {e}", parent.display());
});
}
}
#[test]
#[ignore = "manual performance benchmark; prints Markdown table"]
fn manifest_decode_profile_benchmark_selected_der() {
let dir = env_string("BENCH_DIR")
.map(PathBuf::from)
.unwrap_or_else(default_samples_dir);
let sample_filter = env_string("BENCH_SAMPLE");
let fixed_iters = env_u64_opt("BENCH_ITERS");
let warmup_iters = env_u64("BENCH_WARMUP_ITERS", 100);
let rounds = env_u64("BENCH_ROUNDS", 5);
let min_round_ms = env_u64("BENCH_MIN_ROUND_MS", 200);
let max_adaptive_iters = env_u64("BENCH_MAX_ITERS", 1_000_000);
let verbose = env_bool("BENCH_VERBOSE");
let out_md = env_string("BENCH_OUT_MD").map(|p| PathBuf::from(p));
let out_json = env_string("BENCH_OUT_JSON").map(|p| PathBuf::from(p));
if let Some(n) = fixed_iters {
assert!(n >= 1, "BENCH_ITERS must be >= 1");
}
assert!(rounds >= 1, "BENCH_ROUNDS must be >= 1");
assert!(min_round_ms >= 1, "BENCH_MIN_ROUND_MS must be >= 1");
assert!(max_adaptive_iters >= 1, "BENCH_MAX_ITERS must be >= 1");
let mut samples = read_samples(&dir);
assert!(
!samples.is_empty(),
"no .mft files found under: {}",
dir.display()
);
if let Some(filter) = sample_filter.as_deref() {
samples.retain(|s| s.name == filter);
assert!(!samples.is_empty(), "no sample matched BENCH_SAMPLE={filter}");
}
println!("# Manifest decode + profile validate benchmark (debug build)");
println!();
println!("- dir: {}", dir.display());
if let Some(n) = fixed_iters {
println!("- iters: {} (fixed)", n);
} else {
println!(
"- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})",
warmup_iters, rounds, min_round_ms, max_adaptive_iters
);
}
if let Some(filter) = sample_filter.as_deref() {
println!("- sample: {}", filter);
}
if verbose {
println!("- verbose: true");
}
if let Some(p) = out_md.as_ref() {
println!("- out_md: {}", p.display());
}
if let Some(p) = out_json.as_ref() {
println!("- out_json: {}", p.display());
}
println!();
println!("Samples:");
for s in &samples {
println!("- {}", s.name);
}
println!();
println!("| sample | file_count | avg ns/op | ops/s |");
println!("|---|---:|---:|---:|");
let mut rows: Vec<ResultRow> = Vec::with_capacity(samples.len());
for s in samples {
let bytes =
std::fs::read(&s.path).unwrap_or_else(|e| panic!("read {}: {e}", s.path.display()));
let file_count = ManifestObject::decode_der(bytes.as_slice())
.unwrap_or_else(|e| panic!("decode {}: {e}", s.name))
.manifest
.file_count();
// Warm-up: exercise the exact decode path but don't time it.
for _ in 0..warmup_iters {
let input = std::hint::black_box(bytes.as_slice());
let decoded = ManifestObject::decode_der(input).expect("decode");
std::hint::black_box(decoded);
}
let mut per_round_ns_per_op = Vec::with_capacity(rounds as usize);
for round in 0..rounds {
let iters = if let Some(n) = fixed_iters {
n
} else {
choose_iters_adaptive(
bytes.as_slice(),
min_round_ms,
max_adaptive_iters,
)
};
let start = Instant::now();
for _ in 0..iters {
let input = std::hint::black_box(bytes.as_slice());
let decoded = ManifestObject::decode_der(input).expect("decode");
std::hint::black_box(decoded);
}
let elapsed = start.elapsed();
let total_ns = elapsed.as_secs_f64() * 1e9;
let ns_per_op = total_ns / (iters as f64);
per_round_ns_per_op.push(ns_per_op);
if verbose {
println!(
"# {} round {}: iters={} total_ms={:.2} ns/op={:.2}",
s.name,
round + 1,
iters,
elapsed.as_secs_f64() * 1e3,
ns_per_op
);
}
}
let avg_ns = per_round_ns_per_op.iter().sum::<f64>() / (per_round_ns_per_op.len() as f64);
let ops_per_sec = 1e9_f64 / avg_ns;
println!(
"| {} | {} | {:.2} | {:.2} |",
s.name, file_count, avg_ns, ops_per_sec
);
rows.push(ResultRow {
sample: s.name,
file_count,
avg_ns_per_op: avg_ns,
ops_per_sec,
});
}
if out_md.is_some() || out_json.is_some() {
let timestamp_utc =
time::OffsetDateTime::now_utc().format(&time::format_description::well_known::Rfc3339)
.unwrap_or_else(|_| "unknown".to_string());
let cfg = RunConfig {
dir: dir.display().to_string(),
sample: sample_filter,
fixed_iters,
warmup_iters,
rounds,
min_round_ms,
max_adaptive_iters,
timestamp_utc,
};
if let Some(path) = out_md {
let md = render_markdown(&cfg, &rows);
write_text_file(&path, &md);
eprintln!("Wrote {}", path.display());
}
if let Some(path) = out_json {
let json = serde_json::to_string_pretty(&BenchmarkOutput { config: cfg, rows })
.expect("serialize json");
write_text_file(&path, &json);
eprintln!("Wrote {}", path.display());
}
}
}
fn choose_iters_adaptive(bytes: &[u8], min_round_ms: u64, max_iters: u64) -> u64 {
let min_secs = (min_round_ms as f64) / 1e3;
let mut iters: u64 = 1;
loop {
let start = Instant::now();
for _ in 0..iters {
let input = std::hint::black_box(bytes);
let decoded = ManifestObject::decode_der(input).expect("decode");
std::hint::black_box(decoded);
}
let elapsed = start.elapsed().as_secs_f64();
if elapsed >= min_secs {
return iters;
}
if iters >= max_iters {
return iters;
}
iters = (iters.saturating_mul(2)).min(max_iters);
}
}
fn render_markdown(cfg: &RunConfig, rows: &[ResultRow]) -> String {
let mut out = String::new();
out.push_str("# Manifest decode + profile validate benchmark (debug build)\n\n");
out.push_str(&format!("- timestamp_utc: {}\n", cfg.timestamp_utc));
out.push_str(&format!("- dir: `{}`\n", cfg.dir));
if let Some(s) = cfg.sample.as_deref() {
out.push_str(&format!("- sample: `{}`\n", s));
}
if let Some(n) = cfg.fixed_iters {
out.push_str(&format!("- iters: {} (fixed)\n", n));
} else {
out.push_str(&format!(
"- warmup: {} iters, rounds: {}, min_round: {}ms (adaptive iters, max {})\n",
cfg.warmup_iters, cfg.rounds, cfg.min_round_ms, cfg.max_adaptive_iters
));
}
out.push('\n');
out.push_str("| sample | file_count | avg ns/op | ops/s |\n");
out.push_str("|---|---:|---:|---:|\n");
for r in rows {
out.push_str(&format!(
"| {} | {} | {:.2} | {:.2} |\n",
escape_md(&r.sample),
r.file_count,
r.avg_ns_per_op,
r.ops_per_sec
));
}
out
}
fn write_text_file(path: &Path, content: &str) {
create_parent_dirs(path);
std::fs::write(path, content).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
}
#[derive(Clone, Debug, serde::Serialize)]
struct RunConfig {
dir: String,
sample: Option<String>,
fixed_iters: Option<u64>,
warmup_iters: u64,
rounds: u64,
min_round_ms: u64,
max_adaptive_iters: u64,
timestamp_utc: String,
}
#[derive(Clone, Debug, serde::Serialize)]
struct ResultRow {
sample: String,
file_count: usize,
avg_ns_per_op: f64,
ops_per_sec: f64,
}
#[derive(Clone, Debug, serde::Serialize)]
struct BenchmarkOutput {
config: RunConfig,
rows: Vec<ResultRow>,
}

View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
set -euo pipefail
# Generates a flamegraph SVG for Manifest decode+profile validate.
#
# Prereqs:
# - `perf` installed and usable by current user
# - `cargo flamegraph` installed (`cargo install flamegraph`)
#
# Usage examples:
# BENCH_SAMPLE=large-02 ./tests/benchmark/flamegraph_manifest_decode_profile.sh
# BENCH_SAMPLE=large-02 BENCH_ITERS=5000 FLAMEGRAPH_PROFILE=dev ./tests/benchmark/flamegraph_manifest_decode_profile.sh
# BENCH_SAMPLE=large-02 BENCH_ITERS=2000 FLAMEGRAPH_PROFILE=release ./tests/benchmark/flamegraph_manifest_decode_profile.sh
PROFILE="${FLAMEGRAPH_PROFILE:-dev}" # dev|release
SAMPLE="${BENCH_SAMPLE:-large-02}"
ITERS="${BENCH_ITERS:-2000}"
FREQ="${FLAMEGRAPH_FREQ:-99}"
WARMUP="${BENCH_WARMUP_ITERS:-1}"
ROUNDS="${BENCH_ROUNDS:-1}"
MIN_ROUND_MS="${BENCH_MIN_ROUND_MS:-1}"
MAX_ITERS="${BENCH_MAX_ITERS:-100000}"
if ! command -v perf >/dev/null 2>&1; then
echo "ERROR: perf not found. Install linux-tools/perf first (may require sudo)." >&2
exit 2
fi
if ! command -v cargo-flamegraph >/dev/null 2>&1 && ! command -v flamegraph >/dev/null 2>&1; then
# cargo installs as `cargo flamegraph`, but the binary is `cargo-flamegraph`.
if ! command -v cargo-flamegraph >/dev/null 2>&1; then
echo "ERROR: cargo-flamegraph not found. Install with: cargo install flamegraph" >&2
exit 2
fi
fi
OUT="target/bench/flamegraph_mft_decode_profile_${SAMPLE}_${PROFILE}.svg"
echo "profile=${PROFILE} sample=${SAMPLE} iters=${ITERS} freq=${FREQ} out=${OUT}"
FLAGS=()
if [[ "${PROFILE}" == "release" ]]; then
FLAGS+=(--release)
else
FLAGS+=(--dev)
fi
# On WSL2, /usr/bin/perf is often a wrapper that exits 2 because there is no
# kernel-matched perf binary. In that case, prefer a real perf binary under
# /usr/lib/linux-tools/*/perf by putting a shim earlier in PATH.
PERF_WRAPPER_OK=1
PERF_VERSION_OUT="$(perf --version 2>&1 || true)"
if echo "${PERF_VERSION_OUT}" | grep -q "WARNING: perf not found for kernel"; then
PERF_WRAPPER_OK=0
fi
if [[ "${PERF_WRAPPER_OK}" == "0" ]]; then
PERF_REAL="$(ls -1 /usr/lib/linux-tools/*/perf 2>/dev/null | head -n 1 || true)"
if [[ -z "${PERF_REAL}" ]]; then
echo "ERROR: perf wrapper found, but no real perf binary under /usr/lib/linux-tools/*/perf" >&2
exit 2
fi
SHIM_DIR="target/bench/tools"
mkdir -p "${SHIM_DIR}"
cat > "${SHIM_DIR}/perf" <<EOF
#!/usr/bin/env bash
exec "${PERF_REAL}" "\$@"
EOF
chmod +x "${SHIM_DIR}/perf"
export PATH="$(pwd)/${SHIM_DIR}:${PATH}"
echo "Using perf shim -> ${PERF_REAL}"
fi
BENCH_SAMPLE="${SAMPLE}" \
BENCH_ITERS="${ITERS}" \
BENCH_WARMUP_ITERS="${WARMUP}" \
BENCH_ROUNDS="${ROUNDS}" \
BENCH_MIN_ROUND_MS="${MIN_ROUND_MS}" \
BENCH_MAX_ITERS="${MAX_ITERS}" \
cargo flamegraph "${FLAGS[@]}" \
-F "${FREQ}" \
--output "${OUT}" \
--test bench_manifest_decode_profile -- --ignored --nocapture
echo "Wrote ${OUT}"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,9 @@
name input_path der_path input_size der_size source_format
large-01 benchmark/fixtures/selected/large-01.mft benchmark/fixtures/selected_der/large-01.mft 262869 2250 stored_point_container
large-02 benchmark/fixtures/selected/large-02.mft benchmark/fixtures/selected_der/large-02.mft 264331 10476 stored_point_container
medium-01 benchmark/fixtures/selected/medium-01.mft benchmark/fixtures/selected_der/medium-01.mft 16384 2681 stored_point_container
medium-02 benchmark/fixtures/selected/medium-02.mft benchmark/fixtures/selected_der/medium-02.mft 16384 2681 stored_point_container
small-01 benchmark/fixtures/selected/small-01.mft benchmark/fixtures/selected_der/small-01.mft 3160 1875 stored_point_container
small-02 benchmark/fixtures/selected/small-02.mft benchmark/fixtures/selected_der/small-02.mft 3160 1875 stored_point_container
xlarge-01 benchmark/fixtures/selected/xlarge-01.mft benchmark/fixtures/selected_der/xlarge-01.mft 4522145 144968 stored_point_container
xlarge-02 benchmark/fixtures/selected/xlarge-02.mft benchmark/fixtures/selected_der/xlarge-02.mft 4615094 150261 stored_point_container
1 name input_path der_path input_size der_size source_format
2 large-01 benchmark/fixtures/selected/large-01.mft benchmark/fixtures/selected_der/large-01.mft 262869 2250 stored_point_container
3 large-02 benchmark/fixtures/selected/large-02.mft benchmark/fixtures/selected_der/large-02.mft 264331 10476 stored_point_container
4 medium-01 benchmark/fixtures/selected/medium-01.mft benchmark/fixtures/selected_der/medium-01.mft 16384 2681 stored_point_container
5 medium-02 benchmark/fixtures/selected/medium-02.mft benchmark/fixtures/selected_der/medium-02.mft 16384 2681 stored_point_container
6 small-01 benchmark/fixtures/selected/small-01.mft benchmark/fixtures/selected_der/small-01.mft 3160 1875 stored_point_container
7 small-02 benchmark/fixtures/selected/small-02.mft benchmark/fixtures/selected_der/small-02.mft 3160 1875 stored_point_container
8 xlarge-01 benchmark/fixtures/selected/xlarge-01.mft benchmark/fixtures/selected_der/xlarge-01.mft 4522145 144968 stored_point_container
9 xlarge-02 benchmark/fixtures/selected/xlarge-02.mft benchmark/fixtures/selected_der/xlarge-02.mft 4615094 150261 stored_point_container

Binary file not shown.

Binary file not shown.

View File

@@ -59,8 +59,15 @@ fn store_raw_publication_point_files(
store
.put_raw(manifest_rsync_uri, manifest_bytes)
.expect("store manifest raw");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);

View File

@@ -80,8 +80,15 @@ fn cache_pack_publication_point_mismatch_is_rejected() {
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);

View File

@@ -18,13 +18,16 @@ fn decode_manifest_fixture_smoke() {
rpki::data_model::oid::OID_SHA256
);
assert!(mft.manifest.next_update > mft.manifest.this_update);
assert!(!mft.manifest.files.is_empty());
assert!(mft.manifest.file_count() > 0);
// The manifest file MUST NOT be listed in its own fileList.
let entries = mft
.manifest
.parse_files()
.expect("parse validated manifest fileList");
assert!(
mft.manifest
.files
entries
.iter()
.all(|f| !f.file_name.to_ascii_lowercase().ends_with(".mft"))
.all(|f| !f.file_name.as_str().to_ascii_lowercase().ends_with(".mft"))
);
}

View File

@@ -57,8 +57,15 @@ fn manifest_success_writes_fetch_cache_pp_pack() {
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
@@ -111,8 +118,15 @@ fn manifest_hash_mismatch_falls_back_to_fetch_cache_pp_when_enabled() {
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
@@ -140,11 +154,11 @@ fn manifest_hash_mismatch_falls_back_to_fetch_cache_pp_when_enabled() {
.expect("fetch_cache_pp pack exists");
let cached_pack = FetchCachePpPack::decode(&cached_bytes).expect("decode cached");
let victim = manifest
let entries = manifest
.manifest
.files
.first()
.expect("non-empty file list");
.parse_files()
.expect("parse validated manifest fileList");
let victim = entries.first().expect("non-empty file list");
let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name);
let mut tampered = store
.get_raw(&victim_uri)
@@ -186,8 +200,15 @@ fn manifest_failed_fetch_stop_all_output() {
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
@@ -208,11 +229,11 @@ fn manifest_failed_fetch_stop_all_output() {
)
.expect("first run stores fetch_cache_pp pack");
let victim = manifest
let entries = manifest
.manifest
.files
.first()
.expect("non-empty file list");
.parse_files()
.expect("parse validated manifest fileList");
let victim = entries.first().expect("non-empty file list");
let victim_uri = format!("{publication_point_rsync_uri}{}", victim.file_name);
let mut tampered = store
.get_raw(&victim_uri)
@@ -255,8 +276,15 @@ fn manifest_fallback_pack_is_revalidated_and_rejected_if_stale() {
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);
@@ -314,8 +342,15 @@ fn manifest_replay_is_treated_as_failed_fetch_and_uses_fetch_cache_pp() {
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);

View File

@@ -41,8 +41,12 @@ fn manifest_outside_publication_point_is_failed_fetch_rfc9286_section6_1() {
// Store all referenced files under the (different) publication point so that §6.4/§6.5
// would otherwise succeed if §6.1 was not enforced.
for entry in &manifest.manifest.files {
let file_path = fixture_dir.join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = fixture_dir.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);

View File

@@ -319,13 +319,16 @@ struct ManifestEContentPretty {
impl From<&ManifestEContent> for ManifestEContentPretty {
fn from(v: &ManifestEContent) -> Self {
let entries = v
.parse_files()
.expect("parse validated manifest fileList");
Self {
version: v.version,
manifest_number: v.manifest_number.to_hex_upper(),
this_update: v.this_update,
next_update: v.next_update,
file_hash_alg: v.file_hash_alg.clone(),
files: v.files.iter().map(FileAndHashPretty::from).collect(),
files: entries.iter().map(FileAndHashPretty::from).collect(),
}
}
}
@@ -339,8 +342,8 @@ struct FileAndHashPretty {
impl From<&FileAndHash> for FileAndHashPretty {
fn from(v: &FileAndHash) -> Self {
Self {
file_name: v.file_name.clone(),
hash_hex: hex::encode(&v.hash_bytes),
file_name: v.file_name.as_str().to_string(),
hash_hex: hex::encode(v.hash_bytes.as_ref()),
}
}
}

View File

@@ -51,8 +51,15 @@ fn build_cernet_pack_and_validation_time() -> (
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);

View File

@@ -51,8 +51,15 @@ fn build_cernet_pack_and_validation_time() -> (
store
.put_raw(&manifest_rsync_uri, &manifest_bytes)
.expect("store manifest");
for entry in &manifest.manifest.files {
let file_path = manifest_path.parent().unwrap().join(&entry.file_name);
let entries = manifest
.manifest
.parse_files()
.expect("parse validated manifest fileList");
for entry in &entries {
let file_path = manifest_path
.parent()
.unwrap()
.join(entry.file_name.as_str());
let bytes = std::fs::read(&file_path)
.unwrap_or_else(|_| panic!("read fixture file referenced by manifest: {file_path:?}"));
let rsync_uri = format!("{publication_point_rsync_uri}{}", entry.file_name);

View File

@@ -0,0 +1,33 @@
use std::process::Command;
#[test]
fn rpki_bin_help_exits_success_and_prints_usage() {
// This also increases coverage for `src/bin/rpki.rs` because it executes the binary.
let exe = env!("CARGO_BIN_EXE_rpki");
let out = Command::new(exe)
.arg("--help")
.output()
.expect("run rpki --help");
assert!(out.status.success(), "status={}", out.status);
let stdout = String::from_utf8_lossy(&out.stdout);
// `cli::usage()` contains "Usage:".
assert!(
stdout.contains("Usage:") || stdout.contains("USAGE:"),
"stdout={stdout}"
);
}
#[test]
fn rpki_bin_without_args_exits_2_and_prints_error() {
let exe = env!("CARGO_BIN_EXE_rpki");
let out = Command::new(exe).output().expect("run rpki");
assert_eq!(out.status.code(), Some(2), "status={}", out.status);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.trim().is_empty(),
"expected non-empty stderr, got empty"
);
}