OXYGENHUB
# 2980b7b6d7cd enhanced-x16-emulator / tests / test_klaus.c
6.3 KiB · C · last changed chore(): initial commit 26 days ago source blame history raw
1
// Klaus Dormann's 6502/65C02 functional tests, run against the CPU core.
2
//
3
// Covers sequential composition: ~30 million instructions of interdependent
4
// code, rather than instructions considered one at a time. Not the per-opcode
5
// oracle -- ProcessorTests is -- and it reports a single trap address for a
6
// whole run.
7
//
8
// No timing coverage: it verifies results, not cycle counts. mutation_check.py
9
// confirms that by dropping the page-cross penalty and watching this pass.
10
//
11
// The .bin is a full 64K image, execution starts at $0400, and the program
12
// traps by branching to itself. Success is a trap at one specific address from
13
// the listing; a trap anywhere else means a check failed there.
14
//
15
// Binaries are fetched by tests/fetch_klaus.py rather than committed: GPL and
16
// 64K each. Without them this reports skipped.
17
18
#include "support/cpu_fixture.h"
19
#include "support/harness.h"
20
21
#include <stdio.h>
22
#include <stdlib.h>
23
#include <string.h>
24
25
// Where a passing run comes to rest, per bin_files/*.lst in Klaus's repo.
26
#define FUNCTIONAL_SUCCESS 0x3469
27
#define EXTENDED_SUCCESS   0x24F1
28
29
#define START_PC 0x0400
30
31
// The images are full 64K memory dumps. Named rather than taken from the
32
// fixture's memory size, which is now the 65C816's whole 24-bit space.
33
#define KLAUS_IMAGE_SIZE 0x10000
34
35
// Generous: a full functional run is tens of millions of instructions, and the
36
// point of the ceiling is only to stop a broken core spinning forever.
37
#define MAX_INSTRUCTIONS 200000000u
38
39
static const char *
40
find_binary(const char *name, char *buf, size_t buflen)
41
{
42
	// Run from the build directory or the source root, so try both.
43
	const char *prefixes[] = { "tests/klaus/", "../tests/klaus/", "klaus/" };
44
	for (size_t i = 0; i < sizeof prefixes / sizeof prefixes[0]; i++) {
45
		snprintf(buf, buflen, "%s%s", prefixes[i], name);
46
		FILE *f = fopen(buf, "rb");
47
		if (f) {
48
			fclose(f);
49
			return buf;
50
		}
51
	}
52
	return NULL;
53
}
54
55
// Run one test image. Returns the address it trapped at, or 0 if it never did.
56
static uint16_t
57
run_image(const char *path, bool c816)
58
{
59
	FILE *f = fopen(path, "rb");
60
	if (!f) {
61
		return 0;
62
	}
63
64
	cpu_reset_to(c816 ? CPU_816_EMU : CPU_65C02, START_PC);
65
	size_t got = fread(cpu_mem, 1, KLAUS_IMAGE_SIZE, f);
66
	fclose(f);
67
	if (got != KLAUS_IMAGE_SIZE) {
68
		printf("     image is %zu bytes, expected %d\n", got, KLAUS_IMAGE_SIZE);
69
		return 0;
70
	}
71
72
	// The image includes its own vectors, but execution starts at a fixed
73
	// address rather than through reset.
74
	regs.pc = START_PC;
75
76
	uint16_t prev_pc = regs.pc;
77
	for (uint32_t i = 0; i < MAX_INSTRUCTIONS; i++) {
78
		cpu_steps(1);
79
		if (regs.pc == prev_pc) {
80
			return regs.pc; // branched to itself: a trap, pass or fail
81
		}
82
		prev_pc = regs.pc;
83
	}
84
	return 0;
85
}
86
87
// Print the source around the address the test stopped at.
88
//
89
// A trap address on its own is close to useless: it means opening a listing of
90
// several hundred thousand lines and searching by hand. The listing maps the
91
// address straight back to the check that failed, and the lines above it say
92
// which opcode was under test, so print them.
93
static void
94
print_listing_context(const char *bin_name, uint16_t addr)
95
{
96
	char lst_name[128];
97
	char path[256];
98
99
	// 6502_functional_test.bin -> 6502_functional_test.lst
100
	snprintf(lst_name, sizeof lst_name, "%.*s.lst",
101
	         (int)(strlen(bin_name) - 4), bin_name);
102
103
	if (!find_binary(lst_name, path, sizeof path)) {
104
		printf("     (%s not fetched; re-run tests/fetch_klaus.py for the source)\n",
105
		       lst_name);
106
		return;
107
	}
108
109
	FILE *f = fopen(path, "r");
110
	if (!f) {
111
		return;
112
	}
113
114
	// Keep a window of what came before, since the interesting part -- the
115
	// opcode being tested -- is above the line that trapped.
116
	enum { BEFORE = 14 };
117
	static char ring[BEFORE][256];
118
	int    count = 0;
119
	char   line[256];
120
	char   want[8];
121
	snprintf(want, sizeof want, "%04x :", addr);
122
123
	while (fgets(line, sizeof line, f)) {
124
		if (strncmp(line, want, strlen(want)) == 0) {
125
			printf("     --- %s around $%04X ---\n", lst_name, addr);
126
			int first = count > BEFORE ? count - BEFORE : 0;
127
			for (int i = first; i < count; i++) {
128
				printf("     %s", ring[i % BEFORE]);
129
			}
130
			printf("  >> %s", line);
131
			for (int i = 0; i < 2 && fgets(line, sizeof line, f); i++) {
132
				printf("     %s", line);
133
			}
134
			printf("     --- end ---\n");
135
			fclose(f);
136
			return;
137
		}
138
		snprintf(ring[count % BEFORE], sizeof ring[0], "%s", line);
139
		count++;
140
	}
141
142
	fclose(f);
143
	printf("     ($%04X not found in %s)\n", addr, lst_name);
144
}
145
146
// Whether a missing image is a failure rather than a skip. CI sets this, so a
147
// broken or deleted fetch step shows up as a red test instead of a green run
148
// that quietly checked nothing. Locally it is unset, and the images stay
149
// optional.
150
static bool
151
images_required(void)
152
{
153
	const char *v = getenv("X16_KLAUS_REQUIRED");
154
	return v != NULL && v[0] != '\0' && v[0] != '0';
155
}
156
157
static void
158
run_case(const char *name, uint16_t expected_success, const char *divergence)
159
{
160
	char path[256];
161
	char label[192];
162
163
	if (!find_binary(name, path, sizeof path)) {
164
		if (images_required()) {
165
			snprintf(label, sizeof label,
166
			         "%s is present (X16_KLAUS_REQUIRED is set)", name);
167
			check(false, label);
168
			printf("     run tests/fetch_klaus.py, or unset X16_KLAUS_REQUIRED\n");
169
		} else {
170
			printf("skip: %s not present (run tests/fetch_klaus.py)\n", name);
171
		}
172
		return;
173
	}
174
175
	printf("     running %s\n", path);
176
	uint16_t trap = run_image(path, false);
177
	bool passed = (trap == expected_success);
178
179
	snprintf(label, sizeof label, "%s traps at $%04X, expected $%04X",
180
	         name, trap, expected_success);
181
182
	if (divergence != NULL) {
183
		check_divergent(passed, label, divergence);
184
	} else {
185
		check(passed, label);
186
	}
187
188
	if (!passed) {
189
		printf("     A=$%02X X=$%02X Y=$%02X SP=$%04X P=$%02X after %s\n",
190
		       regs.a, regs.xl, regs.yl, regs.sp, regs.status, name);
191
		print_listing_context(name, trap);
192
	}
193
}
194
195
int
196
main(void)
197
{
198
	// Documented opcodes and addressing modes. This is the one that gates.
199
	run_case("6502_functional_test.bin", FUNCTIONAL_SUCCESS, NULL);
200
201
	// The extended test also exercises undefined opcodes, checking their byte
202
	// counts by putting INY instructions in the bytes a correctly sized opcode
203
	// would skip. It failed while $5C, $DC and $FC were one-byte implied NOPs
204
	// rather than three-byte ones, because the operand bytes were executed
205
	// instead of skipped.
206
	run_case("65C02_extended_opcodes_test.bin", EXTENDED_SUCCESS, NULL);
207
208
	return x16_test_summary("klaus");
209
}