all repos — mgba @ cdcbedc65b942e006775fb105363a82b6eb2f91e

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/table.h>
 15#include <mgba-util/vector.h>
 16#include <mgba-util/vfs.h>
 17
 18#ifdef _MSC_VER
 19#include <mgba-util/platform/windows/getopt.h>
 20#else
 21#include <getopt.h>
 22#endif
 23
 24#include <stdlib.h>
 25#include <sys/stat.h>
 26#include <sys/types.h>
 27
 28#define MAX_TEST 200
 29
 30static const struct option longOpts[] = {
 31	{ "base",       required_argument, 0, 'b' },
 32	{ "diffs",      no_argument, 0, 'd' },
 33	{ "help",       no_argument, 0, 'h' },
 34	{ "dry-run",    no_argument, 0, 'n' },
 35	{ "outdir",     required_argument, 0, 'o' },
 36	{ "quiet",      no_argument, 0, 'q' },
 37	{ "rebaseline", no_argument, 0, 'r' },
 38	{ "verbose",    no_argument, 0, 'v' },
 39	{ "version",    no_argument, 0, '\0' },
 40	{ 0, 0, 0, 0 }
 41};
 42
 43static const char shortOpts[] = "b:dhno:qrv";
 44
 45enum CInemaStatus {
 46	CI_PASS,
 47	CI_FAIL,
 48	CI_XPASS,
 49	CI_XFAIL,
 50	CI_ERROR,
 51	CI_SKIP
 52};
 53
 54struct CInemaTest {
 55	char directory[MAX_TEST];
 56	char filename[MAX_TEST];
 57	char name[MAX_TEST];
 58	enum CInemaStatus status;
 59	unsigned failedFrames;
 60	uint64_t failedPixels;
 61	unsigned totalFrames;
 62	uint64_t totalDistance;
 63	uint64_t totalPixels;
 64};
 65
 66struct CInemaImage {
 67	void* data;
 68	unsigned width;
 69	unsigned height;
 70	unsigned stride;
 71};
 72
 73DECLARE_VECTOR(CInemaTestList, struct CInemaTest)
 74DEFINE_VECTOR(CInemaTestList, struct CInemaTest)
 75
 76DECLARE_VECTOR(ImageList, void*)
 77DEFINE_VECTOR(ImageList, void*)
 78
 79static bool showVersion = false;
 80static bool showUsage = false;
 81static char base[PATH_MAX] = {0};
 82static char outdir[PATH_MAX] = {'.'};
 83static bool dryRun = false;
 84static bool diffs = false;
 85static bool rebaseline = false;
 86static int verbosity = 0;
 87
 88bool CInemaTestInit(struct CInemaTest*, const char* directory, const char* filename);
 89void CInemaTestRun(struct CInemaTest*, struct Table* configTree);
 90
 91bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* value);
 92void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core);
 93
 94static void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args);
 95
 96ATTRIBUTE_FORMAT(printf, 2, 3) void CIlog(int minlevel, const char* format, ...) {
 97	if (verbosity < minlevel) {
 98		return;
 99	}
100	va_list args;
101	va_start(args, format);
102	vprintf(format, args);
103	va_end(args);
104}
105
106ATTRIBUTE_FORMAT(printf, 2, 3) void CIerr(int minlevel, const char* format, ...) {
107	if (verbosity < minlevel) {
108		return;
109	}
110	va_list args;
111	va_start(args, format);
112	vfprintf(stderr, format, args);
113	va_end(args);
114}
115
116static bool parseCInemaArgs(int argc, char* const* argv) {
117	int ch;
118	int index = 0;
119	while ((ch = getopt_long(argc, argv, shortOpts, longOpts, &index)) != -1) {
120		const struct option* opt = &longOpts[index];
121		switch (ch) {
122		case '\0':
123			if (strcmp(opt->name, "version") == 0) {
124				showVersion = true;
125			} else {
126				return false;
127			}
128			break;
129		case 'b':
130			strncpy(base, optarg, sizeof(base));
131			// TODO: Verify path exists
132			break;
133		case 'd':
134			diffs = true;
135			break;
136		case 'h':
137			showUsage = true;
138			break;
139		case 'n':
140			dryRun = true;
141			break;
142		case 'o':
143			strncpy(outdir, optarg, sizeof(outdir));
144			// TODO: Make directory
145			break;
146		case 'q':
147			--verbosity;
148			break;
149		case 'r':
150			rebaseline = true;
151			break;
152		case 'v':
153			++verbosity;
154			break;
155		default:
156			return false;
157		}
158	}
159
160	return true;
161}
162
163static void usageCInema(const char* arg0) {
164	printf("usage: %s [-dhnqv] [-b BASE] [-o DIR] [--version] [test...]\n", arg0);
165	puts("  -b, --base [BASE]          Path to the CInema base directory");
166	puts("  -d, --diffs                Output image diffs from failures");
167	puts("  -h, --help                 Print this usage and exit");
168	puts("  -n, --dry-run              List all collected tests instead of running them");
169	puts("  -o, --output [DIR]         Path to output applicable results");
170	puts("  -q, --quiet                Decrease log verbosity (can be repeated)");
171	puts("  -r, --rebaseline           Rewrite the baseline for failing tests");
172	puts("  -v, --verbose              Increase log verbosity (can be repeated)");
173	puts("  --version                  Print version and exit");
174}
175
176static bool determineBase(int argc, char* const* argv) {
177	// TODO: Better dynamic detection
178	separatePath(__FILE__, base, NULL, NULL);
179	strncat(base, PATH_SEP ".." PATH_SEP ".." PATH_SEP ".." PATH_SEP "cinema", sizeof(base) - strlen(base) - 1);
180	return true;
181}
182
183static bool collectTests(struct CInemaTestList* tests, const char* path) {
184	CIerr(2, "Considering path %s\n", path);
185	struct VDir* dir = VDirOpen(path);
186	if (!dir) {
187		return false;
188	}
189	struct VDirEntry* entry = dir->listNext(dir);
190	while (entry) {
191		char subpath[PATH_MAX];
192		snprintf(subpath, sizeof(subpath), "%s" PATH_SEP "%s", path, entry->name(entry));
193		if (entry->type(entry) == VFS_DIRECTORY && strncmp(entry->name(entry), ".", 2) != 0 && strncmp(entry->name(entry), "..", 3) != 0) {
194			if (!collectTests(tests, subpath)) {
195				dir->close(dir);
196				return false;
197			}
198		} else if (entry->type(entry) == VFS_FILE && strncmp(entry->name(entry), "test.", 5) == 0) {
199			CIerr(3, "Found potential test %s\n", subpath);
200			struct VFile* vf = dir->openFile(dir, entry->name(entry), O_RDONLY);
201			if (vf) {
202				if (mCoreIsCompatible(vf) != PLATFORM_NONE || mVideoLogIsCompatible(vf) != PLATFORM_NONE) {
203					struct CInemaTest* test = CInemaTestListAppend(tests);
204					if (!CInemaTestInit(test, path, entry->name(entry))) {
205						CIerr(3, "Failed to create test\n");
206						CInemaTestListResize(tests, -1);
207					} else {
208						CIerr(2, "Found test %s\n", test->name);
209					}
210				} else {
211					CIerr(3, "Not a compatible file\n");
212				}
213				vf->close(vf);
214			} else {
215				CIerr(3, "Failed to open file\n");
216			}
217		}
218		entry = dir->listNext(dir);
219	}
220	dir->close(dir);
221	return true;
222}
223
224static int _compareNames(const void* a, const void* b) {
225	const struct CInemaTest* ta = a;
226	const struct CInemaTest* tb = b;
227
228	return strncmp(ta->name, tb->name, sizeof(ta->name));
229}
230
231static void reduceTestList(struct CInemaTestList* tests) {
232	qsort(CInemaTestListGetPointer(tests, 0), CInemaTestListSize(tests), sizeof(struct CInemaTest), _compareNames);
233
234	size_t i;
235	for (i = 1; i < CInemaTestListSize(tests);) {
236		struct CInemaTest* cur = CInemaTestListGetPointer(tests, i);
237		struct CInemaTest* prev = CInemaTestListGetPointer(tests, i - 1);
238		if (strncmp(cur->name, prev->name, sizeof(cur->name)) != 0) {
239			++i;
240			continue;
241		}
242		CInemaTestListShift(tests, i, 1);
243	}
244}
245
246static void testToPath(const char* testName, char* path) {
247	strncpy(path, base, PATH_MAX);
248
249	bool dotSeen = true;
250	size_t i;
251	for (i = strlen(path); testName[0] && i < PATH_MAX; ++testName) {
252		if (testName[0] == '.') {
253			dotSeen = true;
254		} else {
255			if (dotSeen) {
256				strncpy(&path[i], PATH_SEP, PATH_MAX - i);
257				i += strlen(PATH_SEP);
258				dotSeen = false;
259				if (!i) {
260					break;
261				}
262			}
263			path[i] = testName[0];
264			++i;
265		}
266	}
267}
268
269static void _loadConfigTree(struct Table* configTree, const char* testName) {
270	char key[MAX_TEST];
271	strncpy(key, testName, sizeof(key) - 1);
272
273	struct mCoreConfig* config;
274	while (!(config = HashTableLookup(configTree, key))) {
275		char path[PATH_MAX];
276		config = malloc(sizeof(*config));
277		mCoreConfigInit(config, "cinema");
278		testToPath(key, path);
279		strncat(path, PATH_SEP, sizeof(path) - 1);
280		strncat(path, "config.ini", sizeof(path) - 1);
281		mCoreConfigLoadPath(config, path);
282		HashTableInsert(configTree, key, config);
283		char* pos = strrchr(key, '.');
284		if (pos) {
285			pos[0] = '\0';
286		} else if (key[0]) {
287			key[0] = '\0';
288		} else {
289			break;
290		}
291	}
292}
293
294static void _unloadConfigTree(const char* key, void* value, void* user) {
295	UNUSED(key);
296	UNUSED(user);
297	mCoreConfigDeinit(value);
298}
299
300static const char* _lookupValue(struct Table* configTree, const char* testName, const char* key) {
301	_loadConfigTree(configTree, testName);
302
303	char testKey[MAX_TEST];
304	strncpy(testKey, testName, sizeof(testKey) - 1);
305
306	struct mCoreConfig* config;
307	while (true) {
308		config = HashTableLookup(configTree, testKey);
309		if (!config) {
310			continue;
311		}
312		const char* str = ConfigurationGetValue(&config->configTable, "testinfo", key);
313		if (str) {
314			return str;
315		}
316		char* pos = strrchr(testKey, '.');
317		if (pos) {
318			pos[0] = '\0';
319		} else if (testKey[0]) {
320			testKey[0] = '\0';
321		} else {
322			break;
323		}
324	}
325	return NULL;
326}
327
328bool CInemaConfigGetUInt(struct Table* configTree, const char* testName, const char* key, unsigned* out) {
329	const char* charValue = _lookupValue(configTree, testName, key);
330	if (!charValue) {
331		return false;
332	}
333	char* end;
334	unsigned long value = strtoul(charValue, &end, 10);
335	if (*end) {
336		return false;
337	}
338	*out = value;
339	return true;
340}
341
342void CInemaConfigLoad(struct Table* configTree, const char* testName, struct mCore* core) {
343	_loadConfigTree(configTree, testName);
344
345	char testKey[MAX_TEST] = {0};
346	char* keyEnd = testKey;
347	const char* pos;
348	while (true) {
349		pos = strchr(testName, '.');
350		size_t maxlen = sizeof(testKey) - (keyEnd - testKey) - 1;
351		size_t len;
352		if (pos) {
353			len = pos - testName;
354		} else {
355			len = strlen(testName);
356		}
357		if (len > maxlen) {
358			len = maxlen;
359		}
360		strncpy(keyEnd, testName, len);
361		keyEnd += len;
362
363		struct mCoreConfig* config = HashTableLookup(configTree, testKey);
364		if (config) {
365			core->loadConfig(core, config);
366		}
367		if (!pos) {
368			break;
369		}
370		testName = pos + 1;
371		keyEnd[0] = '.';
372		++keyEnd;
373	}
374}
375
376bool CInemaTestInit(struct CInemaTest* test, const char* directory, const char* filename) {
377	if (strncmp(base, directory, strlen(base)) != 0) {
378		return false;
379	}
380	memset(test, 0, sizeof(*test));
381	strncpy(test->directory, directory, sizeof(test->directory) - 1);
382	strncpy(test->filename, filename, sizeof(test->filename) - 1);
383	directory += strlen(base) + 1;
384	strncpy(test->name, directory, sizeof(test->name) - 1);
385	char* str = strstr(test->name, PATH_SEP);
386	while (str) {
387		str[0] = '.';
388		str = strstr(str, PATH_SEP);
389	}
390	return true;
391}
392
393static bool _loadBaseline(struct VDir* dir, struct CInemaImage* image, size_t frame, enum CInemaStatus* status) {
394	char baselineName[32];
395	snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
396	struct VFile* baselineVF = dir->openFile(dir, baselineName, O_RDONLY);
397	if (!baselineVF) {
398		if (*status == CI_PASS) {
399			*status = CI_FAIL;
400		}
401		return false;
402	}
403
404	png_structp png = PNGReadOpen(baselineVF, 0);
405	png_infop info = png_create_info_struct(png);
406	png_infop end = png_create_info_struct(png);
407	if (!png || !info || !end || !PNGReadHeader(png, info)) {
408		PNGReadClose(png, info, end);
409		baselineVF->close(baselineVF);
410		CIerr(1, "Failed to load %s\n", baselineName);
411		*status = CI_ERROR;
412		return false;
413	}
414
415	unsigned pwidth = png_get_image_width(png, info);
416	unsigned pheight = png_get_image_height(png, info);
417	if (pheight != image->height || pwidth != image->width) {
418		PNGReadClose(png, info, end);
419		baselineVF->close(baselineVF);
420		CIlog(1, "Size mismatch for %s, expected %ux%u, got %ux%u\n", baselineName, pwidth, pheight, image->width, image->height);
421		if (*status == CI_PASS) {
422			*status = CI_FAIL;
423		}
424		return false;
425	}
426
427	image->data = malloc(pwidth * pheight * BYTES_PER_PIXEL);
428	if (!image->data) {
429		CIerr(1, "Failed to allocate baseline buffer\n");
430		*status = CI_ERROR;
431		PNGReadClose(png, info, end);
432		baselineVF->close(baselineVF);
433		return false;
434	}
435	if (!PNGReadPixels(png, info, image->data, pwidth, pheight, pwidth) || !PNGReadFooter(png, end)) {
436		CIerr(1, "Failed to read %s\n", baselineName);
437		*status = CI_ERROR;
438		free(image->data);
439		return false;
440	}
441	PNGReadClose(png, info, end);
442	baselineVF->close(baselineVF);
443	image->stride = pwidth;
444	return true;
445}
446
447static struct VDir* _makeOutDir(const char* testName) {
448	char path[PATH_MAX] = {0};
449	strncpy(path, outdir, sizeof(path) - 1);
450	char* pathEnd = path + strlen(path);
451	const char* pos;
452	while (true) {
453		pathEnd[0] = PATH_SEP[0];
454		++pathEnd;
455		pos = strchr(testName, '.');
456		size_t maxlen = sizeof(path) - (pathEnd - path) - 1;
457		size_t len;
458		if (pos) {
459			len = pos - testName;
460		} else {
461			len = strlen(testName);
462		}
463		if (len > maxlen) {
464			len = maxlen;
465		}
466		strncpy(pathEnd, testName, len);
467		pathEnd += len;
468
469		mkdir(path, 0777);
470
471		if (!pos) {
472			break;
473		}
474		testName = pos + 1;
475	}
476	return VDirOpen(path);
477}
478
479static void _writeImage(struct VFile* vf, const struct CInemaImage* image) {
480	png_structp png = PNGWriteOpen(vf);
481	png_infop info = PNGWriteHeader(png, image->width, image->height);
482	if (!PNGWritePixels(png, image->width, image->height, image->stride, image->data)) {
483		CIerr(0, "Could not write output image\n");
484	}
485	PNGWriteClose(png, info);
486
487	vf->close(vf);
488}
489
490static void _writeDiff(const char* testName, const struct CInemaImage* image, size_t frame, const char* type) {
491	struct VDir* dir = _makeOutDir(testName);
492	if (!dir) {
493		CIerr(0, "Could not open directory for %s\n", testName);
494		return;
495	}
496	char name[32];
497	snprintf(name, sizeof(name), "%s_%04" PRIz "u.png", type, frame);
498	struct VFile* vf = dir->openFile(dir, name, O_CREAT | O_TRUNC | O_WRONLY);
499	if (!vf) {
500		CIerr(0, "Could not open output file %s\n", name);
501		dir->close(dir);
502		return;
503	}
504	_writeImage(vf, image);
505	dir->close(dir);
506}
507
508static void _writeBaseline(struct VDir* dir, const struct CInemaImage* image, size_t frame) {
509	char baselineName[32];
510	snprintf(baselineName, sizeof(baselineName), "baseline_%04" PRIz "u.png", frame);
511	struct VFile* baselineVF = dir->openFile(dir, baselineName, O_CREAT | O_TRUNC | O_WRONLY);
512	if (baselineVF) {
513		_writeImage(baselineVF, image);
514	} else {
515		CIerr(0, "Could not open output file %s\n", baselineName);
516	}
517}
518
519void CInemaTestRun(struct CInemaTest* test, struct Table* configTree) {
520	unsigned ignore = 0;
521	CInemaConfigGetUInt(configTree, test->name, "ignore", &ignore);
522	if (ignore) {
523		test->status = CI_SKIP;
524		return;
525	}
526
527	struct VDir* dir = VDirOpen(test->directory);
528	if (!dir) {
529		CIerr(0, "Failed to open test directory\n");
530		test->status = CI_ERROR;
531		return;
532	}
533	struct VFile* rom = dir->openFile(dir, test->filename, O_RDONLY);
534	if (!rom) {
535		CIerr(0, "Failed to open test\n");
536		test->status = CI_ERROR;
537		return;
538	}
539	struct mCore* core = mCoreFindVF(rom);
540	if (!core) {
541		CIerr(0, "Failed to load test\n");
542		test->status = CI_ERROR;
543		rom->close(rom);
544		return;
545	}
546	if (!core->init(core)) {
547		CIerr(0, "Failed to init test\n");
548		test->status = CI_ERROR;
549		core->deinit(core);
550		return;
551	}
552	struct CInemaImage image;
553	core->desiredVideoDimensions(core, &image.width, &image.height);
554	ssize_t bufferSize = image.width * image.height * BYTES_PER_PIXEL;
555	image.data = malloc(bufferSize);
556	image.stride = image.width;
557	if (!image.data) {
558		CIerr(0, "Failed to allocate video buffer\n");
559		test->status = CI_ERROR;
560		core->deinit(core);
561	}
562	core->setVideoBuffer(core, image.data, image.stride);
563	mCoreConfigInit(&core->config, "cinema");
564
565	unsigned limit = 9999;
566	unsigned skip = 0;
567	unsigned fail = 0;
568
569	CInemaConfigGetUInt(configTree, test->name, "frames", &limit);
570	CInemaConfigGetUInt(configTree, test->name, "skip", &skip);
571	CInemaConfigGetUInt(configTree, test->name, "fail", &fail);
572	CInemaConfigLoad(configTree, test->name, core);
573
574	core->loadROM(core, rom);
575	core->rtc.override = RTC_FAKE_EPOCH;
576	core->rtc.value = 1200000000;
577	core->reset(core);
578
579	test->status = CI_PASS;
580
581	unsigned minFrame = core->frameCounter(core);
582	size_t frame;
583	for (frame = 0; frame < skip; ++frame) {
584		core->runFrame(core);
585	}
586	for (frame = 0; limit; ++frame, --limit) {
587		core->runFrame(core);
588		++test->totalFrames;
589		unsigned frameCounter = core->frameCounter(core);
590		if (frameCounter <= minFrame) {
591			break;
592		}
593		CIlog(3, "Test frame: %u\n", frameCounter);
594		core->desiredVideoDimensions(core, &image.width, &image.height);
595		uint8_t* diff = NULL;
596		struct CInemaImage expected = {
597			.data = NULL,
598			.width = image.width,
599			.height = image.height,
600			.stride = image.width,
601		};
602		if (_loadBaseline(dir, &expected, frame, &test->status)) {
603			uint8_t* testPixels = image.data;
604			uint8_t* expectPixels = expected.data;
605			size_t x;
606			size_t y;
607			int max = 0;
608			bool failed = false;
609			for (y = 0; y < image.height; ++y) {
610				for (x = 0; x < image.width; ++x) {
611					size_t pix = expected.stride * y + x;
612					size_t tpix = image.stride * y + x;
613					int testR = testPixels[tpix * 4 + 0];
614					int testG = testPixels[tpix * 4 + 1];
615					int testB = testPixels[tpix * 4 + 2];
616					int expectR = expectPixels[pix * 4 + 0];
617					int expectG = expectPixels[pix * 4 + 1];
618					int expectB = expectPixels[pix * 4 + 2];
619					int r = expectR - testR;
620					int g = expectG - testG;
621					int b = expectB - testB;
622					if (r | g | b) {
623						failed = true;
624						if (diffs && !diff) {
625							diff = calloc(expected.width * expected.height, BYTES_PER_PIXEL);
626						}
627						CIlog(3, "Frame %u failed at pixel %" PRIz "ux%" PRIz "u with diff %i,%i,%i (expected %02x%02x%02x, got %02x%02x%02x)\n",
628						    frameCounter, x, y, r, g, b,
629						    expectR, expectG, expectB,
630						    testR, testG, testB);
631						test->status = CI_FAIL;
632						if (r < 0) {
633							r = -r;
634						}
635						if (g < 0) {
636							g = -g;
637						}
638						if (b < 0) {
639							b = -b;
640						}
641
642						if (diff) {
643							if (r > max) {
644								max = r;
645							}
646							if (g > max) {
647								max = g;
648							}
649							if (b > max) {
650								max = b;
651							}
652							diff[pix * 4 + 0] = r;
653							diff[pix * 4 + 1] = g;
654							diff[pix * 4 + 2] = b;
655						}
656
657						test->totalDistance += r + g + b;
658						++test->failedPixels;
659					}
660				}
661			}
662			if (failed) {
663				++test->failedFrames;
664			}
665			test->totalPixels += image.height * image.width;
666			if (rebaseline && failed) {
667				_writeBaseline(dir, &image, frame);
668			}
669			if (diff) {
670				if (failed) {
671					struct CInemaImage outdiff = {
672						.data = diff,
673						.width = image.width,
674						.height = image.height,
675						.stride = image.width,
676					};
677
678					_writeDiff(test->name, &image, frame, "result");
679					_writeDiff(test->name, &expected, frame, "expected");
680					_writeDiff(test->name, &outdiff, frame, "diff");
681
682					for (y = 0; y < outdiff.height; ++y) {
683						for (x = 0; x < outdiff.width; ++x) {
684							size_t pix = outdiff.stride * y + x;
685							diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
686							diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
687							diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
688						}
689					}
690					_writeDiff(test->name, &outdiff, frame, "normalized");
691				}
692				free(diff);
693			}
694			free(expected.data);
695		} else if (test->status == CI_ERROR) {
696			break;
697		} else if (rebaseline) {
698			_writeBaseline(dir, &image, frame);
699		}
700	}
701
702	if (fail) {
703		if (test->status == CI_FAIL) {
704			test->status = CI_XFAIL;
705		} else if (test->status == CI_PASS) {
706			test->status = CI_XPASS;
707		}
708	}
709
710	free(image.data);
711	mCoreConfigDeinit(&core->config);
712	core->deinit(core);
713	dir->close(dir);
714}
715
716void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
717	UNUSED(log);
718	if (verbosity < 0) {
719		return;
720	}
721	int mask = mLOG_FATAL;
722	if (verbosity >= 1) {
723		mask |= mLOG_ERROR;
724	}
725	if (verbosity >= 2) {
726		mask |= mLOG_WARN;
727	}
728	if (verbosity >= 4) {
729		mask |= mLOG_INFO;
730	}
731	if (verbosity >= 5) {
732		mask |= mLOG_ALL;
733	}
734	if (!(mask & level)) {
735		return;
736	}
737
738	char buffer[256];
739	vsnprintf(buffer, sizeof(buffer), format, args);
740	CIerr(0, "[%s] %s\n", mLogCategoryName(category), buffer);
741}
742
743int main(int argc, char** argv) {
744	int status = 0;
745	if (!parseCInemaArgs(argc, argv)) {
746		status = 1;
747		goto cleanup;
748	}
749
750	if (showVersion) {
751		version(argv[0]);
752		goto cleanup;
753	}
754
755	if (showUsage) {
756		usageCInema(argv[0]);
757		goto cleanup;
758	}
759
760	argc -= optind;
761	argv += optind;
762
763	if (!base[0] && !determineBase(argc, argv)) {
764		CIlog(0, "Could not determine CInema test base. Please specify manually.");
765		status = 1;
766		goto cleanup;
767	}
768#ifndef _WIN32
769	char* rbase = realpath(base, NULL);
770	if (rbase) {
771		strncpy(base, rbase, PATH_MAX);
772		free(rbase);
773	}
774#endif
775
776	struct CInemaTestList tests;
777	CInemaTestListInit(&tests, 0);
778
779	struct mLogger logger = { .log = _log };
780	mLogSetDefaultLogger(&logger);
781
782	if (argc > 0) {
783		size_t i;
784		for (i = 0; i < (size_t) argc; ++i) {
785			char path[PATH_MAX + 1] = {0};
786			testToPath(argv[i], path);
787
788			if (!collectTests(&tests, path)) {
789				status = 1;
790				break;
791			}
792		}
793	} else if (!collectTests(&tests, base)) {
794		status = 1;
795	}
796
797	if (CInemaTestListSize(&tests) == 0) {
798		CIlog(1, "No tests found.");
799		status = 1;
800	} else {
801		reduceTestList(&tests);
802	}
803
804	struct Table configTree;
805	HashTableInit(&configTree, 0, free);
806
807	size_t i;
808	for (i = 0; i < CInemaTestListSize(&tests); ++i) {
809		struct CInemaTest* test = CInemaTestListGetPointer(&tests, i);
810		if (dryRun) {
811			CIlog(-1, "%s\n", test->name);
812		} else {
813			CIlog(1, "%s: ", test->name);
814			fflush(stdout);
815			CInemaTestRun(test, &configTree);
816			switch (test->status) {
817			case CI_PASS:
818				CIlog(1, "pass\n");
819				break;
820			case CI_FAIL:
821				status = 1;
822				CIlog(1, "fail\n");
823				break;
824			case CI_XPASS:
825				CIlog(1, "xpass\n");
826				break;
827			case CI_XFAIL:
828				CIlog(1, "xfail\n");
829				break;
830			case CI_SKIP:
831				CIlog(1, "skip\n");
832				break;
833			case CI_ERROR:
834				status = 1;
835				CIlog(1, "error\n");
836				break;
837			}
838			if (test->failedFrames) {
839				CIlog(2, "\tfailed frames: %u/%u (%1.3g%%)\n", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
840				CIlog(2, "\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
841				CIlog(2, "\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)\n", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
842			}
843		}
844	}
845
846	HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
847	HashTableDeinit(&configTree);
848	CInemaTestListDeinit(&tests);
849
850cleanup:
851	return status;
852}