.gitlab-ci/linux-mingw-w64-64bit.txt | 2 src/fcfreetype.c | 6 test/Makefile.am | 6 test/fctest/__init__.py | 347 +++++++++++++++++++++ test/meson.build | 44 +- test/run-test-conf.sh | 62 --- test/run-test.sh | 569 ----------------------------------- test/test_basic.py | 337 ++++++++++++++++++++ test/test_conf.py | 72 ++++ test/test_crbug1004254.py | 46 ++ test/test_fontations_ft_query.py | 63 --- test/test_issue431.py | 41 +- test/test_sandbox.py | 166 ++++++++++ test/test_sysroot.py | 46 ++ test/wrapper-script.sh | 2 15 files changed, 1075 insertions(+), 734 deletions(-) New commits: commit cbbc89033750f6cc0b1bc62d04ee27ca53e6e021 Merge: c1f7076 e421882 Author: Akira TAGOH <akira@xxxxxxxxx> Date: Wed Jul 9 09:51:43 2025 +0000 Merge branch 'port-test-to-python' into 'main' Port test to python See merge request fontconfig/fontconfig!443 commit e42188283f0ee1fb23089f16dbf95e0f3bcbacaf Author: Akira TAGOH <akira@xxxxxxxxx> Date: Wed Jul 9 15:22:44 2025 +0900 do not mix up a slash and a backslash in file object on Win32 All path delimitors in file object will be / with this change. Some config files may need to be updated. Changelog: changed diff --git a/src/fcfreetype.c b/src/fcfreetype.c index 0a87d27..47844b9 100644 --- a/src/fcfreetype.c +++ b/src/fcfreetype.c @@ -1209,6 +1209,7 @@ FcFreeTypeQueryFaceInternal (const FT_Face face, FcBool symbol = FcFalse; FT_Error ftresult; + FcChar8 *canon_file = NULL; FcInitDebug(); /* We might be called with no initizalization whatsoever. */ @@ -1713,7 +1714,8 @@ FcFreeTypeQueryFaceInternal (const FT_Face face, goto bail1; } - if (file && *file && !FcPatternObjectAddString (pat, FC_FILE_OBJECT, file)) + canon_file = FcStrCanonFilename(file); + if (canon_file && *canon_file && !FcPatternObjectAddString (pat, FC_FILE_OBJECT, canon_file)) goto bail1; if (!FcPatternObjectAddInteger (pat, FC_INDEX_OBJECT, id)) @@ -2106,6 +2108,8 @@ bail1: free (name_mapping); if (foundry_) free (foundry_); + if (canon_file) + free (canon_file); bail0: return NULL; } commit 4246b328cbc45dfd2973abca22370ebd4288db45 Author: Akira TAGOH <akira@xxxxxxxxx> Date: Tue Jul 8 21:06:43 2025 +0900 test: update to pass test cases on Win32 diff --git a/test/fctest/__init__.py b/test/fctest/__init__.py index 0d4e533..cbc3782 100644 --- a/test/fctest/__init__.py +++ b/test/fctest/__init__.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from itertools import chain -from pathlib import Path +from pathlib import Path, PureWindowsPath from tempfile import TemporaryDirectory, NamedTemporaryFile from typing import Iterator, Self import logging @@ -19,6 +19,7 @@ logging.basicConfig(level=logging.DEBUG) class FcTest: def __init__(self): + self._with_fontations = False self.logger = logging.getLogger() self._env = os.environ.copy() self._fontdir = TemporaryDirectory(prefix='fontconfig.', @@ -29,17 +30,32 @@ class FcTest: suffix='.host.conf', mode='w', delete_on_close=False) - self._builddir = self._env.get('builddir', 'build') + self._builddir = self._env.get('builddir', str(Path(__file__).parents[2] / 'build')) self._srcdir = self._env.get('srcdir', '.') self._exeext = self._env.get('EXEEXT', '.exe' if sys.platform == 'win32' else '') - self._exewrapper = self._env.get('EXE_WRAPPER', None) - if not self._exewrapper: - raise RuntimeError('No exe wrapper') + self._drive = PureWindowsPath(self._env.get('SystemDrive', '')).drive + self._exewrapper = '' + if self._exeext and sys.platform != 'win32': + self._exewrapper = shutil.which('wine') + if not self._exewrapper: + raise RuntimeError('No runner available') + self._drive = 'z:' + cc = self._env.get('CC', 'cc') + res = subprocess.run([cc, '-print-sysroot'], capture_output=True) + sysroot = res.stdout.decode('utf-8').rstrip() + if res.returncode != 0 or not sysroot: + raise RuntimeError('Unable to get sysroot') + sysroot = Path(sysroot) / 'mingw' / 'bin' + self._env['WINEPATH'] = ';'.join( + [ + self.convert_path(self._builddir), + self.convert_path(sysroot) + ]) self._bwrap = shutil.which('bwrap') def bin_path(bin): fn = bin + self._exeext - return Path(self.builddir) / bin / fn + return self.convert_path(Path(self.builddir) / bin / fn) self._fccache = bin_path('fc-cache') self._fccat = bin_path('fc-cat') self._fclist = bin_path('fc-list') @@ -87,14 +103,22 @@ class FcTest: def remapdir(self): return [x for x in self._extra if re.search(r'\b<remap-dir\b', x)] + @property + def with_fontations(self): + return self._with_fontations + + @with_fontations.setter + def with_fontations(self, v: bool) -> None: + self._with_fontations = v + @remapdir.setter def remapdir(self, v: str) -> None: self._extra = [x for x in self._extra if not re.search(r'\b<remap-dir\b', x)] self._extra += [f'<remap-dir as-path="{self.fontdir.name}">{v}</remap-dir>'] def config(self) -> str: - return self.__conf_templ.format(fontdir=self.fontdir.name, - cachedir=self.cachedir.name, + return self.__conf_templ.format(fontdir=self.convert_path(self.fontdir.name), + cachedir=self.convert_path(self.cachedir.name), extra=self.extra) def setup(self): @@ -113,7 +137,7 @@ class FcTest: self._conffile.close() conf = self._conffile.name - self._env['FONTCONFIG_FILE'] = conf + self._env['FONTCONFIG_FILE'] = self.convert_path(conf) def install_font(self, files, dest, time=None): if not isinstance(files, list): @@ -217,6 +241,8 @@ class FcTest: boxed += self.__bind if debug: boxed += ['--setenv', 'FC_DEBUG', str(debug)] + if self.with_fontations: + boxed += ['--setenv', 'FC_FONTATIONS', '1'] boxed += cmd self.logger.info(boxed) res = subprocess.run(boxed, capture_output=True, @@ -225,6 +251,9 @@ class FcTest: origdebug = self._env.get('FC_DEBUG') if debug: self._env['FC_DEBUG'] = str(debug) + origfontations = self._env.get('FC_FONTATIONS') + if self.with_fontations: + self._env['FC_FONTATIONS'] = '1' self.logger.info(cmd) res = subprocess.run(cmd, capture_output=True, env=self._env) @@ -233,6 +262,11 @@ class FcTest: self._env['FC_DEBUG'] = origdebug else: del self._env['FC_DEBUG'] + if self.with_fontations: + if origfontations: + self._env['FC_FONTATIONS'] = origfontations + else: + del self._env['FC_FONTATIONS'] yield res.returncode, res.stdout.decode('utf-8'), res.stderr.decode('utf-8') def run_cache(self, args, debug=False) -> Iterator[[int, str, str]]: @@ -263,6 +297,12 @@ class FcTest: for c in Path(self.cachedir.name).glob('*cache*'): yield c + def convert_path(self, path) -> str: + winpath = PureWindowsPath(path) + if not winpath.drive and self._drive: + return str(PureWindowsPath(self._drive) / '/' / winpath).replace('\\', '/') + return path + class FcTestFont: @@ -285,10 +325,23 @@ class FcTestFont: return self._fonts +class FcExternalTestFont: + + def __init__(self): + fctest = FcTest() + self._fonts = [str(fn) for fn in (Path(fctest.builddir) / "testfonts").glob('**/*.ttf')] + + @property + def fonts(self): + return self._fonts + + if __name__ == '__main__': f = FcTest() print(f.fontdir.name) print(f.cachedir.name) print(f._conffile.name) - print(f.config) + print(f.config()) f.setup() + f = FcExternalTestFont() + print(f.fonts) diff --git a/test/meson.build b/test/meson.build index 7e0d636..188c8ce 100644 --- a/test/meson.build +++ b/test/meson.build @@ -81,29 +81,26 @@ endif fs = import('fs') -if host_machine.system() != 'windows' - if conf.get('FREETYPE_PCF_LONG_FAMILY_NAMES') - out_expected = fs.copyfile('out.expected-long-family-names', - 'out.expected') - else - out_expected = fs.copyfile('out.expected-no-long-family-names', - 'out.expected') - endif +if conf.get('FREETYPE_PCF_LONG_FAMILY_NAMES') + out_expected = fs.copyfile('out.expected-long-family-names', + 'out.expected') +else + out_expected = fs.copyfile('out.expected-no-long-family-names', + 'out.expected') +endif - wrapper = find_program('wrapper-script.sh') - if pytest.found() - test('pytest', pytest, args: ['--tap'], - workdir: meson.current_source_dir(), - env: [ - 'builddir=@0@'.format(meson.project_build_root()), - 'srcdir=@0@'.format(meson.project_source_root()), - 'EXEEXT=@0@'.format(conf.get('EXEEXT')), - 'EXE_WRAPPER=@0@'.format(wrapper.full_path()) - ], - protocol: 'tap', - timeout: 600, - depends: fetch_test_fonts) - endif +if pytest.found() + test('pytest', pytest, args: ['--tap'], + workdir: meson.current_source_dir(), + env: [ + 'builddir=@0@'.format(meson.project_build_root()), + 'srcdir=@0@'.format(meson.project_source_root()), + 'EXEEXT=@0@'.format(conf.get('EXEEXT')), + 'CC=@0@'.format(meson.get_compiler('c').cmd_array()[0]), + ], + protocol: 'tap', + timeout: 600, + depends: fetch_test_fonts) endif if jsonc_dep.found() diff --git a/test/test_basic.py b/test/test_basic.py index e24d2ae..ebb2c3d 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -231,7 +231,7 @@ def test_multiple_caches(fctest, fcfont): suffix='.extra.conf', mode='w', delete_on_close=False) - fctest._extra.append(f'<include ignore_missing="yes">{extraconffile.name}</include>') + fctest._extra.append(f'<include ignore_missing="yes">{fctest.convert_path(extraconffile.name)}</include>') # Set up for generating original caches fctest.setup() @@ -242,7 +242,7 @@ def test_multiple_caches(fctest, fcfont): fctest.install_font(fcfont.fonts, '.', epoch) if epoch: fctest._env['SOURCE_DATE_EPOCH'] = str(epoch) - for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + for ret, stdout, stderr in fctest.run_cache([fctest.convert_path(fctest.fontdir.name)]): assert ret == 0, stderr time.sleep(1) @@ -265,14 +265,14 @@ def test_multiple_caches(fctest, fcfont): extraconffile.write(f''' <fontconfig> <match target="scan"> - <test name="file"><string>{fctest.fontdir.name}/4x6.pcf</string></test> + <test name="file"><string>{fctest.convert_path(fctest.fontdir.name)}/4x6.pcf</string></test> <edit name="pixelsize"><int>8</int></edit> </match> </fontconfig>''') extraconffile.close() if epoch: fctest._env['SOURCE_DATE_EPOCH'] = str(epoch + 1) - for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + for ret, stdout, stderr in fctest.run_cache([fctest.convert_path(fctest.fontdir.name)]): assert ret == 0, stderr if epoch: fctest._env['SOURCE_DATE_EPOCH'] = origepoch @@ -288,7 +288,7 @@ def test_multiple_caches(fctest, fcfont): delete_on_close=False) fctest._cachedir = oldcachedir fctest._conffile = mixedconffile - fctest._extra.append(f'<cachedir>{newcachedir.name}</cachedir>') + fctest._extra.append(f'<cachedir>{fctest.convert_path(newcachedir.name)}</cachedir>') fctest.setup() l = [] for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): diff --git a/test/test_fontations_ft_query.py b/test/test_fontations_ft_query.py index 3e90285..ae5a976 100644 --- a/test/test_fontations_ft_query.py +++ b/test/test_fontations_ft_query.py @@ -2,67 +2,32 @@ # Copyright (C) 2025 Google LLC. # SPDX-License-Identifier: HPND -import os +from fctest import FcTest, FcExternalTestFont from pathlib import Path import pytest -import re -import requests -import subprocess -def builddir(): - return Path(os.environ.get("builddir", Path(__file__).parent.parent)) +@pytest.fixture +def fctest(): + return FcTest() -def list_test_fonts(): - font_files = [] - for root, _, files in os.walk(builddir() / "testfonts"): - for file in files: - # Variable .ttc not supported yet. - if file.endswith(".ttf"): - font_files.append(os.path.join(root, file)) - return font_files - - -def run_fc_query(font_file, with_fontations=False): - fc_query_path = builddir() / "fc-query" / "fc-query" - - env = os.environ.copy() - if with_fontations: - env["FC_FONTATIONS"] = "" - - result = subprocess.run( - [fc_query_path, font_file], - stdout=subprocess.PIPE, - env=env, - stderr=subprocess.PIPE, - text=True, - check=False, - ) - - assert ( - result.returncode == 0 - ), f"fc-query failed for {font_file} with error: {result.stderr}" - assert result.stdout, f"fc-query produced no output for {font_file}" - - return result - - -@pytest.mark.parametrize("font_file", list_test_fonts()) -def test_fontations_freetype_fcquery_equal(font_file): - print(f"Testing with: {font_file}") # Example usage +@pytest.mark.parametrize("font_file", FcExternalTestFont().fonts) +def test_fontations_freetype_fcquery_equal(fctest, font_file): + fctest.logger.info(f'Testing with: {font_file}') font_path = Path(font_file) if not font_path.exists(): pytest.skip(f"Font file not found: {font_file}") # Skip if file missing - result_freetype = run_fc_query(font_file).stdout.strip().splitlines() - result_fontations = ( - run_fc_query(font_file, with_fontations=True) - .stdout.strip() - .splitlines() - ) + for ret, stdout, stderr in fctest.run_query([font_file]): + assert ret == 0, stderr + result_freetype = stdout.strip().splitlines() + fctest.with_fontations = True + for ret, stdout, stderr in fctest.run_query([font_file]): + assert ret == 0, stderr + result_fontations = stdout.strip().splitlines() assert ( result_freetype == result_fontations diff --git a/test/test_issue431.py b/test/test_issue431.py index ba0b336..6b15a5f 100644 --- a/test/test_issue431.py +++ b/test/test_issue431.py @@ -2,19 +2,20 @@ # Copyright (C) 2024 fontconfig Authors # SPDX-License-Identifier: HPND -import os import pytest import re -import requests -import shutil -import subprocess from pathlib import Path +from fctest import FcTest -def test_issue431(tmp_path): - builddir = Path(os.environ.get("builddir", Path(__file__).parent.parent)) +@pytest.fixture +def fctest(): + return FcTest() + + +def test_issue431(fctest): roboto_flex_font = ( - builddir + Path(fctest.builddir) / "testfonts" / "roboto-flex-fonts/fonts/variable/RobotoFlex[GRAD,XOPQ,XTRA,YOPQ,YTAS,YTDE,YTFI,YTLC,YTUC,opsz,slnt,wdth,wght].ttf" ) @@ -22,19 +23,13 @@ def test_issue431(tmp_path): if not roboto_flex_font.exists(): pytest.skip(f"Font file not found: {roboto_flex_font}") - result = subprocess.run( - [ - builddir / "fc-query" / "fc-query", - "-f", - "%{family[0]}:%{index}:%{style[0]}:%{postscriptname}\n", - roboto_flex_font, - ], - stdout=subprocess.PIPE, - ) - - for line in result.stdout.decode("utf-8").splitlines(): - family, index, style, psname = line.split(":") - normstyle = re.sub("[\x04\\(\\)/<>\\[\\]{}\t\f\r\n ]", "", style) - assert ( - psname.split("-")[-1] == normstyle - ), f"postscriptname `{psname}' does not contain style name `{normstyle}': index {index}" + for ret, stdout, stderr in fctest.run_query(['-f', + '%{family[0]}:%{index}:%{style[0]}:%{postscriptname}\n', + roboto_flex_font]): + assert ret == 0, stderr + for line in stdout.splitlines(): + family, index, style, psname = line.split(":") + normstyle = re.sub("[\x04\\(\\)/<>\\[\\]{}\t\f\r\n ]", "", style) + assert ( + psname.split("-")[-1] == normstyle + ), f"postscriptname `{psname}' does not contain style name `{normstyle}': index {index}" commit eade683ed2eadfaafdf0d9b4423a98143f14f0dc Author: Akira TAGOH <akira@xxxxxxxxx> Date: Thu Jun 12 20:32:17 2025 +0900 test: port basic functionality check to Python diff --git a/.gitlab-ci/linux-mingw-w64-64bit.txt b/.gitlab-ci/linux-mingw-w64-64bit.txt index fd4a640..bb4155e 100644 --- a/.gitlab-ci/linux-mingw-w64-64bit.txt +++ b/.gitlab-ci/linux-mingw-w64-64bit.txt @@ -18,4 +18,4 @@ objcopy = 'x86_64-w64-mingw32-objcopy' strip = 'x86_64-w64-mingw32-strip' pkgconfig = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' -# exe_wrapper = 'wine64' \ No newline at end of file +exe_wrapper = 'wine' diff --git a/test/Makefile.am b/test/Makefile.am index 3b4e3e8..4b6fd1e 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -21,7 +21,6 @@ # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. -check_SCRIPTS=run-test.sh TEST_EXTENSIONS = \ .sh \ $(NULL) @@ -38,7 +37,7 @@ SH_LOG_COMPILER = sh if OS_WIN32 LOG_COMPILER = ${srcdir}/wrapper-script.sh endif -TESTS=run-test.sh +TESTS= TESTDATA = \ 4x6.pcf \ @@ -108,7 +107,6 @@ check_PROGRAMS += test-conf test_conf_CFLAGS = $(JSONC_CFLAGS) test_conf_LDADD = $(top_builddir)/src/libfontconfig.la $(JSONC_LIBS) endif -TESTS += run-test-conf.sh check_PROGRAMS += test-bz106618 test_bz106618_LDADD = $(top_builddir)/src/libfontconfig.la @@ -180,7 +178,7 @@ TESTS += test-family-matching check_PROGRAMS += test-filter test_filter_LDADD = $(top_builddir)/src/libfontconfig.la -EXTRA_DIST=run-test.sh run-test-conf.sh wrapper-script.sh $(TESTDATA) out.expected-long-family-names out.expected-no-long-family-names +EXTRA_DIST=wrapper-script.sh $(TESTDATA) out.expected-long-family-names out.expected-no-long-family-names CLEANFILES = \ fonts.conf \ diff --git a/test/fctest/__init__.py b/test/fctest/__init__.py index ea73fc6..0d4e533 100644 --- a/test/fctest/__init__.py +++ b/test/fctest/__init__.py @@ -1,63 +1,54 @@ # Copyright (C) 2025 fontconfig Authors # SPDX-License-Identifier: HPND +from contextlib import contextmanager +from itertools import chain from pathlib import Path from tempfile import TemporaryDirectory, NamedTemporaryFile -from typing import Iterator +from typing import Iterator, Self +import logging import os +import re import shutil import subprocess +import sys +logging.basicConfig(level=logging.DEBUG) + class FcTest: def __init__(self): + self.logger = logging.getLogger() self._env = os.environ.copy() self._fontdir = TemporaryDirectory(prefix='fontconfig.', - suffix='.fontdir') + suffix='.host_fontdir') self._cachedir = TemporaryDirectory(prefix='fontconfig.', - suffix='.cachedir') + suffix='.host_cachedir') self._conffile = NamedTemporaryFile(prefix='fontconfig.', - suffix='.conf', + suffix='.host.conf', mode='w', delete_on_close=False) self._builddir = self._env.get('builddir', 'build') self._srcdir = self._env.get('srcdir', '.') - exeext = self._env.get('EXEEXT', '') - self._exewrapper = self._env.get('EXEWRAPPER', None) - self._fccache = Path(self.builddir) / 'fc-cache' / ('fc-cache' + exeext) - if not self._fccache.exists(): - raise RuntimeError('No fc-cache binary. builddir might be wrong:' - f' {self._fccache}') - self._fccat = Path(self.builddir) / 'fc-cat' / ('fc-cat' + exeext) - if not self._fccat.exists(): - raise RuntimeError('No fc-cat binary. builddir might be wrong:' - f' {self._fccat}') - self._fclist = Path(self.builddir) / 'fc-list' / ('fc-list' + exeext) - if not self._fclist.exists(): - raise RuntimeError('No fc-list binary. builddir might be wrong:' - f' {self._fclist}') - self._fcmatch = Path(self.builddir) / 'fc-match' / ('fc-match' + exeext) - if not self._fcmatch.exists(): - raise RuntimeError('No fc-match binary. builddir might be wrong:' - f' {self._fcmatch}') - self._fcpattern = Path(self.builddir) / 'fc-pattern' / ('fc-pattern' + exeext) - if not self._fcpattern.exists(): - raise RuntimeError('No fc-pattern binary. builddir might be wrong:' - f' {self._fcpattern}') - self._fcquery = Path(self.builddir) / 'fc-query' / ('fc-query' + exeext) - if not self._fcquery.exists(): - raise RuntimeError('No fc-query binary. builddir might be wrong:' - f' {self._fcquery}') - self._fcscan = Path(self.builddir) / 'fc-scan' / ('fc-scan' + exeext) - if not self._fcscan.exists(): - raise RuntimeError('No fc-scan binary. builddir might be wrong:' - f' {self._fcscan}') - self._fcvalidate = Path(self.builddir) / 'fc-validate' / ('fc-validate' + exeext) - if not self._fcvalidate.exists(): - raise RuntimeError('No fc-validate binary. builddir might be wrong:' - f' {self._fcvalidate}') - self._extra = '' + self._exeext = self._env.get('EXEEXT', + '.exe' if sys.platform == 'win32' else '') + self._exewrapper = self._env.get('EXE_WRAPPER', None) + if not self._exewrapper: + raise RuntimeError('No exe wrapper') + self._bwrap = shutil.which('bwrap') + def bin_path(bin): + fn = bin + self._exeext + return Path(self.builddir) / bin / fn + self._fccache = bin_path('fc-cache') + self._fccat = bin_path('fc-cat') + self._fclist = bin_path('fc-list') + self._fcmatch = bin_path('fc-match') + self._fcpattern = bin_path('fc-pattern') + self._fcquery = bin_path('fc-query') + self._fcscan = bin_path('fc-scan') + self._fcvalidate = bin_path('fc-validate') + self._extra = [] self.__conf_templ = ''' <fontconfig> {extra} @@ -65,6 +56,7 @@ class FcTest: <cachedir>{cachedir}</cachedir> </fontconfig> ''' + self._sandboxed = False def __del__(self): del self._conffile @@ -89,27 +81,55 @@ class FcTest: @property def extra(self): - return self._extra + return '\n'.join(self._extra) @property - def config(self): + def remapdir(self): + return [x for x in self._extra if re.search(r'\b<remap-dir\b', x)] + + @remapdir.setter + def remapdir(self, v: str) -> None: + self._extra = [x for x in self._extra if not re.search(r'\b<remap-dir\b', x)] + self._extra += [f'<remap-dir as-path="{self.fontdir.name}">{v}</remap-dir>'] + + def config(self) -> str: return self.__conf_templ.format(fontdir=self.fontdir.name, cachedir=self.cachedir.name, extra=self.extra) def setup(self): - self._conffile.write(self.config) - self._conffile.close() - self._env['FONTCONFIG_FILE'] = self._conffile.name + if self._sandboxed: + self.logger.info(self.config()) + self._remapped_conffile.write(self.config()) + self._remapped_conffile.close() + conf = self._remapped_conffile.name + try: + fn = Path(self._remapped_conffile.name).relative_to(Path(self._builddir).resolve()) + conf = str(Path(self._remapped_builddir.name) / fn) + except ValueError: + pass + else: + self._conffile.write(self.config()) + self._conffile.close() + conf = self._conffile.name - def install_font(self, files, dest): + self._env['FONTCONFIG_FILE'] = conf + + def install_font(self, files, dest, time=None): if not isinstance(files, list): files = [files] - time = self._env.get('SOURCE_DATE_EPOCH', None) + if not time: + time = self._env.get('SOURCE_DATE_EPOCH', None) for f in files: fn = Path(f).name - dname = Path(self.fontdir.name) / dest / fn + d = Path(dest) + if d.is_absolute(): + dpath = d + else: + dpath = Path(self.fontdir.name) / dest + dname = dpath / fn + os.makedirs(str(dpath), exist_ok=True) shutil.copy2(f, dname) if time: os.utime(str(dname), (time, time)) @@ -117,39 +137,152 @@ class FcTest: if time: os.utime(self.fontdir.name, (time, time)) - def run(self, binary, args) -> Iterator[[int, str, str]]: + @contextmanager + def sandboxed(self, remapped_basedir, bind=None) -> Self: + if not self._bwrap: + raise RuntimeError('No bwrap installed') + self._remapped_fontdir = TemporaryDirectory(prefix='fontconfig.', + suffix='.fontdir', + dir=remapped_basedir, + delete=False) + self._remapped_cachedir = TemporaryDirectory(prefix='fontconfig.', + suffix='.cachedir', + dir=remapped_basedir, + delete=False) + self._remapped_builddir = TemporaryDirectory(prefix='fontconfig.', + suffix='.build', + dir=remapped_basedir, + delete=False) + self._remapped_conffile = NamedTemporaryFile(prefix='fontconfig.', + suffix='.conf', + dir=Path(self._builddir) / 'test', + mode='w', + delete_on_close=False) + self._basedir = remapped_basedir + self.remapdir = self._remapped_fontdir.name + self._orig_cachedir = self.cachedir + self._cachedir = self._remapped_cachedir + self._sandboxed = True + dummy = TemporaryDirectory(prefix='fontconfig.') + # Set same mtime to dummy directory to avoid updating cache + # because of mtime + st = Path(self.fontdir.name).stat() + os.utime(str(dummy.name), (st.st_mtime, st.st_mtime)) + # Set dummy dir as <dir> + orig_fontdir = self.fontdir + self._fontdir = dummy + self.setup() + self._fontdir = orig_fontdir + base_bind = { + self._orig_cachedir.name: self._remapped_cachedir.name, + self._builddir: self._remapped_builddir.name, + } + if not bind: + bind = base_bind | { + self.fontdir.name: self._remapped_fontdir.name, + } + else: + bind = base_bind | bind + b = [('--bind', x, y) for x, y in bind.items()] + self.__bind = list(chain.from_iterable(i for i in b)) + try: + yield self + finally: + self._cachedir = self._orig_cachedir + del self._remapped_conffile + self._sandboxed = False + self._remapped_builddir = None + self._remapped_cachedir = None + self._remapped_conffile = None + self._remapped_fontdir = None + self._orig_cachedir = None + self.remapdir = None + self._basedir = None + self.__bind = None + self._env['FONTCONFIG_FILE'] = self._conffile.name + + def run(self, binary, args=[], debug=False) -> Iterator[[int, str, str]]: cmd = [] if self._exewrapper: - cmd += self._exewrapper + cmd += [self._exewrapper] cmd += [str(binary)] cmd += args - res = subprocess.run(cmd, check=True, capture_output=True, - env=self._env) + if self._sandboxed: + boxed = [self._bwrap, '--ro-bind', '/', '/', + '--dev-bind', '/dev', '/dev', + '--proc', '/proc', + # Use fresh tmpfs to avoid unexpected references + '--tmpfs', '/tmp', + '--setenv', 'FONTCONFIG_FILE', self._env['FONTCONFIG_FILE']] + boxed += self.__bind + if debug: + boxed += ['--setenv', 'FC_DEBUG', str(debug)] + boxed += cmd + self.logger.info(boxed) + res = subprocess.run(boxed, capture_output=True, + env=self._env) + else: + origdebug = self._env.get('FC_DEBUG') + if debug: + self._env['FC_DEBUG'] = str(debug) + self.logger.info(cmd) + res = subprocess.run(cmd, capture_output=True, + env=self._env) + if debug: + if origdebug: + self._env['FC_DEBUG'] = origdebug + else: + del self._env['FC_DEBUG'] yield res.returncode, res.stdout.decode('utf-8'), res.stderr.decode('utf-8') - def run_cache(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fccache, args) + def run_cache(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fccache, args, debug) + + def run_cat(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fccat, args, debug) - def run_cat(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fccat, args) + def run_list(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fclist, args, debug) - def run_list(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fclist, args) + def run_match(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fcmatch, args, debug) - def run_match(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fcmatch, args) + def run_pattern(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fcpattern, args, debug) - def run_pattern(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fcpattern, args) + def run_query(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fcquery, args, debug) - def run_query(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fcquery, args) + def run_scan(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fcscan, args, debug) - def run_scan(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fcscan, args) + def run_validate(self, args, debug=False) -> Iterator[[int, str, str]]: + return self.run(self._fcvalidate, args, debug) - def run_validate(self, args) -> Iterator[[int, str, str]]: - return self.run(self._fcvalidate, args) + def cache_files(self) -> Iterator[Path]: + for c in Path(self.cachedir.name).glob('*cache*'): + yield c + + +class FcTestFont: + + def __init__(self, srcdir='.'): + self._fonts = [] + p = Path(srcdir) + if (p / 'test').exists(): + p = p / 'test' + if not (p / '4x6.pcf').exists(): + raise RuntimeError('No 4x6.pcf available.') + else: + self._fonts.append(p / '4x6.pcf') + if not (p / '8x16.pcf').exists(): + raise RuntimeError('No 8x16.pcf available.') + else: + self._fonts.append(p / '8x16.pcf') + + @property + def fonts(self): + return self._fonts if __name__ == '__main__': diff --git a/test/meson.build b/test/meson.build index 50e1906..7e0d636 100644 --- a/test/meson.build +++ b/test/meson.build @@ -90,12 +90,16 @@ if host_machine.system() != 'windows' 'out.expected') endif - test('run_test_sh', find_program('run-test.sh'), timeout: 600, env: ['srcdir=@0@'.format(meson.current_source_dir()), 'builddir=@0@'.format(meson.current_build_dir()), 'EXEEXT=@0@'.format(conf.get('EXEEXT')), 'VERBOSE=1']) - + wrapper = find_program('wrapper-script.sh') if pytest.found() test('pytest', pytest, args: ['--tap'], workdir: meson.current_source_dir(), - env: ['builddir=@0@'.format(meson.project_build_root())], + env: [ + 'builddir=@0@'.format(meson.project_build_root()), + 'srcdir=@0@'.format(meson.project_source_root()), + 'EXEEXT=@0@'.format(conf.get('EXEEXT')), + 'EXE_WRAPPER=@0@'.format(wrapper.full_path()) + ], protocol: 'tap', timeout: 600, depends: fetch_test_fonts) @@ -105,11 +109,4 @@ endif if jsonc_dep.found() test_conf = executable('test-conf', 'test-conf.c', dependencies: [fontconfig_dep, jsonc_dep]) - test('run_test_conf_sh', find_program('run-test-conf.sh'), - timeout: 120, - env: [ - 'srcdir=@0@'.format(meson.current_source_dir()), - 'builddir=@0@'.format(meson.current_build_dir()) - ], - depends: test_conf) endif diff --git a/test/run-test-conf.sh b/test/run-test-conf.sh deleted file mode 100644 index 6cbdacc..0000000 --- a/test/run-test-conf.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/sh -# test/run-test-conf.sh -# -# Copyright © 2000 Keith Packard -# Copyright © 2018 Akira TAGOH -# -# Permission to use, copy, modify, distribute, and sell this software and its -# documentation for any purpose is hereby granted without fee, provided that -# the above copyright notice appear in all copies and that both that -# copyright notice and this permission notice appear in supporting -# documentation, and that the name of the author(s) not be used in -# advertising or publicity pertaining to distribution of the software without -# specific, written prior permission. The authors make no -# representations about the suitability of this software for any purpose. It -# is provided "as is" without express or implied warranty. -# -# THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO -# EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR -# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, -# DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -set -e - -case "$OSTYPE" in - msys ) MyPWD=`pwd -W` ;; # On Msys/MinGW, returns a MS Windows style path. - * ) MyPWD=`pwd` ;; # On any other platforms, returns a Unix style path. -esac - -TESTDIR=${srcdir-"$MyPWD"} -BUILDTESTDIR=${builddir-"$MyPWD"} - -RUNNER=$BUILDTESTDIR/test-conf$EXEEXT - -if [ ! -f ${RUNNER} ]; then - echo "${RUNNER} not found!\n" - echo "Building this test requires libjson-c development files to be available." - exit 77 # SKIP -fi - -for i in \ - 45-generic.conf \ - 48-guessfamily.conf \ - 60-generic.conf \ - 70-no-bitmaps-and-emoji.conf \ - 70-no-bitmaps-except-emoji.conf \ - 90-synthetic.conf \ - ; do - test_json=$(echo test-$i|sed s'/\.conf/.json/') - echo $RUNNER $TESTDIR/../conf.d/$i $TESTDIR/$test_json - $RUNNER $TESTDIR/../conf.d/$i $TESTDIR/$test_json -done -for i in \ - test-issue-286.json \ - test-style-match.json \ - test-filter.json \ - test-appfont.json \ - ; do - echo $RUNNER $TESTDIR/$i ... - $RUNNER $TESTDIR/../conf.d/10-autohint.conf $TESTDIR/$i -done diff --git a/test/run-test.sh b/test/run-test.sh deleted file mode 100644 index 9b3c91c..0000000 --- a/test/run-test.sh +++ /dev/null @@ -1,569 +0,0 @@ -#!/bin/bash -# fontconfig/test/run-test.sh -# -# Copyright © 2000 Keith Packard -# -# Permission to use, copy, modify, distribute, and sell this software and its -# documentation for any purpose is hereby granted without fee, provided that -# the above copyright notice appear in all copies and that both that -# copyright notice and this permission notice appear in supporting -# documentation, and that the name of the author(s) not be used in -# advertising or publicity pertaining to distribution of the software without -# specific, written prior permission. The authors make no -# representations about the suitability of this software for any purpose. It -# is provided "as is" without express or implied warranty. -# -# THE AUTHOR(S) DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, -# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO -# EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY SPECIAL, INDIRECT OR -# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, -# DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER -# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -set -e - -: "${TMPDIR=/tmp}" - -case "$OSTYPE" in - msys ) MyPWD=$(pwd -W) ;; # On Msys/MinGW, returns a MS Windows style path. - * ) MyPWD=$(pwd) ;; # On any other platforms, returns a Unix style path. -esac - -normpath() { - printf "%s" "$1" | sed -E 's,/+,/,g' -} - -TESTDIR=${srcdir-"$MyPWD"} -BUILDTESTDIR=${builddir-"$MyPWD"} - -BASEDIR=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -FONTDIR=$(normpath "$BASEDIR"/fonts) -CACHEDIR=$(normpath "$BASEDIR"/cache.dir) -EXPECTED=${EXPECTED-"out.expected"} - -FCLIST="$LOG_COMPILER $BUILDTESTDIR/../fc-list/fc-list$EXEEXT" -FCCACHE="$LOG_COMPILER $BUILDTESTDIR/../fc-cache/fc-cache$EXEEXT" - -if [ -x "$(command -v bwrap)" ]; then - BWRAP="$(command -v bwrap)" -fi - -if [ -x "$(command -v md5sum)" ]; then - MD5SUM="$(command -v md5sum)" -elif [ -x "$(command -v md5)" ]; then - MD5SUM="$(command -v md5)" -else - echo "E: No md5sum or equivalent command" - exit 1 -fi - -FONT1=$(normpath $TESTDIR/4x6.pcf) -FONT2=$(normpath $TESTDIR/8x16.pcf) -TEST="" -export TZ=UTC - -fdate() { - sdate=$1 - ret=0 - date -d @0 > /dev/null 2>&1 || ret=$? - if [ $ret -eq 0 ]; then - ret=$(date -u -d @${sdate} +%y%m%d%H%M.%S) - else - ret=$(date -u -j -f "%s" +%y%m%d%H%M.%S $sdate) - fi - echo $ret -} - -fstat() { - fmt=$1 - fn=$2 - ret=0 - stat -c %Y "$fn" > /dev/null 2>&1 || ret=$? - if [ $ret -eq 0 ]; then - # GNU - ret=$(stat -c "$fmt" "$fn") - else - # BSD - if [ "x$fmt" == "x%Y" ]; then - ret=$(stat -f "%m" "$fn") - elif [ "x$fmt" == "x%y" ]; then - ret=$(stat -f "%Sm" -t "%F %T %z" "$fn") - elif [ "x$fmt" == "x%n %s %y %z" ]; then - ret=$(stat -f "%SN %z %Sm %Sc" -t "%F %T %z" "$fn") - else - echo "E: Unknown format" - exit 1 - fi - fi - echo $ret -} - -clean_exit() { - rc=$? - trap - INT TERM ABRT EXIT - if [ "x$TEST" != "x" ]; then - echo "Aborting from '$TEST' with the exit code $rc" - fi - exit $rc -} -trap clean_exit INT TERM ABRT EXIT - -check () { - { - $FCLIST - family pixelsize | sort; - echo "="; - $FCLIST - family pixelsize | sort; - echo "="; - $FCLIST - family pixelsize | sort; - } > "$BUILDTESTDIR"/out - tr -d '\015' <"$BUILDTESTDIR"/out >"$BUILDTESTDIR"/out.tmp; mv "$BUILDTESTDIR"/out.tmp "$BUILDTESTDIR"/out - if cmp "$BUILDTESTDIR"/out "$BUILDTESTDIR"/"$EXPECTED" > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "*** output is in 'out', expected output in '$EXPECTED'" - exit 1 - fi - rm -f "$BUILDTESTDIR"/out -} - -prep() { - rm -rf "$CACHEDIR" - rm -rf "$FONTDIR" - mkdir "$FONTDIR" -} - -dotest () { - TEST=$1 - test x"$VERBOSE" = x || echo "Running: $TEST" -} - -sed "s!@FONTDIR@!$FONTDIR! -s!@REMAPDIR@!! -s!@CACHEDIR@!$CACHEDIR!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/fonts.conf - -FONTCONFIG_FILE="$BUILDTESTDIR"/fonts.conf -export FONTCONFIG_FILE - -dotest "Basic check" -prep -cp "$FONT1" "$FONT2" "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -check - -dotest "With a subdir" -prep -cp "$FONT1" "$FONT2" "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -$FCCACHE "$FONTDIR" -check - -dotest "Subdir with a cache file" -prep -mkdir "$FONTDIR"/a -cp "$FONT1" "$FONT2" "$FONTDIR"/a -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR"/a -fi -$FCCACHE "$FONTDIR"/a -check - -dotest "with a dotfile" -prep -FONT3=$(basename $FONT1) -FONT4=$(basename $FONT2) -cp "$FONT1" "$FONTDIR"/."$FONT3" -cp "$FONT2" "$FONTDIR"/."$FONT4" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -$FCCACHE "$FONTDIR" -check - -dotest "with a dotdir" -prep -mkdir "$FONTDIR"/.a -cp "$FONT1" "$FONT2" "$FONTDIR"/.a -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -$FCCACHE "$FONTDIR" -check - -dotest "Complicated directory structure" -prep -mkdir "$FONTDIR"/a -mkdir "$FONTDIR"/a/a -mkdir "$FONTDIR"/b -mkdir "$FONTDIR"/b/a -cp "$FONT1" "$FONTDIR"/a -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR"/a -fi -cp "$FONT2" "$FONTDIR"/b/a -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR"/b/a -fi -check - -dotest "Subdir with an out-of-date cache file" -prep -mkdir "$FONTDIR"/a -$FCCACHE "$FONTDIR"/a -sleep 1 -cp "$FONT1" "$FONT2" "$FONTDIR"/a -check - -dotest "Dir with an out-of-date cache file" -prep -cp "$FONT1" "$FONTDIR" -$FCCACHE "$FONTDIR" -sleep 1 -mkdir "$FONTDIR"/a -cp "$FONT2" "$FONTDIR"/a -check - -dotest "Keep mtime of the font directory" -prep -cp "$FONT1" "$FONTDIR" -touch -t $(fdate 0) "$FONTDIR" -fstat "%y" "$FONTDIR" > "$BUILDTESTDIR"/out1 -$FCCACHE -v "$FONTDIR" -fstat "%y" "$FONTDIR" > "$BUILDTESTDIR"/out2 -if cmp "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "mtime was modified" - exit 1 -fi - -if [ x"$BWRAP" != "x" ] && [ "x$EXEEXT" = "x" ]; then -dotest "Basic functionality with the bind-mounted cache dir" -prep -cp "$FONT1" "$FONT2" "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -$FCCACHE "$FONTDIR" -sleep 1 -ls -l "$CACHEDIR" > "$BUILDTESTDIR"/out1 -TESTTMPDIR=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -# Once font dir is remapped, we could use $FONTDIR as different one in theory. -# but we don't use it here and to avoid duplicate entries, set the non-existing -# directory here. -sed "s!@FONTDIR@!$FONTDIR/a! -s!@REMAPDIR@!<remap-dir as-path="'"'"$FONTDIR"'"'">$TESTTMPDIR/fonts</remap-dir>! -s!@CACHEDIR@!$TESTTMPDIR/cache.dir!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/bind-fonts.conf -$BWRAP --bind / / --bind "$CACHEDIR" "$TESTTMPDIR"/cache.dir --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev --setenv FONTCONFIG_FILE "$TESTTMPDIR"/build/test/bind-fonts.conf "$TESTTMPDIR"/build/fc-match/fc-match"$EXEEXT" -f "%{file}\n" ":foundry=Misc" > "$BUILDTESTDIR"/xxx -if test -x "$BUILDTESTDIR"/test-bz106618"$EXEEXT"; then - TESTEXE=test-bz106618"$EXEEXT" -elif test -x "$BUILDTESTDIR"/test_bz106618"$EXEEXT"; then - TESTEXE=test_bz106618"$EXEEXT" -else - echo "*** Test failed: no test case for bz106618" - exit 1 -fi -$BWRAP --bind / / --bind "$CACHEDIR" "$TESTTMPDIR"/cache.dir --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev --setenv FONTCONFIG_FILE "$TESTTMPDIR"/build/test/bind-fonts.conf "$TESTTMPDIR"/build/test/"$TESTEXE" | sort > "$BUILDTESTDIR"/flist1 -$BWRAP --bind / / --bind "$CACHEDIR" "$TESTTMPDIR"/cache.dir --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev find "$TESTTMPDIR"/fonts/ -type f -name '*.pcf' | sort > "$BUILDTESTDIR"/flist2 -ls -l "$CACHEDIR" > "$BUILDTESTDIR"/out2 -if cmp "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "cache was created/updated." - echo "Before:" - cat "$BUILDTESTDIR"/out1 - echo "After:" - cat "$BUILDTESTDIR"/out2 - exit 1 -fi -if [ x"$(cat $BUILDTESTDIR/xxx)" != "x$TESTTMPDIR/fonts/4x6.pcf" ]; then - echo "*** Test failed: $TEST" - echo "file property doesn't point to the new place: $TESTTMPDIR/fonts/4x6.pcf" - exit 1 -fi -if cmp "$BUILDTESTDIR"/flist1 "$BUILDTESTDIR"/flist2 > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "file properties doesn't point to the new places" - echo "Expected result:" - cat "$BUILDTESTDIR"/flist2 - echo "Actual result:" - cat "$BUILDTESTDIR"/flist1 - exit 1 -fi -rm -rf "$TESTTMPDIR" "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 "$BUILDTESTDIR"/xxx "$BUILDTESTDIR"/flist1 "$BUILDTESTDIR"/flist2 "$BUILDTESTDIR"/bind-fonts.conf - -dotest "Different directory content between host and sandbox" -prep -cp "$FONT1" "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -$FCCACHE "$FONTDIR" -sleep 1 -ls -1 --color=no "$CACHEDIR"/*cache*> "$BUILDTESTDIR"/out1 -fstat "%n %s %y %z" "$(cat $BUILDTESTDIR/out1)" > "$BUILDTESTDIR"/stat1 -TESTTMPDIR=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -TESTTMP2DIR=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -cp "$FONT2" "$TESTTMP2DIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$TESTTMP2DIR" -fi -sed "s!@FONTDIR@!$TESTTMPDIR/fonts</dir><dir salt="'"'"salt-to-make-different"'"'">$FONTDIR! -s!@REMAPDIR@!<remap-dir as-path="'"'"$FONTDIR"'"'">$TESTTMPDIR/fonts</remap-dir>! -s!@CACHEDIR@!$TESTTMPDIR/cache.dir!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/bind-fonts.conf -$BWRAP --bind / / --bind "$CACHEDIR" "$TESTTMPDIR"/cache.dir --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$TESTTMP2DIR" "$FONTDIR" --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev --setenv FONTCONFIG_FILE "$TESTTMPDIR"/build/test/bind-fonts.conf "$TESTTMPDIR"/build/fc-match/fc-match"$EXEEXT" -f "%{file}\n" ":foundry=Misc" > "$BUILDTESTDIR"/xxx -if test -x "$BUILDTESTDIR"/test-bz106618"$EXEEXT"; then - TESTEXE=test-bz106618"$EXEEXT" -elif test -x "$BUILDTESTDIR"/test_bz106618"$EXEEXT"; then - TESTEXE=test_bz106618"$EXEEXT" -else - echo "*** Test failed: no test case for bz106618" - exit 1 -fi -$BWRAP --bind / / --bind "$CACHEDIR" "$TESTTMPDIR"/cache.dir --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$TESTTMP2DIR" "$FONTDIR" --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev --setenv FONTCONFIG_FILE "$TESTTMPDIR"/build/test/bind-fonts.conf "$TESTTMPDIR"/build/test/"$TESTEXE" | sort > "$BUILDTESTDIR"/flist1 -$BWRAP --bind / / --bind "$CACHEDIR" "$TESTTMPDIR"/cache.dir --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$TESTTMP2DIR" "$FONTDIR" --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev find "$TESTTMPDIR"/fonts/ -type f -name '*.pcf' | sort > "$BUILDTESTDIR"/flist2 -ls -1 --color=no "$CACHEDIR"/*cache* > "$BUILDTESTDIR"/out2 -fstat "%n %s %y %z" "$(cat $BUILDTESTDIR/out1)" > "$BUILDTESTDIR"/stat2 -if cmp "$BUILDTESTDIR"/stat1 "$BUILDTESTDIR"/stat2 > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "cache was created/updated." - cat "$BUILDTESTDIR"/stat1 "$BUILDTESTDIR"/stat2 - exit 1 -fi -if grep -v -- "$(cat $BUILDTESTDIR/out1)" "$BUILDTESTDIR"/out2 > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "cache wasn't created for dir inside sandbox." - cat "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 - exit 1 -fi -if [ x"$(cat $BUILDTESTDIR/xxx)" != "x$TESTTMPDIR/fonts/4x6.pcf" ]; then - echo "*** Test failed: $TEST" - echo "file property doesn't point to the new place: $TESTTMPDIR/fonts/4x6.pcf" - exit 1 -fi -if cmp "$BUILDTESTDIR"/flist1 "$BUILDTESTDIR"/flist2 > /dev/null ; then - echo "*** Test failed: $TEST" - echo "Missing fonts should be available on sandbox" - echo "Expected result:" - cat "$BUILDTESTDIR"/flist2 - echo "Actual result:" - cat "$BUILDTESTDIR"/flist1 - exit 1 -fi -rm -rf "$TESTTMPDIR" "$TESTTMP2DIR" "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 "$BUILDTESTDIR"/xxx "$BUILDTESTDIR"/flist1 "$BUILDTESTDIR"/flist2 "$BUILDTESTDIR"/stat1 "$BUILDTESTDIR"/stat2 "$BUILDTESTDIR"/bind-fonts.conf - -dotest "Check consistency of MD5 in cache name" -prep -mkdir -p "$FONTDIR"/sub -cp "$FONT1" "$FONTDIR"/sub -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR"/sub -fi -$FCCACHE "$FONTDIR" -sleep 1 -(cd "$CACHEDIR"; ls -1 --color=no ./*cache*) > "$BUILDTESTDIR"/out1 -TESTTMPDIR=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -mkdir -p "$TESTTMPDIR"/cache.dir -# Once font dir is remapped, we could use $FONTDIR as different one in theory. -# but we don't use it here and to avoid duplicate entries, set the non-existing -# directory here. -sed "s!@FONTDIR@!$FONTDIR/a! -s!@REMAPDIR@!<remap-dir as-path="'"'"$FONTDIR"'"'">$TESTTMPDIR/fonts</remap-dir>! -s!@CACHEDIR@!$TESTTMPDIR/cache.dir!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/bind-fonts.conf -$BWRAP --bind / / --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev --setenv FONTCONFIG_FILE "$TESTTMPDIR"/build/test/bind-fonts.conf "$TESTTMPDIR"/build/fc-cache/fc-cache"$EXEEXT" "$TESTTMPDIR"/fonts -(cd "$TESTTMPDIR"/cache.dir; ls -1 --color=no ./*cache*) > "$BUILDTESTDIR"/out2 -if cmp "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "cache was created unexpectedly." - echo "Before:" - cat "$BUILDTESTDIR"/out1 - echo "After:" - cat "$BUILDTESTDIR"/out2 - exit 1 -fi -rm -rf "$TESTTMPDIR" "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 "$BUILDTESTDIR"/bind-fonts.conf - -dotest "Fallback to uuid" -prep -cp "$FONT1" "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -touch -t "$(fdate $(fstat "%Y" "$FONTDIR"))" "$FONTDIR" -$FCCACHE "$FONTDIR" -sleep 1 -_cache=$(ls -1 --color=no "$CACHEDIR"/*cache*) -_mtime=$(fstat "%Y" "$FONTDIR") -_uuid=$(uuidgen) -_newcache=$(echo "$_cache" | sed "s/\([0-9a-f]*\)\(\-.*\)/$_uuid\2/") -mv "$_cache" "$_newcache" -echo "$_uuid" > "$FONTDIR"/.uuid -touch -t "$(fdate "$_mtime")" "$FONTDIR" -(cd "$CACHEDIR"; ls -1 --color=no ./*cache*) > "$BUILDTESTDIR"/out1 -TESTTMPDIR=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -mkdir -p "$TESTTMPDIR"/cache.dir -sed "s!@FONTDIR@!$TESTTMPDIR/fonts! -s!@REMAPDIR@!<remap-dir as-path="'"'"$FONTDIR"'"'">$TESTTMPDIR/fonts</remap-dir>! -s!@CACHEDIR@!$TESTTMPDIR/cache.dir!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/bind-fonts.conf -$BWRAP --bind / / --bind "$CACHEDIR" "$TESTTMPDIR"/cache.dir --bind "$FONTDIR" "$TESTTMPDIR"/fonts --bind "$BUILDTESTDIR"/.. "$TESTTMPDIR"/build --dev-bind /dev /dev --setenv FONTCONFIG_FILE "$TESTTMPDIR"/build/test/bind-fonts.conf "$TESTTMPDIR"/build/fc-match/fc-match"$EXEEXT" -f "" -(cd "$CACHEDIR"; ls -1 --color=no ./*cache*) > "$BUILDTESTDIR"/out2 -if cmp "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "cache was created unexpectedly." - echo "Before:" - cat "$BUILDTESTDIR"/out1 - echo "After:" - cat "$BUILDTESTDIR"/out2 - exit 1 -fi -rm -rf "$TESTTMPDIR" "$BUILDTESTDIR"/out1 "$BUILDTESTDIR"/out2 "$BUILDTESTDIR"/bind-fonts.conf - -else - echo "No bubblewrap installed. skipping..." -fi # if [ x"$BWRAP" != "x" -a "x$EXEEXT" = "x" ] - -if [ "x$EXEEXT" = "x" ]; then -dotest "sysroot option" -prep -mkdir -p "$BUILDTESTDIR"/sysroot/"$FONTDIR" -mkdir -p "$BUILDTESTDIR"/sysroot/"$CACHEDIR" -mkdir -p "$BUILDTESTDIR"/sysroot/"$BUILDTESTDIR" -cp "$FONT1" "$BUILDTESTDIR"/sysroot/"$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$BUILDTESTDIR"/sysroot/"$FONTDIR" -fi -cp "$BUILDTESTDIR"/fonts.conf "$BUILDTESTDIR"/sysroot/"$BUILDTESTDIR"/fonts.conf -$FCCACHE -y "$BUILDTESTDIR"/sysroot - -dotest "creating cache file on sysroot" -md5=$(printf "%s" "$FONTDIR" | $MD5SUM | sed 's/ .*$//') -echo "checking for cache file $md5" -if ! ls "$BUILDTESTDIR/sysroot/$CACHEDIR/$md5"*; then - echo "*** Test failed: $TEST" - echo "No cache for $FONTDIR ($md5)" - ls "$BUILDTESTDIR"/sysroot/"$CACHEDIR" - exit 1 -fi - -rm -rf "$BUILDTESTDIR"/sysroot - -dotest "read newer caches when multiple places are allowed to store" -prep -cp "$FONT1" "$FONT2" "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then - # epoch 0 has special meaning. increase to avoid epoch 0 - old_epoch=${SOURCE_DATE_EPOCH} - SOURCE_DATE_EPOCH=$(("$SOURCE_DATE_EPOCH" + 1)) -fi -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -MYCACHEBASEDIR=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -MYCACHEDIR="$MYCACHEBASEDIR"/cache.dir -MYOWNCACHEDIR="$MYCACHEBASEDIR"/owncache.dir -MYCONFIG=$(mktemp "$TMPDIR"/fontconfig.XXXXXXXX) - -mkdir -p "$MYCACHEDIR" -mkdir -p "$MYOWNCACHEDIR" - -sed "s!@FONTDIR@!$FONTDIR! -s!@REMAPDIR@!! -s!@CACHEDIR@!$MYCACHEDIR!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/my-fonts.conf - -FONTCONFIG_FILE="$BUILDTESTDIR"/my-fonts.conf $FCCACHE "$FONTDIR" - -sleep 1 -cat<<EOF>"$MYCONFIG" -<fontconfig> - <match target="scan"> - <test name="file"><string>$FONTDIR/4x6.pcf</string></test> - <edit name="pixelsize"><int>8</int></edit> - </match> -</fontconfig> -EOF -sed "s!@FONTDIR@!$FONTDIR! -s!@REMAPDIR@!<include ignore_missing=\"yes\">$MYCONFIG</include>! -s!@CACHEDIR@!$MYOWNCACHEDIR!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/my-fonts.conf - -if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then - SOURCE_DATE_EPOCH=$(("$SOURCE_DATE_EPOCH" + 1)) -fi -FONTCONFIG_FILE="$BUILDTESTDIR"/my-fonts.conf $FCCACHE -f "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ]; then - SOURCE_DATE_EPOCH=${old_epoch} -fi - -sed "s!@FONTDIR@!$FONTDIR! -s!@REMAPDIR@!<include ignore_missing=\"yes\">$MYCONFIG</include>! -s!@CACHEDIR@!$MYCACHEDIR</cachedir><cachedir>$MYOWNCACHEDIR!" < "$TESTDIR"/fonts.conf.in > "$BUILDTESTDIR"/my-fonts.conf - -{ - FONTCONFIG_FILE="$BUILDTESTDIR"/my-fonts.conf $FCLIST - family pixelsize | sort; - echo "="; - FONTCONFIG_FILE="$BUILDTESTDIR"/my-fonts.conf $FCLIST - family pixelsize | sort; - echo "="; - FONTCONFIG_FILE="$BUILDTESTDIR"/my-fonts.conf $FCLIST - family pixelsize | sort; -} > "$BUILDTESTDIR"/my-out -tr -d '\015' <"$BUILDTESTDIR"/my-out >"$BUILDTESTDIR"/my-out.tmp; mv "$BUILDTESTDIR"/my-out.tmp "$BUILDTESTDIR"/my-out -sed -e 's/pixelsize=6/pixelsize=8/g' "$BUILDTESTDIR"/"$EXPECTED" > "$BUILDTESTDIR"/my-out.expected - -if cmp "$BUILDTESTDIR"/my-out "$BUILDTESTDIR"/my-out.expected > /dev/null ; then : ; else - echo "*** Test failed: $TEST" - echo "*** output is in 'my-out', expected output in 'my-out.expected'" - echo "Actual Result" - cat "$BUILDTESTDIR"/my-out - echo "Expected Result" - cat "$BUILDTESTDIR"/my-out.expected - exit 1 -fi - -rm -rf "$MYCACHEBASEDIR" "$MYCONFIG" "$BUILDTESTDIR"/my-fonts.conf "$BUILDTESTDIR"/my-out "$BUILDTESTDIR"/my-out.expected - -fi # if [ "x$EXEEXT" = "x" ] - -if [ -x "$BUILDTESTDIR"/test-crbug1004254 ]; then - dotest "MT-safe global config" - prep - curl -s -o "$FONTDIR"/noto.zip https://noto-website-2.storage.googleapis.com/pkgs/NotoSans-hinted.zip - (cd "$FONTDIR"; unzip noto.zip) - if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" - fi - "$BUILDTESTDIR"/test-crbug1004254 -else - echo "No test-crbug1004254: skipped" -fi - -if [ "x$EXEEXT" = "x" ]; then - -dotest "empty XDG_CACHE_HOME" -prep -export XDG_CACHE_HOME="" -export old_HOME="$HOME" -export temp_HOME=$(mktemp -d "$TMPDIR"/fontconfig.XXXXXXXX) -export HOME="$temp_HOME" -cp "$FONT1" "$FONT2" "$FONTDIR" -if [ -n "${SOURCE_DATE_EPOCH:-}" ] && [ ${#SOURCE_DATE_EPOCH} -gt 0 ]; then - touch -m -t "$(fdate ${SOURCE_DATE_EPOCH})" "$FONTDIR" -fi -echo "<fontconfig><dir>$FONTDIR</dir><cachedir prefix=\"xdg\">fontconfig</cachedir></fontconfig>" > "$BUILDTESTDIR"/my-fonts.conf -FONTCONFIG_FILE="$BUILDTESTDIR"/my-fonts.conf $FCCACHE "$FONTDIR" || : -if [ -d "$HOME"/.cache ] && [ -d "$HOME"/.cache/fontconfig ]; then : ; else - echo "*** Test failed: $TEST" - echo "No \$HOME/.cache/fontconfig directory" - ls -a "$HOME" - ls -a "$HOME"/.cache - exit 1 -fi - -export HOME="$old_HOME" -rm -rf "$temp_HOME" "$BUILDTESTDIR"/my-fonts.conf -unset XDG_CACHE_HOME -unset old_HOME -unset temp_HOME - -fi # if [ "x$EXEEXT" = "x" ] - -rm -rf "$FONTDIR" "$CACHEFILE" "$CACHEDIR" "$BASEDIR" "$FONTCONFIG_FILE" out - -TEST="" diff --git a/test/test_basic.py b/test/test_basic.py new file mode 100644 index 0000000..e24d2ae --- /dev/null +++ b/test/test_basic.py @@ -0,0 +1,337 @@ +# Copyright (C) 2025 fontconfig Authors +# SPDX-License-Identifier: HPND + +from fctest import FcTest, FcTestFont +from pathlib import Path +from tempfile import TemporaryDirectory, NamedTemporaryFile +import os +import pytest +import time +import types + + +@pytest.fixture +def fctest(): + return FcTest() + + +@pytest.fixture +def fcfont(): + return FcTestFont() + + +def test_basic(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts, '.') + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + + +def test_subdir(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts, 'a') + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + +def test_subdir_with_cache(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts, 'a') + for ret, stdout, stderr in fctest.run_cache([str(Path(fctest.fontdir.name) / 'a')]): + assert ret == 0, stderr + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + + +def test_with_dotfiles(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts, '.') + for f in Path(fctest.fontdir.name).glob('*.pcf'): + f.rename(f.parent / ('.' + f.name)) + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + + +def test_with_dotdir(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts, '.a') + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + + +def test_with_complicated_dir_structure(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts[0], Path('a') / 'a') + fctest.install_font(fcfont.fonts[1], Path('b') / 'b') + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + + +def test_subdir_with_out_of_date_cache(fctest, fcfont): + fctest.setup() + fctest.install_font([], 'a') + for ret, stdout, stderr in fctest.run_cache([str(Path(fctest.fontdir.name) / 'a')]): + assert ret == 0, stderr + time.sleep(1) + fctest.install_font(fcfont.fonts, 'a') + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + + +def test_new_file_with_out_of_date_cache(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts[0], '.') + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + time.sleep(1) + fctest.install_font(fcfont.fonts[1], 'a') + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + out_expected = f.read() + assert out == out_expected + + +def test_keep_mtime(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts, '.', 0) + fontdir = Path(fctest.fontdir.name) + before = fontdir.stat().st_mtime + time.sleep(1) + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + after = fontdir.stat().st_mtime + assert before == after, f'mtime {before} was changed to {after}' + + +def test_multiple_caches(fctest, fcfont): + extraconffile = NamedTemporaryFile(prefix='fontconfig.', + suffix='.extra.conf', + mode='w', + delete_on_close=False) + fctest._extra.append(f'<include ignore_missing="yes">{extraconffile.name}</include>') + + # Set up for generating original caches + fctest.setup() + origepoch = epoch = os.getenv('SOURCE_DATE_EPOCH') + if epoch: + # epoch 0 has special meaning. increase to avoid epoch 0 + epoch = int(epoch) + 1 + fctest.install_font(fcfont.fonts, '.', epoch) + if epoch: + fctest._env['SOURCE_DATE_EPOCH'] = str(epoch) + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + time.sleep(1) + + cache_files1 = [f.stat() for f in fctest.cache_files()] + assert len(cache_files1) == 1, cache_files1 + + # Set up for modified caches + oldcachedir = fctest.cachedir + oldconffile = fctest._conffile + newcachedir = TemporaryDirectory(prefix='fontconfig.', + suffix='.newcachedir') + newconffile = NamedTemporaryFile(prefix='fontconfig.', + suffix='.new.conf', + mode='w', + delete_on_close=False) + fctest._cachedir = newcachedir + fctest._conffile = newconffile + fctest.setup() + + extraconffile.write(f''' +<fontconfig> + <match target="scan"> + <test name="file"><string>{fctest.fontdir.name}/4x6.pcf</string></test> + <edit name="pixelsize"><int>8</int></edit> + </match> +</fontconfig>''') + extraconffile.close() + if epoch: + fctest._env['SOURCE_DATE_EPOCH'] = str(epoch + 1) + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + if epoch: + fctest._env['SOURCE_DATE_EPOCH'] = origepoch + cache_files2 = [f.stat() for f in fctest.cache_files()] + assert len(cache_files2) == 1, cache_files2 + # Make sure if 1 and 2 is different + assert cache_files1 != cache_files2 + + ## Set up for mixed caches + mixedconffile = NamedTemporaryFile(prefix='fontconfig.', + suffix='.mixed.conf', + mode='w', + delete_on_close=False) + fctest._cachedir = oldcachedir + fctest._conffile = mixedconffile + fctest._extra.append(f'<cachedir>{newcachedir.name}</cachedir>') + fctest.setup() + l = [] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += ['='] + for ret, stdout, stderr in fctest.run_list(['-', 'family', 'pixelsize']): + assert ret == 0, stderr + l += sorted(stdout.splitlines()) + l += [''] + out = '\n'.join(l) + with open(Path(fctest.builddir) / 'test' / 'out.expected') as f: + s = f.read() + out_expected = s.replace('pixelsize=6', 'pixelsize=8') + assert out == out_expected + + del oldconffile + + +@pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32') +def test_xdg_cache_home(fctest, fcfont): + fctest._env['XDG_CACHE_HOME'] = '' + old_home = os.getenv('HOME') + new_home = TemporaryDirectory(prefix='fontconfig.', + suffix='.home') + fctest._env['HOME'] = new_home.name + + fctest.install_font(fcfont.fonts, '.') + + def custom_config(self): + return f'<fontconfig><dir>{fctest.fontdir.name}</dir><cachedir prefix="xdg">fontconfig</cachedir></fontconfig>' + + fctest.config = types.MethodType(custom_config, fctest) + fctest.setup() + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + + phome = Path(new_home.name) + assert (phome / '.cache').exists() + assert (phome / '.cache' / 'fontconfig').exists() + cache_files = [f.name for f in (phome / '.cache' / 'fontconfig').glob('*cache*')] + assert len(cache_files) == 1 diff --git a/test/test_conf.py b/test/test_conf.py new file mode 100644 index 0000000..b4840ff --- /dev/null +++ b/test/test_conf.py @@ -0,0 +1,72 @@ +# Copyright (C) 2025 fontconfig Authors +# SPDX-License-Identifier: HPND + +from fctest import FcTest, FcTestFont +from operator import attrgetter +from pathlib import Path +from tempfile import TemporaryDirectory +import os +import pytest +import re +import shutil +import tempfile +import time +import types + + +@pytest.fixture +def fctest(): + return FcTest() + + +def dict_conf_with_json(): + srcdir = os.getenv('srcdir', Path(__file__).parent.parent) + ret = {} + for fn in (Path(srcdir) / 'conf.d').glob('*.conf'): + json = Path(srcdir) / 'test' / ('test-' + fn.stem + '.json') + if json.exists(): + ret[str(fn)] = str(json) + + return ret + + +def list_json(): + srcdir = os.getenv('srcdir', Path(__file__).parent.parent) + pairs = list(dict_conf_with_json().values()) + FcTest().logger.info(pairs) + ret = [] + for fn in (Path(srcdir) / 'test').glob('test-*.json'): + if str(fn) not in pairs: + ret.append(str(fn)) + + return ret + + +@pytest.mark.parametrize( + 'conf, json', + [(k, v) for k, v in dict_conf_with_json().items()], + ids=lambda x: Path(x).name) +def test_pair_of_conf_and_json(fctest, conf, json): + testexe = Path(fctest.builddir) / 'test' / ('test-conf' + fctest._exeext) + if not testexe.exists(): + testexe = Path(fctest.builddir) / 'test' / ('test_conf' + fctest._exeext) + if not testexe.exists(): + pytest.skip('No test executable. maybe missing json-c dependency?') + + for ret, stdout, stderr in fctest.run(testexe, [conf, json]): + assert ret == 0, f'stdout:\n{stdout}\nstderr:\n{stderr}' + fctest.logger.info(stdout) + + +@pytest.mark.parametrize('json', list_json(), ids=lambda x: Path(x).name) +def test_json(fctest, json): + testexe = Path(fctest.builddir) / 'test' / ('test-conf' + fctest._exeext) + if not testexe.exists(): + testexe = Path(fctest.builddir) / 'test' / ('test_conf' + fctest._exeext) + if not testexe.exists(): + pytest.skip('No test executable. maybe missing json-c dependency?') + harmlessconf = str(Path(fctest.srcdir) / 'conf.d' / '10-autohint.conf') + + for ret, stdout, stderr in fctest.run(testexe, [harmlessconf, json]): + assert ret == 0, f'stdout:\n{stdout}\nstderr:\n{stderr}' + fctest.logger.info(stdout) diff --git a/test/test_crbug1004254.py b/test/test_crbug1004254.py new file mode 100644 index 0000000..b843c19 --- /dev/null +++ b/test/test_crbug1004254.py @@ -0,0 +1,46 @@ +# Copyright (C) 2025 fontconfig Authors +# SPDX-License-Identifier: HPND + +from fctest import FcTest, FcTestFont +from pathlib import Path +from tempfile import TemporaryDirectory, NamedTemporaryFile +import os +import pytest +import time +import types + + +@pytest.fixture +def fctest(): + return FcTest() + + +@pytest.fixture +def fcfont(): + return FcTestFont() + + +@pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32') +def test_crbug1004254(fctest, fcfont): + builddir = Path(fctest.builddir) + def custom_config(self): + return f''' +<fontconfig> + <dir>{builddir.resolve()}/testfonts</dir> + <cachedir>{fctest.cachedir.name}</cachedir> +</fontconfig>''' + + fctest.config = types.MethodType(custom_config, fctest) + fctest.setup() + + testexe = Path(fctest.builddir) / 'test' / ('test-crbug1004254' + fctest._exeext) + if not testexe.exists(): + testexe = Path(fctest.builddir) / 'test' / ('test_crbug1004254' + fctest._exeext) + if not testexe.exists(): + raise RuntimeError('No test case for crbug1004254') + + for ret, stdout, stderr in fctest.run(testexe): + assert ret == 0, stderr + fctest.logger.info(stdout) + fctest.logger.info(stderr) + fctest.logger.info(fctest.config()) diff --git a/test/test_sandbox.py b/test/test_sandbox.py new file mode 100644 index 0000000..4732fb6 --- /dev/null +++ b/test/test_sandbox.py @@ -0,0 +1,166 @@ +# Copyright (C) 2025 fontconfig Authors +# SPDX-License-Identifier: HPND + +from fctest import FcTest, FcTestFont +from operator import attrgetter +from pathlib import Path +from tempfile import TemporaryDirectory +import os +import pytest +import re +import shutil +import tempfile +import time +import types + + +@pytest.fixture +def fctest(): + return FcTest() + + +@pytest.fixture +def fcfont(): + return FcTestFont() + + +@pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32') +@pytest.mark.skipif(not shutil.which('bwrap'), reason='No bwrap installed') +def test_bz106618(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts, '.') + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + time.sleep(1) + cache_stat_before = [f.stat() for f in fctest.cache_files()] + cache_stat_after = [] + basedir = tempfile.TemporaryDirectory(prefix='fontconfig.', + suffix='.base') + with fctest.sandboxed(basedir.name) as f: + # Test if font is visible on sandbox + for ret, stdout, stderr in f.run_match(['-f', '%{file}\n', + ':foundry=Misc']): + assert ret == 0, stderr + out = list(filter(None, stdout.splitlines())) + assert len(out) == 1, out + assert re.match(str(Path(f._remapped_fontdir.name) / '4x6.pcf'), + out[0]) + testexe = Path(f.builddir) / 'test' / ('test-bz106618' + fctest._exeext) + if not testexe.exists(): + testexe = Path(f.builddir) / 'test' / ('test_bz106618' + fctest._exeext) + if not testexe.exists(): + raise RuntimeError('No test case for bz106618') + flist1 = [] + for ret, stdout, stderr in f.run(Path(f._remapped_builddir.name) / 'test' / testexe.name): + assert ret == 0, stderr + flist1 = sorted(stdout.splitlines()) + # convert path to bind-mounted one + flist2 = sorted(map(lambda x: str(x) if Path(x).parent != Path(f.fontdir.name) else str(Path(f._remapped_fontdir.name) / Path(x).name), Path(f.fontdir.name).glob('*.pcf'))) + assert flist1 == flist2 + + # Check if cache files isn't created over bind-mounted dir again + cache_stat_after = [f.stat() for f in fctest.cache_files()] + assert len(cache_stat_before) == len(cache_stat_after) + # ignore st_atime + cmp_cache_before = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat_before] + cmp_cache_after = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat_after] + assert cmp_cache_before == cmp_cache_after + + +@pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32') +@pytest.mark.skipif(not shutil.which('bwrap'), reason='No bwrap installed') +def test_different_content(fctest, fcfont): + ''' + Make sure if fontdir where sandbox has own fonts is handled + differently even if they are same directory name for remapped + ''' + fctest.setup() + fctest.install_font(fcfont.fonts[0], '.') + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + time.sleep(1) + cache_stat_before = [f.stat() for f in fctest.cache_files()] + sbox_fontdir = TemporaryDirectory(prefix='fontconfig.', + suffix='.fontdir') + sbox_cachedir = TemporaryDirectory(prefix='fontconfig.', + suffix='.cachedir') + sbox_basedir = TemporaryDirectory(prefix='fontconfig.', + suffix='.basedir') + fontdir2 = TemporaryDirectory(prefix='fontconfig.', + suffix='.fontdir', + dir=sbox_basedir.name) + fctest.install_font(fcfont.fonts[1], fontdir2.name) + fontdir = fctest.fontdir + + def custom_config(self): + return f'<fontconfig><remap-dir as-path="{fontdir.name}">{sbox_fontdir.name}</remap-dir><dir>{sbox_fontdir.name}</dir><dir salt="salt-to-make-difference">{fontdir.name}</dir><cachedir>{sbox_cachedir.name}</cachedir></fontconfig>' + + fctest.config = types.MethodType(custom_config, fctest) + bind = { + # remap to share a host cache + fctest.fontdir.name: sbox_fontdir.name, + # sandbox has own font on same directory like host but different fonts + fontdir2.name: fctest.fontdir.name + } + with fctest.sandboxed(sbox_basedir.name, bind=bind) as f: + # Test if font is visible on sandbox + for ret, stdout, stderr in f.run_match(['-f', '%{file}\n', + ':foundry=Misc']): + assert ret == 0, stderr + out = list(filter(None, stdout.splitlines())) + assert len(out) == 1, out + assert re.match(str(Path(sbox_fontdir.name) / '4x6.pcf'), + out[0]) + testexe = Path(f.builddir) / 'test' / ('test-bz106618' + fctest._exeext) + if not testexe.exists(): + testexe = Path(f.builddir) / 'test' / ('test_bz106618' + fctest._exeext) + if not testexe.exists(): + raise RuntimeError('No test case for bz106618') + flist1 = [] + for ret, stdout, stderr in f.run(Path(f._remapped_builddir.name) / 'test' / testexe.name): + assert ret == 0, stderr + flist1 = sorted(stdout.splitlines()) + # convert path to bind-mounted one + flist2 = list(map(lambda x: str(x) if Path(x).parent != Path(f.fontdir.name) else str(Path(sbox_fontdir.name) / Path(x).name), Path(f.fontdir.name).glob('*.pcf'))) + flist2 += list(map(lambda x: str(x) if Path(x).parent != Path(fontdir2.name) else str(Path(fontdir.name) / Path(x).name), Path(fontdir2.name).glob('*.pcf'))) + flist2 = sorted(flist2) + assert len(flist1) == 2, flist1 + assert flist1 == flist2 + + # Check if cache files isn't created over bind-mounted dir again + cache_stat_after = [f.stat() for f in fctest.cache_files()] + assert len(cache_stat_before) == len(cache_stat_after) + # ignore st_atime + cmp_cache_before = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat_before] + cmp_cache_after = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat_after] + assert cmp_cache_before == cmp_cache_after + + +@pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32') +@pytest.mark.skipif(not shutil.which('bwrap'), reason='No bwrap installed') +def test_md5_consistency(fctest, fcfont): + fctest.setup() + fctest.install_font(fcfont.fonts[0], 'sub') + for ret, stdout, stderr in fctest.run_cache([fctest.fontdir.name]): + assert ret == 0, stderr + time.sleep(1) + cache_files_before = [f.name for f in fctest.cache_files()] + cache_stat_before = [f.stat() for f in fctest.cache_files()] + cachedir2 = TemporaryDirectory(prefix='fontconfig.', + suffix='.cachedir') + basedir = TemporaryDirectory(prefix='fontconfig.', + suffix='.base') + orig_cachedir = fctest.cachedir + fctest._cachedir = cachedir2 + with fctest.sandboxed(basedir.name) as f: + for ret, stdout, stderr in f.run_cache([f._remapped_fontdir.name]): + assert ret == 0, stderr + cache_files_after = [c.name for c in fctest.cache_files()] + cache_stat_after = [c.stat() for c in fctest.cache_files()] + # ignore st_atime + cmp_cache_before = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat_before] + cmp_cache_after = [attrgetter('st_mode', 'st_ino', 'st_dev', 'st_nlink', 'st_uid', 'st_gid', 'st_size', 'st_mtime', 'st_ctime')(st) for st in cache_stat_after] + + # Make sure they are totally different but same filename + assert cache_files_before == cache_files_after + assert cmp_cache_before != cmp_cache_after diff --git a/test/test_sysroot.py b/test/test_sysroot.py new file mode 100644 index 0000000..9b8fcb1 --- /dev/null +++ b/test/test_sysroot.py @@ -0,0 +1,46 @@ +# Copyright (C) 2025 fontconfig Authors +# SPDX-License-Identifier: HPND + +from fctest import FcTest, FcTestFont +from pathlib import Path +from tempfile import TemporaryDirectory +import hashlib +import os +import pytest +import shutil + + +@pytest.fixture +def fctest(): + return FcTest() + + +@pytest.fixture +def fcfont(): + return FcTestFont() + + +@pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32') +def test_sysroot(fctest, fcfont): + basedir = TemporaryDirectory(prefix='fontconfig.', + suffix='.sysroot') + cachedir = Path(basedir.name) / Path(fctest.cachedir.name).relative_to('/') + cachedir.mkdir(parents=True, exist_ok=True) + fontdir = Path(basedir.name) / Path(fctest.fontdir.name).relative_to('/') + fontdir.mkdir(parents=True, exist_ok=True) + configdir = Path(basedir.name) / Path(fctest._conffile.name).parent.relative_to('/') + configdir.mkdir(parents=True, exist_ok=True) + fctest.setup() + fctest.install_font(fcfont.fonts[0], fontdir) + shutil.copy2(fctest._conffile.name, configdir / Path(fctest._conffile.name).name) + for ret, stdout, stderr in fctest.run_cache(['-y', basedir.name]): + assert ret == 0, stderr + font_files = [fn.name for fn in fontdir.glob('*.pcf')] + assert len(font_files) == 1, font_files + cache_files = [c.name for c in cachedir.glob('*cache*')] + assert len(cache_files) == 1, cache_files + + md5 = hashlib.md5() + md5.update(fctest.fontdir.name.encode('utf-8')) + cache_files_based_on_md5 = [c.name for c in cachedir.glob(f'{md5.hexdigest()}*')] + assert len(cache_files_based_on_md5) == 1, cache_files_based_on_md5 diff --git a/test/wrapper-script.sh b/test/wrapper-script.sh index 94dcb85..c9820a9 100755 --- a/test/wrapper-script.sh +++ b/test/wrapper-script.sh @@ -1,5 +1,7 @@ #! /bin/bash +set -e + CC=${CC:-gcc} case "$1" in commit c6e9f54211f37b3129a1e5dfa11c714af6cc4ea7 Author: Akira TAGOH <akira@xxxxxxxxx> Date: Thu Jun 12 20:31:46 2025 +0900 test: add common helper class diff --git a/test/fctest/__init__.py b/test/fctest/__init__.py new file mode 100644 index 0000000..ea73fc6 --- /dev/null +++ b/test/fctest/__init__.py @@ -0,0 +1,161 @@ +# Copyright (C) 2025 fontconfig Authors +# SPDX-License-Identifier: HPND + +from pathlib import Path +from tempfile import TemporaryDirectory, NamedTemporaryFile +from typing import Iterator +import os +import shutil +import subprocess + + +class FcTest: + + def __init__(self): + self._env = os.environ.copy() + self._fontdir = TemporaryDirectory(prefix='fontconfig.', + suffix='.fontdir') + self._cachedir = TemporaryDirectory(prefix='fontconfig.', + suffix='.cachedir') + self._conffile = NamedTemporaryFile(prefix='fontconfig.', + suffix='.conf', + mode='w', + delete_on_close=False) + self._builddir = self._env.get('builddir', 'build') + self._srcdir = self._env.get('srcdir', '.') + exeext = self._env.get('EXEEXT', '') + self._exewrapper = self._env.get('EXEWRAPPER', None) + self._fccache = Path(self.builddir) / 'fc-cache' / ('fc-cache' + exeext) + if not self._fccache.exists(): + raise RuntimeError('No fc-cache binary. builddir might be wrong:' + f' {self._fccache}') + self._fccat = Path(self.builddir) / 'fc-cat' / ('fc-cat' + exeext) + if not self._fccat.exists(): + raise RuntimeError('No fc-cat binary. builddir might be wrong:' + f' {self._fccat}') + self._fclist = Path(self.builddir) / 'fc-list' / ('fc-list' + exeext) + if not self._fclist.exists(): + raise RuntimeError('No fc-list binary. builddir might be wrong:' + f' {self._fclist}') + self._fcmatch = Path(self.builddir) / 'fc-match' / ('fc-match' + exeext) + if not self._fcmatch.exists(): + raise RuntimeError('No fc-match binary. builddir might be wrong:' + f' {self._fcmatch}') + self._fcpattern = Path(self.builddir) / 'fc-pattern' / ('fc-pattern' + exeext) + if not self._fcpattern.exists(): + raise RuntimeError('No fc-pattern binary. builddir might be wrong:' + f' {self._fcpattern}') + self._fcquery = Path(self.builddir) / 'fc-query' / ('fc-query' + exeext) + if not self._fcquery.exists(): + raise RuntimeError('No fc-query binary. builddir might be wrong:' + f' {self._fcquery}') + self._fcscan = Path(self.builddir) / 'fc-scan' / ('fc-scan' + exeext) + if not self._fcscan.exists(): + raise RuntimeError('No fc-scan binary. builddir might be wrong:' + f' {self._fcscan}') + self._fcvalidate = Path(self.builddir) / 'fc-validate' / ('fc-validate' + exeext) + if not self._fcvalidate.exists(): + raise RuntimeError('No fc-validate binary. builddir might be wrong:' + f' {self._fcvalidate}') + self._extra = '' + self.__conf_templ = ''' + <fontconfig> + {extra} + <dir>{fontdir}</dir> + <cachedir>{cachedir}</cachedir> + </fontconfig> + ''' + + def __del__(self): + del self._conffile + del self._fontdir + del self._cachedir + + @property + def builddir(self): + return self._builddir + + @property + def srcdir(self): + return self._srcdir + + @property + def fontdir(self): + return self._fontdir + + @property + def cachedir(self): + return self._cachedir + + @property + def extra(self): + return self._extra + + @property + def config(self): + return self.__conf_templ.format(fontdir=self.fontdir.name, + cachedir=self.cachedir.name, + extra=self.extra) + + def setup(self): + self._conffile.write(self.config) + self._conffile.close() + self._env['FONTCONFIG_FILE'] = self._conffile.name + + def install_font(self, files, dest): + if not isinstance(files, list): + files = [files] + time = self._env.get('SOURCE_DATE_EPOCH', None) + + for f in files: + fn = Path(f).name + dname = Path(self.fontdir.name) / dest / fn + shutil.copy2(f, dname) + if time: + os.utime(str(dname), (time, time)) + + if time: + os.utime(self.fontdir.name, (time, time)) + + def run(self, binary, args) -> Iterator[[int, str, str]]: + cmd = [] + if self._exewrapper: + cmd += self._exewrapper + cmd += [str(binary)] + cmd += args + res = subprocess.run(cmd, check=True, capture_output=True, + env=self._env) + yield res.returncode, res.stdout.decode('utf-8'), res.stderr.decode('utf-8') + + def run_cache(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fccache, args) + + def run_cat(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fccat, args) + + def run_list(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fclist, args) + + def run_match(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fcmatch, args) + + def run_pattern(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fcpattern, args) + + def run_query(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fcquery, args) + + def run_scan(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fcscan, args) + + def run_validate(self, args) -> Iterator[[int, str, str]]: + return self.run(self._fcvalidate, args) + + +if __name__ == '__main__': + f = FcTest() + print(f.fontdir.name) + print(f.cachedir.name) + print(f._conffile.name) + print(f.config) + f.setup()