diff --git a/kitty_tests/__init__.py b/kitty_tests/__init__.py index abfbf8d2c..7bcafed21 100644 --- a/kitty_tests/__init__.py +++ b/kitty_tests/__init__.py @@ -4,3 +4,8 @@ import os is_ci = os.environ.get('CI') == 'true' + + +def in_isolated_test_env() -> bool: + """Whether HOME and the XDG dirs point at the throwaway directory created by env_for_python_tests().""" + return 'KT_ORIGINAL_HOME' in os.environ diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 4d094c880..bfd92009a 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -41,9 +41,19 @@ from kitty.utils import ( shlex_split_with_positions, ) +from . import in_isolated_test_env from .base import BaseTest, filled_cursor, filled_history_buf, filled_line_buf +def rmtree_in_test_home(path): + # Guard against destroying the invoking user's data if the test suite is ever + # run without the isolated $HOME set up by env_for_python_tests() + if not in_isolated_test_env(): + raise AssertionError(f'Refusing to delete {path} as the test suite is not running with an isolated HOME') + if os.path.exists(path): + shutil.rmtree(path) + + def create_lbuf(*lines): maxw = max(map(len, lines)) ans = LineBuf(len(lines), maxw) @@ -692,8 +702,7 @@ class TestDataTypes(BaseTest): saved = {x: os.environ.get(x) for x in 'KITTY_CONFIG_DIRECTORY XDG_CONFIG_DIRS XDG_CONFIG_HOME'.split()} try: dot_config = os.path.expanduser('~/.config') - if os.path.exists(dot_config): - shutil.rmtree(dot_config) + rmtree_in_test_home(dot_config) with tempfile.TemporaryDirectory() as tdir: with open(tdir + '/macos-launch-services-cmdline', 'w') as f: print('kitty --title from-file', file=f) @@ -736,8 +745,7 @@ class TestDataTypes(BaseTest): os.environ[k] = v self.assertEqual(x[-1], get_config_dir(), str(x)) finally: - if os.path.exists(dot_config): - shutil.rmtree(dot_config) + rmtree_in_test_home(dot_config) for k in saved: os.environ.pop(k, None) if saved[k] is not None: diff --git a/kitty_tests/main.py b/kitty_tests/main.py index 6858e7087..571b160ea 100644 --- a/kitty_tests/main.py +++ b/kitty_tests/main.py @@ -225,21 +225,29 @@ def env_vars(**kw: str) -> Iterator[None]: os.environ[k] = v -@contextmanager -def env_for_python_tests(report_env: bool = False) -> Iterator[None]: - gohome = os.path.expanduser('~/go') - slangc = os.environ.get('SLANGC') or shutil.which('slangc') or 'slangc' +def path_for_python_tests() -> str: + """The PATH used by tests: the built launcher dir followed by all entries not under the real home dir.""" current_home = os.path.expanduser('~') + os.sep paths = os.environ.get('PATH', '/usr/local/sbin:/usr/local/bin:/usr/bin').split(os.pathsep) path = os.pathsep.join(x for x in paths if not x.startswith(current_home)) launcher_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'kitty', 'launcher') - path = f'{launcher_dir}{os.pathsep}{path}' - if report_env: - print('Running under CI:', is_ci) - print('Using PATH in test environment:', path) - from kitty.fast_data_types import has_avx2, has_avx512, has_sse4_2 + return f'{launcher_dir}{os.pathsep}{path}' - print(f'Intrinsics: {has_avx512=} {has_avx2=} {has_sse4_2=}') + +def report_test_env() -> None: + """Print details of the environment tests will run in. Called once in the master process as workers have their stdout captured.""" + print('Running under CI:', is_ci) + print('Using PATH in test environment:', path_for_python_tests()) + from kitty.fast_data_types import has_avx2, has_avx512, has_sse4_2 + + print(f'Intrinsics: {has_avx512=} {has_avx2=} {has_sse4_2=}') + + +@contextmanager +def env_for_python_tests() -> Iterator[None]: + gohome = os.path.expanduser('~/go') + slangc = os.environ.get('SLANGC') or shutil.which('slangc') or 'slangc' + path = path_for_python_tests() with ( TemporaryDirectory() as tdir, env_vars( @@ -759,8 +767,11 @@ def run_tests(report_env: bool = False) -> None: if args.name and not tests_list and not has_go: raise SystemExit('No test named %s found' % ' '.join(args.name)) - # Start Python workers before modifying the main-process env; each worker - # calls env_for_python_tests independently for full HOME/XDG isolation. + if report_env: + report_test_env() + + # Start Python workers; each worker calls env_for_python_tests independently + # for full HOME/XDG isolation, as does the serial path below. # On macOS fork()+threading is unsafe, so use subprocess workers there. use_parallel = len(tests_list) > PARALLEL_THRESHOLD worker_pids: list[int] = [] @@ -795,7 +806,15 @@ def run_tests(report_env: bool = False) -> None: if use_parallel: python_ok, go_ok = collect_worker_results(worker_pids, worker_procs, read_fds, len(tests_list), go_proc=go_proc) elif tests_list: - python_ok = run_cli(all_tests, args.verbosity) + # we need fonts installed in the user home directory as well, so initialize + # fontconfig before nuking $HOME and friends + from kitty.fonts.common import all_fonts_map + + all_fonts_map(True) + # Tests must never run against the real $HOME, some of them delete + # files/directories in it. See run_test_worker() for the parallel equivalent. + with env_for_python_tests(): + python_ok = run_cli(all_tests, args.verbosity) if go_proc is not None: _, go_ok = collect_worker_results([], [], [], 0, go_proc=go_proc) else: