use super::*;
#[test]
fn resolve_includes_single_include_inserts_a_unit() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let units = MMixAssembler::resolve_includes(
"INCLUDE lib.mms\nOCTA 2\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
assert_eq!(units.len(), 2);
assert_eq!(units[0].0, "lib.mms");
assert!(units[0].1.contains("OCTA 1"));
assert_eq!(units[1].0, "root.mms");
assert!(units[1].1.contains("OCTA 2"));
}
#[test]
fn resolve_includes_filename_fidelity() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let units = MMixAssembler::resolve_includes(
"INCLUDE lib.mms\nOCTA 2\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
assert_eq!(units[0].0, "lib.mms");
assert_ne!(units[0].0, "root.mms");
}
#[test]
fn resolve_includes_pads_line_numbers_after_an_include() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let units = MMixAssembler::resolve_includes(
"% a\nINCLUDE lib.mms\nBYTE 0\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
let host_segment = &units.last().unwrap().1;
assert_eq!(host_segment.matches('\n').count() - 1, 2);
assert!(host_segment.starts_with("\n\nBYTE 0"));
let mut asm = MMixAssembler::new(&units[0].1, &units[0].0);
for (name, src) in units.iter().skip(1) {
asm.add_source(src, name);
}
asm.parse().unwrap();
assert_eq!(asm.addr_for_line("root.mms", 3), Some(8));
}
#[test]
fn resolve_includes_nested_relative_resolution() {
let reader = fixture_reader(vec![
("a/sub/b.mms", "INCLUDE c.mms\nOCTA 2\n"),
("a/sub/c.mms", "OCTA 3\n"),
]);
let units = MMixAssembler::resolve_includes(
"INCLUDE sub/b.mms\nOCTA 1\n",
"a/root.mms",
std::path::Path::new("a"),
&reader,
)
.unwrap();
let c_unit = units
.iter()
.find(|(name, _)| name.contains("c.mms"))
.expect("c.mms unit present");
assert_eq!(c_unit.0, "a/sub/c.mms");
assert!(c_unit.1.contains("OCTA 3"));
}
#[test]
fn resolve_includes_cycle_is_an_error() {
let reader = fixture_reader(vec![
("a.mms", "INCLUDE b.mms\n"),
("b.mms", "INCLUDE a.mms\n"),
]);
let err = MMixAssembler::resolve_includes(
"INCLUDE a.mms\n",
"driver.mms",
std::path::Path::new(""),
&reader,
)
.unwrap_err();
assert!(err.contains("cycle"));
assert!(err.contains("a.mms"));
assert!(err.contains("b.mms"));
}
#[test]
fn resolve_includes_missing_file_is_an_error_not_a_panic() {
let reader = fixture_reader(vec![]);
let err = MMixAssembler::resolve_includes(
"INCLUDE missing.mms\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap_err();
assert!(err.contains("missing.mms"));
}
#[test]
fn resolve_includes_passthrough_preserves_content_with_no_include() {
let reader = fixture_reader(vec![]);
let source = "OCTA 1\nOCTA 2";
let units =
MMixAssembler::resolve_includes(source, "root.mms", std::path::Path::new(""), &reader)
.unwrap();
assert_eq!(units.len(), 1);
assert_eq!(units[0].0, "root.mms");
assert_eq!(units[0].1, source);
}
#[test]
fn resolve_includes_recognizes_comment_case() {
let reader = fixture_reader(vec![("lib.mms", "OCTA 1\n")]);
let lower_with_comment = MMixAssembler::resolve_includes(
"include lib.mms % pull it in\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
let quoted = MMixAssembler::resolve_includes(
"INCLUDE \"lib.mms\"\n",
"root.mms",
std::path::Path::new(""),
&reader,
)
.unwrap();
assert_eq!(lower_with_comment.len(), 1);
assert_eq!(lower_with_comment[0].1, "include lib.mms % pull it in\n");
assert_eq!(quoted.len(), 1);
assert_eq!(quoted[0].1, "OCTA 1\n");
assert!(quoted[0].1.contains("OCTA 1"));
}
const BOM: &str = "\u{FEFF}";
fn assemble_units(units: &[(String, String)]) -> MMixAssembler {
let mut asm = MMixAssembler::new(&units[0].1, &units[0].0);
for (name, src) in &units[1..] {
asm.add_source(src, name);
}
asm.parse().unwrap();
asm
}
#[test]
fn a_leading_byte_order_mark_leaves_the_first_line_in_force() {
let plain = "LOC #100\nMain SETL $1,5\n";
let mut with_mark = MMixAssembler::new(&format!("{BOM}{plain}"), "<test>");
with_mark.parse().unwrap();
let mut without_mark = MMixAssembler::new(plain, "<test>");
without_mark.parse().unwrap();
assert_eq!(with_mark.instructions, without_mark.instructions);
assert_eq!(with_mark.instructions[0].0, 0x100);
assert_eq!(with_mark.labels.get("Main"), Some(&0x100));
}
#[test]
fn a_first_line_error_names_the_same_column_with_or_without_a_byte_order_mark() {
let plain = "SET $1,-010\n";
let with_mark = assemble_err(&format!("{BOM}{plain}"));
assert_eq!(with_mark, assemble_err(plain));
assert!(with_mark.starts_with("<test>:1:8:"), "err: {with_mark}");
}
#[test]
fn source_text_carries_no_byte_order_mark() {
let mut asm = MMixAssembler::new(&format!("{BOM}LOC #100\nSWYM\n"), "<test>");
asm.add_source(&format!("{BOM}SWYM\n"), "second");
asm.parse().unwrap();
assert_eq!(asm.source_text("<test>", 1), Some("LOC #100"));
assert_eq!(asm.source_text("second", 1), Some("SWYM"));
}
#[test]
fn only_the_leading_mark_is_dropped() {
let mut asm = MMixAssembler::new(&format!("{BOM}SWYM\nBYTE \"{BOM}\"\n"), "<test>");
asm.parse().unwrap();
assert_eq!(
asm.source_text("<test>", 2),
Some(&format!("BYTE \"{BOM}\"")[..])
);
}
#[test]
fn a_byte_order_mark_on_an_add_source_unit_is_dropped() {
let mut with_mark = MMixAssembler::new("SWYM\n", "first");
with_mark.add_source(&format!("{BOM}LOC #200\nSWYM\n"), "second");
with_mark.parse().unwrap();
assert_eq!(with_mark.addr_for_line("second", 2), Some(0x200));
}
fn expand(root: &str, lib: &str) -> Vec<(String, String)> {
let reader = fixture_reader(vec![("lib.mms", lib), ("inner.mms", "OCTA 2\n")]);
MMixAssembler::resolve_includes(root, "root.mms", std::path::Path::new(""), &reader).unwrap()
}
#[test]
fn an_included_file_with_a_byte_order_mark_and_an_include_on_line_one_expands() {
let lib = "INCLUDE inner.mms\nOCTA 1\n";
let root = "INCLUDE lib.mms\nOCTA 3\n";
let with_mark = expand(root, &format!("{BOM}{lib}"));
let without_mark = expand(root, lib);
assert_eq!(with_mark, without_mark);
assert_eq!(with_mark.len(), 3);
assert_eq!(assemble_units(&with_mark).instructions.len(), 3);
}
#[test]
fn a_root_file_with_a_byte_order_mark_and_an_include_on_line_one_expands() {
let root = "INCLUDE lib.mms\nOCTA 3\n";
let with_mark = expand(&format!("{BOM}{root}"), "OCTA 1\n");
assert_eq!(with_mark, expand(root, "OCTA 1\n"));
assert_eq!(with_mark.len(), 2);
assert_eq!(with_mark[0].0, "lib.mms");
}
#[test]
fn an_included_unit_reaches_the_assembler_without_a_byte_order_mark() {
let with_mark = expand("INCLUDE lib.mms\n", &format!("{BOM}LOC #300\nOCTA 1\n"));
assert_eq!(
assemble_units(&with_mark).addr_for_line("lib.mms", 2),
Some(0x300)
);
}
#[test]
fn a_second_byte_order_mark_is_text_on_every_path() {
let source = format!("{BOM}{BOM}LOC #100\nMain SETL $1,5\n");
let mut direct = MMixAssembler::new(&source, "root.mms");
direct.parse().unwrap();
let resolved = expand(&source, "");
let through_includes = assemble_units(&resolved);
assert_eq!(direct.instructions, through_includes.instructions);
assert_eq!(direct.instructions[0].0, 0);
assert_eq!(
direct.source_text("root.mms", 1),
Some(&format!("{BOM}LOC #100")[..])
);
assert_eq!(
through_includes.source_text("root.mms", 1),
direct.source_text("root.mms", 1)
);
}
#[test]
fn a_file_of_one_byte_order_mark_contributes_no_unit() {
assert!(expand(BOM, "").is_empty());
assert!(!expand(&format!("{BOM}{BOM}"), "").is_empty());
}
#[test]
fn a_byte_order_mark_alone_on_a_later_line_is_not_blank() {
let root = format!("INCLUDE lib.mms\n{BOM}\n");
assert_eq!(expand(&root, "OCTA 1\n").len(), 2);
}