Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

unique scenario variables per user type instance #338

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion grizzly/scenarios/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ def on_start(self) -> None:
self.consumer = TestdataConsumer(
scenario=self,
address=producer_address,
identifier=self.__class__.__name__,
)
self.user.scenario_state = ScenarioState.RUNNING
else:
Expand Down
6 changes: 4 additions & 2 deletions grizzly/steps/background/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def step_shapes_user_count(context: Context, value: str, **_kwargs: Any) -> None
user_count = max(int(round(float(resolve_variable(grizzly.scenario, value)), 0)), 1)

if has_template(value):
grizzly.scenario.orphan_templates.append(value)
for scenario in grizzly.scenarios:
scenario.orphan_templates.append(value)

assert user_count >= 0, f'{value} resolved to {user_count} users, which is not valid'

Expand Down Expand Up @@ -85,7 +86,8 @@ def step_shapes_spawn_rate(context: Context, value: str, **_kwargs: Any) -> None
spawn_rate = max(float(resolve_variable(grizzly.scenario, value)), 0.01)

if has_template(value):
grizzly.scenario.orphan_templates.append(value)
for scenario in grizzly.scenarios:
scenario.orphan_templates.append(value)

assert spawn_rate > 0.0, f'{value} resolved to {spawn_rate} users, which is not valid'

Expand Down
10 changes: 6 additions & 4 deletions grizzly/testdata/communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,12 @@ class TestdataConsumer:
__test__: bool = False

scenario: GrizzlyScenario
logger: logging.Logger
identifier: str
stopped: bool

def __init__(self, scenario: GrizzlyScenario, identifier: str, address: str = 'tcp://127.0.0.1:5555') -> None:
def __init__(self, scenario: GrizzlyScenario, address: str = 'tcp://127.0.0.1:5555') -> None:
self.scenario = scenario
self.identifier = identifier
self.logger = logging.getLogger(f'{__name__}/{self.identifier}')
self.identifier = scenario.__class__.__name__

