Testing
uv run pytest tests
uv run pytest tests/test_conf.py::test_load_conf # a single test
uv run pytest -k "run_cmd and bash" # a single shell's parameters
CI runs the test suite on Windows, macOS and Linux on the latest Python, plus one job on the oldest supported Python to catch anything newer than it allows.
The suite adapts to the platform it runs on. It exercises every shell of the platform that is installed — sh, bash and zsh on posix, cmd and powershell on Windows, and pwsh on either, since it installs everywhere and quotes its own way — and it skips the process replacement tests on Windows, which has no execvpe.
A keyring to test against
The tests that read and write credentials need a real OS keyring that can be unlocked without user interaction. They are skipped with a message if there is no such keyring, so the rest of the suite still runs. Set KEYCMD_REQUIRE_OS_KEYRING=1 to turn those skips into failures instead; CI sets it, so that a broken keyring setup can't quietly reduce the coverage of a run.
The credential manager is available to your session out of the box, no setup needed.
Your login keychain works as long as it is unlocked. CI instead creates a throwaway keychain and makes it the default:
If you would rather not involve your OS keyring at all, point keyring at a file-based backend:
uv run --with keyrings.alt pytest tests
# with PYTHON_KEYRING_BACKEND=keyrings.alt.file.PlaintextKeyring set in your environment
Testing WSL
The WSL setup has two halves.
Working inside WSL, keycmd is a posix process like any other, talking to whichever keyring backend the distribution provides; that is the Linux job above, keyring daemon and all.
The other half — calling the Windows install of keycmd from a WSL shell to reach the Windows credential manager — crosses the interop boundary, and that is what tests/test_wsl.py covers: a credential in the credential manager, a shell inside WSL, and the Windows install of keycmd in between.
Everything about that boundary that can be decided without a Windows machine is in tests/test_wsl_interop.py instead, and runs everywhere: which process tree and working directory mean keycmd was called from a distribution, the command lines it builds for wsl.exe, and the WSLENV that carries the credentials across.
The end to end tests run by themselves on a Windows machine whose WSL install answers, and skip themselves with the reason it did not anywhere else — no distribution registered, no keycmd on the PATH for one to call. The report header of every run says which it was:
Set KEYCMD_REQUIRE_WSL=1 to turn those skips into failures, the same way KEYCMD_REQUIRE_OS_KEYRING does for the keyring. CI sets it on the one job that installs WSL — the other jobs have none, and skip — so that a distribution that fails to provision fails the build instead of quietly reducing it.
Things that bite in this suite
- Never assume a shell. The
shellfixture parametrizes over every shell of the platform that is installed, so a test using it runs several times. Ask theShellobject for the dialect (env_var,unset_env_var,command_not_found_statuses) instead of branching on the platform. wsl.exemangles its command line: backslashes disappear and quotes are stripped before the distribution sees them. Pass paths translated to/mnt/...bywsl_path, unquoted and free of spaces, and keep remote scripts on one line.- The remembered backend is redirected, always. An autouse fixture points
backend.CACHE_HOMEat a folder undertmp_path, so a test run neither reads nor writes the note the machine it runs on is using. - A backend subclassed in a test joins keyring's registry for the rest of the session, so a later search can settle on it and hand the suite a backend that holds no credentials, which arrives as an OS keyring that skipped itself. A test double takes itself out of the running with
viable = False. - Do not assume the suite runs unpinned.
PYTHON_KEYRING_BACKENDoutranks everythingbackend.pydoes, so a test about remembering has todelenvit first, or it will be testing the path that deliberately remembers nothing. - Warnings are errors, so a deprecation in a new Python release fails the suite rather than scrolling past.