Skip to content

Commit

Permalink
execute python script step improvements (#309)
Browse files Browse the repository at this point in the history
* variables only used in expressions shouldn't have to appear in any other templates

* move "execute python script" from grizzly.steps.scenario to grizzly.steps

makes it possible to use steps in background section in feature-file.

* inject behave `context` object to any scripts executing

makes it possible to write scripts that takes current state/context into consideration.

* lock dependency versions

so that every installation of a specific grizzly version has the same version of its dependencies.

some dependencies has been upgraded, so some fixes due to changes in them.
  • Loading branch information
mgor authored Mar 25, 2024
1 parent 3e067eb commit 5c5b2e5
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 140 deletions.
30 changes: 16 additions & 14 deletions grizzly/listeners/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
print_percentile_stats,
print_stats,
)
from mypy_extensions import KwArg, VarArg
from typing_extensions import Concatenate, ParamSpec

from grizzly.testdata.communication import TestdataProducer
from grizzly.types import MessageDirection, RequestType, TestdataType
Expand All @@ -24,6 +24,8 @@
if TYPE_CHECKING: # pragma: no cover
from grizzly.context import GrizzlyContext

P = ParamSpec('P')

producer: Optional[TestdataProducer] = None

producer_greenlet: Optional[gevent.Greenlet] = None
Expand All @@ -45,8 +47,8 @@ def gtestdata_producer() -> None:
return gtestdata_producer


def init(grizzly: GrizzlyContext, testdata: Optional[TestdataType] = None) -> Callable[[LocustRunner, KwArg(Any)], None]: # pyright: ignore [reportInvalidTypeForm]
def ginit(runner: LocustRunner, **_kwargs: Any) -> None:
def init(grizzly: GrizzlyContext, testdata: Optional[TestdataType] = None) -> Callable[Concatenate[LocustRunner, P], None]:
def ginit(runner: LocustRunner, **_kwargs: P.kwargs) -> None:
producer_port = environ.get('TESTDATA_PRODUCER_PORT', '5555')
if not isinstance(runner, MasterRunner):
producer_address = runner.master_host if isinstance(runner, WorkerRunner) else '127.0.0.1'
Expand Down Expand Up @@ -79,11 +81,11 @@ def ginit(runner: LocustRunner, **_kwargs: Any) -> None:
for message_type, callback in grizzly.setup.locust.messages.get(MessageDirection.CLIENT_SERVER, {}).items():
runner.register_message(message_type, callback)

return cast(Callable[[LocustRunner, KwArg(Any)], None], ginit) # pyright: ignore [reportInvalidTypeForm]
return cast(Callable[Concatenate[LocustRunner, P], None], ginit)


def init_statistics_listener(url: str) -> Callable[[Environment, VarArg(Any), KwArg(Any)], None]: # pyright: ignore [reportInvalidTypeForm]
def gstatistics_listener(environment: Environment, *_args: Any, **_kwargs: Any) -> None:
def init_statistics_listener(url: str) -> Callable[Concatenate[Environment, P], None]:
def gstatistics_listener(environment: Environment, *_args: P.args, **_kwargs: P.kwargs) -> None:
parsed = urlparse(url)

if parsed.scheme == 'influxdb':
Expand All @@ -99,11 +101,11 @@ def gstatistics_listener(environment: Environment, *_args: Any, **_kwargs: Any)
url=url,
)

return cast(Callable[[Environment, VarArg(Any), KwArg(Any)], None], gstatistics_listener) # pyright: ignore [reportInvalidTypeForm]
return cast(Callable[Concatenate[Environment, P], None], gstatistics_listener)


