diff --git a/scripts/experiments/feature035/experiments.json b/scripts/experiments/feature035/experiments.json index b0888e3..bed11eb 100644 --- a/scripts/experiments/feature035/experiments.json +++ b/scripts/experiments/feature035/experiments.json @@ -4,8 +4,8 @@ "experiments": [ { "id": "sync-ours-rsync-only", - "left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync" }, - "right": { "rpKind": "ours", "mode": "standard", "protocol": "rsync-only" } + "left": { "rpKind": "ours", "mode": "standard", "protocol": "rrdp+rsync", "rsyncScope": "module-root" }, + "right": { "rpKind": "ours", "mode": "standard", "protocol": "rsync-only", "rsyncScope": "module-root" } }, { "id": "sync-rpki-client-rsync-only", diff --git a/scripts/experiments/feature035/run_feature035_experiment.py b/scripts/experiments/feature035/run_feature035_experiment.py index f6196d1..1782973 100755 --- a/scripts/experiments/feature035/run_feature035_experiment.py +++ b/scripts/experiments/feature035/run_feature035_experiment.py @@ -302,7 +302,28 @@ def build_tool_binaries() -> None: ], cwd=REPO_ROOT, ) - run_local(["make", "-j2"], cwd=PORTABLE_ROOT) + rpki_client_bin = PORTABLE_ROOT / "src" / "rpki-client" + if not rpki_client_bin.is_file(): + run_local(["make", "-j2"], cwd=PORTABLE_ROOT) + smoke = run_local( + [str(rpki_client_bin), "-T", "invalid"], + capture=True, + check=False, + ) + if "--ta-fixture requires :" not in (smoke.stderr + smoke.stdout): + cache_bin = DEV_ROOT / ".cache" / "rpki-client-9.8-cir" / "rpki-client" + cache_smoke = run_local( + [str(cache_bin), "-T", "invalid"], + capture=True, + check=False, + ) if cache_bin.is_file() else None + if cache_smoke and "--ta-fixture requires :" in (cache_smoke.stderr + cache_smoke.stdout): + shutil.copy2(cache_bin, rpki_client_bin) + else: + raise SystemExit( + "rpki-client binary lacks local TA fixture support (-T); " + "switch to feature/cir-output-for-rp-compare and rebuild or restore the cached CIR+TA-fixture binary" + ) def build_fixture_proof(run_root: Path, rirs: list[str]) -> Path: @@ -398,6 +419,9 @@ def build_side_command( ) if side["protocol"] == "rsync-only": argv.append("--disable-rrdp") + rsync_scope = side.get("rsyncScope") + if rsync_scope: + argv.extend(["--rsync-scope", str(rsync_scope)]) if side["mode"] == "strict-name": argv.extend(["--strict", "name"]) elif side["mode"] == "strict-cms-der": @@ -426,8 +450,6 @@ def build_side_command( str(run_dir / "vrps.csv"), "--vaps-csv-out", str(run_dir / "vaps.csv"), - "--compare-view-trust-anchor", - compare_view_trust_anchor(rirs), ] ) return "cd {run_dir} && {prefix} /usr/bin/time -v -o process-time.txt -- {cmd} > stdout.log 2> stderr.log".format( @@ -505,139 +527,6 @@ def cir_tal_uri_for_rir(rir: str) -> str: }[rir] -def compare_view_trust_anchor(rirs: list[str]) -> str: - return "all5" if len(rirs) > 1 else rirs[0] - - -def sanitize_run_meta( - run_root: Path, - exp_id: str, - side_label: str, - step: str, - rp_kind: str, - rp_mode: str, - protocol: str, - strict_policies: str, - rirs: list[str], - run_dir: Path, - fixture_proof: Path, - result_ccr: Path, - result_cir: Path, - report_json: Path, - stage_timing_json: Path, - process_time: Path, - stdout_log: Path, - stderr_log: Path, - exit_code: int, - counts: dict[str, Any], - time_info: dict[str, Any], - fixture_pinned: bool, -) -> Path: - repo_root = run_root - run_meta_path = run_dir / "run-meta.json" - run_meta_args = [ - sys.executable, - str(FEATURE_BUNDLE), - "run-meta", - "--out", - str(run_meta_path), - "--repo-root", - str(repo_root), - "--experiment-id", - exp_id, - "--side", - "left" if side_label == "A" else "right", - "--side-label", - side_label, - "--step", - step, - "--run-id", - f"{side_label}-{step}", - "--rp-kind", - rp_kind, - "--rp-binary", - "bin/rpki" if rp_kind == "ours" else "bin/rpki-client", - "--rp-version", - "portable" if rp_kind == "rpki-client" else "ours", - "--rp-mode", - rp_mode, - "--protocol-mode", - protocol, - "--strict-policies", - strict_policies, - "--rirs", - ",".join(rirs), - "--argv-json", - json.dumps([]), - "--env-json", - json.dumps({}), - "--cwd", - str(run_root), - "--reset-before-run", - "--state-root", - str(run_dir.parent / "state"), - "--db", - str(run_dir.parent / "state" / "work-db"), - "--repo-bytes-db", - str(run_dir.parent / "state" / "repo-bytes.db"), - "--raw-store-db", - str(run_dir.parent / "state" / "raw-store.db"), - "--rsync-mirror-root", - str(run_dir.parent / "state" / "rsync-mirror"), - "--cache-root", - str(run_dir.parent / "state" / ("cache" if rp_kind == "rpki-client" else "work-db")), - "--ccr", - str(result_ccr), - "--cir", - str(result_cir), - "--fixture-proof", - str(fixture_proof), - "--fixture-proof-summary-json", - json.dumps( - { - "taFixturePinned": fixture_pinned, - "taOnlineFetchObserved": False, - "trustAnchorCount": len(rirs), - } - ), - "--report-json", - str(report_json), - "--stage-timing-json", - str(stage_timing_json), - "--stdout-log", - str(stdout_log), - "--stderr-log", - str(stderr_log), - "--process-time", - str(process_time), - "--exit-code", - str(exit_code), - "--wall-ms", - str(time_info.get("wallMs", 0)), - "--max-rss-kb", - str(time_info.get("maxRssKb", 0)), - "--vrps", - str(counts.get("vrps", 0)), - "--vaps", - str(counts.get("aspas", 0)), - "--publication-points", - str(counts.get("publicationPoints", counts.get("repositories", 0))), - "--warnings", - str(counts.get("warnings", 0)), - "--cir-object-count", - str(counts.get("cirObjectCount", 0)), - "--cir-reject-count", - str(counts.get("cirRejectCount", 0)), - "--cir-trust-anchor-count", - str(counts.get("cirTrustAnchorCount", len(rirs))), - "--host", - os.uname().nodename, - "--platform", - sys.platform, - ] - run_local(run_meta_args) - return run_meta_path - def run_remote_step( ssh_target: str, @@ -686,6 +575,9 @@ def run_remote_step( ) if side["protocol"] == "rsync-only": argv.append("--disable-rrdp") + rsync_scope = side.get("rsyncScope") + if rsync_scope: + argv.extend(["--rsync-scope", str(rsync_scope)]) if side["mode"] == "strict-name": argv.extend(["--strict", "name"]) elif side["mode"] == "strict-cms-der": @@ -714,8 +606,6 @@ def run_remote_step( str(run_dir / "vrps.csv"), "--vaps-csv-out", str(run_dir / "vaps.csv"), - "--compare-view-trust-anchor", - compare_view_trust_anchor(rirs), ] ) else: @@ -880,7 +770,7 @@ def generate_run_meta( "--rp-mode", side["mode"], "--protocol-mode", - side["protocol"], + side["protocol"] + (f"/rsync-scope:{side['rsyncScope']}" if side.get("rsyncScope") else ""), "--strict-policies", strict_policies, "--rirs", @@ -1046,20 +936,14 @@ def run_experiment( str(local_exp_root / "A" / step / "result.ccr"), "--left-cir", str(local_exp_root / "A" / step / "result.cir"), - "--left-meta", - str(local_exp_root / "A" / step / "run-meta.json"), "--right-ccr", str(local_exp_root / "B" / step / "result.ccr"), "--right-cir", str(local_exp_root / "B" / step / "result.cir"), - "--right-meta", - str(local_exp_root / "B" / step / "run-meta.json"), "--out-dir", str(compare_dir), "--sample-limit", "200", - "--compare-view-trust-anchor", - compare_view_trust_anchor(rirs), ] run_local(triage_cmd, cwd=local_exp_root) diff --git a/src/bin/ccr_state_compare.rs b/src/bin/ccr_state_compare.rs index d9b5f68..9795a05 100644 --- a/src/bin/ccr_state_compare.rs +++ b/src/bin/ccr_state_compare.rs @@ -303,9 +303,9 @@ fn build_compare_view_fallback( fn decode_views( content_info: &CcrContentInfo, - trust_anchor: &str, + _trust_anchor: &str, ) -> Result<(BTreeSet, BTreeSet), String> { - decode_ccr_compare_views(content_info, trust_anchor) + decode_ccr_compare_views(content_info) } fn compare_sets(ours: &BTreeSet, rpki_client: &BTreeSet) -> SetSummary { diff --git a/src/bin/ccr_to_compare_views.rs b/src/bin/ccr_to_compare_views.rs index 7dbb138..4e865d9 100644 --- a/src/bin/ccr_to_compare_views.rs +++ b/src/bin/ccr_to_compare_views.rs @@ -6,18 +6,14 @@ struct Args { ccr_path: Option, vrps_out_path: Option, vaps_out_path: Option, - trust_anchor: String, } fn usage() -> &'static str { - "Usage: ccr_to_compare_views --ccr --vrps-out --vaps-out [--trust-anchor ]" + "Usage: ccr_to_compare_views --ccr --vrps-out --vaps-out [--trust-anchor ]" } fn parse_args(argv: &[String]) -> Result { - let mut args = Args { - trust_anchor: "unknown".to_string(), - ..Args::default() - }; + let mut args = Args::default(); let mut i = 1usize; while i < argv.len() { match argv[i].as_str() { @@ -38,8 +34,7 @@ fn parse_args(argv: &[String]) -> Result { } "--trust-anchor" => { i += 1; - let v = argv.get(i).ok_or("--trust-anchor requires a value")?; - args.trust_anchor = v.clone(); + argv.get(i).ok_or("--trust-anchor requires a value")?; } "-h" | "--help" => return Err(usage().to_string()), other => return Err(format!("unknown argument: {other}\n{}", usage())), @@ -64,8 +59,7 @@ fn main() -> Result<(), String> { let bytes = std::fs::read(ccr_path) .map_err(|e| format!("read ccr failed: {}: {e}", ccr_path.display()))?; let content_info = decode_content_info(&bytes).map_err(|e| e.to_string())?; - let (vrps, vaps) = - decode_ccr_compare_views(&content_info, &args.trust_anchor).map_err(|e| e.to_string())?; + let (vrps, vaps) = decode_ccr_compare_views(&content_info).map_err(|e| e.to_string())?; write_vrp_csv(args.vrps_out_path.as_ref().unwrap(), &vrps)?; write_vap_csv(args.vaps_out_path.as_ref().unwrap(), &vaps)?; println!( @@ -106,7 +100,6 @@ mod tests { args.vaps_out_path.as_deref(), Some(std::path::Path::new("vaps.csv")) ); - assert_eq!(args.trust_anchor, "apnic"); } #[test] diff --git a/src/bin/ccr_to_routinator_csv.rs b/src/bin/ccr_to_routinator_csv.rs index 8fcb25c..5e29239 100644 --- a/src/bin/ccr_to_routinator_csv.rs +++ b/src/bin/ccr_to_routinator_csv.rs @@ -5,18 +5,14 @@ use std::io::Write; struct Args { ccr_path: Option, out_path: Option, - trust_anchor: String, } fn usage() -> &'static str { - "Usage: ccr_to_routinator_csv --ccr --out [--trust-anchor ]" + "Usage: ccr_to_routinator_csv --ccr --out [--trust-anchor ]" } fn parse_args(argv: &[String]) -> Result { - let mut args = Args { - trust_anchor: "unknown".to_string(), - ..Args::default() - }; + let mut args = Args::default(); let mut i = 1usize; while i < argv.len() { match argv[i].as_str() { @@ -32,8 +28,7 @@ fn parse_args(argv: &[String]) -> Result { } "--trust-anchor" => { i += 1; - let v = argv.get(i).ok_or("--trust-anchor requires a value")?; - args.trust_anchor = v.clone(); + argv.get(i).ok_or("--trust-anchor requires a value")?; } "-h" | "--help" => return Err(usage().to_string()), other => return Err(format!("unknown argument: {other}\n{}", usage())), @@ -74,8 +69,7 @@ fn main() -> Result<(), String> { ); writeln!(file, "ASN,IP Prefix,Max Length,Trust Anchor").map_err(|e| e.to_string())?; for (asn, prefix, max_len) in rows { - writeln!(file, "AS{asn},{prefix},{max_len},{}", args.trust_anchor) - .map_err(|e| e.to_string())?; + writeln!(file, "AS{asn},{prefix},{max_len},unknown").map_err(|e| e.to_string())?; } println!("{}", out_path.display()); Ok(()) @@ -105,7 +99,6 @@ mod tests { args.out_path.as_deref(), Some(std::path::Path::new("out.csv")) ); - assert_eq!(args.trust_anchor, "apnic"); } #[test] diff --git a/src/bin/cir_drop_report.rs b/src/bin/cir_drop_report.rs index b793030..d15f811 100644 --- a/src/bin/cir_drop_report.rs +++ b/src/bin/cir_drop_report.rs @@ -117,8 +117,8 @@ fn main() -> Result<(), String> { &std::fs::read(&ccr_path).map_err(|e| format!("read ccr failed: {e}"))?, ) .map_err(|e| format!("decode ccr failed: {e}"))?; - let (vrps, vaps) = decode_ccr_compare_views(&ccr, "unknown") - .map_err(|e| format!("decode compare views failed: {e}"))?; + let (vrps, vaps) = + decode_ccr_compare_views(&ccr).map_err(|e| format!("decode compare views failed: {e}"))?; let report: serde_json::Value = serde_json::from_slice( &std::fs::read(&report_path).map_err(|e| format!("read report failed: {e}"))?, ) diff --git a/src/bin/triage_ccr_cir_pair.rs b/src/bin/triage_ccr_cir_pair.rs index f98e116..5bdfa38 100644 --- a/src/bin/triage_ccr_cir_pair.rs +++ b/src/bin/triage_ccr_cir_pair.rs @@ -4,22 +4,20 @@ use std::path::{Path, PathBuf}; use rpki::ccr::{compare_state_digests, decode_content_info}; use rpki::cir::decode_cir; use serde_json::{Value, json}; +use time::format_description::well_known::Rfc3339; #[derive(Debug, PartialEq, Eq)] struct Args { left_ccr: PathBuf, left_cir: PathBuf, - left_meta: PathBuf, right_ccr: PathBuf, right_cir: PathBuf, - right_meta: PathBuf, out_dir: PathBuf, sample_limit: usize, - compare_view_trust_anchor: String, } fn usage() -> &'static str { - "Usage: triage_ccr_cir_pair --left-ccr --left-cir --left-meta --right-ccr --right-cir --right-meta --out-dir [--sample-limit ] [--compare-view-trust-anchor ]" + "Usage: triage_ccr_cir_pair --left-ccr --left-cir --right-ccr --right-cir --out-dir [--sample-limit ]" } fn main() { @@ -37,13 +35,10 @@ fn real_main() -> Result<(), String> { fn parse_args(argv: &[String]) -> Result { let mut left_ccr = None; let mut left_cir = None; - let mut left_meta = None; let mut right_ccr = None; let mut right_cir = None; - let mut right_meta = None; let mut out_dir = None; let mut sample_limit = 200usize; - let mut compare_view_trust_anchor = "unknown".to_string(); let mut index = 1usize; while index < argv.len() { match argv[index].as_str() { @@ -59,12 +54,6 @@ fn parse_args(argv: &[String]) -> Result { argv.get(index).ok_or("--left-cir requires a value")?, )); } - "--left-meta" => { - index += 1; - left_meta = Some(PathBuf::from( - argv.get(index).ok_or("--left-meta requires a value")?, - )); - } "--right-ccr" => { index += 1; right_ccr = Some(PathBuf::from( @@ -77,12 +66,6 @@ fn parse_args(argv: &[String]) -> Result { argv.get(index).ok_or("--right-cir requires a value")?, )); } - "--right-meta" => { - index += 1; - right_meta = Some(PathBuf::from( - argv.get(index).ok_or("--right-meta requires a value")?, - )); - } "--out-dir" => { index += 1; out_dir = Some(PathBuf::from( @@ -96,12 +79,12 @@ fn parse_args(argv: &[String]) -> Result { .parse::() .map_err(|_| format!("invalid --sample-limit: {value}"))?; } - "--compare-view-trust-anchor" => { - index += 1; - compare_view_trust_anchor = argv - .get(index) - .ok_or("--compare-view-trust-anchor requires a value")? - .clone(); + "--left-meta" | "--right-meta" | "--compare-view-trust-anchor" => { + return Err(format!( + "{} is no longer supported by four-file triage\n{}", + argv[index], + usage() + )); } "-h" | "--help" => return Err(usage().to_string()), other => return Err(format!("unknown argument: {other}\n{}", usage())), @@ -111,38 +94,38 @@ fn parse_args(argv: &[String]) -> Result { Ok(Args { left_ccr: left_ccr.ok_or_else(|| format!("--left-ccr is required\n{}", usage()))?, left_cir: left_cir.ok_or_else(|| format!("--left-cir is required\n{}", usage()))?, - left_meta: left_meta.ok_or_else(|| format!("--left-meta is required\n{}", usage()))?, right_ccr: right_ccr.ok_or_else(|| format!("--right-ccr is required\n{}", usage()))?, right_cir: right_cir.ok_or_else(|| format!("--right-cir is required\n{}", usage()))?, - right_meta: right_meta.ok_or_else(|| format!("--right-meta is required\n{}", usage()))?, out_dir: out_dir.ok_or_else(|| format!("--out-dir is required\n{}", usage()))?, sample_limit, - compare_view_trust_anchor, }) } fn run(args: Args) -> Result<(), String> { std::fs::create_dir_all(&args.out_dir) .map_err(|e| format!("create out-dir failed: {}: {e}", args.out_dir.display()))?; - let left_meta = read_json(&args.left_meta)?; - let right_meta = read_json(&args.right_meta)?; let ccr_summary = build_ccr_summary(&args)?; - let cir_summary = build_cir_summary(&args, &left_meta, &right_meta)?; - let input_integrity = build_input_integrity(&args, &left_meta, &right_meta); - let diagnosis = diagnose(&ccr_summary, &cir_summary, &input_integrity); + let cir_summary = build_cir_summary(&args)?; + let findings = build_findings(&ccr_summary, &cir_summary); + let dominant_findings = dominant_findings(&findings); + let all_match = ccr_summary["stateDigestMatch"].as_bool().unwrap_or(false) + && cir_summary["allMatch"].as_bool().unwrap_or(false); let triage = json!({ - "schemaVersion": 1, + "schemaVersion": 2, "generatedBy": "triage_ccr_cir_pair", - "left": side_summary("left", &args.left_ccr, &args.left_cir, &args.left_meta, &left_meta), - "right": side_summary("right", &args.right_ccr, &args.right_cir, &args.right_meta, &right_meta), + "inputContract": "four-file-ccr-cir-only", + "left": side_summary("left", &args.left_ccr, &args.left_cir), + "right": side_summary("right", &args.right_ccr, &args.right_cir), "sampleLimit": args.sample_limit, - "diagnosis": diagnosis, - "primaryDiagnosis": diagnosis, - "allMatch": diagnosis == "same_state", - "inputIntegrity": input_integrity, + "outputRelation": if ccr_summary["stateDigestMatch"].as_bool().unwrap_or(false) { "same" } else { "different" }, + "processRelation": if cir_summary["allMatch"].as_bool().unwrap_or(false) { "same" } else { "different" }, + "allMatch": all_match, + "findings": findings, + "dominantFindings": dominant_findings, "ccr": ccr_summary, "cir": cir_summary, - "manualInvestigationHints": manual_hints(&diagnosis), + "manualInvestigationHints": manual_hints(), + "interpretationLimits": interpretation_limits(), }); write_json(&args.out_dir.join("ccr-summary.json"), &triage["ccr"])?; write_json(&args.out_dir.join("cir-summary.json"), &triage["cir"])?; @@ -157,6 +140,8 @@ fn build_ccr_summary(args: &Args) -> Result { let left = read_file(&args.left_ccr)?; let right = read_file(&args.right_ccr)?; let comparison = compare_state_digests(&left, &right).map_err(|e| e.to_string())?; + let left_content = decode_content_info(&left).map_err(|e| e.to_string())?; + let right_content = decode_content_info(&right).map_err(|e| e.to_string())?; let state_digest_match = comparison.matches(); let mismatched_states = comparison.mismatched_state_names(); let mut mismatched_components = Vec::new(); @@ -167,13 +152,12 @@ fn build_ccr_summary(args: &Args) -> Result { mismatched_components.push("hashAlgorithm".to_string()); } mismatched_components.extend(mismatched_states.iter().map(|item| (*item).to_string())); - let fallback = if state_digest_match { + let compare_views = if state_digest_match { None } else { Some(build_compare_view_summary( - &left, - &right, - &args.compare_view_trust_anchor, + &left_content, + &right_content, args.sample_limit, )?) }; @@ -181,7 +165,7 @@ fn build_ccr_summary(args: &Args) -> Result { "stateDigestMatch": state_digest_match, "comparePath": if state_digest_match { "ccr_state_digest_match" - } else if fallback.as_ref().is_some_and(|item| item.vrps_match && item.vaps_match) { + } else if compare_views.as_ref().is_some_and(|item| item.vrps_match && item.vaps_match) { "ccr_state_digest_mismatch_with_compare_views_match" } else { "ccr_state_digest_mismatch_with_set_diff" @@ -193,10 +177,12 @@ fn build_ccr_summary(args: &Args) -> Result { "left": { "version": comparison.ours.version, "hashAlg": comparison.ours.hash_alg_oid, + "producedAt": format_time(left_content.content.produced_at)?, }, "right": { "version": comparison.peer.version, "hashAlg": comparison.peer.hash_alg_oid, + "producedAt": format_time(right_content.content.produced_at)?, }, "states": comparison.states.iter().map(|state| json!({ "name": state.name, @@ -206,7 +192,7 @@ fn build_ccr_summary(args: &Args) -> Result { "leftHash": state.ours_hash_hex, "rightHash": state.peer_hash_hex, })).collect::>(), - "compareViews": fallback.map(CompareViewSummary::to_json), + "compareViews": compare_views.map(CompareViewSummary::to_json), })) } @@ -223,31 +209,67 @@ impl CompareViewSummary { } fn build_compare_view_summary( - left_der: &[u8], - right_der: &[u8], - trust_anchor: &str, + left: &rpki::ccr::CcrContentInfo, + right: &rpki::ccr::CcrContentInfo, sample_limit: usize, ) -> Result { - let left = decode_content_info(left_der).map_err(|e| e.to_string())?; - let right = decode_content_info(right_der).map_err(|e| e.to_string())?; - let (left_vrps, left_vaps) = - rpki::ccr::decode_ccr_compare_views(&left, trust_anchor).map_err(|e| e.to_string())?; - let (right_vrps, right_vaps) = - rpki::ccr::decode_ccr_compare_views(&right, trust_anchor).map_err(|e| e.to_string())?; + let left_vrps = ccr_vrp_rows(left)?; + let right_vrps = ccr_vrp_rows(right)?; + let left_vaps = ccr_vap_rows(left); + let right_vaps = ccr_vap_rows(right); let vrps = compare_sets(&left_vrps, &right_vrps, sample_limit); let vaps = compare_sets(&left_vaps, &right_vaps, sample_limit); Ok(CompareViewSummary { vrps_match: vrps.match_, vaps_match: vaps.match_, json: json!({ - "trustAnchor": trust_anchor, - "vrps": vrps.to_json_with(|row| json!([row.asn, row.ip_prefix, row.max_length, row.trust_anchor])), - "vaps": vaps.to_json_with(|row| json!([row.customer_asn, row.providers, row.trust_anchor])), + "vrps": vrps.to_json_with(|row| json!({ + "asn": row.0, + "prefix": row.1, + "maxLength": row.2, + })), + "vaps": vaps.to_json_with(|row| json!({ + "customerAsn": row.0, + "providers": row.1, + })), }), }) } -fn build_cir_summary(args: &Args, left_meta: &Value, right_meta: &Value) -> Result { +fn ccr_vrp_rows( + content_info: &rpki::ccr::CcrContentInfo, +) -> Result, String> { + Ok(rpki::ccr::extract_vrp_rows(content_info) + .map_err(|e| e.to_string())? + .into_iter() + .map(|(asn, prefix, max_length)| (format!("AS{asn}"), prefix, max_length.to_string())) + .collect()) +} + +fn ccr_vap_rows(content_info: &rpki::ccr::CcrContentInfo) -> BTreeSet<(String, String)> { + content_info + .content + .vaps + .as_ref() + .map(|state| { + state + .aps + .iter() + .map(|vap| { + let providers = vap + .providers + .iter() + .map(|provider| format!("AS{provider}")) + .collect::>() + .join(";"); + (format!("AS{}", vap.customer_as_id), providers) + }) + .collect() + }) + .unwrap_or_default() +} + +fn build_cir_summary(args: &Args) -> Result { let left = decode_cir(&read_file(&args.left_cir)?).map_err(|e| e.to_string())?; let right = decode_cir(&read_file(&args.right_cir)?).map_err(|e| e.to_string())?; let left_objects = left @@ -276,7 +298,11 @@ fn build_cir_summary(args: &Args, left_meta: &Value, right_meta: &Value) -> Resu .map(|item| { ( item.ta_rsync_uri.clone(), - hex::encode(&item.ta_certificate_sha256), + TrustAnchorProjection { + tal_uri: item.tal_uri.clone(), + tal_sha256: hex::encode(rpki::cir::sha256(&item.tal_bytes)), + ta_certificate_sha256: hex::encode(&item.ta_certificate_sha256), + }, ) }) .collect::>(); @@ -286,149 +312,230 @@ fn build_cir_summary(args: &Args, left_meta: &Value, right_meta: &Value) -> Resu .map(|item| { ( item.ta_rsync_uri.clone(), - hex::encode(&item.ta_certificate_sha256), + TrustAnchorProjection { + tal_uri: item.tal_uri.clone(), + tal_sha256: hex::encode(rpki::cir::sha256(&item.tal_bytes)), + ta_certificate_sha256: hex::encode(&item.ta_certificate_sha256), + }, ) }) .collect::>(); let objects = compare_object_maps(&left_objects, &right_objects, args.sample_limit); let rejects = compare_sets(&left_rejects, &right_rejects, args.sample_limit); - let trust_anchors = - compare_object_maps(&left_trust_anchors, &right_trust_anchors, args.sample_limit); - let left_fixture = fixture_ta_map(left_meta); - let right_fixture = fixture_ta_map(right_meta); - let left_fixture_match = trust_anchor_fixture_match(&left_trust_anchors, &left_fixture); - let right_fixture_match = trust_anchor_fixture_match(&right_trust_anchors, &right_fixture); + let trust_anchors = compare_projection_maps( + &left_trust_anchors, + &right_trust_anchors, + args.sample_limit, + |projection| projection.to_json(), + ); let reject_hash_match = left.reject_list_sha256 == right.reject_list_sha256; + let validation_time_match = left.validation_time == right.validation_time; Ok(json!({ - "allMatch": objects.match_ && rejects.match_ && trust_anchors.match_ && reject_hash_match, + "allMatch": objects.match_ && rejects.match_ && trust_anchors.match_ && reject_hash_match && validation_time_match, "objects": objects.to_json(), "rejects": rejects.to_json_with(|item| json!(item)), "trustAnchors": trust_anchors.to_json(), - "trustAnchorFixture": { - "leftMatch": left_fixture_match, - "rightMatch": right_fixture_match, - }, "rejectListSha256Match": reject_hash_match, + "validationTimeMatch": validation_time_match, "left": { "objectCount": left.objects.len(), "rejectCount": left.rejected_objects.len(), "trustAnchorCount": left.trust_anchors.len(), "rejectListSha256": hex::encode(&left.reject_list_sha256), - "validationTime": left.validation_time.to_string(), + "validationTime": format_time(left.validation_time)?, }, "right": { "objectCount": right.objects.len(), "rejectCount": right.rejected_objects.len(), "trustAnchorCount": right.trust_anchors.len(), "rejectListSha256": hex::encode(&right.reject_list_sha256), - "validationTime": right.validation_time.to_string(), + "validationTime": format_time(right.validation_time)?, }, })) } -fn diagnose(ccr: &Value, cir: &Value, integrity: &Value) -> String { - if integrity["metadataValid"].as_bool() == Some(false) { - return "metadata_invalid".to_string(); +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +struct TrustAnchorProjection { + tal_uri: String, + tal_sha256: String, + ta_certificate_sha256: String, +} + +impl TrustAnchorProjection { + fn to_json(&self) -> Value { + json!({ + "talUri": self.tal_uri, + "talSha256": self.tal_sha256, + "taCertificateSha256": self.ta_certificate_sha256, + }) } - if integrity["runExitOk"].as_bool() == Some(false) { - return "run_failed".to_string(); - } - if integrity["taFixturePinned"].as_bool() == Some(false) { - return "ta_fixture_not_pinned".to_string(); - } - if ccr["stateDigestMatch"].as_bool().unwrap_or(false) - && cir["allMatch"].as_bool().unwrap_or(false) - { - return "same_state".to_string(); - } - if cir["trustAnchorFixture"]["leftMatch"].as_bool() == Some(false) - || cir["trustAnchorFixture"]["rightMatch"].as_bool() == Some(false) - { - return "ta_fixture_not_pinned".to_string(); +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct Finding { + code: &'static str, + title: &'static str, + evidence_ref: Vec<&'static str>, + reasoning: &'static str, +} + +impl Finding { + fn to_json(&self) -> Value { + json!({ + "code": self.code, + "title": self.title, + "evidenceRef": self.evidence_ref, + "reasoning": self.reasoning, + }) } +} + +fn build_findings(ccr: &Value, cir: &Value) -> Vec { + let mut findings = Vec::new(); if cir["trustAnchors"]["match"].as_bool() != Some(true) { - return "trust_anchor_input_difference".to_string(); + findings.push( + Finding { + code: "P1_TRUST_ANCHOR_DIFFERENCE", + title: "TrustAnchor projection differs", + evidence_ref: vec!["cir.trustAnchors"], + reasoning: "CIR trustAnchors differ by TA URI or self-contained TAL/TA certificate projection.", + } + .to_json(), + ); } let object_uri_diff = cir["objects"]["onlyInLeftCount"].as_u64().unwrap_or(0) > 0 || cir["objects"]["onlyInRightCount"].as_u64().unwrap_or(0) > 0; if object_uri_diff { - return "sync_input_object_difference".to_string(); + findings.push( + Finding { + code: "P2_OBJECT_URI_SET_DIFFERENCE", + title: "Consumed object URI set differs", + evidence_ref: vec!["cir.objects.onlyInLeft", "cir.objects.onlyInRight"], + reasoning: "CIR objects encode the actual validation input objects consumed by each side; URI set difference indicates different consumed input.", + } + .to_json(), + ); } if cir["objects"]["hashMismatchCount"].as_u64().unwrap_or(0) > 0 { - return "sync_input_object_content_difference".to_string(); + findings.push( + Finding { + code: "P3_OBJECT_CONTENT_HASH_DIFFERENCE", + title: "Consumed object content differs", + evidence_ref: vec!["cir.objects.hashMismatches"], + reasoning: "Both sides consumed the same object URI but recorded different SHA-256 values.", + } + .to_json(), + ); } if cir["rejects"]["match"].as_bool() != Some(true) || cir["rejectListSha256Match"].as_bool() != Some(true) { - return "validation_reject_policy_difference".to_string(); + findings.push( + Finding { + code: "P4_REJECT_DECISION_DIFFERENCE", + title: "Reject decision differs", + evidence_ref: vec!["cir.rejects", "cir.rejectListSha256Match"], + reasoning: "Reject-list comparison is based on sorted rejected object URIs; reject reasons are advisory and not part of the digest.", + } + .to_json(), + ); } - if ccr["stateDigestMatch"].as_bool().unwrap_or(false) { - return "equivalent_output_process_difference".to_string(); + if cir["validationTimeMatch"].as_bool() != Some(true) { + findings.push( + Finding { + code: "P5_VALIDATION_TIME_DIFFERENCE", + title: "Validation time differs", + evidence_ref: vec!["cir.left.validationTime", "cir.right.validationTime"], + reasoning: "Validation time can affect time-window checks; four-file triage reports it as a candidate explanation, not a proven root cause.", + } + .to_json(), + ); } - if ccr["compareViews"]["vrps"]["match"].as_bool() == Some(true) - && ccr["compareViews"]["vaps"]["match"].as_bool() == Some(true) - { - return "ccr_projection_or_encoding_difference".to_string(); + let ccr_match = ccr["stateDigestMatch"].as_bool().unwrap_or(false); + let has_process_findings = !findings.is_empty(); + if !ccr_match && !has_process_findings { + findings.push( + Finding { + code: "P6_CCR_ONLY_DIFFERENCE", + title: "CCR differs without CIR-level explanation", + evidence_ref: vec!["ccr.states", "ccr.compareViews"], + reasoning: "CCR state digest differs, but trust anchors, consumed input, reject decisions, and validation time are equivalent in CIR.", + } + .to_json(), + ); + } else if ccr_match && has_process_findings { + findings.push( + Finding { + code: "P7_EQUIVALENT_OUTPUT_PROCESS_DIFFERENCE", + title: "Final CCR equal but validation process differs", + evidence_ref: vec!["ccr.stateDigestMatch", "cir"], + reasoning: "The final CCR state is equivalent even though CIR shows process/input differences.", + } + .to_json(), + ); + } else if ccr_match && !has_process_findings { + findings.push( + Finding { + code: "P8_ALL_EQUAL", + title: "CCR and CIR are equivalent", + evidence_ref: vec!["ccr.stateDigestMatch", "cir.allMatch"], + reasoning: "CCR state digest and all CIR comparison dimensions match.", + } + .to_json(), + ); } - "unknown_needs_manual_object_analysis".to_string() + findings } -fn manual_hints(diagnosis: &str) -> Vec { - match diagnosis { - "same_state" => vec!["CCR state digest matches; no object-level investigation needed.".into()], - "metadata_invalid" => vec![ - "Run metadata is incomplete or artifact paths do not match the explicit CLI inputs.".into(), - "Fix the experiment driver metadata envelope before interpreting RP behavior.".into(), - ], - "run_failed" => vec![ - "At least one side reported a non-zero run exit code in run-meta.json.".into(), - "Inspect the registered run logs outside standard triage before comparing behavior.".into(), - ], - "ta_fixture_not_pinned" => vec![ - "At least one side reported that the local TA fixture was not pinned.".into(), - "Fix fixture pinning before interpreting CCR/CIR differences.".into(), - ], - "trust_anchor_input_difference" => vec![ - "Compare CIR TrustAnchor entries and fixture-proof TAL/TA hashes.".into(), - "If fixture hashes differ, fix experiment input before analyzing RP behavior.".into(), - ], - "sync_input_object_difference" => vec![ - "Inspect CIR object URI/hash differences first; this points to sync/manifest consumption differences.".into(), - "Use a separate object parser workflow for selected URI samples if manual root cause analysis is needed.".into(), - ], - "sync_input_object_content_difference" => vec![ - "CIR object URI sets match but at least one object hash differs.".into(), - "For live all5 runs this usually points to repository view drift or sync content differences.".into(), - ], - "validation_reject_policy_difference" => vec![ - "Inspect reject-only URI sets; this points to validation policy or parser strictness differences.".into(), - "Reject reasons are advisory and are not part of reject-list digest comparison.".into(), - ], - "ccr_projection_or_encoding_difference" => vec![ - "CIR inputs match but CCR state differs; inspect CCR projection, ordering, or encoding.".into(), - ], - "equivalent_output_process_difference" => vec![ - "CCR state digest matches but CIR process inputs differ.".into(), - "Record this as a behavior difference even though the final CCR state is equivalent.".into(), - ], - _ => vec![ - "CCR and standard CIR triage cannot fully explain this mismatch.".into(), - "Use manual parser tools on selected objects outside the standard triage workflow.".into(), - ], +fn dominant_findings(findings: &[Value]) -> Vec { + let dominant = [ + "P0_ARTIFACT_INVALID", + "P1_TRUST_ANCHOR_DIFFERENCE", + "P2_OBJECT_URI_SET_DIFFERENCE", + "P3_OBJECT_CONTENT_HASH_DIFFERENCE", + "P4_REJECT_DECISION_DIFFERENCE", + "P5_VALIDATION_TIME_DIFFERENCE", + "P6_CCR_ONLY_DIFFERENCE", + "P7_EQUIVALENT_OUTPUT_PROCESS_DIFFERENCE", + "P8_ALL_EQUAL", + ]; + for code in dominant { + let selected = findings + .iter() + .filter(|finding| finding["code"].as_str() == Some(code)) + .cloned() + .collect::>(); + if !selected.is_empty() { + return selected; + } } + Vec::new() } -fn side_summary(label: &str, ccr: &Path, cir: &Path, meta: &Path, meta_json: &Value) -> Value { +fn manual_hints() -> Vec { + vec![ + "If P1 is present, first inspect CIR trustAnchors because different TA inputs can dominate all downstream differences.".into(), + "If P2 or P3 is present, inspect consumed object URI/hash samples; this usually points to sync or repository-view differences.".into(), + "If P4 is present without P2/P3, focus on validation policy, parser strictness, or reject decision differences.".into(), + "If only P5 is present, treat validation time as an observation unless a separate object-level investigation proves time-window impact.".into(), + "If P6 is present, CCR projection, ordering, encoding, or CCR-only state needs manual inspection.".into(), + ] +} + +fn interpretation_limits() -> Vec { + vec![ + "Four-file triage only uses two CCR files and two CIR files; it never reads run metadata, logs, report JSON, cache, mirror, repo-bytes DB, or raw objects.".into(), + "The tool can classify observable CCR/CIR differences but cannot prove low-level object root cause without a separate parser workflow.".into(), + "Reject reasons are preserved as advisory fields in CIR but reject-list equality is defined by sorted rejected object URI set.".into(), + "Validation-time differences are reported as candidate explanations, not as definitive causes.".into(), + ] +} + +fn side_summary(label: &str, ccr: &Path, cir: &Path) -> Value { json!({ "label": label, "ccr": path_string(ccr), "cir": path_string(cir), - "metadata": path_string(meta), - "experimentId": meta_json.get("experimentId"), - "sideLabel": meta_json.get("sideLabel"), - "step": meta_json.get("step"), - "rp": meta_json.get("rp"), - "scope": meta_json.get("scope"), }) } @@ -436,198 +543,29 @@ fn path_string(path: &Path) -> String { path.to_string_lossy().into_owned() } -fn build_input_integrity(args: &Args, left_meta: &Value, right_meta: &Value) -> Value { - let mut issues = Vec::new(); - validate_side_metadata( - "left", - left_meta, - &args.left_ccr, - &args.left_cir, - &args.left_meta, - &mut issues, - ); - validate_side_metadata( - "right", - right_meta, - &args.right_ccr, - &args.right_cir, - &args.right_meta, - &mut issues, - ); - let left_step = left_meta["step"].as_str(); - let right_step = right_meta["step"].as_str(); - if left_step.is_some() && right_step.is_some() && left_step != right_step { - issues.push(format!( - "left/right step mismatch: left={} right={}", - left_step.unwrap_or("-"), - right_step.unwrap_or("-") - )); - } - let run_exit_ok = meta_exit_code(left_meta) == Some(0) && meta_exit_code(right_meta) == Some(0); - let ta_fixture_pinned = match ( - meta_ta_fixture_pinned(left_meta), - meta_ta_fixture_pinned(right_meta), - ) { - (Some(false), _) | (_, Some(false)) => Some(false), - (Some(true), Some(true)) => Some(true), - _ => None, - }; - json!({ - "metadataValid": issues.is_empty(), - "issues": issues, - "runExitOk": run_exit_ok, - "leftExitCode": meta_exit_code(left_meta), - "rightExitCode": meta_exit_code(right_meta), - "sameStep": left_step.is_some() && right_step.is_some() && left_step == right_step, - "step": if left_step == right_step { left_step } else { None }, - "scopeAll5": left_meta["scope"]["all5"].as_bool() == Some(true) - && right_meta["scope"]["all5"].as_bool() == Some(true), - "taFixturePinned": ta_fixture_pinned, - }) -} - -fn validate_side_metadata( - label: &str, - meta: &Value, - ccr: &Path, - cir: &Path, - meta_path: &Path, - issues: &mut Vec, -) { - if meta.get("schemaVersion").is_none() { - issues.push(format!("{label}: missing schemaVersion")); - } - if meta.get("experimentId").is_none() { - issues.push(format!("{label}: missing experimentId")); - } - if meta.get("sideLabel").is_none() { - issues.push(format!("{label}: missing sideLabel")); - } - if meta.get("step").is_none() { - issues.push(format!("{label}: missing step")); - } - if meta.get("scope").is_none() { - issues.push(format!("{label}: missing scope")); - } - validate_artifact_path(label, meta, "ccr", ccr, issues); - validate_artifact_path(label, meta, "cir", cir, issues); - validate_artifact_path(label, meta, "runMeta", meta_path, issues); -} - -fn validate_artifact_path( - label: &str, - meta: &Value, - field: &str, - actual: &Path, - issues: &mut Vec, -) { - let Some(recorded) = meta["artifacts"][field].as_str() else { - issues.push(format!("{label}: missing artifacts.{field}")); - return; - }; - if !paths_match(recorded, actual) { - issues.push(format!( - "{label}: artifacts.{field} does not match CLI input: meta={} cli={}", - recorded, - actual.display() - )); - } -} - -fn paths_match(recorded: &str, actual: &Path) -> bool { - let recorded_path = Path::new(recorded); - if recorded_path == actual || recorded == actual.to_string_lossy() { - return true; - } - if !recorded_path.is_absolute() - && let Ok(actual_real) = std::fs::canonicalize(actual) - { - let recorded_normalized = normalize_relative_path(recorded_path); - if actual_real.ends_with(&recorded_normalized) { - return true; - } - } - match ( - std::fs::canonicalize(recorded_path), - std::fs::canonicalize(actual), - ) { - (Ok(recorded_real), Ok(actual_real)) => recorded_real == actual_real, - _ => false, - } -} - -fn normalize_relative_path(path: &Path) -> PathBuf { - let mut normalized = PathBuf::new(); - for component in path.components() { - match component { - std::path::Component::CurDir => {} - std::path::Component::ParentDir => normalized.push(".."), - std::path::Component::Normal(part) => normalized.push(part), - std::path::Component::RootDir | std::path::Component::Prefix(_) => {} - } - } - normalized -} - -fn meta_exit_code(meta: &Value) -> Option { - meta["metrics"]["exitCode"].as_i64() -} - -fn meta_ta_fixture_pinned(meta: &Value) -> Option { - meta.pointer("/fixtureProof/taFixturePinned") - .and_then(Value::as_bool) - .or_else(|| { - meta.pointer("/fixtureProofSummary/taFixturePinned") - .and_then(Value::as_bool) - }) -} - -fn fixture_ta_map(meta: &Value) -> BTreeMap { - meta.pointer("/fixtureProof/trustAnchors") - .and_then(Value::as_array) - .into_iter() - .flatten() - .filter_map(|item| { - Some(( - item.get("taRsyncUri")?.as_str()?.to_string(), - item.get("taCertificateSha256")? - .as_str()? - .to_ascii_lowercase(), - )) - }) - .collect() -} - -fn trust_anchor_fixture_match( - actual: &BTreeMap, - fixture: &BTreeMap, -) -> Option { - if fixture.is_empty() { - return None; - } - Some(actual.iter().all(|(uri, hash)| { - fixture - .get(uri) - .is_some_and(|fixture_hash| fixture_hash == hash) - })) -} - +#[derive(Clone, Debug)] struct SetSummary { left: usize, right: usize, match_: bool, + only_in_left_count: usize, + only_in_right_count: usize, only_in_left: Vec, only_in_right: Vec, } impl SetSummary { fn build(left: &BTreeSet, right: &BTreeSet, sample_limit: usize) -> Self { + let only_in_left_all = left.difference(right).cloned().collect::>(); + let only_in_right_all = right.difference(left).cloned().collect::>(); Self { left: left.len(), right: right.len(), match_: left == right, - only_in_left: left.difference(right).take(sample_limit).cloned().collect(), - only_in_right: right.difference(left).take(sample_limit).cloned().collect(), + only_in_left_count: only_in_left_all.len(), + only_in_right_count: only_in_right_all.len(), + only_in_left: only_in_left_all.into_iter().take(sample_limit).collect(), + only_in_right: only_in_right_all.into_iter().take(sample_limit).collect(), } } @@ -639,6 +577,8 @@ impl SetSummary { "left": self.left, "right": self.right, "match": self.match_, + "onlyInLeftCount": self.only_in_left_count, + "onlyInRightCount": self.only_in_right_count, "onlyInLeft": self.only_in_left.iter().map(&mut encode).collect::>(), "onlyInRight": self.only_in_right.iter().map(&mut encode).collect::>(), }) @@ -761,6 +701,83 @@ fn compare_object_maps( } } +struct ProjectionSummary { + left: usize, + right: usize, + match_: bool, + only_in_left_count: usize, + only_in_right_count: usize, + value_mismatch_count: usize, + only_in_left: Vec, + only_in_right: Vec, + value_mismatches: Vec, +} + +impl ProjectionSummary { + fn to_json(&self) -> Value { + json!({ + "left": self.left, + "right": self.right, + "match": self.match_, + "onlyInLeftCount": self.only_in_left_count, + "onlyInRightCount": self.only_in_right_count, + "valueMismatchCount": self.value_mismatch_count, + "onlyInLeft": self.only_in_left, + "onlyInRight": self.only_in_right, + "valueMismatches": self.value_mismatches, + }) + } +} + +fn compare_projection_maps( + left: &BTreeMap, + right: &BTreeMap, + sample_limit: usize, + mut encode_value: F, +) -> ProjectionSummary +where + T: Ord + Clone, + F: FnMut(&T) -> Value, +{ + let left_keys = left.keys().cloned().collect::>(); + let right_keys = right.keys().cloned().collect::>(); + let only_in_left_all = left_keys + .difference(&right_keys) + .cloned() + .collect::>(); + let only_in_right_all = right_keys + .difference(&left_keys) + .cloned() + .collect::>(); + let mut mismatches = Vec::new(); + let mut mismatch_count = 0usize; + for key in left_keys.intersection(&right_keys) { + let left_value = left.get(key).expect("key from map"); + let right_value = right.get(key).expect("key from map"); + if left_value != right_value { + mismatch_count += 1; + if mismatches.len() < sample_limit { + mismatches.push(json!({ + "key": key, + "left": encode_value(left_value), + "right": encode_value(right_value), + })); + } + } + } + ProjectionSummary { + left: left.len(), + right: right.len(), + match_: left == right, + only_in_left_count: only_in_left_all.len(), + only_in_right_count: only_in_right_all.len(), + value_mismatch_count: mismatch_count, + only_in_left: only_in_left_all.into_iter().take(sample_limit).collect(), + only_in_right: only_in_right_all.into_iter().take(sample_limit).collect(), + value_mismatches: mismatches, + } +} + fn group_by_extension<'a>(uris: impl IntoIterator) -> BTreeMap { let mut counts = BTreeMap::new(); for uri in uris { @@ -811,13 +828,15 @@ fn uri_extension(uri: &str) -> String { "".to_string() } -fn read_file(path: &Path) -> Result, String> { - std::fs::read(path).map_err(|e| format!("read file failed: {}: {e}", path.display())) +fn format_time(value: time::OffsetDateTime) -> Result { + value + .to_offset(time::UtcOffset::UTC) + .format(&Rfc3339) + .map_err(|e| format!("format RFC3339 time failed: {e}")) } -fn read_json(path: &Path) -> Result { - serde_json::from_slice(&read_file(path)?) - .map_err(|e| format!("decode json failed: {}: {e}", path.display())) +fn read_file(path: &Path) -> Result, String> { + std::fs::read(path).map_err(|e| format!("read file failed: {}: {e}", path.display())) } fn write_json(path: &Path, value: &Value) -> Result<(), String> { @@ -833,17 +852,33 @@ fn write_json(path: &Path, value: &Value) -> Result<(), String> { } fn write_markdown(path: &Path, triage: &Value) -> Result<(), String> { + let codes = triage["findings"] + .as_array() + .into_iter() + .flatten() + .filter_map(|finding| finding["code"].as_str()) + .collect::>() + .join(", "); + let dominant = triage["dominantFindings"] + .as_array() + .into_iter() + .flatten() + .filter_map(|finding| finding["code"].as_str()) + .collect::>() + .join(", "); let lines = vec![ "# CCR/CIR Triage Summary".to_string(), String::new(), format!( - "- `diagnosis`: `{}`", - triage["diagnosis"].as_str().unwrap_or("-") + "- `outputRelation`: `{}`", + triage["outputRelation"].as_str().unwrap_or("-") ), format!( - "- `allMatch`: `{}`", - triage["allMatch"].as_bool().unwrap_or(false) + "- `processRelation`: `{}`", + triage["processRelation"].as_str().unwrap_or("-") ), + format!("- `findings`: `{codes}`"), + format!("- `dominantFindings`: `{dominant}`"), format!( "- `ccrStateDigestMatch`: `{}`", triage["ccr"]["stateDigestMatch"].as_bool().unwrap_or(false) @@ -862,6 +897,12 @@ fn write_markdown(path: &Path, triage: &Value) -> Result<(), String> { .as_bool() .unwrap_or(false) ), + format!( + "- `validationTimeMatch`: `{}`", + triage["cir"]["validationTimeMatch"] + .as_bool() + .unwrap_or(false) + ), ]; std::fs::write(path, lines.join("\n") + "\n") .map_err(|e| format!("write markdown failed: {}: {e}", path.display())) @@ -894,6 +935,21 @@ fn write_samples_jsonl(path: &Path, triage: &Value) -> Result<(), String> { "cir.reject.only_in_right", &triage["cir"]["rejects"]["onlyInRight"], ); + push_rows( + &mut rows, + "cir.trust_anchor.only_in_left", + &triage["cir"]["trustAnchors"]["onlyInLeft"], + ); + push_rows( + &mut rows, + "cir.trust_anchor.only_in_right", + &triage["cir"]["trustAnchors"]["onlyInRight"], + ); + push_rows( + &mut rows, + "cir.trust_anchor.value_mismatch", + &triage["cir"]["trustAnchors"]["valueMismatches"], + ); let mut body = String::new(); for row in rows { body.push_str(&serde_json::to_string(&row).map_err(|e| e.to_string())?); @@ -928,21 +984,17 @@ mod tests { use rpki::validation::objects::{AspaAttestation, Vrp}; #[test] - fn parse_args_accepts_left_right_inputs() { + fn parse_args_accepts_only_four_artifact_inputs() { let args = parse_args(&[ "triage_ccr_cir_pair".to_string(), "--left-ccr".to_string(), "left.ccr".to_string(), "--left-cir".to_string(), "left.cir".to_string(), - "--left-meta".to_string(), - "left.json".to_string(), "--right-ccr".to_string(), "right.ccr".to_string(), "--right-cir".to_string(), "right.cir".to_string(), - "--right-meta".to_string(), - "right.json".to_string(), "--out-dir".to_string(), "out".to_string(), "--sample-limit".to_string(), @@ -955,95 +1007,81 @@ mod tests { } #[test] - fn diagnose_prefers_cir_explanation_order() { - let ccr = json!({ - "stateDigestMatch": false, - "compareViews": { - "vrps": {"match": false}, - "vaps": {"match": true} - } - }); - let integrity = json!({ - "metadataValid": true, - "runExitOk": true, - }); - let mut cir = json!({ + fn parse_args_rejects_removed_meta_and_trust_anchor_flags() { + let err = parse_args(&[ + "triage_ccr_cir_pair".to_string(), + "--left-ccr".to_string(), + "left.ccr".to_string(), + "--left-cir".to_string(), + "left.cir".to_string(), + "--left-meta".to_string(), + "left.json".to_string(), + "--right-ccr".to_string(), + "right.ccr".to_string(), + "--right-cir".to_string(), + "right.cir".to_string(), + "--out-dir".to_string(), + "out".to_string(), + ]) + .expect_err("left-meta must be rejected"); + assert!(err.contains("no longer supported")); + } + + #[test] + fn build_findings_allows_p1_p2_p4_p5_to_coexist() { + let ccr = json!({"stateDigestMatch": false}); + let cir = json!({ "trustAnchors": {"match": false}, "objects": { "match": false, "onlyInLeftCount": 1, - "onlyInRightCount": 0, + "onlyInRightCount": 2, "hashMismatchCount": 0 }, "rejects": {"match": false}, - "rejectListSha256Match": false + "rejectListSha256Match": false, + "validationTimeMatch": false }); + let findings = build_findings(&ccr, &cir); + let codes = finding_codes(&findings); assert_eq!( - diagnose(&ccr, &cir, &integrity), - "trust_anchor_input_difference" + codes, + vec![ + "P1_TRUST_ANCHOR_DIFFERENCE", + "P2_OBJECT_URI_SET_DIFFERENCE", + "P4_REJECT_DECISION_DIFFERENCE", + "P5_VALIDATION_TIME_DIFFERENCE", + ] ); - cir["trustAnchors"]["match"] = json!(true); - assert_eq!( - diagnose(&ccr, &cir, &integrity), - "sync_input_object_difference" - ); - cir["objects"]["onlyInLeftCount"] = json!(0); + } + + #[test] + fn build_findings_covers_ccr_only_equivalent_process_and_all_equal() { + let mut ccr = json!({"stateDigestMatch": false}); + let mut cir = equal_cir_summary_json(); + let findings = build_findings(&ccr, &cir); + assert_eq!(finding_codes(&findings), vec!["P6_CCR_ONLY_DIFFERENCE"]); + + ccr["stateDigestMatch"] = json!(true); cir["objects"]["hashMismatchCount"] = json!(1); + let findings = build_findings(&ccr, &cir); assert_eq!( - diagnose(&ccr, &cir, &integrity), - "sync_input_object_content_difference" + finding_codes(&findings), + vec![ + "P3_OBJECT_CONTENT_HASH_DIFFERENCE", + "P7_EQUIVALENT_OUTPUT_PROCESS_DIFFERENCE", + ] ); - cir["objects"]["match"] = json!(true); + cir["objects"]["hashMismatchCount"] = json!(0); assert_eq!( - diagnose(&ccr, &cir, &integrity), - "validation_reject_policy_difference" - ); - cir["rejects"]["match"] = json!(true); - cir["rejectListSha256Match"] = json!(true); - assert_eq!( - diagnose(&ccr, &cir, &integrity), - "unknown_needs_manual_object_analysis" + finding_codes(&build_findings(&ccr, &cir)), + vec!["P8_ALL_EQUAL"] ); } #[test] - fn diagnose_covers_same_state_and_ccr_only_paths() { - let integrity = json!({ - "metadataValid": true, - "runExitOk": true, - }); - let mut ccr = json!({ - "stateDigestMatch": true, - "compareViews": null - }); - let cir = json!({ - "allMatch": true, - "trustAnchors": {"match": true}, - "objects": { - "match": true, - "onlyInLeftCount": 0, - "onlyInRightCount": 0, - "hashMismatchCount": 0 - }, - "rejects": {"match": true}, - "rejectListSha256Match": true - }); - assert_eq!(diagnose(&ccr, &cir, &integrity), "same_state"); - - ccr["stateDigestMatch"] = json!(false); - ccr["compareViews"] = json!({ - "vrps": {"match": true}, - "vaps": {"match": true} - }); - assert_eq!( - diagnose(&ccr, &cir, &integrity), - "ccr_projection_or_encoding_difference" - ); - } - - #[test] - fn run_reports_same_state_with_fixture_files() { + fn run_reports_same_state_with_four_files() { let temp = tempfile::tempdir().expect("tempdir"); let left = write_side_bundle( temp.path(), @@ -1062,21 +1100,23 @@ mod tests { run(Args { left_ccr: left.0, left_cir: left.1, - left_meta: left.2, right_ccr: right.0, right_cir: right.1, - right_meta: right.2, out_dir: out.clone(), sample_limit: 20, - compare_view_trust_anchor: "test".to_string(), }) .expect("run"); let triage: Value = serde_json::from_slice(&std::fs::read(out.join("triage.json")).expect("read triage")) .expect("triage json"); - assert_eq!(triage["primaryDiagnosis"], "same_state"); + assert_eq!(triage["schemaVersion"], 2); + assert_eq!(triage["inputContract"], "four-file-ccr-cir-only"); assert_eq!(triage["allMatch"], true); + assert_eq!( + finding_codes_from_value(&triage["findings"]), + vec!["P8_ALL_EQUAL"] + ); assert!(out.join("ccr-summary.json").exists()); assert!(out.join("cir-summary.json").exists()); assert!(out.join("diff-samples.jsonl").exists()); @@ -1102,13 +1142,10 @@ mod tests { run(Args { left_ccr: left.0, left_cir: left.1, - left_meta: left.2, right_ccr: right.0, right_cir: right.1, - right_meta: right.2, out_dir: out.clone(), sample_limit: 20, - compare_view_trust_anchor: "test".to_string(), }) .expect("run"); @@ -1116,8 +1153,8 @@ mod tests { serde_json::from_slice(&std::fs::read(out.join("triage.json")).expect("read triage")) .expect("triage json"); assert_eq!( - triage["primaryDiagnosis"], - "ccr_projection_or_encoding_difference" + finding_codes_from_value(&triage["findings"]), + vec!["P6_CCR_ONLY_DIFFERENCE"] ); assert_eq!( triage["ccr"]["comparePath"], @@ -1126,64 +1163,46 @@ mod tests { } #[test] - fn run_reports_trust_anchor_object_and_reject_differences() { + fn run_reports_trust_anchor_object_reject_and_validation_time_differences() { let temp = tempfile::tempdir().expect("tempdir"); let ccr = sample_ccr(64496, None); - - let ta_left = write_side_bundle( + let left = write_side_bundle( temp.path(), - "ta-left", + "left", &ccr, - sample_cir_with_ta(0x11, None, sample_trust_anchor("ta-a")), - ); - let ta_right = write_side_bundle( - temp.path(), - "ta-right", - &ccr, - sample_cir_with_ta(0x11, None, sample_trust_anchor("ta-b")), - ); - assert_eq!( - run_and_read_diagnosis(temp.path(), "ta", ta_left, ta_right), - "trust_anchor_input_difference" - ); - - let object_left = - write_side_bundle(temp.path(), "obj-left", &ccr, sample_cir(0x11, None, "ta")); - let object_right = - write_side_bundle(temp.path(), "obj-right", &ccr, sample_cir(0x22, None, "ta")); - assert_eq!( - run_and_read_diagnosis(temp.path(), "object", object_left, object_right), - "sync_input_object_content_difference" - ); - - let reject_left = write_side_bundle( - temp.path(), - "rej-left", - &ccr, - sample_cir( + sample_cir_with_ta_time( 0x11, Some("rsync://example.net/repo/rejected-left.roa"), - "ta", + sample_trust_anchor("ta-a"), + time::OffsetDateTime::UNIX_EPOCH, ), ); - let reject_right = write_side_bundle( + let right = write_side_bundle( temp.path(), - "rej-right", + "right", &ccr, - sample_cir( - 0x11, + sample_cir_with_ta_time( + 0x22, Some("rsync://example.net/repo/rejected-right.roa"), - "ta", + sample_trust_anchor("ta-b"), + time::OffsetDateTime::UNIX_EPOCH + time::Duration::seconds(60), ), ); + let codes = run_and_read_finding_codes(temp.path(), "multi", left, right); assert_eq!( - run_and_read_diagnosis(temp.path(), "reject", reject_left, reject_right), - "validation_reject_policy_difference" + codes, + vec![ + "P1_TRUST_ANCHOR_DIFFERENCE", + "P3_OBJECT_CONTENT_HASH_DIFFERENCE", + "P4_REJECT_DECISION_DIFFERENCE", + "P5_VALIDATION_TIME_DIFFERENCE", + "P7_EQUIVALENT_OUTPUT_PROCESS_DIFFERENCE", + ] ); } #[test] - fn trust_anchor_compare_uses_fixture_ta_hash_not_tal_uri() { + fn trust_anchor_projection_is_self_contained() { let temp = tempfile::tempdir().expect("tempdir"); let ccr = sample_ccr(64496, None); let left_ta = sample_trust_anchor("ta"); @@ -1202,37 +1221,14 @@ mod tests { sample_cir_with_ta(0x11, None, right_ta), ); assert_eq!( - run_and_read_diagnosis(temp.path(), "ta-uri", left, right), - "same_state" + run_and_read_finding_codes(temp.path(), "ta-uri", left, right), + vec![ + "P1_TRUST_ANCHOR_DIFFERENCE", + "P7_EQUIVALENT_OUTPUT_PROCESS_DIFFERENCE", + ] ); } - #[test] - fn fixture_match_allows_strict_policy_to_drop_trust_anchor() { - let mut fixture = BTreeMap::new(); - fixture.insert( - "rsync://example.net/repo/ta-a.cer".to_string(), - "aaaaaaaa".to_string(), - ); - fixture.insert( - "rsync://example.net/repo/ta-b.cer".to_string(), - "bbbbbbbb".to_string(), - ); - let mut actual = BTreeMap::new(); - actual.insert( - "rsync://example.net/repo/ta-a.cer".to_string(), - "aaaaaaaa".to_string(), - ); - - assert_eq!(trust_anchor_fixture_match(&actual, &fixture), Some(true)); - - actual.insert( - "rsync://example.net/repo/ta-online.cer".to_string(), - "cccccccc".to_string(), - ); - assert_eq!(trust_anchor_fixture_match(&actual, &fixture), Some(false)); - } - #[test] fn grouping_helpers_are_stable() { let uris = [ @@ -1249,29 +1245,63 @@ mod tests { assert_eq!(hosts[0]["count"], 2); } - fn run_and_read_diagnosis( + fn equal_cir_summary_json() -> Value { + json!({ + "allMatch": true, + "trustAnchors": {"match": true}, + "objects": { + "match": true, + "onlyInLeftCount": 0, + "onlyInRightCount": 0, + "hashMismatchCount": 0 + }, + "rejects": {"match": true}, + "rejectListSha256Match": true, + "validationTimeMatch": true + }) + } + + fn finding_codes(findings: &[Value]) -> Vec<&str> { + findings + .iter() + .map(|finding| finding["code"].as_str().unwrap()) + .collect() + } + + fn finding_codes_from_value(findings: &Value) -> Vec<&str> { + findings + .as_array() + .unwrap() + .iter() + .map(|finding| finding["code"].as_str().unwrap()) + .collect() + } + + fn run_and_read_finding_codes( root: &Path, name: &str, - left: (PathBuf, PathBuf, PathBuf), - right: (PathBuf, PathBuf, PathBuf), - ) -> String { + left: (PathBuf, PathBuf), + right: (PathBuf, PathBuf), + ) -> Vec { let out = root.join(format!("compare-{name}")); run(Args { left_ccr: left.0, left_cir: left.1, - left_meta: left.2, right_ccr: right.0, right_cir: right.1, - right_meta: right.2, out_dir: out.clone(), sample_limit: 20, - compare_view_trust_anchor: "test".to_string(), }) .expect("run"); let triage: Value = serde_json::from_slice(&std::fs::read(out.join("triage.json")).expect("read triage")) .expect("triage json"); - triage["primaryDiagnosis"].as_str().unwrap().to_string() + triage["findings"] + .as_array() + .unwrap() + .iter() + .map(|finding| finding["code"].as_str().unwrap().to_string()) + .collect() } fn write_side_bundle( @@ -1279,36 +1309,14 @@ mod tests { label: &str, ccr: &[u8], cir: CanonicalInputRepresentation, - ) -> (PathBuf, PathBuf, PathBuf) { + ) -> (PathBuf, PathBuf) { let dir = root.join(label); std::fs::create_dir_all(&dir).expect("side dir"); let ccr_path = dir.join("result.ccr"); let cir_path = dir.join("result.cir"); - let meta_path = dir.join("run-meta.json"); std::fs::write(&ccr_path, ccr).expect("write ccr"); std::fs::write(&cir_path, encode_cir(&cir).expect("encode cir")).expect("write cir"); - let fixture_proof = fixture_proof_for_cir(&cir); - let meta = json!({ - "schemaVersion": 1, - "experimentId": "test", - "sideLabel": label, - "step": "snapshot", - "scope": {"rirs": ["apnic"], "all5": false}, - "rp": {"kind": "test", "mode": "standard"}, - "metrics": {"exitCode": 0}, - "fixtureProof": fixture_proof, - "artifacts": { - "ccr": ccr_path.to_string_lossy(), - "cir": cir_path.to_string_lossy(), - "runMeta": meta_path.to_string_lossy() - } - }); - std::fs::write( - &meta_path, - serde_json::to_vec_pretty(&meta).expect("encode meta"), - ) - .expect("write meta"); - (ccr_path, cir_path, meta_path) + (ccr_path, cir_path) } fn sample_ccr(asn: u32, manifest_hash_fill: Option) -> Vec { @@ -1361,6 +1369,20 @@ mod tests { object_hash_fill: u8, rejected_uri: Option<&str>, trust_anchor: CirTrustAnchor, + ) -> CanonicalInputRepresentation { + sample_cir_with_ta_time( + object_hash_fill, + rejected_uri, + trust_anchor, + time::OffsetDateTime::UNIX_EPOCH, + ) + } + + fn sample_cir_with_ta_time( + object_hash_fill: u8, + rejected_uri: Option<&str>, + trust_anchor: CirTrustAnchor, + validation_time: time::OffsetDateTime, ) -> CanonicalInputRepresentation { let rejected_objects = rejected_uri .map(|uri| { @@ -1373,7 +1395,7 @@ mod tests { CanonicalInputRepresentation { version: CIR_VERSION_V3, hash_alg: CirHashAlgorithm::Sha256, - validation_time: time::OffsetDateTime::UNIX_EPOCH, + validation_time, objects: vec![CirObject { rsync_uri: "rsync://example.net/repo/a.roa".to_string(), sha256: vec![object_hash_fill; 32], @@ -1398,15 +1420,4 @@ mod tests { ta_certificate_der, } } - - fn fixture_proof_for_cir(cir: &CanonicalInputRepresentation) -> Value { - json!({ - "schemaVersion": 1, - "taFixturePinned": true, - "trustAnchors": cir.trust_anchors.iter().map(|item| json!({ - "taRsyncUri": item.ta_rsync_uri, - "taCertificateSha256": hex::encode(&item.ta_certificate_sha256), - })).collect::>(), - }) - } } diff --git a/src/ccr/compare_view.rs b/src/ccr/compare_view.rs index c0ca73a..b424f49 100644 --- a/src/ccr/compare_view.rs +++ b/src/ccr/compare_view.rs @@ -50,21 +50,18 @@ pub fn canonical_vrp_prefix(prefix: &crate::data_model::roa::IpPrefix) -> String } } -pub fn build_vrp_compare_rows(vrps: &[Vrp], trust_anchor: &str) -> BTreeSet { +pub fn build_vrp_compare_rows(vrps: &[Vrp]) -> BTreeSet { vrps.iter() .map(|vrp| VrpCompareRow { asn: normalize_asn(vrp.asn), ip_prefix: canonical_vrp_prefix(&vrp.prefix), max_length: vrp.max_length.to_string(), - trust_anchor: trust_anchor.to_ascii_lowercase(), + trust_anchor: "unknown".to_string(), }) .collect() } -pub fn build_vap_compare_rows( - aspas: &[AspaAttestation], - trust_anchor: &str, -) -> BTreeSet { +pub fn build_vap_compare_rows(aspas: &[AspaAttestation]) -> BTreeSet { aspas .iter() .map(|aspa| { @@ -78,7 +75,7 @@ pub fn build_vap_compare_rows( .map(normalize_asn) .collect::>() .join(";"), - trust_anchor: trust_anchor.to_ascii_lowercase(), + trust_anchor: "unknown".to_string(), } }) .collect() @@ -86,7 +83,6 @@ pub fn build_vap_compare_rows( pub fn decode_ccr_compare_views( content_info: &CcrContentInfo, - trust_anchor: &str, ) -> Result<(BTreeSet, BTreeSet), String> { let vrps = extract_vrp_rows(content_info) .map_err(|e| format!("extract vrp rows from ccr failed: {e}"))? @@ -95,7 +91,7 @@ pub fn decode_ccr_compare_views( asn: normalize_asn(asn), ip_prefix: prefix, max_length: max_length.to_string(), - trust_anchor: trust_anchor.to_ascii_lowercase(), + trust_anchor: "unknown".to_string(), }) .collect::>(); @@ -116,7 +112,7 @@ pub fn decode_ccr_compare_views( .map(normalize_asn) .collect::>() .join(";"), - trust_anchor: trust_anchor.to_ascii_lowercase(), + trust_anchor: "unknown".to_string(), }) .collect::>() }) @@ -178,17 +174,14 @@ mod tests { #[test] fn build_vap_compare_rows_sorts_and_dedups_providers() { - let rows = build_vap_compare_rows( - &[AspaAttestation { - customer_as_id: 64496, - provider_as_ids: vec![64498, 64497, 64498], - }], - "APNIC", - ); + let rows = build_vap_compare_rows(&[AspaAttestation { + customer_as_id: 64496, + provider_as_ids: vec![64498, 64497, 64498], + }]); let row = rows.iter().next().expect("one row"); assert_eq!(row.customer_asn, "AS64496"); assert_eq!(row.providers, "AS64497;AS64498"); - assert_eq!(row.trust_anchor, "apnic"); + assert_eq!(row.trust_anchor, "unknown"); } #[test] @@ -219,7 +212,7 @@ mod tests { rks: None, }); let (vrp_rows, vap_rows) = - decode_ccr_compare_views(&content, "apnic").expect("decode compare views"); + decode_ccr_compare_views(&content).expect("decode compare views"); assert_eq!(vrp_rows.len(), 1); assert_eq!(vap_rows.len(), 1); assert_eq!(vap_rows.iter().next().unwrap().providers, "AS64497"); @@ -227,18 +220,15 @@ mod tests { #[test] fn build_vrp_compare_rows_canonicalizes_ipv6_prefix_text() { - let rows = build_vrp_compare_rows( - &[Vrp { - asn: 64496, - prefix: IpPrefix { - afi: RoaAfi::Ipv6, - prefix_len: 32, - addr: [0x20, 0x01, 0x0d, 0xb8, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], - }, - max_length: 48, - }], - "APNIC", - ); + let rows = build_vrp_compare_rows(&[Vrp { + asn: 64496, + prefix: IpPrefix { + afi: RoaAfi::Ipv6, + prefix_len: 32, + addr: [0x20, 0x01, 0x0d, 0xb8, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], + }, + max_length: 48, + }]); let row = rows.iter().next().expect("row"); assert_eq!(row.ip_prefix, "2001:db8::/32"); } diff --git a/tests/test_ccr_tools_m7.rs b/tests/test_ccr_tools_m7.rs index d96e2b8..a372e39 100644 --- a/tests/test_ccr_tools_m7.rs +++ b/tests/test_ccr_tools_m7.rs @@ -123,7 +123,7 @@ fn ccr_to_routinator_csv_binary_writes_vrp_csv() { ); let csv = std::fs::read_to_string(csv_path).expect("read csv"); assert!(csv.contains("ASN,IP Prefix,Max Length,Trust Anchor")); - assert!(csv.contains("AS64496,203.0.113.0/24,24,apnic")); + assert!(csv.contains("AS64496,203.0.113.0/24,24,unknown")); } #[test] @@ -193,9 +193,9 @@ fn ccr_to_compare_views_binary_writes_vrp_and_vap_csvs() { let vrps_csv = std::fs::read_to_string(vrps_path).expect("read vrps csv"); let vaps_csv = std::fs::read_to_string(vaps_path).expect("read vaps csv"); assert!(vrps_csv.contains("ASN,IP Prefix,Max Length,Trust Anchor")); - assert!(vrps_csv.contains("AS64496,198.51.100.0/24,24,apnic")); + assert!(vrps_csv.contains("AS64496,198.51.100.0/24,24,unknown")); assert!(vaps_csv.contains("Customer ASN,Providers,Trust Anchor")); - assert!(vaps_csv.contains("AS64496,AS64497;AS64498,apnic")); + assert!(vaps_csv.contains("AS64496,AS64497;AS64498,unknown")); } #[test] diff --git a/tests/test_multi_tal_parallel_m2.rs b/tests/test_multi_tal_parallel_m2.rs index fa050f9..197e3f3 100644 --- a/tests/test_multi_tal_parallel_m2.rs +++ b/tests/test_multi_tal_parallel_m2.rs @@ -57,7 +57,7 @@ fn run_case( .expect("parse report"); let ccr = rpki::ccr::decode_content_info(&std::fs::read(&ccr_path).expect("read ccr")) .expect("decode ccr"); - let (vrps, vaps) = decode_ccr_compare_views(&ccr, "multi").expect("compare views"); + let (vrps, vaps) = decode_ccr_compare_views(&ccr).expect("compare views"); (report, vrps, vaps, ccr) } diff --git a/tests/test_parallel_phase1_transport_offline_r5.rs b/tests/test_parallel_phase1_transport_offline_r5.rs index 46c6d6e..589a6b0 100644 --- a/tests/test_parallel_phase1_transport_offline_r5.rs +++ b/tests/test_parallel_phase1_transport_offline_r5.rs @@ -64,10 +64,9 @@ fn offline_default_parallel_and_configured_phase2_match_compare_views() { rpki::ccr::decode_content_info(&configured_ccr_bytes).expect("decode configured ccr"); let (default_vrps, default_vaps) = - rpki::ccr::decode_ccr_compare_views(&default_ccr, "apnic").expect("default compare view"); + rpki::ccr::decode_ccr_compare_views(&default_ccr).expect("default compare view"); let (configured_vrps, configured_vaps) = - rpki::ccr::decode_ccr_compare_views(&configured_ccr, "apnic") - .expect("configured compare view"); + rpki::ccr::decode_ccr_compare_views(&configured_ccr).expect("configured compare view"); assert_eq!( default_vrps, configured_vrps, @@ -95,7 +94,7 @@ fn offline_default_parallel_and_configured_phase2_match_compare_views() { fn offline_default_parallel_emits_online_ccr_accumulator_output() { let (report, ccr_bytes) = run_offline_case(None); let ccr = rpki::ccr::decode_content_info(&ccr_bytes).expect("decode ccr"); - let (_vrps, _vaps) = rpki::ccr::decode_ccr_compare_views(&ccr, "apnic").expect("compare view"); + let (_vrps, _vaps) = rpki::ccr::decode_ccr_compare_views(&ccr).expect("compare view"); assert!( report["publication_points"] .as_array()