From e1e5dfdd62f010db71146aebb86e87c5bd62f7c6 Mon Sep 17 00:00:00 2001 From: Johannes Altmanninger Date: Sat, 11 Oct 2025 16:31:06 +0200 Subject: [PATCH] test_driver: don't chdir in async driver The test driver is async now; so we can't change the process-wide working directory without causing unpredictable behavior. For example, given these two tests: $ cat tests/checks/a.fish #RUN: %fish %s #REQUIRES: sleep 1 pwd >/tmp/pwd-of-a $ cat tests/checks/b.fish #RUN: %fish %s pwd >/tmp/pwd-of-b running them may give both fish processes the same working directory. $ tests/test_driver.py target/debug tests/checks/a.fish tests/checks/b.fish tests/checks/b.fish PASSED 13 ms tests/checks/a.fish PASSED 1019 ms 2 / 2 passed (0 skipped) $ grep . /tmp/pwd-of-a /tmp/pwd-of-b /tmp/pwd-of-a:/tmp/fishtest-root-1q_tnyqa/fishtest-s9cyqkgz /tmp/pwd-of-b:/tmp/fishtest-root-1q_tnyqa/fishtest-s9cyqkgz --- tests/littlecheck.py | 25 ++++++++++++++++--------- tests/test_driver.py | 9 +++++++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/tests/littlecheck.py b/tests/littlecheck.py index e72ea76fb..a275c7dfd 100755 --- a/tests/littlecheck.py +++ b/tests/littlecheck.py @@ -387,16 +387,16 @@ def runproc(cmd, env=None): return asyncio.run(runproc_async(cmd, env=env)) -async def runproc_async(cmd, env=None): +async def runproc_async(cmd, env=None, cwd=None): """Wrapper around subprocess.Popen to save typing""" PIPE = asyncio.subprocess.PIPE return await asyncio.create_subprocess_shell( - cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env + cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env, cwd=cwd ) class TestRun(object): - def __init__(self, name, runcmd, checker, subs, config, env=None): + def __init__(self, name, runcmd, checker, subs, config, env=None, cwd=None): self.name = name self.runcmd = runcmd self.subbed_command = perform_substitution(runcmd.args, subs) @@ -404,6 +404,7 @@ class TestRun(object): self.subs = subs self.config = config self.env = env + self.cwd = cwd def check(self, lines, checks): # Reverse our lines and checks so we can pop off the end. @@ -493,7 +494,7 @@ class TestRun(object): if self.config.verbose: print(self.subbed_command) - proc = await runproc_async(self.subbed_command, env=self.env) + proc = await runproc_async(self.subbed_command, env=self.env, cwd=self.cwd) stdout, stderr = await proc.communicate() # Work around type system limitations / bad API design which makes the typechecker unhappy. @@ -684,7 +685,9 @@ def check_file(input_file, name, subs, config, failure_handler, env=None): ) -async def check_file_async(input_file, name, subs, config, failure_handler, env=None): +async def check_file_async( + input_file, name, subs, config, failure_handler, env=None, cwd=None +): """Check a single file. Return a True on success, False on error.""" success = True lines = Line.readfile(input_file, name) @@ -693,7 +696,9 @@ async def check_file_async(input_file, name, subs, config, failure_handler, env= # Run all the REQUIRES lines first, # if any of them fail it's a SKIP for reqcmd in checker.requirecmds: - proc = await runproc_async(perform_substitution(reqcmd.args, subs), env=env) + proc = await runproc_async( + perform_substitution(reqcmd.args, subs), env=env, cwd=cwd + ) await proc.communicate() # Work around type system limitations / bad API design which makes the typechecker unhappy. if proc.returncode is None: @@ -709,7 +714,7 @@ async def check_file_async(input_file, name, subs, config, failure_handler, env= # Only then run the RUN lines. for runcmd in checker.runcmds: failure = await TestRun( - name, runcmd, checker, subs, config, env=env + name, runcmd, checker, subs, config, env=env, cwd=cwd ).run_async() if failure: failure_handler(failure) @@ -721,9 +726,11 @@ def check_path(path, subs, config, failure_handler, env=None): return asyncio.run(check_path_async(path, subs, config, failure_handler, env=env)) -async def check_path_async(path, subs, config, failure_handler, env=None): +async def check_path_async(path, subs, config, failure_handler, env=None, cwd=None): with io.open(path, encoding="utf-8") as fd: - return await check_file_async(fd, path, subs, config, failure_handler, env=env) + return await check_file_async( + fd, path, subs, config, failure_handler, env=env, cwd=cwd + ) def parse_subs(subs): diff --git a/tests/test_driver.py b/tests/test_driver.py index fe0aacf6a..58ca45a32 100755 --- a/tests/test_driver.py +++ b/tests/test_driver.py @@ -297,7 +297,6 @@ async def run_test( starttime = datetime.now() home = Path(tempfile.mkdtemp(prefix="fishtest-", dir=tmp_root)) test_env = makeenv(script_path, home) - os.chdir(home) if test_file_path.endswith(".fish"): subs = def_subs.copy() subs.update( @@ -309,7 +308,12 @@ async def run_test( # littlecheck ret = await littlecheck.check_path_async( - test_file_path, subs, lconfig, lambda x: print(x.message()), env=test_env + test_file_path, + subs, + lconfig, + lambda x: print(x.message()), + env=test_env, + cwd=home, ) endtime = datetime.now() duration_ms = round((endtime - starttime).total_seconds() * 1000) @@ -339,6 +343,7 @@ async def run_test( stdout=PIPE, stderr=PIPE, env=test_env, + cwd=home, ) stdout, stderr = await proc.communicate() endtime = datetime.now()