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[PATH_MAX];
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 }
582 if (!pos) {
583 break;
584 }
585 testName = pos + 1;
586 keyEnd[0] = '.';
587 ++keyEnd;
588 }
589}
590
591bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
592 if (strncmp(base, directory, strlen(base)) != 0) {
593 return false;
594 }
595 memset(test, 0, sizeof(*test));
596 strlcpy(test->directory, directory, sizeof(test->directory));
597 strlcpy(test->filename, filename, sizeof(test->filename));
598 directory += strlen(base) + 1;
599 strlcpy(test->name, directory, sizeof(test->name));
600 char* str = strstr(test->name, PATH_SEP);
601 while (str) {
602 str[0] = '.';
603 str = strstr(str, PATH_SEP);
604 }
605 return true;
606}
607
608static bool _loadBaselinePNG(struct VDir* dir, const char* type, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) {
609 char baselineName[32];
610 snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame);
611 struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
612 if (!baselineVF) {
613 if (*status == CI_PASS) {
614 *status = CI_FAIL;
615 }
616 return false;
617 }
618
619 png_structp png = PNGReadOpen(baselineVF, 0);
620 png_infop info = png_create_info_struct(png);
621 png_infop end = png_create_info_struct(png);
622 if (!png || !info || !end || !PNGReadHeader(png, info)) {
623 PNGReadClose(png, info, end);
624 baselineVF->close(baselineVF);
625 CIerr(1, "Failed to load %s\n", baselineName);
626 *status = CI_ERROR;
627 return false;
628 }
629
630 unsigned pwidth = png_get_image_width(png, info);
631 unsigned pheight = png_get_image_height(png, info);
632 if (pheight != image->height || pwidth != image->width) {
633 PNGReadClose(png, info, end);
634 baselineVF->close(baselineVF);
635 CIlog(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, image->width, image->height);
636 if (*status == CI_PASS) {
637 *status = CI_FAIL;
638 }
639 return false;
640 }
641
642 image->data = malloc(pwidth * pheight * BYTES_PER_PIXEL);
643 if (!image->data) {
644 CIerr(1, "Failed to allocate baseline buffer\n");
645 *status = CI_ERROR;
646 PNGReadClose(png, info, end);
647 baselineVF->close(baselineVF);
648 return false;
649 }
650 if (!PNGReadPixels(png, info, image->data, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
651 CIerr(1, "Failed to read %s\n", baselineName);
652 *status = CI_ERROR;
653 free(image->data);
654 return false;
655 }
656 PNGReadClose(png, info, end);
657 baselineVF->close(baselineVF);
658 image->stride = pwidth;
659 return true;
660}
661
662#ifdef USE_FFMPEG
663struct CInemaStream {
664 struct mAVStream d;
665 struct CInemaImage* image;
666 enum CInemaStatus* status;
667};
668
669static void _cinemaDimensionsChanged(struct mAVStream* stream, unsigned width, unsigned height) {
670 struct CInemaStream* cistream = (struct CInemaStream*) stream;
671 if (height != cistream->image->height || width != cistream->image->width) {
672 CIlog(1, "Size mismatch for video, expected %ux%u, got %ux%u\n", width, height, cistream->image->width, cistream->image->height);
673 if (*cistream->status == CI_PASS) {
674 *cistream->status = CI_FAIL;
675 }
676 }
677}
678
679static void _cinemaVideoFrame(struct mAVStream* stream, const color_t* pixels, size_t stride) {
680 struct CInemaStream* cistream = (struct CInemaStream*) stream;
681 cistream->image->stride = stride;
682 size_t bufferSize = cistream->image->stride * cistream->image->height * BYTES_PER_PIXEL;
683 cistream->image->data = malloc(bufferSize);
684 memcpy(cistream->image->data, pixels, bufferSize);
685}
686#endif
687
688static struct VDir* _makeOutDir(const char* testName) {
689 char path[PATH_MAX] = {0};
690 strlcpy(path, outdir, sizeof(path));
691 char* pathEnd = path + strlen(path);
692 const char* pos;
693 while (true) {
694 pathEnd[0] = PATH_SEP[0];
695 ++pathEnd;
696 pos = strchr(testName, '.');
697 size_t maxlen = sizeof(path) - (pathEnd - path) - 1;
698 size_t len;
699 if (pos) {
700 len = pos - testName;
701 } else {
702 len = strlen(testName);
703 }
704 if (len > maxlen) {
705 len = maxlen;
706 }
707 strncpy(pathEnd, testName, len);
708 pathEnd += len;
709
710 mkdir(path, 0777);
711
712 if (!pos) {
713 break;
714 }
715 testName = pos + 1;
716 }
717 return VDirOpen(path);
718}
719
720static void _writeImage(struct VFile* vf, const struct CInemaImage* image) {
721 png_structp png = PNGWriteOpen(vf);
722 png_infop info = PNGWriteHeader(png, image->width, image->height);
723 if (!PNGWritePixels(png, image->width, image->height, image->stride, image->data)) {
724 CIerr(0, "Could not write output image\n");
725 }
726 PNGWriteClose(png, info);
727
728 vf->close(vf);
729}
730
731static void _writeDiff(const char* testName, const struct CInemaImage* image, size_t frame, const char* type) {
732 struct VDir* dir = _makeOutDir(testName);
733 if (!dir) {
734 CIerr(0, "Could not open directory for %s\n", testName);
735 return;
736 }
737 char name[32];
738 snprintf(name, sizeof(name), "%s_%04" PRIz "u.png", type, frame);
739 struct VFile* vf = dir->openFile(dir, name, O_CREAT | O_TRUNC | O_WRONLY);
740 if (!vf) {
741 CIerr(0, "Could not open output file %s\n", name);
742 dir->close(dir);
743 return;
744 }
745 _writeImage(vf, image);
746 dir->close(dir);
747}
748
749static void _writeBaseline(struct VDir* dir, const char* type, const struct CInemaImage* image, size_t frame) {
750 char baselineName[32];
751 snprintf(baselineName, sizeof(baselineName), "%s_%04" PRIz "u.png", type, frame);
752 struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY);
753 if (baselineVF) {
754 _writeImage(baselineVF, image);
755 } else {
756 CIerr(0, "Could not open output file %s\n", baselineName);
757 }
758}
759
760static bool _updateInput(struct mCore* core, size_t frame, const char** input) {
761 if (!*input || !*input[0]) {
762 return false;
763 }
764 char* end;
765 uint32_t start = strtoul(*input, &end, 10);
766 if (end[0] != ':') {
767 return false;
768 }
769 if (start != frame) {
770 return true;
771 }
772 ++end;
773 *input = end;
774 uint32_t keys = strtoul(*input, &end, 16);
775 if (end[0] == ',') {
776 ++end;
777 }
778 *input = end;
779 core->setKeys(core, keys);
780 return true;
781}
782
783static bool _compareImages(struct CInemaTest* restrict test, const struct CInemaImage* restrict image, const struct CInemaImage* restrict expected, int* restrict max, uint8_t** restrict outdiff) {
784 const uint8_t* testPixels = image->data;
785 const uint8_t* expectPixels = expected->data;
786 uint8_t* diff = NULL;
787 size_t x;
788 size_t y;
789 bool failed = false;
790 for (y = 0; y < image->height; ++y) {
791 for (x = 0; x < image->width; ++x) {
792 size_t pix = expected->stride * y + x;
793 size_t tpix = image->stride * y + x;
794 int testR = testPixels[tpix * 4 + 0];
795 int testG = testPixels[tpix * 4 + 1];
796 int testB = testPixels[tpix * 4 + 2];
797 int expectR = expectPixels[pix * 4 + 0];
798 int expectG = expectPixels[pix * 4 + 1];
799 int expectB = expectPixels[pix * 4 + 2];
800 int r = expectR - testR;
801 int g = expectG - testG;
802 int b = expectB - testB;
803 if (r | g | b) {
804 failed = true;
805 if (outdiff && !diff) {
806 diff = calloc(expected->stride * expected->height, BYTES_PER_PIXEL);
807 *outdiff = diff;
808 }
809 test->status = CI_FAIL;
810 if (r < 0) {
811 r = -r;
812 }
813 if (g < 0) {
814 g = -g;
815 }
816 if (b < 0) {
817 b = -b;
818 }
819
820 if (diff) {
821 if (r > *max) {
822 *max = r;
823 }
824 if (g > *max) {
825 *max = g;
826 }
827 if (b > *max) {
828 *max = b;
829 }
830 diff[pix * 4 + 0] = r;
831 diff[pix * 4 + 1] = g;
832 diff[pix * 4 + 2] = b;
833 }
834
835 if (test) {
836 test->totalDistance += r + g + b;
837 ++test->failedPixels;
838 }
839 }
840 }
841 }
842 return !failed;
843}
844
845void _writeDiffSet(struct CInemaImage* expected, const char* name, uint8_t* diff, int frame, int max, bool xfail) {
846 struct CInemaImage outdiff = {
847 .data = diff,
848 .width = expected->width,
849 .height = expected->height,
850 .stride = expected->stride,
851 };
852
853 if (xfail) {
854 _writeDiff(name, expected, frame, "xexpected");
855 _writeDiff(name, &outdiff, frame, "xdiff");
856 } else {
857 _writeDiff(name, expected, frame, "expected");
858 _writeDiff(name, &outdiff, frame, "diff");
859 }
860
861 size_t x;
862 size_t y;
863 for (y = 0; y < outdiff.height; ++y) {
864 for (x = 0; x < outdiff.width; ++x) {
865 size_t pix = outdiff.stride * y + x;
866 diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
867 diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
868 diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
869 }
870 }
871 if (xfail) {
872 _writeDiff(name, &outdiff, frame, "xnormalized");
873 }
874}
875
876#ifdef USE_FFMPEG
877static void _replayBaseline(struct CInemaTest* test, struct FFmpegEncoder* encoder, const struct CInemaImage* image, int frame) {
878 char baselineName[PATH_MAX];
879 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
880
881 if (!FFmpegEncoderOpen(encoder, baselineName)) {
882 CIerr(1, "Failed to save baseline video\n");
883 test->status = CI_ERROR;
884 return;
885 }
886
887 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
888
889
890 struct CInemaImage buffer = {
891 .data = NULL,
892 .width = image->width,
893 .height = image->height,
894 .stride = image->width,
895 };
896 struct FFmpegDecoder decoder;
897 struct CInemaStream stream = {0};
898 stream.d.postVideoFrame = _cinemaVideoFrame;
899 stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
900 stream.status = &test->status;
901 stream.image = &buffer;
902
903 FFmpegDecoderInit(&decoder);
904 decoder.out = &stream.d;
905
906 if (!FFmpegDecoderOpen(&decoder, baselineName)) {
907 CIerr(1, "Failed to load baseline video\n");
908 test->status = CI_ERROR;
909 return;
910 }
911
912 int i;
913 for (i = 0; i < frame; ++i) {
914 while (!buffer.data) {
915 if (!FFmpegDecoderRead(&decoder)) {
916 CIlog(1, "Failed to read more frames. EOF?\n");
917 test->status = CI_FAIL;
918 break;
919 }
920 }
921 encoder->d.postVideoFrame(&encoder->d, buffer.data, buffer.stride);
922 free(buffer.data);
923 buffer.data = NULL;
924 }
925 FFmpegDecoderClose(&decoder);
926}
927#endif
928
929void CInemaTestRun(struct CInemaTest* test) {
930 unsigned ignore = 0;
931 MutexLock(&configMutex);
932 CInemaConfigGetUInt(&configTree, test->name, "ignore", &ignore);
933 MutexUnlock(&configMutex);
934 if (ignore) {
935 test->status = CI_SKIP;
936 return;
937 }
938
939 struct VDir* dir = VDirOpen(test->directory);
940 if (!dir) {
941 CIerr(0, "Failed to open test directory\n");
942 test->status = CI_ERROR;
943 return;
944 }
945 struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
946 if (!rom) {
947 CIerr(0, "Failed to open test\n");
948 test->status = CI_ERROR;
949 return;
950 }
951 struct mCore* core = mCoreFindVF(rom);
952 if (!core) {
953 CIerr(0, "Failed to load test\n");
954 test->status = CI_ERROR;
955 rom->close(rom);
956 return;
957 }
958 if (!core->init(core)) {
959 CIerr(0, "Failed to init test\n");
960 test->status = CI_ERROR;
961 core->deinit(core);
962 return;
963 }
964 struct CInemaImage image;
965 core->desiredVideoDimensions(core, &image.width, &image.height);
966 ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL;
967 image.data = malloc(bufferSize);
968 image.stride = image.width;
969 if (!image.data) {
970 CIerr(0, "Failed to allocate video buffer\n");
971 test->status = CI_ERROR;
972 core->deinit(core);
973 }
974 core->setVideoBuffer(core, image.data, image.stride);
975 mCoreConfigInit(&core->config, "cinema");
976
977 unsigned limit = 9999;
978 unsigned skip = 0;
979 unsigned fail = 0;
980 unsigned video = 0;
981 const char* input = NULL;
982
983 MutexLock(&configMutex);
984 CInemaConfigGetUInt(&configTree, test->name, "frames", &limit);
985 CInemaConfigGetUInt(&configTree, test->name, "skip", &skip);
986 CInemaConfigGetUInt(&configTree, test->name, "fail", &fail);
987 CInemaConfigGetUInt(&configTree, test->name, "video", &video);
988 input = CInemaConfigGet(&configTree, test->name, "input");
989 CInemaConfigLoad(&configTree, test->name, core);
990 MutexUnlock(&configMutex);
991
992 struct VFile* save = VFileMemChunk(NULL, 0);
993 core->loadROM(core, rom);
994 if (!core->loadSave(core, save)) {
995 save->close(save);
996 }
997 core->rtc.override = RTC_FAKE_EPOCH;
998 core->rtc.value = 1200000000;
999 core->reset(core);
1000
1001 test->status = CI_PASS;
1002
1003 unsigned minFrame = core->frameCounter(core);
1004 size_t frame;
1005 for (frame = 0; frame < skip; ++frame) {
1006 core->runFrame(core);
1007 }
1008 core->desiredVideoDimensions(core, &image.width, &image.height);
1009
1010#ifdef USE_FFMPEG
1011 struct FFmpegDecoder decoder;
1012 struct FFmpegEncoder encoder;
1013 struct CInemaStream stream = {0};
1014
1015 char baselineName[PATH_MAX];
1016 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
1017 bool exists = access(baselineName, 0) == 0;
1018
1019 if (video) {
1020 FFmpegEncoderInit(&encoder);
1021 FFmpegDecoderInit(&decoder);
1022
1023 FFmpegEncoderSetAudio(&encoder, NULL, 0);
1024 FFmpegEncoderSetVideo(&encoder, "zmbv", 0, 0);
1025 FFmpegEncoderSetContainer(&encoder, "avi");
1026 FFmpegEncoderSetDimensions(&encoder, image.width, image.height);
1027
1028 if (rebaseline && !exists) {
1029 if (!FFmpegEncoderOpen(&encoder, baselineName)) {
1030 CIerr(1, "Failed to save baseline video\n");
1031 } else {
1032 core->setAVStream(core, &encoder.d);
1033 }
1034 }
1035
1036 if (exists) {
1037 stream.d.postVideoFrame = _cinemaVideoFrame;
1038 stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
1039 stream.status = &test->status;
1040 decoder.out = &stream.d;
1041
1042 if (!FFmpegDecoderOpen(&decoder, baselineName)) {
1043 CIerr(1, "Failed to load baseline video\n");
1044 }
1045 } else if (!rebaseline) {
1046 test->status = CI_FAIL;
1047 }
1048 }
1049#else
1050 if (video) {
1051 CIerr(0, "Failed to run video test without ffmpeg linked in\n");
1052 test->status = CI_ERROR;
1053 }
1054#endif
1055
1056 bool xdiff = false;
1057 for (frame = 0; limit; ++frame, --limit) {
1058 _updateInput(core, frame, &input);
1059 core->runFrame(core);
1060 ++test->totalFrames;
1061 unsigned frameCounter = core->frameCounter(core);
1062 if (frameCounter <= minFrame) {
1063 break;
1064 }
1065 if (test->status == CI_ERROR) {
1066 break;
1067 }
1068 CIlog(3, "Test frame: %u\n", frameCounter);
1069 core->desiredVideoDimensions(core, &image.width, &image.height);
1070 uint8_t* diff = NULL;
1071 struct CInemaImage expected = {
1072 .data = NULL,
1073 .width = image.width,
1074 .height = image.height,
1075 .stride = image.width,
1076 };
1077 bool baselineFound;
1078 if (video) {
1079 baselineFound = false;
1080#ifdef USE_FFMPEG
1081 if (FFmpegDecoderIsOpen(&decoder)) {
1082 stream.image = &expected;
1083 while (!expected.data) {
1084 if (!FFmpegDecoderRead(&decoder)) {
1085 CIlog(1, "Failed to read more frames. EOF?\n");
1086 test->status = CI_FAIL;
1087 break;
1088 }
1089 }
1090 baselineFound = expected.data;
1091 }
1092#endif
1093 } else {
1094 baselineFound = _loadBaselinePNG(dir, "baseline", &expected, frame, &test->status);
1095 }
1096 if (test->status == CI_ERROR) {
1097 break;
1098 }
1099 bool failed = false;
1100 if (baselineFound) {
1101 int max = 0;
1102 failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1103 if (failed) {
1104 ++test->failedFrames;
1105#ifdef USE_FFMPEG
1106 if (video && exists && rebaseline && !FFmpegEncoderIsOpen(&encoder)) {
1107 _replayBaseline(test, &encoder, &image, frame);
1108 if (test->status == CI_ERROR) {
1109 break;
1110 }
1111 encoder.d.postVideoFrame(&encoder.d, image.data, image.stride);
1112 core->setAVStream(core, &encoder.d);
1113 }
1114#endif
1115 }
1116 test->totalPixels += image.height * image.width;
1117 if (rebaseline == CI_R_FAILING && !video && failed) {
1118 _writeBaseline(dir, "baseline", &image, frame);
1119 }
1120 if (diff) {
1121 if (failed) {
1122 _writeDiff(test->name, &image, frame, "result");
1123 _writeDiffSet(&expected, test->name, diff, frame, max, false);
1124 }
1125 free(diff);
1126 diff = NULL;
1127 }
1128 free(expected.data);
1129 } else if (rebaseline && !video) {
1130 _writeBaseline(dir, "baseline", &image, frame);
1131 } else if (!rebaseline) {
1132 test->status = CI_FAIL;
1133 }
1134
1135 if (fail && failed) {
1136 if (video) {
1137 // TODO
1138 baselineFound = false;
1139 } else {
1140 baselineFound = _loadBaselinePNG(dir, "xbaseline", &expected, frame, &test->status);
1141 }
1142
1143 if (baselineFound) {
1144 int max = 0;
1145 failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1146 if (diff) {
1147 if (failed) {
1148 _writeDiffSet(&expected, test->name, diff, frame, max, true);
1149 }
1150 free(diff);
1151 diff = NULL;
1152 }
1153 if (failed) {
1154 if (xbaseline == CI_R_FAILING && !video) {
1155 _writeBaseline(dir, "xbaseline", &image, frame);
1156 }
1157 xdiff = true;
1158 }
1159 free(expected.data);
1160 } else if (xbaseline && !video) {
1161 _writeBaseline(dir, "xbaseline", &image, frame);
1162 }
1163 }
1164 }
1165
1166#ifdef USE_FFMPEG
1167 if (video) {
1168 if (FFmpegEncoderIsOpen(&encoder)) {
1169 FFmpegEncoderClose(&encoder);
1170 if (exists && rebaseline) {
1171 char tmpBaselineName[PATH_MAX];
1172 snprintf(tmpBaselineName, sizeof(tmpBaselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
1173#ifdef _WIN32
1174 MoveFileEx(tmpBaselineName, baselineName, MOVEFILE_REPLACE_EXISTING);
1175#else
1176 rename(tmpBaselineName, baselineName);
1177#endif
1178 }
1179 }
1180 if (FFmpegDecoderIsOpen(&decoder)) {
1181 FFmpegDecoderClose(&decoder);
1182 }
1183 }
1184#endif
1185
1186 if (fail) {
1187 if (test->status == CI_FAIL && !xdiff) {
1188 test->status = CI_XFAIL;
1189 } else if (test->status == CI_PASS) {
1190 test->status = CI_XPASS;
1191 }
1192 }
1193
1194 free(image.data);
1195 mCoreConfigDeinit(&core->config);
1196 core->deinit(core);
1197 dir->close(dir);
1198}
1199
1200static bool CInemaTask(struct CInemaTestList* tests, size_t i) {
1201 bool success = true;
1202 struct CInemaTest* test = CInemaTestListGetPointer(tests, i);
1203 if (dryRun) {
1204 CIlog(-1, "%s\n", test->name);
1205 } else {
1206 CIlog(1, "%s: ", test->name);
1207 fflush(stdout);
1208 ThreadLocalSetKey(currentTest, test);
1209 CInemaTestRun(test);
1210 ThreadLocalSetKey(currentTest, NULL);
1211
1212 switch (test->status) {
1213 case CI_PASS:
1214 CIlog(1, "pass\n");
1215 break;
1216 case CI_FAIL:
1217 success = false;
1218 CIlog(1, "fail\n");
1219 break;
1220 case CI_XPASS:
1221 CIlog(1, "xpass\n");
1222 break;
1223 case CI_XFAIL:
1224 CIlog(1, "xfail\n");
1225 break;
1226 case CI_SKIP:
1227 CIlog(1, "skip\n");
1228 break;
1229 case CI_ERROR:
1230 success = false;
1231 CIlog(1, "error\n");
1232 break;
1233 }
1234 if (test->failedFrames) {
1235 CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
1236 CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
1237 CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
1238 }
1239 }
1240 return success;
1241}
1242
1243static THREAD_ENTRY CInemaJob(void* context) {
1244 struct CInemaTestList* tests = context;
1245 struct CInemaLogStream stream;
1246 StringListInit(&stream.out.lines, 0);
1247 StringListInit(&stream.out.partial, 0);
1248 stream.out.repeat = 0;
1249 StringListInit(&stream.err.lines, 0);
1250 StringListInit(&stream.err.partial, 0);
1251 stream.err.repeat = 0;
1252 ThreadLocalSetKey(logStream, &stream);
1253
1254 bool success = true;
1255 while (true) {
1256 size_t i;
1257 MutexLock(&jobMutex);
1258 i = jobIndex;
1259 ++jobIndex;
1260 MutexUnlock(&jobMutex);
1261 if (i >= CInemaTestListSize(tests)) {
1262 break;
1263 }
1264 if (!CInemaTask(tests, i)) {
1265 success = false;
1266 }
1267 CIflush(&stream.out, stdout);
1268 CIflush(&stream.err, stderr);
1269 }
1270 MutexLock(&jobMutex);
1271 if (!success) {
1272 jobStatus = 1;
1273 }
1274 MutexUnlock(&jobMutex);
1275
1276 CIflush(&stream.out, stdout);
1277 StringListDeinit(&stream.out.lines);
1278 StringListDeinit(&stream.out.partial);
1279
1280 CIflush(&stream.err, stderr);
1281 StringListDeinit(&stream.err.lines);
1282 StringListDeinit(&stream.err.partial);
1283}
1284
1285void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
1286 UNUSED(log);
1287 if (level == mLOG_FATAL) {
1288 struct CInemaTest* test = ThreadLocalGetValue(currentTest);
1289 test->status = CI_ERROR;
1290 }
1291 if (verbosity < 0) {
1292 return;
1293 }
1294 int mask = mLOG_FATAL;
1295 if (verbosity >= 1) {
1296 mask |= mLOG_ERROR;
1297 }
1298 if (verbosity >= 2) {
1299 mask |= mLOG_WARN;
1300 }
1301 if (verbosity >= 4) {
1302 mask |= mLOG_INFO;
1303 }
1304 if (verbosity >= 5) {
1305 mask |= mLOG_ALL;
1306 }
1307 if (!(mask & level)) {
1308 return;
1309 }
1310
1311 char buffer[256];
1312 vsnprintf(buffer, sizeof(buffer), format, args);
1313 CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);
1314}
1315
1316int main(int argc, char** argv) {
1317 ThreadLocalInitKey(&logStream);
1318 ThreadLocalSetKey(logStream, NULL);
1319
1320 int status = 0;
1321 if (!parseCInemaArgs(argc, argv)) {
1322 status = 1;
1323 goto cleanup;
1324 }
1325
1326 if (showVersion) {
1327 version(argv[0]);
1328 goto cleanup;
1329 }
1330
1331 if (showUsage) {
1332 usageCInema(argv[0]);
1333 goto cleanup;
1334 }
1335
1336 argc -= optind;
1337 argv += optind;
1338
1339 if (!base[0] && !determineBase(argc, argv)) {
1340 CIlog(0, "Could not determine CInema test base. Please specify manually.");
1341 status = 1;
1342 goto cleanup;
1343 }
1344#ifndef _WIN32
1345 char* rbase = realpath(base, NULL);
1346 if (rbase) {
1347 strlcpy(base, rbase, sizeof(base));
1348 free(rbase);
1349 }
1350#endif
1351
1352 struct CInemaTestList tests;
1353 CInemaTestListInit(&tests, 0);
1354
1355 struct mLogger logger = { .log = _log };
1356 mLogSetDefaultLogger(&logger);
1357#ifdef USE_FFMPEG
1358 if (verbosity < 2) {
1359 av_log_set_level(AV_LOG_ERROR);
1360 }
1361#endif
1362
1363 if (argc > 0) {
1364 size_t i;
1365 for (i = 0; i < (size_t) argc; ++i) {
1366 if (strchr(argv[i], '*')) {
1367 if (!globTests(&tests, argv[i], NULL)) {
1368 status = 1;
1369 break;
1370 }
1371 continue;
1372 }
1373 char path[PATH_MAX + 1] = {0};
1374 testToPath(argv[i], path);
1375
1376 if (!collectTests(&tests, path)) {
1377 status = 1;
1378 break;
1379 }
1380 }
1381 } else if (!collectTests(&tests, base)) {
1382 status = 1;
1383 }
1384
1385 if (CInemaTestListSize(&tests) == 0) {
1386 CIlog(1, "No tests found.");
1387 status = 1;
1388 } else {
1389 reduceTestList(&tests);
1390 }
1391
1392 HashTableInit(&configTree, 0, free);
1393 MutexInit(&configMutex);
1394 ThreadLocalInitKey(¤tTest);
1395 ThreadLocalSetKey(currentTest, NULL);
1396
1397 if (jobs == 1) {
1398 size_t i;
1399 for (i = 0; i < CInemaTestListSize(&tests); ++i) {
1400 bool success = CInemaTask(&tests, i);
1401 if (!success) {
1402 status = 1;
1403 }
1404 }
1405 } else {
1406 MutexInit(&jobMutex);
1407 int i;
1408 for (i = 0; i < jobs; ++i) {
1409 ThreadCreate(&jobThreads[i], CInemaJob, &tests);
1410 }
1411 for (i = 0; i < jobs; ++i) {
1412 ThreadJoin(&jobThreads[i]);
1413 }
1414 MutexDeinit(&jobMutex);
1415 status = jobStatus;
1416 }
1417
1418 MutexDeinit(&configMutex);
1419 HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
1420 HashTableDeinit(&configTree);
1421 CInemaTestListDeinit(&tests);
1422
1423cleanup:
1424 return status;
1425}