Something went wrong. Try again.
Hardware-in-the-loop development platform for the Commodore 64.
Something went wrong. Try again.
21 kB · 667 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668use std::{ fs, io::{Read, Write}, net::TcpListener, process::Command, thread,};
use serde_json::Value;use tempfile::TempDir;
fn fixture() -> (TempDir, std::path::PathBuf) { let root = tempfile::tempdir().unwrap(); fs::write(root.path().join("smoke.prg"), [0x01, 0x08, 0xea]).unwrap(); let manifest = root.path().join("c64dev.toml"); fs::write( &manifest, r#"schema_version = 3[project]name = "cli-test"[build]command = ["true"]working_directory = "."artifact = "smoke.prg"toolchain = "test"[diagnostics]address = 49152length = 256command_address = 49408screen_address = 1024color_address = 55296[vice]executable = "x64sc"poll_interval_ms = 1[ultimate]poll_interval_ms = 1request_timeout_ms = 100[[tests]]name = "smoke"timeout_ms = 2000heartbeat_timeout_ms = 100expected_terminal = "pass"expected_screen = "ULTIMATE TOOL"startup_command = { name = "scenario", payload = "pass" }"#, ) .unwrap(); (root, manifest)}
fn cli() -> Command { Command::new(env!("CARGO_BIN_EXE_ultimate-tool"))}
#[test]fn debug_capture_requires_a_pinned_identity_before_network_io() { let (_root, manifest) = fixture(); let output = cli() .args([ "--format", "json", "capture", "debug-stream", "--manifest", manifest.to_str().unwrap(), "--device", "192.0.2.10", "--interface", "192.0.2.20", "--confirm-stream-start-count", "1", "--confirm-stream-stop-count", "1", ]) .output() .unwrap(); assert!(!output.status.success()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("expected device ID or hostname") );}
#[test]fn source_linked_capture_requires_exact_counts_before_build_or_network_io() { let (_root, manifest) = fixture(); let output = cli() .args([ "--format", "json", "capture", "source-linked", "--manifest", manifest.to_str().unwrap(), "--test", "timeline-probe", "--device", "192.0.2.10", "--interface", "192.0.2.20", "--expect-device-id", "ABC123", "--confirm-reset-count", "0", "--confirm-stream-start-count", "1", "--confirm-stream-stop-count", "1", ]) .output() .unwrap(); assert_eq!(output.status.code(), Some(2)); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("--confirm-reset-count 1") );}
fn executable_riscv32_elf() -> Vec<u8> { const ELF_HEADER_SIZE: usize = 52; const PROGRAM_HEADER_SIZE: usize = 32; const PAYLOAD_OFFSET: usize = 0x100; const LOAD_ADDRESS: u32 = 0x0300_0000; const PT_LOAD: u32 = 1; const PF_X: u32 = 1; const PF_R: u32 = 4;
let payload = [0x13, 0, 0, 0]; let mut bytes = vec![0_u8; PAYLOAD_OFFSET + payload.len()]; bytes[0..4].copy_from_slice(b"\x7fELF"); bytes[4] = 1; bytes[5] = 1; bytes[6] = 1; bytes[16..18].copy_from_slice(&2_u16.to_le_bytes()); bytes[18..20].copy_from_slice(&243_u16.to_le_bytes()); bytes[20..24].copy_from_slice(&1_u32.to_le_bytes()); bytes[24..28].copy_from_slice(&LOAD_ADDRESS.to_le_bytes()); bytes[28..32].copy_from_slice(&(ELF_HEADER_SIZE as u32).to_le_bytes()); bytes[40..42].copy_from_slice(&(ELF_HEADER_SIZE as u16).to_le_bytes()); bytes[42..44].copy_from_slice(&(PROGRAM_HEADER_SIZE as u16).to_le_bytes()); bytes[44..46].copy_from_slice(&1_u16.to_le_bytes());
let program = ELF_HEADER_SIZE; bytes[program..program + 4].copy_from_slice(&PT_LOAD.to_le_bytes()); bytes[program + 4..program + 8].copy_from_slice(&(PAYLOAD_OFFSET as u32).to_le_bytes()); bytes[program + 8..program + 12].copy_from_slice(&LOAD_ADDRESS.to_le_bytes()); bytes[program + 12..program + 16].copy_from_slice(&LOAD_ADDRESS.to_le_bytes()); bytes[program + 16..program + 20].copy_from_slice(&(payload.len() as u32).to_le_bytes()); bytes[program + 20..program + 24].copy_from_slice(&8_u32.to_le_bytes()); bytes[program + 24..program + 28].copy_from_slice(&(PF_R | PF_X).to_le_bytes()); bytes[program + 28..program + 32].copy_from_slice(&4_u32.to_le_bytes()); bytes[PAYLOAD_OFFSET..].copy_from_slice(&payload); bytes}
fn native_fixture() -> (TempDir, std::path::PathBuf) { let root = tempfile::tempdir().unwrap(); fs::write(root.path().join("input.elf"), executable_riscv32_elf()).unwrap(); fs::write(root.path().join("native.map"), "0x03000000 _start\n").unwrap(); let manifest = root.path().join("c64dev.toml"); fs::write( &manifest, r#"schema_version = 10[project]name = "cli-native-test"kind = "ultimate_riscv"[build]command = ["sh", "-c", "cp input.elf native.elf"]working_directory = "."artifact = "native.elf"supporting_artifacts = ["native.map"]toolchain = "test"[ultimate]poll_interval_ms = 50request_timeout_ms = 100[native]protocol = "utr2"timeout_ms = 1000heartbeat_timeout_ms = 200"#, ) .unwrap(); (root, manifest)}
#[test]fn ultimate_probe_returns_clean_json_without_async_runtime_panic() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let address = listener.local_addr().unwrap(); let server = thread::spawn(move || { for _ in 0..2 { let (mut stream, _) = listener.accept().unwrap(); let mut request = [0_u8; 2048]; let length = stream.read(&mut request).unwrap(); let request = String::from_utf8_lossy(&request[..length]); let body = if request.starts_with("GET /v1/version ") { r#"{"version":"1.0"}"# } else if request.starts_with("GET /v1/info ") { r#"{"product":"C64 Ultimate","hostname":"test-c64","unique_id":"ABC123","firmware_version":"1.1.0","fpga_version":"122","core_version":"1.49"}"# } else { panic!("unexpected request: {request}"); }; write!( stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ) .unwrap(); } });
let output = cli() .args([ "--format", "json", "probe", "ultimate", "--device", &address.to_string(), ]) .output() .unwrap(); server.join().unwrap();
assert!( output.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout) ); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["record_kind"], "result"); assert_eq!(value["data"]["unique_id"], "ABC123");}
#[test]fn json_result_and_error_envelopes_keep_stdout_clean() { let (_root, manifest) = fixture(); let success = cli() .args(["--format", "json", "validate", "--manifest"]) .arg(&manifest) .output() .unwrap(); assert!(success.status.success()); assert!(success.stderr.is_empty()); let lines = String::from_utf8(success.stdout).unwrap(); assert_eq!(lines.lines().count(), 1); let value: Value = serde_json::from_str(lines.trim()).unwrap(); assert_eq!(value["record_kind"], "result"); assert_eq!(value["command"], "validate");
let missing = manifest.with_file_name("missing.toml"); let failure = cli() .args(["--format", "json", "validate", "--manifest"]) .arg(missing) .output() .unwrap(); assert_eq!(failure.status.code(), Some(2)); assert!(failure.stderr.is_empty()); let value: Value = serde_json::from_slice(&failure.stdout).unwrap(); assert_eq!(value["record_kind"], "error"); assert_eq!(value["error"]["code"], "configuration_invalid"); assert_eq!(value["error"]["exit_code"], 2);}
#[test]fn ue2_pack_inspect_validate_and_no_overwrite_use_versioned_cli() { let root = tempfile::tempdir().unwrap(); let elf = root.path().join("native.elf"); let ue2 = root.path().join("native.ue2"); fs::write(&elf, executable_riscv32_elf()).unwrap();
let packed = cli() .args(["--format", "json", "ue2", "pack", "--elf"]) .arg(&elf) .arg("--output") .arg(&ue2) .output() .unwrap(); assert!( packed.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&packed.stderr), String::from_utf8_lossy(&packed.stdout) ); assert!(packed.stderr.is_empty()); let value: Value = serde_json::from_slice(&packed.stdout).unwrap(); assert_eq!(value["schema_version"], 1); assert_eq!(value["command"], "ue2.pack"); assert_eq!(value["data"]["validation"]["policy"], "u64ii-volatile-v1"); assert_eq!( value["data"]["validation"]["inspection"]["entry_point"], 0x0300_0000 );
for arguments in [ vec!["ue2", "inspect", ue2.to_str().unwrap()], vec![ "ue2", "validate", ue2.to_str().unwrap(), "--elf", elf.to_str().unwrap(), ], ] { let output = cli() .args(["--format", "json"]) .args(arguments) .output() .unwrap(); assert!(output.status.success()); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["record_kind"], "result"); assert_eq!(value["data"]["policy"], "u64ii-volatile-v1"); }
let no_overwrite = cli() .args(["--format", "json", "ue2", "pack", "--elf"]) .arg(&elf) .arg("--output") .arg(&ue2) .output() .unwrap(); assert_eq!(no_overwrite.status.code(), Some(2)); assert!(no_overwrite.stderr.is_empty()); let value: Value = serde_json::from_slice(&no_overwrite.stdout).unwrap(); assert_eq!(value["error"]["code"], "artifact_invalid");}
#[test]fn native_prepare_creates_an_immutable_hash_bound_bundle() { let (root, manifest) = native_fixture(); let output = cli() .args(["--format", "json", "riscv", "prepare", "--manifest"]) .arg(&manifest) .output() .unwrap(); assert!(output.status.success(), "{:?}", output); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["command"], "riscv.prepare"); assert_eq!(value["data"]["project"], "cli-native-test"); assert_eq!( value["data"]["post_recovery_c64_health_check"]["sha256"], "dac6abb2ea01752e94c4318463efa6c54942713ffb021b67f1a4e1d66fa02560" ); let preparation_id = value["data"]["preparation_id"].as_str().unwrap(); let directory = root .path() .join(".ultimate-tool/preparations") .join(preparation_id); assert!(directory.join("application.elf").is_file()); assert!(directory.join("application.ue2").is_file()); assert!(directory.join("preparation.json").is_file()); assert!(directory.join("SHA256SUMS").is_file());}
#[test]fn native_standing_intent_probe_does_not_drop_blocking_client_in_async_runtime() { let (root, manifest) = native_fixture(); let prepared = cli() .args(["--format", "json", "riscv", "prepare", "--manifest"]) .arg(&manifest) .output() .unwrap(); assert!(prepared.status.success(), "{prepared:?}"); let prepared: Value = serde_json::from_slice(&prepared.stdout).unwrap(); let preparation_id = prepared["data"]["preparation_id"].as_str().unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let address = listener.local_addr().unwrap(); let server = thread::spawn(move || { for _ in 0..2 { let (mut stream, _) = listener.accept().unwrap(); let mut request = [0_u8; 2048]; let length = stream.read(&mut request).unwrap(); let request = String::from_utf8_lossy(&request[..length]); let body = if request.starts_with("GET /v1/version ") { r#"{"version":"1.0"}"# } else if request.starts_with("GET /v1/info ") { r#"{"product":"C64 Ultimate","hostname":"test-c64","unique_id":"ABC123","firmware_version":"1.1.0","fpga_version":"122","core_version":"1.49"}"# } else { panic!("unexpected request: {request}"); }; write!( stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ) .unwrap(); } });
let output = cli() .args([ "--format", "json", "riscv", "intent", "create", "--manifest", ]) .arg(&manifest) .args([ "--preparation", preparation_id, "--device", &address.to_string(), "--expect-device-id", "ABC123", "--authorization", "standing", "--confirm-standing-native-execution", ]) .output() .unwrap(); server.join().unwrap();
assert!( output.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout) ); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["command"], "riscv.intent.create"); assert_eq!(value["data"]["authorization"], "standing"); assert_eq!(value["data"]["use_count"], 0); let intent_id = value["data"]["intent_id"].as_str().unwrap(); assert!( root.path() .join(".ultimate-tool/launch-intents") .join(intent_id) .join("intent.json") .is_file() );}
#[test]fn riscv_smoke_requires_each_explicit_mutation_flag_before_io() { let output = cli() .args([ "--format", "json", "riscv", "smoke", "--device", "unreached.invalid", "--expect-device-id", "ABC123", ]) .output() .unwrap(); assert_eq!(output.status.code(), Some(2)); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("--allow-ftp-create") );}
#[test]fn jsonl_progress_and_run_inspection_are_versioned() { let (root, manifest) = fixture(); let run = cli() .args(["--format", "jsonl", "test", "smoke", "--manifest"]) .arg(&manifest) .args(["--target", "fake"]) .output() .unwrap(); assert!( run.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&run.stderr), String::from_utf8_lossy(&run.stdout) ); assert!(run.stderr.is_empty()); let lines = String::from_utf8(run.stdout).unwrap(); let values = lines .lines() .map(|line| serde_json::from_str::<Value>(line).unwrap()) .collect::<Vec<_>>(); assert!(values.len() > 2); assert_eq!(values.first().unwrap()["record_kind"], "progress"); assert_eq!(values.last().unwrap()["record_kind"], "result"); assert!(values.iter().all(|value| value["schema_version"] == 1)); let run_id = values.last().unwrap()["data"]["runs"][0]["run_id"] .as_str() .unwrap();
for arguments in [ vec!["runs", "list", "--manifest", manifest.to_str().unwrap()], vec![ "runs", "show", run_id, "--manifest", manifest.to_str().unwrap(), ], vec![ "runs", "resolve", run_id, "$B000", "--manifest", manifest.to_str().unwrap(), ], vec![ "runs", "crash-report", run_id, "--manifest", manifest.to_str().unwrap(), ], vec![ "runs", "compare", run_id, run_id, "--manifest", manifest.to_str().unwrap(), ], ] { let output = cli() .args(["--format", "json"]) .args(arguments) .output() .unwrap(); assert!(output.status.success()); assert!(output.stderr.is_empty()); let value: Value = serde_json::from_slice(&output.stdout).unwrap(); assert_eq!(value["record_kind"], "result"); if value["command"] == "runs.resolve" { assert_eq!(value["data"]["state"], "unavailable"); assert_eq!(value["data"]["address"], 0xb000); assert_eq!(value["data"]["address_hex"], "$B000"); } else if value["command"] == "runs.compare" { assert_eq!(value["data"]["state"], "unavailable"); } else if value["command"] == "runs.crash_report" { assert_eq!(value["data"]["state"], "unavailable"); assert_eq!(value["data"]["unavailable_reason"], "no_crash_state"); } }
let copied = root.path().join("copied-events.jsonl"); let evidence = cli() .args([ "--format", "json", "runs", "evidence", run_id, "events.jsonl", "--manifest", ]) .arg(&manifest) .arg("--output") .arg(&copied) .output() .unwrap(); assert!(evidence.status.success()); let value: Value = serde_json::from_slice(&evidence.stdout).unwrap(); assert_eq!(value["data"]["size"], fs::metadata(&copied).unwrap().len());
let no_overwrite = cli() .args([ "--format", "json", "runs", "evidence", run_id, "events.jsonl", "--manifest", ]) .arg(&manifest) .arg("--output") .arg(&copied) .output() .unwrap(); assert_eq!(no_overwrite.status.code(), Some(5)); assert!(no_overwrite.stderr.is_empty()); let value: Value = serde_json::from_slice(&no_overwrite.stdout).unwrap(); assert_eq!(value["error"]["code"], "evidence_failed");
let traversal = cli() .args([ "--format", "json", "runs", "evidence", run_id, "../events.jsonl", "--manifest", ]) .arg(&manifest) .output() .unwrap(); assert_eq!(traversal.status.code(), Some(5)); assert!(traversal.stderr.is_empty()); let value: Value = serde_json::from_slice(&traversal.stdout).unwrap(); assert_eq!(value["error"]["code"], "evidence_failed");}
#[test]fn strict_gate_flag_is_accepted_and_forwarded_without_io() { let (_root, manifest) = fixture();
// The probe surface advertises the shared flag without contacting anything. let probe_help = cli() .args(["probe", "ultimate", "--help"]) .output() .unwrap(); assert!(probe_help.status.success()); let help = String::from_utf8(probe_help.stdout).unwrap(); assert!(help.contains("--strict-gate"), "probe help: {help}");
// The plain fake run keeps its unchanged default. let ordinary = cli() .args(["test", "smoke", "--manifest"]) .arg(&manifest) .args(["--target", "fake"]) .output() .unwrap(); assert!( ordinary.status.success(), "stderr={} stdout={}", String::from_utf8_lossy(&ordinary.stderr), String::from_utf8_lossy(&ordinary.stdout) );
// The forwarded flag reaches the core shape gate: one fake target is not a // supported strict shape, so the run fails before any target with the core's // own strict message rather than the ordinary pass above. let strict = cli() .args(["--format", "json", "test", "smoke", "--manifest"]) .arg(&manifest) .args(["--target", "fake", "--strict-gate"]) .output() .unwrap(); assert!(!strict.status.success()); let value: Value = serde_json::from_slice(&strict.stdout).unwrap(); assert_eq!(value["error"]["code"], "configuration_invalid"); assert!( value["error"]["message"] .as_str() .unwrap() .contains("strict_gate"), "message: {value}" );}