Skip to content

Support matrix

Every pytest construct a suite can contain, and what migration makes of it. This is the table behind the codes: a VC214 in an audit summary, a VOCI-TODO[VC214] left in a converted file and the rewrite rule that put it there all name the same row.

The number blocks the construct by area — VC0xx is fixture wiring, VC1xx marks, VC2xx test bodies, VC3xx configuration and plugins, VC4xx the things that only change once tests overlap.

Read this to judge what a migration costs before running one. For the same verdict measured against your own suite rather than in the abstract, run voci-migrate audit.

Converts as it stands

Nobody has to read the diff for these. The conversion rewrites them and the audit counts the tests carrying them as clean.

Code pytest voci Notes
VC001 @pytest.fixture @voci.fixture() A fixture becomes a module-level object, and every parameter that requested it by name is annotated Annotated[T, Depends(...)] naming the import.
VC002 @pytest.fixture(params=...) @voci.fixture(params=...) The fixture keeps its cases, and request.param becomes the factory's own param argument.
VC004 conftest.py fixture fixtures.py beside the conftest Every fixture in a conftest.py moves to a fixtures.py beside it, and the tests that used it import it from there.
VC005 a fixture overriding one visible from further out a specialized fixture chain The override and every fixture between it and the tests that reach it are generated as a specialized chain named for the directory or class the override rules.
VC007 @pytest.mark.parametrize(..., indirect=True) @voci.fixture(params=...) The values move onto the fixture as params=: indirect parametrization is a call site choosing a fixture's case, and a voci fixture carries its own cases.
VC008 @pytest.fixture(autouse=True) voci.use(...) Becomes one voci.use(...) declaration on the module or package the fixture covered, rather than a parameter on each test.
VC009 @pytest.mark.usefixtures on a module or class voci.use(...) Becomes a voci.use(...) declaration covering the same tests.
VC011 request.getfixturevalue("literal") a Depends() parameter A literal name is a static dependency, so it becomes an ordinary Depends() parameter.
VC013 request.addfinalizer(fn) yield teardown An unconditional finalizer becomes the fixture's yield teardown.
VC018 class Test* grouping class Test* The class stays as namespacing and each method collects as path.py::TestGroup::test_name.
VC026 a fixture requesting request for its own parametrization the param argument request.param in a params= fixture becomes the factory's param argument.
VC032 @pytest.fixture written inside a test class a module-level fixture named for the class A voci test class is pure namespacing, with no fixtures of its own, so the factory is lifted to the module level under a name carrying the class's, and the methods that requested it name it there.
VC034 a relative import in a suite module an absolute import voci imports test modules under synthetic names, which a leading dot resolves against nothing, so each becomes the absolute import of the same module.
VC101 @pytest.mark.parametrize @voci.parametrize Becomes @voci.parametrize carrying pytest's own case ids verbatim, so node ids do not move.
VC102 pytest.param(..., marks=...) voci.case(..., marks=...) Becomes voci.case(*values, marks=...), carrying the same marks translated into their voci spelling, which reach that one case.
VC104 @pytest.mark.skip, @pytest.mark.skipif(bool) @voci.skip, @voci.skipif Same decorator, same reason.
VC105 @pytest.mark.xfail with a condition @voci.xfail(condition=...) The condition becomes @voci.xfail's own condition=, which decides whether the failure is expected at all.
VC107 xfail_strict strict= on each xfail The ini setting is written into each generated @voci.xfail as strict=.
VC108 @pytest.mark.filterwarnings @voci.filterwarnings(...) Becomes @voci.filterwarnings(...), which takes the same filter specs and governs the test that carries it alone, whatever else is running.
VC109 a custom mark @voci.tag(...) Becomes @voci.tag(...), selectable with -m, and needs no registration.
VC110 @pytest.mark.asyncio, @pytest.mark.anyio, event_loop fixtures deleted voci runs async def tests itself, so the mark and the loop fixtures go away. A backend parametrization the mark carried goes with it, and with it that segment of the test's id.
VC111 @pytest.mark.timeout @voci.timeout(...) Becomes @voci.timeout(...).
VC115 pytestmark on a module or a class the same mark on each test voci reads marks from the test function, and a mark on a class is a collection error, so each mark the assignment applied becomes a decorator on every test it reached.
VC201 capsys voci.capture Becomes the voci.capture fixture, whose .out and .err read the live buffers.
VC204 caplog.records, caplog.messages voci.log_records Become the same attributes on voci.log_records.
VC206 caplog.text, caplog.record_tuples, caplog.clear() voci.log_records Become the same names on voci.log_records, which formats .text from its own records on every read rather than keeping a second stream.
VC207 tmp_path, tmp_path_factory tmp_path, tmp_path_factory Same names, same pathlib.Path.
VC208 tmpdir, tmpdir_factory voci.tmpdir, voci.tmpdir_factory Same names, voci.tmpdir/voci.tmpdir_factory's own LegacyPath wrapping .join, .strpath, .write, .mkdir and division the way pytest's own legacy shim does.
VC209 pytest.raises(E, match=...) voci.raises Becomes voci.raises, matching with re.search as pytest does.
VC210 pytest.raises(E, func, *args) voci.raises Becomes voci.raises, which has the same callable form: it calls func(*args, **kwargs) under the hood and returns the resulting ExceptionInfo. Left unconverted when match= is among the kwargs, or a **mapping is unpacked into them: pytest forwards match to func there, but voci.raises always intercepts it.
VC212 pytest.approx(scalar) voci.approx Becomes voci.approx.
VC213 pytest.approx over a list, tuple, or dict voci.approx Becomes voci.approx, which compares a list or tuple elementwise by position and a dict elementwise by key, all under the same tolerances. Left unconverted where a list, tuple, dict, or set is nested one level inside it: voci.approx only walks one level, so there is no position to compare a nested container by.
VC214 pytest.skip(), pytest.fail() as a statement voci.Skipped, voci.Failed voci.skip is a decorator factory, not a runtime call -- calling it in a body builds a decorator and discards it, which would turn a conditional skip into a silent pass, so these become voci.Skipped/voci.Failed, raised directly, instead.
VC217 unittest.mock.patch as a decorator @mock.patch, scheduled solo The patch stays as it is and voci schedules the test alone. Injected parameters are emitted after the mock arguments the decorator fills positionally. Tests using it run alone.
VC218 unittest.mock.patch as a context manager @voci.solo on the test A patch entered inside a body is invisible until it runs, so the test carries @voci.solo and nothing else runs while it holds. Tests using it run alone.
VC301 testpaths testpaths Same meaning under [tool.voci].
VC302 python_files test_file_patterns Becomes test_file_patterns.
VC303 norecursedirs ignore Becomes ignore.
VC304 pytest-timeout's timeout setting timeout Becomes the timeout key, which voci applies per test.
VC306 markers, asyncio_mode, and other settings with nothing to configure dropped voci tags need no registration and it runs async tests without being told to, so these settings have no counterpart to write.
VC307 filterwarnings [tool.voci] filterwarnings Carried to [tool.voci]'s own filterwarnings, which takes the same filter specs.
VC320 an installed plugin migration deletes deleted Its whole job is done by the runner, so the suite loses the dependency.
VC321 an installed plugin with a translation What the suite uses it for has a voci spelling, reached through the rows for the fixtures and marks themselves.

