Chapter 13: Testing the Assembler
In this chapter, we’ll write tests to verify our assembler produces correct machine code.
Testing Strategy
We’ll test at multiple levels:
- Unit tests: Individual components (scanner, parser, evaluator)
- Integration tests: Complete assembly of programs
- Comparison tests: Compare output to known-good binaries
Scanner Tests
Test that the scanner produces correct tokens:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod scanner_tests {
use byte_asm::scanner::*;
#[test]
fn test_number_formats() {
let mut scanner = Scanner::new("0xFF 0b1010 42");
let tok = scanner.scan_token().unwrap();
assert_eq!(tok.kind, TokenKind::Number);
assert_eq!(tok.number(), Some(255));
let tok = scanner.scan_token().unwrap();
assert_eq!(tok.kind, TokenKind::Number);
assert_eq!(tok.number(), Some(10));
let tok = scanner.scan_token().unwrap();
assert_eq!(tok.kind, TokenKind::Number);
assert_eq!(tok.number(), Some(42));
}
#[test]
fn test_instruction() {
let mut scanner = Scanner::new("lda");
let tok = scanner.scan_token().unwrap();
assert_eq!(tok.kind, TokenKind::Instruction);
}
#[test]
fn test_directive() {
let mut scanner = Scanner::new(".org");
let tok = scanner.scan_token().unwrap();
assert_eq!(tok.kind, TokenKind::Directive);
}
#[test]
fn test_local_label() {
let mut scanner = Scanner::new(".loop @temp");
let tok = scanner.scan_token().unwrap();
assert_eq!(tok.kind, TokenKind::LocalLabel);
let tok = scanner.scan_token().unwrap();
assert_eq!(tok.kind, TokenKind::LocalLabel);
}
}
}
Parser Tests
Test that parsing produces correct AST:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod parser_tests {
use byte_asm::parser;
use byte_asm::ast::*;
#[test]
fn test_parse_instruction() {
let program = parser::parse("lda #0x42").unwrap();
assert_eq!(program.statements.len(), 1);
match &program.statements[0] {
Statement::Instruction(i) => {
assert!(matches!(i.operand, Some(Operand::Immediate(_))));
}
_ => panic!("Expected instruction"),
}
}
#[test]
fn test_parse_label() {
let program = parser::parse("main:").unwrap();
match &program.statements[0] {
Statement::Label(l) => {
assert_eq!(l.name, "main");
assert!(!l.is_local);
}
_ => panic!("Expected label"),
}
}
#[test]
fn test_parse_addressing_modes() {
// Immediate
let p = parser::parse("lda #0x42").unwrap();
assert!(matches!(
&p.statements[0],
Statement::Instruction(i) if matches!(i.operand, Some(Operand::Immediate(_)))
));
// Zero Page / Absolute
let p = parser::parse("lda 0x80").unwrap();
assert!(matches!(
&p.statements[0],
Statement::Instruction(i) if matches!(i.operand, Some(Operand::Address(_)))
));
// Indirect X
let p = parser::parse("lda (0x80,x)").unwrap();
assert!(matches!(
&p.statements[0],
Statement::Instruction(i) if matches!(i.operand, Some(Operand::IndirectX(_)))
));
// Indirect Y
let p = parser::parse("lda (0x80),y").unwrap();
assert!(matches!(
&p.statements[0],
Statement::Instruction(i) if matches!(i.operand, Some(Operand::IndirectY(_)))
));
}
}
}
Assembler Tests
Test that assembly produces correct bytes:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod assembler_tests {
use byte_asm::{parser, Assembler};
fn assemble(source: &str) -> Vec<u8> {
let program = parser::parse(source).unwrap();
let mut asm = Assembler::new();
asm.assemble(&program).unwrap()
}
#[test]
fn test_nop() {
let binary = assemble(".org 0x8000\nnop");
assert_eq!(binary[0], 0xEA);
}
#[test]
fn test_lda_immediate() {
let binary = assemble(".org 0x8000\nlda #0x42");
assert_eq!(&binary[0..2], &[0xA9, 0x42]);
}
#[test]
fn test_lda_zero_page() {
let binary = assemble(".org 0x8000\nlda 0x80");
assert_eq!(&binary[0..2], &[0xA5, 0x80]);
}
#[test]
fn test_lda_absolute() {
let binary = assemble(".org 0x8000\nlda 0x2000");
assert_eq!(&binary[0..3], &[0xAD, 0x00, 0x20]);
}
#[test]
fn test_jmp() {
let binary = assemble(".org 0x8000\njmp 0x9000");
assert_eq!(&binary[0..3], &[0x4C, 0x00, 0x90]);
}
#[test]
fn test_label_resolution() {
let source = r#"
.org 0x8000
start:
jmp end
nop
end:
rts
"#;
let binary = assemble(source);
// JMP end = 4C 05 80 (end is at 0x8005)
assert_eq!(&binary[0..3], &[0x4C, 0x05, 0x80]);
}
#[test]
fn test_branch() {
let source = r#"
.org 0x8000
ldx #5
loop:
dex
bne loop
"#;
let binary = assemble(source);
// BNE loop: D0 FD (offset -3)
assert_eq!(&binary[3..5], &[0xD0, 0xFD]);
}
#[test]
fn test_db_bytes() {
let binary = assemble(".org 0x8000\n.db 0x01, 0x02, 0x03");
assert_eq!(&binary[0..3], &[0x01, 0x02, 0x03]);
}
#[test]
fn test_db_string() {
let binary = assemble(".org 0x8000\n.db \"Hi\", 0");
assert_eq!(&binary[0..3], &[0x48, 0x69, 0x00]);
}
#[test]
fn test_dw() {
let binary = assemble(".org 0x8000\n.dw 0x1234");
assert_eq!(&binary[0..2], &[0x34, 0x12]); // Little-endian
}
#[test]
fn test_equ() {
let source = r#"
.equ VALUE 0x42
.org 0x8000
lda #VALUE
"#;
let binary = assemble(source);
assert_eq!(&binary[0..2], &[0xA9, 0x42]);
}
#[test]
fn test_expressions() {
let source = r#"
.equ BASE 0x1000
.org 0x8000
lda #>BASE
lda #<BASE
lda #10 + 5
"#;
let binary = assemble(source);
assert_eq!(&binary[0..6], &[0xA9, 0x10, 0xA9, 0x00, 0xA9, 0x0F]);
}
}
}
Integration Tests
Test complete programs:
#![allow(unused)]
fn main() {
// tests/integration.rs
use byte_asm::{parser, Assembler};
use std::fs;
fn assemble_file(path: &str) -> Vec<u8> {
let source = fs::read_to_string(path).unwrap();
let program = parser::parse(&source).unwrap();
let mut asm = Assembler::new();
asm.assemble(&program).unwrap()
}
#[test]
fn test_basic_program() {
let binary = assemble_file("tests/fixtures/basic.s");
assert!(!binary.is_empty());
}
#[test]
fn test_all_addressing_modes() {
let binary = assemble_file("tests/fixtures/addressing_modes.s");
assert!(!binary.is_empty());
}
}
Test Fixtures
Create test assembly files:
; tests/fixtures/basic.s
.org 0x8000
start:
lda #0x42
sta 0x00
nop
brk
.org 0xFFFC
.dw start
; tests/fixtures/addressing_modes.s
.org 0x8000
nop
asl a
lda #0xFF
lda 0x80
lda 0x80,x
lda 0x2000
lda 0x2000,x
lda 0x2000,y
lda (0x80,x)
lda (0x80),y
.org 0xFFFC
.dw 0x8000
Running Tests
# Run all tests
cargo test -p byte_asm
# Run specific test
cargo test -p byte_asm test_lda_immediate
# Run with output
cargo test -p byte_asm -- --nocapture
Test Output
running 22 tests
test assembler::codegen::tests::test_absolute ... ok
test assembler::codegen::tests::test_accumulator ... ok
test assembler::codegen::tests::test_immediate ... ok
test assembler::codegen::tests::test_implied ... ok
test assembler::codegen::tests::test_indexed_x ... ok
test assembler::codegen::tests::test_indirect_x ... ok
test assembler::codegen::tests::test_indirect_y ... ok
test assembler::codegen::tests::test_zero_page ... ok
test assembler::directives::tests::test_db_bytes ... ok
test assembler::directives::tests::test_db_string ... ok
test assembler::directives::tests::test_dw ... ok
test assembler::directives::tests::test_equ ... ok
test assembler::directives::tests::test_org ... ok
test assembler::eval::tests::test_binary_ops ... ok
test assembler::eval::tests::test_current_address ... ok
test assembler::eval::tests::test_identifier ... ok
test assembler::eval::tests::test_number ... ok
test assembler::eval::tests::test_unary_ops ... ok
test symbol::tests::test_constants ... ok
test symbol::tests::test_define_and_lookup ... ok
test symbol::tests::test_duplicate_symbol ... ok
test symbol::tests::test_local_labels ... ok
test result: ok. 22 passed; 0 failed
Summary
In this chapter, we wrote comprehensive tests:
- Scanner tests: Token recognition for all types
- Parser tests: AST structure for statements and operands
- Assembler tests: Machine code output verification
- Integration tests: Complete program assembly
- Test fixtures: Reusable assembly test files
In the final chapter, we’ll put it all together with a complete game example.
Previous: Chapter 12 - The CLI | Next: Chapter 14 - Complete Example