16 from collections
import OrderedDict
17 from contextlib
import contextmanager
21 from robot.errors import (BreakLoop, ContinueLoop, DataError, ExecutionFailed,
22 ExecutionFailures, ExecutionPassed, ExecutionStatus)
23 from robot.result import (For
as ForResult, While
as WhileResult, If
as IfResult,
24 IfBranch
as IfBranchResult, Try
as TryResult,
25 TryBranch
as TryBranchResult)
27 from robot.utils import (cut_assign_value, frange, get_error_message, is_string,
28 is_list_like, is_number, plural_or_not
as s, seq2str,
29 split_from_equals, type_name, Matcher, timestr_to_secs)
32 from .statusreporter
import StatusReporter
35 DEFAULT_WHILE_LIMIT = 10_000
40 def __init__(self, context, run=True, templated=False):
51 except ExecutionPassed
as exception:
52 exception.set_earlier_failures(errors)
55 except ExecutionFailed
as exception:
56 errors.extend(exception.get_errors())
70 def run(self, step, name=None):
72 runner = context.get_runner(name
or step.name)
74 return runner.dry_run(step, context)
75 return runner.run(step, context, self.
_run_run)
78 def ForRunner(context, flavor='IN', run=True, templated=False):
79 runners = {
'IN': ForInRunner,
80 'IN RANGE': ForInRangeRunner,
81 'IN ZIP': ForInZipRunner,
82 'IN ENUMERATE': ForInEnumerateRunner}
83 runner = runners[flavor
or 'IN']
84 return runner(context, run, templated)
90 def __init__(self, context, run=True, templated=False):
100 error =
DataError(data.error, syntax=
True)
103 result = ForResult(data.variables, data.flavor, data.values)
108 except DataError
as err:
111 if self.
_run_loop_run_loop(data, result, values_for_rounds):
113 status.pass_status = result.NOT_RUN
121 for values
in values_for_rounds:
125 except (BreakLoop, ContinueLoop)
as ctrl:
126 if ctrl.earlier_failures:
127 errors.extend(ctrl.earlier_failures.get_errors())
128 if isinstance(ctrl, BreakLoop):
130 except ExecutionPassed
as passed:
131 passed.set_earlier_failures(errors)
133 except ExecutionFailed
as failed:
134 errors.extend(failed.get_errors())
144 values_per_round = len(data.variables)
154 all_name_value =
True
159 all_name_value =
False
160 if all_name_value
and values:
163 f
"FOR loop iteration over values that are all in 'name=value' "
164 f
"format like '{values[0]}' is deprecated. In the future this syntax "
165 f
"will mean iterating over names and values separately like "
166 f
"when iterating over '&{{dict}} variables. Escape at least one "
167 f
"of the values like '{name}\\={value}' to use normal FOR loop "
168 f
"iteration and to disable this warning."
173 result = OrderedDict()
174 replace_scalar = self.
_context_context.variables.replace_scalar
177 result.update(replace_scalar(item))
181 raise DataError(f
"Invalid FOR loop value '{item}'. When iterating "
182 f
"over dictionaries, values must be '&{{dict}}' "
183 f
"variables or use 'key=value' syntax.", syntax=
True)
185 result[replace_scalar(key)] = replace_scalar(value)
188 raise DataError(f
"Invalid dictionary item '{item}': {err}")
189 return result.items()
193 raise DataError(f
'Number of FOR loop variables must be 1 or 2 when '
194 f
'iterating over dictionaries, got {per_round}.',
199 return self.
_context_context.variables.replace_list(values)
203 if count % per_round != 0:
206 return (values[i:i+per_round]
for i
in range(0, count, per_round))
209 raise DataError(f
'Number of FOR loop values should be multiple of its '
210 f
'variables. Got {variables} variables but {values} '
211 f
'value{s(values)}.')
214 result = result.body.create_iteration()
215 if values
is not None:
216 variables = self.
_context_context.variables
219 values = [
''] * len(data.variables)
221 variables[name] = value
225 runner.run(data.body)
228 if len(variables) == 1
and len(values) != 1:
229 return [(variables[0], tuple(values))]
230 return zip(variables, values)
237 raise DataError(
'FOR IN RANGE loops do not support iterating over '
238 'dictionaries.', syntax=
True)
241 if not 1 <= len(values) <= 3:
242 raise DataError(f
'FOR IN RANGE expected 1-3 values, got {len(values)}.',
248 raise DataError(f
'Converting FOR IN RANGE values failed: {msg}.')
250 return ForInRunner._map_values_to_rounds(self, values, per_round)
255 number = eval(str(item), {})
257 raise TypeError(f
'Expected number, got {type_name(item)}.')
269 raise DataError(
'FOR IN ZIP loops do not support iterating over dictionaries.',
275 raise DataError(f
"FOR IN ZIP items must all be list-like, "
276 f
"got {type_name(item)} '{item}'.")
277 if len(values) % per_round != 0:
279 return zip(*(list(item)
for item
in values))
283 flavor =
'IN ENUMERATE'
286 if values
and values[-1].startswith(
'start='):
291 self._start, values = self.
_get_start_get_start(values)
292 return ForInRunner._resolve_dict_values(self, values)
295 self._start, values = self.
_get_start_get_start(values)
296 return ForInRunner._resolve_values(self, values)
299 if not values[-1].startswith(
'start='):
301 *values, start = values
303 raise DataError(
'FOR loop has no loop values.', syntax=
True)
305 start = self.
_context_context.variables.replace_string(start[6:])
309 raise DataError(f
"Start value must be an integer, got '{start}'.")
310 except DataError
as err:
311 raise DataError(f
'Invalid start value: {err}')
316 raise DataError(f
'Number of FOR IN ENUMERATE loop variables must be 1-3 '
317 f
'when iterating over dictionaries, got {per_round}.',
320 return ((i, v)
for i, v
in enumerate(values, start=self._start))
321 return ((i,) + v
for i, v
in enumerate(values, start=self._start))
324 per_round = max(per_round-1, 1)
325 values = ForInRunner._map_values_to_rounds(self, values, per_round)
326 return ([i] + v
for i, v
in enumerate(values, start=self._start))
329 raise DataError(f
'Number of FOR IN ENUMERATE loop values should be multiple of '
330 f
'its variables (excluding the index). Got {variables} '
331 f
'variables but {values} value{s(values)}.')
336 def __init__(self, context, run=True, templated=False):
348 error =
DataError(data.error, syntax=
True)
349 elif not ctx.dry_run:
351 limit = WhileLimit.create(data.limit, ctx.variables)
352 run = self.
_should_run_should_run(data.condition, ctx.variables)
353 except DataError
as err:
355 result = WhileResult(data.condition, data.limit)
357 if ctx.dry_run
or not run:
367 except (BreakLoop, ContinueLoop)
as ctrl:
368 if ctrl.earlier_failures:
369 errors.extend(ctrl.earlier_failures.get_errors())
370 if isinstance(ctrl, BreakLoop):
372 except ExecutionPassed
as passed:
373 passed.set_earlier_failures(errors)
375 except ExecutionFailed
as failed:
376 errors.extend(failed.get_errors())
377 if not failed.can_continue(ctx, self.
_templated_templated):
379 if not self.
_should_run_should_run(data.condition, ctx.variables):
387 runner.run(data.body)
391 condition = variables.replace_scalar(condition)
394 return bool(condition)
397 raise DataError(f
'Evaluating WHILE condition failed: {msg}')
406 def __init__(self, context, run=True, templated=False):
415 for branch
in data.body:
417 if self.
_run_if_branch_run_if_branch(branch, recursive_dry_run, data.error):
418 self.
_run_run =
False
419 except ExecutionStatus
as err:
421 self.
_run_run =
False
427 dry_run = self.
_context_context.dry_run
432 recursive_dry_run =
False
434 yield recursive_dry_run
441 result = IfBranchResult(branch.type, branch.condition)
445 error =
DataError(syntax_error, syntax=
True)
448 run_branch = self.
_should_run_branch_should_run_branch(branch, context, recursive_dry_run)
449 except DataError
as err:
454 if not recursive_dry_run:
455 runner.run(branch.body)
456 if error
and self.
_run_run:
461 condition = branch.condition
462 variables = context.variables
464 return not recursive_dry_run
465 if not self.
_run_run:
467 if condition
is None:
470 condition = variables.replace_scalar(condition)
473 return bool(condition)
476 raise DataError(f
'Evaluating {branch.type} condition failed: {msg}')
481 def __init__(self, context, run=True, templated=False):
492 error = self.
_run_try_run_try(data, run)
495 error = self.
_run_excepts_run_excepts(data, error, run=run_excepts_or_else)
499 error = self.
_run_else_run_else(data, run=run_excepts_or_else)
500 error = self.
_run_finally_run_finally(data, run)
or error
505 error_reported =
False
506 for branch
in data.body:
507 result = TryBranchResult(branch.type, branch.patterns, branch.variable)
510 runner.run(branch.body)
511 if not error_reported:
512 error_reported =
True
517 result = TryBranchResult(data.TRY)
518 return self.
_run_branch_run_branch(data.try_branch, result, run)
525 return not (error.skip
or error.syntax
or isinstance(error, ExecutionPassed))
533 runner.run(branch.body)
534 except ExecutionStatus
as err:
540 for branch
in data.except_branches:
543 except DataError
as err:
548 result = TryBranchResult(branch.type, branch.patterns,
549 branch.pattern_type, branch.variable)
552 self.
_context_context.variables[branch.variable] = str(error)
553 error = self.
_run_branch_run_branch(branch, result, error=pattern_error)
556 self.
_run_branch_run_branch(branch, result, run=
False)
560 if not branch.patterns:
563 'GLOB':
lambda m, p: Matcher(p, spaceless=
False, caseless=
False).match(m),
564 'LITERAL':
lambda m, p: m == p,
565 'REGEXP':
lambda m, p: re.match(rf
'{p}\Z', m)
is not None,
566 'START':
lambda m, p: m.startswith(p)
568 if branch.pattern_type:
569 pattern_type = self.
_context_context.variables.replace_string(branch.pattern_type)
571 pattern_type =
'LITERAL'
572 matcher = matchers.get(pattern_type.upper())
574 raise DataError(f
"Invalid EXCEPT pattern type '{pattern_type}', "
575 f
"expected {seq2str(matchers, lastsep=' or ')}.")
576 for pattern
in branch.patterns:
577 if matcher(error.message, self.
_context_context.variables.replace_string(pattern)):
583 result = TryBranchResult(data.ELSE)
584 return self.
_run_branch_run_branch(data.else_branch, result, run)
587 if data.finally_branch:
588 result = TryBranchResult(data.FINALLY)
592 runner.run(data.finally_branch.body)
593 except ExecutionStatus
as err:
605 value = variables.replace_string(limit)
606 if value.upper() ==
'NONE':
609 count = int(value.replace(
' ',
''))
614 raise DataError(f
"Invalid WHILE loop limit: Iteration count must be "
615 f
"a positive integer, got '{count}'.")
619 except ValueError
as err:
620 raise DataError(f
'Invalid WHILE loop limit: {err.args[0]}')
625 raise ExecutionFailed(f
"WHILE loop was aborted because it did not finish "
626 f
"within the limit of {self}. Use the 'limit' argument "
627 f
"to increase or remove the limit if needed.")
630 raise NotImplementedError
649 return f
'{self.max_time} seconds'
664 return f
'{self.max_iterations} iterations'
Used for communicating failures in test execution.
def __init__(self, context, run=True, templated=False)
def __init__(self, max_time)
def _map_values_to_rounds(self, values, per_round)
def _raise_wrong_variable_count(self, variables, values)
def _is_dict_iteration(self, values)
def _map_dict_values_to_rounds(self, values, per_round)
def _get_start(self, values)
def _resolve_dict_values(self, values)
def _resolve_values(self, values)
def _to_number_with_arithmetic(self, item)
def _resolve_dict_values(self, values)
def _map_values_to_rounds(self, values, per_round)
def _resolve_values(self, values)
def _is_dict_iteration(self, values)
def _raise_wrong_variable_count(self, variables, values)
def _map_variables_and_values(self, variables, values)
def _run_one_round(self, data, result, values=None, run=True)
def _get_values_for_rounds(self, data)
def _map_values_to_rounds(self, values, per_round)
def _resolve_dict_values(self, values)
def _map_dict_values_to_rounds(self, values, per_round)
def _run_loop(self, data, result, values_for_rounds)
def __init__(self, context, run=True, templated=False)
def _map_values_to_rounds(self, values, per_round)
def _resolve_dict_values(self, values)
def _should_run_branch(self, branch, context, recursive_dry_run=False)
def __init__(self, context, run=True, templated=False)
def _run_if_branch(self, branch, recursive_dry_run=False, syntax_error=None)
def _dry_run_recursion_detection(self, data)
def __init__(self, max_iterations)
def __init__(self, context, run=True)
def run(self, step, name=None)
def _run_branch(self, branch, result, run=True, error=None)
def _run_finally(self, data, run)
def _run_excepts(self, data, error, run)
def _run_invalid(self, data)
def _should_run_excepts_or_else(self, error, run)
def _run_else(self, data, run)
def _should_run_except(self, branch, error)
def __init__(self, context, run=True, templated=False)
def _run_try(self, data, run)
def __exit__(self, exc_type, exc_val, exc_tb)
def create(cls, limit, variables)
def _should_run(self, condition, variables)
def __init__(self, context, run=True, templated=False)
def _run_iteration(self, data, result, run=True)
def ForRunner(context, flavor='IN', run=True, templated=False)
def get_error_message()
Returns error message of the last occurred exception.
def split_from_equals(string)
def frange(*args)
Like range() but accepts float arguments.
def timestr_to_secs(timestr, round_to=3, accept_plain_values=True)
Parses time strings like '1h 10s', '01:00:10' and '42' and returns seconds.
def cut_assign_value(value)
def evaluate_expression(expression, variable_store, modules=None, namespace=None)
def is_dict_variable(string)