Skip to content

Commit

Permalink
support calling python scripts with arguments, by injecting them inte…
Browse files Browse the repository at this point in the history
… globals as "args".
  • Loading branch information
mgor committed Nov 11, 2024
1 parent cf9409e commit 2e797c4
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 12 deletions.
48 changes: 43 additions & 5 deletions grizzly/steps/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from os import environ
from pathlib import Path
from shlex import split as shlex_split
from typing import cast

from grizzly.context import GrizzlyContext
Expand Down Expand Up @@ -149,15 +150,52 @@ def step_setup_variable_value(context: Context, name: str, value: str) -> None:
raise


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

scope = globals()
scope.update({'context': context})
scope_args: list[str] | None = None
if args is not None:
scope_args = shlex_split(args)

scope = {**globals()}
scope.update({'context': context, 'args': scope_args})

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

@then('execute python script "{script_path}" with arguments "{arguments}"')
def step_setup_execute_python_script_with_args(context: Context, script_path: str, arguments: str) -> None:
"""Execute python script located in specified path, providing the specified arguments.
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
and also `args` (list), which is `shlex.split` of specified `arguments`.
This can be useful for generating test data files.
Example:
```gherkin
Then execute python script "../bin/generate-testdata.py"
```
"""
grizzly = cast(GrizzlyContext, context.grizzly)

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'

if has_template(arguments):
grizzly.scenario.orphan_templates.append(arguments)

arguments = cast(str, resolve_variable(grizzly.scenario, arguments, guess_datatype=False, try_file=False))

_execute_python_script(context, script_file.read_text(), arguments)

@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.
Expand All @@ -181,7 +219,7 @@ def step_setup_execute_python_script(context: Context, script_path: str) -> None

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

_execute_python_script(context, script_file.read_text())
_execute_python_script(context, script_file.read_text(), None)

@then('execute python script')
def step_setup_execute_python_script_inline(context: Context) -> None:
Expand All @@ -201,7 +239,7 @@ def step_setup_execute_python_script_inline(context: Context) -> None:
```
"""
_execute_python_script(context, context.text)
_execute_python_script(context, context.text, None)


@given('set context variable "{variable}" to "{value}"')
Expand Down
62 changes: 55 additions & 7 deletions tests/unit/test_grizzly/steps/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,43 @@ def test_step_setup_variable_value(behave_fixture: BehaveFixture, mocker: Mocker
]}


def test_step_setup_execute_python_script_with_args(grizzly_fixture: GrizzlyFixture, mocker: MockerFixture) -> None:
execute_script_mock = mocker.patch('grizzly.steps.setup._execute_python_script', return_value=None)
context = grizzly_fixture.behave.context
grizzly = grizzly_fixture.grizzly

original_cwd = Path.cwd()

try:
chdir(grizzly_fixture.test_context)
script_file = grizzly_fixture.test_context / 'bin' / 'generate-testdata.py'
script_file.parent.mkdir(exist_ok=True, parents=True)
script_file.write_text("print('foobar')")

step_setup_execute_python_script_with_args(context, script_file.as_posix(), '--foo=bar --bar foo --baz')

execute_script_mock.assert_called_once_with(context, "print('foobar')", '--foo=bar --bar foo --baz')
execute_script_mock.reset_mock()

step_setup_execute_python_script_with_args(context, 'bin/generate-testdata.py', '--foo=bar --bar foo --baz')

execute_script_mock.assert_called_once_with(context, "print('foobar')", '--foo=bar --bar foo --baz')
execute_script_mock.reset_mock()

context.feature.location.filename = f'{grizzly_fixture.test_context}/features/test.feature'

grizzly.scenario.variables.update({'foo': 'bar'})
step_setup_execute_python_script_with_args(context, '../bin/generate-testdata.py', '--foo={{ foo }} --bar foo --baz')

execute_script_mock.assert_called_once_with(context, "print('foobar')", '--foo=bar --bar foo --baz')
execute_script_mock.reset_mock()
finally:
with suppress(Exception):
chdir(original_cwd)
script_file.unlink()
script_file.parent.rmdir()


def test_step_setup_execute_python_script(grizzly_fixture: GrizzlyFixture, mocker: MockerFixture) -> None:
execute_script_mock = mocker.patch('grizzly.steps.setup._execute_python_script', return_value=None)
context = grizzly_fixture.behave.context
Expand Down Expand Up @@ -306,27 +343,38 @@ def test_step_setup_execute_python_script_inline(grizzly_fixture: GrizzlyFixture
chdir(original_cwd)


def test__execute_python_script_mock(behave_fixture: BehaveFixture, mocker: MockerFixture) -> None:
context = behave_fixture.context
def test__execute_python_script_mock(grizzly_fixture: GrizzlyFixture, mocker: MockerFixture) -> None:
grizzly = grizzly_fixture.grizzly
context = grizzly_fixture.behave.context
on_worker_mock = mocker.patch('grizzly.steps.setup.on_worker', return_value=True)
exec_mock = mocker.patch('builtins.exec')

# do not execute, since we're on a worker
on_worker_mock.return_value = True

_execute_python_script(context, "print('foobar')")
_execute_python_script(context, "print('foobar')", None)

on_worker_mock.assert_called_once_with(context)
exec_mock.assert_not_called()
on_worker_mock.reset_mock()

# execute
# execute, no args
on_worker_mock.return_value = False

_execute_python_script(context, "print('foobar')")
_execute_python_script(context, "print('foobar')", None)

on_worker_mock.assert_called_once_with(context)
exec_mock.assert_called_once_with("print('foobar')", SOME(dict, context=context, args=None), SOME(dict, context=context, args=None))
on_worker_mock.reset_mock()
exec_mock.reset_mock()

# execute, args
grizzly.scenario.variables.update({'foo': 'bar'})
_execute_python_script(context, "print('foobar')", '--foo=bar --bar foo --baz')

on_worker_mock.assert_called_once_with(context)
exec_mock.assert_called_once_with("print('foobar')", SOME(dict, context=context), SOME(dict, context=context))
scope = SOME(dict, context=context, args=['--foo=bar', '--bar', 'foo', '--baz'])
exec_mock.assert_called_once_with("print('foobar')", scope, scope)

def test__execute_python_script(behave_fixture: BehaveFixture, mocker: MockerFixture) -> None:
context = behave_fixture.context
Expand All @@ -336,7 +384,7 @@ def test__execute_python_script(behave_fixture: BehaveFixture, mocker: MockerFix
with pytest.raises(KeyError):
hasattr(context, '__foobar__')

_execute_python_script(context, "from pathlib import Path\nfrom os import path\nsetattr(context, '__foobar__', 'foobar')")
_execute_python_script(context, "from pathlib import Path\nfrom os import path\nsetattr(context, '__foobar__', 'foobar')", None)

assert context.__foobar__ == 'foobar'
assert hasattr(context, '__foobar__')
Expand Down

0 comments on commit 2e797c4

Please sign in to comment.