Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more CLI tests #152

Merged
merged 14 commits into from
Feb 16, 2025
55 changes: 23 additions & 32 deletions junitparser/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from . import JUnitXml, version


def merge(paths, output, suite_name):
"""Merge XML report."""
def merge(paths, output, suite_name=""):
"""Merge XML reports."""
result = JUnitXml()
for path in paths:
result += JUnitXml.fromfile(path)
Expand Down Expand Up @@ -44,18 +44,23 @@ def _parser(prog_name=None): # pragma: no cover
command_parser = parser.add_subparsers(dest="command", help="command")
command_parser.required = True

# command: merge
merge_parser = command_parser.add_parser(
"merge", help="Merge JUnit XML format reports with junitparser."
)
merge_parser.add_argument(
# an abstract object that defines common arguments used by multiple commands
abstract_parser = ArgumentParser(add_help=False)
abstract_parser.add_argument(
"--glob",
help="Treat original XML path(s) as glob(s).",
dest="paths_are_globs",
action="store_true",
default=False,
)
merge_parser.add_argument("paths", nargs="+", help="Original XML path(s).")
abstract_parser.add_argument("paths", help="Original XML path(s).", nargs="+")

# command: merge
merge_parser = command_parser.add_parser(
"merge",
help="Merge JUnit XML format reports with junitparser.",
parents=[abstract_parser],
)
merge_parser.add_argument(
"output", help='Merged XML Path, setting to "-" will output to the console'
)
Expand All @@ -65,39 +70,25 @@ def _parser(prog_name=None): # pragma: no cover
)

