From c24649a5a3d43a0db48a3ab42a27d6518eba67ba Mon Sep 17 00:00:00 2001 From: Korijn van Golen Date: Wed, 26 Jul 2023 15:02:42 +0200 Subject: [PATCH] Add tests (#7) * add tests * undo change * Correct requirements and bump version * Bump version * fix logging and improve test suite * lock deps * test conf module * load conf test * all tests done * simplify test since I cant figure out CI --- .github/workflows/ci.yml | 21 ++- keycmd/__init__.py | 2 +- keycmd/{__main__.py => cli.py} | 4 +- keycmd/conf.py | 13 +- keycmd/creds.py | 10 +- keycmd/logs.py | 18 ++- poetry.lock | 84 ++++++++++-- pyproject.toml | 9 +- tests/test_cli.py | 96 ++++++++++++++ tests/test_conf.py | 234 +++++++++++++++++++++++++++++++++ tests/test_creds.py | 47 +++++++ tests/test_logs.py | 32 +++++ tests/test_shell.py | 53 ++++++++ 13 files changed, 593 insertions(+), 30 deletions(-) rename keycmd/{__main__.py => cli.py} (95%) create mode 100644 tests/test_cli.py create mode 100644 tests/test_conf.py create mode 100644 tests/test_creds.py create mode 100644 tests/test_logs.py create mode 100644 tests/test_shell.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b2a53e..e41f96c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,8 +33,25 @@ jobs: poetry run ruff keycmd poetry run black --check keycmd + test: + name: Test + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install poetry + run: pip install "poetry==1.4.2" + - name: Install dependencies + run: poetry install + - name: Test + run: | + poetry run pytest -v tests + build: - name: Build and test wheel + name: Build and check wheel runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -59,7 +76,7 @@ jobs: publish: name: Publish release to Github and Pypi runs-on: ubuntu-latest - needs: [lint, build] + needs: [lint, test, build] if: success() && startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/checkout@v3 diff --git a/keycmd/__init__.py b/keycmd/__init__.py index d3ec452..493f741 100644 --- a/keycmd/__init__.py +++ b/keycmd/__init__.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.3.0" diff --git a/keycmd/__main__.py b/keycmd/cli.py similarity index 95% rename from keycmd/__main__.py rename to keycmd/cli.py index 4f0e49b..db2e33e 100644 --- a/keycmd/__main__.py +++ b/keycmd/cli.py @@ -31,9 +31,9 @@ cli.add_argument("command", nargs="*", help="command to run") -def main(): +def main(args=None): """CLI entrypoint""" - args = cli.parse_args() + args = cli.parse_args(args=args) if args.verbose: set_verbose() diff --git a/keycmd/conf.py b/keycmd/conf.py index 9fb42b5..e13cc76 100644 --- a/keycmd/conf.py +++ b/keycmd/conf.py @@ -6,6 +6,10 @@ from .logs import vlog +# exposed for testing +USERPROFILE = "~" + + def load_toml(path): """Load a toml file""" with path.open("rb") as fh: @@ -23,7 +27,7 @@ def load_pyproj(path): def defaults(): """Generate the default config""" - return {} + return {"keys": {}} def merge_conf(a, b): @@ -53,14 +57,14 @@ def load_conf(): cwd = Path.cwd() # ~/.keycmd - fpath = Path.home() / ".keycmd" + fpath = (Path(USERPROFILE).expanduser() / ".keycmd").resolve() if fpath.is_file(): vlog(f"loading config file {fpath}") conf = merge_conf(conf, load_toml(fpath)) # pyproject.toml cur = cwd - while cur != cur.anchor: + while True: pyproj = cur / "pyproject.toml" if pyproj.is_file(): vlog(f"loading config file {pyproj}") @@ -69,6 +73,9 @@ def load_conf(): # stop at the boundary of git repositories if (cur / ".git").is_dir(): break + # stop if we can't go up anymore + if cur.parent == cur: + break cur = cur.parent # ./.keycmd diff --git a/keycmd/creds.py b/keycmd/creds.py index 4af776a..06bf1c2 100644 --- a/keycmd/creds.py +++ b/keycmd/creds.py @@ -3,7 +3,7 @@ import keyring -from .logs import vlog +from .logs import error, vlog def b64(value): @@ -16,12 +16,18 @@ def get_env(conf): env = environ.copy() for key, src in conf["keys"].items(): password = keyring.get_password(src["credential"], src["username"]) + if password is None: + error( + f"MISSING credential {src['credential']}" + f" with user {src['username']} " + f" as it does not exist" + ) if src.get("b64"): password = b64(password) env[key] = password vlog( f"exposing credential {src['credential']}" - f" belonging to user {src['username']}" + f" with user {src['username']} " f" as environment variable {key} (b64: {src.get('b64', False)})" ) return env diff --git a/keycmd/logs.py b/keycmd/logs.py index 51f3ef8..7f23af2 100644 --- a/keycmd/logs.py +++ b/keycmd/logs.py @@ -1,16 +1,20 @@ -from sys import exit +import sys _verbose = False -def set_verbose(): +def set_verbose(verbose=True): global _verbose - _verbose = True + _verbose = verbose -def log(msg): - print(f"keycmd: {msg}") +def log(msg, err=False): + msg = f"keycmd: {msg}" + if err: + print(msg, file=sys.stderr) + else: + print(msg) def vlog(msg): @@ -19,8 +23,8 @@ def vlog(msg): def error(msg): - log(f"error: {msg}") - exit(1) + log(f"error: {msg}", err=True) + sys.exit(1) def vwarn(msg): diff --git a/poetry.lock b/poetry.lock index a2b38e4..bef562c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -68,14 +68,14 @@ css = ["tinycss2 (>=1.1.0,<1.2)"] [[package]] name = "certifi" -version = "2023.5.7" +version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.5.7-py3-none-any.whl", hash = "sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716"}, - {file = "certifi-2023.5.7.tar.gz", hash = "sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7"}, + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] [[package]] @@ -325,6 +325,21 @@ files = [ {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] +[[package]] +name = "exceptiongroup" +version = "1.1.2" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.2-py3-none-any.whl", hash = "sha256:e346e69d186172ca7cf029c8c1d16235aa0e04035e5750b4b95039e65204328f"}, + {file = "exceptiongroup-1.1.2.tar.gz", hash = "sha256:12c3e887d6485d16943a309616de20ae5582633e0a2eda17f4e10fd61c1e8af5"}, +] + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "idna" version = "3.4" @@ -357,6 +372,18 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker perf = ["ipython"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "jaraco-classes" version = "3.3.0" @@ -455,14 +482,14 @@ files = [ [[package]] name = "more-itertools" -version = "9.1.0" +version = "10.0.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"}, - {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"}, + {file = "more-itertools-10.0.0.tar.gz", hash = "sha256:cd65437d7c4b615ab81c0640c0480bc29a550ea032891977681efd28344d51e1"}, + {file = "more_itertools-10.0.0-py3-none-any.whl", hash = "sha256:928d514ffd22b5b0a8fce326d57f423a55d2ff783b093bab217eda71e732330f"}, ] [[package]] @@ -532,6 +559,22 @@ files = [ docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"] +[[package]] +name = "pluggy" +version = "1.2.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849"}, + {file = "pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pycparser" version = "2.21" @@ -559,6 +602,29 @@ files = [ [package.extras] plugins = ["importlib-metadata"] +[[package]] +name = "pytest" +version = "7.4.0" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.0-py3-none-any.whl", hash = "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32"}, + {file = "pytest-7.4.0.tar.gz", hash = "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "pywin32-ctypes" version = "0.2.2" @@ -824,5 +890,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" -python-versions = "^3.9" -content-hash = "1402d1967b8b632383d632dcd2313b90fbe3faba5247e5a5c178c4f5cdb78d82" +python-versions = ">=3.9" +content-hash = "ce97f9f95c3f91fedfc55c5b1faf6abd6c2368b220e67a2b353eb84e45cee681" diff --git a/pyproject.toml b/pyproject.toml index 239ef4f..7ad2f2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,24 +1,25 @@ [tool.poetry] name = "keycmd" -version = "0.2.0" +version = "0.3.0" description = "" authors = ["Korijn van Golen "] license = "MIT" readme = "README.md" [tool.poetry.dependencies] -python = "^3.9" -keyring = "~24.2.0" +python = ">=3.9" +keyring = "^24.2.0" tomli = "^2.0.1" shellingham = "^1.5.0.post1" [tool.poetry.scripts] -keycmd = 'keycmd.__main__:main' +keycmd = 'keycmd.cli:main' [tool.poetry.group.dev.dependencies] ruff = "^0.0.278" black = "^23.7.0" twine = "^4.0.2" +pytest = "^7.4.0" [build-system] requires = ["poetry-core"] diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..fc43319 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,96 @@ +import os +from pathlib import Path +from subprocess import run +from functools import cache + +import pytest +import keyring + +import keycmd.conf +from keycmd.shell import get_shell +from keycmd import __version__ +from keycmd.cli import main + + +varname = "KEYCMD_TEST" +key = "__keycmd_testß" +username = "usernameß" +# TODO: figure out how to simulate pytest capfd encoding on CI +password = "password" + + +@pytest.fixture +def credentials(): + keyring.set_password(key, username, password) + yield + keyring.delete_password(key, username) + + +@pytest.fixture +def ch_tmpdir(tmpdir): + cwd = Path.cwd() + os.chdir(tmpdir) + yield tmpdir + os.chdir(cwd) + + +@pytest.fixture +def userprofile(tmpdir, monkeypatch): + user_dir = Path(tmpdir) / ".user" + user_dir.mkdir(parents=True, exist_ok=True) + monkeypatch.setattr(keycmd.conf, "USERPROFILE", user_dir) + yield user_dir + + +@pytest.fixture +def local_conf(ch_tmpdir): + Path(".keycmd").write_text( + """[keys] +{varname} = {{ credential = "{key}", username = "{username}" }} +""".format( + varname=varname, + key=key, + username=username, + ), + encoding="utf-8", + ) + + +@cache +def get_encoding_stdin(): + shell_name, _ = get_shell() + opt = "-c" + if shell_name == "cmd": + opt = "/C" + p = run( + [shell_name, opt, "python", "-c", "import sys; print(sys.stdin.encoding)"], + shell=False, + capture_output=True, + check=True, + ) + return p.stdout.decode("utf-8").strip() + + +def test_cli_version(capfd): + main(["--version"]) + assert capfd.readouterr().out.strip() == f"keycmd: v{__version__}" + + +def test_cli(capfd, ch_tmpdir, credentials, local_conf, userprofile): + name, _ = get_shell() + if name == "cmd": + var = f"%{varname}%" + elif name in {"pwsh", "powershell"}: + var = f"$env:{varname}" + else: + var = f"${varname}" + + with pytest.raises(SystemExit): + main(["echo", var]) + assert capfd.readouterr().out.strip() == password + + +def test_cli_missing_credential(capfd, ch_tmpdir, local_conf, userprofile): + with pytest.raises(SystemExit) as exc_info: + main(["echo", "foo"]) + assert exc_info.value.args[0] == 1 diff --git a/tests/test_conf.py b/tests/test_conf.py new file mode 100644 index 0000000..9730b42 --- /dev/null +++ b/tests/test_conf.py @@ -0,0 +1,234 @@ +import os +from pathlib import Path + +import tomli +import pytest + +import keycmd.conf +from keycmd.conf import load_toml, load_pyproj, defaults, merge_conf, load_conf + + +def test_defaults(): + # verify that defaults creates + # a new dictionary every time + assert defaults() is not defaults() + + +def test_load_toml(ch_tmpdir): + path = Path("foo.toml") + path.write_text("[keys]", encoding="utf-8") + doc = load_toml(path) + assert doc["keys"] == {} + + path = Path("bar.toml") + with pytest.raises(FileNotFoundError) as err: + load_toml(path) + + path = Path("baz.toml") + path.write_text("[keys}", encoding="utf-8") + with pytest.raises(tomli.TOMLDecodeError) as err: + load_toml(path) + assert path.name in err.value.args[0] + + +def test_load_pyproj(ch_tmpdir): + path = Path("pyproject.toml") + path.write_text("[tool.keycmd.keys]", encoding="utf-8") + doc = load_pyproj(path) + assert doc["keys"] == {} + + path = Path("bar.toml") + with pytest.raises(FileNotFoundError) as err: + load_pyproj(path) + + path = Path("pyproject.toml") + path.write_text("[keys}", encoding="utf-8") + with pytest.raises(tomli.TOMLDecodeError) as err: + load_pyproj(path) + assert path.name in err.value.args[0] + + +def test_merge_conf(): + a = { + "keys": { + "foo": { + "bla": "bla", + }, + "bar": { + "bla": "bla", + }, + } + } + b = { + "something": "else", + "keys": { + "foo": { + "bla": "blabla", + }, + "baz": { + "bla": "bla", + }, + }, + } + + c = merge_conf(a, b) + assert c == { + "something": "else", + "keys": { + "foo": {"bla": "blabla"}, + "bar": {"bla": "bla"}, + "baz": {"bla": "bla"}, + }, + } + + d = {} + e = merge_conf(a, d) + assert e == { + "keys": { + "foo": {"bla": "bla"}, + "bar": {"bla": "bla"}, + }, + } + + +def create_pyproj_conf(relpath="."): + pyproj_dir = Path(relpath) + pyproj_dir.mkdir(exist_ok=True, parents=True) + pyproj_path = (pyproj_dir / "pyproject.toml").resolve() + pyproj_path.write_text( + """[tool.keycmd.keys] +a = { foo = "bar" } +b = { foo = "bar" } +""", + encoding="utf-8", + ) + return pyproj_path + + +def create_user_conf(): + user_path = (keycmd.conf.USERPROFILE / ".keycmd").resolve() + user_path.write_text( + """[keys] +a = { foo = "baz" } +c = { foo = "bar" } +""", + encoding="utf-8", + ) + return user_path + + +def create_local_conf(): + local_path = Path(".keycmd") + local_path.write_text( + """[keys] +a = { foo = "quux" } +d = { foo = "bar" } +""", + encoding="utf-8", + ) + return local_path + + +@pytest.fixture +def userprofile(tmpdir, monkeypatch): + user_dir = Path(tmpdir) / ".user" + user_dir.mkdir(parents=True, exist_ok=True) + monkeypatch.setattr(keycmd.conf, "USERPROFILE", user_dir) + yield user_dir + + +@pytest.fixture +def ch_tmpdir(tmpdir): + cwd = Path.cwd() + tmpdir = Path(tmpdir) / "much" / "nested" / "so" / "deep" + tmpdir.mkdir(parents=True) + os.chdir(tmpdir) + yield tmpdir + os.chdir(cwd) + + +def test_load_conf(ch_tmpdir, userprofile): + conf = load_conf() + assert conf == { + "keys": {}, + } + + pyproj_path = create_pyproj_conf() + conf = load_conf() + assert conf == { + "keys": { + "a": { + "foo": "bar", + }, + "b": { + "foo": "bar", + }, + }, + } + + create_user_conf() + conf = load_conf() + assert conf == { + "keys": { + "a": { + # pyproject.toml takes precedence + "foo": "bar", + }, + "b": { + "foo": "bar", + }, + "c": { + "foo": "bar", + }, + }, + } + + pyproj_path.unlink() + conf = load_conf() + assert conf == { + "keys": { + "a": { + "foo": "baz", + }, + "c": { + "foo": "bar", + }, + }, + } + + create_local_conf() + conf = load_conf() + assert conf == { + "keys": { + "a": { + # .keycmd takes precedence + "foo": "quux", + }, + "c": { + "foo": "bar", + }, + "d": { + "foo": "bar", + }, + }, + } + + pyproj_path = create_pyproj_conf(relpath="..") + conf = load_conf() + assert conf == { + "keys": { + "a": { + # .keycmd takes precedence + "foo": "quux", + }, + "b": { + "foo": "bar", + }, + "c": { + "foo": "bar", + }, + "d": { + "foo": "bar", + }, + }, + } diff --git a/tests/test_creds.py b/tests/test_creds.py new file mode 100644 index 0000000..3e16d4a --- /dev/null +++ b/tests/test_creds.py @@ -0,0 +1,47 @@ +from os import environ + +import keyring +import pytest + +from keycmd.creds import b64, get_env + + +def test_b64(): + assert b64("fooß") == "Zm9vw58=" + + +key = "__keycmd_testß" +username = "usernameß" +password = "passwordß" + + +@pytest.fixture +def credentials(): + keyring.set_password(key, username, password) + yield + keyring.delete_password(key, username) + + +def test_get_env(credentials): + conf = { + "keys": { + "__FOOBAR": { + "credential": key, + "username": username, + }, + "__FOOBAR_B64": { + "credential": key, + "username": username, + "b64": True, + }, + } + } + env = get_env(conf) + assert "__FOOBAR" not in environ + assert "__FOOBAR_B64" not in environ + assert env.get("__FOOBAR") == password + assert env.get("__FOOBAR_B64") == b64(password) + assert set(environ.keys()).intersection(set(env.keys())) == set(environ.keys()) + assert set(environ.keys()).symmetric_difference(set(env.keys())) == set( + conf["keys"].keys() + ) diff --git a/tests/test_logs.py b/tests/test_logs.py new file mode 100644 index 0000000..f22fb63 --- /dev/null +++ b/tests/test_logs.py @@ -0,0 +1,32 @@ +from functools import partial + +import pytest + +from keycmd.logs import set_verbose, log, vlog, error, vwarn + + +def test_logging(capsys, request): + vlog("foo") + vwarn("foo") + log("foo") + assert capsys.readouterr().out == "keycmd: foo\n" + + set_verbose() + request.addfinalizer(partial(set_verbose, False)) + + vlog("foo") + assert capsys.readouterr().out == "keycmd: foo\n" + vwarn("foo") + assert capsys.readouterr().out == "keycmd: warning: foo\n" + log("foo") + assert capsys.readouterr().out == "keycmd: foo\n" + + with pytest.raises(SystemExit) as exc_info: + error("foo") + assert exc_info.value.args[0] == 1 + assert capsys.readouterr().err == "keycmd: error: foo\n" + + set_verbose(False) + + vlog("foo") + assert capsys.readouterr().out == "" diff --git a/tests/test_shell.py b/tests/test_shell.py new file mode 100644 index 0000000..bbea337 --- /dev/null +++ b/tests/test_shell.py @@ -0,0 +1,53 @@ +from os import environ +from shutil import which + +import pytest + +from keycmd.shell import get_shell, run_shell, run_cmd + + +def test_get_shell(): + name, path = get_shell() + assert which(path) is not None + assert len(name) + + +def test_run_shell(): + with pytest.raises(SystemExit) as exc_info: + run_shell() + assert exc_info.value.args[0] == 0 + + +def test_run_cmd(capfd): + with pytest.raises(SystemExit) as exc_info: + run_cmd(["echo", "foo"]) + assert exc_info.value.args[0] == 0 + assert capfd.readouterr().out.strip() == "foo" + + with pytest.raises(SystemExit) as exc_info: + run_cmd(["sadfdasfsdf"]) + assert exc_info.value.args[0] == 1 + + +def test_run_cmd_env(capfd): + env = environ.copy() + var_value = "foobar" + env["KEYCMD_TEST_FOOBAR"] = var_value + + name, _ = get_shell() + if name == "cmd": + var = r"%KEYCMD_TEST_FOOBAR%" + elif name in {"pwsh", "powershell"}: + var = "$env:KEYCMD_TEST_FOOBAR" + else: + var = "$KEYCMD_TEST_FOOBAR" + + with pytest.raises(SystemExit) as exc_info: + run_cmd(["echo", var], env=env) + assert exc_info.value.args[0] == 0 + assert capfd.readouterr().out.strip() == var_value + + with pytest.raises(SystemExit) as exc_info: + run_cmd(["echo", var]) + assert exc_info.value.args[0] == 0 + assert capfd.readouterr().out.strip() == var