harfbuzz/test/shape/run-tests.py
2025-03-28 16:34:12 -06:00

332 lines
10 KiB
Python
Executable file

#!/usr/bin/env python3
import sys, os, subprocess, hashlib
args = sys.argv[1:]
verbose = False
if args and args[0] == "-v":
verbose = True
args = args[1:]
if not args or args[0].find("hb-shape") == -1 or not os.path.exists(args[0]):
sys.exit("""First argument does not seem to point to usable hb-shape.""")
hb_shape, args = args[0], args[1:]
env = os.environ.copy()
env["LC_ALL"] = "C"
EXE_WRAPPER = os.environ.get("MESON_EXE_WRAPPER")
def open_shape_batch_process():
cmd = [hb_shape, "--batch"]
if EXE_WRAPPER:
cmd = [EXE_WRAPPER] + cmd
process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=sys.stdout,
env=env,
)
return process
shape_process = open_shape_batch_process()
no_glyph_names_process = None
def shape_cmd(command, shape_process, verbose=False):
global hb_shape
# (Re)start shaper if it is dead
if shape_process.poll() is not None:
shape_process = open_shape_batch_process()
if verbose:
print(hb_shape + " " + " ".join(command))
shape_process.stdin.write((";".join(command) + "\n").encode("utf-8"))
shape_process.stdin.flush()
return shape_process.stdout.readline().decode("utf-8").strip()
def plural(what):
if not what.endswith("s"):
what += "s"
return what
def whats_var_name(what):
return plural(what).replace("-", "_")
def supported_whats_var_name(what):
whats = whats_var_name(what)
return "supported_" + whats
def supported_whats(what):
return globals()[supported_whats_var_name(what)]
def all_whats_var_name(what):
whats = whats_var_name(what)
return "all_" + whats
def all_whats(what):
return globals()[all_whats_var_name(what)]
# Collect supported backends
for what in ["shaper", "face-loader", "font-funcs"]:
subcommand = "--list-" + plural(what)
cmd = [hb_shape, subcommand]
if EXE_WRAPPER:
cmd = [EXE_WRAPPER] + cmd
what_process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=sys.stdout,
env=env,
)
# Capture the output
what_list = what_process.communicate()[0].decode("utf-8").strip().split()
if what_process.returncode:
sys.exit(f"Failed to run: {hb_shape} {subcommand}")
whats = plural(what)
var_name = supported_whats_var_name(what)
globals()[var_name] = what_list
print(f"Supported {whats}: {what_list}")
# If running under Wine and not native dlls, make the respective shapers unavailable.
if os.environ.get("WINEPATH"):
overrides = os.environ.get("WINEDLLOVERRIDES", "").lower()
if "directwrite" in supported_shapers and overrides.find("dwrite") == -1:
supported_shapers.remove("directwrite")
print("Skipping DirectWrite shaper under Wine.")
if "uniscribe" in supported_shapers and overrides.find("usp10") == -1:
supported_shapers.remove("uniscribe")
print("Skipping Uniscribe shaper under Wine.")
passes = 0
fails = 0
skips = 0
if not len(args):
args = ["-"]
for filename in args:
if filename == "-":
print("Running tests from standard input")
else:
print("Running tests in " + filename)
if filename == "-":
f = sys.stdin
else:
f = open(filename, encoding="utf8")
# By default test all backends
for what in ["shaper", "face-loader", "font-funcs"]:
all_var_name = all_whats_var_name(what)
globals()[all_var_name] = supported_whats(what)
all_shapers = ["ot"] # But only 'ot' shaper
# Right now we only test the 'ot' shaper if nothing specified,
# but try all font-funcs unless overriden.
# Only 'ot' face-loader is tested.
for line in f:
comment = False
if line.startswith("#"):
comment = True
line = line[1:]
if line.startswith(" "):
if verbose:
print("#%s" % line)
continue
line = line.strip()
if not line:
continue
if line.startswith("@"):
# Directive
line = line.strip()
line = line.split("#")[0].strip()[1:]
consumed = False
for what in ["shaper", "face-loader", "font-funcs"]:
whats = plural(what)
if line.startswith(what) or line.startswith(whats):
command, values = line.split("=")
values = values.strip().split(",")
supported = supported_whats(what)
if command[-1] == "-":
# Exclude
values = [v for v in supported if v not in values]
else:
# Specify
values = [v for v in values if v in supported]
var_name = all_whats_var_name(what)
print(f"Setting {whats} to test to {values}")
globals()[var_name] = values
consumed = True
if consumed:
print(line)
continue
else:
print("Unrecognized directive: %s" % line, file=sys.stderr)
sys.exit(1)
fontfile, options, unicodes, glyphs_expected = line.split(";")
options = options.split()
if fontfile.startswith("/") or fontfile.startswith('"/'):
if os.name == "nt": # Skip on Windows
continue
fontfile, expected_hash = (fontfile.split("@") + [""])[:2]
try:
with open(fontfile, "rb") as ff:
if expected_hash:
actual_hash = hashlib.sha1(ff.read()).hexdigest().strip()
if actual_hash != expected_hash:
print(
"different version of %s found; Expected hash %s, got %s; skipping."
% (fontfile, expected_hash, actual_hash)
)
skips += 1
continue
except IOError:
print("%s not found, skip." % fontfile)
skips += 1
continue
else:
cwd = os.path.dirname(filename)
fontfile = os.path.normpath(os.path.join(cwd, fontfile))
if comment:
if verbose:
print('# %s "%s" --unicodes %s' % (hb_shape, fontfile, unicodes))
continue
skip_test = False
shaper = None
face_loader = None
font_funcs = None
new_options = []
it = iter(options)
for option in it:
consumed = False
for what in ["shaper", "face-loader", "font-funcs"]:
if option.startswith("--" + what):
try:
backend = option.split("=")[1]
except IndexError:
backend = next(it)
if backend not in supported_whats(what):
skips += 1
print(f"Skipping test with {what}={backend}.")
skip_test = True
break
what = what.replace("-", "_")
globals()[what] = backend
consumed = True
if not consumed:
new_options.append(option)
if skip_test:
break
if skip_test:
continue
options = new_options
for shaper in [shaper] if shaper else all_whats("shaper"):
for font_funcs in [font_funcs] if font_funcs else all_whats("font-funcs"):
extra_options = []
if shaper:
extra_options.append("--shaper=" + shaper)
if face_loader:
extra_options.append("--face-loader=" + face_loader)
if font_funcs:
extra_options.append("--font-funcs=" + font_funcs)
if glyphs_expected != "*":
extra_options.append("--verify")
extra_options.append("--unsafe-to-concat")
if verbose:
print(
"# shaper=%s face-loader=%s font-funcs=%s"
% (shaper, face_loader, font_funcs)
)
cmd = [fontfile] + ["--unicodes", unicodes] + options + extra_options
glyphs = shape_cmd(cmd, shape_process, verbose).strip()
if glyphs_expected == "*":
passes += 1
continue
final_glyphs = glyphs
final_glyphs_expected = glyphs_expected
if glyphs != glyphs_expected and glyphs.find("gid") != -1:
if not no_glyph_names_process:
no_glyph_names_process = open_shape_batch_process()
cmd2 = [fontfile] + ["--glyphs", "--no-glyph-names", glyphs]
final_glyphs = shape_cmd(cmd2, no_glyph_names_process).strip()
cmd2 = [fontfile] + [
"--glyphs",
"--no-glyph-names",
glyphs_expected,
]
final_glyphs_expected = shape_cmd(
cmd2, no_glyph_names_process
).strip()
# If the removal of glyph_ids failed, fail the test.
# https://github.com/harfbuzz/harfbuzz/issues/5169
if not final_glyphs_expected or final_glyphs != final_glyphs_expected:
print(hb_shape + " " + " ".join(cmd), file=sys.stderr)
print("Actual: " + glyphs, file=sys.stderr)
print("Expected: " + glyphs_expected, file=sys.stderr)
if final_glyphs != glyphs:
print(
"Actual (no glyph names): " + final_glyphs,
file=sys.stderr,
)
print(
"Expected (no glyph names): " + final_glyphs_expected,
file=sys.stderr,
)
fails += 1
else:
passes += 1
print(
"%d tests passed; %d failed; %d skipped." % (passes, fails, skips), file=sys.stderr
)
if not (fails + passes):
print("No tests ran.")
elif not (fails + skips):
print("All tests passed.")
if fails:
sys.exit(1)
elif passes:
sys.exit(0)
else:
sys.exit(77)