fontconfig: Branch 'main' - 5 commits
GitLab Mirror
gitlab-mirror at kemper.freedesktop.org
Wed Jul 9 09:51:47 UTC 2025
.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 at tagoh.org>
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 at tagoh.org>
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 at tagoh.org>
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))
+ at 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
-
-
- at pytest.mark.parametrize("font_file", list_test_fonts())
-def test_fontations_freetype_fcquery_equal(font_file):
- print(f"Testing with: {font_file}") # Example usage
+ at 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))
+ at 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 at tagoh.org>
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
+
+
+ at pytest.fixture
+def fctest():
+ return FcTest()
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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
+
+
+ at 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)
+
+
+ at 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
+
+
+ at pytest.fixture
+def fctest():
+ return FcTest()
+
+
+ at pytest.fixture
+def fcfont():
+ return FcTestFont()
+
+
+ at 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
+
+
+ at pytest.fixture
+def fctest():
+ return FcTest()
+
+
+ at pytest.fixture
+def fcfont():
+ return FcTestFont()
+
+
+ at pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32')
+ at 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
+
+
+ at pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32')
+ at 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
+
+
+ at pytest.mark.skipif(not not os.getenv('EXEEXT'), reason='not working on Win32')
+ at 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
+
+
+ at pytest.fixture
+def fctest():
+ return FcTest()
+
+
+ at pytest.fixture
+def fcfont():
+ return FcTestFont()
+
+
+ at 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 at tagoh.org>
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()
More information about the Fontconfig
mailing list