Converts, with something to check

These convert to working code, but something shifted on the way. The rewrite leaves a VOCI-TODO[code] comment in the source at each one.

Code pytest voci What shifted What to check
VC003 fixture scope="class" or scope="package" @voci.fixture(scope=...) voci scopes are call, function, module and session. A class-scoped fixture becomes module-scoped and a package-scoped one becomes session-scoped, so each is shared by more tests than it was. Confirm the fixture tolerates being shared more widely, or split it.
VC010 @pytest.mark.usefixtures on some of a module's tests voci.use(...) voci.use(...) declares a fixture for a whole module, so the fixture reaches the module's other tests too. Confirm the other tests in the module tolerate the fixture, or move them out.
VC015 request.node, request.config, request.cls, and every other attribute of request voci.test_info voci.test_info carries the test's id, tags, timeout and worker. Anything else these reach — a node's own marks, ini values, the owning class — has no counterpart. Rewrite the use against test_info, or pass what the fixture needs in.
VC024 pytest_generate_tests @voci.parametrize with the generated cases The cases the hook produced are in the dump, ids included, so they convert to an explicit @voci.parametrize — one that lists what this extraction saw and generates nothing new. Confirm the frozen case list is what the suite should keep testing.
VC025 pytest_plugins in a conftest Fixtures the named module contributed are translated from the dump like any other, and the declaration itself goes away. Check the named module for hooks and non-fixture content, which do not travel.
VC103 @pytest.mark.skipif with a string condition @voci.skipif voci takes a bool or a zero-arg callable. A string condition is evaluated by pytest at collection, and a callable is evaluated by voci at run time. Confirm the condition reads the same at run time as it did at collection.
VC106 @pytest.mark.xfail(run=False) @voci.skip Nothing in voci marks a test as expected-to-fail without running it, so it becomes a skip and is reported as one. Confirm a skip is the outcome the suite wants reported.
VC114 a case id composed from more than one axis @voci.parametrize without ids pytest builds one id per case out of every axis that varies it — stacked parametrize marks, or a mark over a params= fixture — so no single mark can carry the ids verbatim, and voci composes its own from the values. Check any CI configuration, dashboard or --last-failed habit that names these ids, and write an explicit ids= where one matters.
VC116 @pytest.mark.xfail with a string condition @voci.xfail(condition=...) voci takes a bool or a zero-arg callable, so the string becomes a lambda over the same expression, evaluated where the module it reads is already imported. Confirm the condition reads the same from the converted module as it did in pytest's own namespace.
VC202 capsys.readouterr() more than once in a body voci.capture readouterr() returns a snapshot and clears the buffer; capture.out is cumulative and never cleared, so a second read sees the first read's output too. Compare against the text captured so far, or slice off what was already asserted.
VC205 caplog.set_level(...) with log_records.set_level(...) LogRecords.set_level(...) returns a context manager and does nothing until it is entered, so the statement becomes a with block around the rest of the test. Logger levels are process-global. Tests using it run alone. Wrap the part of the test that needs the level, and mark the test @voci.solo.
VC219 mocker (pytest-mock) mock.patch plus @voci.solo mocker.patch is mock.patch with teardown attached to the fixture, so each call becomes a mock.patch the test enters, and the test runs alone. Tests using it run alone. Rewrite each mocker. call as the mock call it wraps.
VC305 addopts Each flag needs its own answer: some have a voci spelling, some were a plugin's, and unknown [tool.voci] keys are a hard error, so nothing speculative is written. Translate the flags you rely on into [tool.voci] or into the CI invocation.

