import unittest from hdc.x86 import BIOS, VIDEO_BASE, Boot, CPU, Halt, boot_sector def load(cpu: CPU, code: list[int]) -> None: for i, byte in enumerate(code): cpu.write8(i, byte) cpu.ip = 0 cpu.seg["CS"] = 0 class TestRegisters(unittest.TestCase): def test_eight_bit_moves_hit_both_halves(self): cpu = CPU() load(cpu, [0xB8, 0x34, 0x12, 0xB4, 0x56]) # mov ax,1234h ; mov ah,56h cpu.step() self.assertEqual(cpu.r[0], 0x1234) cpu.step() self.assertEqual(cpu.al, 0x34) self.assertEqual(cpu.ah, 0x56) def test_mov_ds_from_ax(self): cpu = CPU() load(cpu, [0xB8, 0xC0, 0x07, 0x8E, 0xD8]) # mov ax,07C0h ; mov ds,ax cpu.step() cpu.step() self.assertEqual(cpu.seg["DS"], 0x07C0) def test_segment_offset_addressing(self): cpu = CPU() cpu.write8(cpu.physical(0x07C0, 0x18), ord("H")) self.assertEqual(cpu.read8(cpu.physical(0x07C0, 0x18)), ord("H")) class TestDisplacements(unittest.TestCase): def test_short_jump_loops_in_place(self): cpu = CPU() load(cpu, [0xEB, 0xFE]) # jmp $ for _ in range(4): cpu.step() self.assertEqual(cpu.ip, 0) def test_backwards_jump_lands_on_the_instruction(self): cpu = CPU() load(cpu, [0x90, 0xEB, 0xFD]) # nop ; jmp -3 cpu.step() cpu.step() self.assertEqual(cpu.ip, 0) def test_conditional_jump_uses_the_flag(self): cpu = CPU() load(cpu, [0x31, 0xC0, 0x74, 0x02]) # xor ax,ax ; jz +2 cpu.step() cpu.step() self.assertEqual(cpu.ip, 6) def test_taken_and_not_taken_paths(self): cpu = CPU() load(cpu, [0xB8, 0x01, 0x00, 0x74, 0x02]) # mov ax,1 ; jz +2 cpu.step() cpu.step() self.assertEqual(cpu.ip, 5) # not taken class TestVideo(unittest.TestCase): def test_teletype_writes_the_video_buffer(self): cpu = CPU() bios = BIOS(cpu) bios.post() cpu.ah, cpu.al = 0x0E, ord("A") bios.video(cpu) self.assertEqual(chr(cpu.read8(VIDEO_BASE)), "A") def test_newline_moves_the_cursor(self): cpu = CPU() bios = BIOS(cpu) bios.post() bios.write("AB\r\nCD") self.assertEqual(bios.screen()[0], "AB") self.assertEqual(bios.screen()[1].rstrip(), "CD") def test_long_line_wraps(self): cpu = CPU() bios = BIOS(cpu) bios.post() bios.write("X" * 81) self.assertEqual(cpu.cursor, [1, 1]) def test_scrolling_keeps_the_latest_line(self): cpu = CPU() bios = BIOS(cpu) bios.post() for i in range(30): bios.write("line %d\n" % i) visible = [line for line in bios.screen() if line.strip()] self.assertIn("line 29", visible[-1]) self.assertLessEqual(len(visible), 25) class TestDisk(unittest.TestCase): def test_bad_signature_raises(self): cpu = CPU() bios = BIOS(cpu, b"\x00" * 512) bios.post() with self.assertRaises(Halt): bios.boot_attempt(cpu) def test_read_sector_lands_at_es_bx(self): cpu = CPU() bios = BIOS(cpu, bytes(range(256)) * 2) bios.post() cpu.ah, cpu.al = 0x02, 1 cpu.r[1] = 0x0001 # CX: cylinder 0, sector 1 cpu.r[2] = 0x0000 # DX: head 0 cpu.seg["ES"] = 0x1000 cpu.r[3] = 0x0000 # BX bios.read_sector(cpu) self.assertEqual(cpu.read8(0x10000), 0) self.assertEqual(cpu.read8(0x10001), 1) self.assertFalse(cpu.flags & 0x0001) def test_failed_read_sets_carry(self): cpu = CPU() bios = BIOS(cpu, b"") bios.post() cpu.ah = 0x02 bios.read_sector(cpu) self.assertTrue(cpu.flags & 0x0001) class TestBootChain(unittest.TestCase): def test_sector_has_the_signature(self): sector = boot_sector("hi") self.assertEqual(len(sector), 512) self.assertEqual(sector[510:512], b"\x55\xAA") def test_boot_reaches_the_kernel_handoff(self): machine = Boot() machine.post() result = machine.boot() self.assertEqual(result["reason"], "kernel") self.assertEqual(result["stage"], "kernel") self.assertGreater(result["instructions"], 20) def test_banner_is_written_to_video_memory(self): machine = Boot("HELLO KERNEL") machine.post() machine.boot() text = "\n".join(machine.screen()) self.assertIn("HELLO KERNEL", text) self.assertIn("POST", text) def test_ivt_is_populated(self): machine = Boot() machine.post() self.assertNotEqual(machine.cpu.read16(0x10 * 4), 0) self.assertNotEqual(machine.cpu.read16(0x10 * 4 + 2), 0) def test_oversized_banner_is_rejected(self): with self.assertRaises(ValueError): boot_sector("x" * 600) if __name__ == "__main__": unittest.main()