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/string.h>
15#include <mgba-util/table.h>
16#include <mgba-util/threading.h>
17#include <mgba-util/vector.h>
18#include <mgba-util/vfs.h>
19
20#ifdef USE_FFMPEG
21#include "feature/ffmpeg/ffmpeg-decoder.h"
22#include "feature/ffmpeg/ffmpeg-encoder.h"
23#endif
24
25#ifdef _MSC_VER
26#include <mgba-util/platform/windows/getopt.h>
27#else
28#include <getopt.h>
29#endif
30
31#include <stdlib.h>
32#include <sys/stat.h>
33#include <sys/types.h>
34
35#define MAX_TEST 200
36#define MAX_JOBS 128
37#define LOG_THRESHOLD 1000000
38
39static const struct option longOpts[] = {
40 { "base", required_argument, 0, 'b' },
41 { "diffs", no_argument, 0, 'd' },
42 { "help", no_argument, 0, 'h' },
43 { "jobs", required_argument, 0, 'j' },
44 { "dry-run", no_argument, 0, 'n' },
45 { "outdir", required_argument, 0, 'o' },
46 { "quiet", no_argument, 0, 'q' },
47 { "rebaseline", no_argument, 0, 'r' },
48 { "rebaseline-missing", no_argument, 0, 'R' },
49 { "verbose", no_argument, 0, 'v' },
50 { "xbaseline", no_argument, 0, 'x' },
51 { "version", no_argument, 0, '\0' },
52 { 0, 0, 0, 0 }
53};
54
55static const char shortOpts[] = "b:dhj:no:qRrvx";
56
57enum CInemaStatus {
58 CI_PASS,
59 CI_FAIL,
60 CI_XPASS,
61 CI_XFAIL,
62 CI_ERROR,
63 CI_SKIP
64};
65
66enum CInemaRebaseline {
67 CI_R_NONE = 0,
68 CI_R_FAILING,
69 CI_R_MISSING,
70};
71
72struct CInemaTest {
73 char directory[MAX_TEST];
74 char filename[MAX_TEST];
75 char name[MAX_TEST];
76 enum CInemaStatus status;
77 unsigned failedFrames;
78 uint64_t failedPixels;
79 unsigned totalFrames;
80 uint64_t totalDistance;
81 uint64_t totalPixels;
82};
83
84struct CInemaImage {
85 void* data;
86 unsigned width;
87 unsigned height;
88 unsigned stride;
89};
90
91DECLARE_VECTOR(CInemaTestList, struct CInemaTest)
92DEFINE_VECTOR(CInemaTestList, struct CInemaTest)
93
94DECLARE_VECTOR(ImageList, void*)
95DEFINE_VECTOR(ImageList, void*)
96
97struct StringBuilder {
98 struct StringList lines;
99 struct StringList partial;
100 unsigned repeat;
101
102};
103
104struct CInemaLogStream {
105 struct StringBuilder err;
106 struct StringBuilder out;
107};
108
109static bool showVersion = false;
110static bool showUsage = false;
111static char base[PATH_MAX] = {0};
112static char outdir[PATH_MAX] = {'.'};
113static bool dryRun = false;
114static bool diffs = false;
115static enum CInemaRebaseline rebaseline = CI_R_NONE;
116static enum CInemaRebaseline xbaseline = CI_R_NONE;
117static int verbosity = 0;
118
119static struct Table configTree;
120static Mutex configMutex;
121
122static int jobs = 1;
123static size_t jobIndex = 0;
124static Mutex jobMutex;
125static Thread jobThreads[MAX_JOBS];
126static int jobStatus;
127static ThreadLocal logStream;
128static ThreadLocal currentTest;
129
130bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename);
131void CInemaTestRun(struct CInemaTest*);
132
133bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* value);
134void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core);
135
136static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args);
137
138void CIflush(struct StringBuilder* list, FILE* file);
139
140static char* _compileStringList(struct StringList* list) {
141 size_t len = 0;
142 size_t i;
143 for (i = 0; i < StringListSize(list); ++i) {
144 len += strlen(*StringListGetPointer(list, i));
145 }
146 char* string = calloc(len + 1, sizeof(char));
147 char* cur = string;
148 for (i = 0; i < StringListSize(list); ++i) {
149 char* brick = *StringListGetPointer(list, i);
150 size_t portion = strlen(brick);
151 memcpy(cur, brick, portion);
152 free(brick);
153 cur += portion;
154 }
155 StringListClear(list);
156 return string;
157}
158
159static void _logToStream(FILE* file, const char* format, va_list args) {
160#ifdef HAVE_VASPRINTF
161 struct CInemaLogStream* stream = ThreadLocalGetValue(logStream);
162 if (!stream) {
163 vfprintf(file, format, args);
164 } else {
165 struct StringBuilder* builder = &stream->out;
166 if (file == stderr) {
167 builder = &stream->err;
168 }
169 if (StringListSize(&builder->lines) > LOG_THRESHOLD) {
170 CIflush(builder, file);
171 }
172 char** line = StringListAppend(&builder->partial);
173 vasprintf(line, format, args);
174 size_t len = strlen(*line);
175 if (len && (*line)[len - 1] == '\n') {
176 char* string = _compileStringList(&builder->partial);
177 size_t linecount = StringListSize(&builder->lines);
178 if (linecount && strcmp(string, *StringListGetPointer(&builder->lines, linecount - 1)) == 0) {
179 ++builder->repeat;
180 free(string);
181 } else {
182 if (builder->repeat > 1) {
183 asprintf(StringListAppend(&builder->lines), "The previous message was repeated %u times.\n", builder->repeat);
184 }
185 *StringListAppend(&builder->lines) = string;
186 builder->repeat = 1;
187 }
188 }
189 }
190#else
191 vfprintf(file, format, args);
192#endif
193}
194
195ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) {
196 if (verbosity < minlevel) {
197 return;
198 }
199 va_list args;
200 va_start(args, format);
201 _logToStream(stdout, format, args);
202 va_end(args);
203}
204
205ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) {
206 if (verbosity < minlevel) {
207 return;
208 }
209 va_list args;
210 va_start(args, format);
211 _logToStream(stderr, format, args);
212 va_end(args);
213}
214
215void CIflush(struct StringBuilder* builder, FILE* out) {
216 if (StringListSize(&builder->partial)) {
217 *StringListAppend(&builder->lines) = _compileStringList(&builder->partial);
218 }
219#ifdef HAVE_VASPRINTF
220 if (builder->repeat > 1) {
221 asprintf(StringListAppend(&builder->lines), "The previous message was repeated %u times.\n", builder->repeat);
222 }
223#endif
224
225 char* string = _compileStringList(&builder->lines);
226 builder->repeat = 0;
227 fputs(string, out);
228 free(string);
229}
230
231static bool parseCInemaArgs(int argc, char* const* argv) {
232 int ch;
233 int index = 0;
234 while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
235 const struct option* opt = &longOpts[index];
236 switch (ch) {
237 case '\0':
238 if (strcmp(opt->name, "version") == 0) {
239 showVersion = true;
240 } else {
241 return false;
242 }
243 break;
244 case 'b':
245 strlcpy(base, optarg, sizeof(base));
246 // TODO: Verify path exists
247 break;
248 case 'd':
249 diffs = true;
250 break;
251 case 'h':
252 showUsage = true;
253 break;
254 case 'j':
255 jobs = atoi(optarg);
256 if (jobs > MAX_JOBS) {
257 jobs = MAX_JOBS;
258 }
259 if (jobs < 1) {
260 jobs = 1;
261 }
262 break;
263 case 'n':
264 dryRun = true;
265 break;
266 case 'o':
267 strlcpy(outdir, optarg, sizeof(outdir));
268 // TODO: Make directory
269 break;
270 case 'q':
271 --verbosity;
272 break;
273 case 'r':
274 rebaseline = CI_R_FAILING;
275 break;
276 case 'R':
277 rebaseline = CI_R_MISSING;
278 break;
279 case 'v':
280 ++verbosity;
281 break;
282 case 'x':
283 xbaseline = CI_R_FAILING;
284 break;
285 default:
286 return false;
287 }
288 }
289
290 return true;
291}
292
293static void usageCInema(const char* arg0) {
294 printf("usage: %s [-dhnqrRv] [-j JOBS] [-b BASE] [-o DIR] [--version] [test...]\n", arg0);
295 puts(" -b, --base BASE Path to the CInema base directory");
296 puts(" -d, --diffs Output image diffs from failures");
297 puts(" -h, --help Print this usage and exit");
298 puts(" -j, --jobs JOBS Run a number of jobs in parallel");
299 puts(" -n, --dry-run List all collected tests instead of running them");
300 puts(" -o, --output DIR Path to output applicable results");
301 puts(" -q, --quiet Decrease log verbosity (can be repeated)");
302 puts(" -r, --rebaseline Rewrite the baseline for failing tests");
303 puts(" -R, --rebaseline-missing Write missing baselines tests only");
304 puts(" -v, --verbose Increase log verbosity (can be repeated)");
305 puts(" -x, --xbaseline Rewrite the xfail baselines for failing tests");
306 puts(" --version Print version and exit");
307}
308
309static bool determineBase(int argc, char* const* argv) {
310 // TODO: Better dynamic detection
311 separatePath(__FILE__, base, NULL, NULL);
312 strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
313 return true;
314}
315
316static bool collectTests(struct CInemaTestList* tests, const char* path) {
317 CIerr(2, "Considering path %s\n", path);
318 struct VDir* dir = VDirOpen(path);
319 if (!dir) {
320 return false;
321 }
322 struct VDirEntry* entry = dir->listNext(dir);
323 while (entry) {
324 char subpath[PATH_MAX];
325 snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
326 if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
327 if (!collectTests(tests, subpath)) {
328 dir->close(dir);
329 return false;
330 }
331 } else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
332 CIerr(3, "Found potential test %s\n", subpath);
333 struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
334 if (vf) {
335 if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) {
336 struct CInemaTest* test = CInemaTestListAppend(tests);
337 if (!CInemaTestInit(test, path, entry->name(entry))) {
338 CIerr(3, "Failed to create test\n");
339 CInemaTestListResize(tests, -1);
340 } else {
341 CIerr(2, "Found test %s\n", test->name);
342 }
343 } else {
344 CIerr(3, "Not a compatible file\n");
345 }
346 vf->close(vf);
347 } else {
348 CIerr(3, "Failed to open file\n");
349 }
350 }
351 entry = dir->listNext(dir);
352 }
353 dir->close(dir);
354 return true;
355}
356
357static int _compareNames(const void* a, const void* b) {
358 const struct CInemaTest* ta = a;
359 const struct CInemaTest* tb = b;
360
361 return strncmp(ta->name, tb->name, sizeof(ta->name));
362}
363
364static void reduceTestList(struct CInemaTestList* tests) {
365 qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
366
367 size_t i;
368 for (i = 1; i < CInemaTestListSize(tests);) {
369 struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
370 struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
371 if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
372 ++i;
373 continue;
374 }
375 CInemaTestListShift(tests, i, 1);
376 }
377}
378
379static void testToPath(const char* testName, char* path) {
380 strlcpy(path, base, PATH_MAX);
381
382 bool dotSeen = true;
383 size_t i;
384 for (i = strlen(path); testName[0] && i < PATH_MAX; ++testName) {
385 if (testName[0] == '.') {
386 dotSeen = true;
387 } else {
388 if (dotSeen) {
389 strlcpy(&path[i], PATH_SEP, PATH_MAX - i);
390 i += strlen(PATH_SEP);
391 dotSeen = false;
392 if (!i) {
393 break;
394 }
395 }
396 path[i] = testName[0];
397 ++i;
398 }
399 }
400 if (i == PATH_MAX) {
401 --i;
402 }
403 path[i] = '\0';
404}
405
406static bool globTests(struct CInemaTestList* tests, const char* glob, const char* ancestors) {
407 bool success = true;
408 const char* next = strpbrk(glob, "*.");
409
410 char path[PATH_MAX];
411 if (!next) {
412 testToPath(glob, path);
413 return collectTests(tests, path);
414 } else if (next[0] == '.') {
415 char subtest[MAX_TEST];
416 if (!ancestors) {
417 strncpy(subtest, glob, next - glob);
418 } else {
419 size_t len = strlen(ancestors) + (next - glob) + 2;
420 if (len > sizeof(subtest)) {
421 len = sizeof(subtest);
422 }
423 snprintf(subtest, len, "%s.%s", ancestors, glob);
424 }
425 return globTests(tests, next + 1, subtest);
426 } else if (next[0] == '*') {
427 char globBuffer[MAX_TEST];
428 const char* subglob;
429
430 next = strchr(next, '.');
431 if (!next) {
432 subglob = glob;
433 } else {
434 size_t len = next - glob + 1;
435 if (len > sizeof(globBuffer)) {
436 len = sizeof(globBuffer);
437 }
438 strncpy(globBuffer, glob, len - 1);
439 subglob = globBuffer;
440 }
441 bool hasMoreGlobs = next && strchr(next, '*');
442
443 struct VDir* dir;
444 if (ancestors) {
445 testToPath(ancestors, path);
446 dir = VDirOpen(path);
447 } else {
448 dir = VDirOpen(base);
449 }
450 if (!dir) {
451 return false;
452 }
453
454 struct VDirEntry* dirent = dir->listNext(dir);
455 while (dirent) {
456 const char* name = dirent->name(dirent);
457 if (dirent->type(dirent) != VFS_DIRECTORY || strncmp(name, ".", 2) == 0 || strncmp(name, "..", 3) == 0) {
458 dirent = dir->listNext(dir);
459 continue;
460 }
461 if (wildcard(subglob, name)) {
462 char newgen[MAX_TEST];
463 if (ancestors) {
464 snprintf(newgen, sizeof(newgen), "%s.%s", ancestors, name);
465 } else {
466 strlcpy(newgen, name, sizeof(newgen));
467 }
468 if (next && hasMoreGlobs) {
469 globTests(tests, next + 1, newgen);
470 } else {
471 testToPath(newgen, path);
472 collectTests(tests, path);
473 }
474 }
475 dirent = dir->listNext(dir);
476 }
477
478 return true;
479 } else {
480 abort();
481 }
482}
483
484static void _loadConfigTree(struct Table* configTree, const char* testName) {
485 char key[MAX_TEST];
486 strlcpy(key, testName, sizeof(key));
487
488 struct mCoreConfig* config;
489 while (!(config = HashTableLookup(configTree, key))) {
490 char path[PATH_MAX];
491 config = malloc(sizeof(*config));
492 mCoreConfigInit(config, "cinema");
493 testToPath(key, path);
494 strncat(path, PATH_SEP, sizeof(path) - 1);
495 strncat(path, "config.ini", sizeof(path) - 1);
496 mCoreConfigLoadPath(config, path);
497 HashTableInsert(configTree, key, config);
498 char* pos = strrchr(key, '.');
499 if (pos) {
500 pos[0] = '\0';
501 } else if (key[0]) {
502 key[0] = '\0';
503 } else {
504 break;
505 }
506 }
507}
508
509static void _unloadConfigTree(const char* key, void* value, void* user) {
510 UNUSED(key);
511 UNUSED(user);
512 mCoreConfigDeinit(value);
513}
514
515static const char* CInemaConfigGet(struct Table* configTree, const char* testName, const char* key) {
516 _loadConfigTree(configTree, testName);
517
518 char testKey[MAX_TEST];
519 strlcpy(testKey, testName, sizeof(testKey));
520
521 struct mCoreConfig* config;
522 while (true) {
523 config = HashTableLookup(configTree, testKey);
524 if (!config) {
525 continue;
526 }
527 const char* str = ConfigurationGetValue(&config->configTable, "testinfo", key);
528 if (str) {
529 return str;
530 }
531 char* pos = strrchr(testKey, '.');
532 if (pos) {
533 pos[0] = '\0';
534 } else if (testKey[0]) {
535 testKey[0] = '\0';
536 } else {
537 break;
538 }
539 }
540 return NULL;
541}
542
543bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) {
544 const char* charValue = CInemaConfigGet(configTree, testName, key);
545 if (!charValue) {
546 return false;
547 }
548 char* end;
549 unsigned long value = strtoul(charValue, &end, 10);
550 if (*end) {
551 return false;
552 }
553 *out = value;
554 return true;
555}
556
557void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core) {
558 _loadConfigTree(configTree, testName);
559
560 char testKey[MAX_TEST] = {0};
561 char* keyEnd = testKey;
562 const char* pos;
563 while (true) {
564 pos = strchr(testName, '.');
565 size_t maxlen = sizeof(testKey) - (keyEnd - testKey) - 1;
566 size_t len;
567 if (pos) {
568 len = pos - testName;
569 } else {
570 len = strlen(testName);
571 }
572 if (len > maxlen) {
573 len = maxlen;
574 }
575 strncpy(keyEnd, testName, len);
576 keyEnd += len;
577
578 struct mCoreConfig* config = HashTableLookup(configTree, testKey);
579 if (config) {
580 core->loadConfig(core, config);
581 mCoreConfigMap(config, &core->opts);
582 }
583 if (!pos) {
584 break;
585 }
586 testName = pos + 1;
587 keyEnd[0] = '.';
588 ++keyEnd;
589 }
590}
591
592bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
593 if (strncmp(base, directory, strlen(base)) != 0) {
594 return false;
595 }
596 memset(test, 0, sizeof(*test));
597 strlcpy(test->directory, directory, sizeof(test->directory));
598 strlcpy(test->filename, filename, sizeof(test->filename));
599 directory += strlen(base) + 1;
600 strlcpy(test->name, directory, sizeof(test->name));
601 char* str = strstr(test->name, PATH_SEP);
602 while (str) {
603 str[0] = '.';
604 str = strstr(str, PATH_SEP);
605 }
606 return true;
607}
608
609static bool _loadBaselinePNG(struct VDir* dir, const char* type, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) {
610 char baselineName[32];
611 snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame);
612 struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
613 if (!baselineVF) {
614 if (*status == CI_PASS) {
615 *status = CI_FAIL;
616 }
617 return false;
618 }
619
620 png_structp png = PNGReadOpen(baselineVF, 0);
621 png_infop info = png_create_info_struct(png);
622 png_infop end = png_create_info_struct(png);
623 if (!png || !info || !end || !PNGReadHeader(png, info)) {
624 PNGReadClose(png, info, end);
625 baselineVF->close(baselineVF);
626 CIerr(1, "Failed to load %s\n", baselineName);
627 *status = CI_ERROR;
628 return false;
629 }
630
631 unsigned pwidth = png_get_image_width(png, info);
632 unsigned pheight = png_get_image_height(png, info);
633 if (pheight != image->height || pwidth != image->width) {
634 PNGReadClose(png, info, end);
635 baselineVF->close(baselineVF);
636 CIlog(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, image->width, image->height);
637 if (*status == CI_PASS) {
638 *status = CI_FAIL;
639 }
640 return false;
641 }
642
643 image->data = malloc(pwidth * pheight * BYTES_PER_PIXEL);
644 if (!image->data) {
645 CIerr(1, "Failed to allocate baseline buffer\n");
646 *status = CI_ERROR;
647 PNGReadClose(png, info, end);
648 baselineVF->close(baselineVF);
649 return false;
650 }
651 if (!PNGReadPixels(png, info, image->data, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
652 CIerr(1, "Failed to read %s\n", baselineName);
653 *status = CI_ERROR;
654 free(image->data);
655 return false;
656 }
657 PNGReadClose(png, info, end);
658 baselineVF->close(baselineVF);
659 image->stride = pwidth;
660 return true;
661}
662
663#ifdef USE_FFMPEG
664struct CInemaStream {
665 struct mAVStream d;
666 struct CInemaImage* image;
667 enum CInemaStatus* status;
668};
669
670static void _cinemaDimensionsChanged(struct mAVStream* stream, unsigned width, unsigned height) {
671 struct CInemaStream* cistream = (struct CInemaStream*) stream;
672 if (height != cistream->image->height || width != cistream->image->width) {
673 CIlog(1, "Size mismatch for video, expected %ux%u, got %ux%u\n", width, height, cistream->image->width, cistream->image->height);
674 if (*cistream->status == CI_PASS) {
675 *cistream->status = CI_FAIL;
676 }
677 }
678}
679
680static void _cinemaVideoFrame(struct mAVStream* stream, const color_t* pixels, size_t stride) {
681 struct CInemaStream* cistream = (struct CInemaStream*) stream;
682 cistream->image->stride = stride;
683 size_t bufferSize = cistream->image->stride * cistream->image->height * BYTES_PER_PIXEL;
684 cistream->image->data = malloc(bufferSize);
685 memcpy(cistream->image->data, pixels, bufferSize);
686}
687#endif
688
689static struct VDir* _makeOutDir(const char* testName) {
690 char path[PATH_MAX] = {0};
691 strlcpy(path, outdir, sizeof(path));
692 char* pathEnd = path + strlen(path);
693 const char* pos;
694 while (true) {
695 pathEnd[0] = PATH_SEP[0];
696 ++pathEnd;
697 pos = strchr(testName, '.');
698 size_t maxlen = sizeof(path) - (pathEnd - path) - 1;
699 size_t len;
700 if (pos) {
701 len = pos - testName;
702 } else {
703 len = strlen(testName);
704 }
705 if (len > maxlen) {
706 len = maxlen;
707 }
708 strncpy(pathEnd, testName, len);
709 pathEnd += len;
710
711 mkdir(path, 0777);
712
713 if (!pos) {
714 break;
715 }
716 testName = pos + 1;
717 }
718 return VDirOpen(path);
719}
720
721static void _writeImage(struct VFile* vf, const struct CInemaImage* image) {
722 png_structp png = PNGWriteOpen(vf);
723 png_infop info = PNGWriteHeader(png, image->width, image->height);
724 if (!PNGWritePixels(png, image->width, image->height, image->stride, image->data)) {
725 CIerr(0, "Could not write output image\n");
726 }
727 PNGWriteClose(png, info);
728
729 vf->close(vf);
730}
731
732static void _writeDiff(const char* testName, const struct CInemaImage* image, size_t frame, const char* type) {
733 struct VDir* dir = _makeOutDir(testName);
734 if (!dir) {
735 CIerr(0, "Could not open directory for %s\n", testName);
736 return;
737 }
738 char name[32];
739 snprintf(name, sizeof(name), "%s_%04" PRIz "u.png", type, frame);
740 struct VFile* vf = dir->openFile(dir, name, O_CREAT | O_TRUNC | O_WRONLY);
741 if (!vf) {
742 CIerr(0, "Could not open output file %s\n", name);
743 dir->close(dir);
744 return;
745 }
746 _writeImage(vf, image);
747 dir->close(dir);
748}
749
750static void _writeBaseline(struct VDir* dir, const char* type, const struct CInemaImage* image, size_t frame) {
751 char baselineName[32];
752 snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame);
753 struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY);
754 if (baselineVF) {
755 _writeImage(baselineVF, image);
756 } else {
757 CIerr(0, "Could not open output file %s\n", baselineName);
758 }
759}
760
761static bool _updateInput(struct mCore* core, size_t frame, const char** input) {
762 if (!*input || !*input[0]) {
763 return false;
764 }
765 char* end;
766 uint32_t start = strtoul(*input, &end, 10);
767 if (end[0] != ':') {
768 return false;
769 }
770 if (start != frame) {
771 return true;
772 }
773 ++end;
774 *input = end;
775 uint32_t keys = strtoul(*input, &end, 16);
776 if (end[0] == ',') {
777 ++end;
778 }
779 *input = end;
780 core->setKeys(core, keys);
781 return true;
782}
783
784static bool _compareImages(struct CInemaTest* restrict test, const struct CInemaImage* restrict image, const struct CInemaImage* restrict expected, int* restrict max, uint8_t** restrict outdiff) {
785 const uint8_t* testPixels = image->data;
786 const uint8_t* expectPixels = expected->data;
787 uint8_t* diff = NULL;
788 size_t x;
789 size_t y;
790 bool failed = false;
791 for (y = 0; y < image->height; ++y) {
792 for (x = 0; x < image->width; ++x) {
793 size_t pix = expected->stride * y + x;
794 size_t tpix = image->stride * y + x;
795 int testR = testPixels[tpix * 4 + 0];
796 int testG = testPixels[tpix * 4 + 1];
797 int testB = testPixels[tpix * 4 + 2];
798 int expectR = expectPixels[pix * 4 + 0];
799 int expectG = expectPixels[pix * 4 + 1];
800 int expectB = expectPixels[pix * 4 + 2];
801 int r = expectR - testR;
802 int g = expectG - testG;
803 int b = expectB - testB;
804 if (r | g | b) {
805 failed = true;
806 if (outdiff && !diff) {
807 diff = calloc(expected->stride * expected->height, BYTES_PER_PIXEL);
808 *outdiff = diff;
809 }
810 test->status = CI_FAIL;
811 if (r < 0) {
812 r = -r;
813 }
814 if (g < 0) {
815 g = -g;
816 }
817 if (b < 0) {
818 b = -b;
819 }
820
821 if (diff) {
822 if (r > *max) {
823 *max = r;
824 }
825 if (g > *max) {
826 *max = g;
827 }
828 if (b > *max) {
829 *max = b;
830 }
831 diff[pix * 4 + 0] = r;
832 diff[pix * 4 + 1] = g;
833 diff[pix * 4 + 2] = b;
834 }
835
836 if (test) {
837 test->totalDistance += r + g + b;
838 ++test->failedPixels;
839 }
840 }
841 }
842 }
843 return !failed;
844}
845
846void _writeDiffSet(struct CInemaImage* expected, const char* name, uint8_t* diff, int frame, int max, bool xfail) {
847 struct CInemaImage outdiff = {
848 .data = diff,
849 .width = expected->width,
850 .height = expected->height,
851 .stride = expected->stride,
852 };
853
854 if (xfail) {
855 _writeDiff(name, expected, frame, "xexpected");
856 _writeDiff(name, &outdiff, frame, "xdiff");
857 } else {
858 _writeDiff(name, expected, frame, "expected");
859 _writeDiff(name, &outdiff, frame, "diff");
860 }
861
862 size_t x;
863 size_t y;
864 for (y = 0; y < outdiff.height; ++y) {
865 for (x = 0; x < outdiff.width; ++x) {
866 size_t pix = outdiff.stride * y + x;
867 diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
868 diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
869 diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
870 }
871 }
872 if (xfail) {
873 _writeDiff(name, &outdiff, frame, "xnormalized");
874 }
875}
876
877#ifdef USE_FFMPEG
878static void _replayBaseline(struct CInemaTest* test, struct FFmpegEncoder* encoder, const struct CInemaImage* image, int frame) {
879 char baselineName[PATH_MAX];
880 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
881
882 if (!FFmpegEncoderOpen(encoder, baselineName)) {
883 CIerr(1, "Failed to save baseline video\n");
884 test->status = CI_ERROR;
885 return;
886 }
887
888 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
889
890
891 struct CInemaImage buffer = {
892 .data = NULL,
893 .width = image->width,
894 .height = image->height,
895 .stride = image->width,
896 };
897 struct FFmpegDecoder decoder;
898 struct CInemaStream stream = {0};
899 stream.d.postVideoFrame = _cinemaVideoFrame;
900 stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
901 stream.status = &test->status;
902 stream.image = &buffer;
903
904 FFmpegDecoderInit(&decoder);
905 decoder.out = &stream.d;
906
907 if (!FFmpegDecoderOpen(&decoder, baselineName)) {
908 CIerr(1, "Failed to load baseline video\n");
909 test->status = CI_ERROR;
910 return;
911 }
912
913 int i;
914 for (i = 0; i < frame; ++i) {
915 while (!buffer.data) {
916 if (!FFmpegDecoderRead(&decoder)) {
917 CIlog(1, "Failed to read more frames. EOF?\n");
918 test->status = CI_FAIL;
919 break;
920 }
921 }
922 encoder->d.postVideoFrame(&encoder->d, buffer.data, buffer.stride);
923 free(buffer.data);
924 buffer.data = NULL;
925 }
926 FFmpegDecoderClose(&decoder);
927}
928#endif
929
930void CInemaTestRun(struct CInemaTest* test) {
931 unsigned ignore = 0;
932 MutexLock(&configMutex);
933 CInemaConfigGetUInt(&configTree, test->name, "ignore", &ignore);
934 MutexUnlock(&configMutex);
935 if (ignore) {
936 test->status = CI_SKIP;
937 return;
938 }
939
940 struct VDir* dir = VDirOpen(test->directory);
941 if (!dir) {
942 CIerr(0, "Failed to open test directory\n");
943 test->status = CI_ERROR;
944 return;
945 }
946 struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
947 if (!rom) {
948 CIerr(0, "Failed to open test\n");
949 test->status = CI_ERROR;
950 return;
951 }
952 struct mCore* core = mCoreFindVF(rom);
953 if (!core) {
954 CIerr(0, "Failed to load test\n");
955 test->status = CI_ERROR;
956 rom->close(rom);
957 return;
958 }
959 if (!core->init(core)) {
960 CIerr(0, "Failed to init test\n");
961 test->status = CI_ERROR;
962 core->deinit(core);
963 return;
964 }
965 struct CInemaImage image;
966 core->desiredVideoDimensions(core, &image.width, &image.height);
967 ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL;
968 image.data = malloc(bufferSize);
969 image.stride = image.width;
970 if (!image.data) {
971 CIerr(0, "Failed to allocate video buffer\n");
972 test->status = CI_ERROR;
973 core->deinit(core);
974 }
975 core->setVideoBuffer(core, image.data, image.stride);
976 mCoreConfigInit(&core->config, "cinema");
977
978 unsigned limit = 9999;
979 unsigned skip = 0;
980 unsigned fail = 0;
981 unsigned video = 0;
982 const char* input = NULL;
983
984 MutexLock(&configMutex);
985 CInemaConfigGetUInt(&configTree, test->name, "frames", &limit);
986 CInemaConfigGetUInt(&configTree, test->name, "skip", &skip);
987 CInemaConfigGetUInt(&configTree, test->name, "fail", &fail);
988 CInemaConfigGetUInt(&configTree, test->name, "video", &video);
989 input = CInemaConfigGet(&configTree, test->name, "input");
990 CInemaConfigLoad(&configTree, test->name, core);
991 MutexUnlock(&configMutex);
992
993 struct VFile* save = VFileMemChunk(NULL, 0);
994 core->loadROM(core, rom);
995 if (!core->loadSave(core, save)) {
996 save->close(save);
997 }
998 core->rtc.override = RTC_FAKE_EPOCH;
999 core->rtc.value = 1200000000;
1000 core->reset(core);
1001
1002 test->status = CI_PASS;
1003
1004 unsigned minFrame = core->frameCounter(core);
1005 size_t frame;
1006 for (frame = 0; frame < skip; ++frame) {
1007 core->runFrame(core);
1008 }
1009 core->desiredVideoDimensions(core, &image.width, &image.height);
1010
1011#ifdef USE_FFMPEG
1012 struct FFmpegDecoder decoder;
1013 struct FFmpegEncoder encoder;
1014 struct CInemaStream stream = {0};
1015
1016 char baselineName[PATH_MAX];
1017 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
1018 bool exists = access(baselineName, 0) == 0;
1019
1020 if (video) {
1021 FFmpegEncoderInit(&encoder);
1022 FFmpegDecoderInit(&decoder);
1023
1024 FFmpegEncoderSetAudio(&encoder, NULL, 0);
1025 FFmpegEncoderSetVideo(&encoder, "zmbv", 0, 0);
1026 FFmpegEncoderSetContainer(&encoder, "avi");
1027 FFmpegEncoderSetDimensions(&encoder, image.width, image.height);
1028
1029 if (rebaseline && !exists) {
1030 if (!FFmpegEncoderOpen(&encoder, baselineName)) {
1031 CIerr(1, "Failed to save baseline video\n");
1032 } else {
1033 core->setAVStream(core, &encoder.d);
1034 }
1035 }
1036
1037 if (exists) {
1038 stream.d.postVideoFrame = _cinemaVideoFrame;
1039 stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
1040 stream.status = &test->status;
1041 decoder.out = &stream.d;
1042
1043 if (!FFmpegDecoderOpen(&decoder, baselineName)) {
1044 CIerr(1, "Failed to load baseline video\n");
1045 }
1046 } else if (!rebaseline) {
1047 test->status = CI_FAIL;
1048 }
1049 }
1050#else
1051 if (video) {
1052 CIerr(0, "Failed to run video test without ffmpeg linked in\n");
1053 test->status = CI_ERROR;
1054 }
1055#endif
1056
1057 bool xdiff = false;
1058 for (frame = 0; limit; ++frame, --limit) {
1059 _updateInput(core, frame, &input);
1060 core->runFrame(core);
1061 ++test->totalFrames;
1062 unsigned frameCounter = core->frameCounter(core);
1063 if (frameCounter <= minFrame) {
1064 break;
1065 }
1066 if (test->status == CI_ERROR) {
1067 break;
1068 }
1069 CIlog(3, "Test frame: %u\n", frameCounter);
1070 core->desiredVideoDimensions(core, &image.width, &image.height);
1071 uint8_t* diff = NULL;
1072 struct CInemaImage expected = {
1073 .data = NULL,
1074 .width = image.width,
1075 .height = image.height,
1076 .stride = image.width,
1077 };
1078 bool baselineFound;
1079 if (video) {
1080 baselineFound = false;
1081#ifdef USE_FFMPEG
1082 if (FFmpegDecoderIsOpen(&decoder)) {
1083 stream.image = &expected;
1084 while (!expected.data) {
1085 if (!FFmpegDecoderRead(&decoder)) {
1086 CIlog(1, "Failed to read more frames. EOF?\n");
1087 test->status = CI_FAIL;
1088 break;
1089 }
1090 }
1091 baselineFound = expected.data;
1092 }
1093#endif
1094 } else {
1095 baselineFound = _loadBaselinePNG(dir, "baseline", &expected, frame, &test->status);
1096 }
1097 if (test->status == CI_ERROR) {
1098 break;
1099 }
1100 bool failed = false;
1101 if (baselineFound) {
1102 int max = 0;
1103 failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1104 if (failed) {
1105 ++test->failedFrames;
1106#ifdef USE_FFMPEG
1107 if (video && exists && rebaseline && !FFmpegEncoderIsOpen(&encoder)) {
1108 _replayBaseline(test, &encoder, &image, frame);
1109 if (test->status == CI_ERROR) {
1110 break;
1111 }
1112 encoder.d.postVideoFrame(&encoder.d, image.data, image.stride);
1113 core->setAVStream(core, &encoder.d);
1114 }
1115#endif
1116 }
1117 test->totalPixels += image.height * image.width;
1118 if (rebaseline == CI_R_FAILING && !video && failed) {
1119 _writeBaseline(dir, "baseline", &image, frame);
1120 }
1121 if (diff) {
1122 if (failed) {
1123 _writeDiff(test->name, &image, frame, "result");
1124 _writeDiffSet(&expected, test->name, diff, frame, max, false);
1125 }
1126 free(diff);
1127 diff = NULL;
1128 }
1129 free(expected.data);
1130 } else if (rebaseline && !video) {
1131 _writeBaseline(dir, "baseline", &image, frame);
1132 } else if (!rebaseline) {
1133 test->status = CI_FAIL;
1134 }
1135
1136 if (fail && failed) {
1137 if (video) {
1138 // TODO
1139 baselineFound = false;
1140 } else {
1141 baselineFound = _loadBaselinePNG(dir, "xbaseline", &expected, frame, &test->status);
1142 }
1143
1144 if (baselineFound) {
1145 int max = 0;
1146 failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1147 if (diff) {
1148 if (failed) {
1149 _writeDiffSet(&expected, test->name, diff, frame, max, true);
1150 }
1151 free(diff);
1152 diff = NULL;
1153 }
1154 if (failed) {
1155 if (xbaseline == CI_R_FAILING && !video) {
1156 _writeBaseline(dir, "xbaseline", &image, frame);
1157 }
1158 xdiff = true;
1159 }
1160 free(expected.data);
1161 } else if (xbaseline && !video) {
1162 _writeBaseline(dir, "xbaseline", &image, frame);
1163 }
1164 }
1165 }
1166
1167#ifdef USE_FFMPEG
1168 if (video) {
1169 if (FFmpegEncoderIsOpen(&encoder)) {
1170 FFmpegEncoderClose(&encoder);
1171 if (exists && rebaseline) {
1172 char tmpBaselineName[PATH_MAX];
1173 snprintf(tmpBaselineName, sizeof(tmpBaselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
1174#ifdef _WIN32
1175 MoveFileEx(tmpBaselineName, baselineName, MOVEFILE_REPLACE_EXISTING);
1176#else
1177 rename(tmpBaselineName, baselineName);
1178#endif
1179 }
1180 }
1181 if (FFmpegDecoderIsOpen(&decoder)) {
1182 FFmpegDecoderClose(&decoder);
1183 }
1184 }
1185#endif
1186
1187 if (fail) {
1188 if (test->status == CI_FAIL && !xdiff) {
1189 test->status = CI_XFAIL;
1190 } else if (test->status == CI_PASS) {
1191 test->status = CI_XPASS;
1192 }
1193 }
1194
1195 free(image.data);
1196 mCoreConfigDeinit(&core->config);
1197 core->deinit(core);
1198 dir->close(dir);
1199}
1200
1201static bool CInemaTask(struct CInemaTestList* tests, size_t i) {
1202 bool success = true;
1203 struct CInemaTest* test = CInemaTestListGetPointer(tests, i);
1204 if (dryRun) {
1205 CIlog(-1, "%s\n", test->name);
1206 } else {
1207 CIlog(1, "%s: ", test->name);
1208 fflush(stdout);
1209 ThreadLocalSetKey(currentTest, test);
1210 CInemaTestRun(test);
1211 ThreadLocalSetKey(currentTest, NULL);
1212
1213 switch (test->status) {
1214 case CI_PASS:
1215 CIlog(1, "pass\n");
1216 break;
1217 case CI_FAIL:
1218 success = false;
1219 CIlog(1, "fail\n");
1220 break;
1221 case CI_XPASS:
1222 CIlog(1, "xpass\n");
1223 break;
1224 case CI_XFAIL:
1225 CIlog(1, "xfail\n");
1226 break;
1227 case CI_SKIP:
1228 CIlog(1, "skip\n");
1229 break;
1230 case CI_ERROR:
1231 success = false;
1232 CIlog(1, "error\n");
1233 break;
1234 }
1235 if (test->failedFrames) {
1236 CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
1237 CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
1238 CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
1239 }
1240 }
1241 return success;
1242}
1243
1244static THREAD_ENTRY CInemaJob(void* context) {
1245 struct CInemaTestList* tests = context;
1246 struct CInemaLogStream stream;
1247 StringListInit(&stream.out.lines, 0);
1248 StringListInit(&stream.out.partial, 0);
1249 stream.out.repeat = 0;
1250 StringListInit(&stream.err.lines, 0);
1251 StringListInit(&stream.err.partial, 0);
1252 stream.err.repeat = 0;
1253 ThreadLocalSetKey(logStream, &stream);
1254
1255 bool success = true;
1256 while (true) {
1257 size_t i;
1258 MutexLock(&jobMutex);
1259 i = jobIndex;
1260 ++jobIndex;
1261 MutexUnlock(&jobMutex);
1262 if (i >= CInemaTestListSize(tests)) {
1263 break;
1264 }
1265 if (!CInemaTask(tests, i)) {
1266 success = false;
1267 }
1268 CIflush(&stream.out, stdout);
1269 CIflush(&stream.err, stderr);
1270 }
1271 MutexLock(&jobMutex);
1272 if (!success) {
1273 jobStatus = 1;
1274 }
1275 MutexUnlock(&jobMutex);
1276
1277 CIflush(&stream.out, stdout);
1278 StringListDeinit(&stream.out.lines);
1279 StringListDeinit(&stream.out.partial);
1280
1281 CIflush(&stream.err, stderr);
1282 StringListDeinit(&stream.err.lines);
1283 StringListDeinit(&stream.err.partial);
1284}
1285
1286void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
1287 UNUSED(log);
1288 if (level == mLOG_FATAL) {
1289 struct CInemaTest* test = ThreadLocalGetValue(currentTest);
1290 test->status = CI_ERROR;
1291 }
1292 if (verbosity < 0) {
1293 return;
1294 }
1295 int mask = mLOG_FATAL;
1296 if (verbosity >= 1) {
1297 mask |= mLOG_ERROR;
1298 }
1299 if (verbosity >= 2) {
1300 mask |= mLOG_WARN;
1301 }
1302 if (verbosity >= 4) {
1303 mask |= mLOG_INFO;
1304 }
1305 if (verbosity >= 5) {
1306 mask |= mLOG_ALL;
1307 }
1308 if (!(mask & level)) {
1309 return;
1310 }
1311
1312 char buffer[256];
1313 vsnprintf(buffer, sizeof(buffer), format, args);
1314 CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);
1315}
1316
1317int main(int argc, char** argv) {
1318 ThreadLocalInitKey(&logStream);
1319 ThreadLocalSetKey(logStream, NULL);
1320
1321 int status = 0;
1322 if (!parseCInemaArgs(argc, argv)) {
1323 status = 1;
1324 goto cleanup;
1325 }
1326
1327 if (showVersion) {
1328 version(argv[0]);
1329 goto cleanup;
1330 }
1331
1332 if (showUsage) {
1333 usageCInema(argv[0]);
1334 goto cleanup;
1335 }
1336
1337 argc -= optind;
1338 argv += optind;
1339
1340 if (!base[0] && !determineBase(argc, argv)) {
1341 CIlog(0, "Could not determine CInema test base. Please specify manually.");
1342 status = 1;
1343 goto cleanup;
1344 }
1345#ifndef _WIN32
1346 char* rbase = realpath(base, NULL);
1347 if (rbase) {
1348 strlcpy(base, rbase, sizeof(base));
1349 free(rbase);
1350 }
1351#endif
1352
1353 struct CInemaTestList tests;
1354 CInemaTestListInit(&tests, 0);
1355
1356 struct mLogger logger = { .log = _log };
1357 mLogSetDefaultLogger(&logger);
1358#ifdef USE_FFMPEG
1359 if (verbosity < 2) {
1360 av_log_set_level(AV_LOG_ERROR);
1361 }
1362#endif
1363
1364 if (argc > 0) {
1365 size_t i;
1366 for (i = 0; i < (size_t) argc; ++i) {
1367 if (strchr(argv[i], '*')) {
1368 if (!globTests(&tests, argv[i], NULL)) {
1369 status = 1;
1370 break;
1371 }
1372 continue;
1373 }
1374 char path[PATH_MAX + 1] = {0};
1375 testToPath(argv[i], path);
1376
1377 if (!collectTests(&tests, path)) {
1378 status = 1;
1379 break;
1380 }
1381 }
1382 } else if (!collectTests(&tests, base)) {
1383 status = 1;
1384 }
1385
1386 if (CInemaTestListSize(&tests) == 0) {
1387 CIlog(1, "No tests found.");
1388 status = 1;
1389 } else {
1390 reduceTestList(&tests);
1391 }
1392
1393 HashTableInit(&configTree, 0, free);
1394 MutexInit(&configMutex);
1395 ThreadLocalInitKey(¤tTest);
1396 ThreadLocalSetKey(currentTest, NULL);
1397
1398 if (jobs == 1) {
1399 size_t i;
1400 for (i = 0; i < CInemaTestListSize(&tests); ++i) {
1401 bool success = CInemaTask(&tests, i);
1402 if (!success) {
1403 status = 1;
1404 }
1405 }
1406 } else {
1407 MutexInit(&jobMutex);
1408 int i;
1409 for (i = 0; i < jobs; ++i) {
1410 ThreadCreate(&jobThreads[i], CInemaJob, &tests);
1411 }
1412 for (i = 0; i < jobs; ++i) {
1413 ThreadJoin(&jobThreads[i]);
1414 }
1415 MutexDeinit(&jobMutex);
1416 status = jobStatus;
1417 }
1418
1419 MutexDeinit(&configMutex);
1420 HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
1421 HashTableDeinit(&configTree);
1422 CInemaTestListDeinit(&tests);
1423
1424cleanup:
1425 return status;
1426}