Needs a decision first

voci has somewhere for these to go, but not one the conversion can pick on its own. Each is a blocked test until the suite is changed by hand.

Code pytest Why What to do
VC006 conftest override whose chain exceeds the specialization budget Specializing this override would duplicate more fixtures than the budget allows, which is the point where a generated chain stops being reviewable. Unwind the override into an explicit seam or a parametrized fixture before converting.
VC012 request.getfixturevalue(computed) The name is decided at run time, so nothing static can say which fixture is meant. Replace the computed lookup with explicit dependencies, or with a factory fixture that takes them.
VC014 request.addfinalizer where no single yield can take its place A yield fixture hands its value over at one point in the body and tears down after it, so a finalizer registered under a condition, from inside another function, or from somewhere that is not a fixture has nowhere to move to. Register the finalizer unconditionally in the fixture's own body, moving any condition inside it.
VC017 request passed to another function or held past setup The uses cannot be enumerated at the call site, so no per-use translation applies. Narrow the fixture to the values it actually reads from request.
VC019 setup_method, teardown_method, setup_class, setup_function voci builds a fresh instance per test and calls no setup protocol, so these never run. Move each one's body into a fixture the class declares with voci.use(...).
VC027 conftest override reaching an autouse fixture A specialized chain gives the subtree its own objects, and a voci.use(...) declares one of them for a directory — so an autouse fixture the override changes would be declared twice over the same tests, once for each definition. Request the fixture by name where it is needed, or unwind the override.
VC028 request.getfixturevalue of a name a parameter cannot carry A parameter names one object for the whole definition, so a name the suite defines in more than one directory, or one asked for where there is no signature to grow, has no parameter to become. Request the fixture in the signature, or unwind the override that gives the name two meanings.
VC029 indirect parametrization a params= fixture cannot carry A params= fixture has one case list for every test that reaches it, so a name given different values in different tests, one parametrized alongside a direct axis, one whose fixture already has cases of its own, and one whose values have no literal spelling all have nowhere to go. Give the fixture the cases it always has with params=, or take the parametrization off the fixture and pass the value to the test.
VC031 a generated case an explicit parametrize cannot list A frozen case list is written from the value each case was given, so a value with no literal spelling, and an axis the hook composed with one the test was written with, have nothing this can write. Write the cases out as a @pytest.mark.parametrize before converting.
VC033 a fixture inside a test class reading its instance Lifting the factory out of the class takes self away with it, and a self naming anything but an attribute the class body itself binds has no module-level spelling. Read the class attribute through the class, or move what the fixture needs off the instance.
VC036 a fixture requested by a positional-only parameter voci binds every argument by keyword, so a parameter written before the / can never be given the object it names, and moving it out from behind the / would change what the definition can be called with everywhere else. Write the parameter after the /, or take the / off the signature.
VC113 two skip, xfail or timeout marks on one test voci raises when a scalar mark is applied twice, so this is a collection error rather than a last-one-wins. Keep one, having decided which.

