diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index cdd2551..ff9d604 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -20,16 +20,6 @@
"settings": {
"python.pythonPath": "/usr/local/bin/python",
"python.defaultInterpreterPath": "/usr/local/bin/python",
- "pylint.path": [
- "pylint"
- ],
- "flake8.path": [
- "flake8"
- ],
- "mypy-type-checker.path": [
- "mypy"
- ],
- "mypy-type-checker.ignorePatterns": ["tests/e2e/example/**/*.py"],
"mypy.targets": [
"grizzly_cli/",
"tests/"
@@ -53,15 +43,13 @@
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
- "ms-python.pylint",
- "ms-python.flake8",
"ms-python.mypy-type-checker",
"editorconfig.editorconfig",
- "editorconfig.editorconfig",
"eamodio.gitlens",
"ryanluker.vscode-coverage-gutters",
"matangover.mypy",
- "oderwat.indent-rainbow"
+ "oderwat.indent-rainbow",
+ "charliermarsh.ruff"
]
}
},
diff --git a/.editorconfig b/.editorconfig
index 8ca17be..8f6152a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -16,3 +16,6 @@ indent_size = 2
[*.feature]
indent_size = 2
+
+[tests/unit/utils/test___init__.py]
+trim_trailing_whitespace = false
diff --git a/.github/workflows/code-quality.yaml b/.github/workflows/code-quality.yaml
index f33d582..01b72a4 100644
--- a/.github/workflows/code-quality.yaml
+++ b/.github/workflows/code-quality.yaml
@@ -33,17 +33,14 @@ jobs:
id: pip
run: python -m pip install .[dev]
- - name: pylint
- id: pylint
- run: python -m pylint --jobs=0 --fail-under=10 grizzly_cli/ tests/
- name: mypy
id: mypy
run: python -m mypy grizzly_cli/ tests/
- - name: flake8
- id: flake8
- run: python -m flake8
+ - name: ruff
+ id: ruff
+ run: python -m ruff check grizzly_cli/ tests/
test-and-coverage:
name: "test-and-coverage / ${{ matrix.runs-on }} / python-${{ matrix.python-version }}"
diff --git a/grizzly_cli/__init__.py b/grizzly_cli/__init__.py
index 4b3c218..099e646 100644
--- a/grizzly_cli/__init__.py
+++ b/grizzly_cli/__init__.py
@@ -1,28 +1,31 @@
-import os
+from __future__ import annotations
-from typing import Callable, List, Optional
+from os import environ
+from pathlib import Path
+from typing import TYPE_CHECKING, Callable, ClassVar, Optional
-from behave.model import Scenario
+from grizzly_cli.__version__ import __version__
-from .argparse import ArgumentSubParser
-from .__version__ import __version__
+if TYPE_CHECKING: # pragma: no cover
+ from behave.model import Scenario
+ from grizzly_cli.argparse import ArgumentSubParser
-EXECUTION_CONTEXT = os.getcwd()
+EXECUTION_CONTEXT = Path.cwd().as_posix()
-STATIC_CONTEXT = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'static')
+STATIC_CONTEXT = Path.joinpath(Path(__file__).parent.absolute(), 'static').as_posix()
-MOUNT_CONTEXT = os.environ.get('GRIZZLY_MOUNT_CONTEXT', EXECUTION_CONTEXT)
+MOUNT_CONTEXT = environ.get('GRIZZLY_MOUNT_CONTEXT', EXECUTION_CONTEXT)
-PROJECT_NAME = os.path.basename(EXECUTION_CONTEXT)
+PROJECT_NAME = Path(EXECUTION_CONTEXT).name
-SCENARIOS: List[Scenario] = []
+SCENARIOS: list[Scenario] = []
FEATURE_DESCRIPTION: Optional[str] = None
class register_parser:
- registered: List[Callable[[ArgumentSubParser], None]] = []
+ registered: ClassVar[list[Callable[[ArgumentSubParser], None]]] = []
order: Optional[int]
def __init__(self, order: Optional[int] = None) -> None:
diff --git a/grizzly_cli/__main__.py b/grizzly_cli/__main__.py
index 73b535e..212a8b7 100644
--- a/grizzly_cli/__main__.py
+++ b/grizzly_cli/__main__.py
@@ -1,19 +1,23 @@
-import argparse
+from __future__ import annotations
+
import os
import sys
-
+from pathlib import Path
from shutil import which
-from typing import Tuple, Optional, List
from traceback import format_exc
+from typing import TYPE_CHECKING, Optional
-from .argparse import ArgumentParser
-from .utils import ask_yes_no, get_distributed_system, get_dependency_versions, setup_logging
-from .init import init
-from .local import local
-from .distributed import distributed
-from .auth import auth
-from .keyvault import keyvault
-from . import __version__, register_parser
+from grizzly_cli import __version__, register_parser
+from grizzly_cli.argparse import ArgumentParser
+from grizzly_cli.auth import auth
+from grizzly_cli.distributed import distributed
+from grizzly_cli.init import init
+from grizzly_cli.keyvault import keyvault
+from grizzly_cli.local import local
+from grizzly_cli.utils import ask_yes_no, get_dependency_versions, get_distributed_system, setup_logging
+
+if TYPE_CHECKING:
+ import argparse
def _create_parser() -> ArgumentParser:
@@ -53,40 +57,65 @@ def _create_parser() -> ArgumentParser:
return parser
+def _parse_show_version(args: argparse.Namespace) -> None:
+ version = '(development)' if __version__ == '0.0.0' else __version__
+
+ grizzly_versions: Optional[tuple[Optional[str], Optional[list[str]]]] = None
+
+ if args.version == 'all':
+ grizzly_versions, locust_version = get_dependency_versions(local_install=False)
+ else:
+ grizzly_versions, locust_version = None, None
+
+ print(f'grizzly-cli {version}')
+ if grizzly_versions is not None:
+ grizzly_version, grizzly_extras = grizzly_versions
+ if grizzly_version is not None:
+ print(f'└── grizzly {grizzly_version}', end='')
+ if grizzly_extras is not None and len(grizzly_extras) > 0:
+ print(f' ── extras: {", ".join(grizzly_extras)}', end='')
+ print()
+
+ if locust_version is not None:
+ print(f' └── locust {locust_version}')
+
+ raise SystemExit(0)
+
+
+def _parse_run(parser: ArgumentParser, args: argparse.Namespace) -> None:
+ if args.command == 'dist':
+ if args.limit_nofile < 10001 and not args.yes:
+ print('!! this will cause warning messages from locust later on')
+ ask_yes_no('are you sure you know what you are doing?')
+ elif args.command == 'local' and which('behave') is None:
+ parser.error_no_help('"behave" not found in PATH, needed when running local mode')
+
+ if args.testdata_variable is not None:
+ for variable in args.testdata_variable:
+ try:
+ [name, value] = variable.split('=', 1)
+ os.environ[f'TESTDATA_VARIABLE_{name}'] = value
+ except ValueError: # noqa: PERF203
+ parser.error_no_help('-T/--testdata-variable needs to be in the format NAME=VALUE')
+
+ if args.csv_prefix is None:
+ if args.csv_interval is not None:
+ parser.error_no_help('--csv-interval can only be used in combination with --csv-prefix')
+
+ if args.csv_flush_interval is not None:
+ parser.error_no_help('--csv-flush-interval can only be used in combination with --csv-prefix')
+
+
def _parse_arguments() -> argparse.Namespace:
parser = _create_parser()
args = parser.parse_args()
if hasattr(args, 'file'):
# needed to support file names with spaces, which is escaped (sh-style)
- setattr(args, 'file', ' '.join(args.file))
+ args.file = ' '.join(args.file)
if args.version:
- if __version__ == '0.0.0':
- version = '(development)'
- else:
- version = __version__
-
- grizzly_versions: Optional[Tuple[Optional[str], Optional[List[str]]]] = None
-
- if args.version == 'all':
- grizzly_versions, locust_version = get_dependency_versions(False)
- else:
- grizzly_versions, locust_version = None, None
-
- print(f'grizzly-cli {version}')
- if grizzly_versions is not None:
- grizzly_version, grizzly_extras = grizzly_versions
- if grizzly_version is not None:
- print(f'└── grizzly {grizzly_version}', end='')
- if grizzly_extras is not None and len(grizzly_extras) > 0:
- print(f' ── extras: {", ".join(grizzly_extras)}', end='')
- print('')
-
- if locust_version is not None:
- print(f' └── locust {locust_version}')
-
- raise SystemExit(0)
+ _parse_show_version(args)
if args.command is None:
parser.error('no command specified')
@@ -101,36 +130,15 @@ def _parse_arguments() -> argparse.Namespace:
parser.error_no_help('cannot run distributed')
if args.registry is not None and not args.registry.endswith('/'):
- setattr(args, 'registry', f'{args.registry}/')
+ args.registry = f'{args.registry}/'
elif args.command in ['init', 'auth']:
- setattr(args, 'subcommand', None)
+ args.subcommand = None
if args.subcommand == 'run':
- if args.command == 'dist':
- if args.limit_nofile < 10001 and not args.yes:
- print('!! this will cause warning messages from locust later on')
- ask_yes_no('are you sure you know what you are doing?')
- elif args.command == 'local':
- if which('behave') is None:
- parser.error_no_help('"behave" not found in PATH, needed when running local mode')
-
- if args.testdata_variable is not None:
- for variable in args.testdata_variable:
- try:
- [name, value] = variable.split('=', 1)
- os.environ[f'TESTDATA_VARIABLE_{name}'] = value
- except ValueError:
- parser.error_no_help('-T/--testdata-variable needs to be in the format NAME=VALUE')
-
- if args.csv_prefix is None:
- if args.csv_interval is not None:
- parser.error_no_help('--csv-interval can only be used in combination with --csv-prefix')
-
- if args.csv_flush_interval is not None:
- parser.error_no_help('--csv-flush-interval can only be used in combination with --csv-prefix')
+ _parse_run(parser, args)
elif args.command == 'dist' and args.subcommand == 'build':
- setattr(args, 'force_build', args.no_cache)
- setattr(args, 'build', not args.no_cache)
+ args.force_build = args.no_cache
+ args.build = not args.no_cache
log_file = getattr(args, 'log_file', None)
setup_logging(log_file)
@@ -139,8 +147,8 @@ def _parse_arguments() -> argparse.Namespace:
def _inject_additional_arguments_from_metadata(args: argparse.Namespace) -> argparse.Namespace:
- with open(args.file) as fd:
- file_metadata = [line.strip().replace('# grizzly-cli ', '').split(' ') for line in fd.readlines() if line.strip().startswith('# grizzly-cli ')]
+ with Path(args.file).open() as fd:
+ file_metadata = [line.strip().replace('# grizzly-cli ', '').split(' ') for line in fd if line.strip().startswith('# grizzly-cli ')]
if len(file_metadata) < 1:
return args
@@ -149,12 +157,12 @@ def _inject_additional_arguments_from_metadata(args: argparse.Namespace) -> argp
for additional_arguments in file_metadata:
try:
if additional_arguments[0].strip().startswith('-'):
- raise ValueError()
+ raise ValueError
index = argv.index(additional_arguments[0]) + 1
for zindex, additional_argument in enumerate(additional_arguments[1:]):
argv.insert(index + zindex, additional_argument)
- except ValueError:
+ except ValueError: # noqa: PERF203
print('?? ignoring {}'.format(' '.join(additional_arguments)))
sys.argv = sys.argv[0:1] + argv
@@ -182,18 +190,16 @@ def main() -> int:
elif args.command == 'keyvault':
rc = keyvault(args)
else:
- raise ValueError(f'unknown command {args.command}')
-
- return rc
+ message = f'unknown command {args.command}'
+ raise ValueError(message)
except (KeyboardInterrupt, ValueError) as e:
- print('')
+ print()
if isinstance(e, ValueError):
- if args is not None and getattr(args, 'verbose', False):
- exception = format_exc()
- else:
- exception = str(e)
+ exception = format_exc() if args is not None and getattr(args, 'verbose', False) else str(e)
print(exception)
print('\n!! aborted grizzly-cli')
return 1
+ else:
+ return rc
diff --git a/grizzly_cli/argparse/__init__.py b/grizzly_cli/argparse/__init__.py
index 11e238a..858ec2c 100644
--- a/grizzly_cli/argparse/__init__.py
+++ b/grizzly_cli/argparse/__init__.py
@@ -1,18 +1,26 @@
-import sys
+from __future__ import annotations
+
import re
+import sys
+from argparse import ArgumentParser as CoreArgumentParser
+from argparse import Namespace, _SubParsersAction
+from typing import TYPE_CHECKING, Any, Optional, cast
-from typing import Any, Optional, IO, Sequence, cast
-from argparse import ArgumentParser as CoreArgumentParser, Namespace, _SubParsersAction
+from grizzly_cli.argparse.bashcompletion import BashCompletionAction
+from grizzly_cli.argparse.bashcompletion import hook as bashcompletion_hook
+from grizzly_cli.argparse.markdown import MarkdownFormatter, MarkdownHelpAction
-from .markdown import MarkdownFormatter, MarkdownHelpAction
-from .bashcompletion import BashCompletionAction, hook as bashcompletion_hook
+if TYPE_CHECKING: # pragma: no cover
+ from collections.abc import Sequence
+ from typing import IO
+ from _typeshed import SupportsWrite
ArgumentSubParser = _SubParsersAction
class ArgumentParser(CoreArgumentParser):
- def __init__(self, markdown_help: bool = False, bash_completion: bool = False, *args: Any, **kwargs: Any) -> None:
+ def __init__(self, *args: Any, markdown_help: bool = False, bash_completion: bool = False, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.markdown_help = markdown_help
@@ -27,17 +35,17 @@ def __init__(self, markdown_help: bool = False, bash_completion: bool = False, *
self._optionals.title = 'optional arguments'
def error_no_help(self, message: str) -> None:
- sys.stderr.write('{}: error: {}\n'.format(self.prog, message))
+ sys.stderr.write(f'{self.prog}: error: {message}\n')
sys.exit(2)
- def print_help(self, file: Optional[IO[str]] = None) -> None:
- '''Hook to make help more command line friendly, if there is markdown markers in the text.
- '''
+ def print_help(self, file: Optional[SupportsWrite[str]] = None) -> None:
+ """Make help more command line friendly, if there is markdown markers in the text."""
+ file = cast('Optional[IO[str]]', file)
if not self.markdown_help:
super().print_help(file)
return
- if cast(type, self.formatter_class) is not MarkdownFormatter:
+ if self.formatter_class is not MarkdownFormatter:
original_description = self.description
original_actions = self._actions
@@ -53,14 +61,12 @@ def print_help(self, file: Optional[IO[str]] = None) -> None:
super().print_help(file)
- if cast(type, self.formatter_class) is not MarkdownFormatter:
+ if self.formatter_class is not MarkdownFormatter:
self.description = original_description
self._actions = original_actions
- def parse_args(self, args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None) -> Namespace: # type: ignore
- """
- Hook to add `--bash-complete` to all parsers, if enabled for parser.
- """
+ def parse_args(self, args: Optional[Sequence[str]] = None, namespace: Optional[Namespace] = None) -> Namespace: # type: ignore[override]
+ """Add `--bash-complete` to all parsers, if enabled for parser."""
if self.bash_completion:
bashcompletion_hook(self)
diff --git a/grizzly_cli/argparse/bashcompletion/__init__.py b/grizzly_cli/argparse/bashcompletion/__init__.py
index 9a1f786..c816203 100644
--- a/grizzly_cli/argparse/bashcompletion/__init__.py
+++ b/grizzly_cli/argparse/bashcompletion/__init__.py
@@ -1,25 +1,28 @@
-import sys
+from __future__ import annotations
-from typing import Any, Dict, List, Union, Sequence, Optional, cast
+import sys
from argparse import (
- ArgumentParser,
- ArgumentError,
+ SUPPRESS,
Action,
+ ArgumentError,
+ ArgumentParser,
Namespace,
- _SubParsersAction,
- _StoreConstAction,
_AppendAction,
_StoreAction,
- SUPPRESS,
+ _StoreConstAction,
+ _SubParsersAction,
)
+from collections.abc import Sequence
from os import path
+from pathlib import Path
+from typing import Any, Optional, Union, cast
-from .types import BashCompletionTypes
+from grizzly_cli.argparse.bashcompletion.types import BashCompletionTypes
__all__ = [
- 'BashCompletionTypes',
- 'BashCompletionAction',
'BashCompleteAction',
+ 'BashCompletionAction',
+ 'BashCompletionTypes',
'hook',
]
@@ -27,17 +30,17 @@
class BashCompletionAction(Action):
def __init__(
self,
- option_strings: List[str],
+ option_strings: list[str],
dest: str = SUPPRESS,
default: str = SUPPRESS,
- help: str = SUPPRESS,
+ help_text: str = SUPPRESS,
**kwargs: Any,
) -> None:
super().__init__(
option_strings=option_strings,
dest=dest,
default=default,
- help=help,
+ help=help_text,
nargs=0,
**kwargs,
)
@@ -45,12 +48,13 @@ def __init__(
def __call__(
self,
parser: ArgumentParser,
- namespace: Namespace,
- values: Union[str, Sequence[Any], None],
- option_string: Optional[str] = None,
+ *_args: Any,
+ **_kwargs: Any,
) -> None:
- file_directory = path.dirname(__file__)
- with open(path.join(file_directory, 'bashcompletion.bash'), encoding='utf-8') as fd:
+ current_file = Path(__file__)
+ file_directory = current_file.parent
+ bash_script = file_directory / 'bashcompletion.bash'
+ with bash_script.open(encoding='utf-8') as fd:
print(fd.read().replace('bashcompletion_template', parser.prog))
parser.exit()
@@ -59,56 +63,51 @@ def __call__(
class BashCompleteAction(Action):
def __init__(
self,
- option_strings: List[str],
+ option_strings: list[str],
dest: str = SUPPRESS,
default: str = SUPPRESS,
- help: str = SUPPRESS,
+ help_text: str = SUPPRESS,
**kwargs: Any,
) -> None:
super().__init__(
option_strings=option_strings,
dest=dest,
default=default,
- help=help,
+ help=help_text,
nargs=None,
**kwargs,
)
- def get_suggestions(self, parser: ArgumentParser) -> Dict[str, Union[str, Action]]:
- suggestions: Dict[str, Union[str, Action]] = {}
+ def get_suggestions(self, parser: ArgumentParser) -> dict[str, Union[str, Action]]:
+ suggestions: dict[str, Union[str, Action]] = {}
for action in parser._actions:
- if isinstance(action, (BashCompleteAction, BashCompletionAction, )) or (SUPPRESS in [action.help, action.default] and action.dest != 'help'):
+ if isinstance(action, (BashCompleteAction, BashCompletionAction)) or (SUPPRESS in [action.help, action.default] and action.dest != 'help'):
continue
- elif isinstance(action, _SubParsersAction):
- suggestions.update({key: action for key in action.choices.keys()})
+
+ if isinstance(action, _SubParsersAction):
+ suggestions.update(dict.fromkeys(action.choices.keys(), action))
+ elif len(action.option_strings) > 0:
+ suggestions.update(dict.fromkeys(action.option_strings, action))
else:
- if len(action.option_strings) > 0:
- suggestions.update({key: action for key in action.option_strings})
- else:
- suggestions.update({action.dest: action})
+ suggestions.update({action.dest: action})
return suggestions
- def get_exclusive_suggestions(self, parser: ArgumentParser) -> Dict[str, List[str]]:
- exclusive_suggestions: Dict[str, List[str]] = {}
+ def get_exclusive_suggestions(self, parser: ArgumentParser) -> dict[str, list[str]]:
+ exclusive_suggestions: dict[str, list[str]] = {}
for group in parser._mutually_exclusive_groups:
- exclusives: List[str] = []
+ exclusives: list[str] = []
for action in group._group_actions:
- for option in action.option_strings:
- exclusives.append(option)
+ exclusives.extend(action.option_strings)
for exclusive in exclusives:
- exclusives_to: List[str] = []
- for exclusive_to in filter(lambda x: x != exclusive, exclusives):
- exclusives_to.append(exclusive_to)
-
- exclusive_suggestions.update({exclusive: exclusives_to})
+ exclusive_suggestions.update({exclusive: list(filter(lambda x: x != exclusive, exclusives))})
return exclusive_suggestions
- def get_provided_options(self, prog: str, values: Union[str, Sequence[Any], None]) -> List[str]:
- options: List[str] = []
+ def get_provided_options(self, prog: str, values: Union[str, Sequence[Any], None]) -> list[str]:
+ options: list[str] = []
if isinstance(values, str):
options = [value for value in values.replace(f'{prog}', '').split(' ') if len(value.strip()) > 0]
@@ -117,11 +116,11 @@ def get_provided_options(self, prog: str, values: Union[str, Sequence[Any], None
return options
- def remove_completed(self, provided_options: List[str], suggestions: Dict[str, Union[str, Action]], exclusive_suggestions: Dict[str, List[str]]) -> List[str]:
+ def remove_completed(self, provided_options: list[str], suggestions: dict[str, Union[str, Action]], exclusive_suggestions: dict[str, list[str]]) -> list[str]: # noqa: C901, PLR0912
if len(provided_options) <= 1:
return provided_options
- filtered_options: List[str] = []
+ filtered_options: list[str] = []
skip: bool = False
concat: bool = False
@@ -131,13 +130,13 @@ def remove_completed(self, provided_options: List[str], suggestions: Dict[str, U
if concat:
concat = False
- option = '{} {}'.format(provided_options[index - 1], option)
+ option = f'{provided_options[index - 1]} {option}' # noqa: PLW2901
if len(option) < 1 or skip:
skip = False
continue
- suggestion = suggestions.get(option, None)
+ suggestion = suggestions.get(option)
if suggestion is not None:
if isinstance(suggestion, _AppendAction):
@@ -159,29 +158,26 @@ def remove_completed(self, provided_options: List[str], suggestions: Dict[str, U
if remove_suggestion and isinstance(suggestion, Action):
# remove all other, completed, options from suggestion
for suggestion_option in suggestion.option_strings:
- if suggestion_option in suggestions:
- del suggestions[suggestion_option]
+ suggestions.pop(suggestion_option, None)
# remove options that are mutually exclusive to completed option
exclusive_removes = exclusive_suggestions.get(suggestion_option, [])
for exclusive_option in exclusive_removes:
- if exclusive_option in suggestions:
- del suggestions[exclusive_option]
+ suggestions.pop(exclusive_option, None)
continue
- elif not any([suggested_option.startswith(option) for suggested_option in suggestions.keys()]): # could be values for an option
+ elif not any(suggested_option.startswith(option) for suggested_option in suggestions): # could be values for an option
remove = True
if option.endswith('\\') and sys.platform != 'win32':
concat = True
continue
for suggestion in suggestions.values():
- if isinstance(suggestion, Action) and len(suggestion.option_strings) == 0:
- if isinstance(suggestion.type, BashCompletionTypes.File):
- file_suggestions = cast(BashCompletionTypes.File, suggestion.type).list_files(option) # type: ignore
- for file in file_suggestions.keys():
- if file.startswith(option):
- remove = False
- break
+ if isinstance(suggestion, Action) and len(suggestion.option_strings) == 0 and isinstance(suggestion.type, BashCompletionTypes.File):
+ file_suggestions = suggestion.type.list_files(option)
+ for file in file_suggestions:
+ if file.startswith(option):
+ remove = False
+ break
if remove:
continue
@@ -193,11 +189,11 @@ def remove_completed(self, provided_options: List[str], suggestions: Dict[str, U
return filtered_options
- def filter_suggestions(self, provided_options: List[str], suggestions: Dict[str, Union[str, Action]]) -> Dict[str, Union[str, Action]]:
+ def filter_suggestions(self, provided_options: list[str], suggestions: dict[str, Union[str, Action]]) -> dict[str, Union[str, Action]]:
if len(provided_options) < 1:
return suggestions
- filtered_suggestions: Dict[str, Union[str, Action]] = {}
+ filtered_suggestions: dict[str, Union[str, Action]] = {}
for option in provided_options:
for option_suggestion, suggestion in suggestions.items():
if option_suggestion.startswith(option) or (
@@ -210,19 +206,19 @@ def filter_suggestions(self, provided_options: List[str], suggestions: Dict[str,
return filtered_suggestions
- def __call__(
+ def __call__( # noqa: C901, PLR0912, PLR0915
self,
parser: ArgumentParser,
- namespace: Namespace,
+ namespace: Namespace, # noqa: ARG002
values: Union[str, Sequence[Any], None],
- option_string: Optional[str] = None,
+ option_string: Optional[str] = None, # noqa: ARG002
) -> None:
- all_suggestions: Dict[str, Union[str, Action]] = {}
+ all_suggestions: dict[str, Union[str, Action]] = {}
provided_options = self.get_provided_options(parser.prog, values)
suggestions = self.get_suggestions(parser)
if '-h' in provided_options or '--help' in provided_options:
- print('')
+ print()
parser.exit()
# map options that are only allowed by it own
@@ -248,32 +244,25 @@ def __call__(
suggestions = all_suggestions
for option in suggestion.option_strings:
del suggestions[option]
- elif isinstance(suggestion, (_AppendAction, _StoreAction,)):
+ elif isinstance(suggestion, (_AppendAction, _StoreAction)):
# value for append action has been provided
- if len(provided_options) == 2:
- suggestions = all_suggestions
- else:
- # no value, supplied, do not suggest anything
- suggestions = {}
+ suggestions = all_suggestions if len(provided_options) == 2 else {}
# based on option value type
suggestion = all_suggestions.get(provided_options[0], None)
if isinstance(suggestion, Action) and suggestion.type is not None:
value = provided_options[-1] if len(provided_options) > 1 else None
if isinstance(suggestion.type, BashCompletionTypes.File):
- file_suggestions = cast(BashCompletionTypes.File, suggestion.type).list_files(value) # type: ignore
+ file_suggestions = suggestion.type.list_files(value)
if not (len(file_suggestions) == 1 and provided_options[-1] in file_suggestions):
- suggestions = cast(Dict[str, Union[str, Action]], file_suggestions)
+ suggestions = cast('dict[str, Union[str, Action]]', file_suggestions)
else:
suggestions = all_suggestions
for option in suggestion.option_strings:
del suggestions[option]
- else:
- if suggestion.type == str and not isinstance(value, str):
- suggestions = {}
- elif suggestion.type == int and (value is None or not value.isnumeric()):
- suggestions = {}
+ elif (suggestion.type is str and not isinstance(value, str)) or (suggestion.type is int and (value is None or not value.isnumeric())):
+ suggestions = {}
if value is not None and isinstance(suggestion, _StoreAction):
for option in suggestion.option_strings:
@@ -283,31 +272,30 @@ def __call__(
# check for positionals
original_suggestions = suggestions.copy()
for option, suggestion in original_suggestions.items():
- if (option.startswith('-') and isinstance(suggestion, Action)) or (not option.startswith('-') and isinstance(suggestion, (_SubParsersAction, str,))):
+ if (option.startswith('-') and isinstance(suggestion, Action)) or (not option.startswith('-') and isinstance(suggestion, (_SubParsersAction, str))):
continue
del suggestions[option]
- if isinstance(suggestion, Action) and suggestion.type is not None:
- if isinstance(cast(Any, suggestion.type), BashCompletionTypes.File):
- value = provided_options[-1] if len(provided_options) == 1 and not provided_options[-1].startswith('-') else None
-
- if (value is None and len(provided_options) == 0) or (value is not None and len(provided_options) == 1):
- file_suggestions = cast(BashCompletionTypes.File, suggestion.type).list_files(value)
- value_type = file_suggestions.get(value, None) if value is not None else None
-
- # check if suggestion matching provided option (value) is a directory, and if
- # provded option (value) does not end with a path separator, it should be added
- # otherwise it will not be completed correctly
- if value_type == 'dir' and (value is not None and not value.endswith(path.sep)):
- value = '{value}{sep}'.format(value=value, sep=path.sep)
-
- # only provide further suggestions if matches isn't a completed file path
- if not (len(file_suggestions) == 1 and value in file_suggestions) and (value_type is None or value_type != 'file'):
- suggestions.update(file_suggestions)
- else:
- suggestions = all_suggestions
- del suggestions[suggestion.dest]
+ if isinstance(suggestion, Action) and suggestion.type is not None and isinstance(suggestion.type, BashCompletionTypes.File):
+ value = provided_options[-1] if len(provided_options) == 1 and not provided_options[-1].startswith('-') else None
+
+ if (value is None and len(provided_options) == 0) or (value is not None and len(provided_options) == 1):
+ file_suggestions = suggestion.type.list_files(value)
+ value_type = file_suggestions.get(value, None) if value is not None else None
+
+ # check if suggestion matching provided option (value) is a directory, and if
+ # provded option (value) does not end with a path separator, it should be added
+ # otherwise it will not be completed correctly
+ if value_type == 'dir' and (value is not None and not value.endswith(path.sep)):
+ value = f'{value}{path.sep}'
+
+ # only provide further suggestions if matches isn't a completed file path
+ if not (len(file_suggestions) == 1 and value in file_suggestions) and (value_type is None or value_type != 'file'):
+ suggestions.update(file_suggestions)
+ else:
+ suggestions = all_suggestions
+ del suggestions[suggestion.dest]
print('\n'.join(suggestions.keys()))
parser.exit()
@@ -319,7 +307,7 @@ def hook(parser: ArgumentParser) -> None:
except ArgumentError as e:
# we've already "hooked" the parser
if 'conflicting option string: --bash-complete' not in e.message:
- raise e
+ raise
except Exception:
raise
finally:
diff --git a/grizzly_cli/argparse/bashcompletion/types.py b/grizzly_cli/argparse/bashcompletion/types.py
index ffe4259..4a06e21 100644
--- a/grizzly_cli/argparse/bashcompletion/types.py
+++ b/grizzly_cli/argparse/bashcompletion/types.py
@@ -1,18 +1,17 @@
-import sys
+from __future__ import annotations
-from typing import Dict, Optional
-from glob import glob
-from os import getcwd
-from os.path import sep as path_separator, exists, isfile
-from fnmatch import filter as fnmatch_filter
+import sys
from argparse import ArgumentTypeError
-
+from fnmatch import filter as fnmatch_filter
+from os.path import sep as path_sep
+from pathlib import Path
+from typing import Optional, cast
__all__ = [
'BashCompletionTypes',
]
-ESCAPE_CHARACTERS = {
+ESCAPE_CHARACTERS: dict[str, str | int | None] = {
' ': '\\ ',
'(': '\\(',
')': '\\)',
@@ -21,57 +20,84 @@
class BashCompletionTypes:
class File:
- _cwd: str = getcwd()
-
def __init__(self, *args: str, missing_ok: bool = False) -> None:
self.patterns = list(args)
- self.cwd = BashCompletionTypes.File._cwd
+ self.cwd = Path.cwd()
self.missing_ok = missing_ok
def __call__(self, value: str) -> str:
if self.missing_ok:
return value
- if not exists(value):
- raise ArgumentTypeError(f'{value} does not exist')
+ file = Path(value)
- if not isfile(value):
- raise ArgumentTypeError(f'{value} is not a file')
+ if not file.exists():
+ message = f'{value} does not exist'
+ raise ArgumentTypeError(message)
+
+ if not file.is_file():
+ message = f'{value} is not a file'
+ raise ArgumentTypeError(message)
matches = [match for pattern in self.patterns for match in fnmatch_filter([value], pattern)]
if len(matches) < 1:
- raise ArgumentTypeError(f'{value} does not match {", ".join(self.patterns)}')
+ message = f'{value} does not match {", ".join(self.patterns)}'
+ raise ArgumentTypeError(message)
+
+ return value
+
+ @classmethod
+ def _transform_path(cls, value: str) -> str:
+ value = value.translate(str.maketrans(ESCAPE_CHARACTERS))
+ if sys.platform == 'win32':
+ value = value.replace('/', path_sep)
return value
- def list_files(self, value: Optional[str]) -> Dict[str, str]:
- matches: Dict[str, str] = {}
+ def list_files(self, value: Optional[str]) -> dict[str, str]:
+ matches: dict[str, str] = {}
if value is not None:
- if value.endswith('\\') and sys.platform != 'win32':
- value += ' '
- value = value.replace('\\ ', ' ').replace('\\(', '(').replace('\\)', ')')
+ if sys.platform == 'win32':
+ value = value.replace(path_sep, '/') # posix style
+
+ for chr_with, chr_replace in ESCAPE_CHARACTERS.items():
+ value = value.replace(cast('str', chr_replace), chr_with)
for pattern in self.patterns:
- for path in glob('**/{pattern}'.format(pattern=pattern), recursive=True):
- path_match = path.replace('{cwd}{path_separator}'.format(cwd=self.cwd, path_separator=path_separator), '')
+ for path in self.cwd.rglob(f'**/{pattern}'):
+ try:
+ path_match = path.relative_to(self.cwd)
+ except ValueError:
+ path_match = path
+
+ path_match_value = path_match.as_posix()
- if path_match.startswith('.') or (value is not None and not path_match.startswith(value)):
+ # skip any paths where any part is hidden, or any path that is not (partially) relative to provided value
+ if any(part.startswith('.') for part in path_match.parts) or (value is not None and not path_match_value.startswith(value)):
continue
- match: Optional[Dict[str, str]] = None
+ match: Optional[dict[str, str]] = None
- if path_separator in path_match:
+ # all paths are treated in posix style
+ if '/' in path_match_value: # there is a directory in the match
try:
+ """
+ find first part that matches with provided value;
+ value = `hel`
+ path_match_value = `hello/example.txt`
+ should be `hello`, and a dir(ectory)
+ """
index_match = len(value or '')
- index_sep = path_match[index_match:].index(path_separator) + index_match
- match = {path_match[:index_sep].translate(str.maketrans(ESCAPE_CHARACTERS)): 'dir'} # type: ignore
+ index_sep = path_match_value.index('/', index_match)
+ match = {self._transform_path(path_match_value[:index_sep]): 'dir'}
except ValueError:
+ # no match against provided value, so assume file
pass
if match is None:
- match = {path_match.translate(str.maketrans(ESCAPE_CHARACTERS)): 'file'} # type: ignore
+ match = {self._transform_path(path_match_value): 'file'}
matches.update(match)
diff --git a/grizzly_cli/argparse/markdown.py b/grizzly_cli/argparse/markdown.py
index 4273555..5fd8115 100644
--- a/grizzly_cli/argparse/markdown.py
+++ b/grizzly_cli/argparse/markdown.py
@@ -1,26 +1,29 @@
from __future__ import annotations
-from typing import TYPE_CHECKING, Any, List, Union, Sequence, Optional, Iterable, Tuple, Callable, Type, cast
-from types import MethodType
-from argparse import Action, SUPPRESS, ArgumentParser, Namespace, HelpFormatter
+
+from argparse import SUPPRESS, Action, ArgumentParser, HelpFormatter, Namespace
from textwrap import fill as textwrap_fill
+from types import MethodType
+from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast
if TYPE_CHECKING: # pragma: no cover
+ from collections.abc import Iterable, Sequence
+
from typing_extensions import Self
__all__ = [
- 'MarkdownHelpAction',
'MarkdownFormatter',
+ 'MarkdownHelpAction',
]
class MarkdownHelpAction(Action):
def __init__(
self,
- option_strings: List[str],
+ option_strings: list[str],
dest: str = SUPPRESS,
default: str = SUPPRESS,
- help: str = SUPPRESS,
+ help: str = SUPPRESS, # noqa: A002
**kwargs: Any,
) -> None:
super().__init__(
@@ -35,9 +38,9 @@ def __init__(
def __call__(
self,
parser: ArgumentParser,
- namespace: Namespace,
- values: Union[str, Sequence[Any], None],
- option_string: Optional[str] = None,
+ namespace: Namespace, # noqa: ARG002
+ values: Union[str, Sequence[Any], None], # noqa: ARG002
+ option_string: Optional[str] = None, # noqa: ARG002
) -> None:
self.print_help(parser)
@@ -53,10 +56,7 @@ def format_help_markdown(self: ArgumentParser) -> str:
# usage
formatter.add_text('\n')
- formatter.add_usage(self.usage, self._actions,
- self._mutually_exclusive_groups)
-
- # XXX: formatter.add_text(self.description) -- used to be here
+ formatter.add_usage(self.usage, self._actions, self._mutually_exclusive_groups)
# positionals, optionals and user-defined groups
for action_group in self._action_groups:
@@ -73,9 +73,9 @@ def format_help_markdown(self: ArgumentParser) -> str:
#
parser.print_help()
@@ -98,22 +98,22 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self._current_section = self._root_section
@staticmethod
- def factory(level: int) -> Type['MarkdownFormatter']:
+ def factory(level: int) -> type[MarkdownFormatter]:
return type('MarkdownFormatterInstance', (MarkdownFormatter,), {'level': level})
class _MarkdownSection(HelpFormatter._Section):
- def __init__(self, formatter: 'MarkdownFormatter', parent: Optional[Self], heading: Optional[str] = None) -> None:
+ def __init__(self, formatter: MarkdownFormatter, parent: Optional[Self], heading: Optional[str] = None) -> None:
self.formatter = formatter
self.parent = parent
self.heading = heading
- self.items: List[Tuple[Callable[..., str], Iterable[Any]]] = []
+ self.items: list[tuple[Callable[..., str], Iterable[Any]]] = []
def format_help(self) -> str:
# format the indented section
if self.parent is not None:
self.formatter._indent()
join = self.formatter._join_parts
- helps: List[str] = []
+ helps: list[str] = []
# only one table header per section
print_table_headers = True
@@ -122,16 +122,15 @@ def format_help(self) -> str:
name = getattr(func, '__name__', repr(func))
# we need to fix headers for argument tables
- if name == '_format_action':
- if print_table_headers and len(item_help_text) > 0:
- helps.extend([
- '\n',
- '| argument | default | help |',
- '\n',
- '| -------- | ------- | ---- |',
- '\n',
- ])
- print_table_headers = False
+ if name == '_format_action' and print_table_headers and len(item_help_text) > 0:
+ helps.extend([
+ '\n',
+ '| argument | default | help |',
+ '\n',
+ '| -------- | ------- | ---- |',
+ '\n',
+ ])
+ print_table_headers = False
helps.append(item_help_text)
@@ -147,14 +146,14 @@ def format_help(self) -> str:
# add the heading if the section was non-empty
if self.heading is not SUPPRESS and self.heading is not None:
current_indent = self.formatter._current_indent
- heading = '%*s%s\n' % (current_indent, '', self.heading)
+ heading = '%*s%s\n' % (current_indent, '', self.heading) # noqa: UP031
# increase header if we're in a subparser
assert isinstance(self.formatter, MarkdownFormatter)
if self.formatter.level > 0:
# a bit hackish, to get a line break when adding a subparsers help
if self.parent is None:
- print('')
+ print()
heading = f'#{heading}'
else:
@@ -191,13 +190,13 @@ def format_help(self) -> str:
def _format_text(self, text: str) -> str:
if '%(prog)' in text:
- text = text % dict(prog=self._prog)
+ text = text % {'prog': self._prog}
if len(text.strip()) > 0:
- lines: List[str] = []
+ lines: list[str] = []
for line in text.split('\n'):
- line = textwrap_fill(line, 120)
- lines.append(line)
+ filled_line = textwrap_fill(line, 120)
+ lines.append(filled_line)
text = '\n'.join(lines)
return text
@@ -208,7 +207,7 @@ def start_section(self, heading: Optional[str]) -> None:
heading = f'{"#" * self.current_level}# {heading}'
self._indent()
- section = self._MarkdownSection(self, cast(MarkdownFormatter._MarkdownSection, self._current_section), heading)
+ section = self._MarkdownSection(self, cast('MarkdownFormatter._MarkdownSection', self._current_section), heading)
self._add_item(section.format_help, [])
self._current_section = section
@@ -218,7 +217,7 @@ def _format_action(self, action: Action) -> str:
if 'help' in action.dest or action.dest == SUPPRESS:
return ''
- lines: List[str] = []
+ lines: list[str] = []
if action.help is not None:
expanded_help = self._expand_help(action)
@@ -228,9 +227,9 @@ def _format_action(self, action: Action) -> str:
argument = ', '.join(action.option_strings) if getattr(action, 'option_strings', None) is not None and len(action.option_strings) > 0 else action.dest
default = f'`{action.default}`' if action.default is not None else ''
- help = '
'.join(help_text)
+ help_value = '
'.join(help_text)
# format arguments as a markdown table row
- lines.extend([f'| `{argument}` | {default} | {help} |', ''])
+ lines.extend([f'| `{argument}` | {default} | {help_value} |', ''])
return '\n'.join(lines)
diff --git a/grizzly_cli/auth.py b/grizzly_cli/auth.py
index 1e47e07..66cbb01 100644
--- a/grizzly_cli/auth.py
+++ b/grizzly_cli/auth.py
@@ -1,14 +1,18 @@
-import sys
+from __future__ import annotations
+import sys
from os import environ
-from argparse import Namespace as Arguments
-from typing import Optional
from pathlib import Path
+from typing import TYPE_CHECKING, Optional
from pyotp import TOTP
-from . import register_parser
-from .argparse import ArgumentSubParser
+from grizzly_cli import register_parser
+
+if TYPE_CHECKING: # pragma: no cover
+ from argparse import Namespace as Arguments
+
+ from grizzly_cli.argparse import ArgumentSubParser
@register_parser()
@@ -27,7 +31,7 @@ def create_parser(sub_parser: ArgumentSubParser) -> None:
help=(
'where to read OTP secret, nothing specified means environment variable OTP_SECRET, '
'`-` means stdin and anything else is considered a file'
- )
+ ),
)
if auth_parser.prog != 'grizzly-cli auth': # pragma: no cover
@@ -40,31 +44,36 @@ def auth(args: Arguments) -> int:
if args.input is None:
secret = environ.get('OTP_SECRET', None)
if secret is None:
- raise ValueError('environment variable OTP_SECRET is not set')
+ message = 'environment variable OTP_SECRET is not set'
+ raise ValueError(message)
elif args.input == '-':
try:
secret = sys.stdin.read().strip()
- except:
+ except: # noqa: S110
pass
finally:
if secret is None or len(secret.strip()) < 1:
- raise ValueError('OTP secret could not be read from stdin')
+ message = 'OTP secret could not be read from stdin'
+ raise ValueError(message)
else:
input_file = Path(args.input)
if not input_file.exists():
- raise ValueError(f'file {input_file} does not exist')
+ message = f'file {input_file.as_posix()} does not exist'
+ raise ValueError(message)
secret = input_file.read_text().strip()
if ' ' in secret or len(secret.split('\n')) > 1 or secret == '':
- raise ValueError(f'file {input_file} does not seem to contain a single line with a valid OTP secret')
+ message = f'file {input_file.as_posix()} does not seem to contain a single line with a valid OTP secret'
+ raise ValueError(message)
try:
totp = TOTP(secret)
print(totp.now())
except Exception as e:
- raise ValueError(f'unable to generate TOTP code: {e}')
+ message = f'unable to generate TOTP code: {e!s}'
+ raise ValueError(message) from e
return 0
diff --git a/grizzly_cli/distributed/__init__.py b/grizzly_cli/distributed/__init__.py
index 8824f77..be4227e 100644
--- a/grizzly_cli/distributed/__init__.py
+++ b/grizzly_cli/distributed/__init__.py
@@ -1,28 +1,34 @@
-import os
-import sys
+from __future__ import annotations
+
import argparse
+import os
import subprocess
-
-from typing import List, Dict, Any, cast
-from tempfile import NamedTemporaryFile
-from getpass import getuser
-from shutil import get_terminal_size
+import sys
from argparse import Namespace as Arguments
-from socket import gethostname
-from json import loads as jsonloads
+from getpass import getuser
from io import StringIO
+from json import loads as jsonloads
from pathlib import Path
-
-from grizzly_cli import EXECUTION_CONTEXT, STATIC_CONTEXT, MOUNT_CONTEXT, PROJECT_NAME, register_parser
+from shutil import get_terminal_size
+from socket import gethostname
+from tempfile import NamedTemporaryFile
+from typing import IO, TYPE_CHECKING
+
+from grizzly_cli import EXECUTION_CONTEXT, MOUNT_CONTEXT, PROJECT_NAME, STATIC_CONTEXT, register_parser
+from grizzly_cli.distributed.build import build as do_build
+from grizzly_cli.distributed.build import create_parser as build_create_parser
+from grizzly_cli.distributed.clean import clean as do_clean
+from grizzly_cli.distributed.clean import create_parser as clean_create_parser
+from grizzly_cli.run import create_parser as run_create_parser
+from grizzly_cli.run import run
from grizzly_cli.utils import (
- run_command,
get_default_mtu,
list_images,
+ run_command,
)
-from grizzly_cli.run import create_parser as run_create_parser, run
-from grizzly_cli.argparse import ArgumentSubParser
-from .build import build as do_build, create_parser as build_create_parser
-from .clean import clean as do_clean, create_parser as clean_create_parser
+
+if TYPE_CHECKING: # pragma: no cover
+ from grizzly_cli.argparse import ArgumentSubParser
@register_parser(order=3)
@@ -101,7 +107,7 @@ def create_parser(sub_parser: ArgumentSubParser) -> None:
help=(
'sets enviroment variable LOCUST_WAIT_FOR_WORKERS_REPORT_AFTER_RAMP_UP, which tells master to wait '
'this amount of time for worker report'
- )
+ ),
)
dist_parser.add_argument(
@@ -144,29 +150,17 @@ def create_parser(sub_parser: ArgumentSubParser) -> None:
def distributed(args: Arguments) -> int:
if args.subcommand == 'run':
return run(args, distributed_run)
- elif args.subcommand == 'build':
+
+ if args.subcommand == 'build':
return do_build(args)
- elif args.subcommand == 'clean':
+ if args.subcommand == 'clean':
return do_clean(args)
- else:
- raise ValueError(f'unknown subcommand {args.subcommand}')
+ message = f'unknown subcommand {args.subcommand}'
+ raise ValueError(message)
-def distributed_run(args: Arguments, environ: Dict[str, Any], run_arguments: Dict[str, List[str]]) -> int:
- suffix = '' if args.id is None else f'-{args.id}'
- tag = getuser()
-
- if args.project_name is None:
- project_name = PROJECT_NAME
- else:
- project_name = args.project_name
-
- # default locust project
- compose_args: List[str] = [
- '-p', f'{project_name}{suffix}-{tag}',
- '-f', f'{STATIC_CONTEXT}/compose.yaml',
- ]
+def update_os_environ(args: Arguments, run_arguments: dict[str, list[str]], project_name: str, tag: str) -> None:
if args.file is not None:
os.environ['GRIZZLY_RUN_FILE'] = args.file
@@ -182,26 +176,26 @@ def distributed_run(args: Arguments, environ: Dict[str, Any], run_arguments: Dic
columns, lines = get_terminal_size()
# set environment variables needed by compose files, when * compose executes
- os.environ['GRIZZLY_MTU'] = cast(str, mtu)
- os.environ['GRIZZLY_EXECUTION_CONTEXT'] = EXECUTION_CONTEXT
- os.environ['GRIZZLY_STATIC_CONTEXT'] = STATIC_CONTEXT
- os.environ['GRIZZLY_MOUNT_CONTEXT'] = MOUNT_CONTEXT
- os.environ['GRIZZLY_PROJECT_NAME'] = project_name
- os.environ['GRIZZLY_USER_TAG'] = tag
- os.environ['GRIZZLY_EXPECTED_WORKERS'] = str(args.workers)
- os.environ['GRIZZLY_LIMIT_NOFILE'] = str(args.limit_nofile)
- os.environ['GRIZZLY_HEALTH_CHECK_RETRIES'] = str(args.health_retries)
- os.environ['GRIZZLY_HEALTH_CHECK_INTERVAL'] = str(args.health_interval)
- os.environ['GRIZZLY_HEALTH_CHECK_TIMEOUT'] = str(args.health_timeout)
- os.environ['GRIZZLY_IMAGE_REGISTRY'] = getattr(args, 'registry', None) or ''
- os.environ['GRIZZLY_CONTAINER_TTY'] = 'true' if args.tty else 'false'
- os.environ['COLUMNS'] = str(columns)
- os.environ['LINES'] = str(lines)
+ os.environ.update({
+ 'GRIZZLY_MTU': str(mtu),
+ 'GRIZZLY_EXECUTION_CONTEXT': EXECUTION_CONTEXT,
+ 'GRIZZLY_STATIC_CONTEXT': STATIC_CONTEXT,
+ 'GRIZZLY_MOUNT_CONTEXT': MOUNT_CONTEXT,
+ 'GRIZZLY_PROJECT_NAME': project_name,
+ 'GRIZZLY_USER_TAG': tag,
+ 'GRIZZLY_EXPECTED_WORKERS': str(args.workers),
+ 'GRIZZLY_LIMIT_NOFILE': str(args.limit_nofile),
+ 'GRIZZLY_HEALTH_CHECK_RETRIES': str(args.health_retries),
+ 'GRIZZLY_HEALTH_CHECK_INTERVAL': str(args.health_interval),
+ 'GRIZZLY_HEALTH_CHECK_TIMEOUT': str(args.health_timeout),
+ 'GRIZZLY_IMAGE_REGISTRY': getattr(args, 'registry', None) or '',
+ 'GRIZZLY_CONTAINER_TTY': repr(args.tty).lower(),
+ 'COLUMNS': str(columns),
+ 'LINES': str(lines),
+ })
grizzly_mount_context_path = ''
- name_template = '{project}{suffix}-{tag}-{node}-{index}'
-
if EXECUTION_CONTEXT != MOUNT_CONTEXT:
hostname = gethostname()
output = subprocess.check_output(
@@ -227,53 +221,87 @@ def distributed_run(args: Arguments, environ: Dict[str, Any], run_arguments: Dic
if len(run_arguments.get('common', [])) > 0:
os.environ['GRIZZLY_COMMON_RUN_ARGS'] = ' '.join(run_arguments['common'])
- # check if we need to build image
- images = list_images(args)
- with NamedTemporaryFile() as fd:
- # file will be deleted when conContainertext exits
- if len(environ) > 0:
- for key, value in environ.items():
- if key == 'GRIZZLY_CONFIGURATION_FILE':
- value = value.replace(EXECUTION_CONTEXT, MOUNT_CONTEXT).replace(MOUNT_CONTEXT, '/srv/grizzly')
+def write_env_file(fd: IO[bytes], environ: dict, args: Arguments) -> None:
+ if len(environ) > 0:
+ for key, value in environ.items():
+ transformed_value = value.replace(EXECUTION_CONTEXT, MOUNT_CONTEXT).replace(MOUNT_CONTEXT, '/srv/grizzly') if key == 'GRIZZLY_CONFIGURATION_FILE' else value
- fd.write(f'{key}={value}\n'.encode('utf-8'))
+ fd.write(f'{key}={transformed_value}\n'.encode())
- fd.write(f'COLUMNS={columns}\n'.encode('utf-8'))
- fd.write(f'LINES={lines}\n'.encode('utf-8'))
- fd.write(f'GRIZZLY_CONTAINER_TTY={os.environ["GRIZZLY_CONTAINER_TTY"]}\n'.encode('utf-8'))
+ fd.write(f'COLUMNS={os.environ["COLUMNS"]}\n'.encode())
+ fd.write(f'LINES={os.environ["LINES"]}\n'.encode())
+ fd.write(f'GRIZZLY_CONTAINER_TTY={os.environ["GRIZZLY_CONTAINER_TTY"]}\n'.encode())
- if args.wait_for_worker is not None:
- fd.write(f'LOCUST_WAIT_FOR_WORKERS_REPORT_AFTER_RAMP_UP="{args.wait_for_worker}"'.encode('utf-8'))
+ if args.wait_for_worker is not None:
+ fd.write(f'LOCUST_WAIT_FOR_WORKERS_REPORT_AFTER_RAMP_UP="{args.wait_for_worker}"'.encode())
- fd.flush()
+ fd.flush()
- os.environ['GRIZZLY_ENVIRONMENT_FILE'] = fd.name
+ os.environ['GRIZZLY_ENVIRONMENT_FILE'] = fd.name
- validate_config = getattr(args, 'validate_config', False)
- compose_command = [
- args.container_system, 'compose',
- *compose_args,
- 'config',
- ]
+def should_validate_config(args: Arguments, compose_args: list[str]) -> int:
+ validate_config = getattr(args, 'validate_config', False)
+
+ compose_command = [
+ args.container_system, 'compose',
+ *compose_args,
+ 'config',
+ ]
+
+ result = run_command(compose_command, silent=not validate_config)
+
+ if validate_config or result.return_code != 0:
+ if result.return_code != 0 and not validate_config:
+ print('!! something in the compose project is not valid, check with:')
+ argv = sys.argv[:]
+ argv.insert(argv.index('dist') + 1, '--validate-config')
+ print(f'grizzly-cli {" ".join(argv[1:])}')
+
+ return result.return_code
+
+ return 0
- result = run_command(compose_command, silent=not validate_config)
- if validate_config or result.return_code != 0:
- if result.return_code != 0 and not validate_config:
- print('!! something in the compose project is not valid, check with:')
- argv = sys.argv[:]
- argv.insert(argv.index('dist') + 1, '--validate-config')
- print(f'grizzly-cli {" ".join(argv[1:])}')
+def should_build_image(args: Arguments, project_name: str, tag: str) -> int:
+ images = list_images(args)
+
+ if images.get(project_name, {}).get(tag, None) is None or args.force_build or args.build:
+ rc = do_build(args)
+ if rc != 0:
+ print(f'!! failed to build {project_name}, rc={rc}')
+ return rc
+
+ return 0
+
+
+def distributed_run(args: Arguments, environ: dict, run_arguments: dict[str, list[str]]) -> int:
+ suffix = '' if args.id is None else f'-{args.id}'
+ tag = getuser()
+
+ project_name = PROJECT_NAME if args.project_name is None else args.project_name
+
+ # default locust project
+ compose_args: list[str] = [
+ '-p', f'{project_name}{suffix}-{tag}',
+ '-f', f'{STATIC_CONTEXT}/compose.yaml',
+ ]
+
+ update_os_environ(args, run_arguments, project_name, tag)
+
+ name_template = '{project}{suffix}-{tag}-{node}-{index}'
+
+ with NamedTemporaryFile() as fd: # file will be deleted when container exists
+ write_env_file(fd, environ, args)
- return result.return_code
+ rc = should_validate_config(args, compose_args)
+ if rc != 0:
+ return rc
- if images.get(project_name, {}).get(tag, None) is None or args.force_build or args.build:
- rc = do_build(args)
- if rc != 0:
- print(f'!! failed to build {project_name}, rc={rc}')
- return rc
+ rc = should_build_image(args, project_name, tag)
+ if rc != 0:
+ return rc
compose_scale_argument = ['--scale', f'worker={args.workers}']
@@ -339,14 +367,14 @@ def distributed_run(args: Arguments, environ: Dict[str, Any], run_arguments: Dic
stderr=subprocess.STDOUT,
).split('\n')
- log_file = Path(args.log_file).open('a+') if args.log_file is not None else StringIO()
+ log_file = Path(args.log_file).open('a+') if args.log_file is not None else StringIO() # noqa: SIM115
try:
for line in missed_output:
formatted_line = f'{master_node_name} | {line}'
print(formatted_line)
log_file.write(f'{formatted_line}\n')
- except:
+ except: # noqa: S110
pass
finally:
log_file.close()
diff --git a/grizzly_cli/distributed/build.py b/grizzly_cli/distributed/build.py
index feac3a0..8c27215 100644
--- a/grizzly_cli/distributed/build.py
+++ b/grizzly_cli/distributed/build.py
@@ -1,13 +1,18 @@
-import os
+from __future__ import annotations
-from typing import List, cast
-from argparse import SUPPRESS, Namespace as Arguments
+import os
+from argparse import SUPPRESS
+from argparse import Namespace as Arguments
from getpass import getuser
-from socket import gethostbyname, gaierror
+from pathlib import Path
+from socket import gaierror, gethostbyname
+from typing import TYPE_CHECKING
-from grizzly_cli.utils import get_dependency_versions, requirements, run_command
-from grizzly_cli.argparse import ArgumentSubParser
from grizzly_cli import EXECUTION_CONTEXT, PROJECT_NAME, STATIC_CONTEXT
+from grizzly_cli.utils import get_dependency_versions, requirements, run_command
+
+if TYPE_CHECKING: # pragma: no cover
+ from grizzly_cli.argparse import ArgumentSubParser
def create_parser(sub_parser: ArgumentSubParser) -> None:
@@ -63,33 +68,27 @@ def create_parser(sub_parser: ArgumentSubParser) -> None:
def getuid() -> int:
if os.name == 'nt' or not hasattr(os, 'getuid'):
return 1000
- else:
- return cast(int, getattr(os, 'getuid')())
+
+ return os.getuid()
def getgid() -> int:
if os.name == 'nt' or not hasattr(os, 'getgid'):
return 1000
- else:
- return cast(int, getattr(os, 'getgid')())
+
+ return os.getuid()
-def _create_build_command(args: Arguments, containerfile: str, tag: str, context: str) -> List[str]:
+def _create_build_command(args: Arguments, containerfile: str, tag: str, context: str) -> list[str]:
local_install = getattr(args, 'local_install', False)
- if local_install:
- install_type = 'local'
- else:
- install_type = 'remote'
+ install_type = 'local' if local_install else 'remote'
- (_, grizzly_extras, ), _ = get_dependency_versions(local_install)
+ (_, grizzly_extras), _ = get_dependency_versions(local_install=local_install)
- if grizzly_extras is not None and 'mq' in grizzly_extras:
- grizzly_extra = 'mq'
- else:
- grizzly_extra = 'base'
+ grizzly_extra = 'mq' if grizzly_extras is not None and 'mq' in grizzly_extras else 'base'
- extra_args: List[str] = []
+ extra_args: list[str] = []
ibm_mq_lib_host = os.environ.get('IBM_MQ_LIB_HOST', None)
if ibm_mq_lib_host is not None:
@@ -120,7 +119,7 @@ def _create_build_command(args: Arguments, containerfile: str, tag: str, context
*extra_args,
'-f', containerfile,
'-t', tag,
- context
+ context,
]
@@ -128,14 +127,11 @@ def _create_build_command(args: Arguments, containerfile: str, tag: str, context
def build(args: Arguments) -> int:
tag = getuser()
- if args.project_name is None:
- image_name = f'{PROJECT_NAME}:{tag}'
- else:
- image_name = f'{args.project_name}:{tag}'
+ image_name = f'{PROJECT_NAME}:{tag}' if args.project_name is None else f'{args.project_name}:{tag}'
build_command = _create_build_command(
args,
- f'{STATIC_CONTEXT}{os.path.sep}Containerfile',
+ Path.joinpath(Path(STATIC_CONTEXT), 'Containerfile').as_posix(),
image_name,
EXECUTION_CONTEXT,
)
@@ -171,8 +167,8 @@ def build(args: Arguments) -> int:
if result.return_code != 0:
print(f'\n!! failed to tag image {image_name} -> {args.registry}{image_name}')
return result.return_code
- else:
- print(f'tagged image {image_name} -> {args.registry}{image_name}')
+
+ print(f'tagged image {image_name} -> {args.registry}{image_name}')
push_command = [
f'{args.container_system}',
diff --git a/grizzly_cli/distributed/clean.py b/grizzly_cli/distributed/clean.py
index 29f29b4..0a6e702 100644
--- a/grizzly_cli/distributed/clean.py
+++ b/grizzly_cli/distributed/clean.py
@@ -1,19 +1,25 @@
-from os import environ
-from tempfile import NamedTemporaryFile
-from argparse import Namespace as Arguments
+from __future__ import annotations
+
from getpass import getuser
+from os import environ
from shutil import get_terminal_size
+from tempfile import NamedTemporaryFile
+from typing import TYPE_CHECKING
-from grizzly_cli.argparse import ArgumentSubParser
-from grizzly_cli.utils import run_command
from grizzly_cli import PROJECT_NAME, STATIC_CONTEXT
+from grizzly_cli.utils import run_command
+
+if TYPE_CHECKING: # pragma: no cover
+ from argparse import Namespace as Arguments
+
+ from grizzly_cli.argparse import ArgumentSubParser
def create_parser(sub_parser: ArgumentSubParser) -> None:
# grizzly-cli dist clean ...
clean_parser = sub_parser.add_parser('clean', description=(
'clean all grizzly compose project resources; containers, images, networks and volumes'
- ),)
+ ))
clean_parser.add_argument(
'--no-images',
@@ -41,10 +47,7 @@ def clean(args: Arguments) -> int:
suffix = '' if args.id is None else f'-{args.id}'
tag = getuser()
- if args.project_name is not None:
- project_name = args.project_name
- else:
- project_name = PROJECT_NAME
+ project_name = args.project_name if args.project_name is not None else PROJECT_NAME
columns, lines = get_terminal_size()
env = environ.copy()
@@ -72,7 +75,7 @@ def clean(args: Arguments) -> int:
if args.images:
command = [
args.container_system,
- 'image', 'rm', f'{project_name}:{tag}'
+ 'image', 'rm', f'{project_name}:{tag}',
]
run_command(command)
diff --git a/grizzly_cli/init.py b/grizzly_cli/init.py
index 3f489e0..af585df 100644
--- a/grizzly_cli/init.py
+++ b/grizzly_cli/init.py
@@ -1,12 +1,18 @@
-from typing import Generator
-from argparse import Namespace as Arguments
-from os import path
+from __future__ import annotations
+
from pathlib import Path
+from typing import TYPE_CHECKING
+
from packaging.version import Version
-from .utils import ask_yes_no
-from .argparse import ArgumentSubParser
-from . import EXECUTION_CONTEXT, register_parser
+from grizzly_cli import EXECUTION_CONTEXT, register_parser
+from grizzly_cli.utils import ask_yes_no
+
+if TYPE_CHECKING: # pragma: no cover
+ from argparse import Namespace as Arguments
+ from collections.abc import Generator
+
+ from grizzly_cli.argparse import ArgumentSubParser
# prefix components:
space = ' '
@@ -35,7 +41,7 @@ def create_parser(sub_parser: ArgumentSubParser) -> None:
type=str,
required=False,
default=None,
- help='specify which grizzly version to use for project, default is latest'
+ help='specify which grizzly version to use for project, default is latest',
)
init_parser.add_argument(
@@ -59,13 +65,14 @@ def create_parser(sub_parser: ArgumentSubParser) -> None:
def tree(dir_path: Path, prefix: str = '') -> Generator[str, None, None]:
- '''A recursive generator, given a directory Path object
+ """Recursive generator, given a directory Path object
will yield a visual tree structure line by line
- with each line prefixed by the same characters
+ with each line prefixed by the same characters.
credit: https://stackoverflow.com/a/59109706
- '''
- contents = sorted(list(dir_path.iterdir()))
+
+ """
+ contents = sorted(dir_path.iterdir())
# contents each get pointers that are ├── with a final └── :
pointers = [tee] * (len(contents) - 1) + [last]
for pointer, sub_path in zip(pointers, contents):
@@ -77,18 +84,18 @@ def tree(dir_path: Path, prefix: str = '') -> Generator[str, None, None]:
def init(args: Arguments) -> int:
- if path.exists(path.join(EXECUTION_CONTEXT, args.project)):
+ if Path.joinpath(Path(EXECUTION_CONTEXT), args.project).exists():
print(f'"{args.project}" already exists in {EXECUTION_CONTEXT}')
return 1
- if all([path.exists(path.join(EXECUTION_CONTEXT, p)) for p in ['environments', 'features', 'requirements.txt']]):
+ if all(Path.joinpath(Path(EXECUTION_CONTEXT), p).exists() for p in ['environments', 'features', 'requirements.txt']):
print('oops, looks like you are already in a grizzly project directory', end='\n\n')
print(EXECUTION_CONTEXT)
for line in tree(Path(EXECUTION_CONTEXT)):
print(line)
return 1
- layout = f'''
+ layout = f"""
{args.project}
├── environments
│ └── {args.project}.yaml
@@ -99,7 +106,7 @@ def init(args: Arguments) -> int:
│ ├── {args.project}.feature
│ └── requests
└── requirements.txt
-'''
+"""
message = f'the following structure will be created:\n{layout}'
@@ -109,7 +116,7 @@ def init(args: Arguments) -> int:
print(message)
# create project root
- structure = Path(path.join(EXECUTION_CONTEXT, args.project))
+ structure = Path.joinpath(Path(EXECUTION_CONTEXT), args.project)
structure.mkdir()
# create requirements.txt
@@ -128,20 +135,20 @@ def init(args: Arguments) -> int:
structure_environments.mkdir()
# create environments/.yaml
- (structure_environments / f'{args.project}.yaml').write_text('''configuration:
+ (structure_environments / f'{args.project}.yaml').write_text("""configuration:
template:
host: https://localhost
-''')
+""")
# create features/ directory
structure_features = structure / 'features'
structure_features.mkdir()
# create features/.feature
- (structure_features / f'{args.project}.feature').write_text('''Feature: Template feature file
+ (structure_features / f'{args.project}.feature').write_text("""Feature: Template feature file
Scenario: Template scenario
Given a user of type "RestApi" with weight "1" load testing "$conf::template.host"
-''')
+""")
# create features/environment.py
if args.grizzly_version is not None:
diff --git a/grizzly_cli/keyvault.py b/grizzly_cli/keyvault.py
index 9910f63..d701715 100644
--- a/grizzly_cli/keyvault.py
+++ b/grizzly_cli/keyvault.py
@@ -2,23 +2,27 @@
import logging
import re
-
-from typing import Any
-from pathlib import Path
-from argparse import Namespace as Arguments, ArgumentParser as CoreArgumentParser
from base64 import b64encode
-from dataclasses import dataclass
from contextlib import suppress
+from dataclasses import dataclass
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
import yaml
-from azure.core.exceptions import ResourceNotFoundError, ClientAuthenticationError, ServiceRequestError
-from azure.keyvault.secrets import SecretClient
+from azure.core.exceptions import ClientAuthenticationError, ResourceNotFoundError, ServiceRequestError
-from . import register_parser
-from .argparse import ArgumentSubParser
-from .argparse.bashcompletion import BashCompletionTypes
-from .utils import flatten, unflatten, IndentDumper, merge_dicts, logger, chunker
-from .utils.configuration import get_context_root, load_configuration_file, load_configuration_keyvault, get_keyvault_client
+from grizzly_cli import register_parser
+from grizzly_cli.argparse.bashcompletion import BashCompletionTypes
+from grizzly_cli.utils import IndentDumper, chunker, flatten, logger, merge_dicts, unflatten
+from grizzly_cli.utils.configuration import get_context_root, get_keyvault_client, load_configuration_file, load_configuration_keyvault
+
+if TYPE_CHECKING: # pragma: no cover
+ from argparse import ArgumentParser as CoreArgumentParser
+ from argparse import Namespace as Arguments
+
+ from azure.keyvault.secrets import SecretClient
+
+ from grizzly_cli.argparse import ArgumentSubParser
# disable azure.identity warning logs if authentication fails
azure_logger = logging.getLogger('azure')
@@ -32,9 +36,6 @@
@dataclass
class KeyvaultSecretHolder:
- """
- Keyvault secret holder.
- """
name: str
content_type: str | None
value: str
@@ -167,7 +168,7 @@ def encode_mq_certificate(root: Path, environment: str, key: str, base_cert_name
name=key,
content_type='files',
value=','.join(references),
- )
+ ),
)
return secrets
@@ -222,7 +223,7 @@ def _should_export(key: str, secret: Any) -> bool:
return False
for keyword in KEYWORDS:
- key_should = keyword in key.lower() and not f'.{keyword}.' in key
+ key_should = keyword in key.lower() and f'.{keyword}.' not in key
secret_should = isinstance(secret, str) and keyword in secret.lower() and secret not in COMMON_FALSE_POSITIVES
# if key should be exported, but not the secret and the secret is not a string... it shouldn't
@@ -242,14 +243,14 @@ def _build_key_name(environment: str, key: str) -> str:
return f'grizzly--{environment}--{_keyvault_normalize(key)}'
-def _dict_to_yaml(file: Path, content: dict[str, Any], *, indentation: Path | int) -> None:
+def _dict_to_yaml(file: Path, content: dict, *, indentation: Path | int) -> None:
file.write_text('') # make sure file is empty
with file.open('w') as fd:
yaml.dump(content, fd, Dumper=IndentDumper.use_indentation(indentation), default_flow_style=False, sort_keys=False, allow_unicode=True)
-def _extract_metadata(env_file: str) -> tuple[str, str | None, dict[str, Any]]:
+def _extract_metadata(env_file: str) -> tuple[str, str | None, dict]:
file = Path(env_file)
configuration = load_configuration_file(file).get('configuration', {})
@@ -267,10 +268,12 @@ def diff(left_file_name: str, right_file_name: str) -> int:
right_config_file = Path(right_file_name)
if not left_config_file.exists():
- raise ValueError(f'environment configuration file {left_config_file.as_posix()} does not exist')
+ message = f'environment configuration file {left_config_file.as_posix()} does not exist'
+ raise ValueError(message)
if not right_config_file.exists():
- raise ValueError(f'environment configuration file {right_config_file.as_posix()} does not exist')
+ message = f'environment configuration file {right_config_file.as_posix()} does not exist'
+ raise ValueError(message)
left_config = flatten(load_configuration_file(right_config_file)['configuration'])
right_config = flatten(load_configuration_file(left_config_file)['configuration'])
@@ -287,15 +290,15 @@ def diff(left_file_name: str, right_file_name: str) -> int:
diffed_keys.add(key)
for key, value in right_config.items():
- if key not in left_config or left_config.get(key, None) != value and key not in diffed_keys:
+ if key not in left_config or (left_config.get(key, None) != value and key not in diffed_keys):
logger.error(f'+ {key}: {left_config.get(key, None)} != {value}')
return 0
-def keyvault_import(client: SecretClient, environment: str, args: Arguments, root: Path, configuration: dict[str, Any]) -> int:
+def keyvault_import(client: SecretClient, environment: str, args: Arguments, root: Path, configuration: dict) -> int:
# unflatten existing configuration
- configuration_unflatten: dict[str, Any] = {}
+ configuration_unflatten: dict = {}
original_key_count = len(configuration)
@@ -336,45 +339,57 @@ def keyvault_import(client: SecretClient, environment: str, args: Arguments, roo
return 0
-def keyvault_export(client: SecretClient, environment: str, args: Arguments, root: Path, configuration: dict[str, Any]) -> int:
- """
- From grizzly to keyvault.
+def upsert_secrets(client: SecretClient, secrets: list[KeyvaultSecretHolder], *, dry_run: bool) -> int:
+ created_secrets_count = 0
- If the specified secret starts with `cert:`, it indicates that it should reference a keyvault certificate (the name). This value
- can have metadata (secret content type) appended after the actual value the `#` separator. If the output certificate should be
- password protected, `pass:` should reference a keyvault secret which contains the password.
+ for secret in secrets:
+ # check if secret already exists
+ try:
+ current_value = client.get_secret(secret.name)
+ if current_value.value != secret.value or current_value.properties.content_type != secret.content_type:
+ raise ResourceNotFoundError
- `cert:[,pass:][#format:[mqm|pem-public|pem-private]]`
+ # it exists with the same value, skip
+ continue
+ except ResourceNotFoundError: # great, import it into the keyvault
+ pass
- Supported certificate output formats:
- - `mqm`, MQ CMS keystore
- - `pem-public`, public certificate in PEM format
- - `pem-private`, private key in PEM format
+ logger.debug(f'% keyvault secret {secret.name} with content type {secret.content_type}')
- If the configuration key contains `file` in the path, the configuration value will be base64 encoded and, optionally, chunked into
- keyvault secrets. If the path also contains `mq`, all MQ keystore/CMS files will be encoded, chunked and then references to the
- actual configuration key.
- """
- secrets: list[KeyvaultSecretHolder] = []
+ if not dry_run:
+ client.set_secret(secret.name, secret.value, content_type=secret.content_type)
- environment_file = Path(args.env_file)
+ created_secrets_count += 1
+
+ return created_secrets_count
- safe_configuration = {}
+
+def prepare_secrets(
+ client: SecretClient,
+ environment: str,
+ root: Path,
+ configuration: dict,
+ *,
+ filter_keys: list[str] | None,
+ global_configuration: list[str],
+) -> tuple[dict, list[KeyvaultSecretHolder]]:
+ secrets: list[KeyvaultSecretHolder] = []
+ safe_configuration: dict = {}
for key, secret in configuration.items():
if not _should_export(key, secret):
safe_configuration.update({key: secret})
continue
- if args.keys is not None and key not in args.keys:
+ if filter_keys is not None and key not in filter_keys:
continue
- key_environment = _determine_environment(args.global_configuration, environment, key)
+ key_environment = _determine_environment(global_configuration, environment, key)
key_name = _build_key_name(key_environment, key)
if secret.startswith('cert:'):
if '#' in secret:
- secret, content_type = secret.split('#', 1)
+ secret, content_type = secret.split('#', 1) # noqa: PLW2901
else:
content_type = None
@@ -385,7 +400,7 @@ def keyvault_export(client: SecretClient, environment: str, args: Arguments, roo
client.get_secret(password_key)
except ResourceNotFoundError:
message = f'key {password_key} referenced in value for {key} does not exist'
- raise ValueError(message)
+ raise ValueError(message) from None
secrets.append(KeyvaultSecretHolder(
name=key_name,
@@ -404,32 +419,37 @@ def keyvault_export(client: SecretClient, environment: str, args: Arguments, roo
value=secret,
))
- created_secrets_count = 0
+ return safe_configuration, secrets
- for secret in secrets:
- # check if secret already exists
- try:
- current_value = client.get_secret(secret.name)
- if current_value.value != secret.value or current_value.properties.content_type != secret.content_type:
- raise ResourceNotFoundError
- # it exists with the same value, skip
- continue
- except ResourceNotFoundError: # great, import it into the keyvault
- pass
+def keyvault_export(client: SecretClient, environment: str, args: Arguments, root: Path, configuration: dict) -> int:
+ """From grizzly to keyvault.
- logger.debug(f'% keyvault secret {secret.name} with content type {secret.content_type}')
+ If the specified secret starts with `cert:`, it indicates that it should reference a keyvault certificate (the name). This value
+ can have metadata (secret content type) appended after the actual value the `#` separator. If the output certificate should be
+ password protected, `pass:` should reference a keyvault secret which contains the password.
- if not args.dry_run:
- client.set_secret(secret.name, secret.value, content_type=secret.content_type)
+ `cert:[,pass:][#format:[mqm|pem-public|pem-private]]`
- created_secrets_count += 1
+ Supported certificate output formats:
+ - `mqm`, MQ CMS keystore
+ - `pem-public`, public certificate in PEM format
+ - `pem-private`, private key in PEM format
+
+ If the configuration key contains `file` in the path, the configuration value will be base64 encoded and, optionally, chunked into
+ keyvault secrets. If the path also contains `mq`, all MQ keystore/CMS files will be encoded, chunked and then references to the
+ actual configuration key.
+ """
+ environment_file = Path(args.env_file)
+ safe_configuration, secrets = prepare_secrets(client, environment, root, configuration, filter_keys=args.keys, global_configuration=args.global_configuration)
+
+ created_secrets_count = upsert_secrets(client, secrets, dry_run=args.dry_run)
already_exists = len(secrets) - created_secrets_count
if not args.dry_run:
unsafe_environment_file = environment_file.rename(environment_file.with_suffix(f'.unsafe{environment_file.suffix}'))
- safe_yaml_configuration: dict[str, Any] = {}
+ safe_yaml_configuration: dict = {}
for key, value in safe_configuration.items():
safe_yaml_configuration = merge_dicts(safe_yaml_configuration, unflatten(key, value))
@@ -446,7 +466,7 @@ def keyvault_export(client: SecretClient, environment: str, args: Arguments, roo
logger.info(
f'created {created_secrets_count} ({already_exists} already existed) secrets in keyvault {client.vault_url} '
- f'and saved the safe environment configuration in {environment_file.as_posix()}'
+ f'and saved the safe environment configuration in {environment_file.as_posix()}',
)
logger.warning(f'! the unsafe environment configuration is still present in {unsafe_environment_file.as_posix()}')
@@ -465,7 +485,8 @@ def keyvault(args: Arguments) -> int:
env_file = Path(args.env_file)
if args.subcommand == 'import' and not env_file.exists():
if args.keyvault is None:
- raise ValueError(f'--vault-name not specified and environment configuration file {args.env_file} does not exist')
+ message = f'--vault-name not specified and environment configuration file {args.env_file} does not exist'
+ raise ValueError(message)
_dict_to_yaml(env_file, {'configuration': {'keyvault': args_keyvault}}, indentation=2)
@@ -481,19 +502,23 @@ def keyvault(args: Arguments) -> int:
keyvault = keyvault or args_keyvault
if keyvault is None:
- raise ValueError('keyvault not specified, please specify a keyvault')
+ message = 'keyvault not specified, please specify a keyvault'
+ raise ValueError(message)
client = get_keyvault_client(keyvault)
try:
if args.subcommand == 'import': # from keyvault
return keyvault_import(client, environment, args, grizzly_context_root, configuration)
- elif args.subcommand == 'export': # to keyvault
+
+ if args.subcommand == 'export': # to keyvault
return keyvault_export(client, environment, args, grizzly_context_root, configuration)
- elif args.subcommand == 'diff':
+
+ if args.subcommand == 'diff':
return diff(args.env_file, args.orig_file)
- else:
- raise ValueError(f'unknown subcommand {args.subcommand}')
+
+ message = f'unknown subcommand {args.subcommand}'
+ raise ValueError(message)
except ClientAuthenticationError:
logger.error('authentication failed, if you are running from a resource which does not have a managed identity then you must run `az login` first.')
return 1
diff --git a/grizzly_cli/local.py b/grizzly_cli/local.py
index e41b42f..1cd16dd 100644
--- a/grizzly_cli/local.py
+++ b/grizzly_cli/local.py
@@ -1,14 +1,13 @@
import os
-
-from typing import List, Dict, Any
from argparse import Namespace as Arguments
-from . import register_parser
-from .utils import (
+from grizzly_cli import register_parser
+from grizzly_cli.argparse import ArgumentSubParser
+from grizzly_cli.run import create_parser as run_create_parser
+from grizzly_cli.run import run
+from grizzly_cli.utils import (
run_command,
)
-from .run import create_parser as run_create_parser, run
-from .argparse import ArgumentSubParser
@register_parser(order=2)
@@ -26,11 +25,12 @@ def create_parser(sub_parser: ArgumentSubParser) -> None:
def local(args: Arguments) -> int:
if args.subcommand == 'run':
return run(args, local_run)
- else:
- raise ValueError(f'unknown subcommand {args.subcommand}')
+
+ message = f'unknown subcommand {args.subcommand}'
+ raise ValueError(message)
-def local_run(args: Arguments, environ: Dict[str, Any], run_arguments: Dict[str, List[str]]) -> int:
+def local_run(args: Arguments, environ: dict, run_arguments: dict[str, list[str]]) -> int:
for key, value in environ.items():
if key not in os.environ:
os.environ[key] = value
diff --git a/grizzly_cli/run.py b/grizzly_cli/run.py
index 14a2562..05f0da4 100644
--- a/grizzly_cli/run.py
+++ b/grizzly_cli/run.py
@@ -1,37 +1,39 @@
from __future__ import annotations
-import sys
import os
-
+import sys
+from contextlib import suppress
+from datetime import datetime
+from pathlib import Path
+from platform import node as get_hostname
from typing import (
- List,
- Dict,
- Any,
+ TYPE_CHECKING,
Callable,
TextIO,
cast,
)
-from argparse import Namespace as Arguments
-from platform import node as get_hostname
-from datetime import datetime
-from pathlib import Path
-from contextlib import suppress
+
from jinja2 import Environment
import grizzly_cli
-from .utils import (
- logger,
- find_variable_names_in_questions,
- ask_yes_no, get_input,
+from grizzly_cli.argparse.bashcompletion import BashCompletionTypes
+from grizzly_cli.utils import (
+ ask_yes_no,
distribution_of_users_per_scenario,
- requirements,
find_metadata_notices,
+ find_variable_names_in_questions,
+ get_input,
+ logger,
parse_feature_file,
+ requirements,
rm_rf,
)
-from .utils.configuration import ScenarioTag, load_configuration, get_context_root
-from .argparse import ArgumentSubParser
-from .argparse.bashcompletion import BashCompletionTypes
+from grizzly_cli.utils.configuration import ScenarioTag, get_context_root, load_configuration
+
+if TYPE_CHECKING:
+ from argparse import Namespace as Arguments
+
+ from grizzly_cli.argparse import ArgumentSubParser
def create_parser(sub_parser: ArgumentSubParser, parent: str) -> None:
@@ -44,7 +46,7 @@ def create_parser(sub_parser: ArgumentSubParser, parent: str) -> None:
help=(
'changes the log level to `DEBUG`, regardless of what it says in the feature file. gives more verbose logging '
'that can be useful when troubleshooting a problem with a scenario.'
- )
+ ),
)
run_parser.add_argument(
'-T', '--testdata-variable',
@@ -53,7 +55,7 @@ def create_parser(sub_parser: ArgumentSubParser, parent: str) -> None:
required=False,
help=(
'specified in the format `=`. avoids being asked for an initial value for a scenario variable.'
- )
+ ),
)
run_parser.add_argument(
'-y', '--yes',
@@ -132,10 +134,91 @@ def create_parser(sub_parser: ArgumentSubParser, parent: str) -> None:
run_parser.prog = f'grizzly-cli {parent} run'
+def should_prompt_questions(args: Arguments, environ: dict) -> None:
+ variables = find_variable_names_in_questions(args.file)
+ questions = len(variables)
+ manual_input = False
+
+ if questions > 0 and not getattr(args, 'validate_config', False):
+ logger.info(f'feature file requires values for {questions} variables')
+
+ for variable in variables:
+ name = f'TESTDATA_VARIABLE_{variable}'
+ value = os.environ.get(name, '')
+ while len(value) < 1:
+ value = get_input(f'initial value for "{variable}": ')
+ manual_input = True
+
+ environ[name] = value
+
+ logger.info('the following values was provided:')
+ for key, value in environ.items():
+ if not key.startswith('TESTDATA_VARIABLE_'):
+ continue
+ logger.info(f'{key.replace("TESTDATA_VARIABLE_", "")} = {value}')
+
+ if manual_input:
+ ask_yes_no('continue?')
+
+
+def should_prompt_notices(args: Arguments) -> None:
+ notices = find_metadata_notices(args.file)
+
+ if len(notices) > 0:
+ output_func = cast('Callable[[str], None]', logger.info) if args.yes else ask_yes_no
+
+ for notice in notices:
+ output_func(notice)
+
+
+def update_grizzly_environment(args: Arguments, environ: dict) -> None:
+ if args.environment_file is not None:
+ environment_lock_file = load_configuration(Path(args.environment_file).resolve())
+ environ.update({'GRIZZLY_CONFIGURATION_FILE': environment_lock_file.as_posix()})
+
+ if args.dry_run:
+ environ.update({'GRIZZLY_DRY_RUN': 'true'})
+
+ if args.log_dir is not None:
+ environ.update({'GRIZZLY_LOG_DIR': args.log_dir})
+
+
+def build_run_arguments(args: Arguments) -> dict[str, list[str]]:
+ run_arguments: dict[str, list[str]] = {
+ 'master': [],
+ 'worker': [],
+ 'common': [],
+ }
+
+ if args.verbose:
+ run_arguments['common'] += ['--verbose', '--no-logcapture', '--no-capture', '--no-capture-stderr']
+
+ if args.csv_prefix is not None:
+ if args.csv_prefix is True:
+ parse_feature_file(args.file)
+ if grizzly_cli.FEATURE_DESCRIPTION is None:
+ message = 'feature file does not seem to have a `Feature:` description to use as --csv-prefix'
+ raise ValueError(message)
+
+ csv_prefix = grizzly_cli.FEATURE_DESCRIPTION.replace(' ', '_')
+ timestamp = datetime.now().astimezone().strftime('%Y%m%dT%H%M%S')
+ args.csv_prefix = f'{csv_prefix}_{timestamp}'
+
+ run_arguments['common'] += [f'-Dcsv-prefix="{args.csv_prefix}"']
+
+ if args.csv_interval is not None:
+ run_arguments['common'] += [f'-Dcsv-interval={args.csv_interval}']
+
+ if args.csv_flush_interval is not None:
+ run_arguments['common'] += [f'-Dcsv-flush-interval={args.csv_flush_interval}']
+
+ return run_arguments
+
+
@requirements(grizzly_cli.EXECUTION_CONTEXT)
-def run(args: Arguments, run_func: Callable[[Arguments, Dict[str, Any], Dict[str, List[str]]], int]) -> int:
+def run(args: Arguments, run_func: Callable[[Arguments, dict, dict[str, list[str]]], int]) -> int:
# always set hostname of host where grizzly-cli was executed, could be useful
- environ: Dict[str, Any] = {
+ environ: dict = {
'GRIZZLY_CLI_HOST': get_hostname(),
'GRIZZLY_EXECUTION_CONTEXT': grizzly_cli.EXECUTION_CONTEXT,
'GRIZZLY_MOUNT_CONTEXT': grizzly_cli.MOUNT_CONTEXT,
@@ -150,7 +233,7 @@ def run(args: Arguments, run_func: Callable[[Arguments, Dict[str, Any], Dict[str
feature_lock_file = feature_file.parent / f'{feature_file.stem}.lock{feature_file.suffix}'
try:
- buffer: List[str] = []
+ buffer: list[str] = []
remove_endif = False
# remove if-statements containing variables (`{$ .. $}`)
@@ -176,94 +259,27 @@ def run(args: Arguments, run_func: Callable[[Arguments, Dict[str, Any], Dict[str
feature_lock_file.write_text(feature_content)
if args.dump:
- output: TextIO
- if isinstance(args.dump, str):
- output = Path(args.dump).open('w+')
- else:
- output = sys.stdout
+ output: TextIO = Path(args.dump).open('w+') if isinstance(args.dump, str) else sys.stdout # noqa: SIM115
- print(feature_content, file=output)
+ try:
+ print(feature_content, file=output)
+ finally:
+ # do not close stdout...
+ if output is not sys.stdout:
+ output.close()
return 0
args.file = feature_lock_file.as_posix()
- variables = find_variable_names_in_questions(args.file)
- questions = len(variables)
- manual_input = False
-
- if questions > 0 and not getattr(args, 'validate_config', False):
- logger.info(f'feature file requires values for {questions} variables')
-
- for variable in variables:
- name = f'TESTDATA_VARIABLE_{variable}'
- value = os.environ.get(name, '')
- while len(value) < 1:
- value = get_input(f'initial value for "{variable}": ')
- manual_input = True
-
- environ[name] = value
-
- logger.info('the following values was provided:')
- for key, value in environ.items():
- if not key.startswith('TESTDATA_VARIABLE_'):
- continue
- logger.info(f'{key.replace("TESTDATA_VARIABLE_", "")} = {value}')
-
- if manual_input:
- ask_yes_no('continue?')
-
- notices = find_metadata_notices(args.file)
-
- if len(notices) > 0:
- if args.yes:
- output_func = cast(Callable[[str], None], logger.info)
- else:
- output_func = ask_yes_no
-
- for notice in notices:
- output_func(notice)
-
- if args.environment_file is not None:
- environment_file = os.path.realpath(args.environment_file)
- environment_lock_file = load_configuration(environment_file)
- environ.update({'GRIZZLY_CONFIGURATION_FILE': environment_lock_file})
-
- if args.dry_run:
- environ.update({'GRIZZLY_DRY_RUN': 'true'})
-
- if args.log_dir is not None:
- environ.update({'GRIZZLY_LOG_DIR': args.log_dir})
+ should_prompt_questions(args, environ)
+ should_prompt_notices(args)
+ update_grizzly_environment(args, environ)
if not getattr(args, 'validate_config', False):
distribution_of_users_per_scenario(args, environ)
- run_arguments: Dict[str, List[str]] = {
- 'master': [],
- 'worker': [],
- 'common': [],
- }
-
- if args.verbose:
- run_arguments['common'] += ['--verbose', '--no-logcapture', '--no-capture', '--no-capture-stderr']
-
- if args.csv_prefix is not None:
- if args.csv_prefix is True:
- parse_feature_file(args.file)
- if grizzly_cli.FEATURE_DESCRIPTION is None:
- raise ValueError('feature file does not seem to have a `Feature:` description to use as --csv-prefix')
-
- csv_prefix = grizzly_cli.FEATURE_DESCRIPTION.replace(' ', '_')
- timestamp = datetime.now().astimezone().strftime('%Y%m%dT%H%M%S')
- setattr(args, 'csv_prefix', f'{csv_prefix}_{timestamp}')
-
- run_arguments['common'] += [f'-Dcsv-prefix="{args.csv_prefix}"']
-
- if args.csv_interval is not None:
- run_arguments['common'] += [f'-Dcsv-interval={args.csv_interval}']
-
- if args.csv_flush_interval is not None:
- run_arguments['common'] += [f'-Dcsv-flush-interval={args.csv_flush_interval}']
+ run_arguments = build_run_arguments(args)
return run_func(args, environ, run_arguments)
finally:
diff --git a/grizzly_cli/utils/__init__.py b/grizzly_cli/utils/__init__.py
index dad47dd..015e70a 100644
--- a/grizzly_cli/utils/__init__.py
+++ b/grizzly_cli/utils/__init__.py
@@ -1,49 +1,50 @@
from __future__ import annotations
-import re
-import sys
-import subprocess
-import signal as psignal
import logging
import logging.config
-import stat
import os
-
-from typing import Optional, List, Set, Union, Dict, Any, Tuple, Callable, Type, ClassVar, cast
-from types import TracebackType, FrameType
-from os import path, environ
-from shutil import which, rmtree
-from behave.parser import parse_file as feature_file_parser
-from argparse import Namespace as Arguments
-from json import loads as jsonloads
+import re
+import signal as psignal
+import stat
+import subprocess
+import sys
+from collections.abc import Mapping
+from contextlib import suppress
+from copy import deepcopy
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
from functools import wraps
-from packaging import version as versioning
-from tempfile import mkdtemp
from hashlib import sha1
+from json import loads as jsonloads
from math import ceil
-from datetime import datetime, timezone
-from dataclasses import dataclass, field
+from os import environ
from pathlib import Path
-from copy import deepcopy
-from collections.abc import Mapping
+from shutil import rmtree, which
+from tempfile import mkdtemp
+from typing import TYPE_CHECKING, Any, Callable, ClassVar, Optional, Union, cast
import requests
import tomli
-
-from behave.model import Scenario
+from behave.parser import parse_file as feature_file_parser
from jinja2 import Template
+from packaging import version as versioning
from progress.spinner import Spinner
from yaml import Dumper
import grizzly_cli
+if TYPE_CHECKING: # pragma: no cover
+ from argparse import Namespace as Arguments
+ from types import FrameType, TracebackType
+
+ from behave.model import Scenario
logger = logging.getLogger('grizzly-cli')
class SignalHandler:
handler: Callable[[int, Optional[FrameType]], None]
- signals: Dict[int, Union[Callable[[int, Optional[FrameType]], Any], int, None]]
+ signals: dict[int, Union[Callable[[int, Optional[FrameType]], Any], int, None]]
def __init__(self, handler: Callable[[int, Optional[FrameType]], None], signal: int, *signals: int) -> None:
self.handler = handler
@@ -54,11 +55,11 @@ def __init__(self, handler: Callable[[int, Optional[FrameType]], None], signal:
self.signals.update({sig: None})
def __enter__(self) -> None:
- for signal in self.signals.keys():
+ for signal in self.signals:
self.signals.update({signal: psignal.getsignal(signal)})
psignal.signal(signal, self.handler)
- def __exit__(self, exc_type: Optional[Type[BaseException]], exc: Optional[BaseException], tb: Optional[TracebackType]) -> bool:
+ def __exit__(self, exc_type: Optional[type[BaseException]], exc: Optional[BaseException], tb: Optional[TracebackType]) -> bool:
for signal, handler in self.signals.items():
psignal.signal(signal, handler)
@@ -69,15 +70,15 @@ def __exit__(self, exc_type: Optional[Type[BaseException]], exc: Optional[BaseEx
class RunCommandResult:
return_code: int
abort_timestamp: Optional[datetime] = field(init=False, default=None)
- output: Optional[List[bytes]] = field(init=False, default=None)
+ output: Optional[list[bytes]] = field(init=False, default=None)
-def run_command(command: List[str], env: Optional[Dict[str, str]] = None, *, silent: bool = False, verbose: bool = False, spinner: Optional[str] = None) -> RunCommandResult:
+def run_command(command: list[str], env: Optional[dict[str, str]] = None, *, silent: bool = False, verbose: bool = False, spinner: Optional[str] = None) -> RunCommandResult:
if env is None:
env = environ.copy()
if verbose:
- logger.info(f'run_command: {" ".join(command)}')
+ logger.info('run_command: %s', ' '.join(command))
process = subprocess.Popen(
command,
@@ -96,7 +97,7 @@ def run_command(command: List[str], env: Optional[Dict[str, str]] = None, *, sil
if spinner is not None: # pragma: no cover
_spinner = Spinner(f'{spinner} ')
- def sig_handler(signum: int, frame: Optional[FrameType] = None) -> None: # pragma: no cover
+ def sig_handler(*_args: Any, **_kwargs: Any) -> None: # pragma: no cover
if result.abort_timestamp is None:
result.abort_timestamp = datetime.now(timezone.utc)
process.terminate()
@@ -125,10 +126,8 @@ def sig_handler(signum: int, frame: Optional[FrameType] = None) -> None: # prag
except KeyboardInterrupt:
pass
finally:
- try:
+ with suppress(Exception):
process.kill()
- except Exception:
- pass
process.wait()
@@ -140,25 +139,21 @@ def sig_handler(signum: int, frame: Optional[FrameType] = None) -> None: # prag
return result
-def get_docker_compose_version() -> Tuple[int, int, int]: # pragma: no cover
- output = subprocess.getoutput('docker compose version')
+def get_docker_compose_version() -> tuple[int, int, int]: # pragma: no cover
+ output = subprocess.getoutput('docker compose version') # noqa: S605
version_line = output.splitlines()[0]
match = re.match(r'.*version [v]?([1-2]\.[0-9]+\.[0-9]+).*$', version_line)
- if match:
- version = cast(Tuple[int, int, int], tuple([int(part) for part in match.group(1).split('.')]))
- else:
- version = (0, 0, 0,)
-
- return version
+ return cast('tuple[int, int, int]', tuple([int(part) for part in match.group(1).split('.')])) if match else (0, 0, 0)
-def onerror(func: Callable, path: str, exc_info: Union[
- BaseException,
- Tuple[Type[BaseException], BaseException, Optional[TracebackType]],
-]) -> None: # noqa: ARG001 # pragma: no cover
+def onerror(
+ func: Callable,
+ path: str,
+ exc_info: Union[BaseException, tuple[type[BaseException], BaseException, Optional[TracebackType]], # noqa: ARG001
+]) -> None: # pragma: no cover
"""Error handler for shutil.rmtree.
If the error is due to an access error (read only file)
@@ -172,12 +167,13 @@ def onerror(func: Callable, path: str, exc_info: Union[
_path.chmod(stat.S_IWUSR)
func(path)
else:
- raise # pylint: disable=E0704
+ raise # noqa: PLE0704
def rm_rf(path: Union[str, Path], *, missing_ok: bool = False) -> None:
"""Remove the path contents recursively, even if some elements
- are read-only."""
+ are read-only.
+ """
p = path.as_posix() if isinstance(path, Path) else path
try:
@@ -190,14 +186,14 @@ def rm_rf(path: Union[str, Path], *, missing_ok: bool = False) -> None:
raise
-def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Optional[str], Optional[List[str]]], Optional[str]]:
+def get_dependency_versions(*, local_install: Union[bool, str]) -> tuple[tuple[Optional[str], Optional[list[str]]], Optional[str]]: # noqa: C901, PLR0912, PLR0915
grizzly_requirement: Optional[str] = None
grizzly_requirement_egg: str
locust_version: Optional[str] = None
grizzly_version: Optional[str] = None
- grizzly_extras: Optional[List[str]] = None
+ grizzly_extras: Optional[list[str]] = None
- args: tuple[str, ...] = (grizzly_cli.EXECUTION_CONTEXT,)
+ args: tuple[str, ...] = ()
if isinstance(local_install, str):
args += (local_install,)
if not local_install.endswith('requirements.txt'):
@@ -205,20 +201,20 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
else:
args += ('requirements.txt',)
- project_requirements = path.join(*args)
+ project_requirements = Path.joinpath(Path(grizzly_cli.EXECUTION_CONTEXT), *args)
try:
- with open(project_requirements, encoding='utf-8') as fd:
+ with project_requirements.open(encoding='utf-8') as fd:
for line in fd.readlines():
- if any([pkg in line for pkg in ['grizzly-loadtester', 'grizzly.git'] if not re.match(r'^([\s]+)?#', line)]):
+ if any(pkg in line for pkg in ['grizzly-loadtester', 'grizzly.git'] if not re.match(r'^([\s]+)?#', line)):
grizzly_requirement = line.strip()
break
except:
- return (None, None,), None
+ return (None, None), None
if grizzly_requirement is None:
print(f'!! unable to find grizzly dependency in {project_requirements}', file=sys.stderr)
- return ('(unknown)', None, ), '(unknown)'
+ return ('(unknown)', None), '(unknown)'
# check if it's a repo or not
if 'git+' in grizzly_requirement:
@@ -231,24 +227,24 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
url = url.strip()
else:
print(f'!! unable to find properly formatted grizzly dependency in {project_requirements}', file=sys.stderr)
- return ('(unknown)', None, ), '(unknown)'
+ return ('(unknown)', None), '(unknown)'
url, branch = url.rsplit('@', 1)
url = url[4:] # remove git+
- suffix = sha1(grizzly_requirement.encode('utf-8')).hexdigest()
+ suffix = sha1(grizzly_requirement.encode('utf-8')).hexdigest() # noqa: S324
# extras_requirement normalization
egg = grizzly_requirement_egg.replace('[', '__').replace(']', '__').replace(',', '_')
tmp_workspace = mkdtemp(prefix='grizzly-cli-')
- repo_destination = path.join(tmp_workspace, f'{egg}_{suffix}')
+ repo_destination = Path(tmp_workspace) / f'{egg}_{suffix}'
try:
rc = subprocess.check_call(
[
'git', 'clone', '--filter=blob:none', '-q',
url,
- repo_destination
+ repo_destination,
],
shell=False,
stdout=subprocess.DEVNULL,
@@ -257,7 +253,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
if rc != 0:
print(f'!! unable to clone git repo {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
active_branch = branch
@@ -274,7 +270,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
if rc != 0:
print(f'!! unable to check branch name of HEAD in git repo {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
if active_branch != branch:
try:
@@ -290,7 +286,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
git_object_type = 'branch' # assume remote branch
else:
print(f'!! unable to determine git object type for {branch}')
- raise RuntimeError()
+ raise RuntimeError from cpe
if git_object_type == 'tag': # pragma: no cover
rc += subprocess.check_call(
@@ -307,7 +303,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
if rc != 0: # pragma: no cover
print(f'!! unable to checkout tag {branch} from git repo {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
elif git_object_type == 'commit':
rc += subprocess.check_call(
[
@@ -322,7 +318,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
if rc != 0: # pragma: no cover
print(f'!! unable to checkout commit {branch} from git repo {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
else:
rc += subprocess.check_call(
[
@@ -338,30 +334,30 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
if rc != 0:
print(f'!! unable to checkout branch {branch} from git repo {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
- if not path.exists(path.join(repo_destination, 'pyproject.toml')):
- with open(path.join(repo_destination, 'grizzly', '__init__.py'), encoding='utf-8') as fd:
+ if not Path.joinpath(repo_destination, 'pyproject.toml').exists():
+ with Path.joinpath(repo_destination, 'grizzly', '__init__.py').open(encoding='utf-8') as fd:
version_raw = [line.strip() for line in fd.readlines() if line.strip().startswith('__version__ =')]
if len(version_raw) != 1:
print(f'!! unable to find "__version__" declaration in grizzly/__init__.py from {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
_, grizzly_version, _ = version_raw[-1].split("'")
else:
try:
- with open(path.join(repo_destination, 'setup.cfg'), encoding='utf-8') as fd:
+ with Path.joinpath(repo_destination, 'setup.cfg').open(encoding='utf-8') as fd:
version_raw = [line.strip() for line in fd.readlines() if line.strip().startswith('version = ')]
if len(version_raw) != 1:
print(f'!! unable to find "version" declaration in setup.cfg from {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
_, grizzly_version = version_raw[-1].split(' = ')
except FileNotFoundError:
try:
- import setuptools_scm # pylint: disable=unused-import # noqa: F401 # type: ignore
+ import setuptools_scm # noqa: F401, PLC0415
except ModuleNotFoundError: # pragma: no cover
rc = subprocess.check_call([
sys.executable,
@@ -382,20 +378,20 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
universal_newlines=True,
cwd=repo_destination,
).strip()
- except subprocess.CalledProcessError: # pragma: no cover
+ except subprocess.CalledProcessError as e: # pragma: no cover
print(f'!! unable to get setuptools_scm version from {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError from e # abort
if grizzly_version == '0.0.0':
grizzly_version = '(development)'
try:
- with open(path.join(repo_destination, 'requirements.txt'), encoding='utf-8') as fd:
+ with Path.joinpath(repo_destination, 'requirements.txt').open(encoding='utf-8') as fd:
version_raw = [line.strip() for line in fd.readlines() if line.strip().startswith('locust')]
if len(version_raw) != 1:
print(f'!! unable to find "locust" dependency in requirements.txt from {url}', file=sys.stderr)
- raise RuntimeError() # abort
+ raise RuntimeError # abort
match = re.match(r'^locust.{2}(.*?)$', version_raw[-1].strip().split(' ')[0])
@@ -404,7 +400,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
else:
locust_version = match.group(1).strip()
except FileNotFoundError: # pragma: no cover
- with open(path.join(repo_destination, 'pyproject.toml'), 'rb') as fdt:
+ with Path.joinpath(repo_destination, 'pyproject.toml').open('rb') as fdt:
toml_dict = tomli.load(fdt)
dependencies = toml_dict.get('project', {}).get('dependencies', [])
for dependency in dependencies:
@@ -420,7 +416,8 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
rm_rf(tmp_workspace)
else:
response = requests.get(
- 'https://pypi.org/pypi/grizzly-loadtester/json'
+ 'https://pypi.org/pypi/grizzly-loadtester/json',
+ timeout=10,
)
if response.status_code != 200:
@@ -434,7 +431,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
if re.match(r'^grizzly-loadtester(\[[^\]]*\])?$', grizzly_requirement): # latest
grizzly_version = pypi.get('info', {}).get('version', None)
else:
- conditions: List[Callable[[versioning.Version], bool]] = []
+ conditions: list[Callable[[versioning.Version], bool]] = []
match = re.match(r'^(grizzly-loadtester(\[[^\]]*\])?)(.*?)$', grizzly_requirement)
@@ -457,13 +454,11 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
matched_version = None
- for available_version in pypi.get('releases', {}).keys():
- try:
+ for available_version in pypi.get('releases', {}):
+ with suppress(versioning.InvalidVersion):
version = versioning.parse(available_version)
- if len(conditions) > 0 and all([compare(version) for compare in conditions]):
+ if len(conditions) > 0 and all(compare(version) for compare in conditions):
matched_version = version
- except versioning.InvalidVersion:
- pass
if matched_version is None:
print(f'!! could not resolve {grizzly_requirement} to one specific version available at pypi', file=sys.stderr)
@@ -473,7 +468,8 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
if grizzly_version is not None:
# get version from pypi, to be able to get locust version
response = requests.get(
- f'https://pypi.org/pypi/grizzly-loadtester/{grizzly_version}/json'
+ f'https://pypi.org/pypi/grizzly-loadtester/{grizzly_version}/json',
+ timeout=10,
)
if response.status_code != 200:
@@ -487,10 +483,7 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
match = re.match(r'^locust \((.*?)\)$', requires_dist.strip())
- if match:
- locust_version = cast(str, match.group(1))
- else:
- locust_version = requires_dist.replace('locust', '').strip()
+ locust_version = cast('str', match.group(1)) if match else requires_dist.replace('locust', '').strip()
if locust_version is not None and locust_version.startswith('=='):
locust_version = locust_version[2:]
@@ -508,19 +501,16 @@ def get_dependency_versions(local_install: Union[bool, str]) -> Tuple[Tuple[Opti
else:
match = re.match(r'^grizzly-loadtester\[([^\]]*)\]$', grizzly_requirement_egg)
- if match:
- grizzly_extras = [extra.strip() for extra in match.group(1).split(',')]
- else:
- grizzly_extras = []
+ grizzly_extras = [extra.strip() for extra in match.group(1).split(',')] if match else []
if locust_version is None:
locust_version = '(unknown)'
- return (grizzly_version, grizzly_extras, ), locust_version
+ return (grizzly_version, grizzly_extras), locust_version
-def list_images(args: Arguments) -> Dict[str, Any]:
- images: Dict[str, Any] = {}
+def list_images(args: Arguments) -> dict[str, dict[str, str]]:
+ images: dict[str, dict[str, str]] = {}
output = subprocess.check_output([
f'{args.container_system}',
'image',
@@ -542,6 +532,7 @@ def list_images(args: Arguments) -> Dict[str, Any]:
if name not in images:
images[name] = {}
+
images[name].update(version)
return images
@@ -559,7 +550,8 @@ def get_default_mtu(args: Arguments) -> Optional[str]:
]).decode('utf-8')
line, _ = output.split('\n', 1)
- network_options: Dict[str, str] = jsonloads(line)
+ network_options: dict[str, str] = jsonloads(line)
+
return network_options.get('com.docker.network.driver.mtu', '1500')
except:
return None
@@ -568,12 +560,12 @@ def get_default_mtu(args: Arguments) -> Optional[str]:
def requirements(execution_context: str) -> Callable[[Callable[..., int]], Callable[..., int]]:
def wrapper(func: Callable[..., int]) -> Callable[..., int]:
@wraps(func)
- def _wrapper(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> int:
+ def _wrapper(*args: Any, **kwargs: Any) -> int:
return func(*args, **kwargs)
# a bit ugly, but needed for testability
- setattr(func, '__value__', execution_context)
- setattr(_wrapper, '__wrapped__', func)
+ setattr(func, '__value__', execution_context) # noqa: B010
+ setattr(_wrapper, '__wrapped__', func) # noqa: B010
return _wrapper
@@ -590,7 +582,7 @@ def get_distributed_system() -> Optional[str]:
print('neither "podman" nor "docker" found in PATH')
return None
- rc, _ = subprocess.getstatusoutput(f'{container_system} compose version')
+ rc, _ = subprocess.getstatusoutput(f'{container_system} compose version') # noqa: S605
if rc != 0:
print(f'"{container_system} compose" not found in PATH')
@@ -611,7 +603,7 @@ def ask_yes_no(question: str) -> None:
answer = get_input(f'{question} [y/n]: ')
if answer == 'n':
- raise KeyboardInterrupt()
+ raise KeyboardInterrupt
def parse_feature_file(file: str) -> None:
@@ -626,13 +618,13 @@ def parse_feature_file(file: str) -> None:
grizzly_cli.SCENARIOS.append(scenario)
-def find_metadata_notices(file: str) -> List[str]:
- with open(file) as fd:
- return [line.strip().replace('# grizzly-cli:notice ', '') for line in fd.readlines() if line.strip().startswith('# grizzly-cli:notice ')]
+def find_metadata_notices(file: str) -> list[str]:
+ with Path(file).open('r') as fd:
+ return [line.strip().replace('# grizzly-cli:notice ', '') for line in fd if line.strip().startswith('# grizzly-cli:notice ')]
-def find_variable_names_in_questions(file: str) -> List[str]:
- unique_variables: Set[str] = set()
+def find_variable_names_in_questions(file: str) -> list[str]:
+ unique_variables: set[str] = set()
parse_feature_file(file)
@@ -644,84 +636,90 @@ def find_variable_names_in_questions(file: str) -> List[str]:
match = re.match(r'ask for value of variable "([^"]*)"', step.name)
if not match:
- raise ValueError(f'could not find variable name in "{step.name}"')
+ message = f'could not find variable name in "{step.name}"'
+ raise ValueError(message)
unique_variables.add(match.group(1))
- return sorted(list(unique_variables))
+ return sorted(unique_variables)
-def distribution_of_users_per_scenario(args: Arguments, environ: Dict[str, Any]) -> None:
- def _guess_datatype(value: str) -> Union[str, int, float, bool]:
- check_value = value.replace('.', '', 1)
+def _guess_datatype(value: str) -> Union[str, int, float, bool]:
+ check_value = value.replace('.', '', 1)
- if check_value[0] == '-':
- check_value = check_value[1:]
+ if check_value[0] == '-':
+ check_value = check_value[1:]
- if check_value.isdecimal():
- if float(value) % 1 == 0:
- if value.startswith('0'):
- return str(value)
- else:
- return int(float(value))
- else:
- return float(value)
- elif value.lower() in ['true', 'false']:
- return value.lower() == 'true'
- else:
- return value
-
- class ScenarioProperties:
- name: str
- index: int
- identifier: str
- user: Optional[str]
- weight: float
- _iterations: Optional[int]
- _user_count: Optional[int]
-
- def __init__(
- self,
- name: str,
- index: int,
- weight: Optional[float] = None,
- user: Optional[str] = None,
- iterations: Optional[int] = None,
- user_count: Optional[int] = None,
- ) -> None:
- self.name = name
- self.index = index
- self.user = user
- self._iterations = iterations
- self.weight = weight or 1.0
- self.identifier = f'{index:03}'
- self._user_count = user_count
-
- @property
- def iterations(self) -> int:
- if self._iterations is None: # pragma: no cover
- raise ValueError('iterations has not been set')
-
- return self._iterations
-
- @iterations.setter
- def iterations(self, value: int) -> None:
- self._iterations = value
-
- @property
- def user_count(self) -> int:
- if self._user_count is None: # pragma: no cover
- raise ValueError('user count has not been set')
- return self._user_count
-
- @user_count.setter
- def user_count(self, value: int) -> None:
- self._user_count = value
-
- def is_fulfilled(self) -> bool:
- return self.user is not None and self._iterations is not None and self._user_count is not None
-
- distribution: Dict[str, ScenarioProperties] = {}
+ if check_value.isdecimal():
+ if float(value) % 1 == 0:
+ if value.startswith('0'):
+ return str(value)
+
+ return int(float(value))
+
+ return float(value)
+
+ if value.lower() in ['true', 'false']:
+ return value.lower() == 'true'
+
+ return value
+
+
+class ScenarioProperties:
+ name: str
+ index: int
+ identifier: str
+ user: Optional[str]
+ weight: float
+ _iterations: Optional[int]
+ _user_count: Optional[int]
+
+ def __init__(
+ self,
+ name: str,
+ index: int,
+ weight: Optional[float] = None,
+ user: Optional[str] = None,
+ iterations: Optional[int] = None,
+ user_count: Optional[int] = None,
+ ) -> None:
+ self.name = name
+ self.index = index
+ self.user = user
+ self._iterations = iterations
+ self.weight = weight or 1.0
+ self.identifier = f'{index:03}'
+ self._user_count = user_count
+
+ @property
+ def iterations(self) -> int:
+ if self._iterations is None: # pragma: no cover
+ message = 'iterations has not been set'
+ raise ValueError(message)
+
+ return self._iterations
+
+ @iterations.setter
+ def iterations(self, value: int) -> None:
+ self._iterations = value
+
+ @property
+ def user_count(self) -> int:
+ if self._user_count is None: # pragma: no cover
+ message = 'user count has not been set'
+ raise ValueError(message)
+ return self._user_count
+
+ @user_count.setter
+ def user_count(self, value: int) -> None:
+ self._user_count = value
+
+ def is_fulfilled(self) -> bool:
+ return self.user is not None and self._iterations is not None and self._user_count is not None
+
+
+def distribution_of_users_per_scenario(args: Arguments, environ: dict) -> None: # noqa: C901, PLR0912, PLR0915
+ distribution: dict[str, ScenarioProperties] = {}
variables = {key.replace('TESTDATA_VARIABLE_', ''): _guess_datatype(value) for key, value in environ.items() if key.startswith('TESTDATA_VARIABLE_')}
def _pre_populate_scenario(scenario: Scenario, index: int) -> None:
@@ -738,9 +736,10 @@ def _pre_populate_scenario(scenario: Scenario, index: int) -> None:
use_weights = True
for index, scenario in enumerate(grizzly_cli.SCENARIOS):
- scenario_variables: Dict[str, Any] = {}
+ scenario_variables: dict = {}
if len(scenario.steps) < 1:
- raise ValueError(f'scenario "{scenario.name}" does not have any steps')
+ message = f'scenario "{scenario.name}" does not have any steps'
+ raise ValueError(message)
_pre_populate_scenario(scenario, index=index + 1)
@@ -763,7 +762,7 @@ def _pre_populate_scenario(scenario: Scenario, index: int) -> None:
variable_name = match.group(1)
variable_value = Template(match.group(2)).render(**variables, **scenario_variables)
scenario_variables.update({variable_name: variable_value})
- except:
+ except: # noqa: S112
continue
elif step.name.startswith('a user of type'):
match = re.match(r'a user of type "([^"]*)" (with weight "([^"]*)")?.*', step.name)
@@ -774,8 +773,7 @@ def _pre_populate_scenario(scenario: Scenario, index: int) -> None:
match = re.match(r'repeat for "([^"]*)" iteration[s]?', step.name)
if match:
distribution[scenario.name].iterations = int(round(float(Template(match.group(1)).render(**variables, **scenario_variables)), 0))
- (distribution[scenario.name].iterations)
- elif any([pattern in step.name for pattern in ['users of type', 'user of type']]):
+ elif any(pattern in step.name for pattern in ['users of type', 'user of type']):
match = re.match(r'"([^"]*)" user[s]? of type "([^"]*)".*', step.name)
if match:
scenario_user_count = int(round(float(Template(match.group(1)).render(**variables, **scenario_variables)), 0))
@@ -790,13 +788,15 @@ def _pre_populate_scenario(scenario: Scenario, index: int) -> None:
scenario_count = len(distribution.keys())
assert scenario_user_count_total is not None
if scenario_count > scenario_user_count_total:
- raise ValueError(f'grizzly needs at least {scenario_count} users to run this feature')
+ message = f'grizzly needs at least {scenario_count} users to run this feature'
+ raise ValueError(message)
total_weight = 0
total_iterations = 0
for scenario in distribution.values():
if scenario.user is None:
- raise ValueError(f'{scenario.name} does not have a user type')
+ message = f'{scenario.name} does not have a user type'
+ raise ValueError(message)
total_weight += scenario.weight
total_iterations += scenario.iterations
@@ -830,7 +830,7 @@ def print_table_lines(max_length_iterations: int, max_length_users: int, max_len
line += ['-' * (max_length_errors + 1), '-|']
logger.info(''.join(line))
- rows: List[str] = []
+ rows: list[str] = []
max_length_description = len('description')
max_length_iterations = len('#iter')
max_length_users = len('#user')
@@ -840,9 +840,9 @@ def print_table_lines(max_length_iterations: int, max_length_users: int, max_len
if hasattr(args, 'environment_file') and args.environment_file is not None:
message = f'{message} with environment file {environ["GRIZZLY_CONFIGURATION_FILE"]}'
- logger.info(f'{message}\n')
+ logger.info('%s\n', message)
- errors: Dict[str, List[str]] = {}
+ errors: dict[str, list[str]] = {}
for scenario in distribution.values():
# check for errors
@@ -876,7 +876,7 @@ def print_table_lines(max_length_iterations: int, max_length_users: int, max_len
row_format.append(' {}')
for scenario in distribution.values():
- row_format_args: List[Any] = [
+ row_format_args: list = [
scenario.identifier,
scenario.weight,
scenario.iterations,
@@ -899,7 +899,7 @@ def print_table_lines(max_length_iterations: int, max_length_users: int, max_len
rows.append(''.join(row_format).format(*row_format_args))
logger.info('each scenario will execute accordingly:\n')
- header_row_args: List[Any] = [
+ header_row_args: list = [
'ident',
'weight',
'#iter', max_length_iterations,
@@ -926,19 +926,20 @@ def print_table_lines(max_length_iterations: int, max_length_users: int, max_len
arrow_width = len('ident') + 2 + max_length_iterations + 2 + max_length_users + 2 + max_length_description + 2
if use_weights:
arrow_width += len('weight') + 2
- message = f'''{" " * (1 + arrow_width)}^
+ message = f"""{" " * (1 + arrow_width)}^
+{"-" * arrow_width}+
|
+- there were errors when calculating user distribution and iterations per scenario, adjust user "weight", number of users or iterations per scenario
-'''
+"""
raise ValueError(message)
- else:
- logger.info('')
+
+ logger.info('')
for scenario in distribution.values():
if scenario.iterations < scenario.user_count:
- raise ValueError(f'{scenario.name} will have {scenario.user_count} users to run {scenario.iterations} iterations, increase iterations or lower user count')
+ message = f'{scenario.name} will have {scenario.user_count} users to run {scenario.iterations} iterations, increase iterations or lower user count'
+ raise ValueError(message)
if not args.yes:
ask_yes_no('continue?')
@@ -985,7 +986,7 @@ def setup_logging(logfile: Optional[str] = None) -> None:
logging.config.dictConfig(logging_config)
-def unflatten(key: str, value: Any) -> dict[str, Any]:
+def unflatten(key: str, value: Any) -> dict:
paths: list[str] = key.split('.')
# last node should have the value
@@ -1001,9 +1002,9 @@ def unflatten(key: str, value: Any) -> dict[str, Any]:
return struct
-def flatten(node: dict[str, Any], parents: Optional[list[str]] = None) -> dict[str, Any]:
+def flatten(node: dict, parents: Optional[list[str]] = None) -> dict:
"""Flatten a dictionary so each value key is the path down the nested dictionary structure."""
- flat: dict[str, Any] = {}
+ flat: dict = {}
if parents is None:
parents = []
@@ -1019,7 +1020,7 @@ def flatten(node: dict[str, Any], parents: Optional[list[str]] = None) -> dict[s
return flat
-def merge_dicts(merged: dict[str, Any], source: dict[str, Any]) -> dict[str, Any]:
+def merge_dicts(merged: dict, source: dict) -> dict:
"""Merge two dicts recursively, where `source` values takes precedance over `merged` values."""
merged = deepcopy(merged)
source = deepcopy(source)
@@ -1041,9 +1042,7 @@ def merge_dicts(merged: dict[str, Any], source: dict[str, Any]) -> dict[str, Any
def chunker(value: str, size: int) -> list[str]:
- return list(
- map(lambda x: value[x * size:x * size + size],
- list(range(ceil(len(value) / size)))))
+ return [value[x * size:x * size + size] for x in list(range(ceil(len(value) / size)))]
def get_indentation(file: Path) -> int:
@@ -1059,7 +1058,7 @@ class IndentDumper(Dumper):
use_indent: ClassVar[int]
@classmethod
- def use_indentation(cls, target: Path | int) -> type['IndentDumper']:
+ def use_indentation(cls, target: Path | int) -> type[IndentDumper]:
cls.use_indent = get_indentation(target) if isinstance(target, Path) else target
return cls
@@ -1069,5 +1068,5 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.best_indent = self.use_indent
- def increase_indent(self, flow: bool = False, indentless: bool = False) -> None:
- return super().increase_indent(flow, False)
+ def increase_indent(self, flow: bool = False, indentless: bool = False) -> None: # noqa: ARG002, FBT001, FBT002
+ return super().increase_indent(flow, False) # noqa: FBT003
diff --git a/grizzly_cli/utils/configuration.py b/grizzly_cli/utils/configuration.py
index 2fb35eb..f624ee5 100644
--- a/grizzly_cli/utils/configuration.py
+++ b/grizzly_cli/utils/configuration.py
@@ -1,28 +1,32 @@
from __future__ import annotations
import re
-from pathlib import Path
-from contextlib import suppress
-from textwrap import dedent
-from typing import Any, Iterable, ClassVar, cast
from base64 import b64decode
-from cryptography.hazmat.primitives.serialization import pkcs12
-from cryptography.hazmat.primitives._serialization import PBES, KeySerializationEncryptionBuilder, PrivateFormat, KeySerializationEncryption
-from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
-from cryptography.x509 import Certificate
-from cryptography.hazmat.primitives import serialization
+from contextlib import suppress
+from pathlib import Path
from shutil import which
+from textwrap import dedent
+from typing import TYPE_CHECKING, ClassVar, Optional, cast
import yaml
-from azure.identity import AzureCliCredential, ManagedIdentityCredential, ChainedTokenCredential
-from azure.keyvault.secrets import SecretClient, KeyVaultSecret
+from azure.identity import AzureCliCredential, ChainedTokenCredential, ManagedIdentityCredential
+from azure.keyvault.secrets import KeyVaultSecret, SecretClient
+from behave.parser import parse_feature
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives._serialization import PBES, KeySerializationEncryption, KeySerializationEncryptionBuilder, PrivateFormat
+from cryptography.hazmat.primitives.serialization import pkcs12
from jinja2 import Environment
from jinja2.lexer import Token, TokenStream
from jinja2_simple_tags import StandaloneTag
-from behave.parser import parse_feature
-from behave.model import Scenario
-from grizzly_cli.utils import IndentDumper, merge_dicts, logger, unflatten, run_command
+from grizzly_cli.utils import IndentDumper, logger, merge_dicts, run_command, unflatten
+
+if TYPE_CHECKING: # pragma: no cover
+ from collections.abc import Iterable
+
+ from behave.model import Scenario
+ from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
+ from cryptography.x509 import Certificate
def get_context_root() -> Path:
@@ -39,20 +43,21 @@ def get_context_root() -> Path:
context_root = possible_context_root
if context_root is None:
- raise ValueError('context root not found, are you in a grizzly project?')
+ message = 'context root not found, are you in a grizzly project?'
+ raise ValueError(message)
return context_root.parent
class ScenarioTag(StandaloneTag):
- tags = {'scenario'}
+ tags: ClassVar[set[str]] = {'scenario'}
def preprocess(
- self, source: str, name: str | None, filename: str | None = None
+ self, source: str, name: str | None, filename: str | None = None,
) -> str:
self._source = source
- return cast(str, super().preprocess(source, name, filename))
+ return cast('str', super().preprocess(source, name, filename))
@classmethod
def get_scenario_text(cls, name: str, file: Path) -> str:
@@ -64,11 +69,13 @@ def get_scenario_text(cls, name: str, file: Path) -> str:
assert len(content.splitlines()) == len(content_skel.splitlines()), 'oops, there is not a 1:1 match between lines!'
feature = parse_feature(content_skel, filename=file.as_posix())
- scenarios = cast(list[Scenario], feature.scenarios)
+ scenarios = cast('list[Scenario]', feature.scenarios)
lines = content.splitlines()
- for scenario_index, scenario in enumerate(scenarios):
+ scenario_index: int
+ for index, scenario in enumerate(scenarios):
if scenario.name == name:
+ scenario_index = index
break
# check if there are scenarios after our scenario in the source
@@ -155,7 +162,7 @@ def render(self, scenario: str, feature: str, **variables: str) -> str:
return scenario_content
- def filter_stream(self, stream: TokenStream) -> TokenStream | Iterable[Token]: # type: ignore[return]
+ def filter_stream(self, stream: TokenStream) -> TokenStream | Iterable[Token]: # type: ignore[return] # noqa: PLR0912
"""Everything outside of `{% scenario ... %}` (and `{% if ... %}...{% endif %}`) should be treated as "data", e.g. plain text.
Overloaded from `StandaloneTag`, must match method signature, which is not `Generator`, even though we yield
@@ -230,7 +237,7 @@ def preprocess(
self, source: str, name: str | None, filename: str | None = None,
) -> str:
self._source = source
- return cast(str, super().preprocess(source, name, filename))
+ return cast('str', super().preprocess(source, name, filename))
def render(self, filename: str, *filenames: str) -> str:
buffer: list[str] = []
@@ -388,7 +395,8 @@ def _write_mqm_cert(
runmqakm_path = which('runmqakm')
if runmqakm_path is None:
- raise ValueError('runmqakm could not be found, install IBM MQC Redist, and make sure that it\'s bin/ directory is added to PATH')
+ message = "runmqakm could not be found, install IBM MQC Redist, and make sure that it's bin/ directory is added to PATH"
+ raise ValueError(message)
runmqakm_cmd: list[str] = [
runmqakm_path,
@@ -415,7 +423,8 @@ def _write_mqm_cert(
for line in result.output or []:
logger.error(line.decode('utf-8').strip())
- raise ValueError(f'failed to create {relative_file}')
+ message = f'failed to create {relative_file}'
+ raise ValueError(message)
finally:
p12_file.unlink()
cms_file.with_suffix('.crl').unlink(missing_ok=True)
@@ -446,7 +455,7 @@ def _write_pem_public(root: Path, name: str, public_certificate: Certificate, ad
certificate_data: list[bytes] = []
- for certificate in [public_certificate] + additional_certificates:
+ for certificate in [public_certificate, *additional_certificates]:
certificate_pem = certificate.public_bytes(encoding=serialization.Encoding.PEM)
certificate_data.append(certificate_pem)
@@ -459,7 +468,8 @@ def _write_file(root: Path, content_type: str, encoded_content: str) -> str:
file_name = _get_metadata(content_type, 'file')
if file_name is None:
- raise ValueError('could not find `file:` in content type')
+ message = 'could not find `file:` in content type'
+ raise ValueError(message)
file = _create_safe_file_and_parent(root / 'files' / file_name)
@@ -530,9 +540,7 @@ def _import_files(client: SecretClient, root: Path, secret: KeyVaultSecret) -> s
return conf_value
-def load_configuration(configuration_file: str) -> str:
- file = Path(configuration_file)
-
+def load_configuration(file: Path) -> Path:
if not file.exists():
message = f'{file.as_posix()} does not exist'
raise ValueError(message)
@@ -562,12 +570,12 @@ def load_configuration(configuration_file: str) -> str:
with environment_lock_file.open('w') as fd:
yaml.dump(configuration, fd, Dumper=IndentDumper.use_indentation(file), default_flow_style=False, sort_keys=False, allow_unicode=True)
- return configuration_file.replace(file.name, f'{file.stem}.lock{file.suffix}')
+ return file.with_name(f'{file.stem}.lock{file.suffix}')
-def load_configuration_file(file: Path) -> dict[str, Any]:
+def load_configuration_file(file: Path) -> dict:
"""Load a grizzly environment file and flatten the structure."""
- configuration: dict[str, Any] = {}
+ configuration: dict = {}
environment = Environment(autoescape=False, extensions=[MergeYamlTag])
environment.extend(source_file=file)
@@ -586,13 +594,9 @@ def load_configuration_file(file: Path) -> dict[str, Any]:
return configuration
-def load_configuration_keyvault(client: SecretClient, environment: str, root: Path, *, filter_keys: list[str] | None) -> tuple[dict[str, Any], int]:
- environment_filter = ['global', environment]
-
+def filter_secrets(client: SecretClient, environment_filter: list[str]) -> dict[str, str]:
secret_properties = client.list_properties_of_secrets()
-
keys: dict[str, str] = {}
- configuration: dict[str, Any] = {}
# loop through all secrets to find the ones that match the environment filter
for secret_property in secret_properties:
@@ -617,6 +621,107 @@ def load_configuration_keyvault(client: SecretClient, environment: str, root: Pa
keys.update({secret_property.name: name})
+ return keys
+
+def get_certificate_encryption_algorithm(client: SecretClient, password_key: str | None) -> tuple[KeySerializationEncryption, Optional[str]]:
+ # build encryption algorithm
+ if password_key is not None:
+ password_secret = client.get_secret(password_key)
+ password = password_secret.value
+ else:
+ password = None
+
+ if password is not None:
+ encryption_algorithm = KeySerializationEncryptionBuilder(
+ PrivateFormat.PKCS12,
+ _key_cert_algorithm=PBES.PBESv1SHA1And3KeyTripleDESCBC,
+ ).build(password.encode('utf-8'))
+ else:
+ encryption_algorithm = serialization.NoEncryption()
+
+ return encryption_algorithm, password
+
+
+def encode_certificate(client: SecretClient, root: Path, secret: KeyVaultSecret, content_type: str) -> str:
+ assert secret.value is not None
+ arguments: dict[str, str] = {}
+
+ for part in secret.value.split(',', 1) + content_type.split(','):
+ argument, value = part.split(':', 1)
+ arguments.update({argument: value})
+
+ cert_key = arguments['cert']
+ cert_secret = client.get_secret(cert_key)
+
+ if cert_secret.value is None:
+ message = f'unable to download certificate secret {cert_key}'
+ raise ValueError(message)
+
+ if arguments.get('name') is None:
+ name, _ = cert_key.split('-', 1)
+ arguments.update({'name': name.lower()})
+
+ certificate = b64decode(cert_secret.value)
+
+ private_key, public_certificate, additional_certificates = pkcs12.load_key_and_certificates(data=certificate, password=None)
+
+ encryption_algorithm, password = get_certificate_encryption_algorithm(client, arguments.get('pass'))
+
+ # write files
+ cert_format = arguments.get('format')
+ if cert_format == 'pem-private':
+ if private_key is None:
+ message = f'could not find a private key in {cert_key}'
+ raise ValueError(message)
+
+ conf_value = _write_pem_private(root, arguments['name'], encryption_algorithm, private_key)
+ elif cert_format == 'pem-public':
+ if public_certificate is None:
+ message = f'could not find a public certificate in {cert_key}'
+ raise ValueError(message)
+
+ conf_value = _write_pem_public(root, arguments['name'], public_certificate, additional_certificates)
+ elif cert_format == 'mqm':
+ conf_value = _write_mqm_cert(
+ root,
+ arguments['name'],
+ password,
+ cast('pkcs12.PKCS12PrivateKeyTypes | None', private_key),
+ public_certificate,
+ additional_certificates,
+ encryption_algorithm,
+ )
+ else:
+ message = f'{cert_format} is not a supported certificate format'
+ raise ValueError(message)
+
+ return conf_value
+
+
+def encode_secret_value(client: SecretClient, root: Path, secret: KeyVaultSecret, secret_key: str) -> str:
+ assert secret.value is not None
+ assert secret.properties.content_type is not None
+
+ content_type = secret.properties.content_type
+
+ if content_type.startswith('files'):
+ conf_value = _import_files(client, root, secret)
+ if all(keyword in secret_key for keyword in ['mq', 'key']):
+ conf_value = Path(conf_value).with_suffix('').as_posix()
+ elif content_type.startswith('file:'):
+ conf_value = _write_file(root, content_type, secret.value)
+ elif content_type.startswith('format:') and secret.value.startswith('cert:'):
+ conf_value = encode_certificate(client, root, secret, content_type)
+ else:
+ message = f'unknown content type for secret {secret_key}: {content_type}'
+ raise ValueError(message)
+
+ return conf_value
+
+
+def load_configuration_keyvault(client: SecretClient, environment: str, root: Path, *, filter_keys: list[str] | None) -> tuple[dict, int]:
+ keys = filter_secrets(client, environment_filter=['global', environment])
+ configuration: dict = {}
imported_secrets = 0
# get the actual value for all secrets that matched environment filter
@@ -638,78 +743,7 @@ def load_configuration_keyvault(client: SecretClient, environment: str, root: Pa
if content_type is not None:
no_conf = 'noconf' in content_type
-
- if content_type.startswith('files'):
- conf_value = _import_files(client, root, secret)
- if all(keyword in secret_key for keyword in ['mq', 'key']):
- conf_value = Path(conf_value).with_suffix('').as_posix()
- elif content_type.startswith('file:'):
- conf_value = _write_file(root, content_type, secret.value)
- elif content_type.startswith('format:') and secret.value.startswith('cert:'):
- arguments: dict[str, str] = {}
-
- for part in secret.value.split(',', 1) + content_type.split(','):
- argument, value = part.split(':', 1)
- arguments.update({argument: value})
-
- cert_key = arguments['cert']
- cert_secret = client.get_secret(cert_key)
-
- if arguments.get('name') is None:
- name, _ = cert_key.split('-', 1)
- arguments.update({'name': name.lower()})
- if cert_secret.value is None:
- message = f'unable to download certificate secret {cert_key}'
- raise ValueError(message)
-
- certificate = b64decode(cert_secret.value)
-
- private_key, public_certificate, additional_certificates = pkcs12.load_key_and_certificates(data=certificate, password=None)
-
- # build encryption algorithm
- password_key = arguments.get('pass')
- if password_key is not None:
- password_secret = client.get_secret(password_key)
- password = password_secret.value
- else:
- password = None
-
- if password is not None:
- encryption_algorithm = KeySerializationEncryptionBuilder(
- PrivateFormat.PKCS12,
- _key_cert_algorithm=PBES.PBESv1SHA1And3KeyTripleDESCBC,
- ).build(password.encode('utf-8'))
- else:
- encryption_algorithm = serialization.NoEncryption()
-
- # write files
- cert_format = arguments.get('format')
- if cert_format == 'pem-private':
- if private_key is None:
- raise ValueError(f'could not find a private key in {cert_key}')
-
- conf_value = _write_pem_private(root, arguments['name'], encryption_algorithm, private_key)
- elif cert_format == 'pem-public':
- if public_certificate is None:
- raise ValueError(f'could not find a public certificate in {cert_key}')
-
- conf_value = _write_pem_public(root, arguments['name'], public_certificate, additional_certificates)
- elif cert_format == 'mqm':
- conf_value = _write_mqm_cert(
- root,
- arguments['name'],
- password,
- cast(pkcs12.PKCS12PrivateKeyTypes | None, private_key),
- public_certificate,
- additional_certificates,
- encryption_algorithm,
- )
- else:
- message = f'{cert_format} is not a supported certificate format'
- raise ValueError(message)
- else:
- message = f'unknown content type for secret {secret_key}: {content_type}'
- raise ValueError(message)
+ conf_value = encode_secret_value(client, root, secret, secret_key)
if not no_conf:
logger.debug('mapping %s to %s', conf_key, conf_value if content_type is not None else '******')
diff --git a/pyproject.toml b/pyproject.toml
index 60317b0..40dd095 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -61,14 +61,12 @@ grizzly-cli = "grizzly_cli.__main__:main"
[project.optional-dependencies]
dev = [
"mypy ==1.11.1",
- "pylint ==3.2.6",
"pytest ==8.3.2",
"coverage[toml] ==7.6.1",
"pytest-cov ==5.0.0",
"pytest-mock ==3.14.0",
"pytest-timeout ==2.3.1",
"atomicwrites ==1.4.1",
- "flake8-pyproject ==1.2.3",
"requests-mock ==1.12.1",
"pip-licenses ==4.3.4",
"pytablewriter ==1.2.0",
@@ -79,7 +77,8 @@ dev = [
"types-requests >=2.27.13,<3.0.0",
"setuptools-scm==8.1.0",
"types-pyyaml",
- "snakeviz"
+ "snakeviz",
+ "ruff ==0.12.1"
]
ci = [
"build ==1.1.1",
@@ -102,54 +101,6 @@ grizzly_cli = [
"argparse/bashcompletion/bashcompletion.bash"
]
-[tool.pylint.master]
-ignore = [".env", ".venv", ".pytest_tmp", "tests/e2e/example"]
-jobs = 1
-disable = "all"
-enable = [
- 'F',
- 'unreachable',
- 'duplicate-key',
- 'unnecessary-semicolon',
- 'global-variable-not-assigned',
- 'unused-variable',
- 'unused-wildcard-import',
- 'unused-import',
- 'line-too-long',
- 'binary-op-exception',
- 'bad-format-string',
- 'anomalous-backslash-in-string',
- 'bad-open-mode',
- 'E0001', 'E0011', 'E0012', 'E0100', 'E0101',
- 'E0102', 'E0103', 'E0104', 'E0105', 'E0107',
- 'E0108', 'E0110', 'E0111', 'E0112', 'E0113',
- 'E0114', 'E0115', 'E0116', 'E0117', 'E0118',
- 'E0202', 'E0203', 'E0211', 'E0213', 'E0236',
- 'E0237', 'E0238', 'E0239', 'E0240', 'E0241',
- 'E0301', 'E0302', 'E0303', 'E0401', 'E0402',
- 'E0601', 'E0602', 'E0603', 'E0604', 'E0611',
- 'E0632', 'E0633', 'E0701', 'E0702', 'E0703',
- 'E0704', 'E0710', 'E0711', 'E0712', 'E1003',
- 'E1101', 'E1102', 'E1111', 'E1120', 'E1121',
- 'E1123', 'E1124', 'E1125', 'E1126', 'E1127',
- 'E1128', 'E1129', 'E1130', 'E1131', 'E1132',
- 'E1133', 'E1134', 'E1135', 'E1136', 'E1137',
- 'E1138', 'E1139', 'E1200', 'E1201', 'E1205',
- 'E1206', 'E1300', 'E1301', 'E1302', 'E1303',
- 'E1304', 'E1305', 'E1306', 'E1310', 'E1700',
- 'E1701'
-]
-max-line-length = 180
-msg-template = "{path}:{line}:{column},{category},{symbol}:{msg}"
-reports = "no"
-output-format = "text"
-generated-members = []
-
-[tool.flake8]
-max-line-length = 180
-ignore = ["E722", "W503", "E402", "F405", "F403"]
-exclude = [".git", "__pycache__", "docs", "build", "dist", ".pytest_tmp"]
-
[tool.mypy]
exclude = ["tests/e2e/example"]
# https://github.com/python/mypy/issues/5870
@@ -202,3 +153,42 @@ addopts = [
"--cov-report=",
"--no-cov-on-fail"
]
+
+[tool.ruff]
+exclude = [
+ ".env", ".venv", ".pytest_tmp", "build", "dist", "grizzly_cli/__version__.py"
+]
+line-length = 180
+indent-width = 4
+
+[tool.ruff.lint]
+select = ["ALL"]
+ignore = [
+ "ANN401",
+ "BLE001",
+ "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D213", "D205", "D301", "D407", "D417",
+ "DTZ005",
+ "E722", "E402",
+ "F405", "F403",
+ "N801",
+ "PD011",
+ "PLR2004",
+ "UP007", "UP006", "UP045",
+ "Q000",
+ "S101", "S104", "S314", "S603", "S607", "S701",
+ "SLF001",
+ "T201",
+ "TRY301"
+]
+
+[tool.ruff.lint.per-file-ignores]
+"script/**" = ["INP001", "D100"]
+"tests/unit/utils/test___init__.py" = ["W291"]
+"tests/e2e/example/**" = ["ALL"]
+
+[tool.ruff.lint.mccabe]
+max-complexity = 15
+
+[tool.ruff.lint.pylint]
+max-branches = 15
+max-args = 10
diff --git a/tests/conftest.py b/tests/conftest.py
index ddd51cf..7c6de26 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,18 +1,22 @@
-import sys
+from __future__ import annotations
-from typing import Generator, List
+import sys
from os import environ
+from typing import TYPE_CHECKING
import pytest
-from _pytest.tmpdir import TempPathFactory
-from _pytest.config import Config
-from _pytest.fixtures import SubRequest
-
-from .fixtures import (
+from tests.fixtures import (
End2EndFixture,
)
+if TYPE_CHECKING: # pragma: no cover
+ from collections.abc import Generator
+
+ from _pytest.config import Config
+ from _pytest.fixtures import SubRequest
+ from _pytest.tmpdir import TempPathFactory
+
E2E_RUN_MODE = environ.get('E2E_RUN_MODE', 'local')
E2E_RUN_DIST = environ.get('E2E_RUN_DIST', 'False').lower() == 'True'.lower()
@@ -31,7 +35,7 @@ def pytest_configure(config: Config) -> None:
# also, add markers for each test function that starts with test_e2e_, if we're running everything
-def pytest_collection_modifyitems(items: List[pytest.Function]) -> None:
+def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
for item in items:
if item.originalname.startswith('test_e2e_') and item.get_closest_marker('timeout') is None:
item.add_marker(pytest.mark.timeout(PYTEST_TIMEOUT))
diff --git a/tests/e2e/test_auth.py b/tests/e2e/test_auth.py
index 2e8e141..84baeb4 100644
--- a/tests/e2e/test_auth.py
+++ b/tests/e2e/test_auth.py
@@ -1,19 +1,23 @@
-import re
+from __future__ import annotations
+import re
+from contextlib import contextmanager, suppress
from os import environ
-from typing import Generator, Optional, Tuple
-from contextlib import contextmanager
+from typing import TYPE_CHECKING, Optional
import pytest
-from _pytest.tmpdir import TempPathFactory
+from tests.helpers import rm_rf, run_command
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
-from tests.helpers import run_command, rm_rf
+ from _pytest.tmpdir import TempPathFactory
@contextmanager
-def auth_via(tmp_path_factory: TempPathFactory, method: str) -> Generator[Tuple[Optional[str], Optional[str]], None, None]:
- secret = 'asdfasdf'
+def auth_via(tmp_path_factory: TempPathFactory, method: str) -> Generator[tuple[Optional[str], Optional[str]], None, None]:
+ secret = 'asdfasdf' # noqa: S105
test_context = tmp_path_factory.mktemp('test_context')
argument: Optional[str] = None
stdin: Optional[str] = None
@@ -30,13 +34,11 @@ def auth_via(tmp_path_factory: TempPathFactory, method: str) -> Generator[Tuple[
argument = str(file)
try:
- yield (argument, stdin,)
+ yield (argument, stdin)
finally:
if method == 'env':
- try:
+ with suppress(KeyError):
del environ['OTP_SECRET']
- except:
- pass
rm_rf(test_context)
diff --git a/tests/e2e/test_init.py b/tests/e2e/test_init.py
index 17fd1b6..e93cac4 100644
--- a/tests/e2e/test_init.py
+++ b/tests/e2e/test_init.py
@@ -1,14 +1,18 @@
-from typing import List, Dict, Optional
-from packaging.version import Version
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Optional
import pytest
+from packaging.version import Version
-from _pytest.tmpdir import TempPathFactory
+from tests.helpers import rm_rf, run_command
-from tests.helpers import run_command, rm_rf
+if TYPE_CHECKING:
+ from _pytest.tmpdir import TempPathFactory
-@pytest.mark.parametrize('arguments,expected', [
+@pytest.mark.parametrize(
+ ('arguments', 'expected'), [
(
[],
{'mq_output': 'without IBM MQ support', 'grizzly_output': 'latest grizzly version', 'grizzly_requirements': 'grizzly-loadtester'},
@@ -26,7 +30,7 @@
{'mq_output': 'with IBM MQ support', 'grizzly_output': 'pinned to grizzly version 1.1.1', 'grizzly_requirements': 'grizzly-loadtester[mq]==1.1.1'},
),
])
-def test_e2e_init(arguments: List[str], expected: Dict[str, str], tmp_path_factory: TempPathFactory) -> None:
+def test_e2e_init(arguments: list[str], expected: dict[str, str], tmp_path_factory: TempPathFactory) -> None:
test_context = tmp_path_factory.mktemp('test_context')
grizzly_version: Optional[Version] = None
@@ -36,22 +40,19 @@ def test_e2e_init(arguments: List[str], expected: Dict[str, str], tmp_path_facto
except ValueError:
grizzly_version = None
- if grizzly_version is None or grizzly_version >= Version('2.6.0'):
- grizzly_behave_module = 'behave'
- else:
- grizzly_behave_module = 'environment'
+ grizzly_behave_module = 'behave' if grizzly_version is None or grizzly_version >= Version('2.6.0') else 'environment'
try:
rc, output = run_command(
- ['grizzly-cli', 'init', 'foobar', '--yes'] + arguments,
- cwd=str(test_context),
+ ['grizzly-cli', 'init', 'foobar', '--yes', *arguments],
+ cwd=test_context,
)
try:
assert rc == 0
except AssertionError:
print(''.join(output))
raise
- assert ''.join(output) == f'''the following structure will be created:
+ assert ''.join(output) == f"""the following structure will be created:
foobar
├── environments
@@ -67,7 +68,7 @@ def test_e2e_init(arguments: List[str], expected: Dict[str, str], tmp_path_facto
successfully created project "foobar", with the following options:
• {expected["mq_output"]}
• {expected["grizzly_output"]}
-'''
+"""
assert (test_context / 'foobar').is_dir()
assert (test_context / 'foobar' / 'environments').is_dir()
@@ -77,10 +78,10 @@ def test_e2e_init(arguments: List[str], expected: Dict[str, str], tmp_path_facto
environments_file = test_context / 'foobar' / 'environments' / 'foobar.yaml'
assert environments_file.is_file()
- assert environments_file.read_text() == '''configuration:
+ assert environments_file.read_text() == """configuration:
template:
host: https://localhost
-'''
+"""
features_dir = test_context / 'foobar' / 'features'
assert features_dir.is_dir()
@@ -99,9 +100,9 @@ def test_e2e_init(arguments: List[str], expected: Dict[str, str], tmp_path_facto
feature_file = features_dir / 'foobar.feature'
assert feature_file.is_file()
- assert feature_file.read_text() == '''Feature: Template feature file
+ assert feature_file.read_text() == """Feature: Template feature file
Scenario: Template scenario
Given a user of type "RestApi" with weight "1" load testing "$conf::template.host"
-'''
+"""
finally:
rm_rf(test_context)
diff --git a/tests/e2e/test_run.py b/tests/e2e/test_run.py
index f6d0ffd..8a6ce5e 100644
--- a/tests/e2e/test_run.py
+++ b/tests/e2e/test_run.py
@@ -1,116 +1,163 @@
-import sys
+from __future__ import annotations
+import sys
+from os import pathsep
+from pathlib import Path
from tempfile import NamedTemporaryFile
-from typing import Optional
-from os import path, pathsep
+from typing import TYPE_CHECKING, Optional
import pytest
import yaml
-from tests.fixtures import End2EndFixture
-from tests.helpers import run_command, rm_rf
+from tests.helpers import rm_rf, run_command
+if TYPE_CHECKING:
+ from tests.fixtures import End2EndFixture
-def test_e2e_run_example(e2e_fixture: End2EndFixture) -> None:
- if sys.version_info < (3, 8,) and not e2e_fixture._distributed:
- pytest.skip('grizzly-loadtester only supports python >= 3.8')
- if sys.platform == 'win32' and e2e_fixture._distributed:
- pytest.skip('windows github runners do not support running linux containers')
+def prepare_example_project(e2e_fixture: End2EndFixture) -> Path:
+ example_root = e2e_fixture.root / 'grizzly-example'
- result: Optional[str] = None
+ example_root.mkdir()
- try:
- example_root = e2e_fixture.root / 'grizzly-example'
+ rc, _ = run_command([
+ 'git', 'init',
+ ], cwd=example_root)
+
+ assert rc == 0
+
+ rc, _ = run_command([
+ 'git', 'remote', 'add', '-f', 'origin', 'https://github.com/Biometria-se/grizzly.git',
+ ], cwd=example_root)
+
+ assert rc == 0
+
+ rc, _ = run_command([
+ 'git', 'sparse-checkout', 'init',
+ ], cwd=example_root)
- example_root.mkdir()
+ assert rc == 0
- rc, _ = run_command([
- 'git', 'init',
- ], cwd=str(example_root))
+ rc, _ = run_command([
+ 'git', 'sparse-checkout', 'set', 'example',
+ ], cwd=example_root)
- assert rc == 0
+ assert rc == 0
- rc, _ = run_command([
- 'git', 'remote', 'add', '-f', 'origin', 'https://github.com/Biometria-se/grizzly.git'
- ], cwd=str(example_root))
+ rc, _ = run_command([
+ 'git', 'pull', 'origin', 'main',
+ ], cwd=example_root)
- assert rc == 0
+ assert rc == 0
- rc, _ = run_command([
- 'git', 'sparse-checkout', 'init',
- ], cwd=str(example_root))
+ rm_rf(example_root / '.git')
- assert rc == 0
+ return example_root / 'example'
- rc, _ = run_command([
- 'git', 'sparse-checkout', 'set', 'example',
- ], cwd=str(example_root))
- assert rc == 0
+def validate_result(rc: int, result: str, example_root: Path) -> None:
+ assert rc == 0
+ assert 'ERROR' not in result
+ assert 'WARNING' not in result
+ assert '1 feature passed, 0 failed, 0 skipped' in result
+ assert '3 scenarios passed, 0 failed, 0 skipped' in result
+ assert 'steps passed, 0 failed, 0 skipped, 0 undefined' in result
- rc, _ = run_command([
- 'git', 'pull', 'origin', 'main',
- ], cwd=str(example_root))
+ assert 'ident iter status description' in result
+ assert '001 2/2 passed dog facts api' in result
+ assert '002 1/1 passed cat facts api' in result
+ assert '003 1/1 passed book api' in result
+ assert '------|-----|--------|---------------|' in result
- assert rc == 0
+ assert 'executing custom.User.request for 002 get-cat-facts and /facts?limit=' in result
- rm_rf(example_root / '.git')
+ assert 'sending "client_server" from CLIENT' in result
+ assert "received from CLIENT" in result
+ assert "AtomicCustomVariable.foobar='foobar'" in result
- example_root = example_root / 'example'
+ assert 'compose.yaml: `version` is obsolete' not in result
- with open(example_root / 'features' / 'steps' / 'steps.py', 'a') as fd:
+ log_file_result = (example_root / 'test_run.log').read_text()
+
+ # problems with a locust DEBUG log message containing ERROR in the message on macos-latest
+ if sys.platform == 'darwin':
+ output = [line for line in log_file_result.split('\n') if 'ERROR' not in line and 'DEBUG' not in line]
+ log_file_result = '\n'.join(output)
+
+ if sys.version_info >= (3, 12):
+ result = result.replace('\r', '\n')
+
+ assert log_file_result == result
+
+
+def install_dependencies(e2e_fixture: End2EndFixture, example_root: Path) -> None:
+ if e2e_fixture._distributed:
+ command = ['grizzly-cli', 'dist', '--project-name', e2e_fixture.root.name, 'build', '--no-cache']
+ rc, output = run_command(
+ command,
+ cwd=example_root,
+ env=e2e_fixture._env,
+ )
+ try:
+ assert rc == 0
+ except AssertionError:
+ print(''.join(output))
+ raise
+ else:
+ command = ['python', '-m', 'pip', 'install', '--no-cache-dir', '-r', 'requirements.txt']
+ if sys.platform == 'win32':
+ command += ['--user']
+
+ rc, output = run_command(
+ command,
+ cwd=example_root,
+ env=e2e_fixture._env,
+ )
+
+ try:
+ assert rc == 0
+ except AssertionError:
+ print(''.join(output))
+ raise
+
+
+def test_e2e_run_example(e2e_fixture: End2EndFixture) -> None:
+ if sys.version_info < (3, 8) and not e2e_fixture._distributed:
+ pytest.skip('grizzly-loadtester only supports python >= 3.8')
+
+ if sys.platform == 'win32' and e2e_fixture._distributed:
+ pytest.skip('windows github runners do not support running linux containers')
+
+ result: Optional[str] = None
+
+ try:
+ example_root = prepare_example_project(e2e_fixture)
+
+ with (example_root / 'features' / 'steps' / 'steps.py').open('a') as fd:
fd.write(e2e_fixture.start_webserver_step_impl(e2e_fixture.webserver_port))
e2e_fixture.inject_webserver_module(example_root)
- with open(example_root / 'environments' / 'example.yaml') as env_yaml_file:
+ with (example_root / 'environments' / 'example.yaml').open('r') as env_yaml_file:
env_conf = yaml.full_load(env_yaml_file)
for name in ['dog', 'cat', 'book']:
env_conf['configuration']['facts'][name]['host'] = f'http://{e2e_fixture.host}'
- feature_file = path.join('features', 'example.feature')
+ feature_file = Path.joinpath(Path('features'), 'example.feature')
feature_file_path = example_root / 'features' / 'example.feature'
feature_file_contents = feature_file_path.read_text().split('\n')
requirements_file = example_root / 'requirements.txt'
requirements_file.write_text('grizzly-loadtester @ git+https://github.com/Biometria-se/grizzly.git@main\n')
- if e2e_fixture._distributed:
- command = ['grizzly-cli', 'dist', '--project-name', e2e_fixture.root.name, 'build', '--no-cache']
- rc, output = run_command(
- command,
- cwd=str(example_root),
- env=e2e_fixture._env,
- )
- try:
- assert rc == 0
- except AssertionError:
- print(''.join(output))
- raise
- else:
- command = ['python', '-m', 'pip', 'install', '--no-cache-dir', '-r', 'requirements.txt']
- if sys.platform == 'win32':
- command += ['--user']
-
- rc, output = run_command(
- command,
- cwd=str(example_root),
- env=e2e_fixture._env,
- )
-
- try:
- assert rc == 0
- except AssertionError:
- print(''.join(output))
- raise
+ install_dependencies(e2e_fixture, example_root)
index = feature_file_contents.index(' Scenario: dog facts api')
# should go last in "Background"-section
feature_file_contents.insert(index - 1, f' Then start webserver on master port "{e2e_fixture.webserver_port}"')
- with open(feature_file_path, 'w') as fd:
+ with feature_file_path.open('w') as fd:
fd.truncate(0)
fd.write('\n'.join(feature_file_contents))
@@ -120,8 +167,8 @@ def test_e2e_run_example(e2e_fixture: End2EndFixture) -> None:
rc, output = e2e_fixture.execute(
feature_file,
- env_conf_file.name.replace(f'{str(example_root)}{pathsep}', ''),
- cwd=str(example_root),
+ env_conf_file.name.replace(f'{example_root.as_posix()}{pathsep}', ''),
+ cwd=example_root,
arguments=['-l', 'test_run.log'],
)
@@ -131,38 +178,7 @@ def test_e2e_run_example(e2e_fixture: End2EndFixture) -> None:
result = ''.join(output)
- assert rc == 0
- assert 'ERROR' not in result
- assert 'WARNING' not in result
- assert '1 feature passed, 0 failed, 0 skipped' in result
- assert '3 scenarios passed, 0 failed, 0 skipped' in result
- assert 'steps passed, 0 failed, 0 skipped, 0 undefined' in result
-
- assert 'ident iter status description' in result
- assert '001 2/2 passed dog facts api' in result
- assert '002 1/1 passed cat facts api' in result
- assert '003 1/1 passed book api' in result
- assert '------|-----|--------|---------------|' in result
-
- assert 'executing custom.User.request for 002 get-cat-facts and /facts?limit=' in result
-
- assert 'sending "client_server" from CLIENT' in result
- assert "received from CLIENT" in result
- assert "AtomicCustomVariable.foobar='foobar'" in result
-
- assert 'compose.yaml: `version` is obsolete' not in result
-
- log_file_result = (example_root / 'test_run.log').read_text()
-
- # problems with a locust DEBUG log message containing ERROR in the message on macos-latest
- if sys.platform == 'darwin':
- output = [line for line in log_file_result.split('\n') if 'ERROR' not in line and 'DEBUG' not in line]
- log_file_result = '\n'.join(output)
-
- if sys.version_info >= (3, 12):
- result = result.replace('\r', '\n')
-
- assert log_file_result == result
+ validate_result(rc, result, example_root)
except:
if result is not None:
print(result)
diff --git a/tests/e2e/test_version.py b/tests/e2e/test_version.py
index 603e362..8a3e58a 100644
--- a/tests/e2e/test_version.py
+++ b/tests/e2e/test_version.py
@@ -1,25 +1,27 @@
-from typing import Optional
+from __future__ import annotations
-import pytest
+from typing import TYPE_CHECKING, Optional
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
+import pytest
-from tests.helpers import run_command, get_current_version, rm_rf
+from tests.helpers import get_current_version, rm_rf, run_command
+if TYPE_CHECKING:
+ from _pytest.tmpdir import TempPathFactory
CURRENT_VERSION = get_current_version()
-@pytest.mark.parametrize('pip_module,grizzly_version,locust_version', [
- ('grizzly-loadtester==1.0.0', '1.0.0', '2.2.1',),
- ('grizzly-loadtester[mq]==2.4.6', '2.4.6 ── extras: mq', '2.9.0',),
- ('git+https://git@github.com/biometria-se/grizzly.git@v1.4.1#egg=grizzly-loadtester', '(development)', '2.2.1',),
- ('git+https://git@github.com/biometria-se/grizzly.git@v2.4.6#egg=grizzly-loadtester', '(development)', '2.9.0',),
- ('git+https://git@github.com/biometria-se/grizzly.git@7285294b#egg=grizzly-loadtester', '2.4.7.dev7', '>=2.12.0,<2.13',),
- ('grizzly-loadtester[mq] @ git+https://git@github.com/biometria-se/grizzly.git@7285294b', '2.4.7.dev7 ── extras: mq', '>=2.12.0,<2.13',),
+@pytest.mark.parametrize(
+ ('pip_module', 'grizzly_version', 'locust_version'), [
+ ('grizzly-loadtester==1.0.0', '1.0.0', '2.2.1'),
+ ('grizzly-loadtester[mq]==2.4.6', '2.4.6 ── extras: mq', '2.9.0'),
+ ('git+https://git@github.com/biometria-se/grizzly.git@v1.4.1#egg=grizzly-loadtester', '(development)', '2.2.1'),
+ ('git+https://git@github.com/biometria-se/grizzly.git@v2.4.6#egg=grizzly-loadtester', '(development)', '2.9.0'),
+ ('git+https://git@github.com/biometria-se/grizzly.git@7285294b#egg=grizzly-loadtester', '2.4.7.dev7', '>=2.12.0,<2.13'),
+ ('grizzly-loadtester[mq] @ git+https://git@github.com/biometria-se/grizzly.git@7285294b', '2.4.7.dev7 ── extras: mq', '>=2.12.0,<2.13'),
])
-def test_e2e_version(pip_module: str, grizzly_version: str, locust_version: str, tmp_path_factory: TempPathFactory, mocker: MockerFixture) -> None:
+def test_e2e_version(pip_module: str, grizzly_version: str, locust_version: str, tmp_path_factory: TempPathFactory) -> None:
test_context = tmp_path_factory.mktemp('test_context')
result: Optional[str] = None
@@ -28,7 +30,7 @@ def test_e2e_version(pip_module: str, grizzly_version: str, locust_version: str,
# create project
rc, output = run_command(
['grizzly-cli', 'init', 'foobar', '--yes'],
- cwd=str(test_context)
+ cwd=test_context,
)
try:
@@ -44,16 +46,16 @@ def test_e2e_version(pip_module: str, grizzly_version: str, locust_version: str,
rc, output = run_command(
['grizzly-cli', '--version', 'all'],
- cwd=str(test_context / 'foobar')
+ cwd=(test_context / 'foobar'),
)
result = ''.join(output)
assert rc == 0
- assert f'''grizzly-cli {CURRENT_VERSION}
+ assert f"""grizzly-cli {CURRENT_VERSION}
└── grizzly {grizzly_version}
└── locust {locust_version}
-''' in ''.join(output)
+""" in ''.join(output)
except AssertionError:
if result is not None:
print(result)
diff --git a/tests/fixtures.py b/tests/fixtures.py
index a4f1f8c..d8132d5 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -1,26 +1,29 @@
+from __future__ import annotations
+
import inspect
import re
import socket
import sys
-
-from typing import Optional, Callable, Any, List, Tuple, Type, Dict, cast
-from typing_extensions import Literal
-from types import TracebackType
-from os import environ, getcwd, pathsep, linesep
+from contextlib import closing, suppress
+from cProfile import Profile
+from getpass import getuser
+from hashlib import sha1
+from os import environ, linesep
from pathlib import Path
from textwrap import dedent, indent
-from hashlib import sha1
-from getpass import getuser
-from contextlib import closing
-from cProfile import Profile
+from typing import TYPE_CHECKING, Any, Callable, Optional, cast
+
+from typing_extensions import Literal, Self
-from _pytest.tmpdir import TempPathFactory
-from behave.runner import Context
-from behave.model import Feature
from grizzly_cli.utils import rm_rf
+from tests.helpers import run_command
-from .helpers import run_command
+if TYPE_CHECKING:
+ from types import TracebackType
+ from _pytest.tmpdir import TempPathFactory
+ from behave.model import Feature
+ from behave.runner import Context
__all__ = [
'End2EndFixture',
@@ -33,13 +36,13 @@
class End2EndValidator:
name: str
implementation: Any
- table: Optional[List[Dict[str, str]]]
+ table: Optional[list[dict[str, str]]]
def __init__(
self,
name: str,
implementation: Callable[[Context], None],
- table: Optional[List[Dict[str, str]]] = None,
+ table: Optional[list[dict[str, str]]] = None,
) -> None:
self.name = name
self.implementation = implementation
@@ -47,12 +50,10 @@ def __init__(
@property
def expression(self) -> str:
- lines: List[str] = [f'Then run validator {self.name}_{self.implementation.__name__}']
+ lines: list[str] = [f'Then run validator {self.name}_{self.implementation.__name__}']
if self.table is not None and len(self.table) > 0:
- lines.append(f' | {" | ".join([key for key in self.table[0].keys()])} |')
-
- for row in self.table:
- lines.append(f' | {" | ".join([value for value in row.values()])} |')
+ lines.append(f' | {" | ".join(list(self.table[0].keys()))} |')
+ lines.extend(f' | {" | ".join(list(row.values()))} |' for row in self.table)
return '\n'.join(lines)
@@ -62,22 +63,22 @@ def impl(self) -> str:
source_lines[0] = dedent(source_lines[0].replace('def ', f'def {self.name}_'))
source = '\n'.join(source_lines)
- return f'''@then(u'run validator {self.name}_{self.implementation.__name__}')
+ return f"""@then(u'run validator {self.name}_{self.implementation.__name__}')
def {self.name}_{self.implementation.__name__}_wrapper(context: Context) -> None:
{dedent(source)}
if on_local(context) or on_worker(context):
{self.name}_{self.implementation.__name__}(context)
-'''
+"""
class End2EndFixture:
_tmp_path_factory: TempPathFactory
- _env: Dict[str, str]
- _validators: Dict[Optional[str], List[End2EndValidator]]
+ _env: dict[str, str]
+ _validators: dict[Optional[str], list[End2EndValidator]]
_distributed: bool
- _after_features: Dict[str, Callable[[Context, Feature], None]]
- _before_features: Dict[str, Callable[[Context, Feature], None]]
+ _after_features: dict[str, Callable[[Context, Feature], None]]
+ _before_features: dict[str, Callable[[Context, Feature], None]]
_root: Optional[Path]
_port: Optional[int] = None
@@ -87,9 +88,9 @@ class End2EndFixture:
profile: Optional[Profile]
- def __init__(self, tmp_path_factory: TempPathFactory, distributed: bool) -> None:
+ def __init__(self, tmp_path_factory: TempPathFactory, *, distributed: bool) -> None:
self._tmp_path_factory = tmp_path_factory
- self.cwd = Path(getcwd())
+ self.cwd = Path.cwd()
self._env = {}
self._validators = {}
self._root = None
@@ -101,17 +102,19 @@ def __init__(self, tmp_path_factory: TempPathFactory, distributed: bool) -> None
@property
def mode_root(self) -> Path:
if self._root is None:
- raise AttributeError('root is not set')
+ message = 'root is not set'
+ raise AttributeError(message)
if self._distributed:
return Path('/srv/grizzly')
- else:
- return self._root
+
+ return self._root
@property
def root(self) -> Path:
if self._root is None:
- raise AttributeError('root is not set')
+ message = 'root is not set'
+ raise AttributeError(message)
return self._root
@@ -128,10 +131,7 @@ def webserver_port(self) -> int:
@property
def host(self) -> str:
- if self._distributed:
- host = 'master'
- else:
- host = 'localhost'
+ host = 'master' if self._distributed else 'localhost'
return f'{host}:{self.webserver_port}'
@@ -139,7 +139,7 @@ def find_free_port(self) -> int:
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
sock.bind(('', 0))
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- return cast(int, sock.getsockname()[1])
+ return cast('int', sock.getsockname()[1])
def inject_webserver_module(self, path: Path) -> None:
assert self._tmp_path_factory._basetemp is not None
@@ -150,7 +150,7 @@ def inject_webserver_module(self, path: Path) -> None:
webserver_destination.write_text(webserver_source.read_text())
def start_webserver_step_impl(self, port: int) -> str:
- return f'''
+ return f"""
@then(u'start webserver on master port "{port}"')
def step_start_webserver(context: Context) -> None:
@@ -166,9 +166,9 @@ def step_start_webserver(context: Context) -> None:
webserver = webserver_module.Webserver({port})
webserver.start()
-'''
+"""
- def __enter__(self) -> 'End2EndFixture':
+ def __enter__(self) -> Self:
if environ.get('PROFILE', None) is not None:
self.profile = Profile()
self.profile.enable()
@@ -184,7 +184,7 @@ def __enter__(self) -> 'End2EndFixture':
# create virtualenv
rc, output = run_command(
[sys.executable, '-m', 'venv', virtual_env_path.name],
- cwd=str(self.root),
+ cwd=self.root,
)
try:
@@ -202,7 +202,7 @@ def __enter__(self) -> 'End2EndFixture':
virtual_env_bin_dir = 'bin'
self._env.update({
- 'PATH': f'{str(virtual_env_path / virtual_env_bin_dir)}{pathsep}{path}',
+ 'PATH': Path.joinpath(virtual_env_path, virtual_env_bin_dir, path).as_posix(),
'VIRTUAL_ENV': str(virtual_env_path),
'PYTHONPATH': environ.get('PYTHONPATH', '.'),
'HOME': environ.get('HOME', '/'),
@@ -225,7 +225,7 @@ def __enter__(self) -> 'End2EndFixture':
# python 3.6.x is vendord with pip 18.x, which is too old!
rc, output = run_command(
['python', '-m', 'pip', 'install', '--upgrade', 'pip'],
- cwd=str(Path.cwd()),
+ cwd=Path.cwd(),
env=self._env,
)
@@ -237,7 +237,7 @@ def __enter__(self) -> 'End2EndFixture':
rc, output = run_command(
['python', '-m', 'pip', 'install', '.'],
- cwd=str(Path.cwd()),
+ cwd=Path.cwd(),
env=self._env,
)
@@ -251,7 +251,7 @@ def __enter__(self) -> 'End2EndFixture':
def __exit__(
self,
- exc_type: Optional[Type[BaseException]],
+ exc_type: Optional[type[BaseException]],
exc: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Literal[True]:
@@ -260,10 +260,8 @@ def __exit__(
if exc is None:
if environ.get('KEEP_FILES', None) is None:
- try:
+ with suppress(AttributeError):
rm_rf(self.root)
- except AttributeError:
- pass
else:
print(self._root)
@@ -277,7 +275,7 @@ def add_validator(
self,
implementation: Callable[[Context], None],
scenario: Optional[str] = None,
- table: Optional[List[Dict[str, str]]] = None,
+ table: Optional[list[dict[str, str]]] = None,
) -> None:
callee = inspect.stack()[1].function
@@ -294,22 +292,10 @@ def add_before_feature(self, implementation: Callable[[Context, Feature], None])
callee = inspect.stack()[1].function
self._before_features[callee] = implementation
- def create_feature(self, contents: str, name: Optional[str] = None, identifier: Optional[str] = None) -> str:
- if name is None:
- name = inspect.stack()[1].function
-
- if identifier is not None:
- identifier = sha1(identifier.encode()).hexdigest()[:8]
- name = f'{name}_{identifier}'
-
- feature_lines = contents.strip().split('\n')
- feature_lines[0] = f'Feature: {name}'
- steps_file = self.root / 'features' / 'steps' / 'steps.py'
- environment_file = self.root / 'features' / 'environment.py'
-
+ def modify_feature(self, feature_lines: list[str]) -> list[str]:
scenario: Optional[str] = None
indentation = ' '
- modified_feature_lines: List[str] = []
+ modified_feature_lines: list[str] = []
offset = 0 # number of added steps
for nr, line in enumerate(feature_lines):
@@ -323,9 +309,8 @@ def create_feature(self, contents: str, name: Optional[str] = None, identifier:
validators = self._validators.get(scenario, self._validators.get(None, None))
if validators is not None:
for validator in validators:
- nr += offset
validator_expression = indent(f'{validator.expression}', prefix=indentation * 2)
- index = nr
+ index = nr + offset
while modified_feature_lines[index].strip() == '' or 'Scenario:' in modified_feature_lines[index]:
index -= 1
@@ -340,21 +325,38 @@ def create_feature(self, contents: str, name: Optional[str] = None, identifier:
modified_feature_lines.append('')
+ return modified_feature_lines
+
+ def create_feature(self, contents: str, name: Optional[str] = None, identifier: Optional[str] = None) -> str:
+ if name is None:
+ name = inspect.stack()[1].function
+
+ if identifier is not None:
+ identifier = sha1(identifier.encode()).hexdigest()[:8] # noqa: S324
+ name = f'{name}_{identifier}'
+
+ feature_lines = contents.strip().split('\n')
+ feature_lines[0] = f'Feature: {name}'
+ steps_file = self.root / 'features' / 'steps' / 'steps.py'
+ environment_file = self.root / 'features' / 'environment.py'
+
+ modified_feature_lines = self.modify_feature(feature_lines)
+
contents = '\n'.join(modified_feature_lines)
# write feature file
- with open(self.root / 'features' / f'{name}.feature', 'w+') as fd:
+ with (self.root / 'features' / f'{name}.feature').open('w+') as fd:
fd.write(contents)
feature_file_name = fd.name.replace(f'{self.root}/', '')
# cache current step implementations
- with open(steps_file, 'r') as fd:
+ with steps_file.open('r') as fd:
steps_impl = fd.read()
# add step implementations
- with open(steps_file, 'a') as fd:
- added_validators: List[str] = []
+ with steps_file.open('a') as fd:
+ added_validators: list[str] = []
for validators in self._validators.values():
for validator in validators:
# write expression and step implementation to steps/steps.py
@@ -365,19 +367,19 @@ def create_feature(self, contents: str, name: Optional[str] = None, identifier:
added_validators = []
# add after_feature hook, always write all of 'em
- with open(environment_file, 'w') as fd:
+ with environment_file.open('w') as fd:
fd.write('from typing import Any, Tuple, Dict, cast\n\n')
fd.write('from behave.runner import Context\n')
fd.write('from behave.model import Feature\n')
fd.write('from grizzly.context import GrizzlyContext\n')
- fd.write((
+ fd.write(
'from grizzly.environment import before_feature as grizzly_before_feature, '
- 'after_feature as grizzly_after_feature, before_scenario, after_scenario, before_step\n\n'
- ))
+ 'after_feature as grizzly_after_feature, before_scenario, after_scenario, before_step\n\n',
+ )
fd.write('def before_feature(context: Context, feature: Feature, *args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> None:\n')
if len(self._before_features) > 0:
- for feature_name in self._before_features.keys():
+ for feature_name in self._before_features:
fd.write(f' if feature.name == "{feature_name}":\n')
fd.write(f' {feature_name}_before_feature(context, feature)\n\n')
fd.write(' grizzly_before_feature(context, feature)\n\n')
@@ -392,7 +394,7 @@ def create_feature(self, contents: str, name: Optional[str] = None, identifier:
fd.write('def after_feature(context: Context, feature: Feature, *args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> None:\n')
fd.write(' grizzly_after_feature(context, feature)\n\n')
if len(self._after_features) > 0:
- for feature_name in self._after_features.keys():
+ for feature_name in self._after_features:
fd.write(f' if feature.name == "{feature_name}":\n')
fd.write(f' {feature_name}_after_feature(context, feature)\n\n')
@@ -410,26 +412,27 @@ def create_feature(self, contents: str, name: Optional[str] = None, identifier:
def execute(
self,
- feature_file: str,
+ feature_file: Path,
env_conf_file: Optional[str] = None,
- testdata: Optional[Dict[str, str]] = None,
- cwd: Optional[str] = None,
- arguments: Optional[List[str]] = None,
- ) -> Tuple[int, List[str]]:
+ testdata: Optional[dict[str, str]] = None,
+ cwd: Optional[Path] = None,
+ arguments: Optional[list[str]] = None,
+ ) -> tuple[int, list[str]]:
if arguments is None:
arguments = []
- command = [
+
+ command: list[str] = [
'grizzly-cli',
self.mode,
'run',
*arguments,
'--yes',
'--verbose',
- feature_file,
+ feature_file.as_posix(),
]
if self._distributed:
- command = command[:2] + ['--project-name', self.root.name] + command[2:]
+ command = [*command[:2], '--project-name', self.root.name, *command[2:]]
if env_conf_file is not None:
command += ['-e', env_conf_file]
@@ -440,7 +443,7 @@ def execute(
rc, output = run_command(
command,
- cwd=cwd or str(self.mode_root),
+ cwd=cwd or self.mode_root,
env=self._env,
)
@@ -454,7 +457,7 @@ def execute(
command = ['docker', 'container', 'logs', f'{self.root.name}-{getuser()}_{container}_1']
_, output = run_command(
command,
- cwd=str(self.root),
+ cwd=self.root,
env=self._env,
)
diff --git a/tests/helpers.py b/tests/helpers.py
index 39e5b20..9df841d 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -1,11 +1,12 @@
-import subprocess
+from __future__ import annotations
+
import os
+import subprocess
import sys
-
from abc import ABCMeta
-from typing import Any, Dict, Optional, Tuple, List, Generator, cast
+from contextlib import contextmanager, suppress
from pathlib import Path
-from contextlib import contextmanager
+from typing import TYPE_CHECKING, Any, Optional
from behave.model import Scenario, Step
from setuptools_scm import Configuration as SetuptoolsScmConfiguration
@@ -13,11 +14,16 @@
from grizzly_cli.utils import rm_rf
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
__all__ = ['rm_rf']
-def CaseInsensitive(value: str) -> object:
+def CaseInsensitive(value: str) -> object: # noqa: N802
class Wrapped(str):
+ __slots__ = ()
+
def __eq__(self, other: object) -> bool:
return isinstance(other, str) and other.lower() == value.lower()
@@ -27,16 +33,19 @@ def __ne__(self, other: object) -> bool:
def __neq__(self, other: object) -> bool:
return self.__ne__(other)
+ def __hash__(self) -> int:
+ return hash(self)
+
return Wrapped()
-def run_command(command: List[str], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None, stdin: Optional[str] = None) -> Tuple[int, List[str]]:
- output: List[str] = []
+def run_command(command: list[str], env: Optional[dict[str, str]] = None, cwd: Optional[Path] = None, stdin: Optional[str] = None) -> tuple[int, list[str]]:
+ output: list[str] = []
if env is None:
env = os.environ.copy()
if cwd is None:
- cwd = os.getcwd()
+ cwd = Path.cwd()
process = subprocess.Popen(
command,
@@ -49,7 +58,7 @@ def run_command(command: List[str], env: Optional[Dict[str, str]] = None, cwd: O
if stdin is not None:
assert process.stdin is not None
- process.stdin.write(f'{stdin}\n'.encode('utf-8'))
+ process.stdin.write(f'{stdin}\n'.encode())
process.stdin.close()
try:
@@ -72,30 +81,28 @@ def run_command(command: List[str], env: Optional[Dict[str, str]] = None, cwd: O
except KeyboardInterrupt:
pass
finally:
- try:
+ with suppress(Exception):
process.kill()
- except Exception:
- pass
process.wait()
return process.returncode, output
-def create_scenario(name: str, background_steps: List[str], steps: List[str]) -> Scenario:
+def create_scenario(name: str, background_steps: list[str], steps: list[str]) -> Scenario:
scenario = Scenario('', '', '', name)
for background_step in background_steps:
[keyword, name] = background_step.split(' ', 1)
- step = Step('', '', keyword.strip(), keyword.strip(), name.strip())
+ behave_step = Step('', '', keyword.strip(), keyword.strip(), name.strip())
if scenario._background_steps is None:
scenario._background_steps = []
- scenario._background_steps.append(step)
+ scenario._background_steps.append(behave_step)
for step in steps:
[keyword, name] = step.split(' ', 1)
- step = Step('', '', keyword.strip(), keyword.strip(), name.strip())
- scenario.steps.append(step)
+ behave_step = Step('', '', keyword.strip(), keyword.strip(), name.strip())
+ scenario.steps.append(behave_step)
return scenario
@@ -103,11 +110,11 @@ def create_scenario(name: str, background_steps: List[str], steps: List[str]) ->
def get_current_version() -> str:
root = (Path(__file__).parent / '..').resolve()
- version = setuptools_scm_get_version(SetuptoolsScmConfiguration.from_file(str(root / 'pyproject.toml'), str(root)), True)
+ version = setuptools_scm_get_version(SetuptoolsScmConfiguration.from_file(str(root / 'pyproject.toml'), root.as_posix()), force_write_version_files=True)
- assert version is not None, f'setuptools-scm was not able to get current version for {str(root)}'
+ assert version is not None, f'setuptools-scm was not able to get current version for {root.as_posix()}'
- return cast(str, version)
+ return version # type: ignore[no-any-return]
@contextmanager
@@ -145,6 +152,9 @@ def __repr__(self) -> str:
return ''.join(representation)
+ def __hash__(self) -> int:
+ return hash(self)
+
for c in cls:
WrappedAny.register(c)
@@ -173,6 +183,9 @@ def __repr__(self) -> str:
info = ', '.join([f"{key}={value}" for key, value in values.items()])
return f''
+ def __hash__(self) -> int:
+ return hash(self)
+
if len(value) > 0 and len(values) > 0:
message = 'cannot use both positional and named arguments'
raise RuntimeError(message)
diff --git a/tests/unit/argparse/bashcompletion/test__init__.py b/tests/unit/argparse/bashcompletion/test__init__.py
index 235f51d..07b97cc 100644
--- a/tests/unit/argparse/bashcompletion/test__init__.py
+++ b/tests/unit/argparse/bashcompletion/test__init__.py
@@ -1,23 +1,24 @@
+from __future__ import annotations
+
import argparse
import inspect
-
-from typing import Optional, Generator
-from os import path, chdir, getcwd
+from os.path import sep
+from pathlib import Path
+from typing import TYPE_CHECKING, Optional
import pytest
-from pytest_mock import MockerFixture
-from _pytest.capture import CaptureFixture, CaptureResult
-from _pytest.tmpdir import TempPathFactory
-
-from grizzly_cli.argparse.bashcompletion import BashCompleteAction, BashCompletionAction, hook
-from grizzly_cli.argparse import ArgumentParser
from grizzly_cli.__main__ import _create_parser
+from grizzly_cli.argparse import ArgumentParser
+from grizzly_cli.argparse.bashcompletion import BashCompleteAction, BashCompletionAction, hook
+from tests.helpers import cwd, rm_rf
-from tests.helpers import rm_rf
-
+if TYPE_CHECKING: # pragma: no cover
+ from collections.abc import Generator
-CWD = getcwd()
+ from _pytest.capture import CaptureFixture, CaptureResult
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
@pytest.fixture
@@ -74,11 +75,10 @@ def test_file_structure(tmp_path_factory: TempPathFactory) -> Generator[str, Non
file = hidden_dir / 'hidden.txt'
file.write_text('hidden.txt file')
- chdir(test_context)
try:
- yield str(test_context)
+ with cwd(test_context):
+ yield str(test_context)
finally:
- chdir(CWD)
rm_rf(test_context)
@@ -98,12 +98,12 @@ def test___call__(self, capsys: CaptureFixture) -> None:
with pytest.raises(SystemExit) as e:
action(parser, parser.parse_args([]), None)
- assert e.type == SystemExit
+ assert e.type is SystemExit
assert e.value.code == 0
- bash_script_path = path.join(path.dirname(inspect.getfile(action.__class__)), 'bashcompletion.bash')
+ bash_script_path = Path(inspect.getfile(action.__class__)).parent / 'bashcompletion.bash'
- with open(bash_script_path, encoding='utf-8') as fd:
+ with bash_script_path.open(encoding='utf-8') as fd:
bash_script = fd.read().replace('bashcompletion_template', parser.prog) + '\n'
capture = capsys.readouterr()
@@ -173,34 +173,34 @@ def test_remove_completed(self, test_parser: ArgumentParser) -> None:
suggestions = action.get_suggestions(test_parser)
all_suggestions = suggestions.copy()
- all_options_sorted = sorted(list(all_suggestions.keys()))
+ all_options_sorted = sorted(all_suggestions.keys())
exclusive_suggestions = action.get_exclusive_suggestions(test_parser)
assert action.remove_completed([], suggestions, exclusive_suggestions) == []
- assert sorted(list(suggestions.keys())) == all_options_sorted
+ assert sorted(suggestions.keys()) == all_options_sorted
assert action.remove_completed(['--verbose'], suggestions, exclusive_suggestions) == ['--verbose']
- assert sorted(list(suggestions.keys())) == all_options_sorted
+ assert sorted(suggestions.keys()) == all_options_sorted
assert action.remove_completed(['--verbose', '--file'], suggestions, exclusive_suggestions) == ['--file']
- assert sorted(list(suggestions.keys()) + ['--verbose']) == all_options_sorted
+ assert sorted([*suggestions.keys(), '--verbose']) == all_options_sorted
assert action.remove_completed(['--verbose', '--file', 'test.txt'], suggestions, exclusive_suggestions) == []
- assert sorted(list(suggestions.keys()) + ['--verbose']) == all_options_sorted
+ assert sorted([*suggestions.keys(), '--verbose']) == all_options_sorted
assert action.remove_completed(['--verbose', '--file', 'test.txt', 'a'], suggestions, exclusive_suggestions) == ['a']
- assert sorted(list(suggestions.keys()) + ['--verbose']) == all_options_sorted
+ assert sorted([*suggestions.keys(), '--verbose']) == all_options_sorted
# if subparsers are completed, then we move to another parser, with its own arguments
assert action.remove_completed(['--verbose', '--file', 'test.txt', '--value'], suggestions, exclusive_suggestions) == ['--value']
- assert sorted(list(suggestions.keys()) + ['--verbose']) == all_options_sorted
+ assert sorted([*suggestions.keys(), '--verbose']) == all_options_sorted
assert action.remove_completed(['--verbose', '--file', 'test.txt', '--value', '8'], suggestions, exclusive_suggestions) == ['--value', '8']
- assert sorted(list(suggestions.keys()) + ['--verbose']) == all_options_sorted
+ assert sorted([*suggestions.keys(), '--verbose']) == all_options_sorted
# only one of --foo, --bar, --test is valid (mutually exclusive), so all should be removed from suggestions if one of them is specified
assert action.remove_completed(['--verbose', '--file', 'test.txt', '--value', '8', '--foo'], suggestions, exclusive_suggestions) == []
- assert sorted(list(suggestions.keys()) + ['--verbose', '--value', '--foo', '--bar', '--test']) == all_options_sorted
+ assert sorted([*suggestions.keys(), '--verbose', '--value', '--foo', '--bar', '--test']) == all_options_sorted
def test_filter_suggestions(self, test_parser: ArgumentParser) -> None:
action = BashCompleteAction(['--bash-complete'])
@@ -209,30 +209,30 @@ def test_filter_suggestions(self, test_parser: ArgumentParser) -> None:
all_suggestions = suggestions.copy()
assert action.filter_suggestions([], suggestions) == all_suggestions
- assert sorted(list(action.filter_suggestions(['--'], suggestions).keys())) == sorted(['--help', '--test', '--foo', '--bar', '--value', '--verbose', '--file'])
- assert sorted(list(action.filter_suggestions(['--v'], suggestions).keys())) == sorted(['--verbose', '--value'])
- assert sorted(list(action.filter_suggestions(['--f'], suggestions).keys())) == sorted(['--file', '--foo'])
+ assert sorted(action.filter_suggestions(['--'], suggestions).keys()) == sorted(['--help', '--test', '--foo', '--bar', '--value', '--verbose', '--file'])
+ assert sorted(action.filter_suggestions(['--v'], suggestions).keys()) == sorted(['--verbose', '--value'])
+ assert sorted(action.filter_suggestions(['--f'], suggestions).keys()) == sorted(['--file', '--foo'])
@pytest.mark.parametrize(
- 'input,expected',
+ ('command', 'expected'),
[
- ('grizzly-cli ', '-h\n--help\n--version\ninit\nkeyvault\nlocal\ndist\nauth',),
+ ('grizzly-cli ', '-h\n--help\n--version\ninit\nkeyvault\nlocal\ndist\nauth'),
('grizzly-cli -', '-h\n--help\n--version'),
('grizzly-cli --', '--help\n--version'),
('grizzly-cli lo', 'local'),
('grizzly-cli -h', ''),
- ]
+ ],
)
- def test___call__(self, input: str, expected: str, capsys: CaptureFixture) -> None:
+ def test___call__(self, command: str, expected: str, capsys: CaptureFixture) -> None:
parser = _create_parser()
with pytest.raises(SystemExit):
- parser.parse_args([f'--bash-complete={input}'])
+ parser.parse_args([f'--bash-complete={command}'])
capture = capsys.readouterr()
assert sorted(capture.out.split('\n')) == sorted(f'{expected}\n'.split('\n'))
@pytest.mark.parametrize(
- 'input,expected', [
+ ('command', 'expected'), [
(
'grizzly-cli local run ',
(
@@ -291,21 +291,21 @@ def test___call__(self, input: str, expected: str, capsys: CaptureFixture) -> No
),
('grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test', 'test.feature\ntest-dir'),
('grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test-dir', 'test-dir'),
- (f'grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test-dir{path.sep}', f'test-dir{path.sep}test.feature'),
+ (f'grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test-dir{sep}', f'test-dir{sep}test.feature'),
(
- f'grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test-dir{path.sep}tes',
- f'test-dir{path.sep}test.feature',
+ f'grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test-dir{sep}tes',
+ f'test-dir{sep}test.feature',
),
(
- f'grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test-dir{path.sep}test.feature',
+ f'grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test-dir{sep}test.feature',
'-h\n--help\n--verbose\n-T\n--testdata-variable\n--csv-prefix\n--csv-interval\n--csv-flush-interval\n-l\n--log-file\n--log-dir\n--dump\n--dry-run',
),
('grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test.fe', 'test.feature'),
('grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value --help', ''),
('grizzly-cli local run --yes -T key=value --environment-file test.yaml --testdata-variable key=value --help d', ''),
- ]
+ ],
)
- def test___call___local_run(self, input: str, expected: str, capsys: CaptureFixture, test_file_structure: None) -> None:
+ def test___call___local_run(self, command: str, expected: str, capsys: CaptureFixture, test_file_structure: str) -> None: # noqa: ARG002
capture: Optional[CaptureResult] = None
try:
@@ -313,10 +313,11 @@ def test___call___local_run(self, input: str, expected: str, capsys: CaptureFixt
hook(parser)
_subparsers = getattr(parser, '_subparsers', None)
assert _subparsers is not None
- subparser: Optional[argparse.ArgumentParser]
+ subparser: Optional[argparse.ArgumentParser] = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'local':
+ subparser = possible_subparser
break
assert subparser is not None
@@ -324,29 +325,29 @@ def test___call___local_run(self, input: str, expected: str, capsys: CaptureFixt
_subparsers = getattr(subparser, '_subparsers', None)
assert _subparsers is not None
+ subparser = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'run':
+ subparser = possible_subparser
break
assert subparser is not None
assert subparser.prog == 'grizzly-cli local run'
with pytest.raises(SystemExit):
- subparser.parse_args([f'--bash-complete={input}'])
+ subparser.parse_args([f'--bash-complete={command}'])
capture = capsys.readouterr()
assert sorted(capture.out.split('\n')) == sorted(f'{expected}\n'.split('\n'))
except:
- print(f'input={input}')
+ print(f'input={command}')
print(f'expected={expected}')
if capture is not None:
print(f'actual={capture.out}')
raise
- finally:
- chdir(CWD)
@pytest.mark.parametrize(
- 'input,expected', [
+ ('command', 'expected'), [
(
'grizzly-cli dist run ',
(
@@ -370,7 +371,7 @@ def test___call___local_run(self, input: str, expected: str, capsys: CaptureFixt
(
'-h\n--help\n--verbose\n-T\n--testdata-variable\n-e\n--environment-file\n--csv-prefix\n--csv-interval\n--csv-flush-interval\n'
'-l\n--log-file\n--log-dir\n--dump\n--dry-run'
- )
+ ),
),
('grizzly-cli dist run --help --yes', ''),
('grizzly-cli dist run --yes -T', ''),
@@ -407,9 +408,9 @@ def test___call___local_run(self, input: str, expected: str, capsys: CaptureFixt
('grizzly-cli dist run --yes -T key=value --environment-file test.yaml --testdata-variable key=value test.fe', 'test.feature'),
('grizzly-cli dist run --yes -T key=value --environment-file test.yaml --testdata-variable key=value --help', ''),
('grizzly-cli dist run --yes -T key=value --environment-file test.yaml --testdata-variable key=value --help d', ''),
- ]
+ ],
)
- def test___call___dist_run(self, input: str, expected: str, capsys: CaptureFixture, test_file_structure: None) -> None:
+ def test___call___dist_run(self, command: str, expected: str, capsys: CaptureFixture, test_file_structure: str) -> None: # noqa: ARG002
capture: Optional[CaptureResult] = None
try:
@@ -417,10 +418,11 @@ def test___call___dist_run(self, input: str, expected: str, capsys: CaptureFixtu
hook(parser)
_subparsers = getattr(parser, '_subparsers', None)
assert _subparsers is not None
- subparser: Optional[argparse.ArgumentParser]
+ subparser: Optional[argparse.ArgumentParser] = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'dist':
+ subparser = possible_subparser
break
assert subparser is not None
@@ -428,36 +430,36 @@ def test___call___dist_run(self, input: str, expected: str, capsys: CaptureFixtu
_subparsers = getattr(subparser, '_subparsers', None)
assert _subparsers is not None
+ subparser = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'run':
+ subparser = possible_subparser
break
assert subparser is not None
assert subparser.prog == 'grizzly-cli dist run'
with pytest.raises(SystemExit):
- subparser.parse_args([f'--bash-complete={input}'])
+ subparser.parse_args([f'--bash-complete={command}'])
capture = capsys.readouterr()
assert sorted(capture.out.split('\n')) == sorted(f'{expected}\n'.split('\n'))
except:
- print(f'input={input}')
+ print(f'input={command}')
print(f'expected={expected}')
if capture is not None:
print(f'actual={capture.out}')
raise
- finally:
- chdir(CWD)
@pytest.mark.parametrize(
- 'input,expected',
+ ('command', 'expected'),
[
(
'grizzly-cli dist',
(
'-h\n--help\n--workers\n--id\n--limit-nofile\n--health-retries\n--health-timeout\n--health-interval\n--registry\n'
'--tty\n--wait-for-worker\n--project-name\n--force-build\n--build\n--validate-config\nbuild\nclean\nrun'
- )
+ ),
),
(
'grizzly-cli dist -',
@@ -497,7 +499,7 @@ def test___call___dist_run(self, input: str, expected: str, capsys: CaptureFixtu
),
],
)
- def test___call__dist(self, input: str, expected: str, capsys: CaptureFixture, test_file_structure: None) -> None:
+ def test___call__dist(self, command: str, expected: str, capsys: CaptureFixture, test_file_structure: str) -> None: # noqa: ARG002
capture: Optional[CaptureResult] = None
try:
@@ -505,30 +507,29 @@ def test___call__dist(self, input: str, expected: str, capsys: CaptureFixture, t
hook(parser)
_subparsers = getattr(parser, '_subparsers', None)
assert _subparsers is not None
- subparser: Optional[argparse.ArgumentParser]
+ subparser: Optional[argparse.ArgumentParser] = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'dist':
+ subparser = possible_subparser
break
assert subparser is not None
assert subparser.prog == 'grizzly-cli dist'
with pytest.raises(SystemExit):
- subparser.parse_args([f'--bash-complete={input}'])
+ subparser.parse_args([f'--bash-complete={command}'])
capture = capsys.readouterr()
assert sorted(capture.out.split('\n')) == sorted(f'{expected}\n'.split('\n'))
except:
- print(f'input={input}')
+ print(f'input={command}')
print(f'expected={expected}')
if capture is not None:
print(f'actual={capture.out}')
raise
- finally:
- chdir(CWD)
@pytest.mark.parametrize(
- 'input,expected',
+ ('command', 'expected'),
[
(
'grizzly-cli dist build',
@@ -556,7 +557,7 @@ def test___call__dist(self, input: str, expected: str, capsys: CaptureFixture, t
),
],
)
- def test___call__dist_build(self, input: str, expected: str, capsys: CaptureFixture, test_file_structure: None) -> None:
+ def test___call__dist_build(self, command: str, expected: str, capsys: CaptureFixture, test_file_structure: str) -> None: # noqa: ARG002
capture: Optional[CaptureResult] = None
try:
@@ -564,10 +565,11 @@ def test___call__dist_build(self, input: str, expected: str, capsys: CaptureFixt
hook(parser)
_subparsers = getattr(parser, '_subparsers', None)
assert _subparsers is not None
- subparser: Optional[argparse.ArgumentParser]
+ subparser: Optional[argparse.ArgumentParser] = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'dist':
+ subparser = possible_subparser
break
assert subparser is not None
@@ -575,29 +577,29 @@ def test___call__dist_build(self, input: str, expected: str, capsys: CaptureFixt
_subparsers = getattr(subparser, '_subparsers', None)
assert _subparsers is not None
+ subparser = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'build':
+ subparser = possible_subparser
break
assert subparser is not None
assert subparser.prog == 'grizzly-cli dist build'
with pytest.raises(SystemExit):
- subparser.parse_args([f'--bash-complete={input}'])
+ subparser.parse_args([f'--bash-complete={command}'])
capture = capsys.readouterr()
assert sorted(capture.out.split('\n')) == sorted(f'{expected}\n'.split('\n'))
except:
- print(f'input={input}')
+ print(f'input={command}')
print(f'expected={expected}')
if capture is not None:
print(f'actual={capture.out}')
raise
- finally:
- chdir(CWD)
@pytest.mark.parametrize(
- 'input,expected',
+ ('command', 'expected'),
[
(
'grizzly-cli dist clean',
@@ -609,7 +611,7 @@ def test___call__dist_build(self, input: str, expected: str, capsys: CaptureFixt
),
],
)
- def test___call__dist_clean(self, input: str, expected: str, capsys: CaptureFixture, test_file_structure: None) -> None:
+ def test___call__dist_clean(self, command: str, expected: str, capsys: CaptureFixture, test_file_structure: str) -> None: # noqa: ARG002
capture: Optional[CaptureResult] = None
try:
@@ -617,10 +619,11 @@ def test___call__dist_clean(self, input: str, expected: str, capsys: CaptureFixt
hook(parser)
_subparsers = getattr(parser, '_subparsers', None)
assert _subparsers is not None
- subparser: Optional[argparse.ArgumentParser]
+ subparser: Optional[argparse.ArgumentParser] = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'dist':
+ subparser = possible_subparser
break
assert subparser is not None
@@ -628,26 +631,26 @@ def test___call__dist_clean(self, input: str, expected: str, capsys: CaptureFixt
_subparsers = getattr(subparser, '_subparsers', None)
assert _subparsers is not None
+ subparser = None
for subparsers in _subparsers._group_actions:
- for name, subparser in subparsers.choices.items():
+ for name, possible_subparser in subparsers.choices.items():
if name == 'clean':
+ subparser = possible_subparser
break
assert subparser is not None
assert subparser.prog == 'grizzly-cli dist clean'
with pytest.raises(SystemExit):
- subparser.parse_args([f'--bash-complete={input}'])
+ subparser.parse_args([f'--bash-complete={command}'])
capture = capsys.readouterr()
assert sorted(capture.out.split('\n')) == sorted(f'{expected}\n'.split('\n'))
except:
- print(f'input={input}')
+ print(f'input={command}')
print(f'expected={expected}')
if capture is not None:
print(f'actual={capture.out}')
raise
- finally:
- chdir(CWD)
def test_hook(mocker: MockerFixture) -> None:
diff --git a/tests/unit/argparse/bashcompletion/test_types.py b/tests/unit/argparse/bashcompletion/test_types.py
index 0540b87..893f29a 100644
--- a/tests/unit/argparse/bashcompletion/test_types.py
+++ b/tests/unit/argparse/bashcompletion/test_types.py
@@ -1,16 +1,16 @@
-from os import chdir, getcwd, sep
+from __future__ import annotations
+
from argparse import ArgumentTypeError
+from os import sep
+from typing import TYPE_CHECKING
import pytest
-from _pytest.tmpdir import TempPathFactory
-
from grizzly_cli.argparse.bashcompletion.types import BashCompletionTypes
+from tests.helpers import cwd, rm_rf
-from tests.helpers import rm_rf
-
-
-CWD = getcwd()
+if TYPE_CHECKING:
+ from _pytest.tmpdir import TempPathFactory
class TestBashCompletionTypes:
@@ -35,38 +35,35 @@ def test___call__(self, tmp_path_factory: TempPathFactory) -> None:
file = test_context / 'test.xml'
file.touch()
file.write_text('test.xml file')
- test_context_root = str(test_context)
-
- chdir(test_context_root)
try:
- impl = BashCompletionTypes.File('*.txt')
+ with cwd(test_context):
+ impl = BashCompletionTypes.File('*.txt')
- with pytest.raises(ArgumentTypeError) as ate:
- impl('non-existing-directory/')
- assert 'non-existing-directory/ does not exist' in str(ate)
+ with pytest.raises(ArgumentTypeError) as ate:
+ impl('non-existing-directory/')
+ assert 'non-existing-directory/ does not exist' in str(ate)
- with pytest.raises(ArgumentTypeError) as ate:
- impl('test-dir/')
- assert 'test-dir/ is not a file' in str(ate)
+ with pytest.raises(ArgumentTypeError) as ate:
+ impl('test-dir/')
+ assert 'test-dir/ is not a file' in str(ate)
- with pytest.raises(ArgumentTypeError) as ate:
- impl('test.xml')
- assert 'test.xml does not match *.txt' in str(ate)
+ with pytest.raises(ArgumentTypeError) as ate:
+ impl('test.xml')
+ assert 'test.xml does not match *.txt' in str(ate)
- assert impl('test.txt') == 'test.txt'
+ assert impl('test.txt') == 'test.txt'
- impl = BashCompletionTypes.File('*.txt', '*.json')
+ impl = BashCompletionTypes.File('*.txt', '*.json')
- with pytest.raises(ArgumentTypeError) as ate:
- impl('test.xml')
- assert 'test.xml does not match *.txt' in str(ate)
+ with pytest.raises(ArgumentTypeError) as ate:
+ impl('test.xml')
+ assert 'test.xml does not match *.txt' in str(ate)
- assert impl('test.txt') == 'test.txt'
- assert impl('test.json') == 'test.json'
+ assert impl('test.txt') == 'test.txt'
+ assert impl('test.json') == 'test.json'
finally:
- chdir(CWD)
- rm_rf(test_context_root)
+ rm_rf(test_context)
def test_list_files(self, tmp_path_factory: TempPathFactory) -> None:
test_context = tmp_path_factory.mktemp('test_context')
@@ -81,36 +78,32 @@ def test_list_files(self, tmp_path_factory: TempPathFactory) -> None:
hidden_dir = test_context / '.hidden'
hidden_dir.mkdir()
(hidden_dir / 'hidden.txt').write_text('hidden.txt file')
- test_context_root = str(test_context)
-
- chdir(test_context_root)
-
try:
- impl = BashCompletionTypes.File('*.txt')
- assert impl.list_files(None) == {
- 'test.txt': 'file',
- 'test-dir': 'dir',
- }
- assert impl.list_files('te') == {
- 'test.txt': 'file',
- 'test-dir': 'dir',
- }
-
- assert impl.list_files('test-') == {
- 'test-dir': 'dir',
- }
-
- assert impl.list_files(f'test-dir{sep}') == {
- f'test-dir{sep}test.txt': 'file',
- }
-
- impl = BashCompletionTypes.File('*.txt', '*.json', '*.xml')
- assert impl.list_files('te') == {
- 'test.txt': 'file',
- 'test.json': 'file',
- 'test.xml': 'file',
- 'test-dir': 'dir',
- }
+ with cwd(test_context):
+ impl = BashCompletionTypes.File('*.txt')
+ assert impl.list_files(None) == {
+ 'test.txt': 'file',
+ 'test-dir': 'dir',
+ }
+ assert impl.list_files('te') == {
+ 'test.txt': 'file',
+ 'test-dir': 'dir',
+ }
+
+ assert impl.list_files('test-') == {
+ 'test-dir': 'dir',
+ }
+
+ assert impl.list_files(f'test-dir{sep}') == {
+ f'test-dir{sep}test.txt': 'file',
+ }
+
+ impl = BashCompletionTypes.File('*.txt', '*.json', '*.xml')
+ assert impl.list_files('te') == {
+ 'test.txt': 'file',
+ 'test.json': 'file',
+ 'test.xml': 'file',
+ 'test-dir': 'dir',
+ }
finally:
- chdir(CWD)
- rm_rf(test_context_root)
+ rm_rf(test_context)
diff --git a/tests/unit/argparse/test___init__.py b/tests/unit/argparse/test___init__.py
index 70590ce..5d67532 100644
--- a/tests/unit/argparse/test___init__.py
+++ b/tests/unit/argparse/test___init__.py
@@ -1,15 +1,20 @@
-from argparse import ArgumentError, ArgumentParser as CoreArgumentParser
-from typing import Tuple
-import pytest
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
-from _pytest.capture import CaptureFixture
+import pytest
from grizzly_cli.argparse import ArgumentParser
from grizzly_cli.argparse.markdown import MarkdownFormatter
+if TYPE_CHECKING:
+ from argparse import ArgumentParser as CoreArgumentParser
+
+ from _pytest.capture import CaptureFixture
+
@pytest.fixture
-def parsers() -> Tuple[CoreArgumentParser, ...]:
+def parsers() -> tuple[CoreArgumentParser, ...]:
parser = ArgumentParser(prog='test-prog', description='test parser', markdown_help=True, bash_completion=True)
parser.add_argument('--root-parser', type=str, default='root-parser', help='root parser argument')
@@ -76,21 +81,21 @@ def test_error_no_help(self, capsys: CaptureFixture) -> None:
parser = ArgumentParser(markdown_help=False, description='test parser', prog='test-prog')
with pytest.raises(SystemExit) as e:
parser.error_no_help('test test test')
- assert e.type == SystemExit
+ assert e.type is SystemExit
assert e.value.code == 2
capture = capsys.readouterr()
assert capture.err == 'test-prog: error: test test test\n'
assert capture.out == ''
- def test_print_help_HelpFormatter(self, capsys: CaptureFixture, parsers: Tuple[ArgumentParser, ...]) -> None:
+ def test_print_help_help_formatter(self, capsys: CaptureFixture, parsers: tuple[ArgumentParser, ...]) -> None:
parser, a_sub_parser, b_sub_parser = parsers
parser.markdown_help = False
parser.print_help()
capture = capsys.readouterr()
- assert capture.out == '''usage: test-prog [-h] [--root-parser ROOT_PARSER] {a,b} ...
+ assert capture.out == """usage: test-prog [-h] [--root-parser ROOT_PARSER] {a,b} ...
test parser
@@ -103,12 +108,12 @@ def test_print_help_HelpFormatter(self, capsys: CaptureFixture, parsers: Tuple[A
sub parser a
{a,b}
-'''
+"""
parser.markdown_help = True
parser.print_help()
capture = capsys.readouterr()
- assert capture.out == '''usage: test-prog [-h] [--root-parser ROOT_PARSER] {a,b} ...
+ assert capture.out == """usage: test-prog [-h] [--root-parser ROOT_PARSER] {a,b} ...
test parser
@@ -121,48 +126,48 @@ def test_print_help_HelpFormatter(self, capsys: CaptureFixture, parsers: Tuple[A
sub parser a
{a,b}
-'''
+"""
a_sub_parser.markdown_help = False
a_sub_parser.print_help()
capture = capsys.readouterr()
- assert capture.out == '''usage: test-prog a [-h] [--a-parser]
+ assert capture.out == """usage: test-prog a [-h] [--a-parser]
optional arguments:
-h, --help show this help message and exit
--a-parser a parser [argument](http://localhost)
-'''
+"""
a_sub_parser.markdown_help = True
a_sub_parser.print_help()
capture = capsys.readouterr()
- assert capture.out == '''usage: test-prog a [-h] [--a-parser]
+ assert capture.out == """usage: test-prog a [-h] [--a-parser]
optional arguments:
-h, --help show this help message and exit
--a-parser a parser argument
-'''
+"""
b_sub_parser.print_help()
capture = capsys.readouterr()
- assert capture.out == '''usage: test-prog b [-h] [--b-parser B_PARSER]
+ assert capture.out == """usage: test-prog b [-h] [--b-parser B_PARSER]
optional arguments:
-h, --help show this help message and exit
--b-parser B_PARSER b parser argument
-'''
+"""
- def test_print_help_MarkdownFormatter(self, capsys: CaptureFixture, parsers: Tuple[ArgumentParser, ...]) -> None:
+ def test_print_help_markdown_formatter(self, capsys: CaptureFixture, parsers: tuple[ArgumentParser, ...]) -> None:
parser, _, _ = parsers
parser.formatter_class = MarkdownFormatter.factory(0)
parser.print_help()
capture = capsys.readouterr()
- assert capture.out == '''# `test-prog`
+ assert capture.out == """# `test-prog`
### Usage
@@ -178,9 +183,9 @@ def test_print_help_MarkdownFormatter(self, capsys: CaptureFixture, parsers: Tup
## Subcommands
sub parser a
-'''
+"""
- def test_parse_args(self, parsers: Tuple[ArgumentParser, ...]) -> None:
+ def test_parse_args(self, parsers: tuple[ArgumentParser, ...]) -> None:
parser, a_sub_parser, b_sub_parser = parsers
parser.parse_args([])
@@ -197,24 +202,21 @@ def test_parse_args(self, parsers: Tuple[ArgumentParser, ...]) -> None:
all_option_strings = [option for action in b_sub_parser._actions for option in action.option_strings]
assert '--bash-complete' in all_option_strings
- try:
- parser.parse_args([])
- except ArgumentError as e:
- pytest.fail(str(e))
+ parser.parse_args([])
- def test_md_help_action_from_parser(self, capsys: CaptureFixture, parsers: Tuple[ArgumentParser, ...]) -> None:
+ def test_md_help_action_from_parser(self, capsys: CaptureFixture, parsers: tuple[ArgumentParser, ...]) -> None:
parser, _, _ = parsers
with pytest.raises(SystemExit) as e:
parser.parse_args(['--md-help'])
- assert e.type == SystemExit
+ assert e.type is SystemExit
assert e.value.code == 0
capture = capsys.readouterr()
print(capture)
- assert capture.out == '''# `test-prog`
+ assert capture.out == """# `test-prog`
test parser
@@ -260,4 +262,4 @@ def test_md_help_action_from_parser(self, capsys: CaptureFixture, parsers: Tuple
| argument | default | help |
| -------- | ------- | ---- |
| `--b-parser` | | b parser argument |
-'''
+"""
diff --git a/tests/unit/argparse/test_markdown.py b/tests/unit/argparse/test_markdown.py
index af60d97..2a8b148 100644
--- a/tests/unit/argparse/test_markdown.py
+++ b/tests/unit/argparse/test_markdown.py
@@ -1,14 +1,16 @@
-import argparse
+from __future__ import annotations
-from typing import cast
+import argparse
+from typing import TYPE_CHECKING
import pytest
-from pytest_mock import MockerFixture
-from _pytest.capture import CaptureFixture
-
from grizzly_cli.argparse.markdown import MarkdownFormatter, MarkdownHelpAction
+if TYPE_CHECKING:
+ from _pytest.capture import CaptureFixture
+ from pytest_mock import MockerFixture
+
class TestMarkdownHelpAction:
def test___init__(self) -> None:
@@ -28,7 +30,7 @@ def test___call__(self, mocker: MockerFixture) -> None:
with pytest.raises(SystemExit) as e:
parser.parse_args(['--md-help'])
- assert e.type == SystemExit
+ assert e.type is SystemExit
assert e.value.code == 0
assert print_help.call_count == 1
@@ -53,14 +55,14 @@ def test_print_help(self, mocker: MockerFixture) -> None:
action.print_help(parser)
assert print_help.call_count == 4
- assert issubclass(parser.formatter_class, MarkdownFormatter) # type: ignore
+ assert issubclass(parser.formatter_class, MarkdownFormatter) # type: ignore[arg-type]
assert parser._subparsers is not None
_subparsers = getattr(parser, '_subparsers', None)
assert _subparsers is not None
for subparsers in _subparsers._group_actions:
for name, subparser in subparsers.choices.items():
- assert issubclass(subparser.formatter_class, MarkdownFormatter) # type: ignore
+ assert issubclass(subparser.formatter_class, MarkdownFormatter) # type: ignore[arg-type]
if name == 'a':
_subsubparsers = getattr(subparser, '_subparsers', None)
assert _subsubparsers is not None
@@ -105,32 +107,32 @@ def test___init__(self) -> None:
def test__format_usage(self) -> None:
formatter = MarkdownFormatter.factory(0)('test')
usage = formatter._format_usage('test', None, None, 'a prefix')
- assert usage == '''
+ assert usage == """
### Usage
```bash
test
```
-'''
+"""
parser = argparse.ArgumentParser(prog='test', description='test parser')
parser.add_argument('-t', '--test', type=str, required=True, help='test argument')
parser.add_argument('file', nargs=1, help='file argument')
core_formatter = parser.formatter_class(prog=parser.prog)
- usage = core_formatter._format_usage(cast(str, parser.usage), parser._get_positional_actions(), parser._mutually_exclusive_groups, 'a prefix ')
- assert usage == '''a prefix test file
+ usage = core_formatter._format_usage(parser.usage, parser._get_positional_actions(), parser._mutually_exclusive_groups, 'a prefix ')
+ assert usage == """a prefix test file
-'''
+"""
usage = formatter._format_usage(parser.usage, parser._get_positional_actions(), parser._mutually_exclusive_groups, 'a prefix ')
- assert usage == '''
+ assert usage == """
### Usage
```bash
test file
```
-'''
+"""
def test_format_help(self) -> None:
formatter = MarkdownFormatter.factory(0)('test')
@@ -139,7 +141,7 @@ def test_format_help(self) -> None:
def test_format_text(self) -> None:
formatter = MarkdownFormatter('test-prog')
- text = '''%(prog)s is awesome!
+ text = """%(prog)s is awesome!
also, here is a sentence. and here is another one!
```bash
@@ -147,9 +149,9 @@ def test_format_text(self) -> None:
```
you cannot belive it, it's another sentence.
-'''
+"""
print(formatter._format_text(text))
- assert formatter._format_text(text) == '''test-prog is awesome!
+ assert formatter._format_text(text) == """test-prog is awesome!
also, here is a sentence. and here is another one!
```bash
@@ -157,7 +159,7 @@ def test_format_text(self) -> None:
```
you cannot belive it, it's another sentence.
-'''
+"""
def test_start_section(self) -> None:
formatter = MarkdownFormatter.factory(0)('test-prog')
@@ -169,7 +171,7 @@ def test_start_section(self) -> None:
assert formatter._current_section.parent is formatter._root_section
assert formatter._current_section.heading == '## Test-section-01'
assert len(formatter._current_section.items) == 0
- assert formatter._current_section.parent.items[0] == (formatter._current_section.format_help, [],)
+ assert next(iter(formatter._current_section.parent.items)) == (formatter._current_section.format_help, [])
def test__format_action(self) -> None:
formatter = MarkdownFormatter.factory(0)('test-prog')
@@ -226,7 +228,7 @@ def test_format_help(self, capsys: CaptureFixture) -> None:
format_help_text = formatter._current_section.format_help()
assert capsys.readouterr().out == ''
- assert format_help_text == '''
+ assert format_help_text == """
## Root section
@@ -236,7 +238,7 @@ def test_format_help(self, capsys: CaptureFixture) -> None:
| `--root-const` | `True` | |
-'''
+"""
formatter = MarkdownFormatter('test-prog')
formatter.level = 1
@@ -254,7 +256,7 @@ def test_format_help(self, capsys: CaptureFixture) -> None:
format_help_text = formatter._current_section.format_help()
assert capsys.readouterr().out == '\n' # @TODO: whyyyyyyyyyy?!
print(format_help_text)
- assert format_help_text == '''
+ assert format_help_text == """
#### Root section
@@ -264,4 +266,4 @@ def test_format_help(self, capsys: CaptureFixture) -> None:
| `--root-const` | `True` | |
-'''
+"""
diff --git a/tests/unit/distributed/test___init__.py b/tests/unit/distributed/test___init__.py
index f13d02a..0cd78ca 100644
--- a/tests/unit/distributed/test___init__.py
+++ b/tests/unit/distributed/test___init__.py
@@ -1,22 +1,23 @@
-import sys
-import json
+from __future__ import annotations
-from os import getcwd, environ
-from tempfile import gettempdir
+import json
+import sys
from argparse import ArgumentParser, Namespace
+from contextlib import suppress
from datetime import datetime, timezone
+from os import environ
+from tempfile import gettempdir
+from typing import TYPE_CHECKING
import pytest
-from _pytest.capture import CaptureFixture
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
-
+from grizzly_cli.distributed import create_parser, distributed, distributed_run
from grizzly_cli.utils import RunCommandResult, rm_rf
-from grizzly_cli.distributed import create_parser, distributed_run, distributed
-
-CWD = getcwd()
+if TYPE_CHECKING: # pragma: no cover
+ from _pytest.capture import CaptureFixture
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
def test_distributed(mocker: MockerFixture) -> None:
@@ -54,12 +55,11 @@ def test_distributed(mocker: MockerFixture) -> None:
assert args[0] is arguments
arguments = Namespace(subcommand='foo')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='unknown subcommand foo'):
distributed(arguments)
- assert 'unknown subcommand foo' == str(ve.value)
-def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None:
+def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None: # noqa: PLR0915
test_context = tmp_path_factory.mktemp('test_context')
(test_context / 'test.feature').write_text('Feature:')
@@ -68,10 +68,10 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
do_build_mock = mocker.patch('grizzly_cli.distributed.do_build', return_value=None)
list_images_mock = mocker.patch('grizzly_cli.distributed.list_images', return_value=None)
- import grizzly_cli.distributed
- mocker.patch.object(grizzly_cli.distributed, 'EXECUTION_CONTEXT', '/tmp/execution-context')
- mocker.patch.object(grizzly_cli.distributed, 'STATIC_CONTEXT', '/tmp/static-context')
- mocker.patch.object(grizzly_cli.distributed, 'MOUNT_CONTEXT', '/tmp/mount-context')
+ import grizzly_cli.distributed # noqa: PLC0415
+ mocker.patch.object(grizzly_cli.distributed, 'EXECUTION_CONTEXT', '/srv/grizzly/execution-context')
+ mocker.patch.object(grizzly_cli.distributed, 'STATIC_CONTEXT', '/srv/grizzly/static-context')
+ mocker.patch.object(grizzly_cli.distributed, 'MOUNT_CONTEXT', '/srv/grizzly/mount-context')
mocker.patch.object(grizzly_cli.distributed, 'PROJECT_NAME', 'grizzly-cli-test-project')
run_command_result = RunCommandResult(return_code=1)
@@ -92,11 +92,11 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
get_default_mtu_mock.return_value = '1500'
sys.argv = ['grizzly-cli', 'dist', '--workers', '3', '--tty', 'run', f'{test_context}/test.feature']
arguments = parser.parse_args()
- setattr(arguments, 'container_system', 'docker')
- setattr(arguments, 'file', ' '.join(arguments.file))
+ setattr(arguments, 'container_system', 'docker') # noqa: B010
+ setattr(arguments, 'file', ' '.join(arguments.file)) # noqa: B010
# this is set in the devcontainer
- for key in environ.keys():
+ for key in environ:
if key.startswith('GRIZZLY_'):
del environ[key]
@@ -108,10 +108,8 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
f'grizzly-cli dist --validate-config --workers 3 --tty run {test_context}/test.feature\n'
)
- try:
+ with suppress(KeyError):
del environ['GRIZZLY_MTU']
- except KeyError:
- pass
run_command_mock.return_value = RunCommandResult(return_code=0)
do_build_mock.return_value = 255
@@ -127,9 +125,9 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
'!! failed to build grizzly-cli-test-project, rc=255\n'
)
assert environ.get('GRIZZLY_MTU', None) == '1500'
- assert environ.get('GRIZZLY_EXECUTION_CONTEXT', None) == '/tmp/execution-context'
- assert environ.get('GRIZZLY_STATIC_CONTEXT', None) == '/tmp/static-context'
- assert environ.get('GRIZZLY_MOUNT_CONTEXT', None) == '/tmp/mount-context'
+ assert environ.get('GRIZZLY_EXECUTION_CONTEXT', None) == '/srv/grizzly/execution-context'
+ assert environ.get('GRIZZLY_STATIC_CONTEXT', None) == '/srv/grizzly/static-context'
+ assert environ.get('GRIZZLY_MOUNT_CONTEXT', None) == '/srv/grizzly/mount-context'
assert environ.get('GRIZZLY_PROJECT_NAME', None) == 'grizzly-cli-test-project'
assert environ.get('GRIZZLY_USER_TAG', None) == 'test-user'
assert environ.get('GRIZZLY_EXPECTED_WORKERS', None) == '3'
@@ -147,8 +145,8 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
assert environ.get('GRIZZLY_MOUNT_PATH', None) == ''
# this is set in the devcontainer
- for key in environ.keys():
- if key.startswith('GRIZZLY_') or key.startswith('LOCUST_'):
+ for key in environ:
+ if key.startswith(('GRIZZLY_', 'LOCUST_')):
del environ[key]
arguments = parser.parse_args([
@@ -165,8 +163,8 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
'run',
f'{test_context}/test.feature',
])
- setattr(arguments, 'container_system', 'docker')
- setattr(arguments, 'file', ' '.join(arguments.file))
+ setattr(arguments, 'container_system', 'docker') # noqa: B010
+ setattr(arguments, 'file', ' '.join(arguments.file)) # noqa: B010
# docker-compose v2
rcr = RunCommandResult(return_code=1)
@@ -182,7 +180,7 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
assert distributed_run(
arguments,
{
- 'GRIZZLY_CONFIGURATION_FILE': '/tmp/execution-context/configuration.yaml',
+ 'GRIZZLY_CONFIGURATION_FILE': '/srv/grizzly/execution-context/configuration.yaml',
'GRIZZLY_TEST_VAR': 'True',
},
{
@@ -207,14 +205,14 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
assert args[0] == [
'docker', 'compose',
'-p', 'foobar-test-user',
- '-f', '/tmp/static-context/compose.yaml',
+ '-f', '/srv/grizzly/static-context/compose.yaml',
'config',
]
args, _ = run_command_mock.call_args_list[-2]
assert args[0] == [
'docker', 'compose',
'-p', 'foobar-test-user',
- '-f', '/tmp/static-context/compose.yaml',
+ '-f', '/srv/grizzly/static-context/compose.yaml',
'up',
'--scale', 'worker=3',
'--remove-orphans',
@@ -223,15 +221,15 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
assert args[0] == [
'docker', 'compose',
'-p', 'foobar-test-user',
- '-f', '/tmp/static-context/compose.yaml',
+ '-f', '/srv/grizzly/static-context/compose.yaml',
'stop',
]
assert environ.get('GRIZZLY_RUN_FILE', None) == f'{test_context}/test.feature'
assert environ.get('GRIZZLY_MTU', None) == '1400'
- assert environ.get('GRIZZLY_EXECUTION_CONTEXT', None) == '/tmp/execution-context'
- assert environ.get('GRIZZLY_STATIC_CONTEXT', None) == '/tmp/static-context'
- assert environ.get('GRIZZLY_MOUNT_CONTEXT', None) == '/tmp/mount-context'
+ assert environ.get('GRIZZLY_EXECUTION_CONTEXT', None) == '/srv/grizzly/execution-context'
+ assert environ.get('GRIZZLY_STATIC_CONTEXT', None) == '/srv/grizzly/static-context'
+ assert environ.get('GRIZZLY_MOUNT_CONTEXT', None) == '/srv/grizzly/mount-context'
assert environ.get('GRIZZLY_PROJECT_NAME', None) == 'foobar'
assert environ.get('GRIZZLY_USER_TAG', None) == 'test-user'
assert environ.get('GRIZZLY_EXPECTED_WORKERS', None) == '3'
@@ -251,8 +249,8 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
arguments.project_name = None
# this is set in the devcontainer
- for key in environ.keys():
- if key.startswith('GRIZZLY_') or key.startswith('LOCUST_'):
+ for key in environ:
+ if key.startswith(('GRIZZLY_', 'LOCUST_')):
del environ[key]
arguments = parser.parse_args([
@@ -267,21 +265,21 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
'--wait-for-worker', '1.25 * WORKER_REPORT_INTERVAL',
'run', f'{test_context}/test.feature',
])
- setattr(arguments, 'container_system', 'docker')
- setattr(arguments, 'file', ' '.join(arguments.file))
+ setattr(arguments, 'container_system', 'docker') # noqa: B010
+ setattr(arguments, 'file', ' '.join(arguments.file)) # noqa: B010
run_command_mock.return_value = None
run_command_mock.side_effect = [RunCommandResult(return_code=13)]
do_build_mock.return_value = 0
check_output_mock.return_value = None
- check_output_mock.side_effect = [json.dumps([{'Source': '/tmp/mount-context', 'Destination': '/tmp'}]), '13']
+ check_output_mock.side_effect = [json.dumps([{'Source': '/srv/grizzly/mount-context', 'Destination': '/srv/grizzly'}]), '13']
get_default_mtu_mock.return_value = '1800'
list_images_mock.return_value = {'grizzly-cli-test-project': {'test-user': {}}}
assert distributed_run(
arguments,
{
- 'GRIZZLY_CONFIGURATION_FILE': '/tmp/execution-context/configuration.yaml',
+ 'GRIZZLY_CONFIGURATION_FILE': '/srv/grizzly/execution-context/configuration.yaml',
'GRIZZLY_TEST_VAR': 'True',
},
{
@@ -299,15 +297,15 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
assert args[0] == [
'docker', 'compose',
'-p', 'grizzly-cli-test-project-suffix-test-user',
- '-f', '/tmp/static-context/compose.yaml',
+ '-f', '/srv/grizzly/static-context/compose.yaml',
'config',
]
assert environ.get('GRIZZLY_RUN_FILE', None) == f'{test_context}/test.feature'
assert environ.get('GRIZZLY_MTU', None) == '1800'
- assert environ.get('GRIZZLY_EXECUTION_CONTEXT', None) == '/tmp/execution-context'
- assert environ.get('GRIZZLY_STATIC_CONTEXT', None) == '/tmp/static-context'
- assert environ.get('GRIZZLY_MOUNT_CONTEXT', None) == '/tmp/mount-context'
+ assert environ.get('GRIZZLY_EXECUTION_CONTEXT', None) == '/srv/grizzly/execution-context'
+ assert environ.get('GRIZZLY_STATIC_CONTEXT', None) == '/srv/grizzly/static-context'
+ assert environ.get('GRIZZLY_MOUNT_CONTEXT', None) == '/srv/grizzly/mount-context'
assert environ.get('GRIZZLY_PROJECT_NAME', None) == 'grizzly-cli-test-project'
assert environ.get('GRIZZLY_USER_TAG', None) == 'test-user'
assert environ.get('GRIZZLY_EXPECTED_WORKERS', None) == '1'
@@ -325,6 +323,6 @@ def test_distributed_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path
finally:
rm_rf(test_context)
- for key in environ.keys():
+ for key in environ:
if key.startswith('GRIZZLY_'):
del environ[key]
diff --git a/tests/unit/distributed/test_build.py b/tests/unit/distributed/test_build.py
index b4ffc02..b4b133e 100644
--- a/tests/unit/distributed/test_build.py
+++ b/tests/unit/distributed/test_build.py
@@ -1,20 +1,22 @@
-import sys
+from __future__ import annotations
-from os import environ, path, getcwd, chdir
+import sys
+from argparse import Namespace
+from contextlib import suppress
from inspect import getfile
+from os import environ
+from pathlib import Path
from socket import gaierror
+from typing import TYPE_CHECKING
-from _pytest.capture import CaptureFixture
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
-
-from argparse import Namespace
-
+from grizzly_cli.distributed.build import _create_build_command, build, getgid, getuid
from grizzly_cli.utils import RunCommandResult, rm_rf
-from grizzly_cli.distributed.build import _create_build_command, getgid, getuid, build
+from tests.helpers import cwd
-
-CWD = getcwd()
+if TYPE_CHECKING:
+ from _pytest.capture import CaptureFixture
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
def test_getuid_getgid_nt(mocker: MockerFixture) -> None:
@@ -34,7 +36,7 @@ def test__create_build_command(mocker: MockerFixture) -> None:
mocker.patch('grizzly_cli.distributed.build.getgid', return_value=2147483647)
args = Namespace(container_system='test', local_install=False)
- mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', None, ), '2.8.4'))
+ mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', None), '2.8.4'))
assert _create_build_command(args, 'Containerfile.test', 'grizzly-cli:test', '/home/grizzly-cli/') == [
'test',
@@ -51,7 +53,7 @@ def test__create_build_command(mocker: MockerFixture) -> None:
'/home/grizzly-cli/',
]
- mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', [], ), '2.8.4'))
+ mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', []), '2.8.4'))
args.local_install = True
@@ -70,7 +72,7 @@ def test__create_build_command(mocker: MockerFixture) -> None:
'/home/grizzly-cli/',
]
- mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', ['dev', 'ci', 'mq'], ), '2.8.4'))
+ mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', ['dev', 'ci', 'mq']), '2.8.4'))
assert _create_build_command(args, 'Containerfile.test', 'grizzly-cli:test', '/home/grizzly-cli/') == [
'test',
@@ -171,199 +173,194 @@ def test__create_build_command(mocker: MockerFixture) -> None:
]
finally:
- try:
+ with suppress(KeyError):
del environ['IBM_MQ_LIB_HOST']
- except KeyError:
- pass
- try:
+
+ with suppress(KeyError):
del environ['IBM_MQ_LIB']
- except KeyError:
- pass
-def test_build(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None:
+def test_build(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None: # noqa: PLR0915
test_context = tmp_path_factory.mktemp('test_context')
try:
- chdir(test_context)
- mocker.patch('grizzly_cli.EXECUTION_CONTEXT', str(test_context))
- mocker.patch('grizzly_cli.distributed.build.EXECUTION_CONTEXT', str(test_context))
- mocker.patch('grizzly_cli.distributed.build.PROJECT_NAME', 'grizzly-scenarios')
- mocker.patch('grizzly_cli.distributed.build.getuser', return_value='test-user')
- mocker.patch('grizzly_cli.distributed.build.getuid', return_value=1337)
- mocker.patch('grizzly_cli.distributed.build.getgid', return_value=2147483647)
- run_command = mocker.patch('grizzly_cli.distributed.build.run_command', side_effect=[
- RunCommandResult(return_code=254),
- RunCommandResult(return_code=133),
- RunCommandResult(return_code=0),
- RunCommandResult(return_code=1),
- RunCommandResult(return_code=0),
- RunCommandResult(return_code=0),
- RunCommandResult(return_code=2),
- RunCommandResult(return_code=0),
- RunCommandResult(return_code=0),
- RunCommandResult(return_code=0),
- ])
- setattr(getattr(build, '__wrapped__'), '__value__', str(test_context))
-
- test_args = Namespace(container_system='test', force_build=False, project_name=None, local_install=False, no_progress=False, verbose=False)
-
- static_context = path.realpath(path.join(path.dirname(getfile(_create_build_command)), '..', 'static'))
-
- assert build(test_args) == 254
-
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == ''
- assert run_command.call_count == 1
- args, kwargs = run_command.call_args_list[-1]
-
- container_file_actual = args[0].pop(14)
- container_file_expected = f'{static_context}{path.sep}Containerfile'
-
- if sys.platform == 'win32':
- container_file_actual = container_file_actual.lower()
- container_file_expected = container_file_expected.lower()
-
- assert args[0] == [
- 'test',
- 'image',
- 'build',
- '--ssh',
- 'default',
- '--build-arg', 'GRIZZLY_EXTRA=base',
- '--build-arg', 'GRIZZLY_INSTALL_TYPE=remote',
- '--build-arg', 'GRIZZLY_UID=1337',
- '--build-arg', 'GRIZZLY_GID=2147483647',
- '-f',
- '-t', 'grizzly-scenarios:test-user',
- str(test_context),
- ]
-
- assert container_file_actual == container_file_expected
-
- actual_env = kwargs.get('env', None)
- assert actual_env is not None
- assert actual_env.get('DOCKER_BUILDKIT', None) == environ.get('DOCKER_BUILDKIT', None)
-
- test_args = Namespace(container_system='docker', force_build=True, local_install=True, project_name='foobar', no_progress=False, verbose=False)
-
- mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', ['mq', 'dev'], ), '2.8.4'))
-
- assert build(test_args) == 133
- assert run_command.call_count == 2
- args, kwargs = run_command.call_args_list[-1]
-
- container_file_actual = args[0].pop(14)
- container_file_expected = f'{static_context}{path.sep}Containerfile'
-
- if sys.platform == 'win32':
- container_file_actual = container_file_actual.lower()
- container_file_expected = container_file_expected.lower()
-
- assert args[0] == [
- 'docker',
- 'image',
- 'build',
- '--ssh',
- 'default',
- '--build-arg', 'GRIZZLY_EXTRA=mq',
- '--build-arg', 'GRIZZLY_INSTALL_TYPE=local',
- '--build-arg', 'GRIZZLY_UID=1337',
- '--build-arg', 'GRIZZLY_GID=2147483647',
- '-f',
- '-t', 'foobar:test-user',
- str(test_context),
- '--no-cache'
- ]
- assert container_file_actual == container_file_expected
-
- capsys.readouterr()
-
- actual_env = kwargs.get('env', None)
- assert actual_env is not None
- assert actual_env.get('DOCKER_BUILDKIT', None) == '1'
-
- image_name = 'grizzly-scenarios:test-user'
- test_args = Namespace(
- container_system='docker',
- force_build=False,
- local_install=False,
- project_name=None,
- registry='ghcr.io/biometria-se/',
- no_progress=False,
- verbose=False,
- )
-
- assert build(test_args) == 1
-
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- f'\nbuilt image {image_name}\n'
- f'\n!! failed to tag image {image_name} -> ghcr.io/biometria-se/{image_name}\n'
- )
-
- assert run_command.call_count == 4
-
- args, kwargs = run_command.call_args_list[-1]
- assert args[0] == [
- 'docker',
- 'image',
- 'tag',
- image_name,
- f'ghcr.io/biometria-se/{image_name}',
- ]
-
- actual_env = kwargs.get('env', None)
- assert actual_env.get('DOCKER_BUILDKIT', None) == '1'
-
- test_args = Namespace(
- container_system='docker',
- force_build=True,
- no_cache=True,
- build=True,
- registry='ghcr.io/biometria-se/',
- project_name='foobar',
- local_install=True,
- no_progress=False,
- verbose=False,
- )
-
- image_name = 'foobar:test-user'
- assert build(test_args) == 2
-
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- f'\nbuilt image {image_name}\n'
- f'tagged image {image_name} -> ghcr.io/biometria-se/{image_name}\n'
- f'\n!! failed to push image ghcr.io/biometria-se/{image_name}\n'
- )
-
- assert run_command.call_count == 7
-
- args, kwargs = run_command.call_args_list[-1]
- assert args[0] == [
- 'docker',
- 'image',
- 'push',
- f'ghcr.io/biometria-se/{image_name}',
- ]
-
- actual_env = kwargs.get('env', None)
- assert actual_env.get('DOCKER_BUILDKIT', None) == '1'
-
- assert build(test_args) == 0
-
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- f'\nbuilt image {image_name}\n'
- f'tagged image {image_name} -> ghcr.io/biometria-se/{image_name}\n'
- f'pushed image ghcr.io/biometria-se/{image_name}\n'
- )
+ with cwd(test_context):
+ mocker.patch('grizzly_cli.EXECUTION_CONTEXT', test_context.as_posix())
+ mocker.patch('grizzly_cli.distributed.build.EXECUTION_CONTEXT', test_context.as_posix())
+ mocker.patch('grizzly_cli.distributed.build.PROJECT_NAME', 'grizzly-scenarios')
+ mocker.patch('grizzly_cli.distributed.build.getuser', return_value='test-user')
+ mocker.patch('grizzly_cli.distributed.build.getuid', return_value=1337)
+ mocker.patch('grizzly_cli.distributed.build.getgid', return_value=2147483647)
+ run_command = mocker.patch('grizzly_cli.distributed.build.run_command', side_effect=[
+ RunCommandResult(return_code=254),
+ RunCommandResult(return_code=133),
+ RunCommandResult(return_code=0),
+ RunCommandResult(return_code=1),
+ RunCommandResult(return_code=0),
+ RunCommandResult(return_code=0),
+ RunCommandResult(return_code=2),
+ RunCommandResult(return_code=0),
+ RunCommandResult(return_code=0),
+ RunCommandResult(return_code=0),
+ ])
+ setattr(getattr(build, '__wrapped__'), '__value__', test_context.as_posix()) # noqa: B009, B010
+
+ test_args = Namespace(container_system='test', force_build=False, project_name=None, local_install=False, no_progress=False, verbose=False)
+
+ static_context = Path.joinpath(Path(getfile(_create_build_command)).parent, '..', 'static').resolve()
+
+ assert build(test_args) == 254
+
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == ''
+ assert run_command.call_count == 1
+ args, kwargs = run_command.call_args_list[-1]
+
+ container_file_actual = args[0].pop(14)
+ container_file_expected = Path.joinpath(static_context, 'Containerfile').as_posix()
+
+ if sys.platform == 'win32':
+ container_file_actual = container_file_actual.lower()
+ container_file_expected = container_file_expected.lower()
+
+ assert args[0] == [
+ 'test',
+ 'image',
+ 'build',
+ '--ssh',
+ 'default',
+ '--build-arg', 'GRIZZLY_EXTRA=base',
+ '--build-arg', 'GRIZZLY_INSTALL_TYPE=remote',
+ '--build-arg', 'GRIZZLY_UID=1337',
+ '--build-arg', 'GRIZZLY_GID=2147483647',
+ '-f',
+ '-t', 'grizzly-scenarios:test-user',
+ test_context.as_posix(),
+ ]
+
+ assert container_file_actual == container_file_expected
+
+ actual_env = kwargs.get('env', None)
+ assert actual_env is not None
+ assert actual_env.get('DOCKER_BUILDKIT', None) == environ.get('DOCKER_BUILDKIT', None)
+
+ test_args = Namespace(container_system='docker', force_build=True, local_install=True, project_name='foobar', no_progress=False, verbose=False)
+
+ mocker.patch('grizzly_cli.distributed.build.get_dependency_versions', return_value=(('1.1.1', ['mq', 'dev']), '2.8.4'))
+
+ assert build(test_args) == 133
+ assert run_command.call_count == 2
+ args, kwargs = run_command.call_args_list[-1]
+
+ container_file_actual = args[0].pop(14)
+ container_file_expected = Path.joinpath(static_context, 'Containerfile').as_posix()
+
+ if sys.platform == 'win32':
+ container_file_actual = container_file_actual.lower()
+ container_file_expected = container_file_expected.lower()
+
+ assert args[0] == [
+ 'docker',
+ 'image',
+ 'build',
+ '--ssh',
+ 'default',
+ '--build-arg', 'GRIZZLY_EXTRA=mq',
+ '--build-arg', 'GRIZZLY_INSTALL_TYPE=local',
+ '--build-arg', 'GRIZZLY_UID=1337',
+ '--build-arg', 'GRIZZLY_GID=2147483647',
+ '-f',
+ '-t', 'foobar:test-user',
+ test_context.as_posix(),
+ '--no-cache',
+ ]
+ assert container_file_actual == container_file_expected
+
+ capsys.readouterr()
+
+ actual_env = kwargs.get('env', None)
+ assert actual_env is not None
+ assert actual_env.get('DOCKER_BUILDKIT', None) == '1'
+
+ image_name = 'grizzly-scenarios:test-user'
+ test_args = Namespace(
+ container_system='docker',
+ force_build=False,
+ local_install=False,
+ project_name=None,
+ registry='ghcr.io/biometria-se/',
+ no_progress=False,
+ verbose=False,
+ )
+
+ assert build(test_args) == 1
+
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ f'\nbuilt image {image_name}\n'
+ f'\n!! failed to tag image {image_name} -> ghcr.io/biometria-se/{image_name}\n'
+ )
+
+ assert run_command.call_count == 4
+
+ args, kwargs = run_command.call_args_list[-1]
+ assert args[0] == [
+ 'docker',
+ 'image',
+ 'tag',
+ image_name,
+ f'ghcr.io/biometria-se/{image_name}',
+ ]
+
+ actual_env = kwargs.get('env', None)
+ assert actual_env.get('DOCKER_BUILDKIT', None) == '1'
+
+ test_args = Namespace(
+ container_system='docker',
+ force_build=True,
+ no_cache=True,
+ build=True,
+ registry='ghcr.io/biometria-se/',
+ project_name='foobar',
+ local_install=True,
+ no_progress=False,
+ verbose=False,
+ )
+
+ image_name = 'foobar:test-user'
+ assert build(test_args) == 2
+
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ f'\nbuilt image {image_name}\n'
+ f'tagged image {image_name} -> ghcr.io/biometria-se/{image_name}\n'
+ f'\n!! failed to push image ghcr.io/biometria-se/{image_name}\n'
+ )
+
+ assert run_command.call_count == 7
+
+ args, kwargs = run_command.call_args_list[-1]
+ assert args[0] == [
+ 'docker',
+ 'image',
+ 'push',
+ f'ghcr.io/biometria-se/{image_name}',
+ ]
+
+ actual_env = kwargs.get('env', None)
+ assert actual_env.get('DOCKER_BUILDKIT', None) == '1'
+
+ assert build(test_args) == 0
+
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ f'\nbuilt image {image_name}\n'
+ f'tagged image {image_name} -> ghcr.io/biometria-se/{image_name}\n'
+ f'pushed image ghcr.io/biometria-se/{image_name}\n'
+ )
finally:
- chdir(CWD)
-
rm_rf(test_context)
diff --git a/tests/unit/distributed/test_clean.py b/tests/unit/distributed/test_clean.py
index 3772d48..af33972 100644
--- a/tests/unit/distributed/test_clean.py
+++ b/tests/unit/distributed/test_clean.py
@@ -1,19 +1,23 @@
-from argparse import Namespace
+from __future__ import annotations
-from pytest_mock import MockerFixture
+from argparse import Namespace
+from typing import TYPE_CHECKING
-from grizzly_cli.utils import RunCommandResult
from grizzly_cli.distributed.clean import clean
+from grizzly_cli.utils import RunCommandResult
+
+if TYPE_CHECKING:
+ from pytest_mock import MockerFixture
-def test_clean(mocker: MockerFixture) -> None:
- import grizzly_cli.distributed.clean
- mocker.patch.object(grizzly_cli.distributed.clean, 'STATIC_CONTEXT', '/tmp/static-context')
+def test_clean(mocker: MockerFixture) -> None: # noqa: PLR0915
+ import grizzly_cli.distributed.clean # noqa: PLC0415
+ mocker.patch.object(grizzly_cli.distributed.clean, 'STATIC_CONTEXT', '/srv/grizzly/static-context')
mocker.patch.object(grizzly_cli.distributed.clean, 'PROJECT_NAME', 'grizzly-cli-test-project')
arguments = Namespace(networks=True, images=True, project_name='foobar', container_system='docker', id=None)
mocker.patch('grizzly_cli.distributed.clean.getuser', return_value='root')
- mocker.patch('grizzly_cli.distributed.clean.get_terminal_size', return_value=(1024, 1024,))
+ mocker.patch('grizzly_cli.distributed.clean.get_terminal_size', return_value=(1024, 1024))
run_command_spy = mocker.patch('grizzly_cli.distributed.clean.run_command', side_effect=[
RunCommandResult(return_code=0),
@@ -33,7 +37,7 @@ def test_clean(mocker: MockerFixture) -> None:
args, kwargs = run_command_spy.call_args_list[0]
assert args[0] == [
'docker', 'compose',
- '-f', '/tmp/static-context/compose.yaml',
+ '-f', '/srv/grizzly/static-context/compose.yaml',
'-p', 'foobar-root',
'rm', '-f', '-s', '-v',
]
@@ -79,7 +83,7 @@ def test_clean(mocker: MockerFixture) -> None:
args, kwargs = run_command_spy.call_args_list[0]
assert args[0] == [
'docker', 'compose',
- '-f', '/tmp/static-context/compose.yaml',
+ '-f', '/srv/grizzly/static-context/compose.yaml',
'-p', 'grizzly-cli-test-project-foobar-root',
'rm', '-f', '-s', '-v',
]
@@ -108,7 +112,7 @@ def test_clean(mocker: MockerFixture) -> None:
args, kwargs = run_command_spy.call_args_list[0]
assert args[0] == [
'docker', 'compose',
- '-f', '/tmp/static-context/compose.yaml',
+ '-f', '/srv/grizzly/static-context/compose.yaml',
'-p', 'grizzly-cli-test-project-root',
'rm', '-f', '-s', '-v',
]
diff --git a/tests/unit/test___init__.py b/tests/unit/test___init__.py
index 6b77360..0f0d7c5 100644
--- a/tests/unit/test___init__.py
+++ b/tests/unit/test___init__.py
@@ -1,42 +1,41 @@
-from os import chdir, environ, path, getcwd
-from inspect import getfile
-from importlib import reload
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
+from __future__ import annotations
-from tests.helpers import rm_rf
+from contextlib import suppress
+from importlib import reload
+from inspect import getfile
+from os import environ
+from pathlib import Path
+from typing import TYPE_CHECKING
+from tests.helpers import cwd, rm_rf
-CWD = getcwd()
+if TYPE_CHECKING: # pragma: no cover
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
def test___import__(tmp_path_factory: TempPathFactory, mocker: MockerFixture) -> None:
test_context = tmp_path_factory.mktemp('test_context')
- test_context_root = str(test_context)
-
- chdir(test_context_root)
try:
- environ['GRIZZLY_MOUNT_CONTEXT'] = '/var/tmp'
+ with cwd(test_context):
+ environ['GRIZZLY_MOUNT_CONTEXT'] = '/srv/grizzly'
- import grizzly_cli
- reload(grizzly_cli)
- mocker.patch.object(grizzly_cli, '__version__', '1.2.3')
+ import grizzly_cli # noqa: PLC0415
+ reload(grizzly_cli)
+ mocker.patch.object(grizzly_cli, '__version__', '1.2.3')
- static_context = path.join(path.dirname(getfile(grizzly_cli)), 'static')
+ static_context = Path.joinpath(Path(getfile(grizzly_cli)).parent, 'static')
- assert grizzly_cli.__version__ == '1.2.3'
- assert grizzly_cli.EXECUTION_CONTEXT == test_context_root
- assert grizzly_cli.MOUNT_CONTEXT == '/var/tmp'
- assert grizzly_cli.STATIC_CONTEXT == static_context
- assert grizzly_cli.PROJECT_NAME == path.basename(test_context_root)
- assert len(grizzly_cli.SCENARIOS) == 0
+ assert grizzly_cli.__version__ == '1.2.3'
+ assert test_context.as_posix() == grizzly_cli.EXECUTION_CONTEXT
+ assert grizzly_cli.MOUNT_CONTEXT == '/srv/grizzly'
+ assert static_context.as_posix() == grizzly_cli.STATIC_CONTEXT
+ assert test_context.name == grizzly_cli.PROJECT_NAME
+ assert len(grizzly_cli.SCENARIOS) == 0
finally:
- chdir(CWD)
- rm_rf(test_context_root)
+ rm_rf(test_context)
- try:
+ with suppress(KeyError):
del environ['GRIZZLY_MOUNT_CONTEXT']
- except:
- pass
diff --git a/tests/unit/test___main__.py b/tests/unit/test___main__.py
index 7334eb5..42376c4 100644
--- a/tests/unit/test___main__.py
+++ b/tests/unit/test___main__.py
@@ -1,24 +1,25 @@
-import sys
+from __future__ import annotations
+import sys
+from argparse import ArgumentParser as CoreArgumentParser
+from argparse import Namespace
from hashlib import sha1
-from typing import Dict, Optional, cast
-from argparse import ArgumentParser as CoreArgumentParser, Namespace
-from os import getcwd, environ, chdir, path
+from os import environ
+from pathlib import Path
+from typing import TYPE_CHECKING, Optional, cast
import pytest
-from _pytest.capture import CaptureFixture
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
-
-from grizzly_cli.__main__ import _create_parser, _parse_arguments, _inject_additional_arguments_from_metadata, main
+from grizzly_cli.__main__ import _create_parser, _inject_additional_arguments_from_metadata, _parse_arguments, main
+from tests.helpers import SOME, cwd, rm_rf
-from tests.helpers import rm_rf
+if TYPE_CHECKING:
+ from _pytest.capture import CaptureFixture
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
-CWD = getcwd()
-
-def test__create_parser() -> None:
+def test__create_parser() -> None: # noqa: PLR0915
parser = _create_parser()
assert parser.prog == 'grizzly-cli'
@@ -37,9 +38,9 @@ def test__create_parser() -> None:
subparser = parser._subparsers._group_actions[0]
assert subparser is not None
assert subparser.choices is not None
- assert len(cast(Dict[str, Optional[CoreArgumentParser]], subparser.choices).keys()) == 5
+ assert len(cast('dict[str, Optional[CoreArgumentParser]]', subparser.choices).keys()) == 5
- init_parser = cast(Dict[str, Optional[CoreArgumentParser]], subparser.choices).get('init', None)
+ init_parser = cast('dict[str, Optional[CoreArgumentParser]]', subparser.choices).get('init', None)
assert init_parser is not None
assert init_parser._subparsers is None
assert getattr(init_parser, 'prog', None) == 'grizzly-cli init'
@@ -50,7 +51,7 @@ def test__create_parser() -> None:
'--grizzly-version',
])
- auth_parser = cast(Dict[str, Optional[CoreArgumentParser]], subparser.choices).get('auth', None)
+ auth_parser = cast('dict[str, Optional[CoreArgumentParser]]', subparser.choices).get('auth', None)
assert auth_parser is not None
assert auth_parser._subparsers is None
assert getattr(auth_parser, 'prog', None) == 'grizzly-cli auth'
@@ -58,7 +59,7 @@ def test__create_parser() -> None:
'-h', '--help',
])
- keyvault_parser = cast(Dict[str, Optional[CoreArgumentParser]], subparser.choices).get('keyvault', None)
+ keyvault_parser = cast('dict[str, Optional[CoreArgumentParser]]', subparser.choices).get('keyvault', None)
assert keyvault_parser is not None
print(keyvault_parser._subparsers)
assert keyvault_parser._subparsers is not None
@@ -67,7 +68,7 @@ def test__create_parser() -> None:
'--file', '-f', '-h', '--help', '--vault-name',
])
- local_parser = cast(Dict[str, Optional[CoreArgumentParser]], subparser.choices).get('local', None)
+ local_parser = cast('dict[str, Optional[CoreArgumentParser]]', subparser.choices).get('local', None)
assert local_parser is not None
assert local_parser._subparsers is not None
assert getattr(local_parser, 'prog', None) == 'grizzly-cli local'
@@ -78,9 +79,9 @@ def test__create_parser() -> None:
local_subparser = local_parser._subparsers._group_actions[0]
assert local_subparser is not None
assert local_subparser.choices is not None
- assert list(cast(Dict[str, Optional[CoreArgumentParser]], local_subparser.choices).keys()) == ['run']
+ assert list(cast('dict[str, Optional[CoreArgumentParser]]', local_subparser.choices).keys()) == ['run']
- dist_parser = cast(Dict[str, Optional[CoreArgumentParser]], subparser.choices).get('dist', None)
+ dist_parser = cast('dict[str, Optional[CoreArgumentParser]]', subparser.choices).get('dist', None)
assert dist_parser is not None
assert dist_parser._subparsers is not None
assert getattr(dist_parser, 'prog', None) == 'grizzly-cli dist'
@@ -104,9 +105,9 @@ def test__create_parser() -> None:
dist_subparser = dist_parser._subparsers._group_actions[0]
assert dist_subparser is not None
assert dist_subparser.choices is not None
- assert list(cast(Dict[str, Optional[CoreArgumentParser]], dist_subparser.choices).keys()) == ['build', 'clean', 'run']
+ assert list(cast('dict[str, Optional[CoreArgumentParser]]', dist_subparser.choices).keys()) == ['build', 'clean', 'run']
- dist_build_parser = cast(Dict[str, Optional[CoreArgumentParser]], dist_subparser.choices).get('build', None)
+ dist_build_parser = cast('dict[str, Optional[CoreArgumentParser]]', dist_subparser.choices).get('build', None)
assert dist_build_parser is not None
assert dist_build_parser._subparsers is None
assert getattr(dist_build_parser, 'prog', None) == 'grizzly-cli dist build'
@@ -119,22 +120,22 @@ def test__create_parser() -> None:
'--verbose',
])
- dist_clean_parser = cast(Dict[str, Optional[CoreArgumentParser]], dist_subparser.choices).get('clean', None)
+ dist_clean_parser = cast('dict[str, Optional[CoreArgumentParser]]', dist_subparser.choices).get('clean', None)
assert dist_clean_parser is not None
assert dist_clean_parser._subparsers is None
assert getattr(dist_clean_parser, 'prog', None) == 'grizzly-cli dist clean'
assert sorted([option_string for action in dist_clean_parser._actions for option_string in action.option_strings]) == sorted([
'-h', '--help',
'--no-images',
- '--no-networks'
+ '--no-networks',
])
# grizzly-cli ... run
- for tested_parser, parent in [(local_parser, 'local',), (dist_parser, 'dist',)]:
+ for tested_parser, parent in [(local_parser, 'local'), (dist_parser, 'dist')]:
assert tested_parser._subparsers is not None
assert len(tested_parser._subparsers._group_actions) == 1
subparser = tested_parser._subparsers._group_actions[0]
- run_parser = cast(Dict[str, Optional[CoreArgumentParser]], subparser.choices).get('run', None)
+ run_parser = cast('dict[str, Optional[CoreArgumentParser]]', subparser.choices).get('run', None)
assert run_parser is not None
assert getattr(run_parser, 'prog', None) == f'grizzly-cli {parent} run'
assert sorted([option_string for action in run_parser._actions for option_string in action.option_strings]) == sorted([
@@ -151,352 +152,348 @@ def test__create_parser() -> None:
assert sorted([action.dest for action in run_parser._actions if len(action.option_strings) == 0]) == ['file']
-def test__parse_argument(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None:
+def test__parse_argument(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None: # noqa: PLR0915
test_context = tmp_path_factory.mktemp('test_context')
(test_context / 'test.feature').write_text('Feature:')
- test_context_root = str(test_context)
- import sys
+ import sys # noqa: PLC0415
try:
- mocker.patch('grizzly_cli.EXECUTION_CONTEXT', test_context_root)
- chdir(test_context_root)
- sys.argv = ['grizzly-cli']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
- capture = capsys.readouterr()
- assert capture.out == ''
- assert 'usage: grizzly-cli' in capture.err
- assert 'grizzly-cli: error: no command specified' in capture.err
-
- sys.argv = ['grizzly-cli', '--version']
-
- mocker.patch('grizzly_cli.__main__.__version__', '0.0.0')
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == 'grizzly-cli (development)\n'
-
- sys.argv = ['grizzly-cli', '--version', 'foo']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
- capture = capsys.readouterr()
- err = capture.err.split('\n')
- assert len(err) == 4
- assert err[0].startswith('usage: grizzly-cli')
- assert 'init,local,dist,auth,keyvault' in err[1]
- assert err[2] == (
- "grizzly-cli: error: argument --version: invalid choice: 'foo' (choose from 'all')"
- ) or (
- "grizzly-cli: error: argument --version: invalid choice: 'foo' (choose from all)"
- )
- assert err[3] == ''
- assert capture.out == ''
-
- requirements_file = test_context / 'requirements.txt'
- requirements_file.write_text('grizzly-loadtester==1.5.3\n')
-
- sys.argv = ['grizzly-cli', '--version', 'all']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- 'grizzly-cli (development)\n'
- '└── grizzly 1.5.3\n'
- ' └── locust 2.2.1\n'
- )
-
- requirements_file.write_text('grizzly-loadtester[mq]==1.5.3\n')
-
- sys.argv = ['grizzly-cli', '--version', 'all']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- 'grizzly-cli (development)\n'
- '└── grizzly 1.5.3 ── extras: mq\n'
- ' └── locust 2.2.1\n'
- )
-
- requirements_file.unlink()
- requirements_file.write_text('grizzly-loadtester[mq,dev]==1.5.3\n')
-
- sys.argv = ['grizzly-cli', '--version', 'all']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- 'grizzly-cli (development)\n'
- '└── grizzly 1.5.3 ── extras: mq, dev\n'
- ' └── locust 2.2.1\n'
- )
-
- def mocked_mkdtemp(prefix: Optional[str] = '') -> str:
- tmp_dir = path.join(test_context, f'{prefix}test')
-
- return tmp_dir
-
- mocker.patch('grizzly_cli.utils.mkdtemp', mocked_mkdtemp)
- mocker.patch('grizzly_cli.utils.subprocess.check_call', return_value=0)
- mocker.patch('grizzly_cli.utils.subprocess.check_output', return_value='main\n')
-
- repo = 'git+https://git@github.com/biometria-se/grizzly.git@main#egg=grizzly-loadtester'
- repo_suffix = sha1(repo.encode('utf-8')).hexdigest()
- repo_dir = test_context / 'grizzly-cli-test' / f'grizzly-loadtester_{repo_suffix}'
- repo_dir.mkdir(parents=True)
- (repo_dir / 'pyproject.toml').touch()
- (repo_dir / 'setup.cfg').write_text('name = grizzly-loadtester\nversion = 0.0.0\n')
- (repo_dir / 'requirements.txt').write_text('locust==2.8.4 \\ \n')
-
- requirements_file.unlink()
- requirements_file.write_text(f'{repo}\n')
-
- sys.argv = ['grizzly-cli', '--version', 'all']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
-
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- 'grizzly-cli (development)\n'
- '└── grizzly (development)\n'
- ' └── locust 2.8.4\n'
- )
-
- repo = 'git+https://git@github.com/biometria-se/grizzly.git@main#egg=grizzly-loadtester[mq,dev]'
- repo_suffix = sha1(repo.encode('utf-8')).hexdigest()
- repo_dir = test_context / 'grizzly-cli-test' / f'grizzly-loadtester__mq_dev___{repo_suffix}'
- repo_dir.mkdir(parents=True)
- (repo_dir / 'pyproject.toml').touch()
- (repo_dir / 'setup.cfg').write_text('name = grizzly-loadtester\nversion = 0.0.0\n')
- (repo_dir / 'requirements.txt').write_text('locust==2.8.4 \\ \n')
-
- requirements_file.unlink()
- requirements_file.write_text(f'{repo}\n')
-
- sys.argv = ['grizzly-cli', '--version', 'all']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
-
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- 'grizzly-cli (development)\n'
- '└── grizzly (development) ── extras: mq, dev\n'
- ' └── locust 2.8.4\n'
- )
-
- requirements_file.unlink()
- requirements_file.write_text('grizzly-loadtester==1.5.3\n')
-
- sys.argv = ['grizzly-cli', '--version', 'all']
- mocker.patch('grizzly_cli.__main__.__version__', '2.5.0')
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- 'grizzly-cli 2.5.0\n'
- '└── grizzly 1.5.3\n'
- ' └── locust 2.2.1\n'
- )
-
- sys.argv = ['grizzly-cli', '--version', 'all']
- mocker.patch('grizzly_cli.__main__.__version__', '2.5.0')
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 0
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == (
- 'grizzly-cli 2.5.0\n'
- '└── grizzly 1.5.3\n'
- ' └── locust 2.2.1\n'
- )
-
- sys.argv = ['grizzly-cli', 'local']
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
- capture = capsys.readouterr()
- assert capture.out == ''
- assert 'grizzly-cli: error: no subcommand for local specified\n' == capture.err
-
- sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
-
- mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=[None])
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
- capture = capsys.readouterr()
- assert capture.out == ''
- assert capture.err == 'grizzly-cli: error: cannot run distributed\n'
-
- mocker.patch('grizzly_cli.EXECUTION_CONTEXT', getcwd())
- mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'])
- mocker.patch('grizzly_cli.distributed.do_build', side_effect=[0, 4, 0])
-
- sys.argv = ['grizzly-cli', 'dist', '--limit-nofile', '100', '--registry', 'ghcr.io/biometria-se', 'run', 'test.feature']
- (test_context / 'requirements.txt').write_text('grizzly-loadtester')
- mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'])
- ask_yes_no = mocker.patch('grizzly_cli.__main__.ask_yes_no', autospec=True)
-
- arguments = _parse_arguments()
- capture = capsys.readouterr()
- assert arguments.limit_nofile == 100
- assert not arguments.yes
- assert arguments.registry == 'ghcr.io/biometria-se/'
- assert capture.out == '!! this will cause warning messages from locust later on\n'
- assert capture.err == ''
- assert ask_yes_no.call_count == 1
- args, _ = ask_yes_no.call_args_list[-1]
- assert args[0] == 'are you sure you know what you are doing?'
-
- sys.argv = ['grizzly-cli', 'local', 'run', 'test.feature']
- mocker.patch('grizzly_cli.__main__.which', side_effect=[None])
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
-
- capture = capsys.readouterr()
- assert capture.out == ''
- assert capture.err == 'grizzly-cli: error: "behave" not found in PATH, needed when running local mode\n'
-
- # csv logging
- sys.argv = ['grizzly-cli', 'local', 'run', '--csv-interval', '20', 'test.feature']
- mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
-
- capture = capsys.readouterr()
- assert capture.out == ''
- assert capture.err == 'grizzly-cli: error: --csv-interval can only be used in combination with --csv-prefix\n'
-
- sys.argv = ['grizzly-cli', 'dist', 'run', '--csv-flush-interval', '60', 'test.feature']
- mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'])
-
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
-
- capture = capsys.readouterr()
- assert capture.out == ''
- assert capture.err == 'grizzly-cli: error: --csv-flush-interval can only be used in combination with --csv-prefix\n'
-
- sys.argv = ['grizzly-cli', 'local', 'run', '--csv-prefix', '--csv-interval', '20', '--csv-flush-interval', '60', 'test.feature']
- mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
-
- parsed_args = _parse_arguments()
-
- assert getattr(parsed_args, 'csv_prefix', False)
- assert getattr(parsed_args, 'csv_interval', None) == 20
- assert getattr(parsed_args, 'csv_flush_interval', None) == 60
-
- sys.argv = ['grizzly-cli', 'local', 'run', '--csv-prefix', 'static csv prefix', 'test.feature']
- mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
-
- parsed_args = _parse_arguments()
-
- assert getattr(parsed_args, 'csv_prefix', None) == 'static csv prefix'
- assert getattr(parsed_args, 'csv_interval', None) is None
- assert getattr(parsed_args, 'csv_flush_interval', None) is None
- # // csv logging
+ mocker.patch('grizzly_cli.EXECUTION_CONTEXT', test_context.as_posix())
+ with cwd(test_context):
+ sys.argv = ['grizzly-cli']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
+ capture = capsys.readouterr()
+ assert capture.out == ''
+ assert 'usage: grizzly-cli' in capture.err
+ assert 'grizzly-cli: error: no command specified' in capture.err
+
+ sys.argv = ['grizzly-cli', '--version']
+
+ mocker.patch('grizzly_cli.__main__.__version__', '0.0.0')
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == 'grizzly-cli (development)\n'
+
+ sys.argv = ['grizzly-cli', '--version', 'foo']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
+ capture = capsys.readouterr()
+ err = capture.err.split('\n')
+ assert len(err) == 4
+ assert err[0].startswith('usage: grizzly-cli')
+ assert 'init,local,auth,dist,keyvault' in err[1]
+ assert err[2] == (
+ "grizzly-cli: error: argument --version: invalid choice: 'foo' (choose from 'all')"
+ ) or (
+ "grizzly-cli: error: argument --version: invalid choice: 'foo' (choose from all)"
+ )
+ assert err[3] == ''
+ assert capture.out == ''
+
+ requirements_file = test_context / 'requirements.txt'
+ requirements_file.write_text('grizzly-loadtester==1.5.3\n')
+
+ sys.argv = ['grizzly-cli', '--version', 'all']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ 'grizzly-cli (development)\n'
+ '└── grizzly 1.5.3\n'
+ ' └── locust 2.2.1\n'
+ )
+
+ requirements_file.write_text('grizzly-loadtester[mq]==1.5.3\n')
+
+ sys.argv = ['grizzly-cli', '--version', 'all']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ 'grizzly-cli (development)\n'
+ '└── grizzly 1.5.3 ── extras: mq\n'
+ ' └── locust 2.2.1\n'
+ )
+
+ requirements_file.unlink()
+ requirements_file.write_text('grizzly-loadtester[mq,dev]==1.5.3\n')
+
+ sys.argv = ['grizzly-cli', '--version', 'all']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ 'grizzly-cli (development)\n'
+ '└── grizzly 1.5.3 ── extras: mq, dev\n'
+ ' └── locust 2.2.1\n'
+ )
+
+ def mocked_mkdtemp(prefix: Optional[str] = '') -> str:
+ return Path.joinpath(test_context, f'{prefix}test').as_posix()
+
+ mocker.patch('grizzly_cli.utils.mkdtemp', mocked_mkdtemp)
+ mocker.patch('grizzly_cli.utils.subprocess.check_call', return_value=0)
+ mocker.patch('grizzly_cli.utils.subprocess.check_output', return_value='main\n')
+
+ repo = 'git+https://git@github.com/biometria-se/grizzly.git@main#egg=grizzly-loadtester'
+ repo_suffix = sha1(repo.encode('utf-8')).hexdigest() # noqa: S324
+ repo_dir = test_context / 'grizzly-cli-test' / f'grizzly-loadtester_{repo_suffix}'
+ repo_dir.mkdir(parents=True)
+ (repo_dir / 'pyproject.toml').touch()
+ (repo_dir / 'setup.cfg').write_text('name = grizzly-loadtester\nversion = 0.0.0\n')
+ (repo_dir / 'requirements.txt').write_text('locust==2.8.4 \\ \n')
+
+ requirements_file.unlink()
+ requirements_file.write_text(f'{repo}\n')
+
+ sys.argv = ['grizzly-cli', '--version', 'all']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ 'grizzly-cli (development)\n'
+ '└── grizzly (development)\n'
+ ' └── locust 2.8.4\n'
+ )
+
+ repo = 'git+https://git@github.com/biometria-se/grizzly.git@main#egg=grizzly-loadtester[mq,dev]'
+ repo_suffix = sha1(repo.encode('utf-8')).hexdigest() # noqa: S324
+ repo_dir = test_context / 'grizzly-cli-test' / f'grizzly-loadtester__mq_dev___{repo_suffix}'
+ repo_dir.mkdir(parents=True)
+ (repo_dir / 'pyproject.toml').touch()
+ (repo_dir / 'setup.cfg').write_text('name = grizzly-loadtester\nversion = 0.0.0\n')
+ (repo_dir / 'requirements.txt').write_text('locust==2.8.4 \\ \n')
+
+ requirements_file.unlink()
+ requirements_file.write_text(f'{repo}\n')
+
+ sys.argv = ['grizzly-cli', '--version', 'all']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ 'grizzly-cli (development)\n'
+ '└── grizzly (development) ── extras: mq, dev\n'
+ ' └── locust 2.8.4\n'
+ )
+
+ requirements_file.unlink()
+ requirements_file.write_text('grizzly-loadtester==1.5.3\n')
+
+ sys.argv = ['grizzly-cli', '--version', 'all']
+ mocker.patch('grizzly_cli.__main__.__version__', '2.5.0')
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ 'grizzly-cli 2.5.0\n'
+ '└── grizzly 1.5.3\n'
+ ' └── locust 2.2.1\n'
+ )
+
+ sys.argv = ['grizzly-cli', '--version', 'all']
+ mocker.patch('grizzly_cli.__main__.__version__', '2.5.0')
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 0
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == (
+ 'grizzly-cli 2.5.0\n'
+ '└── grizzly 1.5.3\n'
+ ' └── locust 2.2.1\n'
+ )
+
+ sys.argv = ['grizzly-cli', 'local']
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
+ capture = capsys.readouterr()
+ assert capture.out == ''
+ assert capture.err == 'grizzly-cli: error: no subcommand for local specified\n'
+
+ sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
+
+ mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=[None])
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
+ capture = capsys.readouterr()
+ assert capture.out == ''
+ assert capture.err == 'grizzly-cli: error: cannot run distributed\n'
+
+ mocker.patch('grizzly_cli.EXECUTION_CONTEXT', Path.cwd())
+ mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'])
+ mocker.patch('grizzly_cli.distributed.do_build', side_effect=[0, 4, 0])
+
+ sys.argv = ['grizzly-cli', 'dist', '--limit-nofile', '100', '--registry', 'ghcr.io/biometria-se', 'run', 'test.feature']
+ (test_context / 'requirements.txt').write_text('grizzly-loadtester')
+ mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'])
+ ask_yes_no = mocker.patch('grizzly_cli.__main__.ask_yes_no', autospec=True)
+
+ arguments = _parse_arguments()
+ capture = capsys.readouterr()
+ assert arguments.limit_nofile == 100
+ assert not arguments.yes
+ assert arguments.registry == 'ghcr.io/biometria-se/'
+ assert capture.out == '!! this will cause warning messages from locust later on\n'
+ assert capture.err == ''
+ assert ask_yes_no.call_count == 1
+ args, _ = ask_yes_no.call_args_list[-1]
+ assert args[0] == 'are you sure you know what you are doing?'
+
+ sys.argv = ['grizzly-cli', 'local', 'run', 'test.feature']
+ mocker.patch('grizzly_cli.__main__.which', side_effect=[None])
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
+
+ capture = capsys.readouterr()
+ assert capture.out == ''
+ assert capture.err == 'grizzly-cli: error: "behave" not found in PATH, needed when running local mode\n'
+
+ # csv logging
+ sys.argv = ['grizzly-cli', 'local', 'run', '--csv-interval', '20', 'test.feature']
+ mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
+
+ capture = capsys.readouterr()
+ assert capture.out == ''
+ assert capture.err == 'grizzly-cli: error: --csv-interval can only be used in combination with --csv-prefix\n'
+
+ sys.argv = ['grizzly-cli', 'dist', 'run', '--csv-flush-interval', '60', 'test.feature']
+ mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'])
+
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
+
+ capture = capsys.readouterr()
+ assert capture.out == ''
+ assert capture.err == 'grizzly-cli: error: --csv-flush-interval can only be used in combination with --csv-prefix\n'
+
+ sys.argv = ['grizzly-cli', 'local', 'run', '--csv-prefix', '--csv-interval', '20', '--csv-flush-interval', '60', 'test.feature']
+ mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
+
+ parsed_args = _parse_arguments()
+
+ assert getattr(parsed_args, 'csv_prefix', False)
+ assert getattr(parsed_args, 'csv_interval', None) == 20
+ assert getattr(parsed_args, 'csv_flush_interval', None) == 60
+
+ sys.argv = ['grizzly-cli', 'local', 'run', '--csv-prefix', 'static csv prefix', 'test.feature']
+ mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
+
+ parsed_args = _parse_arguments()
+
+ assert getattr(parsed_args, 'csv_prefix', None) == 'static csv prefix'
+ assert getattr(parsed_args, 'csv_interval', None) is None
+ assert getattr(parsed_args, 'csv_flush_interval', None) is None
+ # // csv logging
- # -T/--testdata-variable
- sys.argv = ['grizzly-cli', 'local', 'run', '-T', 'variable', 'test.feature']
- mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
+ # -T/--testdata-variable
+ sys.argv = ['grizzly-cli', 'local', 'run', '-T', 'variable', 'test.feature']
+ mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
- with pytest.raises(SystemExit) as se:
- _parse_arguments()
- assert se.type == SystemExit
- assert se.value.code == 2
+ with pytest.raises(SystemExit) as se:
+ _parse_arguments()
+ assert se.type is SystemExit
+ assert se.value.code == 2
- capture = capsys.readouterr()
- assert capture.out == ''
- assert capture.err == 'grizzly-cli: error: -T/--testdata-variable needs to be in the format NAME=VALUE\n'
+ capture = capsys.readouterr()
+ assert capture.out == ''
+ assert capture.err == 'grizzly-cli: error: -T/--testdata-variable needs to be in the format NAME=VALUE\n'
- sys.argv = ['grizzly-cli', 'local', 'run', '-T', 'key=value', 'test.feature']
- mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
+ sys.argv = ['grizzly-cli', 'local', 'run', '-T', 'key=value', 'test.feature']
+ mocker.patch('grizzly_cli.__main__.which', side_effect=['behave'])
- assert environ.get('TESTDATA_VARIABLE_key', None) is None
-
- arguments = _parse_arguments()
- assert arguments.command == 'local'
- assert arguments.subcommand == 'run'
- assert arguments.file == 'test.feature'
+ assert environ.get('TESTDATA_VARIABLE_key', None) is None # noqa: SIM112
+
+ arguments = _parse_arguments()
+ assert arguments.command == 'local'
+ assert arguments.subcommand == 'run'
+ assert arguments.file == 'test.feature'
- assert environ.get('TESTDATA_VARIABLE_key', None) == 'value'
- # // -T/--testdata-variable
+ assert environ.get('TESTDATA_VARIABLE_key', None) == 'value' # noqa: SIM112
+ # // -T/--testdata-variable
- mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'] * 3)
+ mocker.patch('grizzly_cli.__main__.get_distributed_system', side_effect=['docker'] * 3)
- sys.argv = ['grizzly-cli', 'dist', 'build']
- arguments = _parse_arguments()
+ sys.argv = ['grizzly-cli', 'dist', 'build']
+ arguments = _parse_arguments()
- assert not arguments.no_cache
- assert not arguments.force_build
- assert arguments.build
- assert arguments.registry is None
+ assert not arguments.no_cache
+ assert not arguments.force_build
+ assert arguments.build
+ assert arguments.registry is None
- sys.argv = ['grizzly-cli', 'dist', 'build', '--no-cache', '--registry', 'registry.example.com/biometria-se']
- arguments = _parse_arguments()
+ sys.argv = ['grizzly-cli', 'dist', 'build', '--no-cache', '--registry', 'registry.example.com/biometria-se']
+ arguments = _parse_arguments()
- assert arguments.no_cache
- assert arguments.force_build
- assert not arguments.build
- assert arguments.registry == 'registry.example.com/biometria-se/'
+ assert arguments.no_cache
+ assert arguments.force_build
+ assert not arguments.build
+ assert arguments.registry == 'registry.example.com/biometria-se/'
- sys.argv = ['grizzly-cli', 'init', 'test-project']
- arguments = _parse_arguments()
+ sys.argv = ['grizzly-cli', 'init', 'test-project']
+ arguments = _parse_arguments()
- assert arguments.project == 'test-project'
- assert getattr(arguments, 'subcommand', None) is None
+ assert arguments.project == 'test-project'
+ assert getattr(arguments, 'subcommand', None) is None
finally:
- chdir(CWD)
- rm_rf(test_context_root)
+ rm_rf(test_context)
def test__inject_additional_arguments_from_metadata(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker: MockerFixture) -> None:
@@ -507,69 +504,65 @@ def test__inject_additional_arguments_from_metadata(tmp_path_factory: TempPathFa
mocker.patch('grizzly_cli.__main__.get_distributed_system', return_value='docker')
try:
- chdir(test_context)
- test_feature_file.write_text('# grizzly-cli run --verbose\nFeature:\n')
- sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
+ with cwd(test_context):
+ test_feature_file.write_text('# grizzly-cli run --verbose\nFeature:\n')
+ sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
- orig_args = _parse_arguments()
- assert not orig_args.verbose
+ orig_args = _parse_arguments()
+ assert not orig_args.verbose
- args = _inject_additional_arguments_from_metadata(orig_args)
- capture = capsys.readouterr()
+ args = _inject_additional_arguments_from_metadata(orig_args)
+ capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == ''
- assert args.verbose
+ assert capture.err == capture.out == ''
+ assert args.verbose
- test_feature_file.write_text('# grizzly-cli local --hello\nFeature:\n')
- sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
+ test_feature_file.write_text('# grizzly-cli local --hello\nFeature:\n')
+ sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
- orig_args = _parse_arguments()
- args = _inject_additional_arguments_from_metadata(orig_args)
+ orig_args = _parse_arguments()
+ args = _inject_additional_arguments_from_metadata(orig_args)
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == '?? ignoring local --hello\n'
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == '?? ignoring local --hello\n'
- test_feature_file.write_text('Feature:\n')
- sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
+ test_feature_file.write_text('Feature:\n')
+ sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
- orig_args = _parse_arguments()
- args = _inject_additional_arguments_from_metadata(orig_args)
+ orig_args = _parse_arguments()
+ args = _inject_additional_arguments_from_metadata(orig_args)
- assert args is orig_args
+ assert args is orig_args
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == ''
+ capture = capsys.readouterr()
+ assert capture.err == capture.out == ''
- test_feature_file.write_text('# grizzly-cli --health-timeout 100\nFeature:\n')
- sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
+ test_feature_file.write_text('# grizzly-cli --health-timeout 100\nFeature:\n')
+ sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
- orig_args = _parse_arguments()
- args = _inject_additional_arguments_from_metadata(orig_args)
+ orig_args = _parse_arguments()
+ args = _inject_additional_arguments_from_metadata(orig_args)
- assert args.health_timeout == orig_args.health_timeout
+ assert args.health_timeout == orig_args.health_timeout
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == '?? ignoring --health-timeout 100\n'
+ capture = capsys.readouterr()
+ assert capture.err == ''
+ assert capture.out == '?? ignoring --health-timeout 100\n'
- test_feature_file.write_text('# grizzly-cli dist --health-timeout 100 --health-retries 101\nFeature:\n# grizzly-cli dist --health-interval 5\n')
- sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
+ test_feature_file.write_text('# grizzly-cli dist --health-timeout 100 --health-retries 101\nFeature:\n# grizzly-cli dist --health-interval 5\n')
+ sys.argv = ['grizzly-cli', 'dist', 'run', 'test.feature']
- orig_args = _parse_arguments()
- args = _inject_additional_arguments_from_metadata(orig_args)
+ orig_args = _parse_arguments()
+ args = _inject_additional_arguments_from_metadata(orig_args)
- assert args.health_timeout == 100
- assert args.health_retries == 101
- assert args.health_interval == 5
+ assert args.health_timeout == 100
+ assert args.health_retries == 101
+ assert args.health_interval == 5
- capture = capsys.readouterr()
- assert capture.err == ''
- assert capture.out == ''
+ capture = capsys.readouterr()
+ assert capture.err == capture.out == ''
finally:
- chdir(CWD)
rm_rf(test_context)
@@ -589,25 +582,28 @@ def test_main(mocker: MockerFixture, capsys: CaptureFixture) -> None:
KeyboardInterrupt,
ValueError('hello there'),
Namespace(command='dist', file='test.feature'),
- ],)
+ ])
assert main() == 0
- assert local_mock.call_count == 1
- assert dist_mock.call_count == 0
- assert init_mock.call_count == 0
- assert inject_additional_arguments_from_metadata_mock.call_count == 0
+ local_mock.assert_called_once_with(SOME(Namespace, command='local'))
+ local_mock.reset_mock()
+ dist_mock.assert_not_called()
+ init_mock.assert_not_called()
+ inject_additional_arguments_from_metadata_mock.assert_not_called()
assert main() == 1337
- assert local_mock.call_count == 1
- assert dist_mock.call_count == 1
- assert init_mock.call_count == 0
- assert inject_additional_arguments_from_metadata_mock.call_count == 0
+ local_mock.assert_not_called()
+ dist_mock.assert_called_once_with(SOME(Namespace, command='dist'))
+ dist_mock.reset_mock()
+ init_mock.assert_not_called()
+ inject_additional_arguments_from_metadata_mock.assert_not_called()
assert main() == 7331
- assert local_mock.call_count == 1
- assert dist_mock.call_count == 1
- assert init_mock.call_count == 1
- assert inject_additional_arguments_from_metadata_mock.call_count == 0
+ local_mock.assert_not_called()
+ dist_mock.assert_not_called()
+ init_mock.assert_called_once_with(SOME(Namespace, command='init'))
+ init_mock.reset_mock()
+ inject_additional_arguments_from_metadata_mock.assert_not_called()
assert main() == 1
@@ -629,9 +625,8 @@ def test_main(mocker: MockerFixture, capsys: CaptureFixture) -> None:
assert main() == 1373
capture = capsys.readouterr()
- print(capture.err)
- print(capture.out)
- assert local_mock.call_count == 1
- assert dist_mock.call_count == 2
- assert init_mock.call_count == 1
- assert inject_additional_arguments_from_metadata_mock.call_count == 1
+ local_mock.assert_not_called()
+ dist_mock.assert_called_once_with(SOME(Namespace, command='dist', file='test.feature'))
+ dist_mock.reset_mock()
+ init_mock.assert_not_called()
+ inject_additional_arguments_from_metadata_mock.assert_called_once_with(SOME(Namespace, command='dist', file='test.feature'))
diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py
index d14deb4..23b12d6 100644
--- a/tests/unit/test_auth.py
+++ b/tests/unit/test_auth.py
@@ -1,35 +1,37 @@
-import sys
+from __future__ import annotations
+import sys
+from contextlib import suppress
from os import environ
+from typing import TYPE_CHECKING
import pytest
-from _pytest.tmpdir import TempPathFactory
-from _pytest.capture import CaptureFixture
-from pytest_mock import MockerFixture
-
from grizzly_cli.__main__ import _parse_arguments
from grizzly_cli.auth import auth
+if TYPE_CHECKING: # pragma: no cover
+ from _pytest.capture import CaptureFixture
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
+
def test_auth_env(capsys: CaptureFixture, mocker: MockerFixture) -> None:
try:
sys.argv = ['grizzly-cli', 'auth']
arguments = _parse_arguments()
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='environment variable OTP_SECRET is not set'):
auth(arguments)
- assert str(ve.value) == 'environment variable OTP_SECRET is not set'
capsys.readouterr()
- environ['OTP_SECRET'] = 'f00bar='
+ environ['OTP_SECRET'] = 'f00bar=' # noqa: S105
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='unable to generate TOTP code: Non-base32 digit found'):
auth(arguments)
- assert str(ve.value) == 'unable to generate TOTP code: Non-base32 digit found'
- environ['OTP_SECRET'] = 'asdfasdf'
+ environ['OTP_SECRET'] = 'asdfasdf' # noqa: S105
mocker.patch('grizzly_cli.auth.TOTP.now', return_value=111111)
assert auth(arguments) == 0
@@ -39,10 +41,8 @@ def test_auth_env(capsys: CaptureFixture, mocker: MockerFixture) -> None:
assert capture.err == ''
assert capture.out == '111111\n'
finally:
- try:
+ with suppress(KeyError):
del environ['OTP_SECRET']
- except:
- pass
def test_auth_stdin(capsys: CaptureFixture, mocker: MockerFixture) -> None:
@@ -51,21 +51,18 @@ def test_auth_stdin(capsys: CaptureFixture, mocker: MockerFixture) -> None:
mocker.patch('sys.stdin.read', return_value=None)
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='OTP secret could not be read from stdin'):
auth(arguments)
- assert str(ve.value) == 'OTP secret could not be read from stdin'
mocker.patch('sys.stdin.read', return_value=' ')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='OTP secret could not be read from stdin'):
auth(arguments)
- assert str(ve.value) == 'OTP secret could not be read from stdin'
mocker.patch('sys.stdin.read', return_value='f00bar=')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='unable to generate TOTP code: Non-base32 digit found'):
auth(arguments)
- assert str(ve.value) == 'unable to generate TOTP code: Non-base32 digit found'
capsys.readouterr()
@@ -88,33 +85,28 @@ def test_auth_file(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_facto
sys.argv = ['grizzly-cli', 'auth', str(file)]
arguments = _parse_arguments()
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match=f'file {file.as_posix()} does not exist'):
auth(arguments)
- assert str(ve.value) == f'file {file} does not exist'
file.write_text(' ')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match=f'file {file.as_posix()} does not seem to contain a single line with a valid OTP secret'):
auth(arguments)
- assert str(ve.value) == f'file {file} does not seem to contain a single line with a valid OTP secret'
file.write_text('aasdf\nasdfasdf\n')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match=f'file {file.as_posix()} does not seem to contain a single line with a valid OTP secret'):
auth(arguments)
- assert str(ve.value) == f'file {file} does not seem to contain a single line with a valid OTP secret'
file.write_text('hello world\n')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match=f'file {file.as_posix()} does not seem to contain a single line with a valid OTP secret'):
auth(arguments)
- assert str(ve.value) == f'file {file} does not seem to contain a single line with a valid OTP secret'
file.write_text('f00bar=\n')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='unable to generate TOTP code: Non-base32 digit found'):
auth(arguments)
- assert str(ve.value) == 'unable to generate TOTP code: Non-base32 digit found'
file.write_text('asdfasdf')
mocker.patch('grizzly_cli.auth.TOTP.now', return_value=333333)
diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py
index 52d28d6..68158b3 100644
--- a/tests/unit/test_init.py
+++ b/tests/unit/test_init.py
@@ -1,14 +1,17 @@
-import sys
+from __future__ import annotations
-from _pytest.tmpdir import TempPathFactory
-from _pytest.capture import CaptureFixture
-from pytest_mock import MockerFixture
+import sys
+from typing import TYPE_CHECKING
-from grizzly_cli.init import tree, init
from grizzly_cli.__main__ import _parse_arguments
-
+from grizzly_cli.init import init, tree
from tests.helpers import rm_rf
+if TYPE_CHECKING: # pragma: no cover
+ from _pytest.capture import CaptureFixture
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
+
def test_tree(tmp_path_factory: TempPathFactory) -> None:
test_context = tmp_path_factory.mktemp('test_context')
@@ -26,7 +29,7 @@ def test_tree(tmp_path_factory: TempPathFactory) -> None:
(test_context / 'root.yaml').touch()
try:
- assert '\n'.join([line for line in tree(test_context)]) == '''├── a
+ assert '\n'.join(list(tree(test_context))) == """├── a
│ ├── b
│ │ ├── c
│ │ │ ├── file-c1.txt
@@ -34,12 +37,12 @@ def test_tree(tmp_path_factory: TempPathFactory) -> None:
│ │ └── file-b1.txt
│ ├── file-a1.txt
│ └── file-a2.txt
-└── root.yaml'''
+└── root.yaml"""
finally:
rm_rf(test_context)
-def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker: MockerFixture) -> None:
+def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker: MockerFixture) -> None: # noqa: PLR0915
test_context = tmp_path_factory.mktemp('test_context')
test_existing = test_context / 'foobar'
@@ -67,13 +70,13 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
capture = capsys.readouterr()
assert capture.err == ''
- assert capture.out == f'''oops, looks like you are already in a grizzly project directory
+ assert capture.out == f"""oops, looks like you are already in a grizzly project directory
{test_existing}
├── environments
├── features
└── requirements.txt
-'''
+"""
rm_rf(test_existing)
@@ -84,7 +87,7 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
assert question_mock.call_count == 1
args, _ = question_mock.call_args_list[-1]
- assert args[0] == '''the following structure will be created:
+ assert args[0] == """the following structure will be created:
foobar
├── environments
@@ -97,32 +100,32 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
│ └── requests
└── requirements.txt
-do you want to create grizzly project "foobar"?'''
+do you want to create grizzly project "foobar"?"""
capture = capsys.readouterr()
assert capture.err == ''
- assert capture.out == '''successfully created project "foobar", with the following options:
+ assert capture.out == """successfully created project "foobar", with the following options:
• without IBM MQ support
• latest grizzly version
-'''
+"""
template_root = test_context / 'foobar'
assert template_root.is_dir()
assert (template_root / 'environments').is_dir()
environments_file = template_root / 'environments' / 'foobar.yaml'
assert environments_file.is_file()
- assert environments_file.read_text() == '''configuration:
+ assert environments_file.read_text() == """configuration:
template:
host: https://localhost
-'''
+"""
assert (template_root / 'features').is_dir()
feature_file = template_root / 'features' / 'foobar.feature'
assert feature_file.is_file()
- assert feature_file.read_text() == '''Feature: Template feature file
+ assert feature_file.read_text() == """Feature: Template feature file
Scenario: Template scenario
Given a user of type "RestApi" with weight "1" load testing "$conf::template.host"
-'''
+"""
environment_file = template_root / 'features' / 'environment.py'
assert environment_file.is_file()
@@ -140,8 +143,8 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
assert requirements_file.is_file()
assert requirements_file.read_text() == 'grizzly-loadtester\n'
- created_structure = '\n'.join([line for line in tree(template_root)])
- assert created_structure == '''├── environments
+ created_structure = '\n'.join(list(tree(template_root)))
+ assert created_structure == """├── environments
│ └── foobar.yaml
├── features
│ ├── environment.py
@@ -149,7 +152,7 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
│ ├── requests
│ └── steps
│ └── steps.py
-└── requirements.txt'''
+└── requirements.txt"""
rm_rf(template_root)
@@ -162,10 +165,10 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
capture = capsys.readouterr()
assert capture.err == ''
- assert capture.out == '''successfully created project "foobar", with the following options:
+ assert capture.out == """successfully created project "foobar", with the following options:
• with IBM MQ support
• latest grizzly version
-'''
+"""
requirements_file = template_root / 'requirements.txt'
assert requirements_file.is_file()
assert requirements_file.read_text() == 'grizzly-loadtester[mq]\n'
@@ -179,10 +182,10 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
capture = capsys.readouterr()
assert capture.err == ''
- assert capture.out == '''successfully created project "foobar", with the following options:
+ assert capture.out == """successfully created project "foobar", with the following options:
• without IBM MQ support
• pinned to grizzly version 1.2.4
-'''
+"""
requirements_file = template_root / 'requirements.txt'
assert requirements_file.is_file()
assert requirements_file.read_text() == 'grizzly-loadtester==1.2.4\n'
@@ -190,10 +193,10 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
assert (template_root / 'features').is_dir()
feature_file = template_root / 'features' / 'foobar.feature'
assert feature_file.is_file()
- assert feature_file.read_text() == '''Feature: Template feature file
+ assert feature_file.read_text() == """Feature: Template feature file
Scenario: Template scenario
Given a user of type "RestApi" with weight "1" load testing "$conf::template.host"
-'''
+"""
environment_file = template_root / 'features' / 'environment.py'
assert environment_file.is_file()
@@ -208,10 +211,10 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
capture = capsys.readouterr()
assert capture.err == ''
- assert capture.out == '''successfully created project "foobar", with the following options:
+ assert capture.out == """successfully created project "foobar", with the following options:
• with IBM MQ support
• pinned to grizzly version 1.5.0
-'''
+"""
requirements_file = template_root / 'requirements.txt'
assert requirements_file.is_file()
assert requirements_file.read_text() == 'grizzly-loadtester[mq]==1.5.0\n'
@@ -225,7 +228,7 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
capture = capsys.readouterr()
assert capture.err == ''
- assert capture.out == '''the following structure will be created:
+ assert capture.out == """the following structure will be created:
foobar
├── environments
@@ -241,7 +244,7 @@ def test_init(tmp_path_factory: TempPathFactory, capsys: CaptureFixture, mocker:
successfully created project "foobar", with the following options:
• without IBM MQ support
• latest grizzly version
-'''
+"""
requirements_file = template_root / 'requirements.txt'
assert requirements_file.is_file()
assert requirements_file.read_text() == 'grizzly-loadtester\n'
diff --git a/tests/unit/test_keyvault.py b/tests/unit/test_keyvault.py
index 8aaa5cc..39bea38 100644
--- a/tests/unit/test_keyvault.py
+++ b/tests/unit/test_keyvault.py
@@ -1,22 +1,26 @@
from __future__ import annotations
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock.plugin import MockerFixture
+from typing import TYPE_CHECKING
+
from grizzly_cli.keyvault import (
- _keyvault_normalize,
- _should_export,
- _determine_environment,
+ COMMON_FALSE_POSITIVES,
+ KEYWORDS,
+ KeyvaultSecretHolder,
_build_key_name,
+ _determine_environment,
_dict_to_yaml,
_extract_metadata,
- encode_mq_certificate,
+ _keyvault_normalize,
+ _should_export,
encode_file,
- KeyvaultSecretHolder,
- KEYWORDS,
- COMMON_FALSE_POSITIVES,
+ encode_mq_certificate,
)
from tests.helpers import SOME, rm_rf
+if TYPE_CHECKING: # pragma: no cover
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock.plugin import MockerFixture
+
def test__keyvault_normalize() -> None:
assert _keyvault_normalize('fo0b4R') == 'fo0b4R'
@@ -95,7 +99,7 @@ def test__dict_to_yaml(tmp_path_factory: TempPathFactory) -> None:
content: dict = {
'foo': {
- 'bar': 'hello world'
+ 'bar': 'hello world',
},
'hello': 'world',
'test': {
@@ -145,7 +149,7 @@ def test__extract_metadata(tmp_path_factory: TempPathFactory) -> None:
'env': 'test',
'foo': {
'bar': 'hello world',
- }
+ },
},
}
@@ -165,7 +169,7 @@ def test__extract_metadata(tmp_path_factory: TempPathFactory) -> None:
'configuration': {
'foo': {
'bar': 'hello world',
- }
+ },
},
}
diff --git a/tests/unit/test_local.py b/tests/unit/test_local.py
index d48626f..fd97361 100644
--- a/tests/unit/test_local.py
+++ b/tests/unit/test_local.py
@@ -1,17 +1,18 @@
+from __future__ import annotations
-from os import getcwd, environ
from argparse import ArgumentParser, Namespace
+from contextlib import suppress
+from os import environ
+from typing import TYPE_CHECKING
import pytest
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
-
+from grizzly_cli.local import create_parser, local, local_run
from grizzly_cli.utils import RunCommandResult, rm_rf
-from grizzly_cli.local import create_parser, local_run, local
-
-CWD = getcwd()
+if TYPE_CHECKING:
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
def test_local(mocker: MockerFixture) -> None:
@@ -26,9 +27,8 @@ def test_local(mocker: MockerFixture) -> None:
assert args[1] is local_run
arguments = Namespace(subcommand='foo')
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='unknown subcommand foo'):
local(arguments)
- assert 'unknown subcommand foo' == str(ve.value)
def test_local_run(mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None:
@@ -49,7 +49,7 @@ def test_local_run(mocker: MockerFixture, tmp_path_factory: TempPathFactory) ->
'local', 'run', f'{test_context}/test.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert local_run(
arguments,
@@ -101,7 +101,5 @@ def test_local_run(mocker: MockerFixture, tmp_path_factory: TempPathFactory) ->
assert environ.get('GRIZZLY_TEST_VAR', None) == 'True'
finally:
rm_rf(test_context)
- try:
+ with suppress(KeyError):
del environ['GRIZZLY_TEST_VAR']
- except:
- pass
diff --git a/tests/unit/test_run.py b/tests/unit/test_run.py
index bb1b861..6038c0f 100644
--- a/tests/unit/test_run.py
+++ b/tests/unit/test_run.py
@@ -1,24 +1,27 @@
+from __future__ import annotations
+
import logging
-from os import path
from argparse import ArgumentParser
from datetime import datetime
from pathlib import Path
+from typing import TYPE_CHECKING
import pytest
-from _pytest.capture import CaptureFixture
-from _pytest.logging import LogCaptureFixture
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
from jinja2 import Environment
-from grizzly_cli.run import run, create_parser
+from grizzly_cli.run import create_parser, run
from grizzly_cli.utils import setup_logging
from grizzly_cli.utils.configuration import ScenarioTag
-
from tests.helpers import CaseInsensitive, rm_rf
+if TYPE_CHECKING:
+ from _pytest.capture import CaptureFixture
+ from _pytest.logging import LogCaptureFixture
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
-def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None:
+
+def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None: # noqa: PLR0915
setup_logging()
original_tmp_path = tmp_path_factory._basetemp
@@ -40,8 +43,8 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
create_parser(sub_parsers, parent='local')
try:
- mocker.patch('grizzly_cli.run.grizzly_cli.EXECUTION_CONTEXT', str(execution_context))
- mocker.patch('grizzly_cli.run.grizzly_cli.MOUNT_CONTEXT', str(mount_context))
+ mocker.patch('grizzly_cli.run.grizzly_cli.EXECUTION_CONTEXT', execution_context.as_posix())
+ mocker.patch('grizzly_cli.run.grizzly_cli.MOUNT_CONTEXT', mount_context.as_posix())
mocker.patch('grizzly_cli.run.get_hostname', return_value='localhost')
mocker.patch('grizzly_cli.run.find_variable_names_in_questions', side_effect=[['foo', 'bar'], [], [], [], [], [], [], []])
mocker.patch('grizzly_cli.run.find_metadata_notices', side_effect=[[], ['is the event log cleared?'], ['hello world', 'foo bar'], [], [], [], [], []])
@@ -51,41 +54,41 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
local_mock = mocker.MagicMock(return_value=0)
get_input_mock = mocker.patch('grizzly_cli.run.get_input', side_effect=['bar', 'foo'])
- setattr(getattr(run, '__wrapped__'), '__value__', str(execution_context))
+ setattr(getattr(run, '__wrapped__'), '__value__', execution_context.as_posix()) # noqa: B009, B010
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
- f'{execution_context}/features/test.feature',
- '--verbose'
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
+ f'{execution_context.as_posix()}/features/test.feature',
+ '--verbose',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, distributed_mock) == 0
capture = capsys.readouterr()
assert capture.out == ''
- assert capture.err == '''feature file requires values for 2 variables
+ assert capture.err == """feature file requires values for 2 variables
the following values was provided:
foo = bar
bar = foo
-'''
+"""
local_mock.assert_not_called()
distributed_mock.assert_called_once_with(
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
'TESTDATA_VARIABLE_foo': 'bar',
'TESTDATA_VARIABLE_bar': 'foo',
}, {
'master': [],
'worker': [],
'common': ['--verbose', '--no-logcapture', '--no-capture', '--no-capture-stderr'],
- }
+ },
)
distributed_mock.reset_mock()
@@ -110,10 +113,10 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
- f'{execution_context}/features/test.feature',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
+ f'{execution_context.as_posix()}/features/test.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
@@ -124,14 +127,14 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
}, {
'master': [],
'worker': [],
'common': [],
- }
+ },
)
local_mock.reset_mock()
@@ -145,11 +148,11 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
# with --yes, notices should only be printed, and not needed to be confirmed via ask_yes_no
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
+ f'{execution_context.as_posix()}/features/test.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
@@ -159,14 +162,14 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
}, {
'master': [],
'worker': [],
'common': [],
- }
+ },
)
local_mock.reset_mock()
@@ -179,12 +182,12 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
# no `csv_prefix` nothing should be added
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
+ f'{execution_context.as_posix()}/features/test.feature',
'--csv-interval', '20',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
@@ -192,14 +195,14 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
}, {
'master': [],
'worker': [],
'common': [],
- }
+ },
)
local_mock.reset_mock()
distributed_mock.assert_not_called()
@@ -207,13 +210,13 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
# static csv-prefix
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
+ f'{execution_context.as_posix()}/features/test.feature',
'--csv-interval', '20',
'--csv-prefix', 'test test',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
@@ -222,33 +225,33 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
}, {
'master': [],
'worker': [],
'common': ['-Dcsv-prefix="test test"', '-Dcsv-interval=20'],
- }
+ },
)
local_mock.reset_mock()
# dynamic csv-prefix
datetime_mock = mocker.patch(
'grizzly_cli.run.datetime',
- side_effect=lambda *args, **kwargs: datetime(*args, **kwargs)
+ side_effect=lambda *args, **kwargs: datetime(*args, **kwargs), # noqa: DTZ001
)
- datetime_mock.now.return_value = datetime(2022, 12, 6, 13, 1, 13)
+ datetime_mock.now.return_value = datetime(2022, 12, 6, 13, 1, 13) # noqa: DTZ001
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
+ f'{execution_context.as_posix()}/features/test.feature',
'--csv-prefix',
'--csv-interval', '20',
'--csv-flush-interval', '60',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, distributed_mock) == 0
@@ -257,29 +260,29 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
}, {
'master': [],
'worker': [],
'common': ['-Dcsv-prefix="this_feature_is_testing_something_20221206T130113"', '-Dcsv-interval=20', '-Dcsv-flush-interval=60'],
- }
+ },
)
distributed_mock.reset_mock()
- setattr(arguments, 'csv_prefix', None)
- setattr(arguments, 'csv_flush_interval', None)
+ arguments.csv_prefix = None
+ arguments.csv_flush_interval = None
# --log-dir
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
'--log-dir', 'foobar',
- f'{execution_context}/features/test.feature',
+ f'{execution_context.as_posix()}/features/test.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, distributed_mock) == 0
@@ -288,15 +291,15 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
'GRIZZLY_LOG_DIR': 'foobar',
}, {
'master': [],
'worker': [],
'common': [],
- }
+ },
)
distributed_mock.reset_mock()
@@ -305,13 +308,13 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
# --dry-run
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
'--log-dir', 'foobar',
- f'{execution_context}/features/test.feature',
- '--dry-run'
+ f'{execution_context.as_posix()}/features/test.feature',
+ '--dry-run',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, distributed_mock) == 0
@@ -320,16 +323,16 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
arguments,
{
'GRIZZLY_CLI_HOST': 'localhost',
- 'GRIZZLY_EXECUTION_CONTEXT': str(execution_context),
- 'GRIZZLY_MOUNT_CONTEXT': str(mount_context),
- 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(path.join(execution_context, 'configuration.lock.yaml')),
+ 'GRIZZLY_EXECUTION_CONTEXT': execution_context.as_posix(),
+ 'GRIZZLY_MOUNT_CONTEXT': mount_context.as_posix(),
+ 'GRIZZLY_CONFIGURATION_FILE': CaseInsensitive(Path.joinpath(execution_context, 'configuration.lock.yaml').as_posix()),
'GRIZZLY_LOG_DIR': 'foobar',
'GRIZZLY_DRY_RUN': 'true',
}, {
'master': [],
'worker': [],
'common': [],
- }
+ },
)
distributed_mock.reset_mock()
@@ -338,12 +341,12 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
# --dump
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
+ f'{execution_context.as_posix()}/features/test.feature',
'--dump',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, distributed_mock) == 0
@@ -359,7 +362,7 @@ def test_run(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: Te
rm_rf(test_context)
-def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None:
+def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None: # noqa: PLR0915
setup_logging()
original_tmp_path = tmp_path_factory._basetemp
@@ -381,8 +384,8 @@ def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factor
create_parser(sub_parsers, parent='local')
try:
- mocker.patch('grizzly_cli.run.grizzly_cli.EXECUTION_CONTEXT', str(execution_context))
- mocker.patch('grizzly_cli.run.grizzly_cli.MOUNT_CONTEXT', str(mount_context))
+ mocker.patch('grizzly_cli.run.grizzly_cli.EXECUTION_CONTEXT', execution_context.as_posix())
+ mocker.patch('grizzly_cli.run.grizzly_cli.MOUNT_CONTEXT', mount_context.as_posix())
mocker.patch('grizzly_cli.run.get_hostname', return_value='localhost')
mocker.patch('grizzly_cli.run.find_variable_names_in_questions', side_effect=[['foo', 'bar'], [], [], [], [], [], [], []])
mocker.patch('grizzly_cli.run.find_metadata_notices', side_effect=[[], ['is the event log cleared?'], ['hello world', 'foo bar'], [], [], [], [], []])
@@ -390,7 +393,7 @@ def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factor
distributed_mock = mocker.MagicMock(return_value=0)
local_mock = mocker.MagicMock(return_value=0)
- setattr(getattr(run, '__wrapped__'), '__value__', str(execution_context))
+ setattr(getattr(run, '__wrapped__'), '__value__', execution_context.as_posix()) # noqa: B009, B010
# --dump output.feature
feature_file.write_text("""Feature: a feature
@@ -438,9 +441,9 @@ def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factor
'-e', f'{execution_context}/configuration.yaml',
'--yes',
f'{execution_context}/features/test.feature',
- '--dump', f'{execution_context}/output.feature'
+ '--dump', f'{execution_context}/output.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
@@ -517,12 +520,12 @@ def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factor
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
- '--dump', f'{execution_context}/output.feature'
+ f'{execution_context.as_posix()}/features/test.feature',
+ '--dump', f'{execution_context.as_posix()}/output.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
@@ -600,22 +603,22 @@ def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factor
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
- '--dump', f'{execution_context}/output.feature'
+ f'{execution_context.as_posix()}/features/test.feature',
+ '--dump', f'{execution_context.as_posix()}/output.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError) as ve: # noqa: PT011
run(arguments, local_mock)
- assert str(ve.value) == '''the following variables has been declared in scenario tag but not used in ../second.feature#second:
+ assert str(ve.value) == """the following variables has been declared in scenario tag but not used in ../second.feature#second:
foo
the following variables was used in ../second.feature#second but was not declared in scenario tag:
bar
-'''
+"""
distributed_mock.assert_not_called()
local_mock.assert_not_called()
@@ -666,12 +669,12 @@ def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factor
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
- '--dump', f'{execution_context}/output.feature'
+ f'{execution_context.as_posix()}/features/test.feature',
+ '--dump', f'{execution_context.as_posix()}/output.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
@@ -722,12 +725,12 @@ def test_run_dump(capsys: CaptureFixture, mocker: MockerFixture, tmp_path_factor
arguments = parser.parse_args([
'run',
- '-e', f'{execution_context}/configuration.yaml',
+ '-e', f'{execution_context.as_posix()}/configuration.yaml',
'--yes',
- f'{execution_context}/features/test.feature',
- '--dump', f'{execution_context}/output.feature'
+ f'{execution_context.as_posix()}/features/test.feature',
+ '--dump', f'{execution_context.as_posix()}/output.feature',
])
- setattr(arguments, 'file', ' '.join(arguments.file))
+ arguments.file = ' '.join(arguments.file)
assert run(arguments, local_mock) == 0
diff --git a/tests/unit/utils/test___init__.py b/tests/unit/utils/test___init__.py
index 8c21fd5..a66392d 100644
--- a/tests/unit/utils/test___init__.py
+++ b/tests/unit/utils/test___init__.py
@@ -1,37 +1,39 @@
-from typing import Any, Dict, List, Tuple, Union
-from os import chdir, getcwd
-from textwrap import dedent
-from importlib import reload
+from __future__ import annotations
+
from argparse import Namespace
-from tempfile import gettempdir
from contextlib import ExitStack
+from importlib import reload
+from json.decoder import JSONDecodeError
+from pathlib import Path
+from tempfile import gettempdir
+from textwrap import dedent
+from typing import TYPE_CHECKING, Any, Union
+from unittest.mock import mock_open
+from unittest.mock import patch as unittest_patch
import pytest
-from _pytest.tmpdir import TempPathFactory
-from _pytest.capture import CaptureFixture
-from pytest_mock import MockerFixture
-from unittest.mock import mock_open, patch as unittest_patch
-from requests_mock import Mocker as RequestsMocker
-
from grizzly_cli.utils import (
- parse_feature_file,
- list_images,
+ ask_yes_no,
+ distribution_of_users_per_scenario,
+ find_metadata_notices,
+ find_variable_names_in_questions,
get_default_mtu,
+ get_dependency_versions,
+ get_distributed_system,
+ list_images,
+ parse_feature_file,
requirements,
run_command,
- get_distributed_system,
- find_variable_names_in_questions,
- distribution_of_users_per_scenario,
- ask_yes_no,
- get_dependency_versions,
- find_metadata_notices,
setup_logging,
)
+from tests.helpers import create_scenario, cwd, rm_rf
-from tests.helpers import create_scenario, rm_rf
-
-CWD = getcwd()
+if TYPE_CHECKING: # pragma: no cover
+ from _pytest.capture import CaptureFixture
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
+ from requests_mock import Mocker as RequestsMocker
def test_parse_feature_file(tmp_path_factory: TempPathFactory) -> None:
@@ -39,7 +41,7 @@ def test_parse_feature_file(tmp_path_factory: TempPathFactory) -> None:
test_context_root = str(test_context)
feature_file = test_context / 'test.feature'
feature_file.touch()
- feature_file.write_text(dedent('''
+ feature_file.write_text(dedent("""
Feature: test feature
Background:
Given a common test step
@@ -51,50 +53,47 @@ def test_parse_feature_file(tmp_path_factory: TempPathFactory) -> None:
Given a second test step
Then execute it
When done, just stop
- '''))
-
- chdir(test_context_root)
+ """))
try:
- import grizzly_cli
- reload(grizzly_cli)
- reload(grizzly_cli.utils)
+ with cwd(test_context):
+ import grizzly_cli # noqa: PLC0415
+ reload(grizzly_cli)
+ reload(grizzly_cli.utils)
- assert len(grizzly_cli.SCENARIOS) == 0
+ assert len(grizzly_cli.SCENARIOS) == 0
- parse_feature_file('test.feature')
+ parse_feature_file('test.feature')
- import grizzly_cli
+ import grizzly_cli # noqa: PLC0415
- cached_scenarios = grizzly_cli.SCENARIOS.copy()
- assert len(grizzly_cli.SCENARIOS) == 2
- assert list(grizzly_cli.SCENARIOS)[0].name == 'scenario-1'
- assert len(list(grizzly_cli.SCENARIOS)[0].steps) == 2
- assert len(list(grizzly_cli.SCENARIOS)[0].background_steps) == 2
- assert list(grizzly_cli.SCENARIOS)[1].name == 'scenario-2'
- assert len(list(grizzly_cli.SCENARIOS)[1].steps) == 3
- assert len(list(grizzly_cli.SCENARIOS)[1].background_steps) == 2
+ cached_scenarios = grizzly_cli.SCENARIOS.copy()
+ assert len(grizzly_cli.SCENARIOS) == 2
+ assert next(iter(grizzly_cli.SCENARIOS)).name == 'scenario-1'
+ assert len(next(iter(grizzly_cli.SCENARIOS)).steps) == 2
+ assert len(next(iter(grizzly_cli.SCENARIOS)).background_steps) == 2
+ assert list(grizzly_cli.SCENARIOS)[1].name == 'scenario-2'
+ assert len(list(grizzly_cli.SCENARIOS)[1].steps) == 3
+ assert len(list(grizzly_cli.SCENARIOS)[1].background_steps) == 2
- parse_feature_file('test.feature')
+ parse_feature_file('test.feature')
- import grizzly_cli
-
- assert grizzly_cli.SCENARIOS == cached_scenarios
+ import grizzly_cli # noqa: PLC0415
+ assert cached_scenarios == grizzly_cli.SCENARIOS
finally:
- chdir(CWD)
rm_rf(test_context_root)
def test_list_images(mocker: MockerFixture) -> None:
check_output = mocker.patch('grizzly_cli.utils.subprocess.check_output', side_effect=[(
- '{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.10", "size": "1.16GB", "created": "2021-12-02 23:46:55 +0100 CET", "id": "a05f8cc8454b"}\n'
- '{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.10-bullseye", "size": "1.16GB", "created": "2021-12-02 23:46:55 +0100 CET", "id": "a05f8cc8454b"}\n'
- '{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.9", "size": "1.23GB", "created": "2021-12-02 23:27:50 +0100 CET", "id": "bfbce224d490"}\n'
- '{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.8", "size": "1.23GB", "created": "2021-12-02 23:10:12 +0100 CET", "id": "8a04d9e5df14"}\n'
- '{"name": "mcr.microsoft.com/vscode/devcontainers/base", "tag": "0-focal", "size": "343MB", "created": "2021-12-02 22:44:23 +0100 CET", "id": "0cc1cbb6d08d"}\n'
- '{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.6", "size": "1.22GB", "created": "2021-12-02 22:17:47 +0100 CET", "id": "cc5abbf52b04"}\n'
- ).encode()])
+ b'{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.10", "size": "1.16GB", "created": "2021-12-02 23:46:55 +0100 CET", "id": "a05f8cc8454b"}\n'
+ b'{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.10-bullseye", "size": "1.16GB", "created": "2021-12-02 23:46:55 +0100 CET", "id": "a05f8cc8454b"}\n'
+ b'{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.9", "size": "1.23GB", "created": "2021-12-02 23:27:50 +0100 CET", "id": "bfbce224d490"}\n'
+ b'{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.8", "size": "1.23GB", "created": "2021-12-02 23:10:12 +0100 CET", "id": "8a04d9e5df14"}\n'
+ b'{"name": "mcr.microsoft.com/vscode/devcontainers/base", "tag": "0-focal", "size": "343MB", "created": "2021-12-02 22:44:23 +0100 CET", "id": "0cc1cbb6d08d"}\n'
+ b'{"name": "mcr.microsoft.com/vscode/devcontainers/python", "tag": "0-3.6", "size": "1.22GB", "created": "2021-12-02 22:17:47 +0100 CET", "id": "cc5abbf52b04"}\n'
+ )])
arguments = Namespace(container_system='capsulegirl')
@@ -111,32 +110,31 @@ def test_list_images(mocker: MockerFixture) -> None:
]
assert len(images.keys()) == 2
- assert sorted(list(images.get('mcr.microsoft.com/vscode/devcontainers/python', {}).keys())) == sorted([
+ assert sorted(images.get('mcr.microsoft.com/vscode/devcontainers/python', {}).keys()) == sorted([
'0-3.10',
'0-3.10-bullseye',
'0-3.9',
'0-3.8',
'0-3.6',
])
- assert sorted(list(images.get('mcr.microsoft.com/vscode/devcontainers/base', {}).keys())) == sorted([
- '0-focal'
+ assert sorted(images.get('mcr.microsoft.com/vscode/devcontainers/base', {}).keys()) == sorted([
+ '0-focal',
])
def test_get_default_mtu(mocker: MockerFixture) -> None:
- from json.decoder import JSONDecodeError
check_output = mocker.patch('grizzly_cli.utils.subprocess.check_output', side_effect=[
JSONDecodeError,
(
- '{"com.docker.network.bridge.default_bridge":"true","com.docker.network.bridge.enable_icc":"true",'
- '"com.docker.network.bridge.enable_ip_masquerade":"true","com.docker.network.bridge.host_binding_ipv4":"0.0.0.0",'
- '"com.docker.network.bridge.name":"docker0","com.docker.network.driver.mtu":"1500"}\n'
- ).encode(),
+ b'{"com.docker.network.bridge.default_bridge":"true","com.docker.network.bridge.enable_icc":"true",'
+ b'"com.docker.network.bridge.enable_ip_masquerade":"true","com.docker.network.bridge.host_binding_ipv4":"0.0.0.0",'
+ b'"com.docker.network.bridge.name":"docker0","com.docker.network.driver.mtu":"1500"}\n'
+ ),
(
- '{"com.docker.network.bridge.default_bridge":"true","com.docker.network.bridge.enable_icc":"true",'
- '"com.docker.network.bridge.enable_ip_masquerade":"true","com.docker.network.bridge.host_binding_ipv4":"0.0.0.0",'
- '"com.docker.network.bridge.name":"docker0","com.docker.network.driver.mtu":"1440"}\n'
- ).encode(),
+ b'{"com.docker.network.bridge.default_bridge":"true","com.docker.network.bridge.enable_icc":"true",'
+ b'"com.docker.network.bridge.enable_ip_masquerade":"true","com.docker.network.bridge.host_binding_ipv4":"0.0.0.0",'
+ b'"com.docker.network.bridge.name":"docker0","com.docker.network.driver.mtu":"1440"}\n'
+ ),
])
arguments = Namespace(container_system='capsulegirl')
@@ -166,9 +164,9 @@ def test_run_command(capsys: CaptureFixture, mocker: MockerFixture) -> None:
terminate = mocker.patch('grizzly_cli.utils.subprocess.Popen.terminate', autospec=True)
wait = mocker.patch('grizzly_cli.utils.subprocess.Popen.wait', autospec=True)
- def popen___init___no_stdout(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> None:
- setattr(args[0], 'returncode', 133)
- setattr(args[0], 'stdout', None)
+ def popen___init___no_stdout(*args: Any, **_kwargs: Any) -> None:
+ args[0].returncode = 133
+ args[0].stdout = None
mocker.patch('grizzly_cli.utils.subprocess.Popen.__init__', popen___init___no_stdout)
poll_mock = mocker.patch('grizzly_cli.utils.subprocess.Popen.poll', side_effect=[None])
@@ -185,17 +183,17 @@ def popen___init___no_stdout(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -
assert poll_mock.call_count == 1
assert kill_mock.call_count == 1
- def mock_command_output(output: List[str], returncode: int = 0) -> None:
- output_buffer: List[Union[bytes, int]] = [f'{line}\n'.encode('utf-8') for line in output] + [0]
+ def mock_command_output(output: list[str], returncode: int = 0) -> None:
+ output_buffer: list[Union[bytes, int]] = [f'{line}\n'.encode() for line in output] + [0]
- def popen___init__(*args: Tuple[Any, ...], **kwargs: Dict[str, Any]) -> None:
- setattr(args[0], 'returncode', returncode)
+ def popen___init__(*args: Any, **_kwargs: Any) -> None:
+ args[0].returncode = returncode
class Stdout:
def readline(self) -> Union[bytes, int]:
return output_buffer.pop(0)
- setattr(args[0], 'stdout', Stdout())
+ args[0].stdout = Stdout()
mocker.patch('grizzly_cli.utils.subprocess.Popen.terminate', side_effect=[KeyboardInterrupt])
mocker.patch('grizzly_cli.utils.subprocess.Popen.__init__', popen___init__)
@@ -256,7 +254,7 @@ def test_get_distributed_system(capsys: CaptureFixture, mocker: MockerFixture) -
# test 1
which.side_effect = [None, None]
- getstatusoutput.return_value = (1, 'foobar',)
+ getstatusoutput.return_value = (1, 'foobar')
assert get_distributed_system() is None # neither
capture = capsys.readouterr()
assert capture.out == 'neither "podman" nor "docker" found in PATH\n'
@@ -280,7 +278,7 @@ def test_get_distributed_system(capsys: CaptureFixture, mocker: MockerFixture) -
# test 3
which.side_effect = [None, 'podman']
- getstatusoutput.return_value = (0, 'foobar',)
+ getstatusoutput.return_value = (0, 'foobar')
assert get_distributed_system() == 'podman'
capture = capsys.readouterr()
assert which.call_count == 2
@@ -293,7 +291,7 @@ def test_get_distributed_system(capsys: CaptureFixture, mocker: MockerFixture) -
# test 4
which.side_effect = ['docker']
- getstatusoutput.return_value = (1, 'foobar',)
+ getstatusoutput.return_value = (1, 'foobar')
assert get_distributed_system() is None
capture = capsys.readouterr()
assert which.call_count == 1
@@ -306,7 +304,7 @@ def test_get_distributed_system(capsys: CaptureFixture, mocker: MockerFixture) -
# test 5
which.side_effect = ['docker']
- getstatusoutput.return_value = (0, 'foobar',)
+ getstatusoutput.return_value = (0, 'foobar')
assert get_distributed_system() == 'docker'
capture = capsys.readouterr()
assert which.call_count == 1
@@ -328,13 +326,12 @@ def test_find_variable_names_in_questions(mocker: MockerFixture) -> None:
[
'Given a user of type "RestApi" load testing "https://localhost"',
'And ask for value of variable test_variable_1',
- ]
+ ],
),
])
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='could not find variable name in "ask for value of variable test_variable_1'):
find_variable_names_in_questions('test.feature')
- assert 'could not find variable name in "ask for value of variable test_variable_1"' in str(ve)
mocker.patch('grizzly_cli.SCENARIOS', [
create_scenario(
@@ -344,7 +341,7 @@ def test_find_variable_names_in_questions(mocker: MockerFixture) -> None:
'Given a user of type "RestApi" load testing "https://localhost"',
'And ask for value of variable "test_variable_2"',
'And ask for value of variable "test_variable_1"',
- ]
+ ],
),
create_scenario(
'scenario-2',
@@ -354,48 +351,48 @@ def test_find_variable_names_in_questions(mocker: MockerFixture) -> None:
[
'Given a user of type "MessageQueueUser" load testing "mqs://localhost"',
'And ask for value of variable "foo"',
- ]
- )
+ ],
+ ),
])
variables = find_variable_names_in_questions('test.feature')
assert len(variables) == 4
assert variables == ['bar', 'foo', 'test_variable_1', 'test_variable_2']
-def test_find_metadata_notices(mocker: MockerFixture, tmp_path_factory: TempPathFactory) -> None:
+def test_find_metadata_notices(tmp_path_factory: TempPathFactory) -> None:
test_context = tmp_path_factory.mktemp('test_context')
try:
feature_file = test_context / 'test-1.feature'
- feature_file.write_text('''Feature: test -1
+ feature_file.write_text("""Feature: test -1
Scenario: hello world
Given a feature file with a rich set of expressions
-''')
+""")
assert find_metadata_notices(str(feature_file)) == []
- feature_file.write_text('''# grizzly-cli run --verbose
+ feature_file.write_text("""# grizzly-cli run --verbose
# grizzly-cli:notice have you created testdata?
Feature: test -1
Scenario: hello world
Given a feature file with a rich set of expressions
-''')
+""")
assert find_metadata_notices(str(feature_file)) == ['have you created testdata?']
- feature_file.write_text('''# grizzly-cli run --verbose
+ feature_file.write_text("""# grizzly-cli run --verbose
# grizzly-cli:notice have you created testdata?
Feature: test -1
Scenario: hello world
# grizzly-cli:notice is the event log cleared?
Given a feature file with a rich set of expressions
-''')
+""")
assert find_metadata_notices(str(feature_file)) == ['have you created testdata?', 'is the event log cleared?']
finally:
rm_rf(test_context)
-def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: MockerFixture) -> None:
+def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: MockerFixture) -> None: # noqa: PLR0915
setup_logging()
arguments = Namespace(file='test.feature', yes=False)
@@ -414,9 +411,8 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
),
])
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='grizzly needs at least 1 users to run this feature'):
distribution_of_users_per_scenario(arguments, {})
- assert str(ve.value) == 'grizzly needs at least 1 users to run this feature'
mocker.patch('grizzly_cli.SCENARIOS', [
create_scenario(
@@ -441,12 +437,11 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
'And repeat for "1" iteration',
'And ask for value of variable "foo"',
],
- )
+ ),
])
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='scenario-1 will have 5 users to run 1 iterations, increase iterations or lower user count'):
distribution_of_users_per_scenario(arguments, {})
- assert str(ve.value) == 'scenario-1 will have 5 users to run 1 iterations, increase iterations or lower user count'
capsys.readouterr()
@@ -473,7 +468,7 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
'And repeat for "1" iteration',
'And ask for value of variable "foo"',
],
- )
+ ),
])
distribution_of_users_per_scenario(arguments, {})
@@ -491,7 +486,7 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
'001 1 1 1 scenario-1 \n',
'002 1 1 1 scenario-2 \n',
'------|-------|------|------|-------------|\n',
- '\n'
+ '\n',
]
assert capture.err == ''.join(expected_lines)
capsys.readouterr()
@@ -507,9 +502,8 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
),
])
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='scenario "scenario-1" does not have any steps'):
distribution_of_users_per_scenario(arguments, {})
- assert 'scenario "scenario-1" does not have any steps' in str(ve)
mocker.patch('grizzly_cli.SCENARIOS', [
create_scenario(
@@ -519,9 +513,8 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
),
])
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError, match='scenario-1 does not have a user type'):
distribution_of_users_per_scenario(arguments, {})
- assert 'scenario-1 does not have a user type' in str(ve)
mocker.patch('grizzly_cli.SCENARIOS', [
create_scenario(
@@ -534,7 +527,7 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
'And repeat for "{{ integer * 0.10 }}" iterations'
'And ask for value of variable "test_variable_2"',
'And ask for value of variable "test_variable_1"',
- ]
+ ],
),
create_scenario(
'scenario-2',
@@ -546,12 +539,12 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
'And repeat for "{{ integer * 0.01 }}" iterations',
'And ask for value of variable "foo"',
],
- )
+ ),
])
- import grizzly_cli.utils
+ import grizzly_cli.utils # noqa: PLC0415
- render = mocker.spy(grizzly_cli.utils.Template, 'render') # type: ignore
+ render = mocker.spy(grizzly_cli.utils.Template, 'render') # type: ignore[attr-defined]
distribution_of_users_per_scenario(arguments, {
'TESTDATA_VARIABLE_users': '40',
@@ -566,19 +559,18 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
capture = capsys.readouterr()
assert capture.out == ''
- assert capture.err == ''.join([
- '\n',
- 'feature file test.feature will execute in total 55 iterations divided on 2 scenarios\n'
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n',
- '001 500 50 39 scenario-1 \n',
- '002 1 5 1 scenario-2 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ])
+ assert capture.err == """
+feature file test.feature will execute in total 55 iterations divided on 2 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 500 50 39 scenario-1
+002 1 5 1 scenario-2
+------|-------|------|------|-------------|
+
+"""
capsys.readouterr()
assert ask_yes_no.call_count == 2
args, _ = ask_yes_no.call_args_list[-1]
@@ -605,7 +597,7 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
'And repeat for "500" iterations'
'And ask for value of variable "test_variable_2"',
'And ask for value of variable "test_variable_1"',
- ]
+ ],
),
create_scenario(
'scenario-2 testing a lot more of many different things that scenario-1 does not test',
@@ -624,8 +616,8 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
[
'Given a user of type "RestApi" with weight "1" load testing "https://127.0.0.2"',
'And repeat for "10" iterations',
- ]
- )
+ ],
+ ),
])
arguments = Namespace(file='integration.feature', yes=True)
@@ -634,20 +626,19 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
capture = capsys.readouterr()
assert capture.out == ''
- assert capture.err == ''.join([
- '\n',
- 'feature file integration.feature will execute in total 1260 iterations divided on 3 scenarios\n'
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description \n',
- '------|-------|------|------|--------------------------------------------------------------------------------------|\n'
- '001 100 500 46 scenario-1 testing a lot of stuff \n',
- '002 50 750 23 scenario-2 testing a lot more of many different things that scenario-1 does not test\n',
- '003 1 10 1 scenario-3 \n',
- '------|-------|------|------|--------------------------------------------------------------------------------------|\n',
- '\n',
- ])
+ assert capture.err == """
+feature file integration.feature will execute in total 1260 iterations divided on 3 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|--------------------------------------------------------------------------------------|
+001 100 500 46 scenario-1 testing a lot of stuff
+002 50 750 23 scenario-2 testing a lot more of many different things that scenario-1 does not test
+003 1 10 1 scenario-3
+------|-------|------|------|--------------------------------------------------------------------------------------|
+
+"""
capsys.readouterr()
assert ask_yes_no.call_count == 2
@@ -660,7 +651,7 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
[
'Given a user of type "RestApi" with weight "25" load testing "https://localhost"',
'And repeat for "1" iterations',
- ]
+ ],
),
])
@@ -668,18 +659,17 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
capture = capsys.readouterr()
assert capture.out == ''
- assert capture.err == ''.join([
- '\n',
- 'feature file integration.feature will execute in total 1 iterations divided on 1 scenarios\n'
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n'
- '001 25 1 1 scenario-1 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ])
+ assert capture.err == """
+feature file integration.feature will execute in total 1 iterations divided on 1 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 25 1 1 scenario-1
+------|-------|------|------|-------------|
+
+"""
capsys.readouterr()
arguments = Namespace(file='integration.feature', yes=True, environment_file='environments/local.yaml')
@@ -687,18 +677,17 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
capture = capsys.readouterr()
assert capture.out == ''
- assert capture.err == ''.join([
- '\n',
- 'feature file integration.feature will execute in total 1 iterations divided on 1 scenarios with environment file environments/local.lock.yaml\n'
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n'
- '001 25 1 1 scenario-1 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ])
+ assert capture.err == """
+feature file integration.feature will execute in total 1 iterations divided on 1 scenarios with environment file environments/local.lock.yaml
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 25 1 1 scenario-1
+------|-------|------|------|-------------|
+
+"""
capsys.readouterr()
mocker.patch('grizzly_cli.SCENARIOS', [
@@ -712,7 +701,7 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
'And repeat for "10" iterations'
'And ask for value of variable "test_variable_2"',
'And ask for value of variable "test_variable_1"',
- ]
+ ],
),
create_scenario(
'scenario-2 testing a lot more of many different things that scenario-1 does not test',
@@ -729,7 +718,7 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
[
'Given a user of type "RestApi" with weight "0" load testing "https://127.0.0.2"',
'And repeat for "10" iterations',
- ]
+ ],
),
create_scenario(
'scenario-4',
@@ -737,163 +726,157 @@ def test_distribution_of_users_per_scenario(capsys: CaptureFixture, mocker: Mock
[
'Given a user of type "RestApi" with weight "0" load testing "https://127.0.0.2"',
'And repeat for "0" iterations',
- ]
- )
+ ],
+ ),
])
arguments = Namespace(file='integration.feature', yes=True)
- with pytest.raises(ValueError) as ve:
+ with pytest.raises(ValueError) as ve: # noqa: PT011
distribution_of_users_per_scenario(arguments, {})
capture = capsys.readouterr()
assert capture.out == ''
- assert capture.err == ''.join([
- '\n',
- 'feature file integration.feature will execute in total 20 iterations divided on 4 scenarios\n'
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description errors\n',
- '------|-------|------|------|--------------------------------------------------------------------------------------|----------------------------------|\n'
- '001 1 10 1 scenario-1 testing a lot of stuff \n',
- '002 50 0 3 scenario-2 testing a lot more of many different things that scenario-1 does not test no iterations\n',
- '003 0 10 0 scenario-3 no users assigned\n',
- '004 0 0 0 scenario-4 no users assigned, no iterations\n',
- '------|-------|------|------|--------------------------------------------------------------------------------------|----------------------------------|\n',
- ])
+ assert capture.err == """
+feature file integration.feature will execute in total 20 iterations divided on 4 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description errors
+------|-------|------|------|--------------------------------------------------------------------------------------|----------------------------------|
+001 1 10 1 scenario-1 testing a lot of stuff
+002 50 0 3 scenario-2 testing a lot more of many different things that scenario-1 does not test no iterations
+003 0 10 0 scenario-3 no users assigned
+004 0 0 0 scenario-4 no users assigned, no iterations
+------|-------|------|------|--------------------------------------------------------------------------------------|----------------------------------|
+"""
assert str(ve.value) == """ ^
+-------------------------------------------------------------------------------------------------------------------+
|
+- there were errors when calculating user distribution and iterations per scenario, adjust user "weight", number of users or iterations per scenario\n"""
-@pytest.mark.parametrize('users,iterations,output', [
+@pytest.mark.parametrize(
+ ('users', 'iterations', 'output'), [
(
6, 13,
- ''.join([
- '\n',
- 'feature file integration.feature will execute in total 28 iterations divided on 7 scenarios\n'
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n'
- '001 50 15 5 scenario-0 \n',
- '002 12 3 1 scenario-1 \n',
- '003 21 5 2 scenario-2 \n',
- '004 4 1 1 scenario-3 \n',
- '005 6 2 1 scenario-4 \n',
- '006 3 1 1 scenario-5 \n',
- '007 3 1 1 scenario-6 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ]),
+ """
+feature file integration.feature will execute in total 28 iterations divided on 7 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 50 15 5 scenario-0
+002 12 3 1 scenario-1
+003 21 5 2 scenario-2
+004 4 1 1 scenario-3
+005 6 2 1 scenario-4
+006 3 1 1 scenario-5
+007 3 1 1 scenario-6
+------|-------|------|------|-------------|
+
+""",
),
(
12, 20,
- ''.join([
- '\n',
- 'feature file integration.feature will execute in total 43 iterations divided on 7 scenarios\n',
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n'
- '001 33 23 6 scenario-0 \n',
- '002 16 5 2 scenario-1 \n',
- '003 28 8 5 scenario-2 \n',
- '004 5 2 1 scenario-3 \n',
- '005 8 3 2 scenario-4 \n',
- '006 4 1 1 scenario-5 \n',
- '007 4 1 1 scenario-6 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ]),
+ """
+feature file integration.feature will execute in total 43 iterations divided on 7 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 33 23 6 scenario-0
+002 16 5 2 scenario-1
+003 28 8 5 scenario-2
+004 5 2 1 scenario-3
+005 8 3 2 scenario-4
+006 4 1 1 scenario-5
+007 4 1 1 scenario-6
+------|-------|------|------|-------------|
+
+""",
),
(
18, 31,
- ''.join([
- '\n',
- 'feature file integration.feature will execute in total 66 iterations divided on 7 scenarios\n',
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n',
- '001 25 35 6 scenario-0 \n',
- '002 18 8 4 scenario-1 \n',
- '003 31 13 7 scenario-2 \n',
- '004 6 2 2 scenario-3 \n',
- '005 9 4 3 scenario-4 \n',
- '006 4 2 1 scenario-5 \n',
- '007 4 2 1 scenario-6 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ]),
+ """
+feature file integration.feature will execute in total 66 iterations divided on 7 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 25 35 6 scenario-0
+002 18 8 4 scenario-1
+003 31 13 7 scenario-2
+004 6 2 2 scenario-3
+005 9 4 3 scenario-4
+006 4 2 1 scenario-5
+007 4 2 1 scenario-6
+------|-------|------|------|-------------|
+
+""",
),
(
24, 49,
- ''.join([
- '\n',
- 'feature file integration.feature will execute in total 105 iterations divided on 7 scenarios\n',
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n',
- '001 20 56 6 scenario-0 \n',
- '002 20 12 6 scenario-1 \n',
- '003 33 21 10 scenario-2 \n',
- '004 6 4 1 scenario-3 \n',
- '005 10 6 3 scenario-4 \n',
- '006 4 3 2 scenario-5 \n',
- '007 4 3 2 scenario-6 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ])
+ """
+feature file integration.feature will execute in total 105 iterations divided on 7 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 20 56 6 scenario-0
+002 20 12 6 scenario-1
+003 33 21 10 scenario-2
+004 6 4 1 scenario-3
+005 10 6 3 scenario-4
+006 4 3 2 scenario-5
+007 4 3 2 scenario-6
+------|-------|------|------|-------------|
+
+""",
),
(
30, 58,
- ''.join([
- '\n',
- 'feature file integration.feature will execute in total 124 iterations divided on 7 scenarios\n',
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n',
- '001 16 66 6 scenario-0 \n',
- '002 20 15 7 scenario-1 \n',
- '003 35 24 12 scenario-2 \n',
- '004 6 5 3 scenario-3 \n',
- '005 10 8 4 scenario-4 \n',
- '006 5 3 2 scenario-5 \n',
- '007 5 3 2 scenario-6 \n',
- '------|-------|------|------|-------------|\n'
- '\n',
- ])
+ """
+feature file integration.feature will execute in total 124 iterations divided on 7 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 16 66 6 scenario-0
+002 20 15 7 scenario-1
+003 35 24 12 scenario-2
+004 6 5 3 scenario-3
+005 10 8 4 scenario-4
+006 5 3 2 scenario-5
+007 5 3 2 scenario-6
+------|-------|------|------|-------------|
+
+""",
),
(
30, 21000,
- ''.join([
- '\n',
- 'feature file integration.feature will execute in total 44940 iterations divided on 7 scenarios\n',
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident weight #iter #user description\n',
- '------|-------|------|------|-------------|\n',
- '001 16 23940 6 scenario-0 \n',
- '002 20 5250 7 scenario-1 \n',
- '003 35 8820 12 scenario-2 \n',
- '004 6 1680 3 scenario-3 \n',
- '005 10 2730 4 scenario-4 \n',
- '006 5 1260 2 scenario-5 \n',
- '007 5 1260 2 scenario-6 \n',
- '------|-------|------|------|-------------|\n',
- '\n',
- ])
+ """
+feature file integration.feature will execute in total 44940 iterations divided on 7 scenarios
+
+each scenario will execute accordingly:
+
+ident weight #iter #user description
+------|-------|------|------|-------------|
+001 16 23940 6 scenario-0
+002 20 5250 7 scenario-1
+003 35 8820 12 scenario-2
+004 6 1680 3 scenario-3
+005 10 2730 4 scenario-4
+006 5 1260 2 scenario-5
+007 5 1260 2 scenario-6
+------|-------|------|------|-------------|
+
+""",
),
])
def test_distribution_of_users_per_scenario_advanced(capsys: CaptureFixture, mocker: MockerFixture, users: int, iterations: int, output: str) -> None:
@@ -903,7 +886,7 @@ def test_distribution_of_users_per_scenario_advanced(capsys: CaptureFixture, moc
# grizzly will later make sure that they are only run once
background_steps = [
'Given "{{ (users | int) + 6 }}" users',
- 'And spawn rate is "{{ rate }}" users per second'
+ 'And spawn rate is "{{ rate }}" users per second',
]
mocker.patch('grizzly_cli.SCENARIOS', [
@@ -921,7 +904,7 @@ def test_distribution_of_users_per_scenario_advanced(capsys: CaptureFixture, moc
[
'Given a user of type "RestApi" with weight "{{ (100 - ((6 / ((users | int) + 6) + 0.5 | int) * 100)) * 0.25 }}" load testing "https://localhost"',
'And repeat for "{{ ((leveranser * 0.25) + 0.5) | int }}" iterations',
- ]
+ ],
),
create_scenario(
'scenario-2',
@@ -985,7 +968,7 @@ def test_distribution_of_users_per_scenario_no_weights(capsys: CaptureFixture, m
# all scenarios in a feature file will, at this point, have all the background steps
# grizzly will later make sure that they are only run once
background_steps = [
- 'Given spawn rate is "{{ rate }}" users per second'
+ 'Given spawn rate is "{{ rate }}" users per second',
]
mocker.patch('grizzly_cli.SCENARIOS', [
@@ -1003,7 +986,7 @@ def test_distribution_of_users_per_scenario_no_weights(capsys: CaptureFixture, m
[
'Given "{{ ((((max_users_undefined * 0.3) - 0.5) | int) or 1) if max_users_undefined is defined else 1 }}" user of type "RestApi" load testing "https://localhost"',
'And repeat for "{{ ((leveranser * 0.3) + 0.5) | int }}" iterations',
- ]
+ ],
),
])
@@ -1017,19 +1000,18 @@ def test_distribution_of_users_per_scenario_no_weights(capsys: CaptureFixture, m
capture = capsys.readouterr()
assert capture.out == ''
- assert capture.err == ''.join([
- '\n',
- 'feature file integration.feature will execute in total 23 iterations divided on 2 scenarios\n',
- '\n',
- 'each scenario will execute accordingly:\n',
- '\n',
- 'ident #iter #user description\n',
- '------|------|------|-------------|\n',
- '001 20 6 scenario-0 \n',
- '002 3 1 scenario-1 \n',
- '------|------|------|-------------|\n',
- '\n',
- ])
+ assert capture.err == """
+feature file integration.feature will execute in total 23 iterations divided on 2 scenarios
+
+each scenario will execute accordingly:
+
+ident #iter #user description
+------|------|------|-------------|
+001 20 6 scenario-0
+002 3 1 scenario-1
+------|------|------|-------------|
+
+"""
capsys.readouterr()
@@ -1058,28 +1040,28 @@ def test_ask_yes_no(capsys: CaptureFixture, mocker: MockerFixture) -> None:
assert args[0] == 'are you sure you know what you are doing? [y/n]: '
-def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: TempPathFactory, capsys: CaptureFixture) -> None:
+def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: TempPathFactory, capsys: CaptureFixture) -> None: # noqa: PLR0915
test_context = tmp_path_factory.mktemp('test_context')
requirements_file = test_context / 'requirements.txt'
mocker.patch('grizzly_cli.EXECUTION_CONTEXT', str(test_context))
try:
- grizzly_versions, locust_version = get_dependency_versions(False)
+ grizzly_versions, locust_version = get_dependency_versions(local_install=False)
- assert grizzly_versions == (None, None,)
+ assert grizzly_versions == (None, None)
assert locust_version is None
requirements_file.touch()
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == f'!! unable to find grizzly dependency in {requirements_file.absolute()}\n'
assert capture.out == ''
requirements_file.write_text('git+https://github.com/Biometria-se/grizzly.git@v1.5.3#egg=grizzly-loadtester')
- import subprocess
+ import subprocess # noqa: PLC0415
with ExitStack() as stack:
stack.enter_context(
mocker.patch.context_manager(subprocess, 'check_call', side_effect=[1, 0, 0, 1, 0]),
@@ -1093,74 +1075,78 @@ def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: Te
]),
)
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to clone git repo https://github.com/Biometria-se/grizzly.git\n'
assert capture.out == ''
- assert subprocess.check_call.call_count == 1 # type: ignore # pylint: disable=no-member
- assert subprocess.check_output.call_count == 0 # type: ignore # pylint: disable=no-member
+ assert subprocess.check_call.call_count == 1 # type: ignore[attr-defined]
+ assert subprocess.check_output.call_count == 0 # type: ignore[attr-defined]
# git clone...
- args, kwargs = subprocess.check_call.call_args_list[0] # type: ignore # pylint: disable=no-member
+ args, kwargs = subprocess.check_call.call_args_list[0] # type: ignore[attr-defined]
assert len(args) == 1
args = args[0]
assert args[:-1] == ['git', 'clone', '--filter=blob:none', '-q', 'https://github.com/Biometria-se/grizzly.git']
- assert args[-1].startswith(gettempdir())
- assert args[-1].endswith('grizzly-loadtester_3f210f1809f6ca85ef414b2b4d450bf54353b5e0')
+ assert isinstance(args[-1], Path)
+ assert args[-1].as_posix().startswith(Path(gettempdir()).as_posix())
+ assert args[-1].as_posix().endswith('grizzly-loadtester_3f210f1809f6ca85ef414b2b4d450bf54353b5e0')
assert not kwargs.get('shell', True)
assert kwargs.get('stdout', None) == subprocess.DEVNULL
assert kwargs.get('stderr', None) == subprocess.DEVNULL
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to check branch name of HEAD in git repo https://github.com/Biometria-se/grizzly.git\n'
assert capture.out == ''
- assert subprocess.check_call.call_count == 2 # type: ignore # pylint: disable=no-member
- assert subprocess.check_output.call_count == 1 # type: ignore # pylint: disable=no-member
+ assert subprocess.check_call.call_count == 2 # type: ignore[attr-defined]
+ assert subprocess.check_output.call_count == 1 # type: ignore[attr-defined]
# git rev-parse...
- args, kwargs = subprocess.check_output.call_args_list[0] # type: ignore # pylint: disable=no-member
+ args, kwargs = subprocess.check_output.call_args_list[0] # type: ignore[attr-defined]
assert len(args) == 1
args = args[0]
assert args == ['git', 'rev-parse', '--abbrev-ref', 'HEAD']
assert not kwargs.get('shell', True)
- assert kwargs.get('cwd', '').startswith(gettempdir())
- assert kwargs.get('cwd', '').endswith('grizzly-loadtester_3f210f1809f6ca85ef414b2b4d450bf54353b5e0')
+ kwarg_cwd = kwargs.get('cwd', None)
+ assert isinstance(kwarg_cwd, Path)
+ assert kwarg_cwd.as_posix().startswith(Path(gettempdir()).as_posix())
+ assert kwarg_cwd.as_posix().endswith('grizzly-loadtester_3f210f1809f6ca85ef414b2b4d450bf54353b5e0')
assert kwargs.get('universal_newlines', False)
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(True)
+ assert get_dependency_versions(local_install=True) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to checkout branch v1.5.3 from git repo https://github.com/Biometria-se/grizzly.git\n'
assert capture.out == ''
- assert subprocess.check_call.call_count == 4 # type: ignore # pylint: disable=no-member
- assert subprocess.check_output.call_count == 3 # type: ignore # pylint: disable=no-member
+ assert subprocess.check_call.call_count == 4 # type: ignore[attr-defined]
+ assert subprocess.check_output.call_count == 3 # type: ignore[attr-defined]
# git checkout...
- args, kwargs = subprocess.check_call.call_args_list[-1] # type: ignore # pylint: disable=no-member
+ args, kwargs = subprocess.check_call.call_args_list[-1] # type: ignore[attr-defined]
assert len(args) == 1
args = args[0]
assert args == ['git', 'checkout', '-b', 'v1.5.3', '--track', 'origin/v1.5.3']
- assert kwargs.get('cwd', '').startswith(gettempdir())
- assert kwargs.get('cwd', '').endswith('grizzly-loadtester_3f210f1809f6ca85ef414b2b4d450bf54353b5e0')
+ kwarg_cwd = kwargs.get('cwd', None)
+ assert kwarg_cwd.as_posix().startswith(Path(gettempdir()).as_posix())
+ assert kwarg_cwd.as_posix().endswith('grizzly-loadtester_3f210f1809f6ca85ef414b2b4d450bf54353b5e0')
assert not kwargs.get('shell', True)
assert kwargs.get('stdout', None) == subprocess.DEVNULL
assert kwargs.get('stderr', None) == subprocess.DEVNULL
with pytest.raises(FileNotFoundError):
- get_dependency_versions(False)
+ get_dependency_versions(local_install=False)
capture = capsys.readouterr()
assert capture.err == ''
assert capture.out == ''
- assert subprocess.check_call.call_count == 5 # type: ignore # pylint: disable=no-member
- assert subprocess.check_output.call_count == 4 # type: ignore # pylint: disable=no-member
+ assert subprocess.check_call.call_count == 5 # type: ignore[attr-defined]
+ assert subprocess.check_output.call_count == 4 # type: ignore[attr-defined]
with ExitStack() as stack:
stack.enter_context(mocker.patch.context_manager(subprocess, 'check_call', return_value=0))
@@ -1169,27 +1155,27 @@ def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: Te
)
with pytest.raises(FileNotFoundError) as fne:
- get_dependency_versions(False)
+ get_dependency_versions(local_install=False)
assert fne.value.errno == 2
assert fne.value.strerror == 'No such file or directory'
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='git+https://github.com/Biometria-se/grizzly.git@v1.5.3#egg=grizzly-loadtester\n').return_value,
mock_open(read_data='').return_value,
]) as open_mock:
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to find "__version__" declaration in grizzly/__init__.py from https://github.com/Biometria-se/grizzly.git\n'
assert capture.out == ''
assert open_mock.call_count == 2
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='git+https://github.com/Biometria-se/grizzly.git@v1.5.3#egg=grizzly-loadtester\n').return_value,
mock_open(read_data="__version__ = '0.0.0'").return_value,
mock_open(read_data='').return_value,
]) as open_mock:
- assert (('(development)', [], ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(development)', []), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to find "locust" dependency in requirements.txt from https://github.com/Biometria-se/grizzly.git\n'
@@ -1197,12 +1183,12 @@ def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: Te
assert open_mock.call_count == 3
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='git+https://github.com/Biometria-se/grizzly.git@v1.5.3#egg=grizzly-loadtester[dev,mq]\n').return_value,
mock_open(read_data="__version__ = '1.5.3'").return_value,
mock_open(read_data='locust').return_value,
]) as open_mock:
- assert (('1.5.3', ['dev', 'mq'], ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('1.5.3', ['dev', 'mq']), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to find locust version in "locust" specified in requirements.txt from https://github.com/Biometria-se/grizzly.git\n'
@@ -1210,12 +1196,12 @@ def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: Te
assert open_mock.call_count == 3
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='git+https://github.com/Biometria-se/grizzly.git@v1.5.3#egg=grizzly-loadtester\n').return_value,
mock_open(read_data="__version__ = '1.5.3'").return_value,
mock_open(read_data='locust==2.2.1 \\ ').return_value,
]) as open_mock:
- assert (('1.5.3', [], ), '2.2.1',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('1.5.3', []), '2.2.1')
capture = capsys.readouterr()
assert capture.err == ''
@@ -1223,63 +1209,63 @@ def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: Te
assert open_mock.call_count == 3
- mocker.patch('grizzly_cli.utils.path.exists', return_value=True)
+ mocker.patch('grizzly_cli.utils.Path.exists', return_value=True)
with pytest.raises(FileNotFoundError) as fne:
- get_dependency_versions(False)
+ get_dependency_versions(local_install=False)
assert fne.value.errno == 2
assert fne.value.strerror == 'No such file or directory'
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='git+https://github.com/Biometria-se/grizzly.git@main#egg=grizzly-loadtester\n').return_value,
mock_open(read_data='').return_value,
]) as open_mock:
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to find "version" declaration in setup.cfg from https://github.com/Biometria-se/grizzly.git\n'
assert capture.out == ''
assert open_mock.call_count == 2
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='git+https://github.com/Biometria-se/grizzly.git@main#egg=grizzly-loadtester[mq]\n').return_value,
mock_open(read_data='name = grizzly-loadtester\nversion = 2.0.0').return_value,
mock_open(read_data='locust==2.8.4 \\ ').return_value,
]) as open_mock:
- assert (('2.0.0', ['mq'], ), '2.8.4',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('2.0.0', ['mq']), '2.8.4')
capture = capsys.readouterr()
assert capture.err == ''
assert capture.out == ''
assert open_mock.call_count == 3
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='grizzly-loadtester @ git+https://github.com/Biometria-se/grizzly.git@main\n').return_value,
mock_open(read_data='').return_value,
]) as open_mock:
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to find "version" declaration in setup.cfg from https://github.com/Biometria-se/grizzly.git\n'
assert capture.out == ''
assert open_mock.call_count == 2
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='grizzly-loadtester[mq] @ git+https://github.com/Biometria-se/grizzly.git@main\n').return_value,
mock_open(read_data='name = grizzly-loadtester\nversion = 2.0.0').return_value,
mock_open(read_data='locust==2.8.4 \\ ').return_value,
]) as open_mock:
- assert (('2.0.0', ['mq'], ), '2.8.4',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('2.0.0', ['mq']), '2.8.4')
capture = capsys.readouterr()
assert capture.err == ''
assert capture.out == ''
assert open_mock.call_count == 3
- with unittest_patch('builtins.open', side_effect=[
+ with unittest_patch('grizzly_cli.utils.Path.open', side_effect=[
mock_open(read_data='grizzly-loadtester[mq] % git+https://github.com/Biometria-se/grizzly.git@main\n').return_value,
]) as open_mock:
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == f'!! unable to find properly formatted grizzly dependency in {requirements_file}\n'
@@ -1290,21 +1276,21 @@ def test_get_dependency_versions_git(mocker: MockerFixture, tmp_path_factory: Te
@pytest.mark.filterwarnings('ignore:Creating a LegacyVersion has been deprecated')
-def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: TempPathFactory, capsys: CaptureFixture, requests_mock: RequestsMocker) -> None:
+def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: TempPathFactory, capsys: CaptureFixture, requests_mock: RequestsMocker) -> None: # noqa: PLR0915
test_context = tmp_path_factory.mktemp('test_context')
requirements_file = test_context / 'requirements.txt'
mocker.patch('grizzly_cli.EXECUTION_CONTEXT', str(test_context))
try:
- grizzly_versions, locust_version = get_dependency_versions(False)
+ grizzly_versions, locust_version = get_dependency_versions(local_install=False)
- assert grizzly_versions == (None, None,)
+ assert grizzly_versions == (None, None)
assert locust_version is None
requirements_file.touch()
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == f'!! unable to find grizzly dependency in {requirements_file.absolute()}\n'
@@ -1314,7 +1300,7 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/json', status_code=404)
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to get grizzly package information from https://pypi.org/pypi/grizzly-loadtester/json (404)\n'
@@ -1323,7 +1309,7 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/json', status_code=200, text='{"info": {"version": "1.1.1"}}')
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/1.1.1/json', status_code=400)
- assert (('1.1.1', [], ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('1.1.1', []), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to get grizzly 1.1.1 package information from https://pypi.org/pypi/grizzly-loadtester/1.1.1/json (400)\n'
@@ -1331,7 +1317,7 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/1.1.1/json', status_code=200, text='{"info": {"requires_dist": []}}')
- assert (('1.1.1', [], ), '(unknown)',) == get_dependency_versions(True)
+ assert get_dependency_versions(local_install=True) == (('1.1.1', []), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! could not find "locust" in requires_dist information for grizzly-loadtester 1.1.1\n'
@@ -1342,9 +1328,9 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
requirements_file.unlink()
requirements_file.write_text('grizzly-loadtester[dev,mq]')
- actual_dependency_versions = get_dependency_versions(True)
+ actual_dependency_versions = get_dependency_versions(local_install=True)
- assert (('1.1.1', ['dev', 'mq'], ), '(unknown)',) == actual_dependency_versions
+ assert actual_dependency_versions == (('1.1.1', ['dev', 'mq']), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! unable to find locust version in "locust" specified in pypi for grizzly-loadtester 1.1.1\n'
@@ -1352,7 +1338,7 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/1.1.1/json', status_code=200, text='{"info": {"requires_dist": ["locust (==2.8.5)"]}}')
- assert (('1.1.1', ['dev', 'mq'], ), '2.8.5',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('1.1.1', ['dev', 'mq']), '2.8.5')
capture = capsys.readouterr()
assert capture.err == ''
@@ -1363,7 +1349,7 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/json', status_code=200, text='{"releases": {"1.3.0": [], "1.5.0": []}}')
- assert (('(unknown)', None, ), '(unknown)',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('(unknown)', None), '(unknown)')
capture = capsys.readouterr()
assert capture.err == '!! could not resolve grizzly-loadtester[mq]==1.4.0 to one specific version available at pypi\n'
@@ -1377,7 +1363,7 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
)
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/1.4.0/json', status_code=200, text='{"info": {"requires_dist": ["locust (==1.0.0)"]}}')
- assert (('1.4.0', ['mq'], ), '1.0.0',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('1.4.0', ['mq']), '1.0.0')
capture = capsys.readouterr()
assert capture.err == ''
@@ -1391,7 +1377,7 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
)
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/1.5.0/json', status_code=200, text='{"info": {"requires_dist": ["locust (==1.1.1)"]}}')
- assert (('1.5.0', ['mq'], ), '1.1.1',) == get_dependency_versions(False)
+ assert get_dependency_versions(local_install=False) == (('1.5.0', ['mq']), '1.1.1')
capture = capsys.readouterr()
assert capture.err == ''
@@ -1406,8 +1392,8 @@ def test_get_dependency_versions_pypi(mocker: MockerFixture, tmp_path_factory: T
)
requests_mock.register_uri('GET', 'https://pypi.org/pypi/grizzly-loadtester/1.4.0/json', status_code=200, text='{"info": {"requires_dist": ["locust (==1.0.0)"]}}')
- assert (('1.4.0', ['mq'], ), '1.0.0',) == get_dependency_versions('a/b/')
- assert (('1.4.0', ['mq'], ), '1.0.0',) == get_dependency_versions('a/b/requirements.txt')
+ assert get_dependency_versions(local_install='a/b/') == (('1.4.0', ['mq']), '1.0.0')
+ assert get_dependency_versions(local_install='a/b/requirements.txt') == (('1.4.0', ['mq']), '1.0.0')
finally:
rm_rf(test_context)
@@ -1416,15 +1402,15 @@ def test_requirements(capsys: CaptureFixture, tmp_path_factory: TempPathFactory)
test_context = tmp_path_factory.mktemp('test_context')
requirements_file = test_context / 'requirements.txt'
- def wrapped_test(args: Namespace) -> int:
+ def wrapped_test(_args: Namespace) -> int:
return 1337
try:
assert not requirements_file.exists()
- wrapped = requirements(str(test_context))(wrapped_test)
+ wrapped = requirements(test_context.as_posix())(wrapped_test)
assert getattr(wrapped, '__wrapped__', None) is wrapped_test
- assert getattr(getattr(wrapped, '__wrapped__'), '__value__') == str(test_context)
+ assert getattr(getattr(wrapped, '__wrapped__'), '__value__') == test_context.as_posix() # noqa: B009
assert wrapped(Namespace()) == 1337
diff --git a/tests/unit/utils/test_configuration.py b/tests/unit/utils/test_configuration.py
index 38b3650..7545c8d 100644
--- a/tests/unit/utils/test_configuration.py
+++ b/tests/unit/utils/test_configuration.py
@@ -1,31 +1,33 @@
from __future__ import annotations
-from pathlib import Path
-from unittest.mock import MagicMock
from base64 import b64encode
+from pathlib import Path
from platform import system
+from typing import TYPE_CHECKING
+from unittest.mock import MagicMock
import pytest
-from _pytest.tmpdir import TempPathFactory
-from pytest_mock import MockerFixture
from azure.core.exceptions import ClientAuthenticationError, ServiceRequestError
from azure.identity import ChainedTokenCredential
-from azure.keyvault.secrets import SecretClient, SecretProperties, KeyVaultSecret
+from azure.keyvault.secrets import KeyVaultSecret, SecretClient, SecretProperties
-from grizzly_cli.utils import setup_logging, chunker
+from grizzly_cli.utils import chunker, setup_logging
from grizzly_cli.utils.configuration import (
ScenarioTag,
- load_configuration_file,
- load_configuration,
- load_configuration_keyvault,
- get_keyvault_client,
_get_metadata,
- _write_file,
_import_files,
+ _write_file,
get_context_root,
+ get_keyvault_client,
+ load_configuration,
+ load_configuration_file,
+ load_configuration_keyvault,
)
+from tests.helpers import ANY, cwd, rm_rf
-from tests.helpers import rm_rf, cwd, ANY
+if TYPE_CHECKING:
+ from _pytest.tmpdir import TempPathFactory
+ from pytest_mock import MockerFixture
def create_secret_property(name: str, content_type: str | None = None) -> SecretProperties:
@@ -186,10 +188,7 @@ def test__write_file(tmp_path_factory: TempPathFactory) -> None:
assert _write_file(test_context, content_type, chunk) == 'files/foobar.txt'
f = test_context / 'files' / 'foobar.txt'
- if index < number_of_chunks - 1:
- expected = ''.join(processed_chunks)
- else:
- expected = 'foobarfoobarfoobar'
+ expected = ''.join(processed_chunks) if index < number_of_chunks - 1 else 'foobarfoobarfoobar'
assert f.read_text() == expected
@@ -267,17 +266,15 @@ def test_load_configuration(mocker: MockerFixture, tmp_path_factory: TempPathFac
try:
env_file_local = test_context / 'local.yaml'
- env_file_local.write_text('''configuration:
+ env_file_local.write_text("""configuration:
authentication:
admin:
username: administrator
password: hunter
-''')
-
- env_file_lock_name = load_configuration(env_file_local.as_posix())
- assert env_file_lock_name == f'{test_context.as_posix()}/local.lock.yaml'
+""")
- env_file_lock = Path(env_file_lock_name)
+ env_file_lock = load_configuration(env_file_local)
+ assert env_file_lock.as_posix() == Path.joinpath(test_context, 'local.lock.yaml').as_posix()
if system() != 'Windows':
assert env_file_lock.stat().st_mode & 0x000FFF == 0o600
@@ -286,56 +283,51 @@ def test_load_configuration(mocker: MockerFixture, tmp_path_factory: TempPathFac
load_configuration_keyvault_mock.assert_not_called()
with cwd(test_context):
- env_file_lock_name = load_configuration('local.yaml')
- assert env_file_lock_name == 'local.lock.yaml'
+ env_file_lock = load_configuration(Path('local.yaml'))
+ assert env_file_lock.as_posix() == 'local.lock.yaml'
- env_file_lock = Path(env_file_lock_name)
assert env_file_lock.read_text() == env_file_local.read_text()
load_configuration_keyvault_mock.assert_not_called()
- env_file_local.write_text('''configuration:
+ env_file_local.write_text("""configuration:
keyvault: https://grizzly.keyvault.azure.com
authentication:
admin:
username: administrator
password: hunter
-''')
-
- env_file_lock_name = load_configuration(env_file_local.as_posix())
- assert env_file_lock_name == f'{test_context.as_posix()}/local.lock.yaml'
+""")
- env_file_lock = Path(env_file_lock_name)
+ env_file_lock = load_configuration(env_file_local)
+ assert env_file_lock.as_posix() == Path.joinpath(test_context, 'local.lock.yaml').as_posix()
assert env_file_lock.read_text() == env_file_local.read_text()
load_configuration_keyvault_mock.assert_called_once_with(ANY(SecretClient), 'local', context_root, filter_keys=None)
load_configuration_keyvault_mock.reset_mock()
- env_file_local.write_text('''configuration:
+ env_file_local.write_text("""configuration:
env: test
keyvault: https://grizzly.keyvault.azure.com
authentication:
admin:
username: administrator
password: hunter
-''')
-
- env_file_lock_name = load_configuration(env_file_local.as_posix())
- assert env_file_lock_name == f'{test_context.as_posix()}/local.lock.yaml'
+""")
- env_file_lock = Path(env_file_lock_name)
+ env_file_lock = load_configuration(env_file_local)
+ assert env_file_lock.as_posix() == Path.joinpath(test_context, 'local.lock.yaml').as_posix()
assert env_file_lock.read_text() == env_file_local.read_text()
load_configuration_keyvault_mock.assert_called_once_with(ANY(SecretClient), 'test', context_root, filter_keys=None)
load_configuration_keyvault_mock.reset_mock()
+ dummy_file = test_context / 'dummy.txt'
with pytest.raises(ValueError, match='dummy.txt does not exist'):
- load_configuration('dummy.txt')
+ load_configuration(dummy_file)
- dummy_file = test_context / 'dummy.txt'
dummy_file.touch()
with pytest.raises(ValueError, match='configuration file must have file extension yml or yaml'):
- load_configuration(dummy_file.as_posix())
+ load_configuration(dummy_file)
finally:
rm_rf(test_context)
@@ -346,12 +338,12 @@ def test_load_configuration_file(tmp_path_factory: TempPathFactory) -> None:
try:
env_file_base = test_context / 'base.yaml'
- env_file_base.write_text('''configuration:
+ env_file_base.write_text("""configuration:
authentication:
admin:
username: administrator
password: hunter
-''')
+""")
assert load_configuration_file(env_file_base) == {
'configuration': {
@@ -365,7 +357,7 @@ def test_load_configuration_file(tmp_path_factory: TempPathFactory) -> None:
}
env_file_local = test_context / 'local.yaml'
- env_file_local.write_text('''{% merge "./base.yaml" %}
+ env_file_local.write_text("""{% merge "./base.yaml" %}
configuration:
authentication:
admin:
@@ -374,7 +366,7 @@ def test_load_configuration_file(tmp_path_factory: TempPathFactory) -> None:
logging:
level: DEBUG
max_size: 10000
-''')
+""")
assert load_configuration_file(env_file_local) == {
'configuration': {
'authentication': {
diff --git a/tests/webserver.py b/tests/webserver.py
index c0beb55..75916fa 100644
--- a/tests/webserver.py
+++ b/tests/webserver.py
@@ -1,16 +1,18 @@
+from __future__ import annotations
+
import csv
import logging
-
-from typing import Any, Optional, Type, cast
-from typing_extensions import Literal
-from types import TracebackType
from pathlib import Path
+from typing import TYPE_CHECKING, Any, Optional
import gevent
-
+from flask import Flask, jsonify, request
+from flask import Response as FlaskResponse
from gevent.pywsgi import WSGIServer
+from typing_extensions import Literal, Self
-from flask import Flask, request, jsonify, Response as FlaskResponse
+if TYPE_CHECKING:
+ from types import TracebackType
logger = logging.getLogger('webserver')
@@ -34,7 +36,8 @@ def app_get_cat_fact() -> FlaskResponse:
@app.route('/books/.json')
def app_get_book(book: str) -> FlaskResponse:
- with open(f'{Path.cwd()}/features/requests/books/books.csv', 'r') as fd:
+ books = Path.joinpath(Path.cwd(), 'features', 'requests', 'books', 'books.csv')
+ with books.open('r') as fd:
reader = csv.DictReader(fd)
for row in reader:
if row['book'] == book:
@@ -43,7 +46,7 @@ def app_get_book(book: str) -> FlaskResponse:
'isbn_10': [row['isbn_10']] * 2,
'authors': [
{'key': '/author/' + row['author'].replace(' ', '_').strip() + '|' + row['isbn_10'].strip()},
- ]
+ ],
})
response = jsonify({'success': False})
@@ -57,7 +60,7 @@ def app_get_author(author_key: str) -> FlaskResponse:
name, _ = author_key.rsplit('|', 1)
return jsonify({
- 'name': name.replace('_', ' ')
+ 'name': name.replace('_', ' '),
})
@@ -75,31 +78,34 @@ def __init__(self, port: int = 0) -> None:
app,
log=None,
)
- logger.debug(f'created webserver on port {port}')
+ logger.debug('created webserver on port %d', port)
@property
def port(self) -> int:
- return cast(int, self._web_server.server_port)
+ port = self._web_server.server_port
+ assert port is not None
+
+ return port # type: ignore[no-any-return]
def start(self) -> None:
gevent.spawn(lambda: self._web_server.serve_forever())
gevent.sleep(0.01)
- logger.debug(f'started webserver on port {self.port}')
+ logger.debug('started webserver on port %d', self.port)
- def __enter__(self) -> 'Webserver':
+ def __enter__(self) -> Self:
self.start()
return self
def __exit__(
self,
- exc_type: Optional[Type[BaseException]],
+ exc_type: Optional[type[BaseException]],
exc: Optional[BaseException],
traceback: Optional[TracebackType],
) -> Literal[True]:
self._web_server.stop_accepting()
self._web_server.stop()
- logger.debug(f'stopped webserver on port {self.port}')
+ logger.debug('stopped webserver on port %d', self.port)
return True