| #!/usr/bin/env python3 | |
| import sys, os, subprocess, hashlib | |
| print("TAP version 14") | |
| 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=subprocess.PIPE, | |
| 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[0].poll() is not None: | |
| shape_process[0].stdin.close() | |
| shape_process[0].stdout.close() | |
| shape_process[0].stderr.close() | |
| shape_process[0] = open_shape_batch_process()[0] | |
| if verbose: | |
| print("# " + hb_shape + " " + " ".join(command)) | |
| shape_process[0].stdin.write((";".join(command) + "\n").encode("utf-8")) | |
| shape_process[0].stdin.flush() | |
| return shape_process[0].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.") | |
| number = 0 | |
| 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_face_loaders = os.environ.get("HB_FACE_LOADER", "ot").split(",") | |
| all_shapers = os.environ.get("HB_SHAPER_LIST", "ot").split(",") | |
| # 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: | |
| skips += 1 | |
| print( | |
| "# Different version of %s found; Expected hash %s, got %s; skipping." | |
| % (fontfile, expected_hash, actual_hash) | |
| ) | |
| continue | |
| except IOError: | |
| skips += 1 | |
| print("# %s not found, skip." % fontfile) | |
| 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 | |
| number += 1 | |
| print( | |
| f"ok {number} - {fontfile} # skip {what}={backend} not supported" | |
| ) | |
| 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 face_loader in [face_loader] if face_loader else all_whats("face-loader"): | |
| for shaper in [shaper] if shaper else all_whats("shaper"): | |
| all_font_funcs_for_shaper = ( | |
| all_whats("font-funcs") if shaper == "ot" else [None] | |
| ) | |
| for font_funcs in ( | |
| [font_funcs] if font_funcs else all_font_funcs_for_shaper | |
| ): | |
| number += 1 | |
| 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") | |
| extra_options.append("--safe-to-insert-tatweel") | |
| 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 | |
| print(f"ok {number} - {fontfile}") | |
| 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 | |
| ): | |
| fails += 1 | |
| cmd = hb_shape + " " + " ".join(cmd) | |
| print(f"not ok {number} - {cmd}") | |
| print(" ---", file=sys.stderr) | |
| print(' test_file: "' + filename + '"', file=sys.stderr) | |
| print(' cmd: "' + cmd + '"', file=sys.stderr) | |
| print(' actual: "' + glyphs + '"', file=sys.stderr) | |
| print(' expected: "' + glyphs_expected + '"', file=sys.stderr) | |
| if final_glyphs != glyphs: | |
| print( | |
| ' actual_gids: "' + final_glyphs + '"', | |
| file=sys.stderr, | |
| ) | |
| print( | |
| ' expected_gids: "' + final_glyphs_expected + '"', | |
| file=sys.stderr, | |
| ) | |
| print(" ...", file=sys.stderr) | |
| else: | |
| cmd = hb_shape + " " + " ".join(cmd) | |
| passes += 1 | |
| print(f"ok {number} - {cmd}") | |
| print("# %d tests passed; %d failed; %d skipped." % (passes, fails, skips)) | |
| if not (fails + passes): | |
| print("# No tests ran.") | |
| elif not (fails + skips): | |
| print("# All tests passed.") | |
| print("1..%d" % number) |