robotframework-nl 3.0.0__tar.gz → 3.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {robotframework-nl-3.0.0/robotframework_nl.egg-info → robotframework_nl-3.2.0}/PKG-INFO +11 -7
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/README.md +8 -5
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/pyproject.toml +1 -1
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0/robotframework_nl.egg-info}/PKG-INFO +11 -7
- robotframework_nl-3.2.0/robotnl/CheckOperator.py +394 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotnl/RobotChecks.py +92 -26
- robotframework_nl-3.2.0/robotnl/version.py +1 -0
- robotframework-nl-3.0.0/robotnl/CheckOperator.py +0 -228
- robotframework-nl-3.0.0/robotnl/version.py +0 -1
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/LICENSE +0 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotframework_nl.egg-info/SOURCES.txt +0 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotframework_nl.egg-info/dependency_links.txt +0 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotframework_nl.egg-info/requires.txt +0 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotframework_nl.egg-info/top_level.txt +0 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotnl/__init__.py +0 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotnl/inline_keywords.py +0 -0
- {robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-nl
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: robotnl is a proving ground to boost Robot framework closer to Natural Language.
|
|
5
5
|
Author-email: Johan Foederer <github@famfoe.nl>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -42,14 +42,15 @@ Requires-Python: >=3.8
|
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: robotframework>=7.0
|
|
45
|
+
Dynamic: license-file
|
|
45
46
|
|
|
46
|
-
#
|
|
47
|
-
|
|
47
|
+
# RobotNL - the oneliner
|
|
48
|
+
RobotNL is a proving ground to boost Robot framework closer to Natural Language.
|
|
48
49
|
|
|
49
50
|
## Introduction
|
|
50
51
|
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.
|
|
51
52
|
|
|
52
|
-
This
|
|
53
|
+
This release introduces ``inline keywords`` and a set of ``Check that`` keywords that help to vastly reduce the amount of ``${...}`` cluttering in your test cases. Combining these concepts helps to write a cleaner [Domain Specific Language](https://en.wikipedia.org/wiki/Domain-specific_language) around your [domain vocabulary](https://en.wikipedia.org/wiki/Jargon).
|
|
53
54
|
|
|
54
55
|
## Installation
|
|
55
56
|
The recommended installation method is using [pip](http://pip-installer.org)
|
|
@@ -100,17 +101,20 @@ Using *Check that* keywords offers a large reduction in the need for variables i
|
|
|
100
101
|
|
|
101
102
|
### Time constraints
|
|
102
103
|
|
|
103
|
-
*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
|
|
104
|
+
*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.
|
|
104
105
|
|
|
105
106
|
|**Example using time constaints**||||||
|
|
106
107
|
|---|---|---|---|---|---|
|
|
107
108
|
| Request elevator at floor | 3 |||||
|
|
108
109
|
| Check that | elevator doors are closed | within | 20 seconds ||
|
|
109
110
|
| Check that | current elevator floor | equals | 3 | within | 1 minute |
|
|
111
|
+
| Check that | elevator doors are opened | within | same timespan ||
|
|
112
|
+
|
|
113
|
+
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.
|
|
110
114
|
|
|
111
115
|
### Hybrid manual testing
|
|
112
116
|
|
|
113
|
-
To manually interact with your automated test run during testing or test case development,
|
|
117
|
+
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.
|
|
114
118
|
|
|
115
119
|
***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.
|
|
116
120
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# RobotNL - the oneliner
|
|
2
|
+
RobotNL is a proving ground to boost Robot framework closer to Natural Language.
|
|
3
3
|
|
|
4
4
|
## Introduction
|
|
5
5
|
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.
|
|
6
6
|
|
|
7
|
-
This
|
|
7
|
+
This release introduces ``inline keywords`` and a set of ``Check that`` keywords that help to vastly reduce the amount of ``${...}`` cluttering in your test cases. Combining these concepts helps to write a cleaner [Domain Specific Language](https://en.wikipedia.org/wiki/Domain-specific_language) around your [domain vocabulary](https://en.wikipedia.org/wiki/Jargon).
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
The recommended installation method is using [pip](http://pip-installer.org)
|
|
@@ -55,17 +55,20 @@ Using *Check that* keywords offers a large reduction in the need for variables i
|
|
|
55
55
|
|
|
56
56
|
### Time constraints
|
|
57
57
|
|
|
58
|
-
*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
|
|
58
|
+
*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.
|
|
59
59
|
|
|
60
60
|
|**Example using time constaints**||||||
|
|
61
61
|
|---|---|---|---|---|---|
|
|
62
62
|
| Request elevator at floor | 3 |||||
|
|
63
63
|
| Check that | elevator doors are closed | within | 20 seconds ||
|
|
64
64
|
| Check that | current elevator floor | equals | 3 | within | 1 minute |
|
|
65
|
+
| Check that | elevator doors are opened | within | same timespan ||
|
|
66
|
+
|
|
67
|
+
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.
|
|
65
68
|
|
|
66
69
|
### Hybrid manual testing
|
|
67
70
|
|
|
68
|
-
To manually interact with your automated test run during testing or test case development,
|
|
71
|
+
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.
|
|
69
72
|
|
|
70
73
|
***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.
|
|
71
74
|
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "robotframework-nl"
|
|
10
|
-
version = "3.
|
|
10
|
+
version = "3.2.0"
|
|
11
11
|
description = "robotnl is a proving ground to boost Robot framework closer to Natural Language."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [{ name = "Johan Foederer", email = "github@famfoe.nl" }]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: robotframework-nl
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: robotnl is a proving ground to boost Robot framework closer to Natural Language.
|
|
5
5
|
Author-email: Johan Foederer <github@famfoe.nl>
|
|
6
6
|
License: BSD 3-Clause License
|
|
@@ -42,14 +42,15 @@ Requires-Python: >=3.8
|
|
|
42
42
|
Description-Content-Type: text/markdown
|
|
43
43
|
License-File: LICENSE
|
|
44
44
|
Requires-Dist: robotframework>=7.0
|
|
45
|
+
Dynamic: license-file
|
|
45
46
|
|
|
46
|
-
#
|
|
47
|
-
|
|
47
|
+
# RobotNL - the oneliner
|
|
48
|
+
RobotNL is a proving ground to boost Robot framework closer to Natural Language.
|
|
48
49
|
|
|
49
50
|
## Introduction
|
|
50
51
|
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.
|
|
51
52
|
|
|
52
|
-
This
|
|
53
|
+
This release introduces ``inline keywords`` and a set of ``Check that`` keywords that help to vastly reduce the amount of ``${...}`` cluttering in your test cases. Combining these concepts helps to write a cleaner [Domain Specific Language](https://en.wikipedia.org/wiki/Domain-specific_language) around your [domain vocabulary](https://en.wikipedia.org/wiki/Jargon).
|
|
53
54
|
|
|
54
55
|
## Installation
|
|
55
56
|
The recommended installation method is using [pip](http://pip-installer.org)
|
|
@@ -100,17 +101,20 @@ Using *Check that* keywords offers a large reduction in the need for variables i
|
|
|
100
101
|
|
|
101
102
|
### Time constraints
|
|
102
103
|
|
|
103
|
-
*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
|
|
104
|
+
*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.
|
|
104
105
|
|
|
105
106
|
|**Example using time constaints**||||||
|
|
106
107
|
|---|---|---|---|---|---|
|
|
107
108
|
| Request elevator at floor | 3 |||||
|
|
108
109
|
| Check that | elevator doors are closed | within | 20 seconds ||
|
|
109
110
|
| Check that | current elevator floor | equals | 3 | within | 1 minute |
|
|
111
|
+
| Check that | elevator doors are opened | within | same timespan ||
|
|
112
|
+
|
|
113
|
+
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.
|
|
110
114
|
|
|
111
115
|
### Hybrid manual testing
|
|
112
116
|
|
|
113
|
-
To manually interact with your automated test run during testing or test case development,
|
|
117
|
+
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.
|
|
114
118
|
|
|
115
119
|
***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.
|
|
116
120
|
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2021, J. Foederer
|
|
6
|
+
# All rights reserved.
|
|
7
|
+
#
|
|
8
|
+
# Redistribution and use in source and binary forms, with or without
|
|
9
|
+
# modification, are permitted provided that the following conditions are met:
|
|
10
|
+
#
|
|
11
|
+
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
12
|
+
# list of conditions and the following disclaimer.
|
|
13
|
+
#
|
|
14
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
15
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
16
|
+
# and/or other materials provided with the distribution.
|
|
17
|
+
#
|
|
18
|
+
# 3. Neither the name of the copyright holder nor the names of its
|
|
19
|
+
# contributors may be used to endorse or promote products derived from
|
|
20
|
+
# this software without specific prior written permission.
|
|
21
|
+
#
|
|
22
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
23
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
24
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
25
|
+
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
26
|
+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
27
|
+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
28
|
+
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
29
|
+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
30
|
+
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
31
|
+
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
32
|
+
|
|
33
|
+
from robot.api import TypeInfo
|
|
34
|
+
from robot.libraries.BuiltIn import BuiltIn
|
|
35
|
+
from robot.running.arguments import TypeConverter
|
|
36
|
+
from robot.utils import is_list_like
|
|
37
|
+
|
|
38
|
+
from .inline_keywords import keyword
|
|
39
|
+
|
|
40
|
+
class CheckOperator:
|
|
41
|
+
"""
|
|
42
|
+
This class defines a set of commonly used operators for use by 'Check that'
|
|
43
|
+
and other check keywords
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
################################################################################################
|
|
47
|
+
# Generic operators that can work on basically any object type
|
|
48
|
+
def equals(self, lValue, rValue):
|
|
49
|
+
"""Checks whether the left and right side are equal to each other [`=`]
|
|
50
|
+
|
|
51
|
+
Applies Robot type conversions when executing the check.
|
|
52
|
+
Examples:
|
|
53
|
+
| `Check that` | 7 | `=` | 7 |
|
|
54
|
+
| `Check that` | ${7} | `=` | 7.0 |
|
|
55
|
+
| `Check that` | _Two times_ | 6 | `equals` | 12 |
|
|
56
|
+
| `Check that` | text | `equals` | TeXT |
|
|
57
|
+
"""
|
|
58
|
+
return OperatorProxy("==").basicOperator(lValue, rValue)
|
|
59
|
+
|
|
60
|
+
def is_less_than(self, lValue, rValue):
|
|
61
|
+
"""Checks whether the left side `is less than` or smaller than the right side [`<`]
|
|
62
|
+
|
|
63
|
+
Applies Robot type conversions when executing the check.
|
|
64
|
+
Examples:
|
|
65
|
+
| `Check that` | 2 | `<` | 4 |
|
|
66
|
+
| `Check that` | 2 | `is less than` | 4 |
|
|
67
|
+
"""
|
|
68
|
+
return OperatorProxy("<").basicOperator(lValue, rValue)
|
|
69
|
+
|
|
70
|
+
def is_greater_than(self, lValue, rValue):
|
|
71
|
+
"""Checks whether the left side `is greater than` or larger than the right side [`>`]
|
|
72
|
+
|
|
73
|
+
Applies Robot type conversions when executing the check.
|
|
74
|
+
Examples:
|
|
75
|
+
| `Check that` | 4 | `>` | 2 |
|
|
76
|
+
| `Check that` | 4 | `is greater than` | 2 |
|
|
77
|
+
"""
|
|
78
|
+
return OperatorProxy(">").basicOperator(lValue, rValue)
|
|
79
|
+
|
|
80
|
+
def is_less_than_or_equal_to(self, lValue, rValue):
|
|
81
|
+
"""Checks whether the left side `is less than or equal to` the right side [`≤`]
|
|
82
|
+
|
|
83
|
+
Applies Robot type conversions when executing the check.
|
|
84
|
+
Examples:
|
|
85
|
+
| `Check that` | 2 | `≤` | 2 |
|
|
86
|
+
| `Check that` | 2 | `is less than or equal to` | 4 |
|
|
87
|
+
"""
|
|
88
|
+
return OperatorProxy("<=").basicOperator(lValue, rValue)
|
|
89
|
+
|
|
90
|
+
def is_greater_than_or_equal_to(self, lValue, rValue):
|
|
91
|
+
"""Checks whether the left side `is greater than or equal to` the right side [`≥`]
|
|
92
|
+
|
|
93
|
+
Applies Robot type conversions when executing the check.
|
|
94
|
+
Examples:
|
|
95
|
+
| `Check that` | 4 | `≥` | 4 |
|
|
96
|
+
| `Check that` | 4 | `is greater than or equal to` | 2 |
|
|
97
|
+
"""
|
|
98
|
+
return OperatorProxy(">=").basicOperator(lValue, rValue)
|
|
99
|
+
|
|
100
|
+
def does_not_equal(self, lValue, rValue):
|
|
101
|
+
"""Checks whether the left side `does not equal`, i.e. is different from the right side [`≠`]
|
|
102
|
+
|
|
103
|
+
Applies Robot type conversions when executing the check.
|
|
104
|
+
Examples:
|
|
105
|
+
| `Check that` | 7 | `≠` | 13 |
|
|
106
|
+
| `Check that` | 7 | `≠` | 7.01 |
|
|
107
|
+
| `Check that` | _Two times_ | 6 | `does not equal` | 13 |
|
|
108
|
+
| `Check that` | random text | `does not equal` | my text |
|
|
109
|
+
"""
|
|
110
|
+
return OperatorProxy("!=").basicOperator(lValue, rValue)
|
|
111
|
+
|
|
112
|
+
################################################################################################
|
|
113
|
+
# Operators that work on text items str() or Unicode()
|
|
114
|
+
def contains_text(self, baseString, subString):
|
|
115
|
+
"""Performs a case insensitive check whether the right side is a substring of the left side
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
| `Check that` | the time is now | `contains text` | me |
|
|
119
|
+
| `Check that` | the time is now | `contains text` | ME |
|
|
120
|
+
| `Check That` | Robotstraße | `contains text` | Strasse |
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
return self.contains_exact_text(baseString.casefold(), subString.casefold())
|
|
124
|
+
except:
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
def contains_exact_text(self, baseString, subString):
|
|
128
|
+
"""Performs a case sensitive check whether the right side is a substring of the left side
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
| `Check that` | the time is now | `contains text` | me |
|
|
132
|
+
| `Check That` | Robotstraße | `contains text` | straße |
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
return subString in baseString
|
|
136
|
+
except:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def matches_without_case_to(self, leftText, rightText):
|
|
140
|
+
"""Performs a case insensitive check whether the left and right side texts are equal
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
| `Check that` | the time is now | `matches without case to` | the time is now |
|
|
144
|
+
| `Check that` | the time is now | `matches without case to` | THE TIME IS NOW |
|
|
145
|
+
| `Check That` | Robotstraße | `matches without case to` | ROBOTstrasse |
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
return self.matches_with_case_to(leftText.casefold(), rightText.casefold())
|
|
149
|
+
except:
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
def matches_with_case_to(self, leftText, rightText):
|
|
153
|
+
"""Performs a case sensitive check whether the left and right side texts are equal
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
| `Check that` | the time is now | `matches with case to` | the time is now |
|
|
157
|
+
| `Check That` | Robotstraße | `matches with case to` | Robotstraße |
|
|
158
|
+
"""
|
|
159
|
+
return leftText == rightText
|
|
160
|
+
|
|
161
|
+
def does_not_contain_text(self, baseString, subString):
|
|
162
|
+
"""Performs a case insensitive check whether the right side is not a substring of the left side
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
| `Check that` | random text | `does not contain text` | my text |
|
|
166
|
+
"""
|
|
167
|
+
return not self.contains_text(baseString, subString)
|
|
168
|
+
|
|
169
|
+
def does_not_contain_exact_text(self, baseString, subString):
|
|
170
|
+
"""Performs a case sensitive check whether the right side is not a substring of the left side
|
|
171
|
+
|
|
172
|
+
Examples:
|
|
173
|
+
| `Check that` | random text | `does not contain exact text` | my text |
|
|
174
|
+
| `Check that` | random text | `does not contain exact text` | RANDOM text |
|
|
175
|
+
"""
|
|
176
|
+
return not self.contains_exact_text(baseString, subString)
|
|
177
|
+
|
|
178
|
+
def does_not_match_without_case_to(self, leftText, rightText):
|
|
179
|
+
"""Performs a case insensitive check whether the left and right side texts are different
|
|
180
|
+
|
|
181
|
+
Examples:
|
|
182
|
+
| `Check that` | random text | `does not match without case to` | my text |
|
|
183
|
+
"""
|
|
184
|
+
return not self.matches_without_case_to(leftText, rightText)
|
|
185
|
+
|
|
186
|
+
def does_not_match_with_case_to(self, leftText, rightText):
|
|
187
|
+
"""Performs a case sensitive check whether the left and right side texts are different
|
|
188
|
+
|
|
189
|
+
Examples:
|
|
190
|
+
| `Check that` | random text | `does not match with case to` | my text |
|
|
191
|
+
| `Check that` | random text | `does not match with case to` | RANDOM text |
|
|
192
|
+
"""
|
|
193
|
+
return not self.matches_with_case_to(leftText, rightText)
|
|
194
|
+
|
|
195
|
+
################################################################################################
|
|
196
|
+
# Operators that work on lists or other sequences
|
|
197
|
+
def is_empty(self, sequence):
|
|
198
|
+
"""Checks whether the sequence on the left does not contain any items.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
| Take a new suitcase |
|
|
202
|
+
| `Check that` | suitcase | `is empty` |
|
|
203
|
+
_Assumes a 'box' type to be defined with associated action and observation keywords._
|
|
204
|
+
"""
|
|
205
|
+
return len(sequence) == 0
|
|
206
|
+
|
|
207
|
+
@keyword("contains ${n} items")
|
|
208
|
+
def contains_n_items(self, n:int, sequence):
|
|
209
|
+
"""Checks whether the sequence on the left contains ${n} items. Uses python's len-operator
|
|
210
|
+
to count the number of items.
|
|
211
|
+
|
|
212
|
+
Example:
|
|
213
|
+
| `Check precondition` | suitcase | `is empty` |
|
|
214
|
+
| Put toothbrush into suitcase |
|
|
215
|
+
| `Check that` | suitcase | `contains 1 item` |
|
|
216
|
+
| Put t-shirt into suitcase |
|
|
217
|
+
| `Check that` | suitcase | `contains 2 items` |
|
|
218
|
+
_Assumes a 'suitcase' type to be defined with associated action and observation keywords._
|
|
219
|
+
"""
|
|
220
|
+
count = len(sequence)
|
|
221
|
+
BuiltIn().log(f"Counted {count} items")
|
|
222
|
+
return count == n
|
|
223
|
+
|
|
224
|
+
def contains_1_item(self, sequence):
|
|
225
|
+
return self.contains_n_items(1, sequence)
|
|
226
|
+
contains_1_item.__doc__ = contains_n_items.__doc__
|
|
227
|
+
|
|
228
|
+
def contains(self, sequence, part):
|
|
229
|
+
"""Checks whether part is present in sequence. Uses Python's primitive in-operator.
|
|
230
|
+
|
|
231
|
+
Example:
|
|
232
|
+
| Put toothbrush into suitcase |
|
|
233
|
+
| Put t-shirt into suitcase |
|
|
234
|
+
| `Check that` | suitcase | `contains` | toothbrush |
|
|
235
|
+
| `Check that` | suitcase | `contains` | t-shirt |
|
|
236
|
+
_Assumes a 'suitcase' type to be defined with associated action and observation keywords._
|
|
237
|
+
"""
|
|
238
|
+
return part in sequence
|
|
239
|
+
|
|
240
|
+
def does_not_contain(self, sequence, part):
|
|
241
|
+
"""Checks whether part is present in sequence. Uses Python's primitive 'not in' operator.
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
| `Check precondition` | suitcase | `is empty` |
|
|
245
|
+
| Put toothbrush into suitcase |
|
|
246
|
+
| `Check that` | suitcase | `does not contain` | t-shirt |
|
|
247
|
+
_Assumes a 'suitcase' type to be defined with associated action and observation keywords._
|
|
248
|
+
"""
|
|
249
|
+
return part not in sequence
|
|
250
|
+
|
|
251
|
+
def contains_item(self, sequence, part):
|
|
252
|
+
"""Checks whether the right side item(s) is/are part of the sequence on the left side.
|
|
253
|
+
|
|
254
|
+
`Contains item` and the plural `Contains items` are aliases. The difference with `Contains`
|
|
255
|
+
is that these iterate over the sequence and applies *automatic Robot type conversion*
|
|
256
|
+
between elements when applicable. Duplicate items from the right side can match a single
|
|
257
|
+
item from the left side.
|
|
258
|
+
|
|
259
|
+
Example:
|
|
260
|
+
| Put toothbrush into suitcase |
|
|
261
|
+
| Put t-shirt into suitcase |
|
|
262
|
+
| `Check that` | suitcase | `contains item` | toothbrush |
|
|
263
|
+
| `Check that` | suitcase | `contains items` | t-shirt | toothbrush |
|
|
264
|
+
| `Check that` | suitcase's lock code | `contains items` | 7 | ${2} |
|
|
265
|
+
_Assumes a 'suitcase' type to be defined with associated action and observation keywords._
|
|
266
|
+
"""
|
|
267
|
+
if not is_list_like(part):
|
|
268
|
+
part = [part]
|
|
269
|
+
for elem in part:
|
|
270
|
+
BuiltIn().log(f"Processing '{elem}' from right side")
|
|
271
|
+
for item in sequence:
|
|
272
|
+
if self.equals(elem, item):
|
|
273
|
+
BuiltIn().log("Matched")
|
|
274
|
+
break
|
|
275
|
+
BuiltIn().log("No match")
|
|
276
|
+
else:
|
|
277
|
+
BuiltIn().log(f"{elem} not present in left side list")
|
|
278
|
+
return False
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
# Alias for plural form
|
|
282
|
+
contains_items = contains_item
|
|
283
|
+
|
|
284
|
+
def contains_exactly_the_items_from(self, sequence, sequence_right):
|
|
285
|
+
"""
|
|
286
|
+
Checks whether the sequence on the right side contains all items of the left side and vice versa.
|
|
287
|
+
Iterates over sequence on the left and matches each element with a single element on the right.
|
|
288
|
+
The items can be in any order. Automatic Robot type conversion is applied when applicable.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
| `Check precondition` | suitcase | `is empty` |
|
|
292
|
+
| Put toothbrush into suitcase |
|
|
293
|
+
| Put t-shirt into suitcase |
|
|
294
|
+
| `Check that` | suitcase | `contains exactly the items from` | toothbrush | t-shirt |
|
|
295
|
+
| `Check that` | suitcase | `contains exactly the items from` | t-shirt | toothbrush |
|
|
296
|
+
| `Check that` | my packing list | `contains exactly the items from` | toothbrush | t-shirt |
|
|
297
|
+
| `Check that` | suitcase | `contains exactly the items from` | my packing list |
|
|
298
|
+
_Assumes a 'suitcase' type to be defined with associated action and observation keywords._
|
|
299
|
+
"""
|
|
300
|
+
if isinstance(sequence_right, str):
|
|
301
|
+
sequence_right = [sequence_right]
|
|
302
|
+
sequence_right = [*sequence_right]
|
|
303
|
+
for item in sequence:
|
|
304
|
+
BuiltIn().log(f"Processing '{item}' from left side list")
|
|
305
|
+
for i in range(len(sequence_right)):
|
|
306
|
+
if self.equals(item, sequence_right[i]):
|
|
307
|
+
sequence_right.pop(i)
|
|
308
|
+
break
|
|
309
|
+
else:
|
|
310
|
+
BuiltIn().log(f"Item '{item}' from left side is not found in the list on the right side")
|
|
311
|
+
return False
|
|
312
|
+
if len(sequence_right) > 0:
|
|
313
|
+
BuiltIn().log(f"Not all items from right side list are present: {sequence_right}")
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
return True
|
|
317
|
+
|
|
318
|
+
def does_not_contain_item(self, sequence, part):
|
|
319
|
+
"""
|
|
320
|
+
Checks whether the right side item is not part of the sequence on the left side.
|
|
321
|
+
Iterates over the sequence and applies automatic Robot type conversion where applicable.
|
|
322
|
+
|
|
323
|
+
No plural variant is available for this keyword due to ambiguity with List-like values. When
|
|
324
|
+
writing "[2, 4, 6] does not contain items [4, 5, 6]", one could expect a pass, because the
|
|
325
|
+
set [4, 5, 6] is not contained, but also a fail because item 4 *is* part of the left side set.
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
| `Check precondition` | suitcase | `is empty` |
|
|
329
|
+
| Put toothbrush into suitcase |
|
|
330
|
+
| `Check that` | suitcase | `does not contain item` | t-shirt |
|
|
331
|
+
_Assumes a 'suitcase' type to be defined with associated action and observation keywords._
|
|
332
|
+
"""
|
|
333
|
+
if is_list_like(part):
|
|
334
|
+
raise TypeError("List-like items not accepted as right side value")
|
|
335
|
+
return not self.contains_item(sequence, part)
|
|
336
|
+
|
|
337
|
+
# Add operator keywords that do not comply to Python's identifier syntax
|
|
338
|
+
setattr(CheckOperator, "=", CheckOperator.equals)
|
|
339
|
+
setattr(CheckOperator, "<", CheckOperator.is_less_than)
|
|
340
|
+
setattr(CheckOperator, ">", CheckOperator.is_greater_than)
|
|
341
|
+
setattr(CheckOperator, "≤", CheckOperator.is_less_than_or_equal_to)
|
|
342
|
+
setattr(CheckOperator, "≥", CheckOperator.is_greater_than_or_equal_to)
|
|
343
|
+
setattr(CheckOperator, "≠", CheckOperator.does_not_equal)
|
|
344
|
+
|
|
345
|
+
class OperatorProxy:
|
|
346
|
+
"""
|
|
347
|
+
Proxy class for mapping generic Robot comparison keywords to Python operators
|
|
348
|
+
"""
|
|
349
|
+
def __init__(self, s_operator):
|
|
350
|
+
self.__s_Operator = s_operator
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def __typeCastRobotStringValue(leadingValue, otherValue, name):
|
|
354
|
+
"""
|
|
355
|
+
When Robot files are parsed, arguments are always passed as string even when comparing
|
|
356
|
+
numbers or other objects. When a string argument is detected the other argument is
|
|
357
|
+
deemed leading. This function converts the other argument to the leading value's type
|
|
358
|
+
when possible. Otherwise the value is kept unchanged, inevitably leading to a mismatch
|
|
359
|
+
in comparison.
|
|
360
|
+
|
|
361
|
+
Precondition: otherValue is of types str and supports .casefold()
|
|
362
|
+
returns type casted otherValue
|
|
363
|
+
"""
|
|
364
|
+
CastedOther = otherValue # By default leave untouched
|
|
365
|
+
converter = TypeConverter.converter_for(TypeInfo.from_type(type(leadingValue)))
|
|
366
|
+
if converter:
|
|
367
|
+
try:
|
|
368
|
+
CastedOther = converter.convert(otherValue, name)
|
|
369
|
+
except ValueError as err:
|
|
370
|
+
BuiltIn().log(err, level='DEBUG')
|
|
371
|
+
BuiltIn().log(f"Comparing as {converter.type_name} values")
|
|
372
|
+
|
|
373
|
+
if isinstance(CastedOther, str):
|
|
374
|
+
# By default compare as case insensitive Unicode. Note that it already was a string.
|
|
375
|
+
BuiltIn().log(f"Interpreting {name} '{otherValue}' as string (case insensitive)")
|
|
376
|
+
CastedOther = str(otherValue).casefold()
|
|
377
|
+
|
|
378
|
+
return CastedOther
|
|
379
|
+
|
|
380
|
+
def basicOperator(self, lvalue, rvalue):
|
|
381
|
+
# Local argument copies for assignment
|
|
382
|
+
lValue = lvalue
|
|
383
|
+
rValue = rvalue
|
|
384
|
+
|
|
385
|
+
if type(lvalue) is str:
|
|
386
|
+
lValue = OperatorProxy.__typeCastRobotStringValue(rvalue, lvalue, "left operand")
|
|
387
|
+
|
|
388
|
+
if type(rvalue) is str:
|
|
389
|
+
rValue = OperatorProxy.__typeCastRobotStringValue(lvalue, rvalue, "right operand")
|
|
390
|
+
|
|
391
|
+
if lValue is lvalue and rValue is rvalue:
|
|
392
|
+
BuiltIn().log("Comparing values as is")
|
|
393
|
+
|
|
394
|
+
return eval(f"lValue {self.__s_Operator} rValue")
|
|
@@ -38,6 +38,7 @@ except ImportError:
|
|
|
38
38
|
tkinter = False
|
|
39
39
|
|
|
40
40
|
from robot.libraries.BuiltIn import BuiltIn
|
|
41
|
+
from robot.running import RUN_KW_REGISTER
|
|
41
42
|
from robot.utils import timestr_to_secs, secs_to_timestr
|
|
42
43
|
from .inline_keywords import is_keyword
|
|
43
44
|
|
|
@@ -47,7 +48,25 @@ class CheckFailed(RuntimeError):
|
|
|
47
48
|
class RobotChecks:
|
|
48
49
|
ROBOT_LIBRARY_SCOPE = "GLOBAL"
|
|
49
50
|
def __init__(self):
|
|
51
|
+
self.ROBOT_LIBRARY_LISTENER = self
|
|
50
52
|
self.__gui = None
|
|
53
|
+
self.scope_depth = 0
|
|
54
|
+
self.nested_timespans = {}
|
|
55
|
+
|
|
56
|
+
def _start_suite(self, data, result):
|
|
57
|
+
self.scope_depth = 0
|
|
58
|
+
self.nested_timespans = {}
|
|
59
|
+
_end_suite = _start_suite
|
|
60
|
+
|
|
61
|
+
def _start_keyword(self, data, result):
|
|
62
|
+
self.scope_depth += 1
|
|
63
|
+
|
|
64
|
+
def _end_keyword(self, data, result):
|
|
65
|
+
if self.scope_depth + 1 in self.nested_timespans:
|
|
66
|
+
# use depth+1 because 'check that' itself is also a keyword. The +1
|
|
67
|
+
# keeps all check thats on the same level grouped.
|
|
68
|
+
self.nested_timespans.pop(self.scope_depth + 1)
|
|
69
|
+
self.scope_depth -= 1
|
|
51
70
|
|
|
52
71
|
@property
|
|
53
72
|
def _gui(self):
|
|
@@ -73,10 +92,11 @@ class RobotChecks:
|
|
|
73
92
|
point where it was able to check the requirement it was testing for.
|
|
74
93
|
"""
|
|
75
94
|
try:
|
|
76
|
-
return
|
|
95
|
+
return self.__execute_check("Precondition", *args)
|
|
77
96
|
except CheckFailed as failure:
|
|
78
97
|
failure.ROBOT_CONTINUE_ON_FAILURE = False
|
|
79
98
|
raise failure
|
|
99
|
+
RUN_KW_REGISTER.register_run_keyword('robotnl', check_precondition.__name__, args_to_process=0, deprecation_warning=False)
|
|
80
100
|
|
|
81
101
|
def check_postcondition(self, *args):
|
|
82
102
|
"""
|
|
@@ -89,10 +109,11 @@ class RobotChecks:
|
|
|
89
109
|
the cause of failure.
|
|
90
110
|
"""
|
|
91
111
|
try:
|
|
92
|
-
return
|
|
112
|
+
return self.__execute_check("Postcondition", *args)
|
|
93
113
|
except CheckFailed as failure:
|
|
94
114
|
failure.ROBOT_CONTINUE_ON_FAILURE = False
|
|
95
115
|
raise failure
|
|
116
|
+
RUN_KW_REGISTER.register_run_keyword('robotnl', check_postcondition.__name__, args_to_process=0, deprecation_warning=False)
|
|
96
117
|
|
|
97
118
|
def check_that(self, *args):
|
|
98
119
|
"""
|
|
@@ -105,37 +126,51 @@ class RobotChecks:
|
|
|
105
126
|
Check that has two basic forms.
|
|
106
127
|
- A single keyword (with its arguments) can be evaluated to a truth value
|
|
107
128
|
- Two values or keywords (with their arguments) can be evaluated using an operator. It will
|
|
108
|
-
then have the form Check that ``<keyword or value>`` ``<operator>`` ``<keyword or value>``.
|
|
129
|
+
then have the form: Check that ``<keyword or value>`` ``<operator>`` ``<keyword or value>``.
|
|
109
130
|
|
|
110
|
-
Operator can be any Robot keyword taking
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
Operator can be any Robot keyword taking at least one argument as input. The first argument is
|
|
132
|
+
placed on the left side of the operator, the left operand. Any following arguments are placed
|
|
133
|
+
to the right of the operator and form the right operands. A keyword with its argument values is
|
|
134
|
+
treated as a single operand. A number of predefined operators on numeric, string and list types
|
|
135
|
+
are included in this library.
|
|
113
136
|
|
|
114
137
|
Examples:
|
|
115
138
|
| `Check that` | 3 | `=` | 3 |
|
|
116
|
-
| `Check that` |
|
|
117
|
-
| `Check that` |
|
|
118
|
-
| `Check that` |
|
|
139
|
+
| `Check that` | _Two times_ | 6 | `equals` | 12 |
|
|
140
|
+
| `Check that` | _Two times_ | 5 | `≠` | _Two times_ | 7 |
|
|
141
|
+
| `Check that` | _the shoe box_ | _contains 2 items_ |
|
|
142
|
+
| `Check that` | _Earth exists_ |
|
|
119
143
|
|
|
120
144
|
'Two times' in these examples is assumed to be defined as a Robot keyword that takes one
|
|
121
|
-
argument and multiplies it by 2. Check that will pass if the evaluated result of
|
|
122
|
-
equals the fixed expected value 12.
|
|
145
|
+
argument and multiplies it by 2. `Check that` will pass if the evaluated result of _Two
|
|
146
|
+
times_ 6 equals the fixed expected value 12.
|
|
123
147
|
|
|
124
148
|
*Adding time constraints*:\n
|
|
125
149
|
Any check can be extended with an additional timing constraint by adding ``within``
|
|
126
150
|
This will cause the condition to be reevaluated until it becomes true, or until
|
|
127
151
|
the specified time has passed. In the latter case the test case will fail.
|
|
128
152
|
|
|
153
|
+
Multiple checks can be linked to the same time constraint by using ``same timespan``
|
|
154
|
+
as the time constraint value. Suppose the previous time constraint was 5 seconds
|
|
155
|
+
and that check took 2 seconds to complete. If the next check uses
|
|
156
|
+
``within same timespan``, then this check has the remaining 3 seconds to
|
|
157
|
+
complete.
|
|
158
|
+
|
|
129
159
|
Example with time constraint:
|
|
130
|
-
| `Check that` |
|
|
160
|
+
| `Check that` | _condition is true_ | within | 1 minute 30 seconds |
|
|
131
161
|
|
|
132
162
|
Elevator example:
|
|
133
|
-
|
|
|
134
|
-
|
|
|
135
|
-
| `Check that` |
|
|
136
|
-
| `Check that` |
|
|
163
|
+
| _Request elevator at floor_ | 3 |
|
|
164
|
+
| `Check that` | _elevator doors are closed_ | within | 20 seconds |
|
|
165
|
+
| `Check that` | _elevator floor_ | `equals` | 3 | within | 1 minute |
|
|
166
|
+
| `Check that` | _elevator doors are opened_ | within | same timespan |
|
|
167
|
+
|
|
168
|
+
In the elevator example, the doors should be closed within 20 seconds after requesting a
|
|
169
|
+
floor. Then the elevator can take a maximum of 1 minute to reach the floor and open its
|
|
170
|
+
doors. `Check that` will continue as soon as the condition is detected.
|
|
137
171
|
"""
|
|
138
|
-
return
|
|
172
|
+
return self.__execute_check("Requirement", *args)
|
|
173
|
+
RUN_KW_REGISTER.register_run_keyword('robotnl', check_that.__name__, args_to_process=0, deprecation_warning=False)
|
|
139
174
|
|
|
140
175
|
def check_manual(self, checkRequestText=""):
|
|
141
176
|
"""
|
|
@@ -206,8 +241,7 @@ class RobotChecks:
|
|
|
206
241
|
except Exception as e:
|
|
207
242
|
BuiltIn().log_to_console("Error in interactive keyword '%s'\n\n%s" % (newInput, e))
|
|
208
243
|
|
|
209
|
-
|
|
210
|
-
def __execute_check(checkType, *args):
|
|
244
|
+
def __execute_check(self, checkType, *args):
|
|
211
245
|
"""
|
|
212
246
|
Parse arguments for check keyword to determine its operands, evaluate them and execute the
|
|
213
247
|
check.
|
|
@@ -219,14 +253,35 @@ class RobotChecks:
|
|
|
219
253
|
TimeOutInSeconds = 0
|
|
220
254
|
TimeRemaining = True
|
|
221
255
|
s_TimeConstraint = ""
|
|
256
|
+
StartTime = time.perf_counter()
|
|
222
257
|
if len(Arguments) >= 2 and str(Arguments[-2]).lower() == 'within':
|
|
223
|
-
|
|
224
|
-
TimeOutInSeconds = timestr_to_secs(EvaluatedTimeArg)
|
|
225
|
-
s_TimeConstraint = Arguments[-1]
|
|
258
|
+
timearg = Arguments[-1]
|
|
226
259
|
Arguments = Arguments[:-2]
|
|
260
|
+
running_timespan_start, running_timespan, s_TimeConstraint =self.nested_timespans.get(
|
|
261
|
+
self.scope_depth, (None, None, ''))
|
|
262
|
+
if str(timearg).lower() == 'same timespan':
|
|
263
|
+
if running_timespan is None:
|
|
264
|
+
BuiltIn().fail("Joint timespan expected, but was not set before this check.")
|
|
265
|
+
TimeOutInSeconds = round(running_timespan_start + running_timespan - StartTime, ndigits=3)
|
|
266
|
+
|
|
267
|
+
remaining = self._human_time(0 if TimeOutInSeconds < 0 else TimeOutInSeconds)
|
|
268
|
+
BuiltIn().log(f"Reusing joint timespan: {s_TimeConstraint}\n"
|
|
269
|
+
f"Must complete within the remaining {remaining}")
|
|
270
|
+
else:
|
|
271
|
+
try:
|
|
272
|
+
EvaluatedTimeArg, s_TimeConstraint = RobotChecks.__evaluateOperand([timearg])
|
|
273
|
+
TimeOutInSeconds = timestr_to_secs(EvaluatedTimeArg)
|
|
274
|
+
except:
|
|
275
|
+
# End any running timespan on error
|
|
276
|
+
if self.scope_depth in self.nested_timespans:
|
|
277
|
+
self.nested_timespans.pop(self.scope_depth)
|
|
278
|
+
raise
|
|
279
|
+
if is_keyword(timearg):
|
|
280
|
+
s_TimeConstraint = f"{timearg} [{secs_to_timestr(TimeOutInSeconds)}]"
|
|
281
|
+
self.nested_timespans[self.scope_depth] = (StartTime, TimeOutInSeconds, s_TimeConstraint)
|
|
227
282
|
|
|
228
283
|
if not len(Arguments):
|
|
229
|
-
BuiltIn().fail("
|
|
284
|
+
BuiltIn().fail(f"{checkType} check failed. There was nothing to check.")
|
|
230
285
|
|
|
231
286
|
############################################################################################
|
|
232
287
|
# Build expression
|
|
@@ -259,8 +314,6 @@ class RobotChecks:
|
|
|
259
314
|
# Evaluate expression
|
|
260
315
|
EvaluatedResult = None
|
|
261
316
|
|
|
262
|
-
StartTime = time.perf_counter()
|
|
263
|
-
TimeLeft = TimeOutInSeconds
|
|
264
317
|
PollMax = 20 # After 20s people start wondering: "Is it still going?" Time for an update.
|
|
265
318
|
PollMin = min(PollMax/8, TimeOutInSeconds*3/100) # Shortest delay is 3% of the target time.
|
|
266
319
|
PollDelay = PollMin # Initial poll delay will be 2x PollMin
|
|
@@ -308,8 +361,14 @@ class RobotChecks:
|
|
|
308
361
|
ReportString = f"{checkType} check on '{s_LeftOperand} {OperatorKeyword} {s_RightOperand}'"
|
|
309
362
|
|
|
310
363
|
if s_TimeConstraint:
|
|
311
|
-
|
|
364
|
+
running_start, timespan, timestr = self.nested_timespans.get(self.scope_depth, (None, None, ''))
|
|
365
|
+
if TimeRemaining and running_start != StartTime:
|
|
366
|
+
elapsed = time.perf_counter() - running_start
|
|
367
|
+
BuiltIn().log(f"Check completed {self._human_time(elapsed)} into the alotted timespan ({timestr})")
|
|
368
|
+
ReportString += f" within {timestr}"
|
|
312
369
|
if not TimeRemaining and EvaluatedResult == "passed":
|
|
370
|
+
overshoot = time.perf_counter() - (running_start + timespan)
|
|
371
|
+
BuiltIn().log(f"Check completed {self._human_time(overshoot)} too late")
|
|
313
372
|
ReportString += " (too late)"
|
|
314
373
|
raise CheckFailed(ReportString)
|
|
315
374
|
|
|
@@ -352,3 +411,10 @@ class RobotChecks:
|
|
|
352
411
|
s_Operand += f" [{s_Value}]"
|
|
353
412
|
|
|
354
413
|
return Value, s_Operand
|
|
414
|
+
|
|
415
|
+
@staticmethod
|
|
416
|
+
def _human_time(time_in_seconds: float):
|
|
417
|
+
"""return a human readable string for a time in seconds"""
|
|
418
|
+
SKIP_MILLI = 10 # Do not report milliseconds for timespans above SKIP_MILLI seconds
|
|
419
|
+
rounded_time = int(time_in_seconds) if time_in_seconds > SKIP_MILLI else round(time_in_seconds, ndigits=3)
|
|
420
|
+
return secs_to_timestr(rounded_time)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = '3.2.0'
|
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
# BSD 3-Clause License
|
|
4
|
-
#
|
|
5
|
-
# Copyright (c) 2021, J. Foederer
|
|
6
|
-
# All rights reserved.
|
|
7
|
-
#
|
|
8
|
-
# Redistribution and use in source and binary forms, with or without
|
|
9
|
-
# modification, are permitted provided that the following conditions are met:
|
|
10
|
-
#
|
|
11
|
-
# 1. Redistributions of source code must retain the above copyright notice, this
|
|
12
|
-
# list of conditions and the following disclaimer.
|
|
13
|
-
#
|
|
14
|
-
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
15
|
-
# this list of conditions and the following disclaimer in the documentation
|
|
16
|
-
# and/or other materials provided with the distribution.
|
|
17
|
-
#
|
|
18
|
-
# 3. Neither the name of the copyright holder nor the names of its
|
|
19
|
-
# contributors may be used to endorse or promote products derived from
|
|
20
|
-
# this software without specific prior written permission.
|
|
21
|
-
#
|
|
22
|
-
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
23
|
-
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
24
|
-
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
25
|
-
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
26
|
-
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
27
|
-
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
28
|
-
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
29
|
-
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
30
|
-
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
31
|
-
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
32
|
-
|
|
33
|
-
from robot.api import TypeInfo
|
|
34
|
-
from robot.libraries.BuiltIn import BuiltIn
|
|
35
|
-
from robot.running.arguments import TypeConverter
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class CheckOperator:
|
|
39
|
-
"""
|
|
40
|
-
This class defines a set of commonly used operators for use by 'Check that'
|
|
41
|
-
and other check keywords
|
|
42
|
-
"""
|
|
43
|
-
|
|
44
|
-
################################################################################################
|
|
45
|
-
# Generic operators that can work on basically any object type
|
|
46
|
-
def equals(self, lValue, rValue):
|
|
47
|
-
"""Checks whether the left and right side are equal to each other [=]"""
|
|
48
|
-
return OperatorProxy("==").basicOperator(lValue, rValue)
|
|
49
|
-
|
|
50
|
-
def is_less_than(self, lValue, rValue):
|
|
51
|
-
"""Checks whether the left side is less than or smaller than the right side [<]"""
|
|
52
|
-
return OperatorProxy("<").basicOperator(lValue, rValue)
|
|
53
|
-
|
|
54
|
-
def is_greater_than(self, lValue, rValue):
|
|
55
|
-
"""Checks whether the left side is greater than or larger than the right side [>]"""
|
|
56
|
-
return OperatorProxy(">").basicOperator(lValue, rValue)
|
|
57
|
-
|
|
58
|
-
def is_less_than_or_equal_to(self, lValue, rValue):
|
|
59
|
-
"""Checks whether the left side is less than or equal to the right side [≤]"""
|
|
60
|
-
return OperatorProxy("<=").basicOperator(lValue, rValue)
|
|
61
|
-
|
|
62
|
-
def is_greater_than_or_equal_to(self, lValue, rValue):
|
|
63
|
-
"""Checks whether the left side is greater than or equal to the right side [≥]"""
|
|
64
|
-
return OperatorProxy(">=").basicOperator(lValue, rValue)
|
|
65
|
-
|
|
66
|
-
def does_not_equal(self, lValue, rValue):
|
|
67
|
-
"""Checks whether the left side is different from the right side [≠]"""
|
|
68
|
-
return OperatorProxy("!=").basicOperator(lValue, rValue)
|
|
69
|
-
|
|
70
|
-
################################################################################################
|
|
71
|
-
# Operators that work on text items str() or Unicode()
|
|
72
|
-
def contains_text(self, baseString, subString):
|
|
73
|
-
"""Performs a case insensitive check whether the right side is a substring of the left side"""
|
|
74
|
-
try:
|
|
75
|
-
return self.contains_exact_text(baseString.lower(), subString.lower())
|
|
76
|
-
except:
|
|
77
|
-
return False
|
|
78
|
-
|
|
79
|
-
def contains_exact_text(self, baseString, subString):
|
|
80
|
-
"""Performs a case sensitive check whether the right side is a substring of the left side"""
|
|
81
|
-
try:
|
|
82
|
-
return subString in baseString
|
|
83
|
-
except:
|
|
84
|
-
return False
|
|
85
|
-
|
|
86
|
-
def matches_without_case_to(self, leftText, rightText):
|
|
87
|
-
"""Performs a case insensitive check whether the left and right side texts are equal"""
|
|
88
|
-
try:
|
|
89
|
-
return self.matches_with_case_to(leftText.lower(), rightText.lower())
|
|
90
|
-
except:
|
|
91
|
-
return False
|
|
92
|
-
|
|
93
|
-
def matches_with_case_to(self, leftText, rightText):
|
|
94
|
-
"""Performs a case sensitive check whether the left and right side texts are equal"""
|
|
95
|
-
return leftText == rightText
|
|
96
|
-
|
|
97
|
-
def does_not_contain_text(self, baseString, subString):
|
|
98
|
-
"""Performs a case insensitive check whether the right side is not a substring of the left side"""
|
|
99
|
-
return not self.contains_text(baseString, subString)
|
|
100
|
-
|
|
101
|
-
def does_not_contain_exact_text(self, baseString, subString):
|
|
102
|
-
"""Performs a case sensitive check whether the right side is not a substring of the left side"""
|
|
103
|
-
return not self.contains_exact_text(baseString, subString)
|
|
104
|
-
|
|
105
|
-
def does_not_match_without_case_to(self, leftText, rightText):
|
|
106
|
-
"""Performs a case insensitive check whether the left and right side texts are different"""
|
|
107
|
-
return not self.matches_without_case_to(leftText, rightText)
|
|
108
|
-
|
|
109
|
-
def does_not_match_with_case_to(self, leftText, rightText):
|
|
110
|
-
"""Performs a case sensitive check whether the left and right side texts are different"""
|
|
111
|
-
return not self.matches_with_case_to(leftText, rightText)
|
|
112
|
-
|
|
113
|
-
################################################################################################
|
|
114
|
-
# Operators that work on lists or other sequences
|
|
115
|
-
def is_empty(self, iterable):
|
|
116
|
-
return len(iterable) == 0
|
|
117
|
-
|
|
118
|
-
def contains(self, iterable, part):
|
|
119
|
-
"""Checks whether part is present in iterable. Uses primitive 'in' operator."""
|
|
120
|
-
return part in iterable
|
|
121
|
-
|
|
122
|
-
def does_not_contain(self, iterable, part):
|
|
123
|
-
"""Checks whether part is present in iterable. Uses primitive 'not in' operator."""
|
|
124
|
-
return part not in iterable
|
|
125
|
-
|
|
126
|
-
def contains_item(self, sequence, part):
|
|
127
|
-
"""
|
|
128
|
-
Checks whether the right side item is part of the sequence on the left side.
|
|
129
|
-
Iterates over the sequence to apply automatic Robot type conversion where applicable.
|
|
130
|
-
"""
|
|
131
|
-
for item in sequence:
|
|
132
|
-
BuiltIn().log(f"Processing '{item}' from list")
|
|
133
|
-
if self.equals(part, item):
|
|
134
|
-
BuiltIn().log("Matched")
|
|
135
|
-
return True
|
|
136
|
-
BuiltIn().log("No match")
|
|
137
|
-
return False
|
|
138
|
-
|
|
139
|
-
def contains_exactly_the_items_from(self, sequence, sequence_right):
|
|
140
|
-
"""
|
|
141
|
-
Checks whether the sequence on the right side contains all items of the left side and vice versa.
|
|
142
|
-
Iterates over sequence on the left and matches each element with a single element on the right.
|
|
143
|
-
"""
|
|
144
|
-
if isinstance(sequence_right, str):
|
|
145
|
-
sequence_right = [sequence_right]
|
|
146
|
-
sequence_right = [*sequence_right]
|
|
147
|
-
for item in sequence:
|
|
148
|
-
item_found_in_parts = False
|
|
149
|
-
BuiltIn().log(f"Processing '{item}' from left side list")
|
|
150
|
-
for i in range(len(sequence_right)):
|
|
151
|
-
if self.equals(item, sequence_right[i]):
|
|
152
|
-
sequence_right.pop(i)
|
|
153
|
-
item_found_in_parts = True
|
|
154
|
-
break
|
|
155
|
-
if not item_found_in_parts:
|
|
156
|
-
BuiltIn().log(f"Item '{item}' from left side is not found in the list on the right side")
|
|
157
|
-
return False
|
|
158
|
-
if len(sequence_right) > 0:
|
|
159
|
-
BuiltIn().log(f"Not all items from right side list are present: {sequence_right}")
|
|
160
|
-
return False
|
|
161
|
-
|
|
162
|
-
return True
|
|
163
|
-
|
|
164
|
-
def does_not_contain_item(self, sequence, part):
|
|
165
|
-
"""
|
|
166
|
-
Checks whether the right side item is not part of the sequence on the left side.
|
|
167
|
-
Iterates over the sequence to apply automatic Robot type conversion where applicable.
|
|
168
|
-
"""
|
|
169
|
-
return not self.contains_item(sequence, part)
|
|
170
|
-
|
|
171
|
-
# Add operator keywords that do not comply to Python's identifier syntax
|
|
172
|
-
setattr(CheckOperator, "=", CheckOperator.equals)
|
|
173
|
-
setattr(CheckOperator, "<", CheckOperator.is_less_than)
|
|
174
|
-
setattr(CheckOperator, ">", CheckOperator.is_greater_than)
|
|
175
|
-
setattr(CheckOperator, "≤", CheckOperator.is_less_than_or_equal_to)
|
|
176
|
-
setattr(CheckOperator, "≥", CheckOperator.is_greater_than_or_equal_to)
|
|
177
|
-
setattr(CheckOperator, "≠", CheckOperator.does_not_equal)
|
|
178
|
-
|
|
179
|
-
class OperatorProxy:
|
|
180
|
-
"""
|
|
181
|
-
Proxy class for mapping generic Robot comparison keywords to Python operators
|
|
182
|
-
"""
|
|
183
|
-
def __init__(self, s_operator):
|
|
184
|
-
self.__s_Operator = s_operator
|
|
185
|
-
|
|
186
|
-
@staticmethod
|
|
187
|
-
def __typeCastRobotStringValue(leadingValue, otherValue, name):
|
|
188
|
-
"""
|
|
189
|
-
When Robot files are parsed, arguments are always passed as string even when comparing
|
|
190
|
-
numbers or other objects. When a string argument is detected the other argument is
|
|
191
|
-
deemed leading. This function converts the other argument to the leading value's type
|
|
192
|
-
when possible. Otherwise the value is kept unchanged, inevitably leading to a mismatch
|
|
193
|
-
in comparison.
|
|
194
|
-
|
|
195
|
-
Precondition: otherValue is of types str and supports .casefold()
|
|
196
|
-
returns type casted otherValue
|
|
197
|
-
"""
|
|
198
|
-
CastedOther = otherValue # By default leave untouched
|
|
199
|
-
converter = TypeConverter.converter_for(TypeInfo.from_type(type(leadingValue)))
|
|
200
|
-
if converter:
|
|
201
|
-
try:
|
|
202
|
-
CastedOther = converter.convert(otherValue, name)
|
|
203
|
-
except ValueError as err:
|
|
204
|
-
BuiltIn().log(err, level='DEBUG')
|
|
205
|
-
BuiltIn().log(f"Comparing as {converter.type_name} values")
|
|
206
|
-
|
|
207
|
-
if isinstance(CastedOther, str):
|
|
208
|
-
# By default compare as case insensitive Unicode. Note that it already was a string.
|
|
209
|
-
BuiltIn().log(f"Interpreting {name} '{otherValue}' as string (case insensitive)")
|
|
210
|
-
CastedOther = str(otherValue).casefold()
|
|
211
|
-
|
|
212
|
-
return CastedOther
|
|
213
|
-
|
|
214
|
-
def basicOperator(self, lvalue, rvalue):
|
|
215
|
-
# Local argument copies for assignment
|
|
216
|
-
lValue = lvalue
|
|
217
|
-
rValue = rvalue
|
|
218
|
-
|
|
219
|
-
if type(lvalue) is str:
|
|
220
|
-
lValue = OperatorProxy.__typeCastRobotStringValue(rvalue, lvalue, "left operand")
|
|
221
|
-
|
|
222
|
-
if type(rvalue) is str:
|
|
223
|
-
rValue = OperatorProxy.__typeCastRobotStringValue(lvalue, rvalue, "right operand")
|
|
224
|
-
|
|
225
|
-
if lValue is lvalue and rValue is rvalue:
|
|
226
|
-
BuiltIn().log("Comparing values as is")
|
|
227
|
-
|
|
228
|
-
return eval(f"lValue {self.__s_Operator} rValue")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
VERSION = '3.0.0'
|
|
File without changes
|
|
File without changes
|
{robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotframework_nl.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{robotframework-nl-3.0.0 → robotframework_nl-3.2.0}/robotframework_nl.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|