No voci equivalent

voci provides nothing that plays these parts, so a suite that leans on one has to do without it or keep that part under pytest.

Code pytest Why What to do instead
VC016 request.config.getoption on a custom pytest_addoption flag The flag exists because a conftest hook added it to pytest's command line, and voci has no plugin command-line surface for it to be added to. Read the value from the environment or from configuration instead.
VC020 unittest.TestCase voci collects functions and class Test* methods; a TestCase subclass brings its own lifecycle, assertions and skipping. Rewrite the class as plain test functions.
VC021 doctests voci collects test modules, not docstrings. Keep running doctests under their own command.
VC022 conftest hook (pytest_configure, pytest_collection_modifyitems, ...) Hooks are how a pytest plugin reaches into collection and reporting, and voci has no hook protocol. Decide per hook: fixtures replace setup hooks, and reporting hooks have no counterpart.
VC023 pytest_addoption voci's command line is fixed, so a suite cannot add a flag to it. Read the setting from the environment or from [tool.voci]'s env.
VC030 a fixture an installed plugin provides The fixture lives in a distribution, not in the suite, so there is no source to move and nothing registers it under voci. Write the fixture into the suite, or drop the tests that need it.
VC112 a mark an installed plugin acts on The behaviour was the plugin's, and voci records marks as tags without acting on them. Replace the mark's effect with a fixture, or drop the tests that need it.
VC203 capfd, capsysbinary, capfdbinary voci captures by replacing sys.stdout and sys.stderr, so writes to file descriptor 1 by a subprocess or a C extension are not captured, and there is no binary variant. Redirect the subprocess to a file the test reads, or drop the assertion.
VC211 pytest.raises(asyncio.CancelledError) Cancellation is how voci enforces timeouts, so voci.raises refuses to swallow it. Assert on the cancellation's effect instead of catching it.
VC215 pytest.importorskip("mod") There is no imperative skip to reach for at import time. Guard the import at module level and put @voci.skipif on the tests.
VC216 pytest.warns, recwarn, pytest.deprecated_call Recording warnings means installing a process-global filter, which cannot be scoped to one of several tests in flight. Assert on what the warning accompanies, or catch it inside a @voci.solo test.
VC220 pytestconfig, cache, record_property, pytester and other pytest builtins These fixtures expose pytest's own configuration, cache and self-test machinery. Drop the use, or read the value from configuration.
VC221 pytest.approx over a set or a generator expression Neither has a position to compare by: a set is unordered, and a generator is spent after one read. A numpy array is left unconverted here too, since voci has no numpy dependency to compare it with. Compare a sorted sequence instead of a set, or a list instead of a generator.
VC222 caplog.handler, caplog.get_records(...) voci has no handler object behind voci.log_records, and no per-phase record split for get_records to read. Assert on records or messages directly instead of handler; there is no phase-scoped equivalent for get_records.
VC223 pytest.xfail() as a statement Unlike pytest.skip()/pytest.fail(), this has no runtime target to become: it marks the test as an expected failure and stops it right there, which @voci.xfail(...)'s condition -- decided once at collection, before the test has run at all -- can't reach. Lift the condition into @voci.xfail(condition=...), if it's known before the test runs; otherwise let the test fail and mark it @voci.xfail unconditionally.
VC308 log_cli, console_output_style, required_plugins and other reporting settings These configure pytest's terminal and plugin machinery. Drop them, or reach for the closest voci flag.
VC309 an ini setting a plugin registered The setting exists because a plugin asked pytest for it, and unknown [tool.voci] keys are a hard error. Drop it with the plugin, or move the value into the environment.
VC323 an installed plugin with no voci path Nothing in voci provides what it does. Keep those tests under pytest, or drop the plugin's use.
VC324 a case parametrized onto an event loop other than asyncio voci runs every async test on asyncio, so the cases a backend fixture produces for another loop have nothing to run on and disappear with it. Pin the backend fixture to asyncio, and keep the other loop's coverage under pytest.

