use super::*; use crate::memory_telemetry::{ MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot, }; use crate::storage::{ RocksDbMemorySnapshot, RocksDbMemoryTotals, VcirCcrProjectionSizeBreakdown, VcirChildResourceSizeBreakdown, VcirCoreFieldSizeBreakdown, VcirFieldSizeBreakdown, VcirStorageEntrySummary, VcirStorageSummary, }; #[test] fn parse_help_returns_usage() { let argv = vec!["rpki".to_string(), "--help".to_string()]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("Usage:"), "{err}"); assert!(err.contains("--db"), "{err}"); assert!(err.contains("--rsync-mirror-root"), "{err}"); assert!(err.contains("--rsync-scope"), "{err}"); assert!(err.contains("--parallel-phase2-object-workers"), "{err}"); assert!(err.contains("--memory-trim-after-validation"), "{err}"); assert!(err.contains("--enable-roa-validation-cache"), "{err}"); assert!(!err.contains("--parallel-phase1"), "{err}"); assert!(!err.contains("--parallel-phase2 "), "{err}"); } #[test] fn parse_rejects_unknown_argument() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--nope".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("unknown argument"), "{err}"); } #[test] fn parse_rejects_both_tal_url_and_tal_path() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--tal-path".to_string(), "x.tal".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!( err.contains("one-or-more --tal-url or one-or-more --tal-path/--ta-path pairs"), "{err}" ); } #[test] fn parse_rejects_invalid_max_depth() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--max-depth".to_string(), "nope".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("invalid --max-depth"), "{err}"); } #[test] fn parse_accepts_ccr_out_path() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--rsync-local-dir".to_string(), "repo".to_string(), "--ccr-out".to_string(), "out/example.ccr".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!( args.ccr_out_path.as_deref(), Some(std::path::Path::new("out/example.ccr")) ); } #[test] fn parse_accepts_memory_trim_after_validation() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--memory-trim-after-validation".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.memory_trim_after_validation); } #[test] fn parse_disables_memory_trim_after_validation_by_default() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(!args.memory_trim_after_validation); } #[test] fn parse_accepts_enable_roa_validation_cache() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--enable-roa-validation-cache".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.enable_roa_validation_cache); } #[test] fn parse_accepts_enable_transport_request_prefetch() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--enable-transport-request-prefetch".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.enable_transport_request_prefetch); } #[test] fn parse_disables_roa_validation_cache_by_default() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(!args.enable_roa_validation_cache); assert!(!args.enable_transport_request_prefetch); } #[test] fn parse_accepts_analysis_out_and_implies_analyze() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--analysis-out".to_string(), "run/analyze".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.analyze); assert_eq!( args.analysis_out_path.as_deref(), Some(std::path::Path::new("run/analyze")) ); } #[test] fn parse_accepts_report_json_compact_when_report_json_is_set() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--report-json".to_string(), "out/report.json".to_string(), "--report-json-compact".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!( args.report_json_path.as_deref(), Some(std::path::Path::new("out/report.json")) ); assert!(args.report_json_compact); } #[test] fn parse_accepts_rsync_scope_policy() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--rsync-scope".to_string(), "module-root".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!(args.rsync_scope_policy, RsyncScopePolicy::ModuleRoot); } #[test] fn parse_accepts_host_rsync_scope_policy() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--rsync-scope".to_string(), "host".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!(args.rsync_scope_policy, RsyncScopePolicy::Host); } #[test] fn parse_rejects_invalid_rsync_scope_policy() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--rsync-scope".to_string(), "wide".to_string(), ]; let err = parse_args(&argv).expect_err("invalid rsync scope should fail"); assert!(err.contains("invalid --rsync-scope"), "{err}"); } #[test] fn parse_accepts_strict_policy_list() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--strict".to_string(), "name,cms-der".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!( args.strict_policy, Some(StrictPolicy { name: true, cms_der: true, signed_attrs: false, }) ); } #[test] fn parse_accepts_strict_without_value_as_all() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--strict".to_string(), "--report-json".to_string(), "out/report.json".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!(args.strict_policy, Some(StrictPolicy::all())); } #[test] fn parse_rejects_unknown_strict_policy() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--strict=unknown".to_string(), ]; let err = parse_args(&argv).expect_err("unknown strict policy should fail"); assert!(err.contains("unknown strict policy"), "{err}"); } #[test] fn effective_cir_tal_uris_filters_skipped_multi_tal_inputs() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "afrinic.tal".to_string(), "--ta-path".to_string(), "afrinic.cer".to_string(), "--tal-path".to_string(), "apnic.tal".to_string(), "--ta-path".to_string(), "apnic.cer".to_string(), "--tal-path".to_string(), "arin.tal".to_string(), "--ta-path".to_string(), "arin.cer".to_string(), "--cir-enable".to_string(), "--cir-out".to_string(), "out.cir".to_string(), "--cir-tal-uri".to_string(), "https://example.test/afrinic.cer".to_string(), "--cir-tal-uri".to_string(), "https://example.test/apnic.cer".to_string(), "--cir-tal-uri".to_string(), "https://example.test/arin.cer".to_string(), ]; let args = parse_args(&argv).expect("parse args"); let mut shared = synthetic_post_validation_shared(); shared.discoveries = vec![shared.discovery.clone(), shared.discovery.clone()].into(); shared.successful_tal_inputs = vec![args.tal_inputs[0].clone(), args.tal_inputs[2].clone()].into(); let effective = effective_cir_tal_uris_for_discoveries( &args, &shared, resolve_cir_export_tal_uris(&args).expect("resolve cir tal uris"), ) .expect("map effective cir tal uris"); assert_eq!( effective, vec![ "https://example.test/afrinic.cer".to_string(), "https://example.test/arin.cer".to_string(), ] ); } #[test] fn parse_rejects_report_json_compact_without_report_json() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--report-json-compact".to_string(), ]; let err = parse_args(&argv).expect_err("compact flag without report path should fail"); assert!( err.contains("--report-json-compact requires --report-json"), "{err}" ); } #[test] fn parse_accepts_skip_report_build_without_report_json() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--ccr-out".to_string(), "out/result.ccr".to_string(), "--skip-report-build".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.skip_report_build); assert_eq!( args.ccr_out_path.as_deref(), Some(std::path::Path::new("out/result.ccr")) ); } #[test] fn parse_accepts_skip_vcir_persist() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--skip-vcir-persist".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.skip_vcir_persist); } #[test] fn parse_rejects_skip_report_build_with_report_json() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--report-json".to_string(), "out/report.json".to_string(), "--skip-report-build".to_string(), ]; let err = parse_args(&argv).expect_err("skip report build with report path should fail"); assert!( err.contains("--skip-report-build cannot be combined with --report-json"), "{err}" ); } #[test] fn parse_accepts_direct_compare_view_csv_outputs() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--vrps-csv-out".to_string(), "out/vrps.csv".to_string(), "--vaps-csv-out".to_string(), "out/vaps.csv".to_string(), "--compare-view-trust-anchor".to_string(), "unknown".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!( args.vrps_csv_out_path.as_deref(), Some(std::path::Path::new("out/vrps.csv")) ); assert_eq!( args.vaps_csv_out_path.as_deref(), Some(std::path::Path::new("out/vaps.csv")) ); assert_eq!(args.compare_view_trust_anchor.as_deref(), Some("unknown")); } #[test] fn parse_rejects_partial_direct_compare_view_csv_outputs() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--vrps-csv-out".to_string(), "out/vrps.csv".to_string(), ]; let err = parse_args(&argv).expect_err("partial direct compare view output should fail"); assert!( err.contains("--vrps-csv-out and --vaps-csv-out must be provided together"), "{err}" ); } #[test] fn parse_accepts_external_raw_store_db() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--raw-store-db".to_string(), "raw-store.db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!( args.raw_store_db.as_deref(), Some(std::path::Path::new("raw-store.db")) ); } #[test] fn parse_accepts_external_repo_bytes_db() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--repo-bytes-db".to_string(), "repo-bytes.db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!( args.repo_bytes_db.as_deref(), Some(std::path::Path::new("repo-bytes.db")) ); } #[test] fn parse_accepts_cir_enable_with_raw_store_backend() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--raw-store-db".to_string(), "raw-store.db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--rsync-local-dir".to_string(), "repo".to_string(), "--cir-enable".to_string(), "--cir-out".to_string(), "out/example.cir".to_string(), "--cir-tal-uri".to_string(), "https://example.test/root.tal".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.cir_enabled); assert_eq!( args.raw_store_db.as_deref(), Some(std::path::Path::new("raw-store.db")) ); assert_eq!(args.cir_static_root, None); } #[test] fn parse_accepts_cir_enable_with_required_paths_and_tal_override() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--rsync-local-dir".to_string(), "repo".to_string(), "--cir-enable".to_string(), "--cir-out".to_string(), "out/example.cir".to_string(), "--cir-tal-uri".to_string(), "https://example.test/root.tal".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert!(args.cir_enabled); assert_eq!( args.cir_out_path.as_deref(), Some(std::path::Path::new("out/example.cir")) ); assert_eq!( args.cir_tal_uri.as_deref(), Some("https://example.test/root.tal") ); assert_eq!( args.cir_tal_uris, vec!["https://example.test/root.tal".to_string()] ); } #[test] fn parse_rejects_deprecated_cir_static_root() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--cir-enable".to_string(), "--cir-out".to_string(), "out/example.cir".to_string(), "--cir-static-root".to_string(), "out/static".to_string(), ]; let err = parse_args(&argv).expect_err("cir-static-root should be rejected"); assert!(err.contains("no longer supported"), "{err}"); } #[test] fn parse_accepts_default_parallel_config_and_phase2_overrides() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2-object-workers".to_string(), "3".to_string(), "--parallel-phase2-worker-queue-capacity".to_string(), "17".to_string(), "--parallel-phase2-ready-batch-size".to_string(), "31".to_string(), "--parallel-phase2-ready-batch-wall-time-budget-ms".to_string(), "43".to_string(), "--parallel-phase2-result-drain-batch-size".to_string(), "37".to_string(), "--parallel-phase2-finalize-batch-size".to_string(), "41".to_string(), "--parallel-phase2-finalize-batch-wall-time-budget-ms".to_string(), "47".to_string(), "--parallel-phase2-finalize-queue-capacity".to_string(), "8192".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!(args.parallel_phase2_config.object_workers, 3); assert_eq!(args.parallel_phase2_config.worker_queue_capacity, 17); assert_eq!(args.parallel_phase2_config.ready_batch_size, 31); assert_eq!( args.parallel_phase2_config.ready_batch_wall_time_budget_ms, 43 ); assert_eq!( args.parallel_phase2_config.object_result_drain_batch_size, 37 ); assert_eq!( args.parallel_phase2_config .publication_point_finalize_batch_size, 41 ); assert_eq!( args.parallel_phase2_config .publication_point_finalize_wall_time_budget_ms, 47 ); assert_eq!( args.parallel_phase2_config .publication_point_finalize_queue_capacity, 8192 ); assert_eq!(args.parallel_phase1_config, ParallelPhase1Config::default()); } #[test] fn parse_rejects_zero_phase2_ready_batch_size() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2-ready-batch-size".to_string(), "0".to_string(), ]; let err = parse_args(&argv).expect_err("zero ready batch must fail"); assert!(err.contains("--parallel-phase2-ready-batch-size"), "{err}"); } #[test] fn parse_rejects_zero_phase2_result_drain_batch_size() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2-result-drain-batch-size".to_string(), "0".to_string(), ]; let err = parse_args(&argv).expect_err("zero result drain batch must fail"); assert!( err.contains("--parallel-phase2-result-drain-batch-size"), "{err}" ); } #[test] fn parse_rejects_zero_phase2_ready_batch_wall_time_budget_ms() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2-ready-batch-wall-time-budget-ms".to_string(), "0".to_string(), ]; let err = parse_args(&argv).expect_err("zero ready time budget must fail"); assert!( err.contains("--parallel-phase2-ready-batch-wall-time-budget-ms"), "{err}" ); } #[test] fn parse_rejects_zero_phase2_finalize_batch_size() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2-finalize-batch-size".to_string(), "0".to_string(), ]; let err = parse_args(&argv).expect_err("zero finalize batch must fail"); assert!( err.contains("--parallel-phase2-finalize-batch-size"), "{err}" ); } #[test] fn parse_rejects_zero_phase2_finalize_batch_wall_time_budget_ms() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2-finalize-batch-wall-time-budget-ms".to_string(), "0".to_string(), ]; let err = parse_args(&argv).expect_err("zero finalize time budget must fail"); assert!( err.contains("--parallel-phase2-finalize-batch-wall-time-budget-ms"), "{err}" ); } #[test] fn parse_rejects_zero_phase2_finalize_queue_capacity() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2-finalize-queue-capacity".to_string(), "0".to_string(), ]; let err = parse_args(&argv).expect_err("zero finalize queue capacity must fail"); assert!( err.contains("--parallel-phase2-finalize-queue-capacity"), "{err}" ); } #[test] fn parse_rejects_removed_parallel_enable_flags() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase1".to_string(), ]; let err = parse_args(&argv).expect_err("removed phase flag should fail"); assert!(err.contains("unknown argument: --parallel-phase1"), "{err}"); let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--parallel-phase2".to_string(), ]; let err = parse_args(&argv).expect_err("removed phase flag should fail"); assert!(err.contains("unknown argument: --parallel-phase2"), "{err}"); } #[test] fn parse_accepts_multi_tal_cir_overrides_in_file_mode() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "apnic.tal".to_string(), "--ta-path".to_string(), "apnic.cer".to_string(), "--tal-path".to_string(), "arin.tal".to_string(), "--ta-path".to_string(), "arin.cer".to_string(), "--rsync-local-dir".to_string(), "repo".to_string(), "--cir-enable".to_string(), "--cir-out".to_string(), "out/example.cir".to_string(), "--cir-tal-uri".to_string(), "https://example.test/apnic.tal".to_string(), "--cir-tal-uri".to_string(), "https://example.test/arin.tal".to_string(), ]; let args = parse_args(&argv).expect("parse args"); assert_eq!( args.cir_tal_uris, vec![ "https://example.test/apnic.tal".to_string(), "https://example.test/arin.tal".to_string() ] ); } #[test] fn parse_rejects_incomplete_or_invalid_cir_flags() { let argv_missing = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--cir-enable".to_string(), ]; let err = parse_args(&argv_missing).unwrap_err(); assert!(err.contains("--cir-enable requires --cir-out"), "{err}"); let argv_needs_enable = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/root.tal".to_string(), "--cir-out".to_string(), "out/example.cir".to_string(), ]; let err = parse_args(&argv_needs_enable).unwrap_err(); assert!(err.contains("require --cir-enable"), "{err}"); let argv_offline_missing_uri = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "x.tal".to_string(), "--ta-path".to_string(), "x.cer".to_string(), "--rsync-local-dir".to_string(), "repo".to_string(), "--cir-enable".to_string(), "--cir-out".to_string(), "out/example.cir".to_string(), ]; let err = parse_args(&argv_offline_missing_uri).unwrap_err(); assert!(err.contains("requires --cir-tal-uri"), "{err}"); } #[test] fn parse_rejects_invalid_validation_time() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--validation-time".to_string(), "not-a-time".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("invalid --validation-time"), "{err}"); } #[test] fn parse_rejects_invalid_max_instances() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--max-instances".to_string(), "nope".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("invalid --max-instances"), "{err}"); } #[test] fn parse_rejects_missing_value_for_db() { let argv = vec!["rpki".to_string(), "--db".to_string()]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("--db requires a value"), "{err}"); } #[test] fn parse_rejects_missing_value_for_tal_url() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("--tal-url requires a value"), "{err}"); } #[test] fn parse_rejects_missing_db() { let argv = vec!["rpki".to_string(), "--tal-url".to_string(), "x".to_string()]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("--db is required"), "{err}"); } #[test] fn parse_rejects_missing_tal_mode() { let argv = vec!["rpki".to_string(), "--db".to_string(), "db".to_string()]; let err = parse_args(&argv).unwrap_err(); assert!( err.contains("--tal-url") || err.contains("--tal-path"), "{err}" ); } #[test] fn parse_accepts_tal_url_mode() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!(args.tal_url.as_deref(), Some("https://example.test/x.tal")); assert_eq!( args.tal_urls, vec!["https://example.test/x.tal".to_string()] ); assert!(args.tal_path.is_none()); assert!(args.ta_path.is_none()); assert_eq!(args.tal_inputs.len(), 1); assert_eq!(args.tal_inputs[0].tal_id, "x"); assert_eq!(args.parallel_phase1_config, ParallelPhase1Config::default()); assert_eq!(args.parallel_phase2_config, ParallelPhase2Config::default()); } #[test] fn parse_accepts_multi_tal_without_parallel_flags() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/arin.tal".to_string(), "--tal-url".to_string(), "https://example.test/apnic.tal".to_string(), "--tal-url".to_string(), "https://example.test/ripe.tal".to_string(), "--parallel-max-repo-sync-workers-global".to_string(), "8".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!(args.tal_urls.len(), 3); assert_eq!(args.tal_inputs.len(), 3); assert_eq!(args.tal_inputs[0].tal_id, "arin"); assert_eq!(args.tal_inputs[1].tal_id, "apnic"); assert_eq!(args.tal_inputs[2].tal_id, "ripe"); assert_eq!(args.parallel_phase1_config.max_repo_sync_workers_global, 8); } #[test] fn parse_accepts_multi_tal_urls_by_default() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/arin.tal".to_string(), "--tal-url".to_string(), "https://example.test/apnic.tal".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!(args.tal_urls.len(), 2); assert_eq!(args.tal_inputs.len(), 2); } #[test] fn parse_accepts_offline_mode_requires_ta() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--max-depth".to_string(), "0".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!(args.tal_paths, vec![PathBuf::from("a.tal")]); assert_eq!(args.ta_paths, vec![PathBuf::from("ta.cer")]); assert_eq!(args.tal_path.as_deref(), Some(Path::new("a.tal"))); assert_eq!(args.ta_path.as_deref(), Some(Path::new("ta.cer"))); assert_eq!(args.max_depth, Some(0)); } #[test] fn parse_accepts_multiple_tal_path_pairs_by_default() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "apnic.tal".to_string(), "--ta-path".to_string(), "apnic-ta.cer".to_string(), "--tal-path".to_string(), "arin.tal".to_string(), "--ta-path".to_string(), "arin-ta.cer".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!(args.tal_paths.len(), 2); assert_eq!(args.ta_paths.len(), 2); assert_eq!(args.tal_inputs.len(), 2); } #[test] fn parse_rejects_mixed_tal_url_and_tal_path_modes() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/arin.tal".to_string(), "--tal-path".to_string(), "apnic.tal".to_string(), "--ta-path".to_string(), "apnic-ta.cer".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!( err.contains( "must specify either one-or-more --tal-url or one-or-more --tal-path/--ta-path pairs" ), "{err}" ); } #[test] fn parse_rejects_mismatched_tal_path_and_ta_path_counts() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "apnic.tal".to_string(), "--tal-path".to_string(), "arin.tal".to_string(), "--ta-path".to_string(), "apnic-ta.cer".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!( err.contains("--tal-path and --ta-path counts must match"), "{err}" ); } #[test] fn parse_accepts_tal_path_without_ta_when_disable_rrdp_is_set() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--disable-rrdp".to_string(), "--rsync-command".to_string(), "/tmp/fake-rsync".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!(args.tal_path.as_deref(), Some(Path::new("a.tal"))); assert!(args.ta_path.is_none()); assert!(args.disable_rrdp); assert_eq!( args.rsync_command.as_deref(), Some(Path::new("/tmp/fake-rsync")) ); } #[test] fn parse_accepts_multiple_tal_paths_without_ta_when_disable_rrdp() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--tal-path".to_string(), "b.tal".to_string(), "--disable-rrdp".to_string(), "--rsync-command".to_string(), "/tmp/fake-rsync".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert_eq!( args.tal_paths, vec![PathBuf::from("a.tal"), PathBuf::from("b.tal")] ); assert!(args.ta_paths.is_empty()); assert_eq!(args.tal_inputs.len(), 2); assert!(args.disable_rrdp); } #[test] fn parse_accepts_payload_delta_replay_mode_with_offline_tal_and_ta() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--payload-base-archive".to_string(), "base-archive".to_string(), "--payload-base-locks".to_string(), "base-locks.json".to_string(), "--payload-delta-archive".to_string(), "delta-archive".to_string(), "--payload-delta-locks".to_string(), "delta-locks.json".to_string(), ]; let args = parse_args(&argv).expect("parse delta replay mode"); assert_eq!( args.payload_base_archive.as_deref(), Some(Path::new("base-archive")) ); assert_eq!( args.payload_base_locks.as_deref(), Some(Path::new("base-locks.json")) ); assert_eq!( args.payload_delta_archive.as_deref(), Some(Path::new("delta-archive")) ); assert_eq!( args.payload_delta_locks.as_deref(), Some(Path::new("delta-locks.json")) ); } #[test] fn parse_rejects_partial_payload_delta_arguments_and_mutual_exclusion() { let argv_partial = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--payload-base-archive".to_string(), "base-archive".to_string(), ]; let err = parse_args(&argv_partial).unwrap_err(); assert!(err.contains("must be provided together"), "{err}"); let argv_both = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--payload-replay-archive".to_string(), "archive".to_string(), "--payload-replay-locks".to_string(), "locks.json".to_string(), "--payload-base-archive".to_string(), "base-archive".to_string(), "--payload-base-locks".to_string(), "base-locks.json".to_string(), "--payload-delta-archive".to_string(), "delta-archive".to_string(), "--payload-delta-locks".to_string(), "delta-locks.json".to_string(), ]; let err = parse_args(&argv_both).unwrap_err(); assert!(err.contains("mutually exclusive"), "{err}"); } #[test] fn parse_rejects_payload_delta_with_tal_url_or_rsync_local_dir() { let argv_url = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--payload-base-archive".to_string(), "base-archive".to_string(), "--payload-base-locks".to_string(), "base-locks.json".to_string(), "--payload-delta-archive".to_string(), "delta-archive".to_string(), "--payload-delta-locks".to_string(), "delta-locks.json".to_string(), ]; let err = parse_args(&argv_url).unwrap_err(); assert!(err.contains("--tal-url is not supported"), "{err}"); let argv_rsync = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--payload-base-archive".to_string(), "base-archive".to_string(), "--payload-base-locks".to_string(), "base-locks.json".to_string(), "--payload-delta-archive".to_string(), "delta-archive".to_string(), "--payload-delta-locks".to_string(), "delta-locks.json".to_string(), "--rsync-local-dir".to_string(), "repo".to_string(), ]; let err = parse_args(&argv_rsync).unwrap_err(); assert!( err.contains("payload delta replay mode cannot be combined with --rsync-local-dir"), "{err}" ); } #[test] fn parse_accepts_payload_replay_mode_with_offline_tal_and_ta() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--payload-replay-archive".to_string(), "archive".to_string(), "--payload-replay-locks".to_string(), "locks.json".to_string(), ]; let args = parse_args(&argv).expect("parse replay mode"); assert_eq!( args.payload_replay_archive.as_deref(), Some(Path::new("archive")) ); assert_eq!( args.payload_replay_locks.as_deref(), Some(Path::new("locks.json")) ); } #[test] fn parse_rejects_partial_payload_replay_arguments() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--payload-replay-archive".to_string(), "archive".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("must be provided together"), "{err}"); } #[test] fn parse_rejects_payload_replay_with_tal_url_or_rsync_local_dir() { let argv_url = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--payload-replay-archive".to_string(), "archive".to_string(), "--payload-replay-locks".to_string(), "locks.json".to_string(), ]; let err = parse_args(&argv_url).unwrap_err(); assert!(err.contains("--tal-url is not supported"), "{err}"); let argv_rsync = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-path".to_string(), "a.tal".to_string(), "--ta-path".to_string(), "ta.cer".to_string(), "--payload-replay-archive".to_string(), "archive".to_string(), "--payload-replay-locks".to_string(), "locks.json".to_string(), "--rsync-local-dir".to_string(), "repo".to_string(), ]; let err = parse_args(&argv_rsync).unwrap_err(); assert!( err.contains("cannot be combined with --rsync-local-dir"), "{err}" ); } #[test] fn parse_accepts_validation_time_rfc3339() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--validation-time".to_string(), "2026-01-01T00:00:00Z".to_string(), ]; let args = parse_args(&argv).expect("parse"); assert!(args.validation_time.is_some()); } #[test] fn parse_rejects_removed_revalidate_only_flag() { let argv = vec![ "rpki".to_string(), "--db".to_string(), "db".to_string(), "--tal-url".to_string(), "https://example.test/x.tal".to_string(), "--revalidate-only".to_string(), ]; let err = parse_args(&argv).unwrap_err(); assert!(err.contains("unknown argument: --revalidate-only"), "{err}"); } #[test] fn read_policy_accepts_valid_toml() { let dir = tempfile::tempdir().expect("tmpdir"); let p = dir.path().join("policy.toml"); std::fs::write( &p, "signed_object_failure_policy = \"drop_publication_point\"\n", ) .expect("write policy"); let policy = read_policy(Some(&p)).expect("parse policy"); assert_eq!( policy.signed_object_failure_policy, crate::policy::SignedObjectFailurePolicy::DropPublicationPoint ); assert_eq!(policy.strict, StrictPolicy::default()); } #[test] fn read_policy_accepts_strict_table() { let dir = tempfile::tempdir().expect("tmpdir"); let p = dir.path().join("policy.toml"); std::fs::write( &p, r#" [strict] name = true cms_der = true "#, ) .expect("write policy"); let policy = read_policy(Some(&p)).expect("parse policy"); assert_eq!( policy.strict, StrictPolicy { name: true, cms_der: true, signed_attrs: false, } ); } #[test] fn read_policy_reports_missing_file() { let dir = tempfile::tempdir().expect("tmpdir"); let p = dir.path().join("missing.toml"); let err = read_policy(Some(&p)).unwrap_err(); assert!(err.contains("read policy file failed"), "{err}"); } fn synthetic_post_validation_shared() -> PostValidationShared { let tal_bytes = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/tal/apnic-rfc7730-https.tal"), ) .expect("read tal fixture"); let ta_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"), ) .expect("read ta fixture"); let discovery = crate::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der( &tal_bytes, &ta_der, None, ) .expect("discover root"); let tree = crate::validation::tree::TreeRunOutput { instances_processed: 1, instances_failed: 0, warnings: vec![ crate::report::Warning::new("synthetic warning") .with_rfc_refs(&[crate::report::RfcRef("RFC 6487 ยง4.8.8.1")]) .with_context("rsync://example.test/repo/pp/"), ], vrps: vec![ crate::validation::objects::Vrp { asn: 64496, prefix: crate::data_model::roa::IpPrefix { afi: crate::data_model::roa::RoaAfi::Ipv4, prefix_len: 24, addr: [192, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, max_length: 24, }, crate::validation::objects::Vrp { asn: 64497, prefix: crate::data_model::roa::IpPrefix { afi: crate::data_model::roa::RoaAfi::Ipv6, prefix_len: 48, addr: [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], }, max_length: 64, }, ], aspas: vec![crate::validation::objects::AspaAttestation { customer_as_id: 64496, provider_as_ids: vec![64497, 64498], }], router_keys: Vec::new(), }; let mut pp1 = crate::audit::PublicationPointAudit::default(); pp1.source = "fresh".to_string(); pp1.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string()); pp1.manifest_rsync_uri = "rsync://example.test/repo/pp1/manifest.mft".to_string(); pp1.objects.push(crate::audit::ObjectAuditEntry { rsync_uri: "rsync://example.test/repo/pp1/a.roa".to_string(), sha256_hex: "11".repeat(32), kind: crate::audit::AuditObjectKind::Roa, result: crate::audit::AuditObjectResult::Ok, detail: None, }); let mut pp2 = crate::audit::PublicationPointAudit::default(); pp2.source = "fresh".to_string(); pp2.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string()); let mut pp3 = crate::audit::PublicationPointAudit::default(); pp3.source = "fresh".to_string(); pp3.rrdp_notification_uri = Some("https://example.test/n2.xml".to_string()); let out = crate::validation::run_tree_from_tal::RunTreeFromTalAuditOutput { discovery: discovery.clone(), discoveries: vec![discovery], successful_tal_inputs: Vec::new(), tree, publication_points: vec![pp1, pp2, pp3], roa_cache_stats: crate::validation::objects::RoaValidationCacheStats::default(), downloads: Vec::new(), download_stats: crate::audit::AuditDownloadStats::default(), current_repo_objects: Vec::new(), ccr_accumulator: None, cir_input: crate::cir::CirInputSnapshot::default(), }; PostValidationShared::from_run_output(out) } fn sample_cli_ccr_accumulator() -> CcrAccumulator { let tal_bytes = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests/fixtures/tal/apnic-rfc7730-https.tal"), ) .expect("read tal fixture"); let ta_der = std::fs::read( std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/ta/apnic-ta.cer"), ) .expect("read ta fixture"); let discovery = crate::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der( &tal_bytes, &ta_der, None, ) .expect("discover root"); let mut accumulator = CcrAccumulator::new(vec![discovery.trust_anchor.clone()]); let projection = crate::storage::VcirCcrManifestProjection { manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(), manifest_sha256: vec![0x44; 32], manifest_size: 2048, manifest_ee_aki: vec![0x55; 20], manifest_number_be: vec![1], manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime( time::OffsetDateTime::now_utc(), ), manifest_sia_locations_der: vec![vec![ 0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05, b'r', b's', b'y', b'n', b'c', ]], subordinate_skis: vec![vec![0x33; 20]], }; accumulator .append_manifest_projection(&projection) .expect("append manifest projection"); accumulator } #[test] fn build_report_and_helpers_work_on_synthetic_output() { let shared = synthetic_post_validation_shared(); let policy = Policy::default(); let validation_time = time::OffsetDateTime::now_utc(); let report = build_report(&policy, validation_time, &shared); assert_eq!(unique_rrdp_repos(&report), 2); assert_eq!(report.vrps.len(), 2); assert_eq!(report.aspas.len(), 1); print_summary(&report); } #[test] fn run_report_task_and_stage_timing_work() { let shared = synthetic_post_validation_shared(); let policy = Policy::default(); let validation_time = time::OffsetDateTime::now_utc(); let dir = tempfile::tempdir().expect("tmpdir"); let report_path = dir.path().join("report.json"); let report_output = run_report_task( &policy, validation_time, &shared, Some(&report_path), ReportJsonFormat::Compact, ) .expect("run report task"); assert!(report_output.report_write_ms.is_some()); let report_json = std::fs::read_to_string(&report_path).expect("read report json"); assert!(!report_json.contains('\n'), "{report_json}"); let report: serde_json::Value = serde_json::from_str(&report_json).expect("parse compact report json"); assert_eq!(report["vrps"].as_array().unwrap().len(), 2); assert_eq!(report["aspas"].as_array().unwrap().len(), 1); assert_eq!(report["queryAudit"]["status"].as_str(), Some("complete")); assert!(report["queryAudit"]["eventsCount"].as_u64().unwrap() > 0); let events_path = dir.path().join( report["queryAudit"]["eventsPath"] .as_str() .expect("events path"), ); let events = std::fs::read_to_string(events_path).expect("read validation events"); assert!( events .lines() .any(|line| line.contains("\"eventType\":\"object\"")) ); let stage_timing = RunStageTiming { validation_ms: 1, enable_roa_validation_cache: false, enable_transport_request_prefetch: false, report_build_ms: report_output.report_build_ms, report_write_ms: report_output.report_write_ms, ccr_build_ms: Some(2), ccr_build_breakdown: None, ccr_write_ms: Some(3), compare_view_build_ms: Some(4), compare_view_write_ms: Some(5), cir_build_cir_ms: Some(6), cir_write_cir_ms: Some(7), cir_total_ms: Some(8), total_ms: 9, publication_points: shared.publication_points.len(), repo_sync_ms_total: 10, publication_point_repo_sync_ms_total: 11, download_event_count: 12, rrdp_download_ms_total: 13, rsync_download_ms_total: 14, download_bytes_total: 15, roa_validation_cache: crate::validation::objects::RoaValidationCacheStats::default(), analysis_counts: std::collections::HashMap::new(), vcir_storage_summary_ms: Some(16), vcir_storage: Some(VcirStorageSummary { entry_count: 2, vcir_value_bytes: 100, vcir_value_bytes_max: 60, vcir_value_bytes_max_manifest_rsync_uri: Some( "rsync://example.test/repo/max.mft".to_string(), ), core_fields: VcirCoreFieldSizeBreakdown { manifest_rsync_uri_bytes: 10, ..VcirCoreFieldSizeBreakdown::default() }, ccr_projection: VcirCcrProjectionSizeBreakdown { manifest_sha256_bytes: 32, ..VcirCcrProjectionSizeBreakdown::default() }, child_resources: VcirChildResourceSizeBreakdown { effective_ip_resource_cbor_bytes: 12, effective_as_resource_cbor_bytes: 6, }, field_sizes: VcirFieldSizeBreakdown { local_output_count: 1, local_output_payload_json_bytes: 70, local_output_payload_typed_body_bytes: 20, ..VcirFieldSizeBreakdown::default() }, local_output_old_projection_bytes: 80, local_output_typed_projection_bytes: 30, local_output_projection_saved_bytes: 50, top_entries_by_vcir_value_bytes: vec![VcirStorageEntrySummary { manifest_rsync_uri: "rsync://example.test/repo/max.mft".to_string(), vcir_value_bytes: 60, local_vrp_count: 1, local_aspa_count: 0, local_router_key_count: 0, accepted_object_count: 1, rejected_object_count: 0, child_count: 0, core_fields: VcirCoreFieldSizeBreakdown::default(), ccr_projection: VcirCcrProjectionSizeBreakdown::default(), child_resources: VcirChildResourceSizeBreakdown::default(), field_sizes: VcirFieldSizeBreakdown::default(), local_output_old_projection_bytes: 1, local_output_typed_projection_bytes: 1, local_output_projection_saved_bytes: 0, }], }), memory_telemetry: None, }; write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing"); let stage_timing_json = std::fs::read_to_string(dir.path().join("stage-timing.json")).expect("read timing"); assert!(stage_timing_json.contains("\"validation_ms\"")); assert!(stage_timing_json.contains("\"ccr_build_ms\"")); assert!(stage_timing_json.contains("\"vcir_storage\"")); assert!(stage_timing_json.contains("\"local_output_projection_saved_bytes\"")); let ccr_path = dir.path().join("result.ccr"); write_stage_timing(Some(&ccr_path), &stage_timing).expect("write stage timing via ccr path"); assert!( dir.path().join("stage-timing.json").exists(), "stage timing should use parent directory of the anchor path" ); let skipped = ReportTaskOutput::skipped(); assert_eq!(skipped.report_build_ms, 0); assert!(skipped.report_write_ms.is_none()); } #[test] fn stage_timing_serializes_memory_telemetry() { let dir = tempfile::tempdir().expect("tmpdir"); let report_path = dir.path().join("report.json"); let stage_timing = RunStageTiming { validation_ms: 1, enable_roa_validation_cache: true, enable_transport_request_prefetch: true, report_build_ms: 2, report_write_ms: None, ccr_build_ms: None, ccr_build_breakdown: None, ccr_write_ms: None, compare_view_build_ms: None, compare_view_write_ms: None, cir_build_cir_ms: None, cir_write_cir_ms: None, cir_total_ms: None, total_ms: 3, publication_points: 4, repo_sync_ms_total: 5, publication_point_repo_sync_ms_total: 6, download_event_count: 7, rrdp_download_ms_total: 8, rsync_download_ms_total: 9, download_bytes_total: 10, roa_validation_cache: crate::validation::objects::RoaValidationCacheStats { hit_roas: 2, ..crate::validation::objects::RoaValidationCacheStats::default() }, analysis_counts: std::collections::HashMap::from([( "roa_validation_cache_hit_roas".to_string(), 2, )]), vcir_storage_summary_ms: None, vcir_storage: None, memory_telemetry: Some(MemoryTelemetrySummary { checkpoints: vec![MemoryTelemetryCheckpoint { label: "after_validation".to_string(), elapsed_ms: 11, process: ProcessMemorySnapshot { label: "after_validation".to_string(), vm_rss_kb: Some(12), vm_size_kb: None, vm_data_kb: None, vm_swap_kb: None, rss_anon_kb: Some(13), rss_file_kb: None, rss_shmem_kb: None, threads: Some(14), fd_count: Some(15), smaps_rollup: None, smaps_mapping_summary: None, errors: Vec::new(), }, rocksdb: RocksDbMemorySnapshot { databases: Vec::new(), totals: RocksDbMemoryTotals { cur_size_all_mem_tables: 16, size_all_mem_tables: 17, estimate_table_readers_mem: 18, block_cache_capacity: 19, block_cache_usage: 20, block_cache_pinned_usage: 21, }, }, }], object_graph: None, malloc_trim_probes: Vec::new(), }), }; write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing"); let value: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(dir.path().join("stage-timing.json")).unwrap(), ) .expect("parse stage timing json"); let checkpoint = &value["memory_telemetry"]["checkpoints"][0]; assert_eq!(checkpoint["label"], "after_validation"); assert_eq!(checkpoint["process"]["vm_rss_kb"], 12); assert_eq!( checkpoint["rocksdb"]["totals"]["cur_size_all_mem_tables"], 16 ); assert_eq!(value["analysis_counts"]["roa_validation_cache_hit_roas"], 2); assert_eq!(value["roa_validation_cache"]["hit_roas"], 2); assert!( value["memory_telemetry"] .as_object() .expect("memory telemetry object") .get("malloc_trim_probes") .is_none() ); } #[test] fn shared_object_graph_estimate_counts_audit_and_outputs() { let mut shared = synthetic_post_validation_shared(); let mut publication_points = shared .publication_points .iter() .cloned() .collect::>(); publication_points[0].rsync_base_uri = "rsync://example.test/repo/".to_string(); publication_points[0].manifest_rsync_uri = "rsync://example.test/repo/a.mft".to_string(); publication_points[0].publication_point_rsync_uri = "rsync://example.test/repo/".to_string(); publication_points[0].objects = vec![crate::audit::ObjectAuditEntry { rsync_uri: "rsync://example.test/repo/a.roa".to_string(), sha256_hex: "11".repeat(32), kind: crate::audit::AuditObjectKind::Roa, result: crate::audit::AuditObjectResult::Ok, detail: None, }]; shared.publication_points = publication_points.into(); let graph = estimate_shared_object_graph(&shared); let publication_points_section = graph .sections .iter() .find(|section| section.name == "publication_points") .expect("publication points section"); let object_count = publication_points_section .details .iter() .find(|metric| metric.name == "object_audit_entry_count") .expect("object count metric"); assert_eq!(object_count.value, 1); assert!(publication_points_section.estimated_bytes > 0); let vrps_section = graph .sections .iter() .find(|section| section.name == "vrps") .expect("vrps section"); assert_eq!(vrps_section.item_count, 2); assert!(graph.total_estimated_bytes >= publication_points_section.estimated_bytes); } #[test] fn run_compare_view_task_writes_csv_from_shared_output() { let shared = synthetic_post_validation_shared(); let dir = tempfile::tempdir().expect("tmpdir"); let vrps_path = dir.path().join("vrps.csv"); let vaps_path = dir.path().join("vaps.csv"); let output = run_compare_view_task(&shared, Some(&vrps_path), Some(&vaps_path), "unknown") .expect("write direct compare views"); assert!(output.build_ms.is_some()); assert!(output.write_ms.is_some()); 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,192.0.2.0/24,24,unknown")); assert!(vrps_csv.contains("AS64497,2001:db8::/48,64,unknown")); assert!(vaps_csv.contains("Customer ASN,Providers,Trust Anchor")); assert!(vaps_csv.contains("AS64496,AS64497;AS64498,unknown")); } #[test] fn run_ccr_task_uses_accumulator_when_phase2_output_contains_reuse_sources() { let mut shared = synthetic_post_validation_shared(); shared.ccr_accumulator = Some(sample_cli_ccr_accumulator()); let mut publication_points = shared .publication_points .iter() .cloned() .collect::>(); publication_points[1].source = "vcir_current_instance".to_string(); publication_points[2].source = "failed_no_cache".to_string(); shared.publication_points = publication_points.into(); let dir = tempfile::tempdir().expect("tmpdir"); let ccr_path = dir.path().join("result.ccr"); let store = RocksStore::open(&dir.path().join("db")).expect("open empty store"); let output = run_ccr_task( &store, &shared, Some(&ccr_path), time::OffsetDateTime::now_utc(), ) .expect("run ccr task"); assert!(output.ccr_build_ms.is_some()); assert!(output.ccr_build_breakdown.is_none()); let der = std::fs::read(&ccr_path).expect("read ccr"); let ci = crate::ccr::decode_content_info(&der).expect("decode ccr"); assert_eq!( ci.content .mfts .as_ref() .map(|manifest_state| manifest_state.mis.len()), Some(1) ); } #[test] fn write_json_writes_report() { let report = AuditReportV2 { format_version: 2, meta: AuditRunMeta { validation_time_rfc3339_utc: "2026-01-01T00:00:00Z".to_string(), }, policy: Policy::default(), tree: TreeSummary { instances_processed: 0, instances_failed: 0, warnings: Vec::new(), }, publication_points: Vec::new(), vrps: Vec::new(), aspas: Vec::new(), downloads: Vec::new(), download_stats: crate::audit::AuditDownloadStats::default(), repo_sync_stats: crate::audit::AuditRepoSyncStats::default(), query_audit: None, }; let dir = tempfile::tempdir().expect("tmpdir"); let pretty_path = dir.path().join("report-pretty.json"); write_json(&pretty_path, &report, ReportJsonFormat::Pretty).expect("write pretty json"); let pretty = std::fs::read_to_string(&pretty_path).expect("read pretty report"); assert!(pretty.contains("\"format_version\"")); assert!(pretty.contains("\"policy\"")); assert!(pretty.contains("\n \"format_version\""), "{pretty}"); let compact_path = dir.path().join("report-compact.json"); write_json(&compact_path, &report, ReportJsonFormat::Compact).expect("write compact json"); let compact = std::fs::read_to_string(&compact_path).expect("read compact report"); assert!(compact.contains("\"format_version\"")); assert!(compact.contains("\"policy\"")); assert!(!compact.contains('\n'), "{compact}"); } #[test] fn build_repo_sync_stats_aggregates_phase_and_terminal_state() { let mut pp1 = crate::audit::PublicationPointAudit::default(); pp1.repo_sync_phase = Some("rrdp_ok".to_string()); pp1.repo_sync_duration_ms = Some(10); pp1.repo_terminal_state = "fresh".to_string(); let mut pp2 = crate::audit::PublicationPointAudit::default(); pp2.repo_sync_phase = Some("rrdp_failed_rsync_failed".to_string()); pp2.repo_sync_duration_ms = Some(20); pp2.repo_terminal_state = "failed_no_cache".to_string(); let mut pp3 = crate::audit::PublicationPointAudit::default(); pp3.repo_sync_phase = Some("rrdp_failed_rsync_failed".to_string()); pp3.repo_sync_duration_ms = Some(30); pp3.repo_terminal_state = "failed_no_cache".to_string(); let stats = build_repo_sync_stats(&[pp1, pp2, pp3]); assert_eq!(stats.publication_points_total, 3); assert_eq!(stats.by_phase["rrdp_ok"].count, 1); assert_eq!(stats.by_phase["rrdp_ok"].duration_ms_total, 10); assert_eq!(stats.by_phase["rrdp_failed_rsync_failed"].count, 2); assert_eq!( stats.by_phase["rrdp_failed_rsync_failed"].duration_ms_total, 50 ); assert_eq!(stats.by_terminal_state["fresh"].count, 1); assert_eq!(stats.by_terminal_state["failed_no_cache"].count, 2); assert_eq!( stats.by_terminal_state["failed_no_cache"].duration_ms_total, 50 ); }