OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_io_trace.c
9.8 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// The I/O trace ring buffer: retention, ordering, gating and the CPU stamp.
2
//
3
// The ring is what the I/O panel shows, so the properties worth pinning are the
4
// ones a reader of that panel relies on without thinking about it: that the
5
// events are the most recent ones, in the order they happened; that a device
6
// whose capture bit is clear contributes nothing; and that sequence numbers
7
// keep increasing so "later in the list" always means "later in time", even
8
// across a resize or a clear.
9
//
10
// io_trace.c deliberately has no emulator dependencies, so this test links it
11
// alone -- no CPU, no memory, no SDL.
12
13
#include "support/harness.h"
14
15
#include "../src/io_trace.h"
16
17
#include <string.h>
18
19
// A stand-in for memory.c's provider, so the stamping path is exercised without
20
// linking the CPU.
21
static uint16_t g_pc     = 0;
22
static uint8_t  g_bank   = 0;
23
static uint32_t g_cycles = 0;
24
25
static void
26
fake_context(uint16_t *pc, uint8_t *pc_bank, uint32_t *cycles)
27
{
28
	*pc      = g_pc;
29
	*pc_bank = g_bank;
30
	*cycles  = g_cycles;
31
}
32
33
static void
34
capture_everything(void)
35
{
36
	io_trace_enabled     = true;
37
	io_trace_device_mask = 0xffffffffu;
38
}
39
40
static void
41
test_retains_the_most_recent(void)
42
{
43
	check(io_trace_init(64), "a ring can be allocated");
44
	io_trace_clear();
45
	capture_everything();
46
47
	for (int i = 0; i < 100; i++) {
48
		io_trace_access(IO_DEV_VIA1, (uint16_t)(0x9f00 + (i & 0x0f)), (uint8_t)i, false);
49
	}
50
51
	check_eq((uint32_t)io_trace_count(), 64, "the ring holds its capacity, not every event");
52
	check_eq(io_trace_dropped(), 36, "and reports what it had to overwrite");
53
54
	// Index 0 is the oldest RETAINED event, which after 100 pushes into 64
55
	// slots is the 37th (value 36).
56
	const io_event_t *oldest = io_trace_at(0);
57
	check(oldest != NULL, "the oldest retained event is reachable");
58
	if (oldest != NULL) {
59
		check_eq(oldest->value, 36, "and it is the oldest one that survived");
60
	}
61
62
	const io_event_t *newest = io_trace_at(io_trace_count() - 1);
63
	check(newest != NULL, "the newest event is reachable");
64
	if (newest != NULL) {
65
		check_eq(newest->value, 99, "and it is the one most recently recorded");
66
	}
67
68
	check(io_trace_at(-1) == NULL, "an index below the range is refused");
69
	check(io_trace_at(io_trace_count()) == NULL, "and so is one past the end");
70
}
71
72
static void
73
test_sequence_numbers_only_increase(void)
74
{
75
	check(io_trace_init(128), "a ring can be reallocated");
76
	io_trace_clear();
77
	capture_everything();
78
79
	io_trace_access(IO_DEV_VIA1, 0x9f00, 0x11, false);
80
	const io_event_t *first = io_trace_at(0);
81
	const uint32_t    seq   = (first != NULL) ? first->seq : 0;
82
83
	// A resize throws the contents away, because ordering against a new ring is
84
	// not meaningful -- but the sequence must not restart, or two unrelated
85
	// events would claim the same moment.
86
	check(io_trace_init(256), "the ring can be resized");
87
	check_eq((uint32_t)io_trace_count(), 0, "which discards what it held");
88
	check_eq((uint32_t)io_trace_capacity(), 256, "and adopts the new capacity");
89
90
	io_trace_access(IO_DEV_VIA1, 0x9f00, 0x22, false);
91
	const io_event_t *after = io_trace_at(0);
92
	check(after != NULL, "and keeps recording afterwards");
93
	if (after != NULL) {
94
		check(after->seq > seq, "with a sequence number that did not restart");
95
	}
96
97
	// Ordering within the ring must follow the sequence.
98
	io_trace_access(IO_DEV_VIA1, 0x9f00, 0x33, false);
99
	const io_event_t *a = io_trace_at(0);
100
	const io_event_t *b = io_trace_at(1);
101
	if (a != NULL && b != NULL) {
102
		check(b->seq > a->seq, "later entries carry later sequence numbers");
103
	}
104
}
105
106
static void
107
test_capacity_is_clamped(void)
108
{
109
	// A capacity the caller asks for is a request, not an instruction: a ring
110
	// of four events is useless for reading a trace, and an enormous one is a
111
	// way to exhaust memory from a settings field. Both ends are clamped, and
112
	// the accessor reports what was actually allocated so the UI can show the
113
	// real number rather than the one it asked for.
114
	check(io_trace_init(1), "an absurdly small ring is accepted");
115
	check(io_trace_capacity() >= 64, "but clamped up to something usable");
116
117
	check(io_trace_init(1 << 30), "an absurdly large ring is accepted");
118
	check(io_trace_capacity() <= 262144, "but clamped down to something affordable");
119
}
120
121
static void
122
test_capture_gating(void)
123
{
124
	check(io_trace_init(64), "a ring for the gating checks");
125
	io_trace_clear();
126
127
	// Master switch off: nothing is wanted, whatever the mask says.
128
	io_trace_enabled     = false;
129
	io_trace_device_mask = 0xffffffffu;
130
	check(!io_trace_wants(IO_DEV_VIA1), "a disabled trace wants nothing");
131
	check(!io_trace_wants(IO_DEV_SDCARD), "not even from an unmasked device");
132
133
	// Master on, one device masked out.
134
	io_trace_enabled     = true;
135
	io_trace_device_mask = IO_TRACE_BIT(IO_DEV_SDCARD);
136
	check(io_trace_wants(IO_DEV_SDCARD), "an unmasked device is wanted");
137
	check(!io_trace_wants(IO_DEV_VERA), "a masked-out device is not");
138
139
	// The default mask is the one the panel starts with: everything except
140
	// VERA's registers, which would otherwise be the only thing in the ring,
141
	// and open-bus reads, which say nothing.
142
	io_trace_device_mask = IO_TRACE_MASK_DEFAULT;
143
	check(!io_trace_wants(IO_DEV_VERA), "VERA is excluded by default");
144
	check(!io_trace_wants(IO_DEV_OPENBUS), "and so is open bus");
145
	check(io_trace_wants(IO_DEV_SPI), "but the SD data path is not");
146
	check(io_trace_wants(IO_DEV_VIA1), "nor VIA1");
147
	check(io_trace_wants(IO_DEV_SDCARD), "nor decoded SD events");
148
}
149
150
static void
151
test_records_what_it_was_given(void)
152
{
153
	check(io_trace_init(64), "a ring for the payload checks");
154
	io_trace_clear();
155
	capture_everything();
156
	io_trace_set_context_provider(fake_context);
157
158
	g_pc     = 0xc123;
159
	g_bank   = 0x02;
160
	g_cycles = 999;
161
162
	io_trace_access(IO_DEV_VIA1, 0x9f01, 0x5a, true);
163
164
	const io_event_t *ev = io_trace_at(0);
165
	check(ev != NULL, "an access is recorded");
166
	if (ev != NULL) {
167
		check_eq(ev->addr, 0x9f01, "with the address it was given");
168
		check_eq(ev->value, 0x5a, "and the value");
169
		check(ev->is_write, "and the direction");
170
		check(ev->has_addr, "and is marked as naming a register");
171
		check_eq(ev->device, IO_DEV_VIA1, "and the device");
172
		check_eq(ev->kind, IO_EVENT_ACCESS, "and is an access, not a decoded event");
173
		check_eq(ev->pc, 0xc123, "stamped with the PC from the provider");
174
		check_eq(ev->pc_bank, 0x02, "and the program bank");
175
		check_eq(ev->cycles, 999, "and the cycle count");
176
	}
177
178
	io_trace_set_context_provider(NULL);
179
}
180
181
static void
182
test_decoded_events(void)
183
{
184
	check(io_trace_init(64), "a ring for the decoded-event checks");
185
	io_trace_clear();
186
	capture_everything();
187
188
	io_trace_event(IO_DEV_SDCARD, "CMD%d lba=%u", 17, 74216u);
189
190
	const io_event_t *ev = io_trace_at(0);
191
	check(ev != NULL, "a decoded event is recorded");
192
	if (ev != NULL) {
193
		check_eq(ev->kind, IO_EVENT_DECODED, "marked as decoded");
194
		check(strcmp(ev->text, "CMD17 lba=74216") == 0, "with its text formatted");
195
		check(!ev->has_addr, "and naming no register");
196
	}
197
198
	io_trace_event_at(IO_DEV_SPI, 0x9f3e, true, "byte out $%02X", 0x40);
199
	const io_event_t *at = io_trace_at(1);
200
	check(at != NULL, "a register-attributed decoded event is recorded");
201
	if (at != NULL) {
202
		check(at->has_addr, "and does name a register");
203
		check_eq(at->addr, 0x9f3e, "which is the one given");
204
		check(at->is_write, "with its direction");
205
	}
206
207
	// Over-long text must be truncated rather than overrun the fixed buffer.
208
	io_trace_clear();
209
	io_trace_event(IO_DEV_IEEE,
210
	               "%s",
211
	               "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
212
	               "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
213
	const io_event_t *big = io_trace_at(0);
214
	check(big != NULL, "an over-long decoded event is still recorded");
215
	if (big != NULL) {
216
		check_eq((uint32_t)strlen(big->text), IO_TRACE_TEXT_MAX - 1,
217
		         "with its text truncated to the buffer");
218
	}
219
}
220
221
static void
222
test_clear_keeps_the_ring_usable(void)
223
{
224
	check(io_trace_init(64), "a ring for the clear checks");
225
	capture_everything();
226
227
	io_trace_access(IO_DEV_VIA1, 0x9f00, 0x01, false);
228
	io_trace_clear();
229
	check_eq((uint32_t)io_trace_count(), 0, "clearing empties the ring");
230
	check_eq(io_trace_dropped(), 0, "and resets the overwrite count");
231
	check(io_trace_at(0) == NULL, "leaving nothing to read");
232
233
	io_trace_access(IO_DEV_VIA1, 0x9f00, 0x02, false);
234
	check_eq((uint32_t)io_trace_count(), 1, "and recording resumes");
235
}
236
237
static void
238
test_survives_use_before_init(void)
239
{
240
	// A call site that fires before anything allocated the ring must not crash.
241
	// The module allocates on demand instead, so that merely enabling capture
242
	// works rather than silently recording nothing.
243
	io_trace_shutdown();
244
	check_eq((uint32_t)io_trace_capacity(), 0, "the ring starts unallocated");
245
	capture_everything();
246
247
	io_trace_access(IO_DEV_VIA1, 0x9f00, 0x77, false);
248
	check(io_trace_capacity() > 0, "recording allocates on first use");
249
	check_eq((uint32_t)io_trace_count(), 1, "and the event is kept");
250
251
	const io_event_t *ev = io_trace_at(0);
252
	if (ev != NULL) {
253
		check_eq(ev->value, 0x77, "with its value intact");
254
	}
255
256
	io_trace_shutdown();
257
	check(io_trace_at(0) == NULL, "and shutting down leaves nothing readable");
258
}
259
260
static void
261
test_device_names(void)
262
{
263
	// The panel prints these, so an unnamed device would show as a blank column
264
	// rather than anything a reader could act on.
265
	for (int d = 0; d < IO_DEV_COUNT; d++) {
266
		const char *name = io_trace_device_name((io_device_t)d);
267
		check(name != NULL && name[0] != '\0' && strcmp(name, "?") != 0,
268
		      "every device has a display name");
269
	}
270
	check(strcmp(io_trace_device_name((io_device_t)IO_DEV_COUNT), "?") == 0,
271
	      "and an unknown one is named rather than left dangling");
272
}
273
274
int
275
main(void)
276
{
277
	test_retains_the_most_recent();
278
	test_sequence_numbers_only_increase();
279
	test_capacity_is_clamped();
280
	test_capture_gating();
281
	test_records_what_it_was_given();
282
	test_decoded_events();
283
	test_clear_keeps_the_ring_usable();
284
	test_survives_use_before_init();
285
	test_device_names();
286
287
	io_trace_shutdown();
288
	return x16_test_summary("io_trace");
289
}