all repos — mgba @ 89de06a610caea5f84fcbad139fe481bf8dae943

mGBA Game Boy Advance Emulator

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		}
 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(&currentTest);
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}