Skip to content

Commit

Permalink
tests: consolidate cli runner
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck committed Sep 30, 2024
1 parent 01c1354 commit ba546d5
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 449 deletions.
7 changes: 4 additions & 3 deletions cyclonedx_py/_internal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
import logging
import sys
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
from collections.abc import Sequence
from itertools import chain
from typing import TYPE_CHECKING, Any, Dict, List, Optional, TextIO, Type
from typing import TYPE_CHECKING, Any, Dict, NoReturn, Optional, TextIO, Type, Union

from cyclonedx.model import Property
from cyclonedx.output import make_outputter
Expand Down Expand Up @@ -256,7 +257,7 @@ def __call__(self,
self._write(output, outfile)


def run(*, argv: Optional[List[str]] = None, **kwargs: Any) -> int:
def run(*, argv: Optional[Sequence[str]] = None, **kwargs: Any) -> Union[int, NoReturn]:
arg_co = ArgumentParser(add_help=False)
arg_co.add_argument('-v', '--verbose',
help='Increase the verbosity of messages'
Expand All @@ -267,7 +268,7 @@ def run(*, argv: Optional[List[str]] = None, **kwargs: Any) -> int:
default=0)
arg_parser = Command.make_argument_parser(**kwargs, sco=arg_co)
del arg_co, kwargs
args = vars(arg_parser.parse_args(argv))
args = vars(arg_parser.parse_args(argv)) # may exit -> raise `SystemExit`
if args['command'] is None:
# print the "help" page on error, instead of printing "usage" page
# this is done to have a better user experience.
Expand Down
23 changes: 23 additions & 0 deletions tests/integration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,26 @@
#
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.


from contextlib import redirect_stderr, redirect_stdout
from io import BytesIO, StringIO, TextIOWrapper
from typing import Any, Optional
from unittest.mock import patch

from cyclonedx_py._internal.cli import run as _run_cli


def run_cli(*args: str, inp: Optional[Any] = None) -> (int, str, str):
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
with patch('sys.stdin', TextIOWrapper(inp or BytesIO())):
try:
c_res = _run_cli(argv=args, exit_on_error=False)
except SystemExit as e:
c_res = e.code
c_out = out.getvalue()
c_err = err.getvalue()
return c_res, c_out, c_err
10 changes: 8 additions & 2 deletions tests/integration/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@
from unittest import TestCase

from cyclonedx_py import __version__
from tests.integration import run_cli


class TestPipenv(TestCase):
class TestCli(TestCase):

def test_call_as_module(self) -> None:
def test_version_as_module(self) -> None:
# show that this thing is callable as a module
# show that the version is the one expected
res = run( # nosec:B603
(executable, '-m', 'cyclonedx_py', '--version'),
capture_output=True, encoding='utf8', shell=False)
self.assertEqual(0, res.returncode, '\n'.join((res.stdout, res.stderr)))
self.assertIn(__version__, res.stdout)

def test_version(self) -> None:
res, out, err = run_cli('--version')
self.assertEqual(0, res, '\n'.join((out, err)))
self.assertIn(__version__, out)
220 changes: 82 additions & 138 deletions tests/integration/test_cli_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.

import random
from contextlib import redirect_stderr, redirect_stdout
from glob import glob
from io import StringIO
from os import name as os_name
from os.path import basename, dirname, join
from subprocess import run # nosec:B404
Expand All @@ -29,8 +27,8 @@
from cyclonedx.schema import OutputFormat, SchemaVersion
from ddt import ddt, named_data

from cyclonedx_py._internal.cli import run as run_cli
from tests import INFILES_DIRECTORY, INIT_TESTBEDS, SUPPORTED_OF_SV, SnapshotMixin, make_comparable
from tests.integration import run_cli

initfiles = glob(join(INFILES_DIRECTORY, 'environment', '*', 'init.py'))
test_data = tuple(
Expand Down Expand Up @@ -68,19 +66,13 @@ def setUpClass(cls) -> None:
)
def test_fails_with_python_not_found(self, wrong_python: str, expected_error: str) -> None:
_, _, sv, of = random.choice(test_data) # nosec B311
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
f'--sv={sv.to_version()}',
f'--of={of.name}',
'--outfile=-',
wrong_python])
err = err.getvalue()
out = out.getvalue()
res, out, err = run_cli(
'environment',
'-vvv',
f'--sv={sv.to_version()}',
f'--of={of.name}',
'--outfile=-',
wrong_python)
self.assertNotEqual(0, res, err)
self.assertIn(expected_error, err)

