Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
262 lines (234 loc) · 12.9 KB
/
Copy pathMakefile
File metadata and controls
262 lines (234 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
CC = cc
CSTD = -std=c11
WARN = -Wall -Wextra -Werror
MODE ?= dev
ifeq ($(MODE),release)
OPT = -O2
else
OPT = -O0 -g
endif
# Opt-in debug instrumentation (src/debug.c): bus watchpoints and a triggered
# snapshot. Off by default so the bus path carries no hook in a normal build.
DEBUG_TOOLS ?= 0
ifeq ($(DEBUG_TOOLS),1)
DEBUG_DEF = -DDEBUG_TOOLS=1
else
DEBUG_DEF =
endif
CFLAGS = $(CSTD) $(WARN) $(OPT) $(DEBUG_DEF)
# The Lorenz conformance runner always builds at -O2, regardless of MODE: it is a
# long batch job, never stepped in a debugger, and -O2 roughly halves its runtime.
LORENZ_CFLAGS = $(CSTD) $(WARN) -O2
# SDL2 is linked into the emulator binary only; the Lorenz test runner stays
# headless (no display dependency).
SDL_CFLAGS := $(shell sdl2-config --cflags 2>/dev/null)
SDL_LIBS := $(shell sdl2-config --libs 2>/dev/null)
SRC = $(wildcard src/*.c)
BIN = build/c64
# Core objects without main.c or host.c (no SDL), shared by the test runner. The
# drive is included so the drive suite links; the Lorenz runner does not call it.
CORE_SRC = src/bus.c src/mem.c src/cpu.c src/cpu6502.c src/vic.c src/sid.c src/cia.c src/drive.c src/via.c src/iec.c src/disk.c src/debug.c src/snapshot.c src/fileio.c
# Every src header, a prerequisite of each rule that compiles src/*.c. The recipes
# are single-shot, so a header edit rebuilds the whole artifact rather than leaving
# it stale. test/test.h stays a separate explicit prerequisite of the test rules.
HEADERS = $(wildcard src/*.h)
TEST_SRC = test/runner.c
TEST_BIN = build/lorenz-runner
LORENZ_GATE = build/lorenz-gate
LORENZ_BASELINE = test/lorenz_baseline.txt
# One durable unit-test binary per subsystem, plus the Lorenz runner.
UNIT_TESTS = mem cpu cia sid vic drive via iec gcr debug snapshot lorenz_gate
UNIT_BINS = $(addprefix build/test-,$(UNIT_TESTS))
all: $(BIN)
$(BIN): $(SRC) $(HEADERS)
@command -v sdl2-config >/dev/null 2>&1 || { \
echo "error: SDL2 not found. Install it (e.g. sudo apt install libsdl2-dev)."; \
echo "The headless test runner still builds: make test"; exit 1; }
@mkdir -p build
$(CC) $(CFLAGS) $(SDL_CFLAGS) $(SRC) -o $(BIN) $(SDL_LIBS)
$(TEST_BIN): $(TEST_SRC) $(CORE_SRC) $(HEADERS)
@mkdir -p build
$(CC) $(LORENZ_CFLAGS) -Isrc $(TEST_SRC) $(CORE_SRC) -o $(TEST_BIN)
$(LORENZ_GATE): test/lorenz_gate.c test/lorenz_gate.h
@mkdir -p build
$(CC) $(CSTD) $(WARN) $(OPT) -Itest test/lorenz_gate.c -o $(LORENZ_GATE)
build/test-lorenz_gate: test/lorenz_gate_test.c test/lorenz_gate.h test/test.h $(CORE_SRC) $(HEADERS)
@mkdir -p build
$(CC) $(CFLAGS) -Isrc -Itest $< $(CORE_SRC) -o $@
build/test-debug: test/debug_test.c test/test.h $(CORE_SRC) $(HEADERS)
@mkdir -p build
$(CC) $(CSTD) $(WARN) $(OPT) -DDEBUG_TOOLS=1 -Isrc -Itest $< $(CORE_SRC) -o $@
build/test-%: test/%_test.c test/test.h $(CORE_SRC) $(HEADERS)
@mkdir -p build
$(CC) $(CFLAGS) -Isrc -Itest $< $(CORE_SRC) -o $@
# Three test targets, split by speed. `make test` is the fast inner loop (seconds):
# every unit suite plus the fast group of the drive and iec suites. `make test-slow`
# runs their slow group (DOS/serial integration, ~80s). `make test-cpu` runs the
# Wolfgang Lorenz CPU-conformance suite (~14min). The union of test + test-slow is
# the full unit suite (~1030 checks); test-cpu is separate (the Lorenz count is
# reported inline, not folded into that total).
SLOW_BINS = build/test-drive build/test-iec
test: $(UNIT_BINS)
@rc=0; crashed=0; crashlist=""; failed=0; faillist=""; nochecks=0; nclist=""; : > build/test.log; \
for t in $(UNIT_BINS); do \
arg=""; case $${t##*/} in test-drive|test-iec) arg=fast;; esac; \
if ./$$t $$arg >> build/test.log 2>&1; then :; else \
ec=$$?; rc=1; \
if [ $$ec -eq 2 ]; then nochecks=$$((nochecks+1)); nclist="$$nclist $${t##*/}"; \
elif [ $$ec -eq 1 ]; then failed=$$((failed+1)); faillist="$$faillist $${t##*/}(exit1)"; \
echo "*** SUITE FAILED: $${t##*/} completed with failed assertions (counted) ***" \
>> build/test.log; \
else \
crashed=$$((crashed+1)); crashlist="$$crashlist $${t##*/}(exit$$ec)"; \
echo "*** SUITE CRASHED: $${t##*/} exited $$ec, its assertions are UNCOUNTED ***" \
>> build/test.log; \
fi; \
fi; \
done; \
echo "======== unit tests ========"; cat build/test.log; \
echo "======== summary ========"; \
awk '/passed,/{p+=$$2; f+=$$4; s+=$$6} \
END{printf "TOTAL: %d passed, %d failed, %d skipped (fast suites; run make test-slow and make test-cpu for the rest)\n",p,f,s}' build/test.log; \
if [ $$failed -ne 0 ]; then echo "FAILED SUITES ($$failed):$$faillist"; fi; \
if [ $$crashed -ne 0 ]; then echo "CRASHED SUITES ($$crashed):$$crashlist"; fi; \
if [ $$nochecks -ne 0 ]; then echo "SUITES THAT RAN NO CHECKS ($$nochecks):$$nclist"; fi; \
if [ $$failed -ne 0 ] || [ $$crashed -ne 0 ]; then echo "RESULT: FAILURES"; exit 1; fi; \
if [ $$nochecks -ne 0 ]; then echo "RESULT: no checks ran, nothing was verified"; exit 1; fi; \
echo "RESULT: all fast unit suites passed"
# Wolfgang Lorenz 6502/6510 CPU-conformance suite. Slow (~14min); kept out of the fast
# `make test` loop. The runner emits one canonical result on stdout; the C gate parses
# it strictly and compares it to the checked-in baseline. A failing runner (nonzero
# exit) or a frontier that does not match the baseline fails this target.
test-cpu: $(TEST_BIN) $(LORENZ_GATE)
@echo "======== lorenz (CPU conformance) ========"; \
out=build/lorenz-result.txt; \
./$(TEST_BIN) > $$out 2>/dev/null; rc=$$?; \
if ./$(LORENZ_GATE) $(LORENZ_BASELINE) < $$out; then :; else \
echo "FAIL: lorenz gate rejected the run"; cat $$out; rm -f $$out; exit 1; fi; \
if [ $$rc -ne 0 ]; then \
echo "FAIL: lorenz runner exited $$rc (harness failure)"; rm -f $$out; exit 1; fi; \
echo "RESULT: lorenz frontier matches baseline:"; grep '^LORENZ_RESULT ' $$out; rm -f $$out
# Slow integration group: DOS format, LOAD/SAVE/NEW, near-full BAM, writeback,
# and full IEC serial transactions. Same crash-visible aggregation as `make test`.
test-slow: $(SLOW_BINS)
@rc=0; crashed=0; crashlist=""; failed=0; faillist=""; nochecks=0; nclist=""; : > build/test-slow.log; \
for t in $(SLOW_BINS); do \
if ./$$t slow >> build/test-slow.log 2>&1; then :; else \
ec=$$?; rc=1; \
if [ $$ec -eq 2 ]; then nochecks=$$((nochecks+1)); nclist="$$nclist $${t##*/}"; \
elif [ $$ec -eq 1 ]; then failed=$$((failed+1)); faillist="$$faillist $${t##*/}(exit1)"; \
echo "*** SUITE FAILED: $${t##*/} completed with failed assertions (counted) ***" \
>> build/test-slow.log; \
else \
crashed=$$((crashed+1)); crashlist="$$crashlist $${t##*/}(exit$$ec)"; \
echo "*** SUITE CRASHED: $${t##*/} exited $$ec, its assertions are UNCOUNTED ***" \
>> build/test-slow.log; \
fi; \
fi; \
done; \
echo "======== slow integration tests ========"; cat build/test-slow.log; \
echo "======== summary ========"; \
awk '/passed,/{p+=$$2; f+=$$4; s+=$$6} \
END{printf "TOTAL: %d passed, %d failed, %d skipped (slow suites only)\n",p,f,s}' build/test-slow.log; \
if [ $$failed -ne 0 ]; then echo "FAILED SUITES ($$failed):$$faillist"; fi; \
if [ $$crashed -ne 0 ]; then echo "CRASHED SUITES ($$crashed):$$crashlist"; fi; \
if [ $$nochecks -ne 0 ]; then echo "SUITES THAT RAN NO CHECKS ($$nochecks):$$nclist"; fi; \
if [ $$failed -ne 0 ] || [ $$crashed -ne 0 ]; then echo "RESULT: FAILURES"; exit 1; fi; \
if [ $$nochecks -ne 0 ]; then echo "RESULT: no checks ran, nothing was verified"; exit 1; fi; \
echo "RESULT: all slow suites passed"
# Sanitizer build: run the unit suites under AddressSanitizer and
# UndefinedBehaviorSanitizer. The vic suite boots the real KERNAL for 200 frames,
# so this run exercises CPU, VIC, SID, CIA and memory together as well as the unit
# checks. A finding fails the run (UBSan halts, ASAN reports leaks). This target is
# deliberate and slow; the normal `make test` stays fast and unsanitized.
# Lorenz is not run here: under the sanitizers it is far too slow to finish in a
# reasonable time. Run it unsanitized with `make test`.
ASAN_FLAGS = -fsanitize=address,undefined -fno-omit-frame-pointer -g
ASAN_BINS = $(addprefix build/asan-test-,$(UNIT_TESTS))
build/asan-test-debug: test/debug_test.c test/test.h $(CORE_SRC) $(HEADERS)
@mkdir -p build
$(CC) $(CSTD) $(WARN) -O1 $(ASAN_FLAGS) -DDEBUG_TOOLS=1 -Isrc -Itest $< $(CORE_SRC) -o $@
build/asan-test-%: test/%_test.c test/test.h $(CORE_SRC) $(HEADERS)
@mkdir -p build
$(CC) $(CSTD) $(WARN) -O1 $(ASAN_FLAGS) -Isrc -Itest $< $(CORE_SRC) -o $@
test-asan: $(ASAN_BINS)
@rc=0; nochecks=0; \
export UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1; \
export ASAN_OPTIONS=detect_leaks=1; \
for t in $(ASAN_BINS); do \
echo "======== $$t ========"; \
if ./$$t; then :; else ec=$$?; rc=1; \
if [ $$ec -eq 2 ]; then nochecks=1; echo "NO CHECKS RAN in $$t, nothing was verified"; \
else echo "SANITIZER FINDING in $$t"; fi; \
fi; \
done; \
if command -v sdl2-config >/dev/null 2>&1; then \
echo "======== host smoke (SDL dummy drivers) ========"; \
$(CC) $(CSTD) $(WARN) -O1 $(ASAN_FLAGS) -Isrc -Itest $(SDL_CFLAGS) \
test/host_smoke.c src/host.c $(CORE_SRC) -o build/asan-host-smoke $(SDL_LIBS); \
if SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy ./build/asan-host-smoke; then \
echo "host adapter: no findings"; else rc=1; echo "SANITIZER FINDING in host smoke"; fi; \
fi; \
if [ $$rc -ne 0 ]; then \
if [ $$nochecks -eq 1 ]; then echo "RESULT: a suite ran no checks, nothing was verified"; \
else echo "RESULT: sanitizer findings, see above"; fi; exit 1; fi; \
echo "RESULT: no sanitizer findings in the unit suites (incl. the KERNAL boot and host smoke)"
# Line-coverage report (gcov; gcovr/lcov not required). Instruments the core once
# so the five unit suites accumulate into one profile, plus a headless full-machine
# run for drive.c and main.c. Prints per-file line coverage. The Lorenz runner is
# NOT included: under coverage instrumentation it is too slow to finish in a usable
# time, so cpu6502 coverage here reflects the unit suites and the KERNAL boot only,
# not the far larger opcode coverage Lorenz exercises in `make test`.
COV_DIR = build/cov
COV_FULL_DIR = build/cov-full
COV_CORE = bus mem cpu cpu6502 vic sid cia drive via iec disk
COV_CFLAGS = -std=c11 --coverage -O0 -g
coverage:
@command -v gcov >/dev/null 2>&1 || { \
echo "gcov not installed. It ships with gcc: apt install gcc"; exit 1; }
@rm -rf $(COV_DIR) $(COV_FULL_DIR); mkdir -p $(COV_DIR) $(COV_FULL_DIR)
@for f in $(COV_CORE); do $(CC) $(COV_CFLAGS) -Isrc -c src/$$f.c -o $(COV_DIR)/$$f.o; done
@for t in $(UNIT_TESTS); do \
$(CC) $(COV_CFLAGS) -Isrc -Itest test/$$t\_test.c $(COV_DIR)/*.o -o $(COV_DIR)/cov-$$t; \
./$(COV_DIR)/cov-$$t >/dev/null 2>&1 || true; \
done
@if command -v sdl2-config >/dev/null 2>&1; then \
for f in $$(ls src/*.c | xargs -n1 basename | sed 's/\.c$$//'); do \
$(CC) $(COV_CFLAGS) -Isrc $(SDL_CFLAGS) -c src/$$f.c -o $(COV_FULL_DIR)/$$f.o; done; \
$(CC) $(COV_CFLAGS) $(COV_FULL_DIR)/*.o -o $(COV_FULL_DIR)/cov-c64 $(SDL_LIBS); \
./$(COV_FULL_DIR)/cov-c64 --headless >/dev/null 2>&1 || true; \
fi
@echo "======== line coverage (gcov) ========"
@for f in $(COV_CORE); do \
l=$$(gcov -n -o $(COV_DIR) src/$$f.c 2>/dev/null | grep "Lines executed" | head -1); \
printf " %-11s %s\n" "$$f.c" "$$l"; done
@for f in main host; do \
l=$$(gcov -n -o $(COV_FULL_DIR) src/$$f.c 2>/dev/null | grep "Lines executed" | head -1); \
printf " %-11s %s\n" "$$f.c" "$${l:-no data (SDL not available)}"; done
@echo "note: coverage is what the tests execute, not proof of correctness."
# Bounded memory check under Valgrind: the headless run boots the KERNAL to READY,
# attaches the 1541, and steps it to its idle loop, exercising CPU, VIC, SID, CIA,
# memory and the drive together. Valgrind sees leaks, invalid accesses, and
# uninitialised reads that AddressSanitizer does not. Requires valgrind and the
# ROMs. Findings are reported, not fixed.
valgrind: $(BIN)
@command -v valgrind >/dev/null 2>&1 || { \
echo "valgrind not installed: apt install valgrind"; exit 1; }
valgrind --leak-check=full --show-leak-kinds=definite,indirect,possible \
--track-origins=yes --error-exitcode=1 ./$(BIN) --headless
# Fuzz the .d64 mount path (the only untrusted-input parser) with libFuzzer under
# ASan+UBSan. A blank valid image seeds the corpus so mutations explore content at
# the right size, not just the reject path. FUZZ_TIME bounds the run. Findings are
# real defects: they are reported, not fixed silently. Requires clang.
FUZZ_TIME ?= 60
fuzz:
@command -v clang >/dev/null 2>&1 || { \
echo "clang (with libFuzzer) required: apt install clang"; exit 1; }
@mkdir -p build/fuzz-corpus
@head -c 174848 /dev/zero > build/fuzz-corpus/blank.d64
clang $(CSTD) $(WARN) -g -O1 -fsanitize=fuzzer,address,undefined \
-Isrc test/fuzz_d64.c src/disk.c src/fileio.c -o build/fuzz-d64
./build/fuzz-d64 -max_total_time=$(FUZZ_TIME) -print_final_stats=1 build/fuzz-corpus
clean:
rm -rf build
.PHONY: all clean test test-slow test-cpu test-asan coverage valgrind fuzz