-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Support custom statuses #3869
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Support custom statuses #3869
Changes from all commits
23af2e9
3240fd8
855b5d8
6aca3e2
b999cdc
e4ae451
dfdbc22
f17e11a
709766b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
*** Settings *** | ||
Suite Setup Run With Custom Statuses | ||
Resource atest_resource.robot | ||
|
||
*** Variables *** | ||
${ADDSTATUS} --AddStatus WIP:PASS:wip:blue --addstatus KNOWN-ISSUE:FAIL:known-*:purple --ADDSTATUS NON-CRITICAL:SKIP:non-criticalNOTwip:pink --log log.html | ||
${ERROR} [ ERROR ] Invalid format for option '--addstatus'. Expected 'NEWSTATUS:OLDSTATUS:TAGPATTERN:COLOR' but got 'invalid'.${USAGE TIP}\n | ||
|
||
*** Test Cases *** | ||
Check statuses | ||
Should Be Equal ${TC1.status} WIP | ||
Log should have correct custom status color WIP blue | ||
Should Be Equal ${TC2.status} KNOWN-ISSUE | ||
Log should have correct custom status color KNOWN-ISSUE purple | ||
Should Be Equal ${TC3.status} NON-CRITICAL | ||
Log should have correct custom status color NON-CRITICAL pink | ||
|
||
Invalid usage | ||
Run Tests Without Processing Output --AddStatus invalid output/custom_statuses.robot | ||
Stderr Should Be Equal To ${ERROR} | ||
|
||
*** Keywords *** | ||
Run With Custom Statuses | ||
Run Tests ${ADDSTATUS} output/custom_statuses.robot | ||
${LOG} = Get File ${OUTDIR}/log.html | ||
Set Suite Variable $LOG | ||
${TC1} = Check Test Case Pass message=Pass | ||
Set Suite Variable $TC1 | ||
${TC2} = Check Test Case Fail message=Fail | ||
Set Suite Variable $TC2 | ||
${TC3} = Check Test Case Skip message=Skip | ||
Set Suite Variable $TC3 | ||
|
||
Log should have correct custom status color | ||
[Arguments] ${status} ${color} | ||
Log ${LOG} | ||
Should Contain ${LOG} "${status}":"${color}" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
*** Test Cases *** | ||
Pass | ||
[Tags] wip | ||
Pass Execution Pass | ||
|
||
Fail | ||
[Tags] known-issue | ||
Fail Fail | ||
|
||
Skip | ||
[Tags] non-critical | ||
Skip Skip |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1322,7 +1322,6 @@ | |
var idCounter = 0; | ||
var _statistics = null; | ||
var LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FAIL', 'SKIP']; | ||
var STATUSES = ['FAIL', 'PASS', 'NOT_RUN', 'SKIP']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to change examples under |
||
var KEYWORDS = ['KEYWORD', 'SETUP', 'TEARDOWN', 'FOR', 'VAR', 'IF', 'ELSE IF', 'ELSE']; | ||
function addElement(elem) { | ||
if (!elem.id) | ||
|
@@ -1350,7 +1349,7 @@ | |
strings.get(element[3]))); | ||
} | ||
function parseStatus(stats) { | ||
return STATUSES[stats[0]]; | ||
return stats[0]; | ||
} | ||
function childCreator(parent, childType) { | ||
return function (elem, strings, index) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -69,7 +69,8 @@ class _BaseSettings(object): | |
'ConsoleColors' : ('consolecolors', 'AUTO'), | ||
'StdOut' : ('stdout', None), | ||
'StdErr' : ('stderr', None), | ||
'XUnitSkipNonCritical' : ('xunitskipnoncritical', False)} | ||
'XUnitSkipNonCritical' : ('xunitskipnoncritical', False), | ||
'AddStatus' : ('addstatus', [])} | ||
_output_opts = ['Output', 'Log', 'Report', 'XUnit', 'DebugFile'] | ||
|
||
def __init__(self, options=None, **extra_options): | ||
|
@@ -137,8 +138,16 @@ def _process_value(self, name, value): | |
self._validate_expandkeywords(value) | ||
if name == 'Extension': | ||
return tuple(ext.lower().lstrip('.') for ext in value.split(':')) | ||
if name == 'AddStatus': | ||
return self._validate_custom_statuses(value) | ||
return value | ||
|
||
def _validate_custom_statuses(self, values): | ||
custom_statuses = [] | ||
for value in values: | ||
custom_statuses.append(self._process_custom_statuses(value)) | ||
return custom_statuses | ||
|
||
def _escape_as_data(self, value): | ||
return value | ||
|
||
|
@@ -273,6 +282,13 @@ def _process_tag_stat_link(self, value): | |
raise DataError("Invalid format for option '--tagstatlink'. " | ||
"Expected 'tag:link:title' but got '%s'." % value) | ||
|
||
def _process_custom_statuses(self, value): | ||
tokens = value.split(':') | ||
if len(tokens) == 4 and tokens[1] in ['PASS', 'FAIL', 'SKIP']: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should support also lower case values. |
||
return tokens | ||
raise DataError("Invalid format for option '--addstatus'. " | ||
"Expected 'NEWSTATUS:OLDSTATUS:TAGPATTERN:COLOR' but got '%s'." % value) | ||
|
||
def _convert_to_positive_integer_or_default(self, name, value): | ||
value = self._convert_to_integer(name, value) | ||
return value if value > 0 else self._get_default_value(name) | ||
|
@@ -385,6 +401,10 @@ def rpa(self): | |
def rpa(self, value): | ||
self['RPA'] = value | ||
|
||
@property | ||
def add_status(self): | ||
return self['AddStatus'] | ||
|
||
|
||
class RobotSettings(_BaseSettings): | ||
_extra_cli_opts = {'Extension' : ('extension', ('robot',)), | ||
|
@@ -583,7 +603,8 @@ def log_config(self): | |
'title': html_escape(self['LogTitle'] or ''), | ||
'reportURL': self._url_from_path(self.log, self.report), | ||
'splitLogBase': os.path.basename(os.path.splitext(self.log)[0]), | ||
'defaultLevel': self['VisibleLogLevel'] | ||
'defaultLevel': self['VisibleLogLevel'], | ||
'customStatusColor': self._resolve_custom_status_colors() | ||
} | ||
|
||
@property | ||
|
@@ -606,6 +627,13 @@ def _resolve_background_colors(self): | |
colors = self['ReportBackground'] | ||
return {'pass': colors[0], 'fail': colors[1], 'skip': colors[2]} | ||
|
||
def _resolve_custom_status_colors(self): | ||
statuses = self['AddStatus'] | ||
status_colors = dict() | ||
for item in statuses: | ||
status_colors[item[0]] = item[3] | ||
return status_colors | ||
|
||
@property | ||
def merge(self): | ||
return self['Merge'] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,6 +50,7 @@ <h1>Opening Robot Framework log failed</h1> | |
initLayout(topsuite.name, 'Log'); | ||
addStatistics(); | ||
addErrors(); | ||
setCustomStatusColors(); | ||
addExecutionLog(topsuite); | ||
addLogLevelSelector(window.settings['minLevel'], window.settings['defaultLevel']); | ||
if (window.location.hash) { | ||
|
@@ -60,6 +61,14 @@ <h1>Opening Robot Framework log failed</h1> | |
setTimeout(function () { loadAndExpandElementIds(window.output['expand_keywords']); }, 100); | ||
}); | ||
|
||
function setCustomStatusColors() { | ||
let statuses = window.settings['customStatusColor'] | ||
let css = $("<style type='text/css'> </style>").appendTo($('body')); | ||
for (let status of Object.keys(statuses)) { | ||
css.append(".label.".concat(status.toLowerCase()).concat("{ background-color:").concat(statuses[status]).concat("}")); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it possible to just modify CSS directly and not inject |
||
|
||
function addLogLevelSelector(minLevel, defaultLevel) { | ||
var controller = LogLevelController(minLevel, defaultLevel); | ||
if (controller.showLogLevelSelector()) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,6 @@ window.testdata = function () { | |
var idCounter = 0; | ||
var _statistics = null; | ||
var LEVELS = ['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FAIL', 'SKIP']; | ||
var STATUSES = ['FAIL', 'PASS', 'SKIP', 'NOT RUN']; | ||
var KEYWORD_TYPES = ['KEYWORD', 'SETUP', 'TEARDOWN', 'FOR', 'VAR', 'IF', 'ELSE IF', 'ELSE']; | ||
var MESSAGE_TYPE = 8; | ||
|
||
|
@@ -38,7 +37,7 @@ window.testdata = function () { | |
} | ||
|
||
function parseStatus(stats) { | ||
return STATUSES[stats[0]]; | ||
return stats[0]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Statuses should be written to log.html as integers like earlier. That takes less space and lists containing only integers are faster to process by browsers than lists containing integers and strings. The hard coded |
||
} | ||
|
||
function childCreator(parent, childType) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ | |
windll = None | ||
|
||
from robot.errors import DataError | ||
from robot.result.model import StatusMixin | ||
from robot.utils import console_encode, isatty, WINDOWS | ||
|
||
|
||
|
@@ -98,16 +99,20 @@ def error(self, message, level): | |
@contextmanager | ||
def _highlighting(self, status): | ||
highlighter = self._highlighter | ||
start = {'PASS': highlighter.green, | ||
'FAIL': highlighter.red, | ||
start = {StatusMixin.PASS: highlighter.green, | ||
StatusMixin.FAIL: highlighter.red, | ||
'ERROR': highlighter.red, | ||
'WARN': highlighter.yellow, | ||
'SKIP': highlighter.yellow}[status] | ||
StatusMixin.SKIP: highlighter.yellow}[status] | ||
start() | ||
try: | ||
yield | ||
finally: | ||
highlighter.reset() | ||
# reset custom statuses | ||
StatusMixin.PASS = 'PASS' | ||
StatusMixin.FAIL = 'FAIL' | ||
StatusMixin.SKIP = 'SKIP' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks really strange. Do I get the logic right that code elsewhere sets custom values for |
||
|
||
|
||
def Highlighter(stream): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,6 @@ | |
|
||
|
||
IF_ELSE_ROOT = BodyItem.IF_ELSE_ROOT | ||
STATUSES = {'FAIL': 0, 'PASS': 1, 'SKIP': 2, 'NOT RUN': 3} | ||
KEYWORD_TYPES = {'KEYWORD': 0, 'SETUP': 1, 'TEARDOWN': 2, | ||
'FOR': 3, 'FOR ITERATION': 4, | ||
'IF': 5, 'ELSE IF': 6, 'ELSE': 7} | ||
|
@@ -60,7 +59,7 @@ def __init__(self, context): | |
def _get_status(self, item): | ||
# Branch status with IF/ELSE, "normal" status with others. | ||
status = getattr(item, 'branch_status', item.status) | ||
model = (STATUSES[status], | ||
model = (status, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I already commented the related JS code on log.html side, statuses should still be integers in log.html. Should somehow be able to map also custom statuses to integers here. Probably the mapping could be passed to |
||
self._timestamp(item.starttime), | ||
item.elapsedtime) | ||
msg = getattr(item, 'message', '') | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -283,6 +283,13 @@ | |
tag:<pattern>: flatten matched keywords using same | ||
matching rules as with | ||
`--removekeywords tag:<pattern>` | ||
--addstatus for|foritem|<NEWSTATUS:OLDSTATUS:TAGPATTERN:COLOR> * | ||
Allows the possibility to create custom statuses | ||
based on existing statues and tags. | ||
Examples: | ||
--addstatus KNOWN_ISSUE:FAIL:bug-id-*:purple | ||
--addstatus NON-CRITICAL:SKIP:non-critical:pink | ||
New in RF 4.x | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also |
||
--listener class * A class for monitoring test execution. Gets | ||
notifications e.g. when tests start and end. | ||
Arguments to the listener class can be given after | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
|
||
from robot.result.model import StatusMixin | ||
from robot.errors import ExecutionFailed, PassExecution | ||
from robot.model import TagPatterns | ||
from robot.utils import html_escape, py3to2, unic, test_or_task | ||
|
@@ -76,6 +77,7 @@ def __init__(self, parent=None, *exit_modes): | |
self.skipped = False | ||
self._teardown_allowed = False | ||
self._rpa = False | ||
self._custom_statuses = None | ||
if parent: | ||
parent.children.append(self) | ||
|
||
|
@@ -127,11 +129,21 @@ def failed(self): | |
|
||
@property | ||
def status(self): | ||
if self._custom_statuses and isinstance(self, TestStatus): | ||
for custom_status in self._custom_statuses: | ||
if TagPatterns(custom_status[2]).match(self._test.tags): | ||
if custom_status[1] == 'PASS': | ||
StatusMixin.PASS = custom_status[0] | ||
elif custom_status[1] == 'FAIL': | ||
StatusMixin.FAIL = custom_status[0] | ||
else: | ||
StatusMixin.SKIP = custom_status[0] | ||
break | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I commented the code on the highlighter, the logic to here modify constants and then elsewhere resetting them isn't a valid approach. It's too fragile because it requires code to be run in certain exact order and doesn't work at all if analyzing/modifying results programmatically. I'm not exactly sure what's the best way to handle this core logic, but I write some ideas as general comments. |
||
if self.skipped or (self.parent and self.parent.skipped): | ||
return 'SKIP' | ||
return StatusMixin.SKIP | ||
if self.failed: | ||
return 'FAIL' | ||
return 'PASS' | ||
return StatusMixin.FAIL | ||
return StatusMixin.PASS | ||
|
||
def _skip_on_failure(self): | ||
return False | ||
|
@@ -172,13 +184,14 @@ def _my_message(self): | |
class TestStatus(_ExecutionStatus): | ||
|
||
def __init__(self, parent, test, skip_on_failure=None, critical_tags=None, | ||
rpa=False): | ||
rpa=False, custom_statuses=None): | ||
_ExecutionStatus.__init__(self, parent) | ||
self.exit = parent.exit | ||
self._test = test | ||
self._skip_on_failure_tags = skip_on_failure | ||
self._critical_tags = critical_tags | ||
self._rpa = rpa | ||
self._custom_statuses = custom_statuses | ||
|
||
def test_failed(self, failure): | ||
if hasattr(failure, 'skip') and failure.skip: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change looks risky. Doesn't it mean that custom statuses aren't validated at all?