Skip to content

Commit

Permalink
Added CLI tests
Browse files Browse the repository at this point in the history
  • Loading branch information
campos-ddc committed Aug 25, 2021
1 parent f744107 commit 60cc129
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 15 deletions.
19 changes: 18 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ click = "^8.0.1"
pytest = "^6.2.4"
black = "^21.7b0"
tox = "^3.24.3"
mock = "^4.0.3"

[build-system]
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
Expand Down
195 changes: 195 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import io
import textwrap
import traceback

import mock
from click.testing import CliRunner, Result


def test_no_patches():
"""
Test that we call yaml_patch correctly
"""

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, input="key: value")

__assert_result_success(result)
assert result.stdout == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=dict())


def test_patches():
"""
Test that patch arguments from CLI are parsed correctly
"""
runner = CliRunner()

cli_patches = [
"key='new value'",
"key.subkey='new subvalue'",
"key.list.[0]='value in list'",
"key.subint=5",
"key.subbool=true",
]

expected_patches = {
"key": "new value",
"key.subkey": "new subvalue",
"key.list.[0]": "value in list",
"key.subint": 5,
"key.subbool": True,
}

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, cli_patches, input="key: value")

__assert_result_success(result)
assert result.stdout == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=expected_patches)


def test_patch_file_input(tmp_path):
"""
Test that we can patch a file other than stdin and still output to stdout
"""
tmp_yaml = tmp_path / "test.yml"
tmp_yaml.write_text("key: 'value in file'")

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={tmp_yaml}"])

__assert_result_success(result)
assert result.stdout == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())


def test_patch_file_output(tmp_path):
"""
Test that we can patch stdin and output to a file
"""
tmp_yaml = tmp_path / "test.yml"
# tmp_yaml.write_text("key: 'value in file'")

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--output={tmp_yaml}"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert tmp_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: value", patches=dict())


def test_patch_file_input_and_output(tmp_path):
"""
Test that we can patch from file into another file
"""
input_yaml = tmp_path / "input.yml"
input_yaml.write_text("key: 'value in file'")
output_yaml = tmp_path / "output.yml"

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={input_yaml}", f"--output={output_yaml}"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert output_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())


def test_patch_file_same_input_and_output(tmp_path):
"""
Test that we can patch from file into the same file
"""
tmp_yaml = tmp_path / "input.yml"
tmp_yaml.write_text("key: 'value in file'")

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={tmp_yaml}", f"--output={tmp_yaml}"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert tmp_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())


def test_inplace(tmp_path):
"""
Test that we can patch from file into the same file using --in-place
"""
tmp_yaml = tmp_path / "input.yml"
tmp_yaml.write_text("key: 'value in file'")

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, [f"--file={tmp_yaml}", "--in-place"], input="key: value")

__assert_result_success(result)
assert result.stdout == "" # stdout is empty because we output to a file
assert tmp_yaml.read_text() == "mock_output"
mock_patch.assert_called_once_with(yaml_contents="key: 'value in file'", patches=dict())


def test_cant_use_inplace_with_stdin(tmp_path):
"""
Test that we get an error if trying to use --in-place with stdin as input
"""
tmp_yaml = tmp_path / "input.yml"
tmp_yaml.write_text("key: 'value in file'")

runner = CliRunner()

with mock.patch("yaml_patch.patch", return_value="mock_output") as mock_patch:
from yaml_patch.cli import cli

result = runner.invoke(cli, ["--in-place"], input="key: value")

assert result.exit_code == 1, __result_str(result)
assert str(result.exception) == str(ValueError("Cannot use --in-place with stdin as the source")), __result_str(
result
)
mock_patch.assert_not_called()


def __assert_result_success(result):
assert result.exception is None and result.exit_code == 0, __result_str(result)


def __result_str(result: Result):
as_str = f"\nexit_code: {result.exit_code}\n"

if result.exception is not None:
buf = io.StringIO()
traceback.print_tb(result.exc_info[2], file=buf)

as_str += f"exception: {result.exception}\n"
as_str += f"traceback:\n{textwrap.indent( buf.getvalue(), prefix=' ')}\n"

as_str += f"stdout:\n{textwrap.indent(result.stdout, prefix=' ')}\n"

return as_str
35 changes: 21 additions & 14 deletions yaml_patch/cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import sys

import click
from ruamel.yaml import YAML

from yaml_patch import patch


@click.command(
Expand Down Expand Up @@ -38,32 +35,42 @@
""",
)
@click.option(
"--file", "-f", type=click.File("r"), default=sys.stdin, help="Path to yaml file being patched. Defaults to stdin."
)
@click.option(
"--output", "-o", type=click.File("w"), default=sys.stdout, help="Path to output patched file. Defaults to stdout."
"--file", "-f", type=click.File("r"), default=None, help="Path to yaml file being patched. Defaults to stdin."
)
@click.option(
"--in-place", "-i", is_flag=True, help="Replace source file in-place. Overrides --output."
"--output", "-o", type=click.File("w"), default=None, help="Path to output patched file. Defaults to stdout."
)
@click.option("--in-place", "-i", is_flag=True, help="Replace source file in-place. Overrides --output.")
@click.argument("patches", nargs=-1)
def cli(file, output, in_place, patches):
# Default i/o to stdin/stdout down here so click.testing has time to capture those streams
if file is None:
file = sys.stdin
if output is None:
output = sys.stdout

# Check for bad arguments
if in_place and file == sys.stdin:
raise ValueError("Cannot use --in-place with stdin as the source")

# Split each patch into key+value separated by `=`. Use YAML to load the values coming from command line to ensure
# they are parsed into yaml syntax equivalents (automatically detect strings, ints, bools, etc).
from ruamel.yaml import YAML

yaml = YAML()
dict_patches = dict()
for p in patches:
k, v = p.split("=")
dict_patches[k] = yaml.load(v)

with file:
patched = patch(file.read(), dict_patches)
# Apply patches
from yaml_patch import patch

if in_place:
if file == sys.stdin:
raise RuntimeError("Cannot use --in-place with stdin as the source")
output = open(file.name, 'w')
patched = patch(yaml_contents=file.read(), patches=dict_patches)

# Output results
if in_place:
output = open(file.name, "w")
output.write(patched)


Expand Down

0 comments on commit 60cc129

Please sign in to comment.