Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Unit tests: Individual components (scanner, parser, evaluator)
  2. Integration tests: Complete assembly of programs
  3. 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