import hashlib
import os
import pathlib
import shutil
import sys
import subprocess
from pathlib import Path
from sys import platform
IGNORE_TESTS = [
"macos.tests",
"coretext.tests",
"directwrite.tests",
"uniscribe.tests",
"arabic-fallback-shaping.tests",
"emoji-clusters.tests",
]
IGNORE_TEST_CASES = [
"simple_002",
"collections_001",
"collections_002",
"collections_003",
"collections_006",
"indic_decompose_001",
"morx_34_001",
"morx_36_001",
"macos_002",
"macos_122",
"glyph_flags_002",
]
def check_hb_build(hb_shape_exe):
if not hb_shape_exe.exists():
print("Build harfbuzz first using:")
print(" meson builddir")
print(" ninja -Cbuilddir")
exit(1)
def update_font_path(tests_name, fontfile):
if not fontfile.startswith("/"):
fontfile = fontfile.replace("../fonts/", "")
return f"tests/fonts/{tests_name}/{fontfile}" else:
return fontfile
def convert_unicodes(unicodes):
text = ""
for i, u in enumerate(unicodes.split(",")):
if i > 0 and i % 10 == 0:
text += "\\\n "
if u.startswith("U+"):
u = u[2:]
text += f"\\u{{{u}}}"
return text
def prune_test_options(options):
options = options.replace("--shaper=ot", "")
options = options.replace(" --font-funcs=ft", "").replace("--font-funcs=ft", "")
options = options.replace(" --font-funcs=ot", "").replace("--font-funcs=ot", "")
options = options.replace("--font-size=1000", "")
options = options.replace("--not-found-variation-selector-glyph=1000000", "--not-found-variation-selector-glyph=64000")
options = options.strip()
return options
def convert_test_file(
root_dir, hb_shape_exe, tests_name, file_name, idx, data, fonts, custom
):
fontfile, options, unicodes, glyphs_expected = data.split(";")
if "@" in fontfile:
fontfile, _ = fontfile.split("@")
fontfile = fontfile.replace("\\ ", " ")
fontfile_rs = fontfile if custom else update_font_path(tests_name, fontfile)
unicodes_rs = convert_unicodes(unicodes)
test_name = file_name.replace(".tests", "").replace("-", "_") + f"_{idx:03d}"
test_name = test_name.lower()
if test_name in IGNORE_TEST_CASES:
return ""
options = prune_test_options(options)
if len(options) != 0:
options_list = options.split(" ")
else:
options_list = []
options_list.insert(0, str(hb_shape_exe))
abs_font_path = (
root_dir.joinpath(fontfile_rs)
if custom
else (
root_dir.joinpath("test/shape/data")
.joinpath(tests_name)
.joinpath("tests")
.joinpath(fontfile)
)
)
options_list.append(str(abs_font_path))
options_list.append(f"--unicodes={unicodes}")
glyphs_expected = subprocess.run(
options_list, check=True, stdout=subprocess.PIPE
).stdout.decode()
glyphs_expected = glyphs_expected.strip()[
1:-1
] glyphs_expected = glyphs_expected.replace("|", "|\\\n ")
options_rs = options
options_rs = options_rs.replace('"', '\\"')
options_rs = options_rs.replace(" --single-par", "")
if not fontfile.startswith("/"):
fonts.add(os.path.split(fontfile_rs)[1])
final_string = (
f"#[test]\n"
f"fn {test_name}() {{\n"
f" assert_eq!(\n"
f" shape(\n"
f' "{fontfile_rs}",\n'
f' "{unicodes_rs}",\n'
f' "{options_rs}",\n'
f" ),\n"
f' "{glyphs_expected}"\n'
f" );\n"
f"}}\n"
"\n"
)
if file_name == "macos.tests":
final_string = '#[cfg(target_os = "macos")]\n' + final_string
return final_string
def read_test_cases(path):
with open(path, "r") as f:
idx = 0
for test in f.read().splitlines():
if test.startswith("#") or len(test) == 0:
continue
yield idx, test
idx += 1
def convert_test_folder(root_dir, hb_shape_exe, tests_dir, tests_name, custom):
files = sorted(os.listdir(tests_dir))
files = [f for f in files if f.endswith(".tests") and f not in IGNORE_TESTS]
return convert_test_files(
root_dir, hb_shape_exe, tests_dir, tests_name, files, custom
)
def convert_test_files(root_dir, hb_shape_exe, tests_dir, tests_name, files, custom):
fonts = set()
rust_code = (
"// WARNING: this file was generated by ../scripts/gen-shaping-tests.py\n"
"\n"
"use crate::shape;\n"
"\n"
)
for file in files:
path = tests_dir / file
for idx, test in read_test_cases(path):
rust_code += convert_test_file(
root_dir, hb_shape_exe, tests_name, file, idx + 1, test, fonts, custom
)
tests_name_snake_case = tests_name.replace("-", "_")
with open(f"../tests/shaping/{tests_name_snake_case}.rs", "w") as f:
f.write(rust_code)
return fonts
def main():
if len(sys.argv) != 2:
print("Usage: gen-shaping-tests.py /path/to/harfbuzz-src")
exit(1)
hb_dir = Path(sys.argv[1])
assert hb_dir.exists()
rb_root = pathlib.Path(__file__).parent.parent
hb_shape_exe = hb_dir.joinpath("builddir/util/hb-shape")
check_hb_build(hb_shape_exe)
def to_hb_absolute(name):
return hb_dir / f"test/shape/data/{name}/tests"
test_dir_names = ["aots", "in-house", "text-rendering-tests"]
for test_dir_name in test_dir_names:
tests_dir = to_hb_absolute(test_dir_name)
dir_used_fonts = convert_test_folder(
hb_dir, hb_shape_exe, tests_dir, test_dir_name, False
)
for filename in dir_used_fonts:
shutil.copy(
hb_dir / f"test/shape/data/{test_dir_name}/fonts/{filename}",
f"../tests/fonts/{test_dir_name}",
)
if platform == "darwin":
tests_dir = rb_root / "tests" / "custom"
convert_test_files(
rb_root, hb_shape_exe, tests_dir, "macos", ["macos.tests"], False
)
convert_test_folder(
rb_root, hb_shape_exe, rb_root / "tests" / "custom", "custom", True
)
if __name__ == "__main__":
main()