src/platform/test/cinema-main.c (view raw)
1/* Copyright (c) 2013-2020 Jeffrey Pfau
2 *
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6#include <mgba/core/config.h>
7#include <mgba/core/core.h>
8#include <mgba/core/log.h>
9#include <mgba/core/version.h>
10#include <mgba/feature/commandline.h>
11#include <mgba/feature/video-logger.h>
12
13#include <mgba-util/png-io.h>
14#include <mgba-util/vector.h>
15#include <mgba-util/vfs.h>
16
17#ifdef _MSC_VER
18#include <mgba-util/platform/windows/getopt.h>
19#else
20#include <getopt.h>
21#endif
22
23#include <stdlib.h>
24
25#define MAX_TEST 200
26
27static const struct option longOpts[] = {
28 { "base", required_argument, 0, 'b' },
29 { "help", no_argument, 0, 'h' },
30 { "quiet", no_argument, 0, 'q' },
31 { "dry-run", no_argument, 0, 'n' },
32 { "verbose", no_argument, 0, 'v' },
33 { "version", no_argument, 0, '\0' },
34 { 0, 0, 0, 0 }
35};
36
37static const char shortOpts[] = "b:hnqv";
38
39enum CInemaStatus {
40 CI_PASS,
41 CI_FAIL,
42 CI_XPASS,
43 CI_XFAIL,
44 CI_ERROR,
45 CI_SKIP
46};
47
48struct CInemaTest {
49 char directory[MAX_TEST];
50 char filename[MAX_TEST];
51 char name[MAX_TEST];
52 enum CInemaStatus status;
53 int failedFrames;
54};
55
56DECLARE_VECTOR(CInemaTestList, struct CInemaTest)
57DEFINE_VECTOR(CInemaTestList, struct CInemaTest)
58
59DECLARE_VECTOR(ImageList, void*)
60DEFINE_VECTOR(ImageList, void*)
61
62static bool showVersion = false;
63static bool showUsage = false;
64static char base[PATH_MAX] = {0};
65static bool dryRun = false;
66static int verbosity = 0;
67
68bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename);
69void CInemaTestRun(struct CInemaTest*);
70
71static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args);
72
73ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) {
74 if (verbosity < minlevel) {
75 return;
76 }
77 va_list args;
78 va_start(args, format);
79 vprintf(format, args);
80 va_end(args);
81}
82
83ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) {
84 if (verbosity < minlevel) {
85 return;
86 }
87 va_list args;
88 va_start(args, format);
89 vfprintf(stderr, format, args);
90 va_end(args);
91}
92
93static bool parseCInemaArgs(int argc, char* const* argv) {
94 int ch;
95 int index = 0;
96 while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
97 const struct option* opt = &longOpts[index];
98 switch (ch) {
99 case '\0':
100 if (strcmp(opt->name, "version") == 0) {
101 showVersion = true;
102 } else {
103 return false;
104 }
105 break;
106 case 'b':
107 strncpy(base, optarg, sizeof(base));
108 // TODO: Verify path exists
109 break;
110 case 'h':
111 showUsage = true;
112 break;
113 case 'n':
114 dryRun = true;
115 break;
116 case 'q':
117 --verbosity;
118 break;
119 case 'v':
120 ++verbosity;
121 break;
122 default:
123 return false;
124 }
125 }
126
127 return true;
128}
129
130static void usageCInema(const char* arg0) {
131 printf("usage: %s [-h] [-b BASE] [--version] [test...]\n", arg0);
132 puts(" -b, --base Path to the CInema base directory");
133 puts(" -h, --help Print this usage and exit");
134 puts(" -n, --dry-run List all collected tests instead of running them");
135 puts(" -q, --quiet Decrease log verbosity (can be repeated)");
136 puts(" -v, --verbose Increase log verbosity (can be repeated)");
137 puts(" --version Print version and exit");
138}
139
140static bool determineBase(int argc, char* const* argv) {
141 // TODO: Better dynamic detection
142 separatePath(__FILE__, base, NULL, NULL);
143 strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
144 return true;
145}
146
147static bool collectTests(struct CInemaTestList* tests, const char* path) {
148 CIerr(2, "Considering path %s\n", path);
149 struct VDir* dir = VDirOpen(path);
150 if (!dir) {
151 return false;
152 }
153 struct VDirEntry* entry = dir->listNext(dir);
154 while (entry) {
155 char subpath[PATH_MAX];
156 snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
157 if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
158 if (!collectTests(tests, subpath)) {
159 dir->close(dir);
160 return false;
161 }
162 } else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
163 CIerr(2, "Found potential test %s\n", subpath);
164 struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
165 if (vf) {
166 if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) {
167 struct CInemaTest* test = CInemaTestListAppend(tests);
168 if (!CInemaTestInit(test, path, entry->name(entry))) {
169 CIerr(2, "Failed to create test\n");
170 CInemaTestListResize(tests, -1);
171 } else {
172 CIerr(1, "Found test %s\n", test->name);
173 }
174 } else {
175 CIerr(2, "Not a compatible file\n");
176 }
177 vf->close(vf);
178 } else {
179 CIerr(2, "Failed to open file\n");
180 }
181 }
182 entry = dir->listNext(dir);
183 }
184 dir->close(dir);
185 return true;
186}
187
188static int _compareNames(const void* a, const void* b) {
189 const struct CInemaTest* ta = a;
190 const struct CInemaTest* tb = b;
191
192 return strncmp(ta->name, tb->name, sizeof(ta->name));
193}
194
195static void reduceTestList(struct CInemaTestList* tests) {
196 qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
197
198 size_t i;
199 for (i = 1; i < CInemaTestListSize(tests);) {
200 struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
201 struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
202 if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
203 ++i;
204 continue;
205 }
206 CInemaTestListShift(tests, i, 1);
207 }
208}
209
210bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
211 if (strncmp(base, directory, strlen(base)) != 0) {
212 return false;
213 }
214 strncpy(test->directory, directory, sizeof(test->directory));
215 strncpy(test->filename, filename, sizeof(test->filename));
216 directory += strlen(base) + 1;
217 strncpy(test->name, directory, sizeof(test->name));
218 char* str = strstr(test->name, PATH_SEP);
219 while (str) {
220 str[0] = '.';
221 str = strstr(str, PATH_SEP);
222 }
223 return true;
224}
225
226void CInemaTestRun(struct CInemaTest* test) {
227 struct VDir* dir = VDirOpen(test->directory);
228 if (!dir) {
229 CIerr(0, "Failed to open test directory\n");
230 test->status = CI_ERROR;
231 return;
232 }
233 struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
234 if (!rom) {
235 CIerr(0, "Failed to open test\n");
236 test->status = CI_ERROR;
237 return;
238 }
239 struct mCore* core = mCoreFindVF(rom);
240 if (!core) {
241 CIerr(0, "Failed to load test\n");
242 test->status = CI_ERROR;
243 rom->close(rom);
244 return;
245 }
246 if (!core->init(core)) {
247 CIerr(0, "Failed to init test\n");
248 test->status = CI_ERROR;
249 core->deinit(core);
250 return;
251 }
252 unsigned width, height;
253 core->desiredVideoDimensions(core, &width, &height);
254 ssize_t bufferSize = width * height * BYTES_PER_PIXEL;
255 void* buffer = malloc(bufferSize);
256 if (!buffer) {
257 CIerr(0, "Failed to allocate video buffer\n");
258 test->status = CI_ERROR;
259 core->deinit(core);
260 }
261 core->setVideoBuffer(core, buffer, width);
262 mCoreConfigInit(&core->config, "cinema");
263
264 core->loadROM(core, rom);
265 core->reset(core);
266
267 test->status = CI_PASS;
268
269 unsigned minFrame = core->frameCounter(core);
270 unsigned limit = 9999;
271 size_t frame;
272 for (frame = 0; limit; ++frame, --limit) {
273 char baselineName[32];
274 snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
275 core->runFrame(core);
276 unsigned frameCounter = core->frameCounter(core);
277 if (frameCounter <= minFrame) {
278 break;
279 }
280 CIerr(2, "Test frame: %u\n", frameCounter);
281
282 struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
283 if (!baselineVF) {
284 test->status = CI_FAIL;
285 } else {
286 png_structp png = PNGReadOpen(baselineVF, 0);
287 png_infop info = png_create_info_struct(png);
288 png_infop end = png_create_info_struct(png);
289 if (!png || !info || !end || !PNGReadHeader(png, info)) {
290 PNGReadClose(png, info, end);
291 CIerr(1, "Failed to load %s\n", baselineName);
292 test->status = CI_ERROR;
293 } else {
294 unsigned pwidth = png_get_image_width(png, info);
295 unsigned pheight = png_get_image_height(png, info);
296 unsigned twidth, theight;
297 core->desiredVideoDimensions(core, &twidth, &theight);
298 if (pheight != theight || pwidth != twidth) {
299 PNGReadClose(png, info, end);
300 CIerr(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, twidth, theight);
301 test->status = CI_FAIL;
302 } else {
303 uint8_t* pixels = malloc(pwidth * pheight * BYTES_PER_PIXEL);
304 if (!pixels) {
305 CIerr(1, "Failed to allocate baseline buffer\n");
306 test->status = CI_ERROR;
307 } else {
308 if (!PNGReadPixels(png, info, pixels, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
309 CIerr(1, "Failed to read %s\n", baselineName);
310 test->status = CI_ERROR;
311 } else {
312 uint8_t* testPixels = buffer;
313 size_t x;
314 size_t y;
315 for (y = 0; y < theight; ++y) {
316 for (x = 0; x < twidth; ++x) {
317 size_t pix = pwidth * y + x;
318 size_t tpix = width * y + x;
319 int testR = testPixels[tpix * 4 + 0];
320 int testG = testPixels[tpix * 4 + 1];
321 int testB = testPixels[tpix * 4 + 2];
322 int expectR = pixels[pix * 4 + 0];
323 int expectG = pixels[pix * 4 + 1];
324 int expectB = pixels[pix * 4 + 2];
325 int r = expectR - testR;
326 int g = expectG - testG;
327 int b = expectB - testB;
328 if (r | g | b) {
329 CIerr(2, "Frame %u failed at pixel %" PRIz "ux%" PRIz "u with diff %i,%i,%i (expected %02x%02x%02x, got %02x%02x%02x)\n",
330 frameCounter, x, y, r, g, b,
331 expectR, expectG, expectB,
332 testR, testG, testB);
333 test->status = CI_FAIL;
334 ++test->failedFrames;
335 }
336 }
337 }
338 }
339 }
340 PNGReadClose(png, info, end);
341 free(pixels);
342 }
343 }
344 baselineVF->close(baselineVF);
345 }
346 }
347
348 free(buffer);
349 core->deinit(core);
350 dir->close(dir);
351}
352
353void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
354 // TODO: Write
355}
356
357int main(int argc, char** argv) {
358 int status = 0;
359 if (!parseCInemaArgs(argc, argv)) {
360 status = 1;
361 goto cleanup;
362 }
363
364 if (showVersion) {
365 version(argv[0]);
366 goto cleanup;
367 }
368
369 if (showUsage) {
370 usageCInema(argv[0]);
371 goto cleanup;
372 }
373
374 argc -= optind;
375 argv += optind;
376
377 if (!base[0] && !determineBase(argc, argv)) {
378 CIerr(0, "Could not determine CInema test base. Please specify manually.");
379 status = 1;
380 goto cleanup;
381 }
382#ifndef _WIN32
383 char* rbase = realpath(base, NULL);
384 strncpy(base, rbase, PATH_MAX);
385 free(rbase);
386#endif
387
388 struct CInemaTestList tests;
389 CInemaTestListInit(&tests, 0);
390
391 struct mLogger logger = { .log = _log };
392 mLogSetDefaultLogger(&logger);
393
394 if (argc > 0) {
395 size_t i;
396 for (i = 0; i < (size_t) argc; ++i) {
397 char path[PATH_MAX + 1] = {0};
398 char* arg = argv[i];
399 strncpy(path, base, sizeof(path));
400
401 bool dotSeen = true;
402 size_t j;
403 for (arg = argv[i], j = strlen(path); arg[0] && j < sizeof(path); ++arg) {
404 if (arg[0] == '.') {
405 dotSeen = true;
406 } else {
407 if (dotSeen) {
408 strncpy(&path[j], PATH_SEP, sizeof(path) - j);
409 j += strlen(PATH_SEP);
410 dotSeen = false;
411 if (!j) {
412 break;
413 }
414 }
415 path[j] = arg[0];
416 ++j;
417 }
418 }
419
420 if (!collectTests(&tests, path)) {
421 status = 1;
422 break;
423 }
424 }
425 } else if (!collectTests(&tests, base)) {
426 status = 1;
427 }
428
429 if (CInemaTestListSize(&tests) == 0) {
430 CIerr(1, "No tests found.");
431 status = 1;
432 } else {
433 reduceTestList(&tests);
434 }
435
436 size_t i;
437 for (i = 0; i < CInemaTestListSize(&tests); ++i) {
438 struct CInemaTest* test = CInemaTestListGetPointer(&tests, i);
439 if (dryRun) {
440 CIlog(-1, "%s\n", test->name);
441 } else {
442 CIerr(1, "%s: ", test->name);
443 CInemaTestRun(test);
444 switch (test->status) {
445 case CI_PASS:
446 CIerr(1, "pass");
447 break;
448 case CI_FAIL:
449 status = 1;
450 CIerr(1, "fail");
451 break;
452 case CI_XPASS:
453 CIerr(1, "xpass");
454 break;
455 case CI_XFAIL:
456 CIerr(1, "xfail");
457 break;
458 case CI_SKIP:
459 CIerr(1, "skip");
460 break;
461 case CI_ERROR:
462 status = 1;
463 CIerr(1, "error");
464 break;
465 }
466 CIerr(1, "\n");
467 }
468 }
469
470 CInemaTestListDeinit(&tests);
471
472cleanup:
473 return status;
474}