Description
xdist currently supports 2 ways of distributing tests across a number of workers:
- "Each" Scheduling: Given a set of tests, "each" scheduling sends each test in the set to each available worker.
- Load Scheduling: Given a set of tests, load scheduling distributes the tests among the available workers. Each test runs only once, and xdist tries to give each worker the same amount of work.
Problems with Load Scheduling's current implementation
The current load scheduling implementation distributes tests naively across the workers. Often this means that two tests which depend on the same fixture get assigned to different runners, and the fixture has to be created on each runner.
This is a problem. Fixtures often capture expensive operations. When multiple tests depend on the same fixture, the author typically expects the expensive operation represented by that fixture to happen only once and be reused by all dependent tests. When tests that depend on the same fixture are sent to different workers, that expensive operation is executed multiple times. This is wasteful and can add significantly to the overall testing runtime.
Our goal should be to reuse the existing pytest concept of the fixture to better distribute tests and reduce overall testing time, preferably without adding new options or APIs. This benefits the most users and strengthens pytest's declarative style.
Proposed solution
We can solve this problem in 3 phases:
-
Let's formalize the concept of a "test chunk" or "test block". A test chunk is a group of tests that always execute on the same worker. This is an internal xdist abstraction that the user normally doesn't have to know about.
The master will only send complete test chunks to workers, not individual tests. Initially, each test will be assigned to its own chunk, so this won't change xdist's behavior at first. But it will pave the way for us to chunk tests by their attributes, like what fixtures they depend on.
Once we have this internal abstraction, we can optionally also expose a hook that lets users define their own chunking algorithm to replace the initial default of "1 test -> 1 chunk". The hook won't be very useful until more information about each test is made available, which brings us to the next phase.
-
We need to pass in additional information to the xdist master about the tests it's running so it can better distribute them. Specifically the master needs to be able to identify unique instances of every fixture that each test depends on. Tests that depend on distinct fixture instances can be assigned to different chunks and thus sent to different workers.
To identify the distinct fixture instances that each test depends on, we need the following pieces of information for each test:
- Test name.
- For each fixture that the test depends on:
- Fixture name.
- Fixture scope and "scope instance".
- e.g. This fixture is module-scoped and this particular instance of the fixture is for module
test_a.py
.
- e.g. This fixture is module-scoped and this particular instance of the fixture is for module
- Fixture parameter inputs, if the fixture is parameterized.
- e.g. We need to distinguish
fixture(request.param=1)
andfixture(request.param=2)
as separate fixture instances.
- e.g. We need to distinguish
Initially this information won't be used for anything. It will just be made available to the master. At this point, we'll be ready for the final phase.
-
Using the new information we have about tests, along with the new internal abstraction of a test chunk, we can now chunk tests up by the list of unique fixture instances they depend on.
Tests that depend on the same instance of a fixture will now always be sent to the same worker. 🎉
Advantages over other solutions
This approach has two major advantages over other proposals.
- This approach adds no new configuration options or public APIs that users have to learn. Everyone using pytest automatically gets a better, arguably even more correct, distribution of their tests to workers without having to do any work.
- This approach promotes a declarative style of writing tests over an imperative one. The goal we should strive for is: Capture your ideas correctly, and pytest will figure out the appropriate execution details. In practice, this is probably not always feasible, and the user will want to exercise control in specific cases. But the closer we can stick to this goal the better the pytest user experience will be.
How this approach addresses common use cases
There are several use cases described in the original issue requesting some way to control parallel execution. This is how the approach described here addresses those use cases:
-
Use case: "I want all the tests in a module to go to the same worker."
Solution: Make all the tests in that module depend on the same module-scoped fixture.
If the tests don't need to depend on the same fixture, then why do they need to go to the same worker? (The most likely reason is that they are not correctly isolated.)
-
Use case: "I want all the tests in a class to go the same worker."
Solution: The solution here is the same. If the tests need to go to the same worker, then they should depend on a common fixture.
To cleanly address this case, we may need to implement the concept of a class-scoped fixture. (Fixtures can currently be scoped only by function, module, or session.) Otherwise, the tests can depend on a common, module-scoped fixture and achieve the same result.
-
Use case: "I want all the tests in X to go to the same worker."
Solution: You know the drill. If tests belong on the same worker, we are betting that there is an implied, shared dependency. Express that shared dependency as a fixture and pytest will take care of the rest for you.
Counter-proposal to #17.
Addresses pytest-dev/pytest#175.
Adapted from this comment by @hpk42.
Activity
nicoddemus commentedon Dec 2, 2015
@nchammas wow, congratulations on a very well written and thought out proposal! 😄
It clearly states the problems we have today, the solution and perhaps most importantly the steps to achieve it. I will give it some more thought and post my comments later.
Thanks!
hpk42 commentedon Dec 3, 2015
i think it would be instructive to write a plugin that hacks into the internal data structures to get the "feature instance" thing in association with tests. So one could print a mapping of "fixture-instance -> [testids]" and get it over some existing test suites to know what we are dealing with. I am not sure how easy it is to get this information - getting the fixturenames per test ("request.fixturenames") and probably the fixture definition function (working with the internal registry) for each fixturename should be doable but that doesn't provide the instances yet. the "instance" structure, so to say, is more obvious during execution i think. So maybe the plugin (or pytest hack) could first try to get the information during execution instead of during collection.
My other thought is that i am not sure if we can just send the tests for each fixture instance to one worker. If you have an autouse - db-transaction session-scoped fixture it will be present in most tests but you still want to send them to different workers. Maybe it's easier to consider first chunking by class and module scoped fixtures and ignore session/function.
so much for now.
nicoddemus commentedon Dec 3, 2015
Could you please clarify what you mean by instance structure? Also, what phase do you mean by "during execution*? As I understand it, we have collection and test execution phases only, with the test execution happening at the slaves...
Good point, but I think
autouse
fixtures should not be taken in account by default, since they would for all tests affected by it into the same slave. Perhaps they should opt-in using some flag or mark?hpk42 commentedon Dec 3, 2015
"instance structure" -- i meant which fixture instances map to which tests. With "during execution" i mean indeed test execution. The plugin is not meant for implementing the proposal but for getting a better idea which kind of "fixture instances versus tests" we have.
nicoddemus commentedon Dec 3, 2015
I see, thanks for the clarification. 😄
nicoddemus commentedon Dec 3, 2015
Just to make my point clear about
autouse
fixtures maybe being opt-in to be taken into consideration for chunking, it's possible to have session-autouse fixtures in aconftest.py
deep in the test hierarchy, affecting only a few tests so in this case it might make sense to chunk those tests together.But of course that is a refinement that might be done later on.
hpk42 commentedon Dec 4, 2015
sure, there are all kinds of situations with fixture setups. The decision how to distribute given all the information is probably not a simple "just send all tests using a particular fixture to the same worker" because there are many fixtures involved in real-world projects. We might want to also inform the master about the dependencies between fixtures (or even fixture instances).
nchammas commentedon Dec 4, 2015
It sounds like we agree on the overall approach described here, and we want to start by tackling phase 2 (whether by writing a plugin, hacking pytest, etc.). Is that correct?
I'm trying to get a clear sense of whether there is consensus on the next step to take before moving forward.
nicoddemus commentedon Dec 4, 2015
I think so. I would hack it into a
pytest
orpytest-xdist
fork which could discover and dump fixture information into ajson
file containing test ids and fixtures used by each test id along with their scopes. Perhaps a challenge here will be how to identify that two fixtures with the same name are actually different fixtures because fixtures can be overwritten in different hierarchies.With this in place, we can ask people to run their test suites with it, and then take a look at the results. 😁
RonnyPfannschmidt commentedon Dec 4, 2015
Note that itwill break for remote usage
nchammas commentedon Dec 5, 2015
@RonnyPfannschmidt - Once we have an implementation that collects this additional information about tests, I'm sure we'll uncover many issues. It'll probably be easier to understand them if we have an implementation to refer to, or at least the beginnings of one.
@nicoddemus / @hpk42 - I have a few questions which will help me get started on making this additional information available to the xdist master. Since we don't have any architecture docs, I can even use your answers to start building minimal documentation of how xdist works, if we want.
NodeManager
?FixtureManager
. Is that correct? From your knowledge of how fixtures work, is all the information listed in "phase 2" already available viaFixtureManager
?62 remaining items