Survives conversion, changes under concurrency

These translate as written and then mean something different, because voci overlaps the tests that pytest ran one at a time. Most cost the tests carrying them their overlap with the rest of the suite.

Code Construct What changes What to do
VC322 an installed plugin that mutates process-global state It works, and what it changes is visible to every test running at the same time. Tests using it run alone. Schedule the tests that use it alone, or isolate them.
VC401 monkeypatch Every monkeypatch call changes state the whole process shares — an attribute, an environment variable, sys.path, the working directory — which a serial runner made private to the running test. Tests using it run alone. Inject the dependency instead, or mark the test @voci.solo; chdir needs @voci.isolated.
VC402 os.environ writes The environment is process-wide, so a variable one test sets is set for every test in flight. Tests using it run alone. Set suite-wide values in [tool.voci]'s env, and mark tests that need their own value @voci.solo.
VC403 warnings filter mutation simplefilter and filterwarnings write a process-global list, and catch_warnings restores it wholesale on exit — including changes another test made meanwhile. Tests using it run alone. Mark the test @voci.solo.
VC404 logging level or handler mutation Logger objects are process-global, so a level a test raises is raised for everything logging at the same time. Tests using it run alone. Mark the test @voci.solo, or assert on records without changing levels.
VC405 sys.modules surgery or importlib.reload Replacing or reloading a module rebinds it for every test that has already imported it. Tests using it run alone. Mark the test @voci.isolated.
VC406 os.chdir The working directory belongs to the process, and a relative path elsewhere in the suite resolves against whatever it currently is. Tests using it run alone. Use absolute paths, or mark the test @voci.isolated.
VC407 locale, decimal context or interpreter limits These are interpreter-wide settings with no per-task scope. Tests using it run alone. Mark the test @voci.isolated.
VC408 a globally patched clock (freezegun, time-machine) Freezing time replaces the clock for the whole process, so every test in flight sees the frozen time. Tests using it run alone. Inject a clock, or mark the test @voci.solo.
VC409 asyncio.run, get_event_loop, new_event_loop, run_until_complete voci runs the test on a loop that is already running, and starting a second loop from inside it fails. Await the coroutine directly in an async def test.
VC410 a blocking call in an async body One blocking call in an async def holds the loop, and every other test in flight waits for it. A sync def test is safe: it runs on an executor thread and holds only its own slot. Use the async client, or make the test a plain def.
VC411 a module-level or class-level global a test writes State that outlives a test is shared with every test that reads it, and nothing sequences them any more. Move the state into a fixture.
VC412 seeded randomness and sequence counters A seed set in a fixture, or a factory sequence, produced deterministic values because tests drew from it one at a time. Seed per test, or assert on shape rather than on generated values.
VC413 a fixed external resource A hardcoded port, path or database worked because one test used it at a time. Allocate per test, or mark the tests that share it exclusive= on their fixture.

What no scan can see

A dump records what pytest resolved and a parse records what the sources say. Neither reaches these, so the audit names them in its report rather than counting them.

