From a80f3046ce697e652d52507c713895f992ca8b3c Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Wed, 25 Mar 2026 08:29:12 +0100 Subject: [PATCH 01/10] add check within tests --- .../Check within time span.robot | 33 +++++++++++++++++++ .../Time constraints/base.resource | 2 ++ .../Time constraints/timed_keywords.py | 8 +++++ 3 files changed, 43 insertions(+) create mode 100644 atest/robotNL tests/01__Check that/Time constraints/Check within time span.robot create mode 100644 atest/robotNL tests/01__Check that/Time constraints/base.resource create mode 100644 atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py diff --git a/atest/robotNL tests/01__Check that/Time constraints/Check within time span.robot b/atest/robotNL tests/01__Check that/Time constraints/Check within time span.robot new file mode 100644 index 0000000..9a7830c --- /dev/null +++ b/atest/robotNL tests/01__Check that/Time constraints/Check within time span.robot @@ -0,0 +1,33 @@ +*** Settings *** +Resource base.resource +Library timed_keywords.py + +*** Test Cases *** +Single action passes within time span + Check that a delay of 10ms completes within 1 second + +Polling action passes within time span + Check that pass at the third attempt within 2 seconds + +Keyword passes, but too late + Run keyword and expect error *too late* + ... Check that a delay of 500ms completes within 100ms + +Single action fails after time span + ${message}= Run keyword and expect error CheckFailed* + ... Check that a delay of 200ms completes equals ${False} within 100ms + Check that ${message} does not contain text too late + +Polling action keeps failing within time span + Run keyword and expect error CheckFailed* + ... Check that Apple equals Pear within 100ms + +Invalid time string + Run keyword and expect error *Invalid time string* + ... Check that ${True} within the blink of an eye + + +*** Keywords *** +a delay of ${timespan} completes + Sleep ${timespan} + RETURN ${True} diff --git a/atest/robotNL tests/01__Check that/Time constraints/base.resource b/atest/robotNL tests/01__Check that/Time constraints/base.resource new file mode 100644 index 0000000..14a15f2 --- /dev/null +++ b/atest/robotNL tests/01__Check that/Time constraints/base.resource @@ -0,0 +1,2 @@ +*** Settings *** +Resource ../base.resource diff --git a/atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py b/atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py new file mode 100644 index 0000000..f9d1b4c --- /dev/null +++ b/atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py @@ -0,0 +1,8 @@ +def _fail_fail_pass_generator(): + yield False + yield False + yield True +gen = _fail_fail_pass_generator() + +def pass_at_the_third_attempt(): + return next(gen) From cecb1abb3a1143613552b898bc2df9092509df44 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:51:07 +0100 Subject: [PATCH 02/10] check within same timespan (part 1) --- ...time span.robot => 01__Check within.robot} | 8 +-- .../02__Check within same timespan.robot | 49 +++++++++++++++++++ .../Time constraints/base.resource | 7 +++ robotnl/RobotChecks.py | 26 ++++++---- 4 files changed, 74 insertions(+), 16 deletions(-) rename atest/robotNL tests/01__Check that/Time constraints/{Check within time span.robot => 01__Check within.robot} (88%) create mode 100644 atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot diff --git a/atest/robotNL tests/01__Check that/Time constraints/Check within time span.robot b/atest/robotNL tests/01__Check that/Time constraints/01__Check within.robot similarity index 88% rename from atest/robotNL tests/01__Check that/Time constraints/Check within time span.robot rename to atest/robotNL tests/01__Check that/Time constraints/01__Check within.robot index 9a7830c..1d1b122 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/Check within time span.robot +++ b/atest/robotNL tests/01__Check that/Time constraints/01__Check within.robot @@ -1,6 +1,6 @@ *** Settings *** Resource base.resource -Library timed_keywords.py + *** Test Cases *** Single action passes within time span @@ -25,9 +25,3 @@ Polling action keeps failing within time span Invalid time string Run keyword and expect error *Invalid time string* ... Check that ${True} within the blink of an eye - - -*** Keywords *** -a delay of ${timespan} completes - Sleep ${timespan} - RETURN ${True} diff --git a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot new file mode 100644 index 0000000..d0e12e3 --- /dev/null +++ b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot @@ -0,0 +1,49 @@ +*** Settings *** +Resource base.resource + + +*** Test Cases *** +The first check in a suite cannot refer to 'same timespan' + Run keyword and expect error *Joint timespan expected, but was not set* + ... Check that ${True} within same timespan + +Two checks pass within joint time constraint + Check that a delay of 10ms completes within 100ms + Check that a delay of 10ms completes within same timespan + +Second check fails the joint time constraint + Check that a delay of 10ms completes within 100ms + Run keyword and expect error *too late* + ... Check that a delay of 100ms completes within same timespan + +A timespan can carry over to the next Test + Run keyword and expect error *too late* + ... Check that a delay of 1ms completes within same timespan + +A check without time constraint ends the running timespan + Check that a delay of 10ms completes within 100ms + Check that ${True} + Run keyword and expect error *Joint timespan expected, but was not set* + ... Check that a delay of 10ms completes within same timespan + +Keywords do not affect the timespan of the Test + Check that a delay of 10ms completes within 100ms + Keyword that uses a shorter check within + Check that a delay of 50ms completes within same timespan + +Keywords do not affect the timespan of other keywords + Check that a delay of 10ms completes within 50ms + Nested keyword that uses check within + Check that a delay of 10ms completes within same timespan + + +*** Keywords *** +Keyword that uses a shorter check within + Check that ${True} within 10ms + +Nested keyword that uses check within + [Documentation] Note that a check without 'within' would end a timespan + ... when used in the same scope. + Check that ${True} + Keyword that uses a shorter check within + Check that ${True} diff --git a/atest/robotNL tests/01__Check that/Time constraints/base.resource b/atest/robotNL tests/01__Check that/Time constraints/base.resource index 14a15f2..c36817a 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/base.resource +++ b/atest/robotNL tests/01__Check that/Time constraints/base.resource @@ -1,2 +1,9 @@ *** Settings *** Resource ../base.resource +Library timed_keywords.py + + +*** Keywords *** +a delay of ${timespan} completes + Sleep ${timespan} + RETURN ${True} diff --git a/robotnl/RobotChecks.py b/robotnl/RobotChecks.py index 6a9ee3a..26908d5 100644 --- a/robotnl/RobotChecks.py +++ b/robotnl/RobotChecks.py @@ -49,6 +49,7 @@ class RobotChecks: ROBOT_LIBRARY_SCOPE = "GLOBAL" def __init__(self): self.__gui = None + self.running_timespan_end = None @property def _gui(self): @@ -74,7 +75,7 @@ def check_precondition(self, *args): point where it was able to check the requirement it was testing for. """ try: - return RobotChecks.__execute_check("Precondition", *args) + return self.__execute_check("Precondition", *args) except CheckFailed as failure: failure.ROBOT_CONTINUE_ON_FAILURE = False raise failure @@ -91,7 +92,7 @@ def check_postcondition(self, *args): the cause of failure. """ try: - return RobotChecks.__execute_check("Postcondition", *args) + return self.__execute_check("Postcondition", *args) except CheckFailed as failure: failure.ROBOT_CONTINUE_ON_FAILURE = False raise failure @@ -138,7 +139,7 @@ def check_that(self, *args): | `Check that` | _elevator floor_ | `equals` | 3 | within | 20 seconds | | `Check that` | _offset to floor level in mm_ | `≤` | 5 | within | 3 seconds | """ - return RobotChecks.__execute_check("Requirement", *args) + return self.__execute_check("Requirement", *args) RUN_KW_REGISTER.register_run_keyword('robotnl', check_that.__name__, args_to_process=0, deprecation_warning=False) def check_manual(self, checkRequestText=""): @@ -210,8 +211,7 @@ def check_interactive(self): except Exception as e: BuiltIn().log_to_console("Error in interactive keyword '%s'\n\n%s" % (newInput, e)) - @staticmethod - def __execute_check(checkType, *args): + def __execute_check(self, checkType, *args): """ Parse arguments for check keyword to determine its operands, evaluate them and execute the check. @@ -223,11 +223,21 @@ def __execute_check(checkType, *args): TimeOutInSeconds = 0 TimeRemaining = True s_TimeConstraint = "" + StartTime = time.perf_counter() if len(Arguments) >= 2 and str(Arguments[-2]).lower() == 'within': - EvaluatedTimeArg = RobotChecks.__evaluateOperand([Arguments[-1]])[0] - TimeOutInSeconds = timestr_to_secs(EvaluatedTimeArg) + if Arguments[-1].lower() == 'same timespan': + if self.running_timespan_end is None: + BuiltIn().fail("Joint timespan expected, but was not set before this check.") + TimeOutInSeconds = round(self.running_timespan_end - time.perf_counter(), ndigits=3) + else: + EvaluatedTimeArg = RobotChecks.__evaluateOperand([Arguments[-1]])[0] + TimeOutInSeconds = timestr_to_secs(EvaluatedTimeArg) s_TimeConstraint = Arguments[-1] Arguments = Arguments[:-2] + self.running_timespan_end = StartTime + TimeOutInSeconds + else: + # Checks without time contraint end any running timespan + self.running_timespan_end = None if not len(Arguments): BuiltIn().fail("%s check failed. There was nothing to check." % checkType) @@ -263,8 +273,6 @@ def __execute_check(checkType, *args): # Evaluate expression EvaluatedResult = None - StartTime = time.perf_counter() - TimeLeft = TimeOutInSeconds PollMax = 20 # After 20s people start wondering: "Is it still going?" Time for an update. PollMin = min(PollMax/8, TimeOutInSeconds*3/100) # Shortest delay is 3% of the target time. PollDelay = PollMin # Initial poll delay will be 2x PollMin From 4f1542db27fd69ee8d1618289bcdc1fe79a99e4a Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:27:42 +0100 Subject: [PATCH 03/10] check within same timespan (track nesting) --- .../02__Check within same timespan.robot | 125 +++++++++++++++--- .../Time constraints/base.resource | 2 +- .../Time constraints/helper_keywords.py | 31 +++++ .../Time constraints/timed_keywords.py | 8 -- robotnl/RobotChecks.py | 29 +++- 5 files changed, 164 insertions(+), 31 deletions(-) create mode 100644 atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py delete mode 100644 atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py diff --git a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot index d0e12e3..cc47b3d 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot +++ b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot @@ -4,46 +4,137 @@ Resource base.resource *** Test Cases *** The first check in a suite cannot refer to 'same timespan' - Run keyword and expect error *Joint timespan expected, but was not set* - ... Check that ${True} within same timespan + TRY + Check that ${True} within same timespan + EXCEPT Joint timespan expected, but was not set* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END Two checks pass within joint time constraint Check that a delay of 10ms completes within 100ms Check that a delay of 10ms completes within same timespan -Second check fails the joint time constraint +Second check passes, but not within the joint time constraint + Check that a delay of 10ms completes within 100ms + TRY + Check postcondition a delay of 100ms completes within same timespan + EXCEPT *too late* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END + +Setup new timespan to carry over to next test + Comment Setup only. Actual checks are executed in the next test. Check that a delay of 10ms completes within 100ms - Run keyword and expect error *too late* - ... Check that a delay of 100ms completes within same timespan A timespan can carry over to the next Test - Run keyword and expect error *too late* - ... Check that a delay of 1ms completes within same timespan + [Documentation] Carry over of a timespan to the next test within a suite is + ... useful when you have an expensive action that has multiple asynchronous + ... results that you want to report and tag independently. + Comment Expects the running timespan from the previous test + Check that a delay of 10ms completes within same timespan + TRY + Check that a delay of 90ms completes within same timespan + EXCEPT *too late* type=GLOB + Comment Total delays add up to 110ms for the alotted 100ms. + ELSE + Fail Expected fail did not occur + END A check without time constraint ends the running timespan Check that a delay of 10ms completes within 100ms Check that ${True} - Run keyword and expect error *Joint timespan expected, but was not set* - ... Check that a delay of 10ms completes within same timespan + TRY + Check that a delay of 10ms completes within same timespan + EXCEPT Joint timespan expected, but was not set* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END + +Timespans can be reused inside control structures + Check that a delay of 10ms completes within 100ms + FOR ${i} IN RANGE 2 + Check that a delay of 10ms completes within same timespan + END + +Checks after the timespan expired are executed exactly once + Reset counter + TRY + Check that ${False} within 10ms + EXCEPT CheckFailed* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END + TRY + Check that counter increases, then pass within same timespan + EXCEPT *too late* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END + Check that Counter value equals 1 Keywords do not affect the timespan of the Test + [Documentation] The keyword that separates the check that sets the timespan + ... from the one using it, sets a new timnespan internally of just 10ms. If + ... If the timespans are not properly separated, the second check will fail + ... as 'too late'. Check that a delay of 10ms completes within 100ms Keyword that uses a shorter check within - Check that a delay of 50ms completes within same timespan + Check that a delay of 20ms completes within same timespan + +Keywords do not have access to the timespan of the calling test + Check that ${True} within 100ms + TRY + Keyword that tries to use existing timespan + EXCEPT Joint timespan expected, but was not set* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END Keywords do not affect the timespan of other keywords - Check that a delay of 10ms completes within 50ms + Check that ${True} # Ends any running timespan Nested keyword that uses check within - Check that a delay of 10ms completes within same timespan + TRY + Check that a delay of 10ms completes within same timespan + EXCEPT Joint timespan expected, but was not set* type=GLOB + Comment Would not have failed if the nested keyword's timespan were accessable + ELSE + Fail Expected fail did not occur + END *** Keywords *** Keyword that uses a shorter check within Check that ${True} within 10ms +Keyword that tries to use existing timespan + Check that a delay of 10ms completes within same timespan + Nested keyword that uses check within - [Documentation] Note that a check without 'within' would end a timespan - ... when used in the same scope. - Check that ${True} - Keyword that uses a shorter check within - Check that ${True} + Check that a delay of 10ms completes within 50ms + Keyword that uses a longer check within + TRY + Check that a delay of 50ms completes within same timespan + EXCEPT *too late** type=GLOB + Comment Would not have failed if the longer timespan were set + ELSE + Fail Expected fail did not occur + END + +Keyword that uses a longer check within + TRY + Check that a delay of 10ms completes within same timespan + EXCEPT Joint timespan expected, but was not set* type=GLOB + Comment Would not have failed if the calling keyword's timespan were accessable + ELSE + Fail Expected fail did not occur + END + Check that a delay of 10ms completes within 1 second + Check that a delay of 10ms completes within same timespan diff --git a/atest/robotNL tests/01__Check that/Time constraints/base.resource b/atest/robotNL tests/01__Check that/Time constraints/base.resource index c36817a..a1e3f71 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/base.resource +++ b/atest/robotNL tests/01__Check that/Time constraints/base.resource @@ -1,6 +1,6 @@ *** Settings *** Resource ../base.resource -Library timed_keywords.py +Library helper_keywords.py *** Keywords *** diff --git a/atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py b/atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py new file mode 100644 index 0000000..7ae5520 --- /dev/null +++ b/atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py @@ -0,0 +1,31 @@ +from robot.api.deco import library, keyword + + +@library +class HelperKeywords: + def __init__(self): + self.gen = self._fail_fail_pass_generator() + self.counter = 0 + + @staticmethod + def _fail_fail_pass_generator(): + yield False + yield False + yield True + + @keyword("Pass at the third attempt") + def pass_at_third_attempt(self): + return next(self.gen) + + @keyword("Reset counter") + def reset_counter(self): + self.counter = 0 + + @keyword("counter increases, then pass") + def plus_counter(self): + self.counter += 1 + return True + + @keyword("counter value") + def counter_value(self): + return self.counter diff --git a/atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py b/atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py deleted file mode 100644 index f9d1b4c..0000000 --- a/atest/robotNL tests/01__Check that/Time constraints/timed_keywords.py +++ /dev/null @@ -1,8 +0,0 @@ -def _fail_fail_pass_generator(): - yield False - yield False - yield True -gen = _fail_fail_pass_generator() - -def pass_at_the_third_attempt(): - return next(gen) diff --git a/robotnl/RobotChecks.py b/robotnl/RobotChecks.py index 26908d5..0ac3889 100644 --- a/robotnl/RobotChecks.py +++ b/robotnl/RobotChecks.py @@ -48,8 +48,25 @@ class CheckFailed(RuntimeError): class RobotChecks: ROBOT_LIBRARY_SCOPE = "GLOBAL" def __init__(self): + self.ROBOT_LIBRARY_LISTENER = self self.__gui = None - self.running_timespan_end = None + self.scope_depth = 0 + self.nested_timespans = {} + + def _start_suite(self, data, result): + self.scope_depth = 0 + self.nested_timespans = {} + _end_suite = _start_suite + + def _start_keyword(self, data, result): + self.scope_depth += 1 + + def _end_keyword(self, data, result): + if self.scope_depth + 1 in self.nested_timespans: + # use depth+1 because 'check that' itself is also a keyword. The +1 + # keeps all check thats on the same level grouped. + self.nested_timespans.pop(self.scope_depth + 1) + self.scope_depth -= 1 @property def _gui(self): @@ -225,19 +242,21 @@ def __execute_check(self, checkType, *args): s_TimeConstraint = "" StartTime = time.perf_counter() if len(Arguments) >= 2 and str(Arguments[-2]).lower() == 'within': + running_timespan_end = self.nested_timespans.get(self.scope_depth) if Arguments[-1].lower() == 'same timespan': - if self.running_timespan_end is None: + if running_timespan_end is None: BuiltIn().fail("Joint timespan expected, but was not set before this check.") - TimeOutInSeconds = round(self.running_timespan_end - time.perf_counter(), ndigits=3) + TimeOutInSeconds = round(running_timespan_end - time.perf_counter(), ndigits=3) else: EvaluatedTimeArg = RobotChecks.__evaluateOperand([Arguments[-1]])[0] TimeOutInSeconds = timestr_to_secs(EvaluatedTimeArg) s_TimeConstraint = Arguments[-1] Arguments = Arguments[:-2] - self.running_timespan_end = StartTime + TimeOutInSeconds + self.nested_timespans[self.scope_depth] = StartTime + TimeOutInSeconds else: # Checks without time contraint end any running timespan - self.running_timespan_end = None + if self.scope_depth in self.nested_timespans: + self.nested_timespans.pop(self.scope_depth) if not len(Arguments): BuiltIn().fail("%s check failed. There was nothing to check." % checkType) From 9e919587b6ae7869abaabac91ee1f218877e477e Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Sat, 4 Apr 2026 12:43:07 +0200 Subject: [PATCH 04/10] checks without time constraint should not end timespan --- .../02__Check within same timespan.robot | 26 +++++++------------ robotnl/RobotChecks.py | 4 --- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot index cc47b3d..5693288 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot +++ b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot @@ -5,7 +5,7 @@ Resource base.resource *** Test Cases *** The first check in a suite cannot refer to 'same timespan' TRY - Check that ${True} within same timespan + Check that True within same timespan EXCEPT Joint timespan expected, but was not set* type=GLOB No Operation ELSE @@ -44,16 +44,10 @@ A timespan can carry over to the next Test Fail Expected fail did not occur END -A check without time constraint ends the running timespan +A check without time constraint does not end the running timespan Check that a delay of 10ms completes within 100ms - Check that ${True} - TRY - Check that a delay of 10ms completes within same timespan - EXCEPT Joint timespan expected, but was not set* type=GLOB - No Operation - ELSE - Fail Expected fail did not occur - END + Check that True + Check that a delay of 10ms completes within same timespan Timespans can be reused inside control structures Check that a delay of 10ms completes within 100ms @@ -89,7 +83,7 @@ Keywords do not affect the timespan of the Test Check that a delay of 20ms completes within same timespan Keywords do not have access to the timespan of the calling test - Check that ${True} within 100ms + Check that True within 100ms TRY Keyword that tries to use existing timespan EXCEPT Joint timespan expected, but was not set* type=GLOB @@ -99,12 +93,12 @@ Keywords do not have access to the timespan of the calling test END Keywords do not affect the timespan of other keywords - Check that ${True} # Ends any running timespan + Check that True within 50ms Nested keyword that uses check within TRY - Check that a delay of 10ms completes within same timespan - EXCEPT Joint timespan expected, but was not set* type=GLOB - Comment Would not have failed if the nested keyword's timespan were accessable + Check that True within same timespan + EXCEPT *too late* type=GLOB + Comment Timespan already passed during the nested keyword ELSE Fail Expected fail did not occur END @@ -112,7 +106,7 @@ Keywords do not affect the timespan of other keywords *** Keywords *** Keyword that uses a shorter check within - Check that ${True} within 10ms + Check that True within 10ms Keyword that tries to use existing timespan Check that a delay of 10ms completes within same timespan diff --git a/robotnl/RobotChecks.py b/robotnl/RobotChecks.py index 0ac3889..27a31f1 100644 --- a/robotnl/RobotChecks.py +++ b/robotnl/RobotChecks.py @@ -253,10 +253,6 @@ def __execute_check(self, checkType, *args): s_TimeConstraint = Arguments[-1] Arguments = Arguments[:-2] self.nested_timespans[self.scope_depth] = StartTime + TimeOutInSeconds - else: - # Checks without time contraint end any running timespan - if self.scope_depth in self.nested_timespans: - self.nested_timespans.pop(self.scope_depth) if not len(Arguments): BuiltIn().fail("%s check failed. There was nothing to check." % checkType) From 265ef1adea0abde73b24dea96fc58a2dd002a5bd Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Sun, 5 Apr 2026 09:59:33 +0200 Subject: [PATCH 05/10] Report details of reused timespan --- .../Time constraints/01__Check within.robot | 16 ++++++-- .../02__Check within same timespan.robot | 37 +++++++++++++++++++ .../Time constraints/helper_keywords.py | 11 ++++++ robotnl/RobotChecks.py | 34 +++++++++++++---- 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/atest/robotNL tests/01__Check that/Time constraints/01__Check within.robot b/atest/robotNL tests/01__Check that/Time constraints/01__Check within.robot index 1d1b122..be6c3cd 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/01__Check within.robot +++ b/atest/robotNL tests/01__Check that/Time constraints/01__Check within.robot @@ -11,7 +11,7 @@ Polling action passes within time span Keyword passes, but too late Run keyword and expect error *too late* - ... Check that a delay of 500ms completes within 100ms + ... Check that a delay of 50ms completes within 10ms Single action fails after time span ${message}= Run keyword and expect error CheckFailed* @@ -22,6 +22,14 @@ Polling action keeps failing within time span Run keyword and expect error CheckFailed* ... Check that Apple equals Pear within 100ms -Invalid time string - Run keyword and expect error *Invalid time string* - ... Check that ${True} within the blink of an eye +Time constraint from keyword + Run keyword and expect error *within 1/100th of a second [[]10 milliseconds[]] (too late)* + ... Check that a delay of 100ms completes within 1/100th of a second + +Invalid timespan text + Run keyword and expect error *Invalid time string 'the blink of an eye'. + ... Check that True within the blink of an eye + +Invalid timespan from keyword + Run keyword and expect error *Invalid time string 'My object'. + ... Check that True within not a timespan diff --git a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot index 5693288..69aac60 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot +++ b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot @@ -103,6 +103,43 @@ Keywords do not affect the timespan of other keywords Fail Expected fail did not occur END +Reporting shows the original timespan + Check that True within 10ms + TRY + Check that False within same timespan + EXCEPT *within 10ms* type=GLOB + Comment Note that the original text is kept, so '10ms' instead of the formatted '10 milliseconds' + ELSE + Fail Expected fail did not occur + END + +Reporting shows the original timespan including the keyword that produced it + Check that True within 1/100th of a second + TRY + Check that False within same timespan + EXCEPT *within 1/100th of a second [[]10 milliseconds[]]* type=GLOB + Comment Reports the keyword with its resulting value formatted between brackets + ELSE + Fail Expected fail did not occur + END + +Errors in timespan cancel the running timespan + Check that True within 10ms + TRY + Check that True within not a timespan + EXCEPT *Invalid time string 'My object'.* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END + TRY + Check that True within same timespan + EXCEPT Joint timespan expected, but was not set* type=GLOB + No Operation + ELSE + Fail Expected fail did not occur + END + *** Keywords *** Keyword that uses a shorter check within diff --git a/atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py b/atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py index 7ae5520..43e3071 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py +++ b/atest/robotNL tests/01__Check that/Time constraints/helper_keywords.py @@ -29,3 +29,14 @@ def plus_counter(self): @keyword("counter value") def counter_value(self): return self.counter + + @keyword("1/100th of a second") + def timespan_keyword(self): + return '0.01 sec' + + @keyword("Not a timespan") + def return_object(self): + class MyObject: + def __str__(self): + return 'My object' + return MyObject() diff --git a/robotnl/RobotChecks.py b/robotnl/RobotChecks.py index 27a31f1..18e08a4 100644 --- a/robotnl/RobotChecks.py +++ b/robotnl/RobotChecks.py @@ -242,17 +242,37 @@ def __execute_check(self, checkType, *args): s_TimeConstraint = "" StartTime = time.perf_counter() if len(Arguments) >= 2 and str(Arguments[-2]).lower() == 'within': - running_timespan_end = self.nested_timespans.get(self.scope_depth) - if Arguments[-1].lower() == 'same timespan': + timearg = Arguments[-1] + running_timespan_end, s_TimeConstraint = self.nested_timespans.get(self.scope_depth, (None, '')) + if str(timearg).lower() == 'same timespan': if running_timespan_end is None: BuiltIn().fail("Joint timespan expected, but was not set before this check.") TimeOutInSeconds = round(running_timespan_end - time.perf_counter(), ndigits=3) + + # Report timespan reuse + if TimeOutInSeconds < 0: + remaining = '0 seconds' + elif TimeOutInSeconds > 5: + # skip milliseconds for larger timespans + remaining = secs_to_timestr(round(TimeOutInSeconds)) + else: + remaining = secs_to_timestr(TimeOutInSeconds) + BuiltIn().log(f"Reusing joint timespan: {s_TimeConstraint}\n" + f"Must complete within the remaining {remaining}.") else: - EvaluatedTimeArg = RobotChecks.__evaluateOperand([Arguments[-1]])[0] - TimeOutInSeconds = timestr_to_secs(EvaluatedTimeArg) - s_TimeConstraint = Arguments[-1] + try: + EvaluatedTimeArg, s_TimeConstraint = RobotChecks.__evaluateOperand([timearg]) + TimeOutInSeconds = timestr_to_secs(EvaluatedTimeArg) + except: + # End any running timespan on error + if self.scope_depth in self.nested_timespans: + self.nested_timespans.pop(self.scope_depth) + raise + if is_keyword(timearg): + s_TimeConstraint = f"{timearg} [{secs_to_timestr(TimeOutInSeconds)}]" + Arguments = Arguments[:-2] - self.nested_timespans[self.scope_depth] = StartTime + TimeOutInSeconds + self.nested_timespans[self.scope_depth] = (StartTime + TimeOutInSeconds, s_TimeConstraint) if not len(Arguments): BuiltIn().fail("%s check failed. There was nothing to check." % checkType) @@ -335,7 +355,7 @@ def __execute_check(self, checkType, *args): ReportString = f"{checkType} check on '{s_LeftOperand} {OperatorKeyword} {s_RightOperand}'" if s_TimeConstraint: - ReportString += " within %s" % secs_to_timestr(TimeOutInSeconds) + ReportString += f" within {s_TimeConstraint}" if not TimeRemaining and EvaluatedResult == "passed": ReportString += " (too late)" raise CheckFailed(ReportString) From 43b9527fdfcfaf800a6e9b60bdd8466eed9c0b76 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Sun, 5 Apr 2026 13:08:54 +0200 Subject: [PATCH 06/10] report timespan remaining/overshoot --- .../02__Check within same timespan.robot | 11 +++++ robotnl/RobotChecks.py | 42 +++++++++++-------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot index 69aac60..4064a22 100644 --- a/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot +++ b/atest/robotNL tests/01__Check that/Time constraints/02__Check within same timespan.robot @@ -140,6 +140,17 @@ Errors in timespan cancel the running timespan Fail Expected fail did not occur END +Timespan is kept from the first instance + Check that a delay of 50ms completes within 100ms + Check that a delay of 10ms completes within same timespan + TRY + Check postcondition a delay of 50ms completes within same timespan + EXCEPT *too late* type=GLOB + Comment Would not have failed if the timer were reset at the first 'same timespan' + ELSE + Fail Expected fail did not occur + END + *** Keywords *** Keyword that uses a shorter check within diff --git a/robotnl/RobotChecks.py b/robotnl/RobotChecks.py index 18e08a4..47d9dd3 100644 --- a/robotnl/RobotChecks.py +++ b/robotnl/RobotChecks.py @@ -243,22 +243,17 @@ def __execute_check(self, checkType, *args): StartTime = time.perf_counter() if len(Arguments) >= 2 and str(Arguments[-2]).lower() == 'within': timearg = Arguments[-1] - running_timespan_end, s_TimeConstraint = self.nested_timespans.get(self.scope_depth, (None, '')) + Arguments = Arguments[:-2] + running_timespan_start, running_timespan, s_TimeConstraint =self.nested_timespans.get( + self.scope_depth, (None, None, '')) if str(timearg).lower() == 'same timespan': - if running_timespan_end is None: + if running_timespan is None: BuiltIn().fail("Joint timespan expected, but was not set before this check.") - TimeOutInSeconds = round(running_timespan_end - time.perf_counter(), ndigits=3) - - # Report timespan reuse - if TimeOutInSeconds < 0: - remaining = '0 seconds' - elif TimeOutInSeconds > 5: - # skip milliseconds for larger timespans - remaining = secs_to_timestr(round(TimeOutInSeconds)) - else: - remaining = secs_to_timestr(TimeOutInSeconds) + TimeOutInSeconds = round(running_timespan_start + running_timespan - StartTime, ndigits=3) + + remaining = self._human_time(0 if TimeOutInSeconds < 0 else TimeOutInSeconds) BuiltIn().log(f"Reusing joint timespan: {s_TimeConstraint}\n" - f"Must complete within the remaining {remaining}.") + f"Must complete within the remaining {remaining}") else: try: EvaluatedTimeArg, s_TimeConstraint = RobotChecks.__evaluateOperand([timearg]) @@ -270,12 +265,10 @@ def __execute_check(self, checkType, *args): raise if is_keyword(timearg): s_TimeConstraint = f"{timearg} [{secs_to_timestr(TimeOutInSeconds)}]" - - Arguments = Arguments[:-2] - self.nested_timespans[self.scope_depth] = (StartTime + TimeOutInSeconds, s_TimeConstraint) + self.nested_timespans[self.scope_depth] = (StartTime, TimeOutInSeconds, s_TimeConstraint) if not len(Arguments): - BuiltIn().fail("%s check failed. There was nothing to check." % checkType) + BuiltIn().fail(f"{checkType} check failed. There was nothing to check.") ############################################################################################ # Build expression @@ -355,8 +348,14 @@ def __execute_check(self, checkType, *args): ReportString = f"{checkType} check on '{s_LeftOperand} {OperatorKeyword} {s_RightOperand}'" if s_TimeConstraint: - ReportString += f" within {s_TimeConstraint}" + running_start, timespan, timestr = self.nested_timespans.get(self.scope_depth, (None, None, '')) + if TimeRemaining and running_start != StartTime: + elapsed = time.perf_counter() - running_start + BuiltIn().log(f"Check completed {self._human_time(elapsed)} into the alotted timespan ({timestr})") + ReportString += f" within {timestr}" if not TimeRemaining and EvaluatedResult == "passed": + overshoot = time.perf_counter() - (running_start + timespan) + BuiltIn().log(f"Check completed {self._human_time(overshoot)} too late") ReportString += " (too late)" raise CheckFailed(ReportString) @@ -399,3 +398,10 @@ def __evaluateOperand(operand): s_Operand += f" [{s_Value}]" return Value, s_Operand + + @staticmethod + def _human_time(time_in_seconds: float): + """return a human readable string for a time in seconds""" + SKIP_MILLI = 5 # Do not report milliseconds for timespans above SKIP_MILLI seconds + rounded_time = round(time_in_seconds, ndigits=0 if time_in_seconds > SKIP_MILLI else 3) + return secs_to_timestr(rounded_time) From 4375cdbe83d97d8be97f2e45af1a2da9ff7f7d0c Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:37:40 +0200 Subject: [PATCH 07/10] round down for timespans >10s --- robotnl/RobotChecks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/robotnl/RobotChecks.py b/robotnl/RobotChecks.py index 47d9dd3..db57299 100644 --- a/robotnl/RobotChecks.py +++ b/robotnl/RobotChecks.py @@ -402,6 +402,6 @@ def __evaluateOperand(operand): @staticmethod def _human_time(time_in_seconds: float): """return a human readable string for a time in seconds""" - SKIP_MILLI = 5 # Do not report milliseconds for timespans above SKIP_MILLI seconds - rounded_time = round(time_in_seconds, ndigits=0 if time_in_seconds > SKIP_MILLI else 3) + SKIP_MILLI = 10 # Do not report milliseconds for timespans above SKIP_MILLI seconds + rounded_time = int(time_in_seconds) if time_in_seconds > SKIP_MILLI else round(time_in_seconds, ndigits=3) return secs_to_timestr(rounded_time) From 8f50d92747c55d8d85e3b72db06101aa37b4b7b2 Mon Sep 17 00:00:00 2001 From: JFoederer <32476108+JFoederer@users.noreply.github.com> Date: Mon, 6 Apr 2026 14:15:24 +0200 Subject: [PATCH 08/10] docs for check within same timespan --- README.md | 11 +- docs/robotnl-libdoc.html | 2213 +++++++------------------------------- robotnl/RobotChecks.py | 26 +- 3 files changed, 388 insertions(+), 1862 deletions(-) diff --git a/README.md b/README.md index d3b15d4..dc92c3e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -# robotframeworkNL - the oneliner -robotframeworkNL is a proving ground to boost Robot framework closer to Natural Language. +# RobotNL - the oneliner +RobotNL is a proving ground to boost Robot framework closer to Natural Language. ## Introduction This project is an extension to [Robot framework](https://robotframework.org/) and although [Robot framework](https://robotframework.org/) made a very good step towards the goals of [keyword-driven testing ](https://en.wikipedia.org/wiki/Keyword-driven_testing) to make it readable for all stakeholders, there is still quite a lot of syntax involved that keeps test cases from really staying concise and to-the-point. In this project we will be introducing concepts to lift [Robot framework](https://robotframework.org/) to an even higher level. @@ -55,17 +55,20 @@ Using *Check that* keywords offers a large reduction in the need for variables i ### Time constraints -*Check that* offers support for executing checks that may take some time to complete. When using the optional `within` argument, followed by a time duration, *Check that* will apply *smart polling* to re-evaluate the expression and the keywords during the given period. Specifying the time limit is done using the standard [Robot Framework time format](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#toc-entry-176). It is advised to use a realistic time duration. This sets the correct expectation for the reader and helps robotnl optimise its polling algorithm. +*Check that* offers support for executing checks that may take some time to complete. When using the optional `within` argument, followed by a time duration, *Check that* will apply *smart polling* to re-evaluate the expression and the keywords during the given period. Specifying the time limit is done using the standard [Robot Framework time format](https://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html#toc-entry-176). It is advised to use a realistic time duration. This sets the correct expectation for the reader and helps RobotNL optimise its polling algorithm. |**Example using time constaints**|||||| |---|---|---|---|---|---| | Request elevator at floor | 3 ||||| | Check that | elevator doors are closed | within | 20 seconds || | Check that | current elevator floor | equals | 3 | within | 1 minute | +| Check that | elevator doors are opened | within | same timespan || + +The last line in the example uses the special argument `same timespan` to indicate that this check is linked to the same time constraint as the previous check. So, once the doors are closed, the elevator should not only reach the requested floor within 1 minute, but also the doors should be opened within that same 1-minute timespan. To use `within same timespan` you must have executed a check with a time constraint before. This is not necessarily the directly preceding line. The most recent time constraint set within scope is used as starting point. For test suites, that is the current test suite, for keywords that is the current keyword. ### Hybrid manual testing -To manually interact with your automated test run during testing or test case development, robotnl offers the *Check manual* and *Check interactive* keywords. These keywords can be included at any point in the test case to suspend the test run at the current position for user input. +To manually interact with your automated test run during testing or test case development, RobotNL offers the *Check manual* and *Check interactive* keywords. These keywords can be included at any point in the test case to suspend the test run at the current position for user input. ***Check Manual*** allows asking the tester a question. The question typically requests manual verification of an expected outcome. The answer will PASS or FAIL the test case, which is also reflected in the test report. diff --git a/docs/robotnl-libdoc.html b/docs/robotnl-libdoc.html index f0596b0..027d513 100644 --- a/docs/robotnl-libdoc.html +++ b/docs/robotnl-libdoc.html @@ -1,1876 +1,387 @@ - - -
- - - - - - - - - - - - - - - - - + + + + + + + -