Skip to content

Give each test its own scratch directory and each TestScheduler test its own fixtures - #1028

Open
calvinp0 wants to merge 8 commits into
mainfrom
fix_test_isolation_sweep
Open

Give each test its own scratch directory and each TestScheduler test its own fixtures#1028
calvinp0 wants to merge 8 commits into
mainfrom
fix_test_isolation_sweep

Conversation

@calvinp0

Copy link
Copy Markdown
Member

Supersedes #1008 and #1009, which are closed in favour of this branch. Eight commits, no file touched by more than one of them.

Tests wrote their scratch into arc/testing, the tree that holds read-only fixture inputs, and TestScheduler shared one scheduler and one project directory across its tests. Under -n auto --dist=worksteal that makes the outcome depend on which tests happen to land on a worker together, so the same commit passes and fails across runs.

What it changes

Writable scratch comes from tempfile.mkdtemp(), released with addCleanup / addClassCleanup, so no two tests can contend for a path and nothing is left behind when a run dies. arc/testing is read from and not written to. TestScheduler builds its own scheduler and project directory per test.

Across the tree, passing ARC_TESTING_PATH as a project_directory goes from 15 files to 2, and rmtree of a subdirectory of it from 8 to 3.

Measured

Order dependence is only visible if the order is varied, so the suite was run under pytest-random-order with --random-order-bucket=global, which shuffles across files, at eight fixed seeds. The floor is 5: the arc/job/adapters/torch_ani_test.py tests, which fail on an unset TANI_PYTHON and are unrelated to ordering.

seed main this branch
101 9 5
202 12 6
303 10 5
404 8 5
505 9 5
606 10 5
707 11 6
808 9 5

Six of the eight land on the floor exactly. The failures that disappear at every seed are arc/checks/ts_test.py::test_check_rxn_e0 and ::test_compute_rxn_e0, arc/job/adapter_test.py::test_determine_job_status, arc/scheduler_test.py::test_initialize_output_dict, and the xtb, obabel, xtbgsm and processor adapter tests.

Serial runs are identical either side, 5 failed, 2924 passed, 43 skipped, so nothing here trades parallel stability for a serial regression.

The two seeds that do not reach the floor

Seeds 202 and 707 leave one failure each, both in test_GaussianAdapter:

FileNotFoundError: arc/testing/test_GaussianAdapter/calcs/Species/spc1/composite_a43/input.gjf

arc/job/adapters/common_test.py and arc/job/adapters/gaussian_test.py both use ARC_TESTING_PATH/test_GaussianAdapter as a project directory and both rmtree it, so one module's teardown deletes the directory the other is writing into. Neither file is touched here. Running only those two files against this branch's base under -n 4 --dist worksteal fails 3 times out of 3, so the race is a property of main rather than of this change: removing the other collisions stops masking it. Those two files, and common_test.py's test_MolproAdapter directory, are what the counts above do not cover.

How the two branches were combined

#1008's seven commits were rebased onto current main and #1009's single commit cherry-picked on top. main had moved a long way, and some of #1008's work had landed in the meantime: arc/job/adapters/pyscf_test.py was already converted to tempfile.mkdtemp(), so main's version is kept verbatim and the file drops out of this branch rather than being converted a second time in a different style.

Where main had added tests to a file #1008 also changed, both are kept. arc/checks/nmd_test.py and arc/checks/ts_test.py keep the sulfur and chlorine fixtures and the mode-convention assertions; arc/job/adapters/ase_test.py keeps the relaxed rotor scan tests, and its os.makedirs(..., exist_ok=True) guard against the worker race is removed by the isolation rather than left alongside it; arc/statmech/arkane_test.py keeps both new tests and retains its ARC_TESTING_PATH import, which it uses correctly to read fixture inputs.

arc/checks/nmd_test.py also carries one change that is not a conflict resolution: a test added on main used a fixed ARC_PATH/Projects/tmp_nmd_unsupported_ess_project directory, which is shared across workers in the same way, and it now uses tempfile.mkdtemp().

Copilot AI lite review requested due to automatic review settings August 27, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

ARC_TESTING_PATH holds read-only fixture inputs. Passing a subdirectory of it as a
project_directory made every test class in these modules write into one shared, fixed
path, and tearing that path down with shutil.rmtree at the end of the class.

Under pytest-xdist a single TestCase class is split across workers, so every worker runs
tearDownClass. One worker therefore deletes the shared directory while another is still
writing input files into it, which surfaces as FileExistsError / FileNotFoundError on an
unrelated test. A crashed run also leaves the directory behind inside the repository.

Each class now allocates its scratch with tempfile.mkdtemp() in setUpClass and releases it
with addClassCleanup. mkdtemp is unique per setUpClass invocation, so concurrent workers
never share a path and nothing is ever written under arc/testing.

Read-only fixture reads (the PBS time-limit err.txt in adapter_test) still come from
ARC_TESTING_PATH and are unchanged.
Same change as for the ESS job adapters: the TS search adapter tests wrote their project
directories into ARC_TESTING_PATH and removed them from tearDownClass or from ad-hoc
per-test cleanup helpers. Scratch now comes from tempfile.mkdtemp() and is released with
addClassCleanup / addCleanup.

