ELL3N HSU
← Articles

XAT concurrency: method isolation, fixture scope, and deadline boundaries

Compare method and file subprocesses through shared fixtures, then follow discovery isolation, checkpoints, and parent-driven termination.

Suppose three methods in one test file share a client created by setUpClass. To run concurrently, should the runner assign the whole file to one process or give each method its own? That choice determines how often the client is created, which state can be shared, and whether other methods can continue when one stalls.

XAT gives each method a subprocess by default and also supports whole-file execution. Following the shared-client example explains how each mode prepares resources and how the parent handles a method deadline. This account covers the current local runner; throughput has not been compared between the modes.

The scheduling unit defines shared state

Suppose a file has methods A, B, and C, and setUpClass creates a shared client. File mode puts the whole file in one subprocess and runs all three methods serially with that class fixture. Method mode starts three interpreters, each running its selected method and fixtures, so class preparation happens three times.

ObservationMethod modeFile mode
Scheduling unitA methodA file
Methods in one fileCan overlapRun serially
Class/module fixturesRepeated per methodShared within the file suite
Module variablesNot shared between methodsMay retain changes from earlier methods
A stalled methodIts worker can be terminated while other tasks continueThe file worker ends; later methods in that file do not execute

Method mode suits independently prepared and cleaned tests. Existing cases relying on shared fixtures can select file mode. CPU count alone does not determine concurrency: accounts, service connections, and database records may also limit simultaneous tasks.

Process isolation separates interpreter state. Two methods writing the same database row can still conflict. Separate request Sessions do not change that external dependency.

Discovery also runs in a subprocess

Discovering Python tests imports modules and executes top-level code. Doing that in the scheduling parent would let business modules change parent state or even terminate it.

XAT discovers methods in separate processes, checks file identity, schema, and duplicate method identities, and freezes the task list before execution. Import or discovery errors are recorded separately. A partially discovered file is not treated as a complete list and executed.

A discovery process does not eliminate top-level external side effects. Importing during discovery and again during execution can create business data twice. Discovery should establish executable identities; fixtures should prepare test data. Configuration and working-directory assumptions also need to tolerate repeated imports.

Threads schedule; child processes execute

Once the task list is fixed, the parent starts workers and waits for their results. A thread pool handles that waiting and the logs; the test methods still run in child processes. -c limits the number of worker processes running at once.

xatrun tests -c 4 --case-timeout 600
xatrun tests -c 4 --parallel-scope file

The scheduler tracks queued tasks and submitted futures. After a worker finishes, the parent validates its report, prints its log, and submits a replacement. Concurrent output stays grouped by task, but a task’s full output is generally delayed until completion. With concurrency set to one, execution logs can be drained continuously.

Fresh interpreters still cost startup, imports, and repeated fixtures. A reusable process pool could reduce some overhead but would need to handle state left between tasks. In-process threads share more state and cannot safely be forcibly stopped individually. These are execution-model tradeoffs, not a claim that the project benchmarked every alternative.

Where the deadline starts

The default method limit is 300 seconds, configurable with --case-timeout. It is distinct from an HTTP timeout and from a deadline for the entire command.

In method mode, the worker marks the task active before entering its suite. The limit includes class/module fixtures, method setup, the test, teardown, cleanup, and fixture finalization. Queuing, discovery, module import, and interpreter startup are excluded. File mode updates progress when unittest starts a method, so shared class/module fixtures lie outside that method’s timing interval. The coverage is not identical across modes.

Checkpoints contain task identity, the active method, and its start time. A temporary file is written and replaced into place to reduce partial-read risk. The embedded report is marked incomplete, preventing progress from masquerading as a final result.

After detecting a deadline, the parent reads again. A file worker may have just finished A and started B. Using A’s stale start time could kill B; the second read confirms that the active method has not changed. Normal returns also receive an elapsed-time check, covering completion between polling intervals.

What remains after termination

The parent records TIMEOUT_ERROR, keeps completed results, and marks the batch incomplete. In file mode it also records that subsequent methods in the file did not run. They are not automatically replayed in another process because shared fixtures and earlier external side effects may already have changed the environment.

Regression coverage includes a CPU loop that catches exceptions. The checks verify parent-driven termination, whether later methods run, whether the process exits, and whether registered cleanup remains unexecuted. Merely raising TimeoutError would not test this: the business code would still be returning voluntarily.

Waiting outside the method interval remains possible, including stalled discovery or imports. A future global watchdog would need separate discovery, startup, and batch budgets. Those waits cannot simply be classified as one method’s business failure. The XAT project account connects these execution boundaries to report validation.