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