We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Avoid `File.cd!/2` in async tests
almirsarajcic
Do not use File.cd!/2 to isolate filesystem work in concurrent tests. Its callback looks scoped, but the current working directory belongs to the entire BEAM.
root = System.tmp_dir!() |> Path.join("file-cd-race")
dir_a = Path.join(root, "a")
dir_b = Path.join(root, "b")
File.mkdir_p!(dir_a)
File.mkdir_p!(dir_b)
original = File.cwd!()
parent = self()
task =
Task.async(fn ->
File.cd!(dir_a, fn ->
send(parent, :inside_a)
receive do
:check -> File.cwd!() |> Path.basename()
end
end)
end)
receive do
:inside_a -> :ok
end
File.cd!(dir_b)
send(task.pid, :check)
IO.inspect(Task.await(task), label: "task inside A read")
# => task inside A read: "b"
File.cd!(original)
The task entered dir_a, but it read dir_b after another process changed the working directory. File.cd!/2 restores the previous directory after its callback, even when the callback raises, but it cannot make a VM-global setting private to that process.
This is especially dangerous in async: true tests: relative reads, writes, and commands can occasionally run against another test’s fixture directory. The failure depends on timing, so it often appears only in the full suite.
Keep filesystem paths absolute. When an external command needs a particular directory, use the :cd option of System.cmd/3; it sets the directory for that OS process without changing the BEAM’s working directory.
results =
[dir_a, dir_b]
|> Task.async_stream(fn dir ->
System.cmd("pwd", [], cd: dir)
end)
|> Enum.map(fn {:ok, {output, 0}} ->
output |> String.trim() |> Path.basename()
end)
IO.inspect(results)
# => ["a", "b"]
File.rm_rf!(root)
The callback form remains useful in sequential scripts, but it is not an isolation boundary for concurrent code.
copied to clipboard