mirror of
https://github.com/syoyo/tinygltf.git
synced 2026-08-16 08:09:51 +00:00
Cross-version verifier comparing v3 C parser against v1 ground truth
Adds a structured DIGEST block (asset, buffers w/ FNV-1a hash, bufferViews,
accessors w/ min/max, mesh primitives w/ sorted attribute maps, nodes w/
normalized TRS+matrix, materials, textures/samplers/images, skins,
animations, cameras, scenes) emitted by both loader_example (v1) and
tester_v3_c (v3 C, now accepting a file arg). test_runner.py runs both,
diffs the digests, and reports counts/digest mismatches with v1 as truth.
Also rolls in /simplify follow-ups on top of d2837e3: a shared
tg3__json_number_to_double helper to dedupe inline number coercions, a
collapsed fuzz_gltf_v3_c harness using a single tg3_fuzz_run dispatcher,
a rewritten max_safe_uint64_real comment explaining the 53-bit mantissa
constraint, and a tests/Makefile fix so tester_v3_c is a real prerequisite
of `all` (built once via the dedicated rule, not duplicated).
Verifier passes 134/134 on the Khronos glTF-Sample-Models/2.0 suite.
bufferView.target and image.mime_type/uri are intentionally excluded from
the digest: v1 infers target from accessor usage and rewrites image
URIs/mime via stb_image, neither of which is a parse-fidelity concern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
172
test_runner.py
172
test_runner.py
@@ -2,63 +2,167 @@
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
## Simple test runner.
|
||||
## Cross-version verifier: parses each sample model with the mature v1
|
||||
## (loader_example) and the new v3 C tester, then compares both the COUNTS
|
||||
## summary line and the structured DIGEST block both binaries emit.
|
||||
## v1 is the ground truth.
|
||||
|
||||
# -- config -----------------------
|
||||
|
||||
# Absolute path pointing to your cloned git repo of https://github.com/KhronosGroup/glTF-Sample-Models
|
||||
sample_model_dir = "/home/syoyo/work/glTF-Sample-Models"
|
||||
sample_model_dir = "/mnt/nfs/syoyo/glTF-Sample-Models"
|
||||
base_model_dir = os.path.join(sample_model_dir, "2.0")
|
||||
|
||||
# Include `glTF-Draco` when you build `loader_example` with draco support.
|
||||
kinds = [ "glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"]
|
||||
v1_bin = "./loader_example"
|
||||
v3_bin = "./tests/tester_v3_c"
|
||||
|
||||
kinds = ["glTF", "glTF-Binary", "glTF-Embedded", "glTF-MaterialsCommon"]
|
||||
# ---------------------------------
|
||||
|
||||
failed = []
|
||||
success = []
|
||||
COUNTS_RE = re.compile(r"^COUNTS\s+(.*)$", re.MULTILINE)
|
||||
DIGEST_RE = re.compile(r"^DIGEST_BEGIN\n(.*?)^DIGEST_END$", re.MULTILINE | re.DOTALL)
|
||||
|
||||
def run(filename):
|
||||
|
||||
def parse_counts(output):
|
||||
m = COUNTS_RE.search(output)
|
||||
if not m:
|
||||
return None
|
||||
counts = {}
|
||||
for tok in m.group(1).split():
|
||||
if "=" not in tok:
|
||||
continue
|
||||
k, v = tok.split("=", 1)
|
||||
counts[k] = int(v)
|
||||
return counts
|
||||
|
||||
|
||||
def parse_digest(output):
|
||||
m = DIGEST_RE.search(output)
|
||||
if not m:
|
||||
return None
|
||||
return [line for line in m.group(1).splitlines() if line]
|
||||
|
||||
|
||||
def run_binary(binary, filename):
|
||||
p = subprocess.Popen(
|
||||
[binary, filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||
)
|
||||
out, err = p.communicate()
|
||||
return p.returncode, out.decode("utf-8", "replace"), err.decode("utf-8", "replace")
|
||||
|
||||
|
||||
def diff_digests(v1, v3, max_lines=20):
|
||||
"""Return a short summary of differences between two digest line lists."""
|
||||
diffs = []
|
||||
n = max(len(v1), len(v3))
|
||||
for i in range(n):
|
||||
a = v1[i] if i < len(v1) else "<missing>"
|
||||
b = v3[i] if i < len(v3) else "<missing>"
|
||||
if a != b:
|
||||
diffs.append(" v1[{0}]: {1}".format(i, a))
|
||||
diffs.append(" v3[{0}]: {1}".format(i, b))
|
||||
if len(diffs) >= max_lines * 2:
|
||||
diffs.append(" ... (truncated)")
|
||||
break
|
||||
return diffs
|
||||
|
||||
|
||||
parse_failed = [] # v3 returned non-zero or no COUNTS/DIGEST
|
||||
v1_skipped = [] # v1 returned non-zero or no COUNTS/DIGEST
|
||||
counts_diff = [] # counts disagree
|
||||
digest_diff = [] # digest disagrees
|
||||
ok = []
|
||||
|
||||
|
||||
def verify(filename):
|
||||
print("Testing: " + filename)
|
||||
cmd = ["./loader_example", filename]
|
||||
try:
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
(stdout, stderr) = p.communicate()
|
||||
except:
|
||||
print("Failed to execute: ", cmd)
|
||||
raise
|
||||
|
||||
if p.returncode != 0:
|
||||
failed.append(filename)
|
||||
print(stdout)
|
||||
print(stderr)
|
||||
else:
|
||||
success.append(filename)
|
||||
rc1, out1, err1 = run_binary(v1_bin, filename)
|
||||
c1 = parse_counts(out1) if rc1 == 0 else None
|
||||
d1 = parse_digest(out1) if rc1 == 0 else None
|
||||
if c1 is None or d1 is None:
|
||||
v1_skipped.append(filename)
|
||||
print(" v1 ground truth unavailable (rc={0}); skipping".format(rc1))
|
||||
return
|
||||
|
||||
rc3, out3, err3 = run_binary(v3_bin, filename)
|
||||
c3 = parse_counts(out3) if rc3 == 0 else None
|
||||
d3 = parse_digest(out3) if rc3 == 0 else None
|
||||
if c3 is None or d3 is None:
|
||||
parse_failed.append((filename, rc3, err3.strip()))
|
||||
print(" v3 FAILED (rc={0}): {1}".format(rc3, err3.strip()[:200]))
|
||||
return
|
||||
|
||||
cdiffs = []
|
||||
for k in sorted(set(c1) | set(c3)):
|
||||
if c1.get(k) != c3.get(k):
|
||||
cdiffs.append((k, c1.get(k), c3.get(k)))
|
||||
if cdiffs:
|
||||
counts_diff.append((filename, cdiffs))
|
||||
print(" COUNTS MISMATCH:")
|
||||
for k, a, b in cdiffs:
|
||||
print(" {0}: v1={1} v3={2}".format(k, a, b))
|
||||
return
|
||||
|
||||
if d1 != d3:
|
||||
diffs = diff_digests(d1, d3)
|
||||
digest_diff.append((filename, diffs))
|
||||
print(" DIGEST MISMATCH ({0} v1 lines, {1} v3 lines):".format(len(d1), len(d3)))
|
||||
for line in diffs[:8]:
|
||||
print(line)
|
||||
return
|
||||
|
||||
ok.append(filename)
|
||||
|
||||
|
||||
def test():
|
||||
|
||||
for d in os.listdir(base_model_dir):
|
||||
for d in sorted(os.listdir(base_model_dir)):
|
||||
p = os.path.join(base_model_dir, d)
|
||||
if os.path.isdir(p):
|
||||
for k in kinds:
|
||||
targetDir = os.path.join(p, k)
|
||||
g = glob.glob(targetDir + "/*.gltf") + glob.glob(targetDir + "/*.glb")
|
||||
for gltf in g:
|
||||
run(gltf)
|
||||
if not os.path.isdir(p):
|
||||
continue
|
||||
for k in kinds:
|
||||
targetDir = os.path.join(p, k)
|
||||
g = sorted(
|
||||
glob.glob(targetDir + "/*.gltf")
|
||||
+ glob.glob(targetDir + "/*.glb")
|
||||
)
|
||||
for gltf in g:
|
||||
verify(gltf)
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(v1_bin):
|
||||
sys.exit("error: v1 binary not found at {0}".format(v1_bin))
|
||||
if not os.path.exists(v3_bin):
|
||||
sys.exit("error: v3 binary not found at {0}".format(v3_bin))
|
||||
|
||||
test()
|
||||
|
||||
print("Success : {0}".format(len(success)))
|
||||
print("Failed : {0}".format(len(failed)))
|
||||
print("")
|
||||
print("=== Summary ===")
|
||||
print("OK : {0}".format(len(ok)))
|
||||
print("Counts diff : {0}".format(len(counts_diff)))
|
||||
print("Digest diff : {0}".format(len(digest_diff)))
|
||||
print("v3 failed : {0}".format(len(parse_failed)))
|
||||
print("v1 skipped : {0}".format(len(v1_skipped)))
|
||||
|
||||
for fail in failed:
|
||||
print("FAIL: " + fail)
|
||||
for f, diffs in counts_diff:
|
||||
print("COUNTS DIFF: " + f)
|
||||
for k, a, b in diffs:
|
||||
print(" {0}: v1={1} v3={2}".format(k, a, b))
|
||||
for f, diffs in digest_diff:
|
||||
print("DIGEST DIFF: " + f)
|
||||
for line in diffs:
|
||||
print(line)
|
||||
for f, rc, err in parse_failed:
|
||||
print("V3 FAIL: {0} (rc={1}) {2}".format(f, rc, err[:200]))
|
||||
|
||||
if __name__ == '__main__':
|
||||
if counts_diff or digest_diff or parse_failed:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user