Code Construct Why it is invisible What to do
VC035 a suite that reads its own module names voci imports each test module by path, under a synthetic voci_tests. name that makes two files of the same name in different directories two modules. Anything keyed on __module__ — a class registry, a plugin lookup, a snapshot path — sees that name. Key on something the import name does not decide, or move what registers itself out of the test module.
VC414 test-order dependence A test that passes only because something earlier in the file ran first has nothing in its source saying so. Run the suite shuffled under pytest before converting.
VC415 fixture teardown timing pytest tears a module-scoped fixture down when the module's last test finishes; voci tears it down when its last holder releases, with other modules still running. Check anything that asserts a teardown has happened.
VC416 shared state behind application code Singleton caches, functools.lru_cache on application functions, ORM identity maps and module-level registries are shared by tests that never mention them. Reset them in a fixture, or inject them.

Installed plugins

What migration makes of each plugin it recognizes, by the matrix row that classifies it: VC320 is deleted outright, VC321 translates into something voci already has, VC322 survives but mutates state the whole process shares, and VC323 has no voci path. A plugin absent from this table is reported as unrecognized rather than assumed harmless.

Plugin Code Disposition Notes
pytest-asyncio VC320 mechanical voci runs async tests itself.
anyio VC320 mechanical voci runs async tests itself.
pytest-trio VC323 unsupported voci runs tests on asyncio.
pytest-tornasync VC323 unsupported voci runs tests on asyncio.
pytest-xdist VC320 mechanical voci runs tests concurrently in one process, so there are no workers to distribute across. Its flags live on in CI invocations.
pytest-randomly VC320 mechanical Ordering is not voci's to shuffle. A suite that ran green under it is evidence the suite does not depend on order.
pytest-timeout VC320 mechanical @voci.timeout(...) and the timeout setting.
pytest-env VC320 mechanical [tool.voci]'s env sets the suite's environment.
pytest-mock VC321 mechanical mocker becomes mock.patch, and the tests that use it run alone.
pytest-httpx VC321 mechanical Transport-level patching, per test, which stays as it is.
respx VC321 mechanical Transport-level patching, per test, which stays as it is.
pytest-recording VC321 mechanical Transport-level patching, per test, which stays as it is.
pytest-vcr VC321 mechanical Transport-level patching, per test, which stays as it is.
pytest-freezegun VC322 hazard It patches the process clock for the running test.
pytest-freezer VC322 hazard It patches the process clock for the running test.
pytest-time-machine VC322 hazard It patches the process clock for the running test.
factory-boy VC322 hazard Factory sequences are class-level counters shared by every test drawing from them.
pytest-django VC322 hazard Its database fixtures translate through the fixture rows, and its per-test transaction and settings overrides are process-global.
pytest-flask VC322 hazard Its app and client fixtures translate through the fixture rows.
pytest-postgresql VC322 hazard Its database fixtures translate through the fixture rows; the database itself is a shared resource.
pytest-cov VC323 unsupported Coverage of a concurrent single-process run is measured by running coverage around voci, not by a plugin.
pytest-subtests VC323 unsupported Nothing in voci reports sub-results.
pytest-benchmark VC323 unsupported A timing measurement taken while other tests run means nothing.
pytest-repeat VC323 unsupported Nothing in voci repeats a test.
pytest-rerunfailures VC323 unsupported Nothing in voci retries a test.
pytest-flakefinder VC323 unsupported Nothing in voci repeats a test.
hypothesis VC323 unsupported @given rewrites a test's signature, which is where voci reads injected dependencies from.
pytest-bdd VC323 unsupported Its step decorators build tests through pytest hooks.
pytest-splinter VC323 unsupported Nothing in voci provides browser fixtures.

pytest's builtin fixtures

Every fixture pytest provides without the suite declaring it, and the row that decides what a request for it becomes.

Fixture Code Becomes
request VC026 the param argument
tmp_path VC207 tmp_path, tmp_path_factory
tmp_path_factory VC207 tmp_path, tmp_path_factory
tmpdir VC208 voci.tmpdir, voci.tmpdir_factory
tmpdir_factory VC208 voci.tmpdir, voci.tmpdir_factory
capsys VC201 voci.capture
caplog VC204 voci.log_records
capfd VC203
capsysbinary VC203
capfdbinary VC203
capteesys VC203
monkeypatch VC401
recwarn VC216
pytestconfig VC220
cache VC220
record_property VC220
record_testsuite_property VC220
record_xml_attribute VC220
pytester VC220
testdir VC220
doctest_namespace VC021