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 fflush(out);
230}
231
232static bool parseCInemaArgs(int argc, char* const* argv) {
233 int ch;
234 int index = 0;
235 while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
236 const struct option* opt = &longOpts[index];
237 switch (ch) {
238 case '\0':
239 if (strcmp(opt->name, "version") == 0) {
240 showVersion = true;
241 } else {
242 return false;
243 }
244 break;
245 case 'b':
246 strlcpy(base, optarg, sizeof(base));
247 // TODO: Verify path exists
248 break;
249 case 'd':
250 diffs = true;
251 break;
252 case 'h':
253 showUsage = true;
254 break;
255 case 'j':
256 jobs = atoi(optarg);
257 if (jobs > MAX_JOBS) {
258 jobs = MAX_JOBS;
259 }
260 if (jobs < 1) {
261 jobs = 1;
262 }
263 break;
264 case 'n':
265 dryRun = true;
266 break;
267 case 'o':
268 strlcpy(outdir, optarg, sizeof(outdir));
269 // TODO: Make directory
270 break;
271 case 'q':
272 --verbosity;
273 break;
274 case 'r':
275 rebaseline = CI_R_FAILING;
276 break;
277 case 'R':
278 rebaseline = CI_R_MISSING;
279 break;
280 case 'v':
281 ++verbosity;
282 break;
283 case 'x':
284 xbaseline = CI_R_FAILING;
285 break;
286 default:
287 return false;
288 }
289 }
290
291 return true;
292}
293
294static void usageCInema(const char* arg0) {
295 printf("usage: %s [-dhnqrRv] [-j JOBS] [-b BASE] [-o DIR] [--version] [test...]\n", arg0);
296 puts(" -b, --base BASE Path to the CInema base directory");
297 puts(" -d, --diffs Output image diffs from failures");
298 puts(" -h, --help Print this usage and exit");
299 puts(" -j, --jobs JOBS Run a number of jobs in parallel");
300 puts(" -n, --dry-run List all collected tests instead of running them");
301 puts(" -o, --output DIR Path to output applicable results");
302 puts(" -q, --quiet Decrease log verbosity (can be repeated)");
303 puts(" -r, --rebaseline Rewrite the baseline for failing tests");
304 puts(" -R, --rebaseline-missing Write missing baselines tests only");
305 puts(" -v, --verbose Increase log verbosity (can be repeated)");
306 puts(" -x, --xbaseline Rewrite the xfail baselines for failing tests");
307 puts(" --version Print version and exit");
308}
309
310static bool determineBase(int argc, char* const* argv) {
311 // TODO: Better dynamic detection
312 separatePath(__FILE__, base, NULL, NULL);
313 strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
314 return true;
315}
316
317static bool collectTests(struct CInemaTestList* tests, const char* path) {
318 CIerr(2, "Considering path %s\n", path);
319 struct VDir* dir = VDirOpen(path);
320 if (!dir) {
321 return false;
322 }
323 struct VDirEntry* entry = dir->listNext(dir);
324 while (entry) {
325 char subpath[PATH_MAX];
326 snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
327 if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
328 if (!collectTests(tests, subpath)) {
329 dir->close(dir);
330 return false;
331 }
332 } else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
333 CIerr(3, "Found potential test %s\n", subpath);
334 struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
335 if (vf) {
336 if (mCoreIsCompatible(vf) != mPLATFORM_NONE || mVideoLogIsCompatible(vf) != mPLATFORM_NONE) {
337 struct CInemaTest* test = CInemaTestListAppend(tests);
338 if (!CInemaTestInit(test, path, entry->name(entry))) {
339 CIerr(3, "Failed to create test\n");
340 CInemaTestListResize(tests, -1);
341 } else {
342 CIerr(2, "Found test %s\n", test->name);
343 }
344 } else {
345 CIerr(3, "Not a compatible file\n");
346 }
347 vf->close(vf);
348 } else {
349 CIerr(3, "Failed to open file\n");
350 }
351 }
352 entry = dir->listNext(dir);
353 }
354 dir->close(dir);
355 return true;
356}
357
358static int _compareNames(const void* a, const void* b) {
359 const struct CInemaTest* ta = a;
360 const struct CInemaTest* tb = b;
361
362 return strncmp(ta->name, tb->name, sizeof(ta->name));
363}
364
365static void reduceTestList(struct CInemaTestList* tests) {
366 qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
367
368 size_t i;
369 for (i = 1; i < CInemaTestListSize(tests);) {
370 struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
371 struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
372 if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
373 ++i;
374 continue;
375 }
376 CInemaTestListShift(tests, i, 1);
377 }
378}
379
380static void testToPath(const char* testName, char* path) {
381 strlcpy(path, base, PATH_MAX);
382
383 bool dotSeen = true;
384 size_t i;
385 for (i = strlen(path); testName[0] && i < PATH_MAX; ++testName) {
386 if (testName[0] == '.') {
387 dotSeen = true;
388 } else {
389 if (dotSeen) {
390 strlcpy(&path[i], PATH_SEP, PATH_MAX - i);
391 i += strlen(PATH_SEP);
392 dotSeen = false;
393 if (!i) {
394 break;
395 }
396 }
397 path[i] = testName[0];
398 ++i;
399 }
400 }
401 if (i == PATH_MAX) {
402 --i;
403 }
404 path[i] = '\0';
405}
406
407static bool globTests(struct CInemaTestList* tests, const char* glob, const char* ancestors) {
408 bool success = true;
409 const char* next = strpbrk(glob, "*.");
410
411 char path[PATH_MAX];
412 if (!next) {
413 testToPath(glob, path);
414 return collectTests(tests, path);
415 } else if (next[0] == '.') {
416 char subtest[MAX_TEST];
417 if (!ancestors) {
418 strncpy(subtest, glob, next - glob);
419 } else {
420 size_t len = strlen(ancestors) + (next - glob) + 2;
421 if (len > sizeof(subtest)) {
422 len = sizeof(subtest);
423 }
424 snprintf(subtest, len, "%s.%s", ancestors, glob);
425 }
426 return globTests(tests, next + 1, subtest);
427 } else if (next[0] == '*') {
428 char globBuffer[MAX_TEST];
429 const char* subglob;
430
431 next = strchr(next, '.');
432 if (!next) {
433 subglob = glob;
434 } else {
435 size_t len = next - glob + 1;
436 if (len > sizeof(globBuffer)) {
437 len = sizeof(globBuffer);
438 }
439 strncpy(globBuffer, glob, len - 1);
440 subglob = globBuffer;
441 }
442 bool hasMoreGlobs = next && strchr(next, '*');
443
444 struct VDir* dir;
445 if (ancestors) {
446 testToPath(ancestors, path);
447 dir = VDirOpen(path);
448 } else {
449 dir = VDirOpen(base);
450 }
451 if (!dir) {
452 return false;
453 }
454
455 struct VDirEntry* dirent = dir->listNext(dir);
456 while (dirent) {
457 const char* name = dirent->name(dirent);
458 if (dirent->type(dirent) != VFS_DIRECTORY || strncmp(name, ".", 2) == 0 || strncmp(name, "..", 3) == 0) {
459 dirent = dir->listNext(dir);
460 continue;
461 }
462 if (wildcard(subglob, name)) {
463 char newgen[MAX_TEST];
464 if (ancestors) {
465 snprintf(newgen, sizeof(newgen), "%s.%s", ancestors, name);
466 } else {
467 strlcpy(newgen, name, sizeof(newgen));
468 }
469 if (next && hasMoreGlobs) {
470 globTests(tests, next + 1, newgen);
471 } else {
472 testToPath(newgen, path);
473 collectTests(tests, path);
474 }
475 }
476 dirent = dir->listNext(dir);
477 }
478
479 return true;
480 } else {
481 abort();
482 }
483}
484
485static void _loadConfigTree(struct Table* configTree, const char* testName) {
486 char key[MAX_TEST];
487 strlcpy(key, testName, sizeof(key));
488
489 struct mCoreConfig* config;
490 while (!(config = HashTableLookup(configTree, key))) {
491 char path[PATH_MAX];
492 config = malloc(sizeof(*config));
493 mCoreConfigInit(config, "cinema");
494 testToPath(key, path);
495 strncat(path, PATH_SEP, sizeof(path) - 1);
496 strncat(path, "config.ini", sizeof(path) - 1);
497 mCoreConfigLoadPath(config, path);
498 HashTableInsert(configTree, key, config);
499 char* pos = strrchr(key, '.');
500 if (pos) {
501 pos[0] = '\0';
502 } else if (key[0]) {
503 key[0] = '\0';
504 } else {
505 break;
506 }
507 }
508}
509
510static void _unloadConfigTree(const char* key, void* value, void* user) {
511 UNUSED(key);
512 UNUSED(user);
513 mCoreConfigDeinit(value);
514}
515
516static const char* CInemaConfigGet(struct Table* configTree, const char* testName, const char* key) {
517 _loadConfigTree(configTree, testName);
518
519 char testKey[MAX_TEST];
520 strlcpy(testKey, testName, sizeof(testKey));
521
522 struct mCoreConfig* config;
523 while (true) {
524 config = HashTableLookup(configTree, testKey);
525 if (!config) {
526 continue;
527 }
528 const char* str = ConfigurationGetValue(&config->configTable, "testinfo", key);
529 if (str) {
530 return str;
531 }
532 char* pos = strrchr(testKey, '.');
533 if (pos) {
534 pos[0] = '\0';
535 } else if (testKey[0]) {
536 testKey[0] = '\0';
537 } else {
538 break;
539 }
540 }
541 return NULL;
542}
543
544bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) {
545 const char* charValue = CInemaConfigGet(configTree, testName, key);
546 if (!charValue) {
547 return false;
548 }
549 char* end;
550 unsigned long value = strtoul(charValue, &end, 10);
551 if (*end) {
552 return false;
553 }
554 *out = value;
555 return true;
556}
557
558void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core) {
559 _loadConfigTree(configTree, testName);
560
561 char testKey[MAX_TEST] = {0};
562 char* keyEnd = testKey;
563 const char* pos;
564 while (true) {
565 pos = strchr(testName, '.');
566 size_t maxlen = sizeof(testKey) - (keyEnd - testKey) - 1;
567 size_t len;
568 if (pos) {
569 len = pos - testName;
570 } else {
571 len = strlen(testName);
572 }
573 if (len > maxlen) {
574 len = maxlen;
575 }
576 strncpy(keyEnd, testName, len);
577 keyEnd += len;
578
579 struct mCoreConfig* config = HashTableLookup(configTree, testKey);
580 if (config) {
581 core->loadConfig(core, config);
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_%05" 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#ifndef __BIG_ENDIAN__
796 int testR = testPixels[tpix * 4 + 0];
797 int testG = testPixels[tpix * 4 + 1];
798 int testB = testPixels[tpix * 4 + 2];
799 int expectR = expectPixels[pix * 4 + 0];
800 int expectG = expectPixels[pix * 4 + 1];
801 int expectB = expectPixels[pix * 4 + 2];
802#else
803 int testB = testPixels[tpix * 4 + 1];
804 int testG = testPixels[tpix * 4 + 2];
805 int testR = testPixels[tpix * 4 + 3];
806 int expectB = expectPixels[pix * 4 + 1];
807 int expectG = expectPixels[pix * 4 + 2];
808 int expectR = expectPixels[pix * 4 + 3];
809#endif
810 int r = expectR - testR;
811 int g = expectG - testG;
812 int b = expectB - testB;
813 if (r | g | b) {
814 failed = true;
815 if (outdiff && !diff) {
816 diff = calloc(expected->stride * expected->height, BYTES_PER_PIXEL);
817 *outdiff = diff;
818 }
819 test->status = CI_FAIL;
820 if (r < 0) {
821 r = -r;
822 }
823 if (g < 0) {
824 g = -g;
825 }
826 if (b < 0) {
827 b = -b;
828 }
829
830 if (diff) {
831 if (r > *max) {
832 *max = r;
833 }
834 if (g > *max) {
835 *max = g;
836 }
837 if (b > *max) {
838 *max = b;
839 }
840#ifndef __BIG_ENDIAN__
841 diff[pix * 4 + 0] = r;
842 diff[pix * 4 + 1] = g;
843 diff[pix * 4 + 2] = b;
844#else
845 diff[pix * 4 + 1] = b;
846 diff[pix * 4 + 2] = g;
847 diff[pix * 4 + 3] = r;
848#endif
849 }
850
851 if (test) {
852 test->totalDistance += r + g + b;
853 ++test->failedPixels;
854 }
855 }
856 }
857 }
858 return !failed;
859}
860
861void _writeDiffSet(struct CInemaImage* expected, const char* name, uint8_t* diff, int frame, int max, bool xfail) {
862 struct CInemaImage outdiff = {
863 .data = diff,
864 .width = expected->width,
865 .height = expected->height,
866 .stride = expected->stride,
867 };
868
869 if (xfail) {
870 _writeDiff(name, expected, frame, "xexpected");
871 _writeDiff(name, &outdiff, frame, "xdiff");
872 } else {
873 _writeDiff(name, expected, frame, "expected");
874 _writeDiff(name, &outdiff, frame, "diff");
875 }
876
877 size_t x;
878 size_t y;
879 for (y = 0; y < outdiff.height; ++y) {
880 for (x = 0; x < outdiff.width; ++x) {
881 size_t pix = outdiff.stride * y + x;
882#ifndef __BIG_ENDIAN__
883 diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
884 diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
885 diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
886#else
887 diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
888 diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
889 diff[pix * 4 + 3] = diff[pix * 4 + 3] * 255 / max;
890#endif
891 }
892 }
893 if (xfail) {
894 _writeDiff(name, &outdiff, frame, "xnormalized");
895 } else {
896 _writeDiff(name, &outdiff, frame, "normalized");
897 }
898}
899
900#ifdef USE_FFMPEG
901static void _replayBaseline(struct CInemaTest* test, struct FFmpegEncoder* encoder, const struct CInemaImage* image, int frame) {
902 char baselineName[PATH_MAX];
903 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
904
905 if (!FFmpegEncoderOpen(encoder, baselineName)) {
906 CIerr(1, "Failed to save baseline video\n");
907 test->status = CI_ERROR;
908 return;
909 }
910 encoder->d.videoDimensionsChanged(&encoder->d, image->width, image->height);
911
912 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
913
914 struct CInemaImage buffer = {
915 .data = NULL,
916 .width = image->width,
917 .height = image->height,
918 .stride = image->width,
919 };
920 struct FFmpegDecoder decoder;
921 struct CInemaStream stream = {0};
922 stream.d.postVideoFrame = _cinemaVideoFrame;
923 stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
924 stream.status = &test->status;
925 stream.image = &buffer;
926
927 FFmpegDecoderInit(&decoder);
928 decoder.out = &stream.d;
929
930 if (!FFmpegDecoderOpen(&decoder, baselineName)) {
931 CIerr(1, "Failed to load baseline video\n");
932 test->status = CI_ERROR;
933 return;
934 }
935
936 int i;
937 for (i = 0; i < frame; ++i) {
938 while (!buffer.data) {
939 if (!FFmpegDecoderRead(&decoder)) {
940 CIlog(1, "Failed to read more frames. EOF?\n");
941 test->status = CI_FAIL;
942 break;
943 }
944 }
945 encoder->d.postVideoFrame(&encoder->d, buffer.data, buffer.stride);
946 free(buffer.data);
947 buffer.data = NULL;
948 }
949 FFmpegDecoderClose(&decoder);
950}
951#endif
952
953void CInemaTestRun(struct CInemaTest* test) {
954 unsigned ignore = 0;
955 MutexLock(&configMutex);
956 CInemaConfigGetUInt(&configTree, test->name, "ignore", &ignore);
957 MutexUnlock(&configMutex);
958 if (ignore) {
959 test->status = CI_SKIP;
960 return;
961 }
962
963 struct VDir* dir = VDirOpen(test->directory);
964 if (!dir) {
965 CIerr(0, "Failed to open test directory\n");
966 test->status = CI_ERROR;
967 return;
968 }
969 struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
970 if (!rom) {
971 CIerr(0, "Failed to open test\n");
972 test->status = CI_ERROR;
973 return;
974 }
975 struct mCore* core = mCoreFindVF(rom);
976 if (!core) {
977 CIerr(0, "Failed to load test\n");
978 test->status = CI_ERROR;
979 rom->close(rom);
980 return;
981 }
982 if (!core->init(core)) {
983 CIerr(0, "Failed to init test\n");
984 test->status = CI_ERROR;
985 core->deinit(core);
986 return;
987 }
988 struct CInemaImage image;
989 core->desiredVideoDimensions(core, &image.width, &image.height);
990 ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL;
991 image.data = malloc(bufferSize);
992 image.stride = image.width;
993 if (!image.data) {
994 CIerr(0, "Failed to allocate video buffer\n");
995 test->status = CI_ERROR;
996 core->deinit(core);
997 }
998 core->setVideoBuffer(core, image.data, image.stride);
999 mCoreConfigInit(&core->config, "cinema");
1000
1001 unsigned limit = 3600;
1002 unsigned skip = 0;
1003 unsigned fail = 0;
1004 unsigned video = 0;
1005 const char* input = NULL;
1006
1007 MutexLock(&configMutex);
1008 CInemaConfigGetUInt(&configTree, test->name, "frames", &limit);
1009 CInemaConfigGetUInt(&configTree, test->name, "skip", &skip);
1010 CInemaConfigGetUInt(&configTree, test->name, "fail", &fail);
1011 CInemaConfigGetUInt(&configTree, test->name, "video", &video);
1012 input = CInemaConfigGet(&configTree, test->name, "input");
1013 CInemaConfigLoad(&configTree, test->name, core);
1014 MutexUnlock(&configMutex);
1015
1016 struct VFile* save = VFileMemChunk(NULL, 0);
1017 core->loadROM(core, rom);
1018 if (!core->loadSave(core, save)) {
1019 save->close(save);
1020 }
1021 core->rtc.override = RTC_FAKE_EPOCH;
1022 core->rtc.value = 1200000000;
1023 core->reset(core);
1024
1025 test->status = CI_PASS;
1026
1027 unsigned minFrame = core->frameCounter(core);
1028 size_t frame;
1029 for (frame = 0; frame < skip; ++frame) {
1030 core->runFrame(core);
1031 }
1032 core->desiredVideoDimensions(core, &image.width, &image.height);
1033
1034#ifdef USE_FFMPEG
1035 struct FFmpegDecoder decoder;
1036 struct FFmpegEncoder encoder;
1037 struct CInemaStream stream = {0};
1038
1039 char baselineName[PATH_MAX];
1040 snprintf(baselineName, sizeof(baselineName), "%s" PATH_SEP "baseline.avi", test->directory);
1041 bool exists = access(baselineName, 0) == 0;
1042
1043 if (video) {
1044 FFmpegEncoderInit(&encoder);
1045 FFmpegDecoderInit(&decoder);
1046
1047 FFmpegEncoderSetAudio(&encoder, NULL, 0);
1048 FFmpegEncoderSetVideo(&encoder, "zmbv", 0, 0);
1049 FFmpegEncoderSetContainer(&encoder, "avi");
1050 FFmpegEncoderSetDimensions(&encoder, image.width, image.height);
1051
1052 if (rebaseline && !exists) {
1053 if (!FFmpegEncoderOpen(&encoder, baselineName)) {
1054 CIerr(1, "Failed to save baseline video\n");
1055 } else {
1056 core->setAVStream(core, &encoder.d);
1057 }
1058 }
1059
1060 if (exists) {
1061 stream.d.postVideoFrame = _cinemaVideoFrame;
1062 stream.d.videoDimensionsChanged = _cinemaDimensionsChanged;
1063 stream.status = &test->status;
1064 decoder.out = &stream.d;
1065
1066 if (!FFmpegDecoderOpen(&decoder, baselineName)) {
1067 CIerr(1, "Failed to load baseline video\n");
1068 }
1069 } else if (!rebaseline) {
1070 test->status = CI_FAIL;
1071 }
1072 }
1073#else
1074 if (video) {
1075 CIerr(0, "Failed to run video test without ffmpeg linked in\n");
1076 test->status = CI_ERROR;
1077 }
1078#endif
1079
1080 bool xdiff = false;
1081 for (frame = 0; limit; ++frame, --limit) {
1082 _updateInput(core, frame, &input);
1083 core->runFrame(core);
1084 ++test->totalFrames;
1085 unsigned frameCounter = core->frameCounter(core);
1086 if (frameCounter <= minFrame) {
1087 break;
1088 }
1089 if (test->status == CI_ERROR) {
1090 break;
1091 }
1092 CIlog(3, "Test frame: %u\n", frameCounter);
1093 core->desiredVideoDimensions(core, &image.width, &image.height);
1094 uint8_t* diff = NULL;
1095 struct CInemaImage expected = {
1096 .data = NULL,
1097 .width = image.width,
1098 .height = image.height,
1099 .stride = image.width,
1100 };
1101 bool baselineFound;
1102 if (video) {
1103 baselineFound = false;
1104#ifdef USE_FFMPEG
1105 if (FFmpegDecoderIsOpen(&decoder)) {
1106 stream.image = &expected;
1107 while (!expected.data) {
1108 if (!FFmpegDecoderRead(&decoder)) {
1109 CIlog(1, "Failed to read more frames. EOF?\n");
1110 test->status = CI_FAIL;
1111 if (rebaseline && !FFmpegEncoderIsOpen(&encoder)) {
1112 _replayBaseline(test, &encoder, &image, frame);
1113 if (test->status == CI_ERROR) {
1114 break;
1115 }
1116 encoder.d.postVideoFrame(&encoder.d, image.data, image.stride);
1117 core->setAVStream(core, &encoder.d);
1118 }
1119 break;
1120 }
1121 }
1122 baselineFound = expected.data;
1123 }
1124#endif
1125 } else {
1126 baselineFound = _loadBaselinePNG(dir, "baseline", &expected, frame, &test->status);
1127 }
1128 if (test->status == CI_ERROR) {
1129 break;
1130 }
1131 bool failed = false;
1132 if (baselineFound) {
1133 int max = 0;
1134 failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1135 if (failed) {
1136 ++test->failedFrames;
1137#ifdef USE_FFMPEG
1138 if (video && exists && rebaseline && !FFmpegEncoderIsOpen(&encoder)) {
1139 _replayBaseline(test, &encoder, &image, frame);
1140 if (test->status == CI_ERROR) {
1141 break;
1142 }
1143 encoder.d.postVideoFrame(&encoder.d, image.data, image.stride);
1144 core->setAVStream(core, &encoder.d);
1145 }
1146#endif
1147 }
1148 test->totalPixels += image.height * image.width;
1149 if (rebaseline == CI_R_FAILING && !video && failed) {
1150 _writeBaseline(dir, "baseline", &image, frame);
1151 }
1152 if (diff) {
1153 if (failed) {
1154 _writeDiff(test->name, &image, frame, "result");
1155 _writeDiffSet(&expected, test->name, diff, frame, max, false);
1156 }
1157 free(diff);
1158 diff = NULL;
1159 }
1160 free(expected.data);
1161 } else if (rebaseline && !video) {
1162 _writeBaseline(dir, "baseline", &image, frame);
1163 } else if (!rebaseline) {
1164 test->status = CI_FAIL;
1165 }
1166
1167 if (fail && failed) {
1168 if (video) {
1169 // TODO
1170 baselineFound = false;
1171 } else {
1172 baselineFound = _loadBaselinePNG(dir, "xbaseline", &expected, frame, &test->status);
1173 }
1174
1175 if (baselineFound) {
1176 int max = 0;
1177 failed = !_compareImages(test, &image, &expected, &max, diffs ? &diff : NULL);
1178 if (diff) {
1179 if (failed) {
1180 _writeDiffSet(&expected, test->name, diff, frame, max, true);
1181 }
1182 free(diff);
1183 diff = NULL;
1184 }
1185 if (failed) {
1186 if (xbaseline == CI_R_FAILING && !video) {
1187 _writeBaseline(dir, "xbaseline", &image, frame);
1188 }
1189 xdiff = true;
1190 }
1191 free(expected.data);
1192 } else if (xbaseline && !video) {
1193 _writeBaseline(dir, "xbaseline", &image, frame);
1194 }
1195 }
1196 }
1197
1198#ifdef USE_FFMPEG
1199 if (video) {
1200 if (FFmpegEncoderIsOpen(&encoder)) {
1201 FFmpegEncoderClose(&encoder);
1202 if (exists && rebaseline) {
1203 char tmpBaselineName[PATH_MAX];
1204 snprintf(tmpBaselineName, sizeof(tmpBaselineName), "%s" PATH_SEP ".baseline.avi", test->directory);
1205#ifdef _WIN32
1206 MoveFileEx(tmpBaselineName, baselineName, MOVEFILE_REPLACE_EXISTING);
1207#else
1208 rename(tmpBaselineName, baselineName);
1209#endif
1210 }
1211 }
1212 if (FFmpegDecoderIsOpen(&decoder)) {
1213 FFmpegDecoderClose(&decoder);
1214 }
1215 }
1216#endif
1217
1218 if (fail) {
1219 if (test->status == CI_FAIL && !xdiff) {
1220 test->status = CI_XFAIL;
1221 } else if (test->status == CI_PASS) {
1222 test->status = CI_XPASS;
1223 }
1224 }
1225
1226 free(image.data);
1227 mCoreConfigDeinit(&core->config);
1228 core->deinit(core);
1229 dir->close(dir);
1230}
1231
1232static bool CInemaTask(struct CInemaTestList* tests, size_t i) {
1233 bool success = true;
1234 struct CInemaTest* test = CInemaTestListGetPointer(tests, i);
1235 if (dryRun) {
1236 CIlog(-1, "%s\n", test->name);
1237 } else {
1238 CIlog(1, "%s: ", test->name);
1239 fflush(stdout);
1240 ThreadLocalSetKey(currentTest, test);
1241 CInemaTestRun(test);
1242 ThreadLocalSetKey(currentTest, NULL);
1243
1244 switch (test->status) {
1245 case CI_PASS:
1246 CIlog(1, "pass\n");
1247 break;
1248 case CI_FAIL:
1249 success = false;
1250 CIlog(1, "fail\n");
1251 break;
1252 case CI_XPASS:
1253 CIlog(1, "xpass\n");
1254 break;
1255 case CI_XFAIL:
1256 CIlog(1, "xfail\n");
1257 break;
1258 case CI_SKIP:
1259 CIlog(1, "skip\n");
1260 break;
1261 case CI_ERROR:
1262 success = false;
1263 CIlog(1, "error\n");
1264 break;
1265 }
1266 if (test->failedFrames) {
1267 CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
1268 CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
1269 CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
1270 }
1271 }
1272 return success;
1273}
1274
1275static THREAD_ENTRY CInemaJob(void* context) {
1276 struct CInemaTestList* tests = context;
1277 struct CInemaLogStream stream;
1278 StringListInit(&stream.out.lines, 0);
1279 StringListInit(&stream.out.partial, 0);
1280 stream.out.repeat = 0;
1281 StringListInit(&stream.err.lines, 0);
1282 StringListInit(&stream.err.partial, 0);
1283 stream.err.repeat = 0;
1284 ThreadLocalSetKey(logStream, &stream);
1285
1286 bool success = true;
1287 while (true) {
1288 size_t i;
1289 MutexLock(&jobMutex);
1290 i = jobIndex;
1291 ++jobIndex;
1292 MutexUnlock(&jobMutex);
1293 if (i >= CInemaTestListSize(tests)) {
1294 break;
1295 }
1296 if (!CInemaTask(tests, i)) {
1297 success = false;
1298 }
1299 CIflush(&stream.out, stdout);
1300 CIflush(&stream.err, stderr);
1301 }
1302 MutexLock(&jobMutex);
1303 if (!success) {
1304 jobStatus = 1;
1305 }
1306 MutexUnlock(&jobMutex);
1307
1308 CIflush(&stream.out, stdout);
1309 StringListDeinit(&stream.out.lines);
1310 StringListDeinit(&stream.out.partial);
1311
1312 CIflush(&stream.err, stderr);
1313 StringListDeinit(&stream.err.lines);
1314 StringListDeinit(&stream.err.partial);
1315}
1316
1317void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
1318 UNUSED(log);
1319 if (level == mLOG_FATAL) {
1320 struct CInemaTest* test = ThreadLocalGetValue(currentTest);
1321 test->status = CI_ERROR;
1322 }
1323 if (verbosity < 0) {
1324 return;
1325 }
1326 int mask = mLOG_FATAL;
1327 if (verbosity >= 1) {
1328 mask |= mLOG_ERROR;
1329 }
1330 if (verbosity >= 2) {
1331 mask |= mLOG_WARN;
1332 }
1333 if (verbosity >= 4) {
1334 mask |= mLOG_INFO;
1335 }
1336 if (verbosity >= 5) {
1337 mask |= mLOG_ALL;
1338 }
1339 if (!(mask & level)) {
1340 return;
1341 }
1342
1343 char buffer[256];
1344 vsnprintf(buffer, sizeof(buffer), format, args);
1345 CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);
1346}
1347
1348int main(int argc, char** argv) {
1349 ThreadLocalInitKey(&logStream);
1350 ThreadLocalSetKey(logStream, NULL);
1351 putenv("TZ=UTC");
1352
1353 int status = 0;
1354 if (!parseCInemaArgs(argc, argv)) {
1355 status = 1;
1356 goto cleanup;
1357 }
1358
1359 if (showVersion) {
1360 version(argv[0]);
1361 goto cleanup;
1362 }
1363
1364 if (showUsage) {
1365 usageCInema(argv[0]);
1366 goto cleanup;
1367 }
1368
1369 argc -= optind;
1370 argv += optind;
1371
1372 if (!base[0] && !determineBase(argc, argv)) {
1373 CIlog(0, "Could not determine CInema test base. Please specify manually.");
1374 status = 1;
1375 goto cleanup;
1376 }
1377#ifndef _WIN32
1378 char* rbase = realpath(base, NULL);
1379 if (rbase) {
1380 strlcpy(base, rbase, sizeof(base));
1381 free(rbase);
1382 }
1383#endif
1384
1385 struct CInemaTestList tests;
1386 CInemaTestListInit(&tests, 0);
1387
1388 struct mLogger logger = { .log = _log };
1389 mLogSetDefaultLogger(&logger);
1390#ifdef USE_FFMPEG
1391 if (verbosity < 2) {
1392 av_log_set_level(AV_LOG_ERROR);
1393 }
1394#endif
1395
1396 if (argc > 0) {
1397 size_t i;
1398 for (i = 0; i < (size_t) argc; ++i) {
1399 if (strchr(argv[i], '*')) {
1400 if (!globTests(&tests, argv[i], NULL)) {
1401 status = 1;
1402 break;
1403 }
1404 continue;
1405 }
1406 char path[PATH_MAX + 1] = {0};
1407 testToPath(argv[i], path);
1408
1409 if (!collectTests(&tests, path)) {
1410 status = 1;
1411 break;
1412 }
1413 }
1414 } else if (!collectTests(&tests, base)) {
1415 status = 1;
1416 }
1417
1418 if (CInemaTestListSize(&tests) == 0) {
1419 CIlog(1, "No tests found.");
1420 status = 1;
1421 } else {
1422 reduceTestList(&tests);
1423 }
1424
1425 HashTableInit(&configTree, 0, free);
1426 MutexInit(&configMutex);
1427 ThreadLocalInitKey(¤tTest);
1428 ThreadLocalSetKey(currentTest, NULL);
1429
1430 if (jobs == 1) {
1431 size_t i;
1432 for (i = 0; i < CInemaTestListSize(&tests); ++i) {
1433 bool success = CInemaTask(&tests, i);
1434 if (!success) {
1435 status = 1;
1436 }
1437 }
1438 } else {
1439 MutexInit(&jobMutex);
1440 int i;
1441 for (i = 0; i < jobs; ++i) {
1442 ThreadCreate(&jobThreads[i], CInemaJob, &tests);
1443 }
1444 for (i = 0; i < jobs; ++i) {
1445 ThreadJoin(&jobThreads[i]);
1446 }
1447 MutexDeinit(&jobMutex);
1448 status = jobStatus;
1449 }
1450
1451 MutexDeinit(&configMutex);
1452 HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
1453 HashTableDeinit(&configTree);
1454 CInemaTestListDeinit(&tests);
1455
1456cleanup:
1457 return status;
1458}