all repos — mgba @ 72c904cdedaa79eea800fe05a5cf5ad8e1c7ce96

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		CIerr(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->reset(core);
576
577	test->status = CI_PASS;
578
579	unsigned minFrame = core->frameCounter(core);
580	size_t frame;
581	for (frame = 0; frame < skip; ++frame) {
582		core->runFrame(core);
583	}
584	for (frame = 0; limit; ++frame, --limit) {
585		core->runFrame(core);
586		++test->totalFrames;
587		unsigned frameCounter = core->frameCounter(core);
588		if (frameCounter <= minFrame) {
589			break;
590		}
591		CIerr(3, "Test frame: %u\n", frameCounter);
592		core->desiredVideoDimensions(core, &image.width, &image.height);
593		uint8_t* diff = NULL;
594		struct CInemaImage expected = {
595			.data = NULL,
596			.width = image.width,
597			.height = image.height,
598			.stride = image.width,
599		};
600		if (_loadBaseline(dir, &expected, frame, &test->status)) {
601			uint8_t* testPixels = image.data;
602			uint8_t* expectPixels = expected.data;
603			size_t x;
604			size_t y;
605			int max = 0;
606			bool failed = false;
607			for (y = 0; y < image.height; ++y) {
608				for (x = 0; x < image.width; ++x) {
609					size_t pix = expected.stride * y + x;
610					size_t tpix = image.stride * y + x;
611					int testR = testPixels[tpix * 4 + 0];
612					int testG = testPixels[tpix * 4 + 1];
613					int testB = testPixels[tpix * 4 + 2];
614					int expectR = expectPixels[pix * 4 + 0];
615					int expectG = expectPixels[pix * 4 + 1];
616					int expectB = expectPixels[pix * 4 + 2];
617					int r = expectR - testR;
618					int g = expectG - testG;
619					int b = expectB - testB;
620					if (r | g | b) {
621						failed = true;
622						if (diffs && !diff) {
623							diff = calloc(expected.width * expected.height, BYTES_PER_PIXEL);
624						}
625						CIerr(3, "Frame %u failed at pixel %" PRIz "ux%" PRIz "u with diff %i,%i,%i (expected %02x%02x%02x, got %02x%02x%02x)\n",
626						    frameCounter, x, y, r, g, b,
627						    expectR, expectG, expectB,
628						    testR, testG, testB);
629						test->status = CI_FAIL;
630						if (r < 0) {
631							r = -r;
632						}
633						if (g < 0) {
634							g = -g;
635						}
636						if (b < 0) {
637							b = -b;
638						}
639
640						if (diff) {
641							if (r > max) {
642								max = r;
643							}
644							if (g > max) {
645								max = g;
646							}
647							if (b > max) {
648								max = b;
649							}
650							diff[pix * 4 + 0] = r;
651							diff[pix * 4 + 1] = g;
652							diff[pix * 4 + 2] = b;
653						}
654
655						test->totalDistance += r + g + b;
656						++test->failedPixels;
657					}
658				}
659			}
660			if (failed) {
661				++test->failedFrames;
662			}
663			test->totalPixels += image.height * image.width;
664			if (rebaseline && failed) {
665				_writeBaseline(dir, &image, frame);
666			}
667			if (diff) {
668				if (failed) {
669					struct CInemaImage outdiff = {
670						.data = diff,
671						.width = image.width,
672						.height = image.height,
673						.stride = image.width,
674					};
675
676					_writeDiff(test->name, &image, frame, "result");
677					_writeDiff(test->name, &expected, frame, "expected");
678					_writeDiff(test->name, &outdiff, frame, "diff");
679
680					for (y = 0; y < outdiff.height; ++y) {
681						for (x = 0; x < outdiff.width; ++x) {
682							size_t pix = outdiff.stride * y + x;
683							diff[pix * 4 + 0] = diff[pix * 4 + 0] * 255 / max;
684							diff[pix * 4 + 1] = diff[pix * 4 + 1] * 255 / max;
685							diff[pix * 4 + 2] = diff[pix * 4 + 2] * 255 / max;
686						}
687					}
688					_writeDiff(test->name, &outdiff, frame, "normalized");
689				}
690				free(diff);
691			}
692			free(expected.data);
693		} else if (test->status == CI_ERROR) {
694			break;
695		} else if (rebaseline) {
696			_writeBaseline(dir, &image, frame);
697		}
698	}
699
700	if (fail) {
701		if (test->status == CI_FAIL) {
702			test->status = CI_XFAIL;
703		} else if (test->status == CI_PASS) {
704			test->status = CI_XPASS;
705		}
706	}
707
708	free(image.data);
709	mCoreConfigDeinit(&core->config);
710	core->deinit(core);
711	dir->close(dir);
712}
713
714void _log(struct mLogger* log, int category, enum mLogLevel level, const char* format, va_list args) {
715	// TODO: Write
716}
717
718int main(int argc, char** argv) {
719	int status = 0;
720	if (!parseCInemaArgs(argc, argv)) {
721		status = 1;
722		goto cleanup;
723	}
724
725	if (showVersion) {
726		version(argv[0]);
727		goto cleanup;
728	}
729
730	if (showUsage) {
731		usageCInema(argv[0]);
732		goto cleanup;
733	}
734
735	argc -= optind;
736	argv += optind;
737
738	if (!base[0] && !determineBase(argc, argv)) {
739		CIerr(0, "Could not determine CInema test base. Please specify manually.");
740		status = 1;
741		goto cleanup;
742	}
743#ifndef _WIN32
744	char* rbase = realpath(base, NULL);
745	strncpy(base, rbase, PATH_MAX);
746	free(rbase);
747#endif
748
749	struct CInemaTestList tests;
750	CInemaTestListInit(&tests, 0);
751
752	struct mLogger logger = { .log = _log };
753	mLogSetDefaultLogger(&logger);
754
755	if (argc > 0) {
756		size_t i;
757		for (i = 0; i < (size_t) argc; ++i) {
758			char path[PATH_MAX + 1] = {0};
759			testToPath(argv[i], path);
760
761			if (!collectTests(&tests, path)) {
762				status = 1;
763				break;
764			}
765		}
766	} else if (!collectTests(&tests, base)) {
767		status = 1;
768	}
769
770	if (CInemaTestListSize(&tests) == 0) {
771		CIerr(1, "No tests found.");
772		status = 1;
773	} else {
774		reduceTestList(&tests);
775	}
776
777	struct Table configTree;
778	HashTableInit(&configTree, 0, free);
779
780	size_t i;
781	for (i = 0; i < CInemaTestListSize(&tests); ++i) {
782		struct CInemaTest* test = CInemaTestListGetPointer(&tests, i);
783		if (dryRun) {
784			CIlog(-1, "%s\n", test->name);
785		} else {
786			CIerr(1, "%s: ", test->name);
787			CInemaTestRun(test, &configTree);
788			switch (test->status) {
789			case CI_PASS:
790				CIerr(1, "pass");
791				break;
792			case CI_FAIL:
793				status = 1;
794				CIerr(1, "fail");
795				break;
796			case CI_XPASS:
797				CIerr(1, "xpass");
798				break;
799			case CI_XFAIL:
800				CIerr(1, "xfail");
801				break;
802			case CI_SKIP:
803				CIerr(1, "skip");
804				break;
805			case CI_ERROR:
806				status = 1;
807				CIerr(1, "error");
808				break;
809			}
810			if (test->failedFrames) {
811				CIerr(2, "\n\tfailed frames: %u/%u (%1.3g%%)", test->failedFrames, test->totalFrames, test->failedFrames / (test->totalFrames * 0.01));
812				CIerr(2, "\n\tfailed pixels: %" PRIu64 "/%" PRIu64 " (%1.3g%%)", test->failedPixels, test->totalPixels, test->failedPixels / (test->totalPixels * 0.01));
813				CIerr(2, "\n\tdistance: %" PRIu64 "/%" PRIu64 " (%1.3g%%)", test->totalDistance, test->totalPixels * 765, test->totalDistance / (test->totalPixels * 7.65));
814			}
815
816			CIerr(1, "\n");
817		}
818	}
819
820	HashTableEnumerate(&configTree, _unloadConfigTree, NULL);
821	HashTableDeinit(&configTree);
822	CInemaTestListDeinit(&tests);
823
824cleanup:
825	return status;
826}