Skip to content

Commit

Permalink
Preserve test results in memory until test ends.
Browse files Browse the repository at this point in the history
Same with suite setup/teardown until suite sends. This is mostly
useful for listeners, especially for new listener v3 methods (robotframework#3296).

Preserving results during the whole run would consume too much memory.
  • Loading branch information
pekkaklarck committed Dec 12, 2023
1 parent c8a8fad commit 861c65a
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 250 deletions.
3 changes: 3 additions & 0 deletions atest/testdata/output/listener_interface/v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def start_suite(data, result):
def end_suite(data, result):
assert len(data.tests) == 5, '%d tests, not 5' % len(data.tests)
assert len(result.tests) == 5, '%d tests, not 5' % len(result.tests)
for test in result.tests:
if test.setup or test.body or test.teardown:
raise AssertionError(f"Result test '{test.name}' not cleared")
assert data.name == data.doc == result.name == 'Not visible in results'
assert result.doc.endswith('[start suite]')
assert result.metadata['suite'] == '[start]'
Expand Down
13 changes: 9 additions & 4 deletions src/robot/libraries/BuiltIn.py
Original file line number Diff line number Diff line change
Expand Up @@ -1896,10 +1896,15 @@ def run_keyword(self, name, *args):
ctx = self._context
if not (ctx.dry_run or self._accepts_embedded_arguments(name, ctx)):
name, args = self._replace_variables_in_name([name] + list(args))
parent = ctx.steps[-1][0] if ctx.steps else (ctx.test or ctx.suite)
kw = Keyword(name, args=args, parent=parent,
lineno=getattr(parent, 'lineno', None))
return kw.run(ctx)
if ctx.steps:
data, result = ctx.steps[-1]
lineno = data.lineno
else: # Called, typically by a listener, when no keyword started.
data = lineno = None
result = ctx.test or (ctx.suite.setup if not ctx.suite.has_tests
else ctx.suite.teardown)
kw = Keyword(name, args=args, parent=data, lineno=lineno)
return kw.run(result, ctx)

def _accepts_embedded_arguments(self, name, ctx):
if '{' in name:
Expand Down
76 changes: 33 additions & 43 deletions src/robot/running/bodyrunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@

from robot.errors import (BreakLoop, ContinueLoop, DataError, ExecutionFailed,
ExecutionFailures, ExecutionPassed, ExecutionStatus)
from robot.result import (For as ForResult, While as WhileResult, If as IfResult,
Try as TryResult)
from robot.output import librarylogger as logger
from robot.utils import (cut_assign_value, frange, get_error_message, is_list_like,
is_number, plural_or_not as s, secs_to_timestr, seq2str,
Expand All @@ -43,12 +41,12 @@ def __init__(self, context, run=True, templated=False):
self._run = run
self._templated = templated

def run(self, body):
def run(self, data, result):
errors = []
passed = None
for step in body:
for item in data.body:
try:
step.run(self._context, self._run, self._templated)
item.run(result, self._context, self._run, self._templated)
except ExecutionPassed as exception:
exception.set_earlier_failures(errors)
passed = exception
Expand All @@ -68,12 +66,12 @@ def __init__(self, context, run=True):
self._context = context
self._run = run

def run(self, step, name=None):
def run(self, data, result, name=None):
context = self._context
runner = context.get_runner(name or step.name)
runner = context.get_runner(name or data.name)
if context.dry_run:
return runner.dry_run(step, context)
return runner.run(step, context, self._run)
return runner.dry_run(data, result, context)
return runner.run(data, result, context, self._run)


def ForRunner(context, flavor='IN', run=True, templated=False):
Expand All @@ -93,16 +91,14 @@ def __init__(self, context, run=True, templated=False):
self._run = run
self._templated = templated

def run(self, data):
def run(self, data, result):
error = None
run = False
if self._run:
if data.error:
error = DataError(data.error, syntax=True)
else:
run = True
result = ForResult(data.assign, data.flavor, data.values, data.start,
data.mode, data.fill)
with StatusReporter(data, result, self._context, run) as status:
if run:
try:
Expand Down Expand Up @@ -224,7 +220,8 @@ def _run_one_round(self, data, result, values=None, run=True):
result.assign[name] = cut_assign_value(value)
runner = BodyRunner(self._context, run, self._templated)
with StatusReporter(data, result, self._context, run):
runner.run(data.body)
# FIXME: Should create ForIteration data object here.
runner.run(data, result)

def _map_variables_and_values(self, variables, values):
if len(variables) == 1 and len(values) != 1:
Expand Down Expand Up @@ -381,17 +378,13 @@ def __init__(self, context, run=True, templated=False):
self._run = run
self._templated = templated

def run(self, data):
def run(self, data, result):
ctx = self._context
error = None
run = False
limit = None
loop_result = WhileResult(data.condition,
data.limit,
data.on_limit,
data.on_limit_message,
start_time=datetime.now())
iter_result = loop_result.body.create_iteration(start_time=datetime.now())
result.start_time = datetime.now()
iter_result = result.body.create_iteration(start_time=datetime.now())
if self._run:
if data.error:
error = DataError(data.error, syntax=True)
Expand All @@ -404,7 +397,7 @@ def run(self, data):
run = self._should_run(data.condition, ctx.variables)
except DataError as err:
error = err
with StatusReporter(data, loop_result, self._context, run):
with StatusReporter(data, result, self._context, run):
if ctx.dry_run or not run:
self._run_iteration(data, iter_result, run)
if error:
Expand Down Expand Up @@ -433,7 +426,7 @@ def run(self, data):
errors.extend(failed.get_errors())
if not failed.can_continue(ctx, self._templated):
break
iter_result = loop_result.body.create_iteration(start_time=datetime.now())
iter_result = result.body.create_iteration(start_time=datetime.now())
if not self._should_run(data.condition, ctx.variables):
break
if errors:
Expand All @@ -442,7 +435,8 @@ def run(self, data):
def _run_iteration(self, data, result, run=True):
runner = BodyRunner(self._context, run, self._templated)
with StatusReporter(data, result, self._context, run):
runner.run(data.body)
# FIXME: Should create WhileIteration data object here.
runner.run(data, result)

def _should_run(self, condition, variables):
if not condition:
Expand All @@ -463,10 +457,9 @@ def __init__(self, context, run=True, templated=False):
self._run = run
self._templated = templated

def run(self, data):
def run(self, data, result):
with self._dry_run_recursion_detection(data) as recursive_dry_run:
error = None
result = IfResult()
with StatusReporter(data, result, self._context, self._run):
for branch in data.body:
try:
Expand All @@ -491,43 +484,41 @@ def _dry_run_recursion_detection(self, data):
finally:
self._dry_run_stack.pop()

def _run_if_branch(self, branch, result, recursive_dry_run=False, syntax_error=None):
def _run_if_branch(self, data, result, recursive_dry_run=False, syntax_error=None):
context = self._context
result = result.body.create_branch(branch.type, branch.condition,
result = result.body.create_branch(data.type, data.condition,
start_time=datetime.now())
error = None
if syntax_error:
run_branch = False
error = DataError(syntax_error, syntax=True)
else:
try:
run_branch = self._should_run_branch(branch, context, recursive_dry_run)
run_branch = self._should_run_branch(data, context, recursive_dry_run)
except DataError as err:
error = err
run_branch = False
with StatusReporter(branch, result, context, run_branch):
with StatusReporter(data, result, context, run_branch):
runner = BodyRunner(context, run_branch, self._templated)
if not recursive_dry_run:
runner.run(branch.body)
runner.run(data, result)
if error and self._run:
raise error
return run_branch

def _should_run_branch(self, branch, context, recursive_dry_run=False):
condition = branch.condition
variables = context.variables
def _should_run_branch(self, data, context, recursive_dry_run=False):
if context.dry_run:
return not recursive_dry_run
if not self._run:
return False
if condition is None:
if data.condition is None:
return True
try:
return evaluate_expression(condition, variables.current,
return evaluate_expression(data.condition, context.variables.current,
resolve_variables=True)
except Exception:
msg = get_error_message()
raise DataError(f'Invalid {branch.type} condition: {msg}')
raise DataError(f'Invalid {data.type} condition: {msg}')


class TryRunner:
Expand All @@ -537,9 +528,8 @@ def __init__(self, context, run=True, templated=False):
self._run = run
self._templated = templated

def run(self, data):
def run(self, data, result):
run = self._run
result = TryResult()
with StatusReporter(data, result, self._context, run):
if data.error:
self._run_invalid(data, result)
Expand All @@ -563,7 +553,7 @@ def _run_invalid(self, data, result):
branch.pattern_type, branch.assign)
with StatusReporter(branch, branch_result, self._context, run=False, suppress=True):
runner = BodyRunner(self._context, run=False, templated=self._templated)
runner.run(branch.body)
runner.run(branch, branch_result)
if not error_reported:
error_reported = True
raise DataError(data.error, syntax=True)
Expand All @@ -580,13 +570,13 @@ def _should_run_excepts_or_else(self, error, run):
return True
return not (error.skip or error.syntax or isinstance(error, ExecutionPassed))

def _run_branch(self, branch, result, run=True, error=None):
def _run_branch(self, data, result, run=True, error=None):
try:
with StatusReporter(branch, result, self._context, run):
with StatusReporter(data, result, self._context, run):
if error:
raise error
runner = BodyRunner(self._context, run, self._templated)
runner.run(branch.body)
runner.run(data, result)
except ExecutionStatus as err:
return err
else:
Expand Down Expand Up @@ -645,7 +635,7 @@ def _run_finally(self, data, result, run):
try:
with StatusReporter(data.finally_branch, result, self._context, run):
runner = BodyRunner(self._context, run, self._templated)
runner.run(data.finally_branch.body)
runner.run(data.finally_branch, result)
except ExecutionStatus as err:
return err
else:
Expand Down
18 changes: 9 additions & 9 deletions src/robot/running/invalidkeyword.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from robot.variables import VariableAssignment

from .arguments import EmbeddedArguments
from .model import Keyword
from .model import Keyword as KeywordData
from .statusreporter import StatusReporter
from .keywordimplementation import KeywordImplementation

Expand All @@ -35,23 +35,23 @@ def _get_embedded(self, name) -> 'EmbeddedArguments|None':
def create_runner(self, name, languages=None):
return InvalidKeywordRunner(self, name)

def bind(self, data: Keyword) -> 'InvalidKeyword':
def bind(self, data: KeywordData) -> 'InvalidKeyword':
return self.copy(parent=data.parent)


class InvalidKeywordRunner:

def __init__(self, keyword, name=None):
def __init__(self, keyword: InvalidKeyword, name: 'str|None' = None):
self.keyword = keyword
self.name = name or keyword.name

def run(self, data, context, run=True):
def run(self, data: KeywordData, result: KeywordResult, context, run=True):
kw = self.keyword.bind(data)
result = KeywordResult(name=self.name,
owner=kw.owner.name if kw.owner else None,
args=data.args,
assign=tuple(VariableAssignment(data.assign)),
type=data.type)
result.config(name=self.name,
owner=kw.owner.name if kw.owner else None,
args=data.args,
assign=tuple(VariableAssignment(data.assign)),
type=data.type)
with StatusReporter(data, result, context, run, implementation=kw):
if run:
raise DataError(kw.error)
Expand Down
Loading

0 comments on commit 861c65a

Please sign in to comment.