| #!/usr/bin/env python3 | |
| import os | |
| import re | |
| import shlex | |
| import subprocess | |
| import sys | |
| import tempfile | |
| from difflib import unified_diff | |
| print("TAP version 14") | |
| args = sys.argv[1:] | |
| if ( | |
| not args | |
| or not os.path.exists(args[0]) | |
| or "hb-vector" not in args[0] | |
| ): | |
| sys.exit("First argument does not seem to point to usable hb-vector.") | |
| hb_vector, args = args[0], args[1:] | |
| if not args: | |
| args = ["-"] | |
| env = os.environ.copy() | |
| env["LC_ALL"] = "C" | |
| EXE_WRAPPER = os.environ.get("MESON_EXE_WRAPPER") | |
| def resolve_path(base, path): | |
| if os.path.isabs(path): | |
| return path | |
| return os.path.normpath(os.path.join(base, path)) | |
| def open_vector_batch_process(): | |
| cmd = [hb_vector, "--batch"] | |
| if EXE_WRAPPER: | |
| cmd = [EXE_WRAPPER] + cmd | |
| process = subprocess.Popen( | |
| cmd, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| env=env, | |
| text=True, | |
| ) | |
| return [process] | |
| def close_vector_batch_process(vector_process): | |
| process = vector_process[0] | |
| if process is None: | |
| return | |
| if process.stdin: | |
| process.stdin.close() | |
| if process.stdout: | |
| process.stdout.close() | |
| process.wait() | |
| vector_process[0] = None | |
| vector_process = open_vector_batch_process() | |
| def build_vector_command(font_path, options, unicodes, output_path): | |
| command = [] | |
| if options: | |
| command.extend(shlex.split(options)) | |
| command.extend([f"--output-file={output_path}", f"--unicodes={unicodes}", font_path]) | |
| return command | |
| def run_vector_single(command): | |
| cmd = [hb_vector] | |
| cmd.extend(command) | |
| if EXE_WRAPPER: | |
| cmd = [EXE_WRAPPER] + cmd | |
| return subprocess.run(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) | |
| def vector_cmd(command, vector_process): | |
| process = vector_process[0] | |
| # (Re)start vector tool if it is dead. | |
| if process.poll() is not None: | |
| if process.stdin: | |
| process.stdin.close() | |
| if process.stdout: | |
| process.stdout.close() | |
| vector_process[0] = open_vector_batch_process()[0] | |
| process = vector_process[0] | |
| process.stdin.write(";".join(command) + "\n") | |
| process.stdin.flush() | |
| diagnostics = [] | |
| while True: | |
| line = process.stdout.readline() | |
| if line == "": | |
| return None, diagnostics | |
| line = line.rstrip("\n") | |
| if line in ("success", "failure"): | |
| return line, diagnostics | |
| diagnostics.append(line) | |
| def run_vector(font_path, options, unicodes, output_path): | |
| command = build_vector_command(font_path, options, unicodes, output_path) | |
| status, diagnostics = vector_cmd(command, vector_process) | |
| if status == "success": | |
| return subprocess.CompletedProcess( | |
| args=command, | |
| returncode=0, | |
| stdout="", | |
| stderr="\n".join(diagnostics), | |
| ) | |
| # Fall back to one-shot execution for proper diagnostics. | |
| result = run_vector_single(command) | |
| if diagnostics: | |
| stderr = "\n".join(diagnostics) | |
| if result.stderr: | |
| stderr += "\n" + result.stderr | |
| result = subprocess.CompletedProcess( | |
| args=result.args, | |
| returncode=result.returncode, | |
| stdout=result.stdout, | |
| stderr=stderr, | |
| ) | |
| return result | |
| def parse_unicodes(s): | |
| # Match test/shape/hb_test_tools.py Unicode.parse semantics. | |
| s = re.sub(r"0[xX]", " ", s) | |
| s = re.sub(r"[<+\->{},;&#\\xXuUnNiI\n\t]", " ", s) | |
| try: | |
| return "".join(chr(int(x, 16)) for x in s.split()) | |
| except ValueError as e: | |
| raise ValueError(f"invalid unicode codepoint list: {s!r}") from e | |
| number = 0 | |
| fails = 0 | |
| try: | |
| for filename in args: | |
| if filename == "-": | |
| f = sys.stdin | |
| base = os.getcwd() | |
| print("# Running tests from standard input") | |
| else: | |
| f = open(filename, encoding="utf-8") | |
| base = os.path.dirname(os.path.abspath(filename)) | |
| print("# Running tests in " + filename) | |
| for lineno, line in enumerate(f, start=1): | |
| line = line.strip() | |
| if not line or line.startswith("#"): | |
| continue | |
| number += 1 | |
| fields = line.split(";", 3) | |
| if len(fields) != 4: | |
| fails += 1 | |
| print(f"not ok {number} - parse:{filename}:{lineno}") | |
| print("# malformed test line, expected 4 ';'-separated fields") | |
| continue | |
| font_file, options, unicodes, expected_file = fields | |
| name = expected_file | |
| font_path = resolve_path(base, font_file) | |
| expected_path = resolve_path(base, expected_file) | |
| if not os.path.exists(font_path): | |
| fails += 1 | |
| print(f"not ok {number} - {name}") | |
| print(f"# font file not found: {font_path}") | |
| continue | |
| if not os.path.exists(expected_path): | |
| fails += 1 | |
| print(f"not ok {number} - {name}") | |
| print(f"# expected output file not found: {expected_path}") | |
| continue | |
| try: | |
| parse_unicodes(unicodes) | |
| except ValueError as e: | |
| fails += 1 | |
| print(f"not ok {number} - {name}") | |
| print("# " + str(e)) | |
| continue | |
| suffix = os.path.splitext(expected_file)[1] | |
| with tempfile.NamedTemporaryFile(prefix="hb-vector-test-", suffix=suffix, delete=False) as tmp: | |
| output_path = tmp.name | |
| try: | |
| result = run_vector(font_path, options, unicodes, output_path) | |
| if result.returncode != 0: | |
| fails += 1 | |
| print(f"not ok {number} - {name}") | |
| print(f"# hb-vector exited with code {result.returncode}") | |
| stderr = result.stderr.strip() | |
| if stderr: | |
| for l in stderr.splitlines(): | |
| print("# " + l) | |
| continue | |
| if not os.path.exists(output_path): | |
| fails += 1 | |
| print(f"not ok {number} - {name}") | |
| print("# hb-vector did not create output file") | |
| continue | |
| with open(expected_path, encoding="utf-8", errors="surrogateescape") as fp: | |
| expected = fp.read() | |
| with open(output_path, encoding="utf-8", errors="surrogateescape") as fp: | |
| actual = fp.read() | |
| if actual == expected: | |
| print(f"ok {number} - {name}") | |
| continue | |
| fails += 1 | |
| print(f"not ok {number} - {name}") | |
| diff = unified_diff( | |
| expected.splitlines(keepends=True), | |
| actual.splitlines(keepends=True), | |
| fromfile=expected_path, | |
| tofile="actual", | |
| ) | |
| for diff_line in diff: | |
| print("# " + diff_line.rstrip("\n")) | |
| finally: | |
| if os.path.exists(output_path): | |
| os.unlink(output_path) | |
| if f is not sys.stdin: | |
| f.close() | |
| finally: | |
| close_vector_batch_process(vector_process) | |
| print(f"1..{number}") | |
| if fails: | |
| sys.exit(1) |