From aa15ec4ceb34af7cbd692f752b30720bfd5e3e8b Mon Sep 17 00:00:00 2001 From: Will Bates Date: Sat, 4 Apr 2026 22:07:44 -0400 Subject: [PATCH] Fix: use if/else pattern for skip() guards in unit tests Nim's skip() template marks a test as SKIPPED but does not stop execution -- the test body continues running. Tests that called skip() and then used check() after it were being marked FAILED rather than SKIPPED when optional native libraries (Capstone, Keystone, Unicorn) were absent. Fixed by wrapping the test body in an else branch so checks only execute when the library is actually available: test_assembler.nim - Keystone-dependent tests (6 tests) test_disassembler.nim - Capstone-dependent tests (6 tests), also removed the now-redundant requireCapstone helper template test_emulator.nim - Unicorn-dependent tests (8 tests) test_patcher.nim - Keystone/Unicorn-dependent tests (5 tests) test_runtime.nim - Qualified ambiguous calls (attachProcess, detachProcess, etc.) with runtime. prefix to fix compile error when process module is also imported --- tests/test_assembler.nim | 46 +++++++------ tests/test_disassembler.nim | 105 +++++++++++++++-------------- tests/test_emulator.nim | 131 +++++++++++++++++++----------------- tests/test_patcher.nim | 65 +++++++++--------- tests/test_runtime.nim | 12 ++-- 5 files changed, 192 insertions(+), 167 deletions(-) diff --git a/tests/test_assembler.nim b/tests/test_assembler.nim index 69ceb6e..bb12865 100644 --- a/tests/test_assembler.nim +++ b/tests/test_assembler.nim @@ -19,46 +19,52 @@ suite "Assembler Module Tests": test "assembleInstruction encodes NOP correctly": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("nop", archX64) - check bytes.len == 1 - check bytes[0] == 0x90'u8 + else: + let bytes = assembleInstruction("nop", archX64) + check bytes.len == 1 + check bytes[0] == 0x90'u8 test "assembleInstruction encodes RET correctly": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("ret", archX64) - check bytes.len == 1 - check bytes[0] == 0xC3'u8 + else: + let bytes = assembleInstruction("ret", archX64) + check bytes.len == 1 + check bytes[0] == 0xC3'u8 test "assembleInstruction returns empty seq for invalid instruction": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("notavalidinstruction xyz_!!!", archX64) - check bytes.len == 0 + else: + let bytes = assembleInstruction("notavalidinstruction xyz_!!!", archX64) + check bytes.len == 0 test "assembleBlock encodes multiple semicolon-separated instructions": if not isKeystoneAvailable(): skip() - let output = assembleBlock("nop; nop; ret", archX64) - check output.bytes.len == 3 - check output.statCount == 3 - check output.bytes[0] == 0x90'u8 - check output.bytes[1] == 0x90'u8 - check output.bytes[2] == 0xC3'u8 + else: + let output = assembleBlock("nop; nop; ret", archX64) + check output.bytes.len == 3 + check output.statCount == 3 + check output.bytes[0] == 0x90'u8 + check output.bytes[1] == 0x90'u8 + check output.bytes[2] == 0xC3'u8 test "assembleBlock returns empty output for invalid instructions": if not isKeystoneAvailable(): skip() - let output = assembleBlock("notvalid !!!", archX64) - check output.bytes.len == 0 - check output.statCount == 0 + else: + let output = assembleBlock("notvalid !!!", archX64) + check output.bytes.len == 0 + check output.statCount == 0 test "assembleInstruction handles x86 32-bit mode": if not isKeystoneAvailable(): skip() - let bytes = assembleInstruction("nop", archX86) - check bytes.len == 1 - check bytes[0] == 0x90'u8 + else: + let bytes = assembleInstruction("nop", archX86) + check bytes.len == 1 + check bytes[0] == 0x90'u8 test "makeNops generates the correct number of NOP bytes": let nops = makeNops(4) diff --git a/tests/test_disassembler.nim b/tests/test_disassembler.nim index 668b85a..ac61625 100644 --- a/tests/test_disassembler.nim +++ b/tests/test_disassembler.nim @@ -6,13 +6,6 @@ # unconditionally. import unittest, disassembler, binary -# --------------------------------------------------------------------------- -# Helper: skip a test when Capstone is not installed. -# --------------------------------------------------------------------------- -template requireCapstone() = - if not isCapstoneAvailable(): - skip() - # --------------------------------------------------------------------------- # Suite: Capstone availability probe # --------------------------------------------------------------------------- @@ -34,59 +27,71 @@ suite "disassembleBytes": check result.len == 0 test "NOP byte (0x90) decodes as 'nop' on x64": - requireCapstone() - let data: seq[byte] = @[0x90'u8] - let instrs = disassembleBytes(data, 0x1000'u64, archX64) - check instrs.len == 1 - check instrs[0].mnemonic == "nop" - check instrs[0].address == 0x1000'u64 - check instrs[0].rawBytes == @[0x90'u8] + if not isCapstoneAvailable(): + skip() + else: + let data: seq[byte] = @[0x90'u8] + let instrs = disassembleBytes(data, 0x1000'u64, archX64) + check instrs.len == 1 + check instrs[0].mnemonic == "nop" + check instrs[0].address == 0x1000'u64 + check instrs[0].rawBytes == @[0x90'u8] test "RET byte (0xC3) decodes as 'ret' on x64": - requireCapstone() - let data: seq[byte] = @[0xC3'u8] - let instrs = disassembleBytes(data, 0x2000'u64, archX64) - check instrs.len == 1 - check instrs[0].mnemonic == "ret" - check instrs[0].address == 0x2000'u64 + if not isCapstoneAvailable(): + skip() + else: + let data: seq[byte] = @[0xC3'u8] + let instrs = disassembleBytes(data, 0x2000'u64, archX64) + check instrs.len == 1 + check instrs[0].mnemonic == "ret" + check instrs[0].address == 0x2000'u64 test "Function prologue bytes decode to push/mov on x64": - requireCapstone() - # 55 push rbp - # 48 89 E5 mov rbp, rsp - let data: seq[byte] = @[0x55'u8, 0x48'u8, 0x89'u8, 0xE5'u8] - let instrs = disassembleBytes(data, 0x4000'u64, archX64) - check instrs.len == 2 - check instrs[0].mnemonic == "push" - check instrs[0].address == 0x4000'u64 - check instrs[1].mnemonic == "mov" - check instrs[1].address == 0x4001'u64 + if not isCapstoneAvailable(): + skip() + else: + # 55 push rbp + # 48 89 E5 mov rbp, rsp + let data: seq[byte] = @[0x55'u8, 0x48'u8, 0x89'u8, 0xE5'u8] + let instrs = disassembleBytes(data, 0x4000'u64, archX64) + check instrs.len == 2 + check instrs[0].mnemonic == "push" + check instrs[0].address == 0x4000'u64 + check instrs[1].mnemonic == "mov" + check instrs[1].address == 0x4001'u64 test "Multiple instructions have sequential addresses": - requireCapstone() - # 90 = nop (1 byte), C3 = ret (1 byte) - let data: seq[byte] = @[0x90'u8, 0xC3'u8] - let instrs = disassembleBytes(data, 0x5000'u64, archX64) - check instrs.len == 2 - check instrs[0].address == 0x5000'u64 - check instrs[1].address == 0x5001'u64 + if not isCapstoneAvailable(): + skip() + else: + # 90 = nop (1 byte), C3 = ret (1 byte) + let data: seq[byte] = @[0x90'u8, 0xC3'u8] + let instrs = disassembleBytes(data, 0x5000'u64, archX64) + check instrs.len == 2 + check instrs[0].address == 0x5000'u64 + check instrs[1].address == 0x5001'u64 test "Invalid/garbage bytes return empty or partial result without crashing": - requireCapstone() - # 0xFF 0xFF is not a valid x64 instruction at this position; Capstone - # may decode it partially or return empty, but must not raise. - let data: seq[byte] = @[0xFF'u8, 0xFF'u8, 0xFF'u8, 0xFF'u8] - let instrs = disassembleBytes(data, 0'u64, archX64) - # We only check it did not crash; result may be empty or partial. - check instrs.len >= 0 + if not isCapstoneAvailable(): + skip() + else: + # 0xFF 0xFF is not a valid x64 instruction at this position; Capstone + # may decode it partially or return empty, but must not raise. + let data: seq[byte] = @[0xFF'u8, 0xFF'u8, 0xFF'u8, 0xFF'u8] + let instrs = disassembleBytes(data, 0'u64, archX64) + # We only check it did not crash; result may be empty or partial. + check instrs.len >= 0 test "Base address is reflected in instruction addresses": - requireCapstone() - let data: seq[byte] = @[0x90'u8] - let base = 0xDEAD0000'u64 - let instrs = disassembleBytes(data, base, archX64) - check instrs.len == 1 - check instrs[0].address == base + if not isCapstoneAvailable(): + skip() + else: + let data: seq[byte] = @[0x90'u8] + let base = 0xDEAD0000'u64 + let instrs = disassembleBytes(data, base, archX64) + check instrs.len == 1 + check instrs[0].address == base # --------------------------------------------------------------------------- # Suite: disassembleSection (pure-Nim paths that do not need Capstone) diff --git a/tests/test_emulator.nim b/tests/test_emulator.nim index e99a14b..5cfbfb3 100644 --- a/tests/test_emulator.nim +++ b/tests/test_emulator.nim @@ -35,9 +35,10 @@ suite "Emulator Module Tests": test "closeEmulator is safe to call twice on a real context": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - closeEmulator(ctx) - closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + closeEmulator(ctx) + closeEmulator(ctx) test "loadMemory returns false when engine is nil": var ctx = EmulatorContext() @@ -51,10 +52,11 @@ suite "Emulator Module Tests": test "loadMemory succeeds with valid data when Unicorn available": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - let data: seq[byte] = @[0x90'u8, 0x90'u8] - check ctx.loadMemory(0x00401000'u64, data) == true + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + let data: seq[byte] = @[0x90'u8, 0x90'u8] + check ctx.loadMemory(0x00401000'u64, data) == true test "readRegister returns 0 for nil engine": let ctx = EmulatorContext() @@ -83,86 +85,92 @@ suite "Emulator Module Tests": test "register write and read round-trip": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - check ctx.writeRegister(UC_X86_REG_RAX, 0xDEADBEEF'u64) == true - check ctx.readRegister(UC_X86_REG_RAX) == 0xDEADBEEF'u64 + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + check ctx.writeRegister(UC_X86_REG_RAX, 0xDEADBEEF'u64) == true + check ctx.readRegister(UC_X86_REG_RAX) == 0xDEADBEEF'u64 test "register write and read round-trip for RBX": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - check ctx.writeRegister(UC_X86_REG_RBX, 0x1234'u64) == true - check ctx.readRegister(UC_X86_REG_RBX) == 0x1234'u64 + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + check ctx.writeRegister(UC_X86_REG_RBX, 0x1234'u64) == true + check ctx.readRegister(UC_X86_REG_RBX) == 0x1234'u64 test "emulate a NOP sled executes without error": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - # Four NOP bytes. The emulator stops after maxInstructions = 4. - let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] - let base = 0x00401000'u64 - check ctx.loadMemory(base, code) == true + # Four NOP bytes. The emulator stops after maxInstructions = 4. + let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] + let base = 0x00401000'u64 + check ctx.loadMemory(base, code) == true - let res = ctx.emulateRange(base, base + uint64(code.len), 4) - check res.success == true + let res = ctx.emulateRange(base, base + uint64(code.len), 4) + check res.success == true test "emulate mov eax, 42 and read register result": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - # Encoding: b8 2a 00 00 00 (mov eax, 42) - let code: seq[byte] = @[0xb8'u8, 0x2a'u8, 0x00'u8, 0x00'u8, 0x00'u8] - let base = 0x00401000'u64 - check ctx.loadMemory(base, code) == true + # Encoding: b8 2a 00 00 00 (mov eax, 42) + let code: seq[byte] = @[0xb8'u8, 0x2a'u8, 0x00'u8, 0x00'u8, 0x00'u8] + let base = 0x00401000'u64 + check ctx.loadMemory(base, code) == true - # Execute exactly 1 instruction and verify EAX. - let res = ctx.emulateRange(base, base + uint64(code.len), 1) - check res.success == true - check ctx.readRegister(UC_X86_REG_EAX) == 42'u64 + # Execute exactly 1 instruction and verify EAX. + let res = ctx.emulateRange(base, base + uint64(code.len), 1) + check res.success == true + check ctx.readRegister(UC_X86_REG_EAX) == 42'u64 test "emulate xor rax, rax zeroes the register": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - check ctx.writeRegister(UC_X86_REG_RAX, 0xFFFF'u64) == true + check ctx.writeRegister(UC_X86_REG_RAX, 0xFFFF'u64) == true - # Encoding: 48 31 c0 (xor rax, rax) - let code: seq[byte] = @[0x48'u8, 0x31'u8, 0xc0'u8] - let base = 0x00401000'u64 - check ctx.loadMemory(base, code) == true + # Encoding: 48 31 c0 (xor rax, rax) + let code: seq[byte] = @[0x48'u8, 0x31'u8, 0xc0'u8] + let base = 0x00401000'u64 + check ctx.loadMemory(base, code) == true - let res = ctx.emulateRange(base, base + uint64(code.len), 1) - check res.success == true - check ctx.readRegister(UC_X86_REG_RAX) == 0'u64 + let res = ctx.emulateRange(base, base + uint64(code.len), 1) + check res.success == true + check ctx.readRegister(UC_X86_REG_RAX) == 0'u64 test "code hook fires for each instruction in a NOP sled": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) - var counter: int = 0 + var counter: int = 0 - proc countHook(uc: ptr UcEngine, address: uint64, size: uint32, - userData: pointer) {.cdecl.} = - var p = cast[ptr int](userData) - p[] += 1 + proc countHook(uc: ptr UcEngine, address: uint64, size: uint32, + userData: pointer) {.cdecl.} = + var p = cast[ptr int](userData) + p[] += 1 - let base = 0x00401000'u64 - let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] - check ctx.loadMemory(base, code) == true - check ctx.addCodeHook(countHook, addr counter) == true + let base = 0x00401000'u64 + let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8] + check ctx.loadMemory(base, code) == true + check ctx.addCodeHook(countHook, addr counter) == true - discard ctx.emulateRange(base, base + uint64(code.len), 4) - check counter == 4 + discard ctx.emulateRange(base, base + uint64(code.len), 4) + check counter == 4 test "loadBinary returns false when engine is nil": var ctx = EmulatorContext() @@ -172,11 +180,12 @@ suite "Emulator Module Tests": test "loadBinary returns false for unknown binary format": if not isUnicornAvailable(): skip() - var ctx = createEmulator(archX64) - defer: closeEmulator(ctx) - # parseBinary on a nonexistent file returns bfUnknown with no sections. - let info = parseBinary("nonexistent_binary_xyz.elf") - check ctx.loadBinary(info, ".text") == false + else: + var ctx = createEmulator(archX64) + defer: closeEmulator(ctx) + # parseBinary on a nonexistent file returns bfUnknown with no sections. + let info = parseBinary("nonexistent_binary_xyz.elf") + check ctx.loadBinary(info, ".text") == false test "isUnicornAvailable does not raise when library is missing": # Regardless of whether Unicorn is installed, the call must not raise. diff --git a/tests/test_patcher.nim b/tests/test_patcher.nim index 6b95386..a310368 100644 --- a/tests/test_patcher.nim +++ b/tests/test_patcher.nim @@ -121,16 +121,17 @@ suite "Patcher Module Tests": test "assembleAndPatch writes assembled bytes when Keystone available": if not isKeystoneAvailable(): skip() - let srcFile = "test_aap_src.bin" - let dstFile = "test_aap_dst.bin" - defer: - removeFile(srcFile) - removeFile(dstFile) - writeFile(srcFile, newString(8)) - check assembleAndPatch(srcFile, dstFile, 0, "nop", archX64) == true - let result = readFile(dstFile) - check result.len == 8 - check byte(result[0]) == 0x90'u8 + else: + let srcFile = "test_aap_src.bin" + let dstFile = "test_aap_dst.bin" + defer: + removeFile(srcFile) + removeFile(dstFile) + writeFile(srcFile, newString(8)) + check assembleAndPatch(srcFile, dstFile, 0, "nop", archX64) == true + let result = readFile(dstFile) + check result.len == 8 + check byte(result[0]) == 0x90'u8 suite "Emulation-Based Patch Testing": @@ -146,35 +147,39 @@ suite "Emulation-Based Patch Testing": test "testPatchInEmulator returns false for non-existent source file": if not isUnicornAvailable(): skip() - check testPatchInEmulator("no_such_binary_xyz.bin", 0, - @[0x90'u8], archX64) == false + else: + check testPatchInEmulator("no_such_binary_xyz.bin", 0, + @[0x90'u8], archX64) == false test "testPatchInEmulator returns false for empty patch bytes": if not isUnicornAvailable(): skip() - let srcFile = "test_emu_patch_src.bin" - defer: removeFile(srcFile) - writeFile(srcFile, newString(16)) - check testPatchInEmulator(srcFile, 0, @[], archX64) == false + else: + let srcFile = "test_emu_patch_src.bin" + defer: removeFile(srcFile) + writeFile(srcFile, newString(16)) + check testPatchInEmulator(srcFile, 0, @[], archX64) == false test "testPatchInEmulator returns false when offset is out of range": if not isUnicornAvailable(): skip() - let srcFile = "test_emu_oob.bin" - defer: removeFile(srcFile) - writeFile(srcFile, newString(4)) - check testPatchInEmulator(srcFile, 100, @[0x90'u8], archX64) == false + else: + let srcFile = "test_emu_oob.bin" + defer: removeFile(srcFile) + writeFile(srcFile, newString(4)) + check testPatchInEmulator(srcFile, 100, @[0x90'u8], archX64) == false test "testPatchInEmulator executes a NOP sled patch without error": if not isUnicornAvailable(): skip() - let srcFile = "test_emu_nop.bin" - defer: removeFile(srcFile) - # Write a page of NOP bytes so emulation has valid instructions to run. - var buf = newString(0x1000) - for i in 0 ..< 0x1000: - buf[i] = char(0x90'u8) - writeFile(srcFile, buf) - # Patch the first 4 bytes with NOPs and emulate from offset 0. - check testPatchInEmulator(srcFile, 0, @[0x90'u8, 0x90'u8, 0x90'u8, - 0x90'u8], archX64, 4) == true + else: + let srcFile = "test_emu_nop.bin" + defer: removeFile(srcFile) + # Write a page of NOP bytes so emulation has valid instructions to run. + var buf = newString(0x1000) + for i in 0 ..< 0x1000: + buf[i] = char(0x90'u8) + writeFile(srcFile, buf) + # Patch the first 4 bytes with NOPs and emulate from offset 0. + check testPatchInEmulator(srcFile, 0, @[0x90'u8, 0x90'u8, 0x90'u8, + 0x90'u8], archX64, 4) == true diff --git a/tests/test_runtime.nim b/tests/test_runtime.nim index 1c9c0c6..178e0ec 100644 --- a/tests/test_runtime.nim +++ b/tests/test_runtime.nim @@ -13,7 +13,7 @@ suite "Runtime Module - Platform": test "attachProcess returns failure on non-Linux": when not defined(linux): - let r = attachProcess(1) + let r = runtime.attachProcess(1) check r.success == false else: # On Linux this would actually attach; skip the negative test. @@ -21,31 +21,31 @@ suite "Runtime Module - Platform": test "detachProcess returns failure on non-Linux": when not defined(linux): - let r = detachProcess(1) + let r = runtime.detachProcess(1) check r.success == false else: check true test "patchProcessMemory returns failure for empty bytes on all platforms": - let r = patchProcessMemory(1, 0x1000'u64, @[]) + let r = runtime.patchProcessMemory(1, 0x1000'u64, @[]) check r.success == false test "injectBreakpoint returns failure on non-Linux": when not defined(linux): - let (_, r) = injectBreakpoint(1, 0x1000'u64) + let (_, r) = runtime.injectBreakpoint(1, 0x1000'u64) check r.success == false else: check true test "monitorSyscalls returns empty seq on non-Linux": when not defined(linux): - let events = monitorSyscalls(1, 5) + let events = runtime.monitorSyscalls(1, 5) check events.len == 0 else: check true test "disassembleAtAddress returns empty seq for zero count": - let instrs = disassembleAtAddress(1, 0x1000'u64, 0) + let instrs = runtime.disassembleAtAddress(1, 0x1000'u64, 0) check instrs.len == 0 # ---------------------------------------------------------------------------