#!/usr/bin/env python3 import asyncio import itertools import subprocess import time import re import os import sys import shutil import functools import argparse import copy from typing import NamedTuple parser = argparse.ArgumentParser(description="Run ufbx tests") parser.add_argument("tests", type=str, nargs="*", help="Names of tests to run") parser.add_argument("--additional-compiler", default=[], action="append", help="Additional compiler(s) to use") parser.add_argument("--remove-compiler", default=[], action="append", help="Remove a compiler from being used") parser.add_argument("--remove-arch", default=[], action="append", help="Remove an architecture") parser.add_argument("--compiler", default=[], action="append", help="Specify exact compiler(s) to use") parser.add_argument("--define", default=[], action="append", help="Define preprocessor symbols") parser.add_argument("--no-sse", action="store_true", help="Don't make SSE builds when possible") parser.add_argument("--wasi-sdk", default=[], action="append", help="WASI SDK path") parser.add_argument("--wasm-runtime", default="wasmtime", type=str, help="WASM runtime command") parser.add_argument("--no-sanitize", action="store_true", help="Don't compile builds with ASAN/UBSAN") parser.add_argument("--no-sanitize-arch", default=[], action="append", help="Disable sanitization on specific architectures") parser.add_argument("--threads", type=int, default=0, help="Number of threads to use (0 to autodetect)") parser.add_argument("--verbose", action="store_true", help="Verbose output") parser.add_argument("--hash-file", help="Hash test input file") parser.add_argument("--runner", help="Descriptive name for the runner") parser.add_argument("--fail-on-pre-test", action="store_true", help="Indicate failure if pre-test checks fail") argv = parser.parse_args() color_out = sys.stdout has_color = False if sys.platform == "win32": try: import colorama colorama.init(wrap=False) color_out = colorama.AnsiToWin32(color_out).stream has_color = True except ImportError: print("# Run 'pip install colorama' for colorful output") else: has_color = sys.stdout.isatty() STYLE_FAIL = "\x1b[31m" STYLE_WARN = "\x1b[33m" STYLE_CMD = "\x1b[36m" def log(line, *, style=""): if style and has_color: line = style + line + "\x1b[39m" print(line, file=color_out, flush=True) def log_cmd(line): log(line, style=STYLE_CMD) def log_mkdir(path): log_cmd("mkdir " + path) def log_comment(line, fail=False, warn=False): style = "" if fail: style = STYLE_FAIL if warn: style = STYLE_WARN if sys.platform == "win32": log("rem " + line, style=style) else: log("# " + line, style=style) def flatten_str_list(str_list): """Flatten arbitrarily nested str list `item` to `dst`""" def inner(result, str_list): if isinstance(str_list, str): result.append(str_list) else: for s in str_list: inner(result, s) result = [] inner(result, str_list) return result if argv.threads > 0: num_threads = argv.threads else: num_threads = os.cpu_count() g_cmd_sema = None def get_cmd_sema(): global g_cmd_sema if not g_cmd_sema: g_cmd_sema = asyncio.Semaphore(num_threads) return g_cmd_sema async def run_cmd(*args, realtime_output=False, env=None, cwd=None): """Asynchronously run a command""" cmd_sema = get_cmd_sema() await cmd_sema.acquire() cmd_args = flatten_str_list(args) cmd = cmd_args[0] cmd_args = cmd_args[1:] exec_cmd = cmd if cwd: line_cmd = os.path.relpath(cmd, cwd) exec_cmd = os.path.abspath(cmd) cmdline = subprocess.list2cmdline([line_cmd] + cmd_args) if sys.platform == "win32": cmdline = f"pushd {cwd} & {cmdline} & popd" else: cmdline = f"pushd {cwd} ; {cmdline} ; popd" else: cmdline = subprocess.list2cmdline([cmd] + cmd_args) pipe = None if realtime_output else asyncio.subprocess.PIPE out = err = "" ok = False log_cmd(cmdline) begin = time.time() try: proc = await asyncio.create_subprocess_exec(exec_cmd, *cmd_args, stdout=pipe, stderr=pipe, env=env, cwd=cwd) if not realtime_output: out, err = await proc.communicate() out = out.decode("utf-8", errors="ignore").strip() err = err.decode("utf-8", errors="ignore").strip() ok = proc.returncode == 0 except FileNotFoundError: err = f"{cmd} not found" except OSError as e: err = str(e) end = time.time() cmd_sema.release() return ok, out, err, cmdline, end - begin def config_fmt_arch(config): arch = config["arch"] if config.get("san"): arch += "-san" return arch async def run_fail(message): return False, "", "CL: asan not supported", "", 0.0 class Compiler: def __init__(self, name, exe): self.name = name self.exe = exe self.env = None self.compile_archs = set() self.run_archs = set() def run(self, *args, **kwargs): return run_cmd(self.exe, args, env=self.env, **kwargs) class CLCompiler(Compiler): def __init__(self, name, exe): super().__init__(name, exe) self.has_c = True self.has_cpp = True async def check_version(self): _, out, err, _, _ = await self.run() m = re.search(r"Version ([.0-9]+) for (\w+)", out + err, re.M) if not m: return False self.arch = m.group(2).lower() self.version = m.group(1) return True def supported_archs(self): if self.arch == "x86": return ["x86"] if self.arch == "x64": return ["x64"] return [] def compile(self, config): if config.get("asan"): return run_fail("CL: asan not supported") if config.get("ubsan"): return run_fail("CL: ubsan not supported") sources = config["sources"] output = config["output"] args = [] args += sources args += ["/MT", "/nologo"] if not config.get("compile_only"): obj_dir = os.path.dirname(output) args.append(f"/Fo{obj_dir}\\") if config.get("warnings", False): args.append("/W4") args.append("/WX") else: args.append("/W3") if config.get("compile_only"): args.append("/c") if config.get("dev", True): args.append("/DUFBX_DEV") if config.get("optimize", False): args.append("/Ox") else: args.append("/DDEBUG=1") if config.get("regression", False): args.append("/DUFBX_REGRESSION=1") for key, val in config.get("defines", {}).items(): if not val: args.append(f"/D{key}") else: args.append(f"/D{key}={val}") if config.get("openmp", False): args.append("/openmp") if config.get("cpp", False): args.append("/EHsc") if config.get("sse", False): args.append("/DSSE=1") if config.get("compile_only"): args.append(f"/Fo\"{output}\"") else: args.append("/link") args += ["/opt:ref"] args.append(f"-out:{output}") return self.run(args) class GCCCompiler(Compiler): def __init__(self, name, exe, cpp, has_m32=True): super().__init__(name, exe) self.has_c = not cpp self.has_cpp = cpp self.has_m32 = has_m32 self.sysroot = "" async def check_version(self): _, vout, _, _, _ = await self.run("-dumpversion") _, mout, _, _, _ = await self.run("-dumpmachine") if not (vout and mout): return False self.version = vout self.arch = mout.lower() return True def supported_archs_raw(self): if "x86_64" in self.arch: return ["x86", "x64"] if self.has_m32 else ["x64"] if "i686" in self.arch: return ["x86"] if "arm-" in self.arch or "armv7l-" in self.arch or "armv7b-" in self.arch: return ["arm32"] if "aarch64" in self.arch: return ["arm64"] if "wasm32" in self.arch: return ["wasm32"] return [] def supported_archs(self): raw_archs = self.supported_archs_raw() archs = [*raw_archs] archs += (a + "-san" for a in raw_archs if a != "wasm32") return archs def modify_compile_args(self, config, args): pass def compile(self, config): sources = config["sources"] output = config["output"] args = [] if self.sysroot: args += ["--sysroot", self.sysroot] if config.get("warnings", False): args += ["-Wall", "-Wextra", "-Wsign-conversion", "-Wmissing-prototypes"] if self.has_cpp: args += ["-Wconversion-null"] args += ["-Werror"] args.append("-g") if config.get("optimize", False): if config.get("san", False): args.append("-O0") else: args.append("-O2") args.append("-DNDEBUG=1") if config.get("regression", False): args.append("-DUFBX_REGRESSION=1") if config.get("openmp", False): args.append("-openmp") if self.has_m32 and config.get("arch", "") == "x86": args.append("-m32") if config.get("compile_only"): args.append("-c") if self.has_cpp: std = "c++11" else: std = "gnu99" std = config.get("std", std) args.append(f"-std={std}") if config.get("dev", True): args.append("-DUFBX_DEV") use_sse = False if config.get("sse", False): args.append("-DSSE=1") args += ["-mbmi", "-msse3", "-msse4.1", "-msse4.2"] if config.get("arch", "") == "x86": use_sse = True if config.get("arch", "") == "x86" and config.get("ieee754", False): use_sse = True args += ["-mfpmath=sse"] if config.get("ieee754", False): args += ["-ffp-contract=off"] if use_sse: args += ["-msse", "-msse2"] if config.get("threads", False): args.append("-pthread") if config.get("stack_protector", False): args.append("-fstack-protector") if config.get("san"): args.append("-fsanitize=address") if sys.platform != "darwin": args.append("-fsanitize=leak") args.append("-fsanitize=undefined") args.append("-fno-sanitize=float-cast-overflow") args.append("-fno-sanitize-recover=undefined") args.append("-DUFBX_UBSAN") if "mingw" in self.arch: args.append("-D__USE_MINGW_ANSI_STDIO=1") for key, val in config.get("defines", {}).items(): if not val: args.append(f"-D{key}") else: args.append(f"-D{key}={val}") args += sources if "msvc" not in self.arch and not config.get("compile_only"): args.append("-lm") args += ["-o", output] self.modify_compile_args(config, args) return self.run(args) class ClangCompiler(GCCCompiler): def __init__(self, name, exe, cpp, **kwargs): super().__init__(name, exe, cpp, **kwargs) class TCCCompiler(GCCCompiler): def __init__(self, name, exe): super().__init__(name, exe, False) self.has_c = True self.has_cpp = False self.has_m32 = True self.sysroot = "" async def check_version(self): _, out, err, _, _ = await self.run("-v") print(out + err) m = re.search(r"tcc version ([.0-9]+) \((\w+).*\)", out + err, re.M) if not m: return False self.arch = m.group(2).lower() self.version = m.group(1) return True def supported_archs(self): if "x86_64" in self.arch: return ["x86", "x64"] if self.has_m32 else ["x64"] if "i686" in self.arch: return ["x86"] return [] class EmscriptenCompiler(ClangCompiler): def __init__(self, name, exe, cpp): super().__init__(name, exe, cpp) class WasiCompiler(ClangCompiler): def __init__(self, name, exe, cpp, sysroot): super().__init__(name, exe, cpp) self.sysroot = sysroot def modify_compile_args(self, config, args): args.append("-Wl,--stack-first") class ZigCompiler(ClangCompiler): def __init__(self, name, exe, cpp): super().__init__(name, exe, cpp) @functools.lru_cache(8) def get_vcvars(bat_name): vswhere_path = r"%ProgramFiles(x86)%/Microsoft Visual Studio/Installer/vswhere.exe" vswhere_path = os.path.expandvars(vswhere_path) if not os.path.exists(vswhere_path): raise EnvironmentError("vswhere.exe not found at: %s", vswhere_path) vs_path = os.popen('"{}" -latest -property installationPath'.format(vswhere_path)).read().rstrip() vsvars_path = os.path.join(vs_path, f"VC\\Auxiliary\\Build\\{bat_name}") output = os.popen('"{}" && set'.format(vsvars_path)).read() env = { } for line in output.splitlines(): items = tuple(line.split("=", 1)) if len(items) == 2: env[items[0]] = items[1] return env class VsCompiler(Compiler): def __init__(self, name, bat, inner): super().__init__(name, inner.exe) self.bat = bat self.inner = inner async def check_version(self): try: env = get_vcvars(self.bat) self.inner.env = env for key, value in env.items(): if key.upper() != "PATH": continue for path in value.split(";"): cl_path = os.path.join(path, self.exe) if os.path.exists(cl_path): self.inner.exe = cl_path if await self.inner.check_version(): self.exe = self.inner.exe self.arch = self.inner.arch self.version = self.inner.version self.has_c = self.inner.has_c self.has_cpp = self.inner.has_cpp return True return False except: return False def supported_archs(self): return self.inner.supported_archs() def supports_config(self, config): return self.inner.supports_config(config) def compile(self, config): return self.inner.compile(config) def supported_archs(compiler): archs = compiler.supported_archs() if argv.no_sanitize: archs = [a for a in archs if not a.endswith("-san")] if argv.no_sanitize_arch: archs = [a for a in archs if not a.endswith("-san") or a[:-4] not in argv.no_sanitize_arch] return archs all_compilers = [ CLCompiler("cl", "cl.exe"), GCCCompiler("gcc", "gcc", False), GCCCompiler("gcc", "g++", True), ClangCompiler("clang", "clang", False), ClangCompiler("clang", "clang++", True), VsCompiler("vs_cl64", "vcvars64.bat", CLCompiler("cl", "cl.exe")), VsCompiler("vs_cl32", "vcvars32.bat", CLCompiler("cl", "cl.exe")), # VsCompiler("vs_clang64", "vcvars64.bat", ClangCompiler("clang", "clang.exe", False, has_m32=False)), # VsCompiler("vs_clang64", "vcvars64.bat", ClangCompiler("clang", "clang++.exe", True, has_m32=False)), # VsCompiler("vs_clang32", "vcvars32.bat", ClangCompiler("clang", "clang.exe", False, has_m32=False)), # VsCompiler("vs_clang32", "vcvars32.bat", ClangCompiler("clang", "clang++.exe", True, has_m32=False)), # EmscriptenCompiler("emcc", emcc", False), # EmscriptenCompiler("emcc", emcc++", True), ] ichain = itertools.chain.from_iterable for sdk in argv.wasi_sdk: path = os.path.join(sdk, "bin", "clang") cpp_path = os.path.join(sdk, "bin", "clang++") sysroot = os.path.join(sdk, "share", "wasi-sysroot") all_compilers += [ WasiCompiler("wasi_clang", path, False, sysroot), WasiCompiler("wasi_clang", cpp_path, True, sysroot), ] for desc in argv.additional_compiler: name = re.sub(r"[^A-Za-z0-9\-]", "", desc) if "clang" in desc: cpp = re.sub(r"clang", "clang++", desc, 1) all_compilers += [ ClangCompiler(name, desc, False), ClangCompiler(name, cpp, True), ] elif "gcc" in desc: cpp = re.sub(r"gcc", "g++", desc, 1) all_compilers += [ GCCCompiler(name, desc, False), GCCCompiler(name, cpp, True), ] elif "tcc" in desc: all_compilers += [ TCCCompiler(name, desc), ] else: raise RuntimeError(f"Could not parse compiler for {desc}") if argv.remove_compiler: all_compilers = [c for c in all_compilers if c.name not in argv.remove_compiler] if argv.compiler: compilers = set(argv.compiler) all_compilers = [c for c in all_compilers if c.name in compilers] def gather(aws): return asyncio.gather(*aws) async def check_compiler(compiler): if await compiler.check_version(): return [compiler] else: return [] async def find_compilers(): return list(ichain(await gather(check_compiler(c) for c in all_compilers))) class Target: def __init__(self, name, suffix, compiler, config): self.name = name self.suffix = suffix self.compiler = compiler self.config = config self.skipped = False self.compiled = False self.ran = False self.ok = True self.log = [] async def compile_target(t): arch_test = t.config.get("arch_test", False) if config_fmt_arch(t.config) not in t.compiler.compile_archs and not arch_test: t.skipped = True return path = os.path.dirname(t.config["output"]) if not os.path.exists(path): os.makedirs(path, exist_ok=True) log_mkdir(path) ok, out, err, cmdline, time = await t.compiler.compile(t.config) t.log.append("$ " + cmdline) t.log.append(out) t.log.append(err) if argv.verbose: log("\n".join(["$ " + cmdline, out, err])) if ok: t.compiled = True else: t.ok = False head = f"Compile {t.name}" tail = f"[{time:.1f}s OK]" if ok else ("[WARN]" if arch_test else "[FAIL]") log_comment(f"{tail} {head}", fail=not ok and not arch_test, warn=not ok and arch_test) return t class RunOpts(NamedTuple): rerunnable: bool = False info: str = "" async def run_target(t, args, opts=RunOpts()): if not t.compiled: return arch_test = t.config.get("arch_test", False) if config_fmt_arch(t.config) not in t.compiler.run_archs and not arch_test: return args = args[:] if t.config.get("dedicated-allocs", False): args += ["--dedicated-allocs"] args = [a.replace("%TARGET_SUFFIX%", t.suffix) for a in args] cwd = t.config.get("cwd") if opts.rerunnable and not t.ok: return if t.config.get("arch") == "wasm32": wasm_args = [argv.wasm_runtime, "run", "--dir", ".", t.config["output"], "--"] wasm_args += args ok, out, err, cmdline, time = await run_cmd(wasm_args, cwd=cwd) else: ok, out, err, cmdline, time = await run_cmd(t.config["output"], args, cwd=cwd) if opts.rerunnable: if not t.ok: return t.log.clear() t.ran = False t.log.append("$ " + cmdline) t.log.append(out) t.log.append(err) if argv.verbose: log("\n".join(["$ " + cmdline, out, err])) if ok: t.ran = True else: t.ok = False head = f"Run {t.name}" if opts.info: head = f"{head} {opts.info}" tail = f"[{time:.1f}s OK]" if ok else ("[WARN]" if arch_test else "[FAIL]") log_comment(f"{tail} {head}", fail=not ok and not arch_test, warn=not ok and arch_test) return t async def compile_and_run_target(t, args): await compile_target(t) if args is not None: await run_target(t, args) return t async def run_viewer_target(target): log_comment(f"-- Running viewer with {target.name} --") for root, _, files in os.walk("data"): for file in files: if "_fail_" in file: continue if "\U0001F602" in file: continue path = os.path.join(root, file) if "fuzz" in path: continue if path.endswith(".fbx") or path.endswith(".obj"): target.log.clear() target.ran = False await run_target(target, [path]) if not target.ran: return def copy_file(src, dst): shutil.copy(src, dst) if sys.platform == "win32": log_cmd(f"copy {src} {dst}") else: log_cmd(f"cp {src} {dst}") exit_code = 0 def decorate_arch(compiler, arch): if arch not in compiler.compile_archs: return arch + " (FAIL)" if arch not in compiler.run_archs: return arch + " (compile only)" return arch tests = set(argv.tests) impicit_tests = False if not tests: tests = ["tests", "stack", "picort", "viewer", "domfuzz", "objfuzz", "readme", "threadcheck", "hashes"] implicit_tests = True async def main(): global exit_code log_comment(f"-- Running CI tests, using {num_threads} threads --") log_comment("-- Searching for compilers --") compilers = await find_compilers() all_configs = { "optimize": { "debug": { "optimize": False, "regression": True }, "release": { "optimize": True }, }, "arch": { "x86": { "arch": "x86" }, "x64": { "arch": "x64" }, "arm32": { "arch": "arm32" }, "arm64": { "arch": "arm64" }, "wasm32": { "arch": "wasm32" }, }, "sanitize": { "": { }, "sanitize": { "san": True, "dedicated-allocs": True }, }, } for arch in argv.remove_arch: del all_configs["arch"][arch] arch_configs = { "arch": all_configs["arch"], } arch_test_configs = { "arch": all_configs["arch"], "sanitize": all_configs["sanitize"], } build_path = "build" if not os.path.exists(build_path): os.makedirs(build_path, exist_ok=True) log_mkdir(build_path) def compile_permutations(prefix, config, config_options, run_args): opt_combos = [[(name, opt) for opt in opts] for name, opts in config_options.items()] opt_combos = list(itertools.product(*opt_combos)) is_cpp = config.get("cpp", False) is_c = not is_cpp for compiler in compilers: archs = set(supported_archs(compiler)) if is_c and not compiler.has_c: continue if is_cpp and not compiler.has_cpp: continue for opts in opt_combos: optstring = "_".join(opt for _,opt in opts if opt) suffix = f"{compiler.name}_{optstring}" name = f"{prefix}_{suffix}" path = os.path.join(build_path, name) conf = copy.deepcopy(config) conf["output"] = os.path.join(path, config.get("output", "a.exe")) if "defines" not in conf: conf["defines"] = { } if not conf.get("ignore_user_defines", False): for k in argv.define: v = "1" if "=" in k: k, v = k.split("=", 1) conf["defines"][k] = v for opt_name, opt in opts: conf.update(config_options[opt_name][opt]) overrides = conf.get("overrides") if overrides: overrides(conf, compiler) if conf.get("skip", False): continue if config_fmt_arch(conf) not in archs: continue target = Target(name, suffix, compiler, conf) yield compile_and_run_target(target, run_args) exe_suffix = "" obj_suffix = ".o" if sys.platform == "win32": exe_suffix = ".exe" obj_suffix = ".obj" ctest_tasks = [] ctest_config = { "sources": ["misc/compiler_test.c"], "output": "ctest" + exe_suffix, "arch_test": True, "ignore_user_defines": True, } ctest_tasks += compile_permutations("ctest", ctest_config, arch_test_configs, ["1.5"]) cpptest_config = { "sources": ["misc/compiler_test.cpp"], "output": "cpptest" + exe_suffix, "cpp": True, "arch_test": True, "ignore_user_defines": True, } ctest_tasks += compile_permutations("cpptest", cpptest_config, arch_test_configs, ["1.5"]) bad_compiles = 0 bad_runs = 0 compiler_test_tasks = await gather(ctest_tasks) for target in compiler_test_tasks: arch = config_fmt_arch(target.config) if target.compiled: target.compiler.compile_archs.add(arch) else: bad_compiles += 1 if target.ran and "sin(1.50) = 1.00" in target.log: target.compiler.run_archs.add(arch) elif target.compiled: bad_runs += 1 log_comment("-- Detected the following compilers --") removed_archs = set(argv.remove_arch) removed_archs.update(f"{arch}-san" for arch in argv.remove_arch) for compiler in compilers: archs = ", ".join(decorate_arch(compiler, arch) for arch in supported_archs(compiler) if arch not in removed_archs) log_comment(f" {compiler.exe}: {compiler.arch} {compiler.version} [{archs}]") num_fail = 0 all_targets = [] if "tests" in tests: log_comment("-- Compiling and running tests --") target_tasks = [] runner_config = { "sources": ["test/runner.c", "ufbx.c"], "output": "runner" + exe_suffix, } target_tasks += compile_permutations("runner", runner_config, all_configs, ["-d", "data"]) cpp_config = { "sources": ["misc/test_build.cpp"], "output": "cpp" + exe_suffix, "cpp": True, "warnings": True, } target_tasks += compile_permutations("cpp", cpp_config, arch_configs, []) cpp_no_dev_config = { "sources": ["misc/test_build.cpp"], "output": "cpp" + exe_suffix, "cpp": True, "warnings": True, "optimize": True, "dev": False, } target_tasks += compile_permutations("cpp_no_dev", cpp_no_dev_config, arch_configs, []) targets = await gather(target_tasks) all_targets += targets if "stack" in tests: log_comment("-- Compiling and running stack limited tests --") target_tasks = [] def debug_overrides(config, compiler): stack_limit = 128*1024 if sys.platform == "win32" and compiler.name in ["gcc"]: # GCC can't handle CreateThread on CI.. config["skip"] = True elif sys.platform == "win32" and compiler.name in ["clang"]: # TODO: Check what causes this stack usage stack_limit = 256*1024 config["defines"]["UFBXT_STACK_LIMIT"] = stack_limit debug_stack_config = { "sources": ["test/runner.c", "ufbx.c"], "output": "runner" + exe_suffix, "optimize": False, "threads": True, "stack_protector": True, "defines": { }, "overrides": debug_overrides, } target_tasks += compile_permutations("runner_debug_stack", debug_stack_config, arch_configs, ["-d", "data"]) def release_overrides(config, compiler): stack_limit = 64*1024 if sys.platform == "win32" and compiler.name in ["gcc"]: # GCC can't handle CreateThread on CI.. config["skip"] = True elif sys.platform == "win32" and compiler.name in ["clang", "vs_cl64"]: # TODO: Check what causes this stack usage stack_limit = 128*1024 elif config["arch"] == "arm64": # Fails with 'Failed to run thread with stack size of 65536 bytes' # with 64kiB stack.. stack_limit = 128*1024 config["defines"]["UFBXT_STACK_LIMIT"] = stack_limit release_stack_config = { "sources": ["test/runner.c", "ufbx.c"], "output": "runner" + exe_suffix, "optimize": True, "threads": True, "stack_protector": True, "defines": { }, "overrides": release_overrides, } target_tasks += compile_permutations("runner_release_stack", release_stack_config, arch_configs, ["-d", "data"]) targets = await gather(target_tasks) all_targets += targets if "features" in tests: log_comment("-- Compiling and running partial features --") feature_defines = [ "UFBX_NO_SUBDIVISION", "UFBX_NO_TESSELLATION", "UFBX_NO_GEOMETRY_CACHE", "UFBX_NO_SCENE_EVALUATION", "UFBX_NO_TRIANGULATION", "UFBX_NO_ERROR_STACK", ] target_tasks = [] for bits in range(1, 1 << len(feature_defines)): defines = { name: 1 for ix, name in enumerate(feature_defines) if (1 << ix) & bits } feature_config = { "sources": ["ufbx.c"], "output": f"features_{bits}" + obj_suffix, "warnings": True, "compile_only": True, "defines": defines, } target_tasks += compile_permutations("features", feature_config, arch_configs, None) targets = await gather(target_tasks) all_targets += targets if "picort" in tests: log_comment("-- Compiling and running picort --") target_tasks = [] picort_configs = { "arch": arch_configs["arch"], "sse": { "scalar": { "sse": False }, "sse": { "sse": True }, }, } picort_config = { "sources": ["ufbx.c", "examples/picort/picort.cpp"], "output": "picort" + exe_suffix, "cpp": True, "optimize": True, "std": "c++14", "threads": True, } target_tasks += compile_permutations("picort", picort_config, picort_configs, None) targets = await gather(target_tasks) all_targets += targets def target_score(target, want_sse): compiler = target.compiler config = target.config if not target.compiled: return (0, 0) score = 1 if config.get("sse", False) == want_sse: score += 100 if config["arch"] == "x64": score += 10 if "clang" in compiler.name: score += 10 if "msvc" in compiler.name: score += 5 version = re.search(r"\d+", compiler.version) version = int(version.group(0)) if version else 0 return (score, version) image_path = os.path.join("build", "images") if not os.path.exists(image_path): os.makedirs(image_path, exist_ok=True) log_mkdir(image_path) scalar_target = max(targets, key=lambda t: target_score(t, False)) sse_target = max(targets, key=lambda t: target_score(t, True)) if scalar_target.compiled and scalar_target != sse_target: log_comment(f"-- Rendering scenes with {scalar_target.name} --") scalar_target.log.clear() scalar_target.ran = False args = [ "data/picort/barbarian.picort.txt", "-o", "build/images/barbarian-scalar.png", "--samples", "64", "--error-threshold", "0.02", ] await run_target(scalar_target, args) if scalar_target.ran: for line in scalar_target.log[1].splitlines(keepends=False): log_comment(line) if sse_target.compiled: log_comment(f"-- Rendering scenes with {sse_target.name} --") scenes = [ "data/picort/barbarian.picort.txt", "data/picort/barbarian-big.picort.txt", "data/picort/slime-binary.picort.txt", "data/picort/slime-ascii.picort.txt", "data/picort/slime-big.picort.txt", ] for scene in scenes: sse_target.log.clear() sse_target.ran = False await run_target(sse_target, [scene]) if not sse_target.ran: break for line in sse_target.log[1].splitlines(keepends=False): log_comment(line) if "viewer" in tests: log_comment("-- Compiling and running viewer --") target_tasks = [] viewer_configs = { "arch": arch_configs["arch"], "sanitize": { "": { }, "sanitize": { "san": True }, } } viewer_config = { "sources": ["ufbx.c", "examples/viewer/viewer.c", "examples/viewer/external.c"], "output": "viewer" + exe_suffix, "optimize": True, "defines": { "TEST_VIEWER": "", }, } target_tasks += compile_permutations("viewer", viewer_config, viewer_configs, None) targets = await gather(target_tasks) all_targets += targets running_viewers = [] for target in targets: if target.compiled: running_viewers.append(run_viewer_target(target)) await gather(running_viewers) if "domfuzz" in tests: log_comment("-- Compiling and running domfuzz --") target_tasks = [] domfuzz_configs = { "arch": arch_configs["arch"], } domfuzz_config = { "sources": ["ufbx.c", "test/domfuzz/fbxdom.cpp", "test/domfuzz/domfuzz_main.cpp"], "output": "domfuzz" + exe_suffix, "cpp": True, "optimize": True, "std": "c++14", } target_tasks += compile_permutations("domfuzz", domfuzz_config, domfuzz_configs, None) targets = await gather(target_tasks) all_targets += targets def target_score(target): compiler = target.compiler config = target.config if not target.compiled: return (0, 0) score = 1 if config["arch"] == "x64": score += 10 if "clang" in compiler.name: score += 10 if "msvc" in compiler.name: score += 5 version = re.search(r"\d+", compiler.version) version = int(version.group(0)) if version else 0 return (score, version) target = max(targets, key=target_score) if target.compiled: log_comment(f"-- Running domfuzz with {target.name} --") too_heavy_files = [ "blender_293_barbarian_7400_binary", "maya_slime_7500_binary", "maya_character_6100_binary", "maya_character_7500_binary", "maya_human_ik_6100_binary", "maya_human_ik_7400_binary", "max2009_blob_6100_binary", "synthetic_unicode_7500_binary", "maya_lod_group_6100_binary", "maya_dq_weights_7500_binary", "maya_triangulate_triangulated_7500_binary", "maya_kenney_character_7700_binary", "maya_node_attribute_zoo_6100_binary", "max_physical_material_textures_6100_binary", ] run_tasks = [] for root, _, files in os.walk("data"): for file in files: if "_ascii" in file: continue if any(f in file for f in too_heavy_files): continue path = os.path.join(root, file) if "fuzz" in path: continue if path.endswith(".fbx"): target.log.clear() target.ran = False run_tasks.append(run_target(target, [path], RunOpts( rerunnable=True, info=path, ) )) targets = await gather(run_tasks) if "objfuzz" in tests: log_comment("-- Compiling and running objfuzz --") target_tasks = [] objfuzz_configs = { "arch": arch_configs["arch"], } objfuzz_config = { "sources": ["ufbx.c", "test/objfuzz.cpp"], "output": "objfuzz" + exe_suffix, "cpp": True, "optimize": True, "std": "c++14", } target_tasks += compile_permutations("objfuzz", objfuzz_config, objfuzz_configs, None) targets = await gather(target_tasks) all_targets += targets def target_score(target): compiler = target.compiler config = target.config if not target.compiled: return (0, 0) score = 1 if config["arch"] == "x64": score += 10 if "clang" in compiler.name: score += 10 if "msvc" in compiler.name: score += 5 version = re.search(r"\d+", compiler.version) version = int(version.group(0)) if version else 0 return (score, version) target = max(targets, key=target_score) if target.compiled: log_comment(f"-- Running objfuzz with {target.name} --") too_heavy_files = [ "synthetic_color_suzanne_0_obj.obj", "synthetic_color_suzanne_1_obj.obj", "zbrush_polygroup_mess_0_obj.obj", ] run_tasks = [] for root, _, files in os.walk("data"): for file in files: path = os.path.join(root, file) if any(f in file for f in too_heavy_files): continue if re.match(r"^.*_\d+_obj.obj$", file): target.log.clear() target.ran = False run_tasks.append(run_target(target, [path], RunOpts( rerunnable=True, info=path, ) )) elif re.match(r"^.*_\d+_mtl.mtl$", file): target.log.clear() target.ran = False run_tasks.append(run_target(target, ["--mtl", path], RunOpts( rerunnable=True, info=path, ) )) targets = await gather(run_tasks) if "readme" in tests: log_comment("-- Compiling and running README.md --") prologue = """ #include #include #include "../ufbx.h" void push_vertex(const ufbx_vec3 *position, const ufbx_vec3 *normal) { } void push_pose(const ufbx_matrix *matrix) { } int main(int argc, char **argv) { """ epilogue = """ return 0; } """ readme_dst = os.path.join(build_path, "readme.c") with open(readme_dst, "wt") as outf: for line in prologue.strip().splitlines(): print(line.strip(), file=outf) in_c = False with open("README.md", "rt") as inf: for line in inf: if line.strip() == "```c": in_c = True elif line.strip() == "```": in_c = False elif in_c: print(line.rstrip(), file=outf) for line in epilogue.strip().splitlines(): print(line.strip(), file=outf) readme_cpp_dst = os.path.join(build_path, "readme.cpp") shutil.copyfile(readme_dst, readme_cpp_dst) copy_file( os.path.join("data" ,"blender_279_default_7400_binary.fbx"), os.path.join(build_path, "thing.fbx")) target_tasks = [] readme_config = { "sources": ["build/readme.c", "ufbx.c"], "output": "readme" + exe_suffix, "cwd": "build", } target_tasks += compile_permutations("readme", readme_config, arch_configs, []) readme_cpp_config = { "sources": ["build/readme.cpp", "ufbx.c"], "output": "readme_cpp" + exe_suffix, "cpp": True, "cwd": "build", } target_tasks += compile_permutations("readme_cpp", readme_cpp_config, arch_configs, []) targets = await gather(target_tasks) all_targets += targets if "threadcheck" in tests: log_comment("-- Compiling and running threadcheck --") target_tasks = [] threadcheck_config = { "sources": ["ufbx.c", "test/threadcheck.cpp"], "output": "threadcheck" + exe_suffix, "cpp": True, "optimize": False, "std": "c++14", "threads": True, } target_tasks += compile_permutations("threadcheck", threadcheck_config, arch_configs, None) targets = await gather(target_tasks) all_targets += targets def target_score(target): compiler = target.compiler config = target.config if not target.compiled: return (0, 0) score = 1 if config["arch"] == "x64": score += 10 if "clang" in compiler.name: score += 10 if "msvc" in compiler.name: score += 5 version = re.search(r"\d+", compiler.version) version = int(version.group(0)) if version else 0 return (score, version) best_target = max(targets, key=target_score) if best_target.compiled: log_comment(f"-- Running {best_target.name} --") best_target.log.clear() best_target.ran = False await run_target(best_target, ["data/maya_cube_7500_binary.fbx", "2"]) for line in best_target.log[1].splitlines(keepends=False): log_comment(line) if "hashes" in tests: hash_file = argv.hash_file if not hash_file: hash_file = os.path.join(build_path, "hashes.txt") if hash_file and os.path.exists(hash_file): log_comment("-- Compiling and running hash_scene --") target_tasks = [] hash_scene_configs = { "arch": all_configs["arch"], "optimize": all_configs["optimize"], } hash_scene_config = { "sources": ["test/hash_scene.c", "misc/fdlibm.c"], "output": "hash_scene" + exe_suffix, "ieee754": True, } dump_path = os.path.join(build_path, "hashdumps") if not os.path.exists(dump_path): os.makedirs(dump_path, exist_ok=True) log_mkdir(dump_path) else: for dump_file in os.listdir(dump_path): assert dump_file.endswith(".txt") os.remove(os.path.join(dump_path, dump_file)) if argv.runner: dump_file = os.path.join(dump_path, f"{argv.runner}_%TARGET_SUFFIX%.txt") else: dump_file = os.path.join(dump_path, "%TARGET_SUFFIX%.txt") args = ["--check", hash_file, "--dump", dump_file, "--max-dump-errors", "3"] target_tasks += compile_permutations("hash_scene", hash_scene_config, hash_scene_configs, args) targets = await gather(target_tasks) all_targets += targets else: print("No hash file found, skipping hashes", file=sys.stderr) if not implicit_tests: exit_code = 2 for target in all_targets: if target.ok: continue print() log(f"-- FAIL: {target.name}", style=STYLE_FAIL) print("\n".join(target.log)) num_fail += 1 print() severity = "ERROR" if argv.fail_on_pre_test else "WARNING" if bad_compiles > 0: print(f"{severity}: {bad_compiles} pre-test configurations failed to compile") if argv.fail_on_pre_test: exit_code = 2 if bad_runs > 0: print(f"{severity}: {bad_runs} pre-test configurations failed to run") if argv.fail_on_pre_test: exit_code = 2 print(f"{len(all_targets) - num_fail}/{len(all_targets)} targets succeeded") if num_fail > 0: exit_code = 1 if __name__ == "__main__": if sys.platform == "win32": loop = asyncio.ProactorEventLoop() else: loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() sys.exit(exit_code)