# command: verify
merge_parser = command_parser.add_parser(
verify_parser = command_parser.add_parser( # noqa: F841
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I presume the variable is not needed if unused.

Suggested change
verify_parser = command_parser.add_parser( # noqa: F841
command_parser.add_parser(

Copy link
Contributor Author

@Cube707 Cube707 Feb 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The verify_parser is still relevant and used and might be modified in the future.

Its variable binding is only unused because it currently only has the attributes inherited from the abstract_parser and no explicit ones of its own.

I believe it should be kept for consistency with the other parsers and to increase the readability for future modifications.

That's why I ignored the warning for flake8

"verify",
help="Return a non-zero exit code if one of the testcases failed or errored.",
)
merge_parser.add_argument(
"--glob",
help="Treat original XML path(s) as glob(s).",
dest="paths_are_globs",
action="store_true",
default=False,
)
merge_parser.add_argument(
"paths", nargs="+", help="XML path(s) of reports to verify."
parents=[abstract_parser],
)

return parser


def main(args=None, prog_name=None):
"""CLI's main runner."""
args = args or _parser(prog_name=prog_name).parse_args()
args = _parser(prog_name=prog_name).parse_args(args)
paths = (
chain.from_iterable(iglob(path) for path in args.paths)
if args.paths_are_globs
else args.paths
)
if args.command == "merge":
return merge(
chain.from_iterable(iglob(path) for path in args.paths)
if args.paths_are_globs
else args.paths,
args.output,
args.suite_name,
)
return merge(paths, args.output, args.suite_name)
if args.command == "verify":
return verify(
chain.from_iterable(iglob(path) for path in args.paths)
if args.paths_are_globs
else args.paths
)
return verify(paths)
return 255
8 changes: 8 additions & 0 deletions tests/data/pytest_error.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="pytest" errors="0" failures="1" skipped="0" tests="1" time="0.039" timestamp="2025-01-14T20:33:43.564504+01:00">
<testcase classname="tests.test_cli" name="test_merge" time="0.001">
<failure message="NameError: name 'file' is not defined" />
</testcase>
</testsuite>
</testsuites>
6 changes: 6 additions & 0 deletions tests/data/pytest_success.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<testsuites>
<testsuite name="pytest" errors="0" failures="0" skipped="0" tests="1" time="0.026" timestamp="2025-01-14T20:33:46.249864+01:00" >
<testcase classname="tests.test_fromfile" name="test_fromfile" time="0.001" />
</testsuite>
</testsuites>
107 changes: 101 additions & 6 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,107 @@
import os
from pathlib import Path
import pytest
from junitparser.cli import verify
from junitparser import cli
from junitparser import version

DATA_DIR = Path(__file__).parent / "data"


@pytest.mark.parametrize(
"file, expected_exitcode",
[("data/jenkins.xml", 1), ("data/no_fails.xml", 0), ("data/normal.xml", 1)],
[("jenkins.xml", 1), ("no_fails.xml", 0), ("normal.xml", 1)],
)
def test_verify(file, expected_exitcode):
path = os.path.join(os.path.dirname(__file__), file)
assert verify([path]) == expected_exitcode
def test_verify(file: str, expected_exitcode: int):
path = DATA_DIR / file
assert cli.verify([path]) == expected_exitcode


def test_merge(tmp_path: Path):
files = [DATA_DIR / "jenkins.xml", DATA_DIR / "pytest_success.xml"]
suites = ["JUnitXmlReporter", "JUnitXmlReporter.constructor", "pytest"]
outfile = tmp_path / "merged.xml"
cli.merge(files, str(outfile))
xml = outfile.read_text()
for s in suites:
assert f'name="{s}"' in xml


def test_merge_output_to_terminal(capsys: pytest.CaptureFixture):
ret = cli.main(["merge", str(DATA_DIR / "normal.xml"), "-"])
assert ret == 0
captured = capsys.readouterr()
assert captured.out.startswith("<?xml version='1.0'")


def test_verify_with_glob():
ret = cli.main(["verify", "--glob", str(DATA_DIR / "pytest_*.xml")])
# we expect failure, as one of the files has errors
assert ret == 1


class Test_CommandlineOptions:

@classmethod
def setup_class(cls):
cls.parser = cli._parser("junitparser")

@pytest.mark.parametrize("arg", ["-v", "--version"])
def test_version(self, arg, capsys):
with pytest.raises(SystemExit) as e:
self.parser.parse_args([arg])
captured = capsys.readouterr()
assert e.value.code == 0
assert captured.out == f"junitparser {version}\n"

def test_help_shows_subcommands(self, capsys):
with pytest.raises(SystemExit) as e:
self.parser.parse_args(["--help"])
captured = capsys.readouterr()
assert "{merge,verify} ...\n" in captured.out
assert e.value.code == 0

@pytest.mark.parametrize("command", ["merge", "verify"])
def test_subcommand_help(self, command):
with pytest.raises(SystemExit) as e:
self.parser.parse_args([command, "--help"])
assert e.value.code == 0

@pytest.mark.parametrize("command", ["merge", "verify"])
def test_subcommands_help_general_options(self, command, capsys):
with pytest.raises(SystemExit):
self.parser.parse_args([command, "--help"])
captured = capsys.readouterr()
assert "[--glob]" in captured.out
assert "paths [paths ...]" in captured.out

def test_merge_help_options(self, capsys):
with pytest.raises(SystemExit):
self.parser.parse_args(["merge", "--help"])
captured = capsys.readouterr()
assert "[--suite-name SUITE_NAME]" in captured.out
assert "output\n" in captured.out

@pytest.mark.parametrize("command", ["merge", "verify"])
def test_option_glob(
self,
command,
):
args = self.parser.parse_args([command, "--glob", "pytest_*.xml", "-"])
assert args.paths_are_globs

def test_verify_argument_path(self):
files = ["foo", "bar"]
args = self.parser.parse_args(["verify", *files])
assert args.paths == files

def test_merge_argument_path(self):
files = ["foo", "bar"]
args = self.parser.parse_args(["merge", *files, "-"])
assert args.paths == files

def test_merge_option_suite_name(self):
args = self.parser.parse_args(["merge", "--suite-name", "foo", "_", "-"])
assert args.suite_name == "foo"

def test_merge_argument_output(self):
args = self.parser.parse_args(["merge", "foo", "bar"])
assert args.output == "bar"
Loading