def locust_test_start(grizzly: GrizzlyContext) -> Callable[[Environment, KwArg(Any)], None]: # pyright: ignore [reportInvalidTypeForm]
def gtest_start(environment: Environment, **_kwargs: Any) -> None:
def locust_test_start(grizzly: GrizzlyContext) -> Callable[Concatenate[Environment, P], None]:
def gtest_start(environment: Environment, **_kwargs: P.kwargs) -> None:
if isinstance(environment.runner, MasterRunner):
num_connected_workers = (
len(environment.runner.clients.ready)
Expand All @@ -117,15 +119,15 @@ def gtest_start(environment: Environment, **_kwargs: Any) -> None:
if total_iterations < num_connected_workers:
logger.error('number of iterations is lower than number of workers, %d < %d', total_iterations, num_connected_workers)

return cast(Callable[[Environment, KwArg(Any)], None], gtest_start) # pyright: ignore [reportInvalidTypeForm]
return cast(Callable[Concatenate[Environment, P], None], gtest_start)


def locust_test_stop(**_kwargs: Any) -> None:
if producer is not None:
producer.on_test_stop()


def spawning_complete(grizzly: GrizzlyContext) -> Callable[[KwArg(Any)], None]: # pyright: ignore [reportInvalidTypeForm]
def spawning_complete(grizzly: GrizzlyContext) -> Callable[..., None]:
def gspawning_complete(**_kwargs: Any) -> None:
logger.debug('spawning complete!')
grizzly.state.spawning_complete = True
Expand Down Expand Up @@ -173,8 +175,8 @@ def grizzly_worker_quit(environment: Environment, msg: Message, **_kwargs: Any)
raise SystemExit(code)


def validate_result(grizzly: GrizzlyContext) -> Callable[[Environment, KwArg(Any)], None]: # pyright: ignore [reportInvalidTypeForm]
def gvalidate_result(environment: Environment, **_kwargs: Any) -> None:
def validate_result(grizzly: GrizzlyContext) -> Callable[Concatenate[Environment, P], None]:
def gvalidate_result(environment: Environment, **_kwargs: P.kwargs) -> None:
# first, aggregate statistics per scenario
scenario_stats: Dict[str, RequestStats] = {}

Expand Down Expand Up @@ -243,4 +245,4 @@ def gvalidate_result(environment: Environment, **_kwargs: Any) -> None:
if environment.process_exit_code == 1 and hasattr(scenario, 'behave') and scenario.behave is not None:
scenario.behave.set_status(Status.failed)

return cast(Callable[[Environment, KwArg(Any)], None], gvalidate_result) # pyright: ignore [reportInvalidTypeForm]
return cast(Callable[Concatenate[Environment, P], None], gvalidate_result)
56 changes: 1 addition & 55 deletions grizzly/steps/scenario/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@
"""
from __future__ import annotations

from pathlib import Path
from typing import Any, Optional, cast

import parse

from grizzly.auth import GrizzlyHttpAuthClient
from grizzly.context import GrizzlyContext
from grizzly.exceptions import RestartScenario
from grizzly.locust import on_worker
from grizzly.tasks import GrizzlyTask, RequestTask, SetVariableTask
from grizzly.testdata.utils import create_context_variable, resolve_variable
from grizzly.types import VariableType
from grizzly.types.behave import Context, Feature, given, register_type, then
from grizzly.types.behave import Context, given, register_type, then
from grizzly.types.locust import StopUser
from grizzly.utils import has_template, merge_dicts
from grizzly_extras.text import permutation
Expand Down Expand Up @@ -258,55 +256,3 @@ def step_setup_metadata(context: Context, key: str, value: str) -> None:
grizzly.scenario.context['metadata'] = {}

grizzly.scenario.context['metadata'].update({key: casted_value})


def _execute_python_script(context: Context, source: str) -> None:
if on_worker(context):
return

exec(source, globals(), globals()) # noqa: S102

@then('execute python script "{script_path}"')
def step_setup_execute_python_script(context: Context, script_path: str) -> None:
"""Execute python script located in specified path.
The script will not execute on workers, only on master (distributed mode) or local (local mode), and
it will only execute once before the test starts.
This can be useful for generating test data files.
Example:
```gherkin
Then execute python script "../bin/generate-testdata.py"
```
"""
script_file = Path(script_path)
if not script_file.exists():
feature = cast(Feature, context.feature)
base_path = Path(feature.filename).parent if feature.filename not in [None, '<string>'] else Path.cwd()
script_file = (base_path / script_path).resolve()

assert script_file.exists(), f'script {script_path} does not exist'

_execute_python_script(context, script_file.read_text())

@then('execute python script')
def step_setup_execute_python_script_inline(context: Context) -> None:
"""Execute inline python script specified in the step text.
The script will not execute on workers, only on master (distributed mode) or local (local mode), and
it will only execute once before the test starts.
This can be useful for generating test data files.
Example:
```gherkin
Then execute python script
\"\"\"
print('foobar script')
\"\"\"
```
"""
_execute_python_script(context, context.text)
59 changes: 58 additions & 1 deletion grizzly/steps/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
from __future__ import annotations

from os import environ
from pathlib import Path
from typing import cast

from grizzly.context import GrizzlyContext
from grizzly.locust import on_worker
from grizzly.tasks import SetVariableTask
from grizzly.testdata import GrizzlyVariables
from grizzly.testdata.utils import resolve_variable
from grizzly.types import VariableType
from grizzly.types.behave import Context, given, then
from grizzly.types.behave import Context, Feature, given, then
from grizzly.utils import has_template


Expand Down Expand Up @@ -131,3 +133,58 @@ def step_setup_variable_value(context: Context, name: str, value: str) -> None:
raise AssertionError(e) from e # noqa: TRY004

raise


def _execute_python_script(context: Context, source: str) -> None:
if on_worker(context):
return

scope = globals()
scope.update({'context': context})

exec(source, scope, scope) # noqa: S102

@then('execute python script "{script_path}"')
def step_setup_execute_python_script(context: Context, script_path: str) -> None:
"""Execute python script located in specified path.
The script will not execute on workers, only on master (distributed mode) or local (local mode), and
it will only execute once before the test starts. Available in the scope is the current `context` object.
This can be useful for generating test data files.
Example:
```gherkin
Then execute python script "../bin/generate-testdata.py"
```
"""
script_file = Path(script_path)
if not script_file.exists():
feature = cast(Feature, context.feature)
base_path = Path(feature.filename).parent if feature.filename not in [None, '<string>'] else Path.cwd()
script_file = (base_path / script_path).resolve()

assert script_file.exists(), f'script {script_path} does not exist'

_execute_python_script(context, script_file.read_text())

@then('execute python script')
def step_setup_execute_python_script_inline(context: Context) -> None:
"""Execute inline python script specified in the step text.
The script will not execute on workers, only on master (distributed mode) or local (local mode), and
it will only execute once before the test starts. Available in the scope is the current `context` object.
This can be useful for generating test data files.
Example:
```gherkin
Then execute python script
\"\"\"
print('foobar script')
\"\"\"
```
"""
_execute_python_script(context, context.text)
2 changes: 1 addition & 1 deletion grizzly/tasks/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from grizzly.scenarios import GrizzlyScenario


@template('content')
@template('content', 'expression')
class TransformerTask(GrizzlyTask):
expression: str
variable: str
Expand Down
6 changes: 4 additions & 2 deletions grizzly/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from typing import Any, Callable, Dict, List, Optional, Tuple, TypeVar, Union, cast

from locust.rpc.protocol import Message
from mypy_extensions import Arg, KwArg
from typing_extensions import Concatenate, ParamSpec

P = ParamSpec('P')

from grizzly_extras.text import PermutationEnum

Expand Down Expand Up @@ -236,7 +238,7 @@ def from_string(cls, key: str) -> str:

GrizzlyVariableType = Union[str, float, int, bool]

MessageCallback = Callable[[Arg(Environment, 'environment'), Arg(Message, 'msg'), KwArg(Any)], None]
MessageCallback = Callable[Concatenate[Environment, Message, P], None]

WrappedFunc = TypeVar('WrappedFunc', bound=Callable[..., Any])

Expand Down
94 changes: 46 additions & 48 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=60.10.0", "wheel>=0.37.0", "setuptools-scm>=7.0.3"]
requires = ["setuptools ==69.2.0", "wheel ==0.43.0", "setuptools-scm ==8.0.4"]
build-backend = "setuptools.build_meta"

[project]
Expand All @@ -14,24 +14,23 @@ license = {text = 'MIT'}
requires-python = ">=3.8"
dependencies = [
"locust >=2.24.0,<2.25.0",
"azure-core >=1.24.0,<2.0.0",
"azure-servicebus >=7.6.0,<7.10.0",
"azure-storage-blob >=12.9.0,<13.0.0",
"azure-iot-device >=2.12.0,<3.0.0",
"behave >=1.2.6,<2.0.0",
"influxdb >=5.3.1,<6.0.0",
"Jinja2 >=3.0.3,<4.0.0",
"jsonpath-ng >=1.5.3,<2.0.0",
"lxml >=4.8.0,<5.0.0",
"mypy-extensions >=0.4.3",
"opencensus-ext-azure >=1.1.1",
"python-dateutil >=2.8.2,<3.0.0",
"backports.zoneinfo >=0.2.1;python_version<'3.9'",
"pyzmq >=22.2.1,!=23.0.0",
"PyYAML >=6.0.1,<7.0.0",
"setproctitle >=1.2.2",
"tzdata >=2022.1",
"pyotp >=2.9.0,<3.0.0"
"azure-core ==1.30.1",
"azure-servicebus ==7.9.0",
"azure-storage-blob ==12.19.1",
"azure-iot-device ==2.13.0",
"behave ==1.2.6",
"influxdb ==5.3.1",
"Jinja2 ==3.1.3",
"jsonpath-ng ==1.6.1",
"lxml ==5.1.0",
"opencensus-ext-azure ==1.1.13",
"python-dateutil ==2.9.0.post0",
"backports.zoneinfo ==0.2.1;python_version<'3.9'",
"pyzmq ==25.1.2",
"PyYAML ==6.0.1",
"setproctitle ==1.3.3",
"pyotp ==2.9.0",
"tzdata >=2022.1"
]
classifiers = [
"Development Status :: 4 - Beta",
Expand Down Expand Up @@ -73,44 +72,43 @@ mq = [
"pymqi ==1.12.10"
]
dev = [
"wheel>=0.37.0",
"astunparse >=1.6.3",
"mypy >=1.3.0",
"pytest >=7.0.0",
"coverage[toml] >=6.4.4",
"pytest-cov >=3.0.0",
"pytest-mock >=3.7.0",
"pytest-timeout >=2.1.0",
"atomicwrites >= 1.4.0",
"wheel ==0.43.0",
"astunparse ==1.6.3",
"mypy ==1.9.0",
"pytest ==8.1.1",
"coverage[toml] ==6.4.4",
"pytest-cov ==5.0.0",
"pytest-mock ==3.14.0",
"pytest-timeout ==2.3.1",
"atomicwrites ==1.4.1",
"snakeviz ==2.2.0",
"ruff ==0.3.4",
"parameterized ==0.9.0",
"line-profiler ==4.1.2",
"types-paramiko >=2.8.13",
"types-python-dateutil >=2.8.9",
"types-PyYAML <6.0.0,>=5.3.0",
"types-requests >=2.27.0",
"types-Jinja2 >=2.0.0",
"types-backports >=0.1.3",
"snakeviz",
"ruff <1.0.0",
"parameterized >=0.9.0,<1.0.0",
"line-profiler ==4.1.2"
"types-backports >=0.1.3"
]
ci = [
"build >=0.7.0",
"twine >=3.8.0,<4.0.0"
"build ==1.1.1",
"twine ==5.0.0"
]
docs = [
"novella >=0.2.6",
"pydoc-markdown >=4.6.1",
"docspec >=2.1.2",
"docspec-python >=2.1.2",
"pytablewriter >=0.64.1",
"pip-licenses >=3.5.3,<4.2.0",
"requests >=2.27.1",
"mkdocs >=1.3.0",
"mkdocs-material >=8.3.2",
"packaging >=21.3",
"grizzly-loadtester-cli",
"python-frontmatter",
"mistune >=3.0.2"
"novella ==0.2.6",
"pydoc-markdown ==4.8.2",
"databind ==4.4.2",
"pytablewriter ==1.2.0",
"pip-licenses ==4.3.4",
"requests ==2.31.0",
"mkdocs ==1.5.3",
"mkdocs-material ==9.5.15",
"packaging ==24.0",
"mistune ==3.0.2",
"python-frontmatter ==1.1.0",
"grizzly-loadtester-cli"
]

[tool.setuptools_scm]
Expand Down
Loading

0 comments on commit 5c5b2e5

Please sign in to comment.