Expand All @@ -91,76 +83,52 @@ def test_fails_with_python_not_found(self, wrong_python: str, expected_error: st
@skipIf(os_name == 'nt', 'cannot run on win')
def test_fails_with_python_unexpected(self, wrong_python: str, expected_error: str) -> None:
_, _, sv, of = random.choice(test_data) # nosec B311
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
f'--sv={sv.to_version()}',
f'--of={of.name}',
'--outfile=-',
wrong_python])
err = err.getvalue()
out = out.getvalue()
res, out, err = run_cli(
'environment',
'-vvv',
f'--sv={sv.to_version()}',
f'--of={of.name}',
'--outfile=-',
wrong_python)
self.assertNotEqual(0, res, err)
self.assertIn(expected_error, err)

def test_with_pyproject_not_found(self) -> None:
_, projectdir, sv, of = random.choice(test_data) # nosec B311
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--outfile=-',
'--pyproject=something-that-must-not-exist.testing',
projectdir
])
err = err.getvalue()
out = out.getvalue()
res, out, err = run_cli(
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--outfile=-',
'--pyproject=something-that-must-not-exist.testing',
projectdir
)
self.assertNotEqual(0, res, err)
self.assertIn('Could not open pyproject file: something-that-must-not-exist.testing', err)

def test_with_current_python(self) -> None:
sv = SchemaVersion.V1_6
of = random.choice((OutputFormat.XML, OutputFormat.JSON)) # nosec B311
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
# no project dir -> search in current python
])
err = err.getvalue()
sbom1 = out.getvalue()
res, sbom1, err = run_cli(
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
# no project dir -> search in current python
)
self.assertEqual(0, res, err)
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
executable # explicitly current python
])
err = err.getvalue()
sbom2 = out.getvalue()
res, sbom2, err = run_cli(
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
executable # explicitly current python
)
self.assertEqual(0, res, err)
self.assertEqual(
make_comparable(sbom1, of),
Expand All @@ -169,85 +137,61 @@ def test_with_current_python(self) -> None:

@named_data(*test_data)
def test_plain_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputFormat) -> None:
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
join(projectdir, '.venv')])
err = err.getvalue()
out = out.getvalue()
res, out, err = run_cli(
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
join(projectdir, '.venv'))
self.assertEqual(0, res, err)
self.assertEqualSnapshot(out, 'plain', projectdir, sv, of)

@named_data(*test_data_file_filter('pep639'))
def test_pep639_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputFormat) -> None:
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
'--PEP-639',
join(projectdir, '.venv')])
err = err.getvalue()
out = out.getvalue()
res, out, err = run_cli(
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
'--PEP-639',
join(projectdir, '.venv'))
self.assertEqual(0, res, err)
self.assertEqualSnapshot(out, 'pep639', projectdir, sv, of)

@named_data(*test_data_file_filter('pep639'))
def test_pep639_texts_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputFormat) -> None:
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
'--PEP-639',
'--gather-license-texts',
join(projectdir, '.venv')])
err = err.getvalue()
out = out.getvalue()
res, out, err = run_cli(
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
'--PEP-639',
'--gather-license-texts',
join(projectdir, '.venv'))
self.assertEqual(0, res, err)
self.assertEqualSnapshot(out, 'pep639-texts', projectdir, sv, of)

@named_data(*test_data_file_filter('pep639'))
def test_texts_as_expected(self, projectdir: str, sv: SchemaVersion, of: OutputFormat) -> None:
with StringIO() as err, StringIO() as out:
err.name = '<fakeerr>'
out.name = '<fakeout>'
with redirect_stderr(err), redirect_stdout(out):
res = run_cli(argv=[
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
'--gather-license-texts',
join(projectdir, '.venv')])
err = err.getvalue()
out = out.getvalue()
res, out, err = run_cli(
'environment',
'-vvv',
'--sv', sv.to_version(),
'--of', of.name,
'--output-reproducible',
'--outfile=-',
'--pyproject', join(projectdir, 'pyproject.toml'),
'--gather-license-texts',
join(projectdir, '.venv'))
self.assertEqual(0, res, err)
self.assertEqualSnapshot(out, 'texts', projectdir, sv, of)

Expand Down
Loading

0 comments on commit ba546d5

Please sign in to comment.