| @@ -3,3 +3,5 @@ roms/* | |||||
| !roms/README | !roms/README | ||||
| crater | crater | ||||
| crater-dev | crater-dev | ||||
| tests/runner | |||||
| tests/asm/*.gg | |||||
| @@ -1,4 +1,4 @@ | |||||
| Copyright (C) 2014-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | of this software and associated documentation files (the "Software"), to deal | ||||
| @@ -24,12 +24,19 @@ Installing | |||||
| ---------- | ---------- | ||||
| Only OS X and Linux are tested. You'll need a modern compiler that supports C11 | Only OS X and Linux are tested. You'll need a modern compiler that supports C11 | ||||
| (clang preferred) and SDL 2. Using Homebrew, you can `brew install sdl2`; using | |||||
| apt, you can `apt-get install libsdl2-dev`. | |||||
| ([clang][clang] preferred) and [SDL 2][sdl2]. Using Homebrew, you can | |||||
| `brew install sdl2`; using apt, you can `apt-get install libsdl2-dev`. | |||||
| Run `make` to create `./crater`. To build the development version with debug | Run `make` to create `./crater`. To build the development version with debug | ||||
| symbols (they can exist simultaneously), run `make DEBUG=1`, which creates | |||||
| `./crater-dev`. This also enables the printing of debugging info to stdout. | |||||
| symbols and extra diagnostic info (they can exist simultaneously), run | |||||
| `make DEBUG=1`, which creates `./crater-dev`. | |||||
| crater has a number of test cases. Run the entire suite with `make test`; | |||||
| individual components can be tested by doing `make test-{component}`, where | |||||
| `{component}` is one of `cpu`, `vdp`, `psg`, `asm`, `dis`, or `integrate`. | |||||
| [clang]: http://clang.llvm.org/ | |||||
| [sdl2]: https://www.libsdl.org/ | |||||
| Usage | Usage | ||||
| ----- | ----- | ||||
| @@ -1,50 +1,59 @@ | |||||
| # Copyright (C) 2014 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Released under the terms of the MIT License. See LICENSE for details. | # Released under the terms of the MIT License. See LICENSE for details. | ||||
| PROGRAM = crater | PROGRAM = crater | ||||
| SOURCES = src | SOURCES = src | ||||
| BUILD = build | BUILD = build | ||||
| DEVEXT = -dev | DEVEXT = -dev | ||||
| TESTS = cpu vdp psg asm dis integrate | |||||
| CC = clang | CC = clang | ||||
| FLAGS = -O2 -Wall -Wextra -pedantic -std=c11 | FLAGS = -O2 -Wall -Wextra -pedantic -std=c11 | ||||
| CFLAGS = $(shell sdl2-config --cflags) | CFLAGS = $(shell sdl2-config --cflags) | ||||
| LIBS = $(shell sdl2-config --libs) | LIBS = $(shell sdl2-config --libs) | ||||
| DFLAGS = -g -DDEBUG_MODE | |||||
| MKDIR = mkdir -p | MKDIR = mkdir -p | ||||
| RM = rm -rf | RM = rm -rf | ||||
| ASM_UP = scripts/update_asm_instructions.py | ASM_UP = scripts/update_asm_instructions.py | ||||
| MODE = release | MODE = release | ||||
| BNRY = $(PROGRAM) | BNRY = $(PROGRAM) | ||||
| FLGS = $(FLAGS) | |||||
| SDRS = $(shell find $(SOURCES) -type d | xargs echo) | SDRS = $(shell find $(SOURCES) -type d | xargs echo) | ||||
| SRCS = $(filter-out %.inc.c,$(foreach d,. $(SDRS),$(wildcard $(addprefix $(d)/*,.c)))) | SRCS = $(filter-out %.inc.c,$(foreach d,. $(SDRS),$(wildcard $(addprefix $(d)/*,.c)))) | ||||
| OBJS = $(patsubst %.c,%.o,$(addprefix $(BUILD)/$(MODE)/,$(SRCS))) | OBJS = $(patsubst %.c,%.o,$(addprefix $(BUILD)/$(MODE)/,$(SRCS))) | ||||
| DEPS = $(OBJS:%.o=%.d) | DEPS = $(OBJS:%.o=%.d) | ||||
| DIRS = $(sort $(dir $(OBJS))) | DIRS = $(sort $(dir $(OBJS))) | ||||
| TCPS = $(addprefix test-,$(TESTS)) | |||||
| ifdef DEBUG | ifdef DEBUG | ||||
| BNRY := $(BNRY)$(DEVEXT) | |||||
| FLAGS += -g -DDEBUG_MODE | |||||
| MODE = debug | |||||
| BNRY := $(BNRY)$(DEVEXT) | |||||
| FLGS += $(DFLAGS) | |||||
| MODE = debug | |||||
| endif | endif | ||||
| .PHONY: all clean test test-all test-z80 test-asm test-dasm | |||||
| export CC | |||||
| export FLAGS | |||||
| export RM | |||||
| .PHONY: all clean test tests test-prereqs test-make-prereqs $(TCPS) | |||||
| all: $(BNRY) | all: $(BNRY) | ||||
| clean: | clean: | ||||
| $(RM) $(BUILD) $(PROGRAM) $(PROGRAM)$(DEVEXT) | $(RM) $(BUILD) $(PROGRAM) $(PROGRAM)$(DEVEXT) | ||||
| @$(MAKE) -C tests clean | |||||
| $(DIRS): | $(DIRS): | ||||
| $(MKDIR) $@ | $(MKDIR) $@ | ||||
| $(BNRY): $(OBJS) | $(BNRY): $(OBJS) | ||||
| $(CC) $(FLAGS) $(LIBS) $(OBJS) -o $@ | |||||
| $(CC) $(FLGS) $(LIBS) $(OBJS) -o $@ | |||||
| $(OBJS): | $(DIRS) | $(OBJS): | $(DIRS) | ||||
| $(BUILD)/$(MODE)/%.o: %.c | $(BUILD)/$(MODE)/%.o: %.c | ||||
| $(CC) $(FLAGS) $(CFLAGS) -MMD -MP -c $< -o $@ | |||||
| $(CC) $(FLGS) $(CFLAGS) -MMD -MP -c $< -o $@ | |||||
| -include $(DEPS) | -include $(DEPS) | ||||
| @@ -52,16 +61,16 @@ ASM_INST = $(SOURCES)/assembler/instructions | |||||
| $(ASM_INST).inc.c: $(ASM_INST).yml $(ASM_UP) | $(ASM_INST).inc.c: $(ASM_INST).yml $(ASM_UP) | ||||
| python $(ASM_UP) | python $(ASM_UP) | ||||
| test: test-all test-z80 test-asm test-dasm | |||||
| test-prereqs: $(PROGRAM) | |||||
| @: # No-op; prevents make from cluttering output with "X is up to date" | |||||
| test-all: | |||||
| @echo "running all tests" | |||||
| test-make-prereqs: | |||||
| @$(MAKE) test-prereqs DEBUG= | |||||
| test-z80: | |||||
| @echo "running Z80 CPU tests" | |||||
| test: test-make-prereqs | |||||
| @$(MAKE) -C tests -s all | |||||
| test-asm: | |||||
| @echo "running assembler tests" | |||||
| tests: test | |||||
| test-dasm: | |||||
| @echo "running disassembler tests" | |||||
| $(TCPS): test-make-prereqs | |||||
| @$(MAKE) -C tests -s $(subst test-,,$@) | |||||
| @@ -98,8 +98,6 @@ static ASMErrorDesc parse_inst_##mnemonic( \ | |||||
| #define INST_INDEX_PREFIX(n) INST_PREFIX_(INST_INDEX(n).reg) | #define INST_INDEX_PREFIX(n) INST_PREFIX_(INST_INDEX(n).reg) | ||||
| /* ----------------------------- END WORK BLOCK ---------------------------- */ | |||||
| /* | /* | ||||
| Fill an instruction's byte array with the given data. | Fill an instruction's byte array with the given data. | ||||
| @@ -276,6 +276,21 @@ static bool parse_space( | |||||
| } | } | ||||
| /* | /* | ||||
| Parse a string like parse_string(), but null-terminate it. | |||||
| */ | |||||
| static bool parse_cstring( | |||||
| char **result, size_t *length, const char *arg, ssize_t size) | |||||
| { | |||||
| if (!parse_string(result, length, arg, size)) | |||||
| return false; | |||||
| (*length)++; | |||||
| *result = cr_realloc(*result, sizeof(char) * (*length)); | |||||
| (*result)[*length - 1] = '\0'; | |||||
| return true; | |||||
| } | |||||
| /* | |||||
| Parse data encoded in a line into an ASMData object. | Parse data encoded in a line into an ASMData object. | ||||
| On success, return NULL and store the instruction in *data_ptr. On failure, | On success, return NULL and store the instruction in *data_ptr. On failure, | ||||
| @@ -285,7 +300,7 @@ static ErrorInfo* parse_data( | |||||
| const ASMLine *line, ASMData **data_ptr, size_t offset) | const ASMLine *line, ASMData **data_ptr, size_t offset) | ||||
| { | { | ||||
| const char *directive; | const char *directive; | ||||
| parser_func parser = (parser_func) parse_string; | |||||
| parser_func parser; | |||||
| if (IS_DIRECTIVE(line, DIR_BYTE)) { | if (IS_DIRECTIVE(line, DIR_BYTE)) { | ||||
| directive = DIR_BYTE; | directive = DIR_BYTE; | ||||
| @@ -295,10 +310,13 @@ static ErrorInfo* parse_data( | |||||
| parser = parse_space; | parser = parse_space; | ||||
| } else if (IS_DIRECTIVE(line, DIR_ASCII)) { | } else if (IS_DIRECTIVE(line, DIR_ASCII)) { | ||||
| directive = DIR_ASCII; | directive = DIR_ASCII; | ||||
| parser = (parser_func) parse_string; | |||||
| } else if (IS_DIRECTIVE(line, DIR_ASCIZ)) { | } else if (IS_DIRECTIVE(line, DIR_ASCIZ)) { | ||||
| directive = DIR_ASCIZ; | directive = DIR_ASCIZ; | ||||
| parser = (parser_func) parse_cstring; | |||||
| } else if (IS_DIRECTIVE(line, DIR_ASCIIZ)) { | } else if (IS_DIRECTIVE(line, DIR_ASCIIZ)) { | ||||
| directive = DIR_ASCIIZ; | directive = DIR_ASCIIZ; | ||||
| parser = (parser_func) parse_cstring; | |||||
| } else { | } else { | ||||
| return error_info_create(line, ET_PREPROC, ED_PP_UNKNOWN); | return error_info_create(line, ET_PREPROC, ED_PP_UNKNOWN); | ||||
| } | } | ||||
| @@ -90,7 +90,7 @@ static int get_rom_paths(char ***path_ptr) | |||||
| } | } | ||||
| closedir(dirp); | closedir(dirp); | ||||
| } else { | } else { | ||||
| WARN_ERRNO("couldn't open 'roms/'") | |||||
| WARN_ERRNO("couldn't open '" ROMS_DIR "/'") | |||||
| } | } | ||||
| *path_ptr = paths; | *path_ptr = paths; | ||||
| return npaths; | return npaths; | ||||
| @@ -10,12 +10,13 @@ | |||||
| /* Internal usage only */ | /* Internal usage only */ | ||||
| #define LOG_MSG_(dest, level, extra, after, ...) { \ | |||||
| #define LOG_MSG_(dest, level, extra, after, ...) \ | |||||
| do { \ | |||||
| fprintf(dest, level ": " __VA_ARGS__); \ | fprintf(dest, level ": " __VA_ARGS__); \ | ||||
| extra; \ | extra; \ | ||||
| fprintf(dest, "\n"); \ | fprintf(dest, "\n"); \ | ||||
| after; \ | after; \ | ||||
| } | |||||
| } while (0); | |||||
| #define LOG_ERR_(...) LOG_MSG_(stderr, __VA_ARGS__) | #define LOG_ERR_(...) LOG_MSG_(stderr, __VA_ARGS__) | ||||
| #define LOG_OUT_(...) LOG_MSG_(stdout, __VA_ARGS__) | #define LOG_OUT_(...) LOG_MSG_(stdout, __VA_ARGS__) | ||||
| @@ -32,7 +32,7 @@ static void print_header(const uint8_t *header) | |||||
| snprintf(&header_chr[3 * i], 3, "%2c", header[i]); | snprintf(&header_chr[3 * i], 3, "%2c", header[i]); | ||||
| else { | else { | ||||
| header_chr[3 * i] = ' '; | header_chr[3 * i] = ' '; | ||||
| header_chr[3 * i + 1] = '?'; | |||||
| header_chr[3 * i + 1] = '.'; | |||||
| } | } | ||||
| header_hex[3 * i + 2] = header_chr[3 * i + 2] = ' '; | header_hex[3 * i + 2] = header_chr[3 * i + 2] = ' '; | ||||
| } | } | ||||
| @@ -1,4 +1,4 @@ | |||||
| /* Copyright (C) 2014-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| /* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| Released under the terms of the MIT License. See LICENSE for details. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
| /* | /* | ||||
| @@ -0,0 +1,16 @@ | |||||
| ;; Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| ;; Released under the terms of the MIT License. See LICENSE for details. | |||||
| ; ----- CRATER UNIT TESTING SUITE --------------------------------------------- | |||||
| ; 02-headers1.asm | |||||
| ; Basic test for headers and other directives, mostly using default values | |||||
| .rom_size auto | |||||
| .rom_header auto | |||||
| .rom_product 0 | |||||
| .rom_version 0 | |||||
| .rom_region "GG Export" | |||||
| .rom_checksum off | |||||
| .rom_declsize auto | |||||
| .cross_blocks auto | |||||
| @@ -0,0 +1,16 @@ | |||||
| ;; Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| ;; Released under the terms of the MIT License. See LICENSE for details. | |||||
| ; ----- CRATER UNIT TESTING SUITE --------------------------------------------- | |||||
| ; 03-headers2.asm | |||||
| ; Header/directive test using non-default values | |||||
| .rom_size "64 KB" | |||||
| .rom_header $7FF0 | |||||
| .rom_product 101893 | |||||
| .rom_version 3 | |||||
| .rom_region "GG International" | |||||
| .rom_checksum on | |||||
| .rom_declsize "32 KB" | |||||
| .cross_blocks off | |||||
| @@ -0,0 +1,21 @@ | |||||
| ;; Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| ;; Released under the terms of the MIT License. See LICENSE for details. | |||||
| ; ----- CRATER UNIT TESTING SUITE --------------------------------------------- | |||||
| ; 04-basic.asm | |||||
| ; Basic instruction test | |||||
| .org $0000 | |||||
| main: | |||||
| di | |||||
| ld a, $23 | |||||
| inc a | |||||
| call foo | |||||
| foo: | |||||
| push bc | |||||
| xor c | |||||
| ret z | |||||
| rrca | |||||
| ret | |||||
| @@ -0,0 +1,17 @@ | |||||
| ;; Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| ;; Released under the terms of the MIT License. See LICENSE for details. | |||||
| ; ----- CRATER UNIT TESTING SUITE --------------------------------------------- | |||||
| ; 05-includes.asm | |||||
| ; Source file inclusion test, involving cross-file label references and complex | |||||
| ; origins | |||||
| .include "05.inc1.asm" | |||||
| .org $0000 | |||||
| main: | |||||
| di | |||||
| ld c, $FA | |||||
| inc c | |||||
| call bar | |||||
| @@ -0,0 +1,13 @@ | |||||
| .org $0100 | |||||
| .include "05.inc2.asm" | |||||
| .include "05.inc3.asm" | |||||
| bar: | |||||
| push de | |||||
| call blah | |||||
| xor d | |||||
| ret pe | |||||
| call testfunc | |||||
| exx | |||||
| ret | |||||
| @@ -0,0 +1,3 @@ | |||||
| testfunc: | |||||
| sub b | |||||
| ret | |||||
| @@ -0,0 +1,2 @@ | |||||
| .include "05.inc4.asm" | |||||
| .rom_product 57005 | |||||
| @@ -0,0 +1,3 @@ | |||||
| blah: | |||||
| ei | |||||
| ret | |||||
| @@ -0,0 +1,16 @@ | |||||
| ;; Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| ;; Released under the terms of the MIT License. See LICENSE for details. | |||||
| ; ----- CRATER UNIT TESTING SUITE --------------------------------------------- | |||||
| ; 06-formatting.asm | |||||
| ; Complex formatting test, involving strange indentation and newline usage | |||||
| foo: .inCLUde "06.inc.asm" | |||||
| .ORG $0100 | |||||
| ayy: lmao: | |||||
| di | |||||
| InC a | |||||
| ret | |||||
| @@ -0,0 +1,13 @@ | |||||
| test1:test2: test3: test4: | |||||
| inc a | |||||
| inc b | |||||
| inc c | |||||
| ret | |||||
| @@ -0,0 +1,31 @@ | |||||
| ;; Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| ;; Released under the terms of the MIT License. See LICENSE for details. | |||||
| ; ----- CRATER UNIT TESTING SUITE --------------------------------------------- | |||||
| ; 07-data.asm | |||||
| ; Test of data directives to fill ROM with non-code bytes | |||||
| .org $1000 | |||||
| str1: .ascii "Hello, world!" | |||||
| str2: .asciz "World, hello!" | |||||
| str3: .asciiz "foobar" | |||||
| arr1: .byte 1 2 3 4 5 6 | |||||
| arr2: .byte 14 $14 $FF 255 $F0 | |||||
| arr3: .byte 8, $10, 4 5 6, 8, 10 | |||||
| void1: .space 3 | |||||
| void2: .space 6 $D0 | |||||
| void3: .space 128 $DE | |||||
| .org $0000 | |||||
| main: | |||||
| di | |||||
| loop: | |||||
| jp loop | |||||
| .org $0066 | |||||
| nmi: | |||||
| retn | |||||
| @@ -0,0 +1,103 @@ | |||||
| ;; Copyright (C) 2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| ;; Released under the terms of the MIT License. See LICENSE for details. | |||||
| ; ----- CRATER UNIT TESTING SUITE --------------------------------------------- | |||||
| ; 08-instructions.asm | |||||
| ; Exhaustive test of instruction syntax | |||||
| .define COUNTER $C010 | |||||
| .define OFFSET 6 | |||||
| .define OFFSET2 157 | |||||
| .org $1000 | |||||
| str1: .ascii "Hello, world!" | |||||
| .org $0000 | |||||
| main: | |||||
| di | |||||
| call inst | |||||
| halt | |||||
| .org $0066 | |||||
| nmi: | |||||
| retn | |||||
| .org $2000 | |||||
| inst: | |||||
| inc a | |||||
| inc c | |||||
| inc hl | |||||
| inc sp | |||||
| inc (hl) | |||||
| inc ( hl ) | |||||
| inc ix | |||||
| inc iyl | |||||
| inc (ix) | |||||
| inc ( ix ) | |||||
| inc (iy+0) | |||||
| inc (ix+3) | |||||
| inc (ix+OFFSET) | |||||
| inc (ix-OFFSET) | |||||
| inc ( ix + 7 ) | |||||
| inc (ix-8) | |||||
| inc (ix - +9) | |||||
| inc (iy+127) | |||||
| inc (iy-128) | |||||
| add a, e | |||||
| add a, 5 | |||||
| add a, $12 | |||||
| add a, (hl) | |||||
| add a, (ix+4) | |||||
| add hl, bc | |||||
| add ix, de | |||||
| adc a, e | |||||
| adc a, 5 | |||||
| adc a, $12 | |||||
| adc a, (hl) | |||||
| adc a, (ix+4) | |||||
| adc hl, bc | |||||
| rlca | |||||
| rrca | |||||
| rla | |||||
| rra | |||||
| daa | |||||
| cpl | |||||
| scf | |||||
| ccf | |||||
| halt | |||||
| exx | |||||
| ei | |||||
| di | |||||
| cpd | |||||
| cpdr | |||||
| cpi | |||||
| cpir | |||||
| ind | |||||
| indr | |||||
| ini | |||||
| inir | |||||
| ldd | |||||
| lddr | |||||
| ldi | |||||
| ldir | |||||
| otdr | |||||
| otir | |||||
| outd | |||||
| outi | |||||
| rrd | |||||
| rld | |||||
| ld hl, COUNTER ; H contains $C0, L contains $10 | |||||
| ld d, (hl) ; D contains $FF (garbage) | |||||
| ld c, $3B ; C contains $3B | |||||
| ld (hl), c ; memory address $C010 contains $3B | |||||
| ld hl, str1 | |||||
| ld b, (hl) ; B contains 'H' | |||||
| ld hl, (str1) ; H contains 'e', L contains 'H' | |||||
| ld (hl), b ; error, writing to cartridge ROM | |||||
| @@ -0,0 +1,8 @@ | |||||
| 01-empty.asm 01-empty.gg | |||||
| 02-headers1.asm 02-headers1.gg | |||||
| 03-headers2.asm 03-headers2.gg | |||||
| 04-basic.asm 04-basic.gg | |||||
| 05-includes.asm 05-includes.gg | |||||
| 06-formatting.asm 06-formatting.gg | |||||
| 07-data.asm 07-data.gg | |||||
| 08-instructions.asm 08-instructions.gg | |||||
| @@ -0,0 +1,27 @@ | |||||
| # Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # Released under the terms of the MIT License. See LICENSE for details. | |||||
| RUNNER = runner | |||||
| COMPONENTS = cpu vdp psg asm dis integrate | |||||
| .PHONY: all clean $(COMPONENTS) | |||||
| all: $(COMPONENTS) | |||||
| clean: | |||||
| $(RM) $(RUNNER) | |||||
| $(RM) asm/*.gg | |||||
| $(RUNNER): $(RUNNER).c | |||||
| $(CC) $(FLAGS) $< -o $@ | |||||
| $(COMPONENTS): $(RUNNER) | |||||
| ./$(RUNNER) $@ | |||||
| asm: asm-unarchive | |||||
| asm-archive: | |||||
| tar -czf asm/roms.tar.gz asm/*.gg | |||||
| asm-unarchive: | |||||
| tar -xf asm/roms.tar.gz | |||||
| @@ -1,4 +1,274 @@ | |||||
| /* Copyright (C) 2014-2015 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| /* Copyright (C) 2014-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| Released under the terms of the MIT License. See LICENSE for details. */ | Released under the terms of the MIT License. See LICENSE for details. */ | ||||
| // ... | |||||
| #include <stdbool.h> | |||||
| #include <stdio.h> | |||||
| #include <stdlib.h> | |||||
| #include <string.h> | |||||
| #include <unistd.h> | |||||
| #include "../src/logging.h" | |||||
| #include "../src/util.h" | |||||
| #define ASM_PREFIX "asm/" | |||||
| #define ASM_OUTFILE ASM_PREFIX ".output.gg" | |||||
| /* Helper macros for reporting test passings/failures */ | |||||
| #define PASS_TEST() \ | |||||
| do { \ | |||||
| printf("."); \ | |||||
| fflush(stdout); \ | |||||
| passed_tests++; \ | |||||
| pending_nl = true; \ | |||||
| } while(0); | |||||
| #define FAIL_MSG "***** FAILURE *****" | |||||
| #define FAIL_TEST(format, ...) \ | |||||
| do { \ | |||||
| printf("F\n"); \ | |||||
| fprintf(stderr, FAIL_MSG "\n" format "\n", __VA_ARGS__); \ | |||||
| failed_tests++; \ | |||||
| pending_nl = false; \ | |||||
| } while(0); | |||||
| #define READY_STDOUT() \ | |||||
| do { \ | |||||
| if (pending_nl) { \ | |||||
| printf("\n"); \ | |||||
| pending_nl = false; \ | |||||
| } \ | |||||
| } while(0); | |||||
| static int passed_tests = 0, failed_tests = 0; | |||||
| static bool pending_nl = false; | |||||
| /* | |||||
| Prints out the test report. Called before exiting using atexit(). | |||||
| */ | |||||
| static void finalize() { | |||||
| READY_STDOUT() | |||||
| if (failed_tests) | |||||
| printf("fail (%d/%d)\n", passed_tests, passed_tests + failed_tests); | |||||
| else | |||||
| printf("pass (%d/%d)\n", passed_tests, passed_tests); | |||||
| } | |||||
| /* | |||||
| Compare two files. If they are identical, then return true. Otherwise, | |||||
| return false and print an error message showing the difference. | |||||
| */ | |||||
| static bool diff_files(const char *expected_path, const char *actual_path) | |||||
| { | |||||
| bool same = false; | |||||
| FILE *expected = NULL, *actual = NULL; | |||||
| if (!(actual = fopen(actual_path, "rb"))) { | |||||
| FAIL_TEST("missing output file: %s", actual_path) | |||||
| goto cleanup; | |||||
| } | |||||
| if (!(expected = fopen(expected_path, "rb"))) { | |||||
| FAIL_TEST("missing reference file: %s", expected_path) | |||||
| goto cleanup; | |||||
| } | |||||
| size_t len = 0; | |||||
| int e, a; | |||||
| while ((e = fgetc(expected)) != EOF) { | |||||
| a = fgetc(actual); | |||||
| if (a == EOF) { | |||||
| FAIL_TEST("files differ: output file too short (index %zu)", len) | |||||
| goto cleanup; | |||||
| } | |||||
| if (e != a) { | |||||
| FAIL_TEST("files differ: 0x%02X != 0x%02X (expected vs. actual at " | |||||
| "index %zu)", e, a, len) | |||||
| goto cleanup; | |||||
| } | |||||
| len++; | |||||
| } | |||||
| if (fgetc(actual) != EOF) { | |||||
| FAIL_TEST("files differ: junk at end of output file (index %zu)", len) | |||||
| goto cleanup; | |||||
| } | |||||
| same = true; | |||||
| cleanup: | |||||
| if (expected) | |||||
| fclose(expected); | |||||
| if (actual) | |||||
| fclose(actual); | |||||
| return same; | |||||
| } | |||||
| /* | |||||
| Return whether the given character is valid within a filename. | |||||
| */ | |||||
| static bool is_valid_filename_char(char c) { | |||||
| return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || | |||||
| (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; | |||||
| } | |||||
| /* | |||||
| Run a single ASM->ROM test, converting the given source file to a temporary | |||||
| output file, compared against the reference file. | |||||
| */ | |||||
| static bool run_asm_test(const char *src_file, const char *ref_file) | |||||
| { | |||||
| char *cmd_prefix = "../crater --assemble " ASM_PREFIX; | |||||
| char *cmd = cr_malloc(sizeof(char) * | |||||
| (strlen(cmd_prefix) + strlen(ASM_OUTFILE) + strlen(src_file)) + 2); | |||||
| // Construct the command by concatenating: | |||||
| // ../crater --assemble asm/<src_file> asm/.output.gg | |||||
| stpcpy(stpcpy(stpcpy(cmd, cmd_prefix), src_file), " " ASM_OUTFILE); | |||||
| unlink(ASM_OUTFILE); | |||||
| system(cmd); | |||||
| free(cmd); | |||||
| // Construct the full reference file path in a temporary variable and diff | |||||
| // it with the output file: | |||||
| char *ref_path = malloc(sizeof(char) * | |||||
| (strlen(ASM_PREFIX) + strlen(ref_file) + 1)); | |||||
| stpcpy(stpcpy(ref_path, ASM_PREFIX), ref_file); | |||||
| bool diff = diff_files(ref_path, ASM_OUTFILE); | |||||
| free(ref_path); | |||||
| return diff; | |||||
| } | |||||
| /* --------------------------- Main test runners --------------------------- */ | |||||
| /* | |||||
| Run tests for the Z80 CPU. | |||||
| */ | |||||
| static bool test_cpu() | |||||
| { | |||||
| // TODO | |||||
| return true; | |||||
| } | |||||
| /* | |||||
| Run tests for the VDP. | |||||
| */ | |||||
| static bool test_vdp() | |||||
| { | |||||
| // TODO | |||||
| return true; | |||||
| } | |||||
| /* | |||||
| Run tests for the SN76489 PSG. | |||||
| */ | |||||
| static bool test_psg() | |||||
| { | |||||
| // TODO | |||||
| return true; | |||||
| } | |||||
| /* | |||||
| Run tests for the assembler. | |||||
| */ | |||||
| static bool test_asm() | |||||
| { | |||||
| FILE *fp = fopen(ASM_PREFIX "manifest", "r"); | |||||
| if (!fp) { | |||||
| ERROR_ERRNO("couldn't open manifest file") | |||||
| return false; | |||||
| } | |||||
| char *line = NULL, *split, c; | |||||
| size_t cap = 0, lineno = 0, i; | |||||
| ssize_t len; | |||||
| while ((len = getline(&line, &cap, fp)) > 0) { | |||||
| lineno++; | |||||
| line[--len] = '\0'; | |||||
| if (!len) | |||||
| continue; | |||||
| i = 0; | |||||
| while ((c = line[i++])) { | |||||
| if (!is_valid_filename_char(c) && c != ' ') { | |||||
| READY_STDOUT() | |||||
| ERROR("bad character in manifest file on line %zu", lineno) | |||||
| return false; | |||||
| } | |||||
| } | |||||
| split = strchr(line, ' '); | |||||
| if (!split || strchr(split + 1, ' ')) { | |||||
| READY_STDOUT() | |||||
| ERROR("bad format in manifest file on line %zu", lineno) | |||||
| return false; | |||||
| } | |||||
| *(split++) = '\0'; | |||||
| if (!run_asm_test(line, split)) { | |||||
| fprintf(stderr, "in test: %s -> %s\n", line, split); | |||||
| return false; | |||||
| } | |||||
| PASS_TEST() | |||||
| } | |||||
| unlink(ASM_OUTFILE); | |||||
| free(line); | |||||
| return true; | |||||
| } | |||||
| /* | |||||
| Run tests for the disassembler. | |||||
| */ | |||||
| static bool test_dis() | |||||
| { | |||||
| // TODO | |||||
| return true; | |||||
| } | |||||
| /* | |||||
| Run integration tests (i.e., multiple components working together). | |||||
| */ | |||||
| static bool test_integrate() | |||||
| { | |||||
| // TODO | |||||
| return true; | |||||
| } | |||||
| /* | |||||
| Main function. | |||||
| */ | |||||
| int main(int argc, char *argv[]) | |||||
| { | |||||
| if (argc != 2) | |||||
| FATAL("a single component name is required") | |||||
| const char *component = argv[1], *name; | |||||
| bool (*func)(); | |||||
| if (!strcmp(component, "cpu")) { | |||||
| name = "Z80 CPU"; | |||||
| func = test_cpu; | |||||
| } else if (!strcmp(component, "vdp")) { | |||||
| name = "VDP"; | |||||
| func = test_vdp; | |||||
| } else if (!strcmp(component, "psg")) { | |||||
| name = "SN76489 PSG"; | |||||
| func = test_psg; | |||||
| } else if (!strcmp(component, "asm")) { | |||||
| name = "assembler"; | |||||
| func = test_asm; | |||||
| } else if (!strcmp(component, "dis")) { | |||||
| name = "disassembler"; | |||||
| func = test_dis; | |||||
| } else if (!strcmp(component, "integrate")) { | |||||
| name = "integration"; | |||||
| func = test_integrate; | |||||
| } else { | |||||
| FATAL("unknown component: %s", component) | |||||
| } | |||||
| printf("crater: running %s tests\n", name); | |||||
| atexit(finalize); | |||||
| return func() ? EXIT_SUCCESS : EXIT_FAILURE; | |||||
| } | |||||