The KinBot helper that removed a single test's subdirectory and then attempted to rmdir the
shared parent existed only to make the shared parent safe under xdist; with a per-class
temporary root the whole tree is released at once and the helper is dropped.

gcn_test.py also drops the `from arc.job.adapters.ts.gcn_ts import GCNAdapter` line, which
duplicated the `import arc.job.adapters.ts.gcn_ts as ts_gcn` already present; kinbot_test.py
imports unittest.mock as mock rather than `from unittest import mock` alongside
`import unittest`.
These modules built their project directories under ARC_TESTING_PATH and removed them in
tearDownClass, which is not safe when xdist splits the class across workers. Both UMA test
classes now allocate their own mkdtemp root, so the env-independent and model-dependent
classes can no longer reach into each other's tree either.

arc/job/adapters/pyscf_test.py is left as main has it: PR #970 already moved that class
onto tempfile.mkdtemp(), so the isolation is in place and reapplying it in a second style
would only churn the file.
Four tests treated a fixture directory as writable scratch:

- common_test.test_globalize_paths wrote restart_paths_globalized.yml into
  arc/testing/restart/4_globalized_paths, which needed a self-healing cleanup helper called
  from both setUpClass and tearDownClass to keep the artifact from leaking into the repo.
  The fixture is now copied into a temporary directory and globalized there, and the two
  assertions that hard-coded the arc/testing path now assert against the temporary project
  directory.
- common_test.test_safe_copy_file copied its destination file next to the source fixture.
- processor_test.test_compare_rates wrote rate plots into arc/testing/process_kinetics.
- reaction_test.test_load_ts_xyz_user_guess_from_files passed the fixture directory itself
  as an ARC project_directory, so a Scheduler run created arc.log, output/ and
  log_and_restart_archive/ inside arc/testing/reactions/methanoate_hydrolysis. The fixture
  is now copied to a temporary directory first; the TS guess filenames in input_1.yml and
  input_2.yml resolve relative to project_directory, so the copy keeps them working.
- arkane_test.TestArkaneAdapter released its mkdtemp from tearDownClass rather than
  addClassCleanup, and wrote generate_arkane_input output into arc/testing.
The TestNMD and TestTSChecks classes built their job project directories under
ARC's shared Projects folder and deleted them in tearDownClass. pytest-xdist may
split a single TestCase class across workers, so each worker runs setUpClass and
tearDownClass independently and one worker's teardown removes a directory another
worker is still using. Allocate the scratch with tempfile.mkdtemp() in setUpClass
and release it with addClassCleanup instead.
…irectories

TestARC instantiated ARC without a project directory, so every instance created a
directory named after its project under ARC's shared Projects folder, several of
which were never deleted. TestPlotter additionally wrote its multi-species logs,
its BDE report and its IRC animation into the read-only arc/testing fixture tree,
where one test asserted the absence of files another test had created. Give each
of them a tempfile.mkdtemp() scratch directory released through addCleanup or
addClassCleanup, and drop the tearDownClass removals of shared paths.

The gcn_tst cleanup entry in species_test is dropped as well; nothing creates that
directory any more.
The restart tests derived their project directories from a per-xdist-worker
project name, which does not help when pytest-xdist splits the class across
workers. Allocate each project directory with tempfile.mkdtemp() and release it
with addCleanup.
…ther

TestScheduler built its schedulers, species and jobs once in setUpClass and shared
them across every test in the class, while eleven of those tests mutate what they are
handed: they replace scheduler.output, inject species into species_dict and job_dict,
flip job_types['rotors'], overwrite job_status and output paths, and append to
unique_species_labels. All of them also shared a single fixed project directory that
tearDownClass deleted.

Under pytest-xdist the class is split across workers, each running its own class-level
setup and teardown, so one worker's tearDownClass removed the project directory another
worker was still writing into, and a test that had implicitly relied on an earlier test
dirtying the shared scheduler saw a clean one instead. Roughly half of the runs failed,
with a different set of tests each time.

Build the fixtures per test in setUp instead, and give every test its own project
directory removed via addCleanup, which also lets tearDownClass go. Constructing the
fixtures costs about 60 ms, so the serial run of the file grows from ~5.0 s to ~6.7 s
and the -n 4 run is unchanged.

test_initialize_output_dict opened by asserting that the shared output dict already
contained information, which only held because an earlier test had put it there; it
failed when run on its own. It now writes that information itself before asserting.
@calvinp0
calvinp0 force-pushed the fix_test_isolation_sweep branch from a4f2fe7 to 9611d7b Compare August 27, 2026 21:24
@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.80%. Comparing base (09e9ede) to head (9611d7b).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1028      +/-   ##
==========================================
+ Coverage   64.76%   64.80%   +0.03%     
==========================================
  Files         119      119              
  Lines       40039    40039              
  Branches    10350    10350              
==========================================
+ Hits        25932    25948      +16     
+ Misses      11129    11119      -10     
+ Partials     2978     2972       -6     
Flag Coverage Δ
functionaltests 64.80% <ø> (+0.03%) ⬆️
unittests 64.80% <ø> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants