#![cfg(feature = "cli")]
use std::path::PathBuf;
use std::process::{Command, Output, Stdio};
fn checksmix() -> Command {
Command::new(env!("CARGO_BIN_EXE_checksmix"))
}
fn mmixasm() -> Command {
Command::new(env!("CARGO_BIN_EXE_mmixasm"))
}
fn mmixdb() -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_mmixdb"));
cmd.stdin(Stdio::null());
cmd
}
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join(name)
}
fn assert_exit_1_names_input(out: &Output, input: &str) {
assert_eq!(
out.status.code(),
Some(1),
"expected exit 1; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains(input),
"stderr should name '{input}'; stderr: {stderr}"
);
assert!(
stderr.contains("contributed no source"),
"stderr should say 'contributed no source'; stderr: {stderr}"
);
}
#[test]
fn check_clean_two_file_program() {
let out = checksmix()
.args(["check"])
.arg(fixture("multi_main.mms"))
.arg(fixture("multi_lib.mms"))
.output()
.unwrap();
assert!(
out.status.success(),
"check should succeed; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
out.stdout.is_empty(),
"check should produce no stdout on success"
);
}
#[test]
fn check_undefined_symbol_exits_nonzero() {
let out = checksmix()
.args(["check"])
.arg(fixture("undef_ref.mms"))
.output()
.unwrap();
assert!(
!out.status.success(),
"check should fail on undefined symbol"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("NoSuchLabel"),
"error should name the undefined symbol; stderr: {stderr}"
);
assert!(
stderr.contains("undef_ref.mms:"),
"error should contain the source file name; stderr: {stderr}"
);
}
#[test]
fn check_duplicate_global_names_both_files() {
let out = checksmix()
.args(["check"])
.arg(fixture("dup_global_a.mms"))
.arg(fixture("dup_global_b.mms"))
.output()
.unwrap();
assert!(
!out.status.success(),
"check should fail on duplicate symbol"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("dup_global_a.mms"),
"error should mention first-definition file; stderr: {stderr}"
);
assert!(
stderr.contains("dup_global_b.mms"),
"error should mention redefinition file; stderr: {stderr}"
);
}
#[test]
fn build_produces_mmo_and_run_succeeds() {
let tmp_mmo = std::env::temp_dir().join("checksmix_test_hello.mmo");
let build_out = checksmix()
.args(["build", "-o"])
.arg(&tmp_mmo)
.arg(fixture("hello.mms"))
.output()
.unwrap();
assert!(
build_out.status.success(),
"build should succeed; stderr: {}",
String::from_utf8_lossy(&build_out.stderr)
);
let stdout = String::from_utf8_lossy(&build_out.stdout);
let printed_path = stdout.trim();
assert!(
printed_path.ends_with("checksmix_test_hello.mmo"),
"build should print the output path; got: {printed_path}"
);
assert!(tmp_mmo.exists(), "output .mmo file must exist");
let run_status = checksmix().args(["run"]).arg(&tmp_mmo).status().unwrap();
assert!(run_status.success(), "run of built .mmo should succeed");
let _ = std::fs::remove_file(&tmp_mmo);
}
#[test]
fn run_mmo_bare_invocation() {
let tmp_mmo = std::env::temp_dir().join("checksmix_test_bare_run.mmo");
let build_status = checksmix()
.args(["build", "-o"])
.arg(&tmp_mmo)
.arg(fixture("hello.mms"))
.status()
.unwrap();
assert!(build_status.success());
let run_output = checksmix().arg(&tmp_mmo).output().unwrap();
assert!(
run_output.status.success(),
"bare run of .mmo should succeed"
);
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(
!stdout.contains("Debug:"),
"loading a .mmo emitted debug output:\n{stdout}"
);
let _ = std::fs::remove_file(&tmp_mmo);
}
#[test]
fn run_multi_source_mms() {
let status = checksmix()
.args(["run"])
.arg(fixture("multi_main.mms"))
.arg(fixture("multi_lib.mms"))
.status()
.unwrap();
assert!(status.success(), "run of multi-source .mms should succeed");
}
#[test]
fn run_illegal_instruction_exits_1() {
let status = checksmix()
.args(["run"])
.arg(fixture("put_rc_halts.mms"))
.status()
.unwrap();
assert_eq!(status.code(), Some(1), "a diagnostic halt (PUT rC) exits 1");
}
#[test]
fn run_halt_trap_exits_with_its_own_255_value() {
let status = checksmix()
.args(["run"])
.arg(fixture("halt_with_255.mms"))
.status()
.unwrap();
assert_eq!(
status.code(),
Some(255),
"TRAP 0,Halt,0 exits with $255, not 1"
);
}
#[test]
fn run_all_instructions_test_has_no_debug_write_byte_noise() {
let example = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("all_instructions_test.mms");
let out = checksmix().arg(&example).output().unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!stderr.contains("DBG write_byte"),
"stderr should not contain leftover debug output from write_byte; stderr: {}",
stderr
);
}
fn find_lop_post(mmo_data: &[u8]) -> usize {
mmo_data
.windows(2)
.position(|w| w[0] == 0x98 && w[1] == 0x0A)
.expect("built .mmo must have a lop_post record")
}
#[test]
fn each_binarys_build_of_a_program_with_a_greg_carries_it_in_the_postamble() {
let cases: [(Command, &[&str], &str); 2] = [
(
checksmix(),
&["build", "-o"],
"checksmix_test_greg_build.mmo",
),
(mmixasm(), &["-o"], "checksmix_test_greg_mmixasm.mmo"),
];
for (mut cmd, args, tmp_name) in cases {
let tmp_mmo = std::env::temp_dir().join(tmp_name);
let build_out = cmd
.args(args)
.arg(&tmp_mmo)
.arg(fixture("greg_postamble.mms"))
.output()
.unwrap();
assert!(
build_out.status.success(),
"build should succeed; stderr: {}",
String::from_utf8_lossy(&build_out.stderr)
);
let mmo_data = std::fs::read(&tmp_mmo).unwrap();
let post = find_lop_post(&mmo_data);
assert_eq!(mmo_data[post + 3], 254, "G must be 254 for one GREG");
let reg254 = u64::from_be_bytes(mmo_data[post + 4..post + 12].try_into().unwrap());
assert_eq!(reg254, 1000, "$254 must carry the GREG's value");
let _ = std::fs::remove_file(&tmp_mmo);
}
}
#[test]
fn run_greg_base_address_fixture_passes() {
let out = checksmix()
.args(["run"])
.arg(fixture("greg_base_address.mms"))
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("All tests passed!"),
"stdout should report success; stdout: {stdout}"
);
assert_eq!(out.status.code(), Some(0), "the fixture halts with $255=0");
}
#[test]
fn check_empty_file_exits_one() {
let out = checksmix()
.args(["check"])
.arg(fixture("empty.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "empty.mms");
}
#[test]
fn mmixasm_empty_file_exits_one() {
let out = mmixasm().arg(fixture("empty.mms")).output().unwrap();
assert_exit_1_names_input(&out, "empty.mms");
}
#[test]
fn mmixdb_empty_file_exits_one() {
let out = mmixdb().arg(fixture("empty.mms")).output().unwrap();
assert_exit_1_names_input(&out, "empty.mms");
}
#[test]
fn check_whitespace_only_file_exits_one() {
let out = checksmix()
.args(["check"])
.arg(fixture("whitespace_only.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "whitespace_only.mms");
}
#[test]
fn mmixasm_whitespace_only_file_exits_one() {
let out = mmixasm()
.arg(fixture("whitespace_only.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "whitespace_only.mms");
}
#[test]
fn mmixdb_whitespace_only_file_exits_one() {
let out = mmixdb()
.arg(fixture("whitespace_only.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "whitespace_only.mms");
}
#[test]
fn check_include_of_empty_file_exits_one() {
let out = checksmix()
.args(["check"])
.arg(fixture("include_empty.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "include_empty.mms");
}
#[test]
fn mmixasm_include_of_empty_file_exits_one() {
let out = mmixasm()
.arg(fixture("include_empty.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "include_empty.mms");
}
#[test]
fn mmixdb_include_of_empty_file_exits_one() {
let out = mmixdb().arg(fixture("include_empty.mms")).output().unwrap();
assert_exit_1_names_input(&out, "include_empty.mms");
}
#[test]
fn run_empty_file_exits_one_without_doubled_prefix() {
let out = checksmix()
.args(["run"])
.arg(fixture("empty.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "empty.mms");
let stderr = String::from_utf8_lossy(&out.stderr);
assert_eq!(
stderr.matches("Error: ").count(),
1,
"run_mms prefixes the error itself; embedding a second prefix in the \
guard would double it; stderr: {stderr}"
);
}
#[test]
fn build_empty_file_exits_one() {
let out = checksmix()
.args(["build"])
.arg(fixture("empty.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "empty.mms");
}
#[test]
fn check_empty_file_among_others_still_exits_one() {
let out = checksmix()
.args(["check"])
.arg(fixture("empty.mms"))
.arg(fixture("hello.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "empty.mms");
}
#[test]
fn mmixasm_empty_file_among_others_exits_one_without_banner() {
let out = mmixasm()
.arg(fixture("empty.mms"))
.arg(fixture("hello.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "empty.mms");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("Assembling 0 inputs:"),
"the per-path guard fires before the input-count banner; stdout: {stdout}"
);
}
#[test]
fn mmixdb_empty_file_among_others_still_exits_one() {
let out = mmixdb()
.arg(fixture("empty.mms"))
.arg(fixture("hello.mms"))
.output()
.unwrap();
assert_exit_1_names_input(&out, "empty.mms");
}
#[test]
fn check_comment_only_file_exits_zero() {
let status = checksmix()
.args(["check"])
.arg(fixture("comment_only.mms"))
.status()
.unwrap();
assert!(status.success(), "a comment-only file is a valid program");
}
#[test]
fn check_defs_only_file_exits_zero() {
let status = checksmix()
.args(["check"])
.arg(fixture("defs_only.mms"))
.status()
.unwrap();
assert!(
status.success(),
"a definitions-only module has zero instructions and is still valid"
);
}
#[test]
fn build_defs_only_file_fails_on_instruction_count_not_the_source_guard() {
let out = checksmix()
.args(["build"])
.arg(fixture("defs_only.mms"))
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("no instructions"),
"build's existing zero-instruction guard must still fire; stderr: {stderr}"
);
assert!(
!stderr.contains("contributed no source"),
"defs_only.mms resolves to a unit; the new guard must not fire; stderr: {stderr}"
);
}