self.context = zmq.Context()
self.socket = self.context.socket(zmq.REQ)
Expand All @@ -49,6 +47,10 @@ def __init__(self, scenario: GrizzlyScenario, identifier: str, address: str = 't

self.logger.debug('connected to producer at %s', address)

@property
def logger(self) -> logging.Logger:
return self.scenario.logger

def stop(self) -> None:
if self.stopped:
return
Expand Down
2 changes: 1 addition & 1 deletion grizzly/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def from_string(cls, key: str) -> str:

HandlerContextType = Union[dict[str, Any], Optional[Any]]

GrizzlyVariableType = Union[str, float, int, bool] # , dict, list]
GrizzlyVariableType = Union[str, float, int, bool]

MessageCallback = Callable[Concatenate[Environment, Message, P], None]

Expand Down
21 changes: 18 additions & 3 deletions grizzly/users/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from grizzly.context import GrizzlyContext
from grizzly.events import GrizzlyEventHook, RequestLogger, ResponseHandler
from grizzly.exceptions import AsyncMessageAbort, RestartScenario
from grizzly.testdata import GrizzlyVariables
from grizzly.types import GrizzlyResponse, RequestType, ScenarioState
from grizzly.types.locust import Environment, StopUser
from grizzly.utils import has_template, merge_dicts
Expand Down Expand Up @@ -92,14 +93,21 @@ class GrizzlyUser(User, metaclass=GrizzlyUserMeta):
def __init__(self, environment: Environment, *args: Any, **kwargs: Any) -> None:
super().__init__(environment, *args, **kwargs)

self.logger = logging.getLogger(f'{self.__class__.__name__}/{id(self)}')

self._context_root = Path(environ.get('GRIZZLY_CONTEXT_ROOT', '.'))
self._context = deepcopy(self.__class__.__context__)
self._scenario_state = None
self._scenario = copy(self.__scenario__)

# these are not copied, and we can share reference
self._scenario._tasks = self.__scenario__._tasks

self.logger = logging.getLogger(f'{self.__class__.__name__}/{id(self)}')
# each instance of a user type should have their own globals dict
self._scenario._jinja2 = self._scenario._jinja2.overlay()
self._scenario._jinja2.globals = GrizzlyVariables(**self._scenario.jinja2.globals)
self.logger.debug('variables: type=%d, instance=%d', id(self._scenario.jinja2.globals), id(self.__scenario__.jinja2.globals))

self.abort = False
self.event_hook = GrizzlyEventHook()
self.event_hook.add_listener(ResponseHandler(self))
Expand All @@ -112,6 +120,12 @@ def on_quitting(self, *_args: Any, **kwargs: Any) -> None:
if not self.abort:
self.abort = cast(bool, kwargs.get('abort', False))

def on_start(self) -> None:
super().on_start()

def on_stop(self) -> None:
super().on_stop()

@property
def metadata(self) -> dict[str, Any]:
return self._context.get('metadata', None) or {}
Expand Down Expand Up @@ -267,7 +281,7 @@ def render(self, request_template: RequestTask) -> RequestTask:

request.__rendered__ = True
except Exception as e:
message = 'failed to render request template'
message = f'failed to render request template:\n! source:\n{request.source}\n! variables:\n{self._scenario.jinja2.globals}'
self.logger.exception(message)
raise StopUser from e
else:
Expand All @@ -288,8 +302,9 @@ def add_context(self, context: dict[str, Any]) -> None:
def set_variable(self, variable: str, value: Any) -> None:
old_value = self._scenario.variables.get(variable, None)
self._scenario.variables.update({variable: value})
message = f'context {variable=}, value={old_value} -> {value}'
message = f'instance {variable=}, value={old_value} -> {value} in {id(self._scenario.jinja2.globals)}'
self.logger.debug(message)
assert self._scenario.variables[variable] == self._scenario.jinja2.globals[variable]


from .blobstorage import BlobStorageUser
Expand Down
25 changes: 14 additions & 11 deletions tests/e2e/test_variables.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
def test_e2e_variables(e2e_fixture: End2EndFixture) -> None:
feature_file = e2e_fixture.create_feature(dedent("""Feature: variables
Background: common configuration
Given "2" users
And spawn rate is "2" users per second
Given spawn rate is "2" users per second
And value for variable "background_variable" is "foobar"
And value for variable "AtomicIntegerIncrementer.test" is "10"
Scenario: Scenario 1
Given a user of type "Dummy" load testing "null"
And repeat for "1" iteration
Given "2" users of type "Dummy" load testing "null"
And repeat for "2" iteration
And wait "0.0..0.5" seconds between tasks
And value for variable "scenario_1" is "{{ background_variable }}"
And value for variable "AtomicRandomString.scenario" is "AA%s | upper=True, count=10"

Expand All @@ -26,8 +26,9 @@ def test_e2e_variables(e2e_fixture: End2EndFixture) -> None:
Then log message "Scenario 1::AtomicIntegerIncrementer.test={{ AtomicIntegerIncrementer.test }}"
Then log message "Scenario 1::AtomicRandomString.scenario={{ AtomicRandomString.scenario }}"
Scenario: Scenario 2
Given a user of type "Dummy" load testing "null"
And repeat for "1" iteration
Given "2" users of type "Dummy" load testing "null"
And repeat for "2" iteration
And wait "0.0..0.5" seconds between tasks
And value for variable "scenario_2" is "{{ background_variable }}"
And value for variable "AtomicRandomString.scenario" is "BB%s | upper=True, count=5"

Expand All @@ -45,10 +46,12 @@ def test_e2e_variables(e2e_fixture: End2EndFixture) -> None:

print(result)

assert result.count('scenario_1=foobar') == 1
assert result.count('scenario_2=foobar') == 1
assert result.count('background_variable=foobar') == 2
assert result.count('scenario_1=foobar') == 2
assert result.count('scenario_2=foobar') == 2
assert result.count('background_variable=foobar') == 4
assert result.count('Scenario 1::AtomicIntegerIncrementer.test=10') == 1
assert result.count('Scenario 1::AtomicIntegerIncrementer.test=11') == 1
assert result.count('Scenario 2::AtomicIntegerIncrementer.test=10') == 1
assert result.count('Scenario 1::AtomicRandomString.scenario=AA') == 1
assert result.count('Scenario 2::AtomicRandomString.scenario=BB') == 1
assert result.count('Scenario 2::AtomicIntegerIncrementer.test=11') == 1
assert result.count('Scenario 1::AtomicRandomString.scenario=AA') == 2
assert result.count('Scenario 2::AtomicRandomString.scenario=BB') == 2
2 changes: 1 addition & 1 deletion tests/unit/test_grizzly/scenarios/test_iterator.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def test_iterator(self, grizzly_fixture: GrizzlyFixture, mocker: MockerFixture)
assert isinstance(parent, IteratorScenario)
assert not parent._prefetch

parent.consumer = TestdataConsumer(parent, identifier='test')
parent.consumer = TestdataConsumer(parent)

def mock_request(data: Optional[dict[str, Any]]) -> None:
def testdata_request(self: TestdataConsumer, scenario: str) -> Optional[dict[str, Any]]: # noqa: ARG001
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/test_grizzly/testdata/test_communication.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ def mock_recv_json(data: dict[str, Any], action: Optional[str] = 'consume') -> N
parent = grizzly_fixture()
grizzly = grizzly_fixture.grizzly

consumer = TestdataConsumer(parent, identifier='test')
consumer = TestdataConsumer(parent)

try:
mock_recv_json({})
Expand Down Expand Up @@ -759,7 +759,7 @@ def test_stop_exception(self, mocker: MockerFixture, noop_zmq: NoopZmqFixture, c

parent = grizzly_fixture()

consumer = TestdataConsumer(parent, identifier='test')
consumer = TestdataConsumer(parent)

with caplog.at_level(logging.DEBUG):
consumer.stop()
Expand Down Expand Up @@ -789,7 +789,7 @@ def test_testdata_exception(self, mocker: MockerFixture, noop_zmq: NoopZmqFixtur

parent = grizzly_fixture()

consumer = TestdataConsumer(parent, identifier='test')
consumer = TestdataConsumer(parent)

with pytest.raises(ZMQAgain):
consumer.testdata('test')
Expand All @@ -807,7 +807,7 @@ def test_keystore(self, mocker: MockerFixture, noop_zmq: NoopZmqFixture, grizzly
noop_zmq('grizzly.testdata.communication')
parent = grizzly_fixture()

consumer = TestdataConsumer(parent, identifier='test')
consumer = TestdataConsumer(parent)

def echo(value: dict[str, Any]) -> dict[str, Any]:
return value
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/test_grizzly/users/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,14 @@ def test_request(self, grizzly_fixture: GrizzlyFixture, mocker: MockerFixture) -
def test_context(self, behave_fixture: BehaveFixture) -> None:
behave_fixture.grizzly.scenarios.create(behave_fixture.create_scenario('test scenario'))
DummyGrizzlyUser.__scenario__ = behave_fixture.grizzly.scenario
original_id = id(DummyGrizzlyUser.__scenario__._jinja2)
user = DummyGrizzlyUser(behave_fixture.locust.environment)

assert user._scenario is not DummyGrizzlyUser.__scenario__
assert id(user._scenario._jinja2) != original_id
assert id(user._scenario._jinja2.globals) != id(DummyGrizzlyUser.__scenario__._jinja2.globals)
assert user._scenario._jinja2.globals.keys() == DummyGrizzlyUser.__scenario__._jinja2.globals.keys()

context = user.context()

assert isinstance(context, dict)
Expand Down
Loading