cucu 1.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of cucu might be problematic. Click here for more details.
- cucu/__init__.py +38 -0
- cucu/ansi_parser.py +58 -0
- cucu/behave_tweaks.py +196 -0
- cucu/browser/__init__.py +0 -0
- cucu/browser/core.py +80 -0
- cucu/browser/frames.py +106 -0
- cucu/browser/selenium.py +323 -0
- cucu/browser/selenium_tweaks.py +27 -0
- cucu/cli/__init__.py +3 -0
- cucu/cli/core.py +788 -0
- cucu/cli/run.py +207 -0
- cucu/cli/steps.py +137 -0
- cucu/cli/thread_dumper.py +55 -0
- cucu/config.py +440 -0
- cucu/edgedriver_autoinstaller/README.md +1 -0
- cucu/edgedriver_autoinstaller/__init__.py +37 -0
- cucu/edgedriver_autoinstaller/utils.py +231 -0
- cucu/environment.py +283 -0
- cucu/external/jquery/jquery-3.5.1.min.js +2 -0
- cucu/formatter/__init__.py +0 -0
- cucu/formatter/cucu.py +261 -0
- cucu/formatter/json.py +321 -0
- cucu/formatter/junit.py +289 -0
- cucu/fuzzy/__init__.py +3 -0
- cucu/fuzzy/core.py +107 -0
- cucu/fuzzy/fuzzy.js +253 -0
- cucu/helpers.py +875 -0
- cucu/hooks.py +205 -0
- cucu/language_server/__init__.py +3 -0
- cucu/language_server/core.py +114 -0
- cucu/lint/__init__.py +0 -0
- cucu/lint/linter.py +397 -0
- cucu/lint/rules/format.yaml +125 -0
- cucu/logger.py +113 -0
- cucu/matcher/__init__.py +0 -0
- cucu/matcher/core.py +30 -0
- cucu/page_checks.py +63 -0
- cucu/reporter/__init__.py +3 -0
- cucu/reporter/external/bootstrap.min.css +7 -0
- cucu/reporter/external/bootstrap.min.js +7 -0
- cucu/reporter/external/dataTables.bootstrap.min.css +1 -0
- cucu/reporter/external/dataTables.bootstrap.min.js +14 -0
- cucu/reporter/external/jquery-3.5.1.min.js +2 -0
- cucu/reporter/external/jquery.dataTables.min.js +192 -0
- cucu/reporter/external/popper.min.js +5 -0
- cucu/reporter/favicon.png +0 -0
- cucu/reporter/html.py +452 -0
- cucu/reporter/templates/feature.html +72 -0
- cucu/reporter/templates/flat.html +48 -0
- cucu/reporter/templates/index.html +49 -0
- cucu/reporter/templates/layout.html +109 -0
- cucu/reporter/templates/scenario.html +200 -0
- cucu/steps/__init__.py +27 -0
- cucu/steps/base_steps.py +88 -0
- cucu/steps/browser_steps.py +337 -0
- cucu/steps/button_steps.py +91 -0
- cucu/steps/checkbox_steps.py +111 -0
- cucu/steps/command_steps.py +181 -0
- cucu/steps/comment_steps.py +17 -0
- cucu/steps/draggable_steps.py +168 -0
- cucu/steps/dropdown_steps.py +467 -0
- cucu/steps/file_input_steps.py +80 -0
- cucu/steps/filesystem_steps.py +144 -0
- cucu/steps/flow_control_steps.py +198 -0
- cucu/steps/image_steps.py +37 -0
- cucu/steps/input_steps.py +301 -0
- cucu/steps/link_steps.py +63 -0
- cucu/steps/menuitem_steps.py +39 -0
- cucu/steps/platform_steps.py +29 -0
- cucu/steps/radio_steps.py +187 -0
- cucu/steps/step_utils.py +55 -0
- cucu/steps/tab_steps.py +68 -0
- cucu/steps/table_steps.py +437 -0
- cucu/steps/tables.js +28 -0
- cucu/steps/text_steps.py +78 -0
- cucu/steps/variable_steps.py +100 -0
- cucu/steps/webserver_steps.py +40 -0
- cucu/utils.py +269 -0
- cucu-1.0.0.dist-info/METADATA +424 -0
- cucu-1.0.0.dist-info/RECORD +83 -0
- cucu-1.0.0.dist-info/WHEEL +4 -0
- cucu-1.0.0.dist-info/entry_points.txt +2 -0
- cucu-1.0.0.dist-info/licenses/LICENSE +32 -0
cucu/formatter/cucu.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from __future__ import absolute_import
|
|
3
|
+
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
from behave.formatter.ansi_escapes import colors, escapes, up
|
|
7
|
+
from behave.formatter.base import Formatter
|
|
8
|
+
from behave.model_core import Status
|
|
9
|
+
from behave.model_describe import ModelPrinter
|
|
10
|
+
from behave.textutil import make_indentation
|
|
11
|
+
|
|
12
|
+
from cucu.config import CONFIG
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CucuFormatter(Formatter):
|
|
16
|
+
""" """
|
|
17
|
+
|
|
18
|
+
name = "cucu"
|
|
19
|
+
description = "cucu specific formatter to make the logs more readable"
|
|
20
|
+
|
|
21
|
+
DEFAULT_INDENT_SIZE = 2
|
|
22
|
+
SUBSTEP_PREFIX = " ⤷"
|
|
23
|
+
|
|
24
|
+
def __init__(self, stream_opener, config, **kwargs):
|
|
25
|
+
super(CucuFormatter, self).__init__(stream_opener, config)
|
|
26
|
+
self.current_scenario = None
|
|
27
|
+
self.steps = []
|
|
28
|
+
self.match_step_index = 0
|
|
29
|
+
self.show_timings = config.show_timings
|
|
30
|
+
self.indent_size = self.DEFAULT_INDENT_SIZE
|
|
31
|
+
# -- ENSURE: Output stream is open.
|
|
32
|
+
self.stream = self.open()
|
|
33
|
+
self.printer = ModelPrinter(self.stream)
|
|
34
|
+
# -- LAZY-EVALUATE:
|
|
35
|
+
self._multiline_indentation = None
|
|
36
|
+
|
|
37
|
+
color_output = CONFIG["CUCU_COLOR_OUTPUT"]
|
|
38
|
+
self.monochrome = color_output != "true"
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def multiline_indentation(self):
|
|
42
|
+
if self._multiline_indentation is None:
|
|
43
|
+
# align keywords
|
|
44
|
+
offset = 2
|
|
45
|
+
indentation = make_indentation(3 * self.indent_size + offset)
|
|
46
|
+
self._multiline_indentation = indentation
|
|
47
|
+
|
|
48
|
+
return self._multiline_indentation
|
|
49
|
+
|
|
50
|
+
def write_tags(self, tags, indent=None, for_feature=False):
|
|
51
|
+
"""
|
|
52
|
+
writes the tags for a scenario or feature and takes extra care to be
|
|
53
|
+
sure to indent corectly for scenarios when `indent` is specified. We
|
|
54
|
+
also handle putting new lines in the right locations if its a scenario
|
|
55
|
+
or feature based on `for_feature` boolean flag.
|
|
56
|
+
"""
|
|
57
|
+
indent = indent or ""
|
|
58
|
+
if tags:
|
|
59
|
+
text = " @".join(tags)
|
|
60
|
+
line = self.colorize(f"{indent}@{text}", "cyan")
|
|
61
|
+
if for_feature:
|
|
62
|
+
self.stream.write(f"{line}\n")
|
|
63
|
+
else:
|
|
64
|
+
self.stream.write(f"\n{line}")
|
|
65
|
+
|
|
66
|
+
# -- IMPLEMENT-INTERFACE FOR: Formatter
|
|
67
|
+
def feature(self, feature):
|
|
68
|
+
self.write_tags(feature.tags, for_feature=True)
|
|
69
|
+
text = f'{self.colorize(feature.keyword, "magenta")}: {feature.name}\n'
|
|
70
|
+
self.stream.write(text)
|
|
71
|
+
|
|
72
|
+
def colorize(self, text, color):
|
|
73
|
+
if self.monochrome:
|
|
74
|
+
return text
|
|
75
|
+
else:
|
|
76
|
+
return colors[color] + text + escapes["reset"]
|
|
77
|
+
|
|
78
|
+
def background(self, background):
|
|
79
|
+
if not self.monochrome:
|
|
80
|
+
indent = make_indentation(self.indent_size)
|
|
81
|
+
keyword = self.colorize(background.keyword, "magenta")
|
|
82
|
+
text = f"\n{indent}{keyword}: {background.name}\n"
|
|
83
|
+
self.stream.write(text)
|
|
84
|
+
|
|
85
|
+
def scenario(self, scenario):
|
|
86
|
+
self.current_scenario = scenario
|
|
87
|
+
|
|
88
|
+
indent = make_indentation(self.indent_size)
|
|
89
|
+
text = "\n%s%s: %s\n" % (
|
|
90
|
+
indent,
|
|
91
|
+
self.colorize(scenario.keyword, "magenta"),
|
|
92
|
+
scenario.name,
|
|
93
|
+
)
|
|
94
|
+
self.write_tags(scenario.tags, indent)
|
|
95
|
+
self.stream.write(text)
|
|
96
|
+
self.steps = []
|
|
97
|
+
self.match_step_index = 0
|
|
98
|
+
|
|
99
|
+
def step(self, step):
|
|
100
|
+
self.insert_step(step)
|
|
101
|
+
|
|
102
|
+
def insert_step(self, step, index=-1):
|
|
103
|
+
# used to determine how to better handle console output
|
|
104
|
+
step.has_substeps = False
|
|
105
|
+
step.is_substep = False
|
|
106
|
+
|
|
107
|
+
if index == -1:
|
|
108
|
+
self.steps.append(step)
|
|
109
|
+
else:
|
|
110
|
+
self.steps.insert(index, step)
|
|
111
|
+
|
|
112
|
+
def match(self, match):
|
|
113
|
+
if self.monochrome:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# we'll write a step line in grey and not hit the carriage return
|
|
117
|
+
step = self.steps[self.match_step_index]
|
|
118
|
+
self.match_step_index += 1
|
|
119
|
+
|
|
120
|
+
indent = make_indentation(2 * self.indent_size)
|
|
121
|
+
keyword = step.keyword.rjust(5)
|
|
122
|
+
|
|
123
|
+
prefix = ""
|
|
124
|
+
if step.is_substep:
|
|
125
|
+
prefix = self.SUBSTEP_PREFIX
|
|
126
|
+
|
|
127
|
+
text = self.colorize(
|
|
128
|
+
f"{indent}{prefix}{keyword} {step.name}\n", "grey"
|
|
129
|
+
)
|
|
130
|
+
self.stream.write(text)
|
|
131
|
+
self.stream.flush()
|
|
132
|
+
|
|
133
|
+
def calculate_max_line_length(self):
|
|
134
|
+
line_lengths = [
|
|
135
|
+
len(f"{step.keyword.rjust(5)} {step.name}") for step in self.steps
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
return max(line_lengths) + 4
|
|
139
|
+
|
|
140
|
+
def result(self, step):
|
|
141
|
+
indent = make_indentation(2 * self.indent_size)
|
|
142
|
+
keyword = step.keyword.rjust(5)
|
|
143
|
+
|
|
144
|
+
if not (
|
|
145
|
+
self.monochrome
|
|
146
|
+
or step.has_substeps
|
|
147
|
+
or CONFIG["__CUCU_WROTE_TO_OUTPUT"]
|
|
148
|
+
):
|
|
149
|
+
self.stream.write(up(1))
|
|
150
|
+
|
|
151
|
+
prefix = ""
|
|
152
|
+
if step.is_substep:
|
|
153
|
+
prefix = self.SUBSTEP_PREFIX
|
|
154
|
+
|
|
155
|
+
if step.status == Status.passed:
|
|
156
|
+
keyword = self.colorize(keyword, "green")
|
|
157
|
+
text = f"{indent}{prefix}{keyword} {step.name}"
|
|
158
|
+
elif step.status == Status.failed:
|
|
159
|
+
text = self.colorize(
|
|
160
|
+
f"{indent}{prefix}{keyword} {step.name}", "red"
|
|
161
|
+
)
|
|
162
|
+
elif step.status == Status.undefined:
|
|
163
|
+
text = self.colorize(
|
|
164
|
+
f"{indent}{prefix}{keyword} {step.name}", "yellow"
|
|
165
|
+
)
|
|
166
|
+
elif step.status == Status.skipped:
|
|
167
|
+
text = self.colorize(
|
|
168
|
+
f"{indent}{prefix}{keyword} {step.name}\n", "cyan"
|
|
169
|
+
)
|
|
170
|
+
elif step.status == Status.untested:
|
|
171
|
+
text = self.colorize(
|
|
172
|
+
f"{indent}{prefix}{keyword} {step.name}\n", "cyan"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if self.monochrome:
|
|
176
|
+
self.stream.write(f"{text}")
|
|
177
|
+
else:
|
|
178
|
+
self.stream.write(f"\r{text}")
|
|
179
|
+
|
|
180
|
+
if step.status in (Status.passed, Status.failed):
|
|
181
|
+
max_line_length = self.calculate_max_line_length()
|
|
182
|
+
status_text = ""
|
|
183
|
+
if self.show_timings:
|
|
184
|
+
start = step.start_timestamp
|
|
185
|
+
duration = f"{step.duration:.3f}"
|
|
186
|
+
status_text += f" # started at {start} took {duration}s"
|
|
187
|
+
|
|
188
|
+
current_step_text = f"{step.keyword.rjust(5)} {step.name}"
|
|
189
|
+
status_text_padding = (
|
|
190
|
+
max_line_length - len(current_step_text) - len(prefix)
|
|
191
|
+
)
|
|
192
|
+
status_text = f'{" " * status_text_padding}{status_text}'
|
|
193
|
+
status_text = self.colorize(status_text, "yellow")
|
|
194
|
+
|
|
195
|
+
self.stream.write(f"{status_text}\n")
|
|
196
|
+
if step.error_message:
|
|
197
|
+
self.stream.write(f"{step.error_message}\n")
|
|
198
|
+
|
|
199
|
+
if step.text:
|
|
200
|
+
self.doc_string(step.text)
|
|
201
|
+
|
|
202
|
+
if step.table:
|
|
203
|
+
self.table(getattr(step.table, "original", step.table))
|
|
204
|
+
|
|
205
|
+
if step.status in (Status.passed, Status.failed):
|
|
206
|
+
# print the variable values in step name, multiline/table arguments
|
|
207
|
+
# as a comment to ease debugging
|
|
208
|
+
step_variables = CONFIG.expand(step.name)
|
|
209
|
+
|
|
210
|
+
if step.text:
|
|
211
|
+
step_variables.update(CONFIG.expand(step.text))
|
|
212
|
+
|
|
213
|
+
if step.table:
|
|
214
|
+
for row in step.table.original.rows:
|
|
215
|
+
for value in row:
|
|
216
|
+
step_variables.update(CONFIG.expand(value))
|
|
217
|
+
|
|
218
|
+
if step_variables:
|
|
219
|
+
expanded = " ".join(
|
|
220
|
+
[
|
|
221
|
+
f'{key}="{value}"'
|
|
222
|
+
for (key, value) in step_variables.items()
|
|
223
|
+
]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
padding = f" {' '*(len('Given')-len(step.keyword))}"
|
|
227
|
+
variable_line = f"{padding}# {expanded}\n"
|
|
228
|
+
# hide secrets before we do anything to add color which could
|
|
229
|
+
# modify the output and result in not being able to correctly
|
|
230
|
+
# parse
|
|
231
|
+
# TODO: I'd like to move this out of here as we should be able
|
|
232
|
+
# to intercept all of the stdout/stderr writes but seems
|
|
233
|
+
# behaves underlying self.stream here is getting around
|
|
234
|
+
# that by accessing stdout/stderr another way.
|
|
235
|
+
variable_line = CONFIG.hide_secrets(variable_line)
|
|
236
|
+
colored_variable_line = self.colorize(variable_line, "blue")
|
|
237
|
+
self.stream.write(colored_variable_line)
|
|
238
|
+
self.stream.flush()
|
|
239
|
+
|
|
240
|
+
self.previous_step = step
|
|
241
|
+
CONFIG["__CUCU_WROTE_TO_OUTPUT"] = False
|
|
242
|
+
|
|
243
|
+
def eof(self):
|
|
244
|
+
self.stream.write("\n")
|
|
245
|
+
|
|
246
|
+
if self.current_scenario:
|
|
247
|
+
if self.current_scenario.status.name == "failed":
|
|
248
|
+
# we need to record the error_message and exc_traceback in the
|
|
249
|
+
# last executed step and mark it as failed so the reporting can
|
|
250
|
+
# show the result correctly
|
|
251
|
+
error_message = traceback.format_tb(
|
|
252
|
+
self.current_scenario.exc_traceback
|
|
253
|
+
)
|
|
254
|
+
self.stream.write("\n".join(error_message))
|
|
255
|
+
|
|
256
|
+
# -- MORE: Formatter helpers
|
|
257
|
+
def doc_string(self, doc_string):
|
|
258
|
+
self.printer.print_docstring(doc_string, self.multiline_indentation)
|
|
259
|
+
|
|
260
|
+
def table(self, table):
|
|
261
|
+
self.printer.print_table(table, self.multiline_indentation)
|
cucu/formatter/json.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# adapted from:
|
|
4
|
+
# https://github.com/behave/behave/blob/master/behave/formatter/json.py
|
|
5
|
+
#
|
|
6
|
+
from __future__ import absolute_import
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import traceback
|
|
10
|
+
import uuid
|
|
11
|
+
|
|
12
|
+
import six
|
|
13
|
+
from behave.formatter.base import Formatter
|
|
14
|
+
from behave.model_core import Status
|
|
15
|
+
from tenacity import RetryError
|
|
16
|
+
|
|
17
|
+
from cucu.config import CONFIG
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# -----------------------------------------------------------------------------
|
|
21
|
+
# CLASS: JSONFormatter
|
|
22
|
+
# -----------------------------------------------------------------------------
|
|
23
|
+
class CucuJSONFormatter(Formatter):
|
|
24
|
+
name = "cucu-json"
|
|
25
|
+
description = "JSON dump of test run"
|
|
26
|
+
dumps_kwargs = {"indent": 2, "sort_keys": True}
|
|
27
|
+
|
|
28
|
+
split_text_into_lines = True # EXPERIMENT for better readability.
|
|
29
|
+
|
|
30
|
+
json_number_types = six.integer_types + (float,)
|
|
31
|
+
json_scalar_types = json_number_types + (six.text_type, bool, type(None))
|
|
32
|
+
|
|
33
|
+
def __init__(self, stream_opener, config):
|
|
34
|
+
super(CucuJSONFormatter, self).__init__(stream_opener, config)
|
|
35
|
+
# -- ENSURE: Output stream is open.
|
|
36
|
+
self.stream = self.open()
|
|
37
|
+
self.feature_count = 0
|
|
38
|
+
self.current_feature = None
|
|
39
|
+
self.current_feature_data = None
|
|
40
|
+
self.current_scenario = None
|
|
41
|
+
self.last_step = None
|
|
42
|
+
self.steps = []
|
|
43
|
+
self.write_json_header()
|
|
44
|
+
|
|
45
|
+
def reset(self):
|
|
46
|
+
self.current_feature = None
|
|
47
|
+
self.current_feature_data = None
|
|
48
|
+
self.current_scenario = None
|
|
49
|
+
|
|
50
|
+
# -- FORMATTER API:
|
|
51
|
+
def uri(self, uri):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def feature(self, feature):
|
|
55
|
+
self.reset()
|
|
56
|
+
self.current_feature = feature
|
|
57
|
+
self.current_feature_data = {
|
|
58
|
+
"keyword": feature.keyword,
|
|
59
|
+
"name": feature.name,
|
|
60
|
+
"tags": list(feature.tags),
|
|
61
|
+
"location": six.text_type(feature.location),
|
|
62
|
+
"status": None, # Not known before feature run.
|
|
63
|
+
}
|
|
64
|
+
element = self.current_feature_data
|
|
65
|
+
if feature.description:
|
|
66
|
+
element["description"] = feature.description
|
|
67
|
+
|
|
68
|
+
def background(self, background):
|
|
69
|
+
element = self.add_feature_element(
|
|
70
|
+
{
|
|
71
|
+
"type": "background",
|
|
72
|
+
"keyword": background.keyword,
|
|
73
|
+
"name": background.name,
|
|
74
|
+
"location": six.text_type(background.location),
|
|
75
|
+
"steps": [],
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
if background.name:
|
|
79
|
+
element["name"] = background.name
|
|
80
|
+
|
|
81
|
+
# -- ADD BACKGROUND STEPS: Support *.feature file regeneration.
|
|
82
|
+
for step_ in background.steps:
|
|
83
|
+
self.step(step_)
|
|
84
|
+
|
|
85
|
+
def scenario(self, scenario):
|
|
86
|
+
self.finish_current_scenario()
|
|
87
|
+
self.current_scenario = scenario
|
|
88
|
+
|
|
89
|
+
element = self.add_feature_element(
|
|
90
|
+
{
|
|
91
|
+
"type": "scenario",
|
|
92
|
+
"keyword": scenario.keyword,
|
|
93
|
+
"name": scenario.name,
|
|
94
|
+
"tags": scenario.tags,
|
|
95
|
+
"location": six.text_type(scenario.location),
|
|
96
|
+
"steps": [],
|
|
97
|
+
"status": None,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
self.steps = []
|
|
101
|
+
if scenario.description:
|
|
102
|
+
element["description"] = scenario.description
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def make_table(cls, table):
|
|
106
|
+
table_data = {
|
|
107
|
+
"headings": table.headings,
|
|
108
|
+
"rows": [list(row) for row in table.rows],
|
|
109
|
+
}
|
|
110
|
+
return table_data
|
|
111
|
+
|
|
112
|
+
def insert_step(self, step, index=-1):
|
|
113
|
+
step.unique_id = uuid.uuid1()
|
|
114
|
+
is_substep = getattr(step, "is_substep", False)
|
|
115
|
+
|
|
116
|
+
step_details = {
|
|
117
|
+
"keyword": step.keyword,
|
|
118
|
+
"step_type": step.step_type,
|
|
119
|
+
"name": step.name,
|
|
120
|
+
"location": six.text_type(step.location),
|
|
121
|
+
"substep": is_substep,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if step.text:
|
|
125
|
+
text = step.text
|
|
126
|
+
if self.split_text_into_lines and "\n" in text:
|
|
127
|
+
text = text.splitlines()
|
|
128
|
+
step_details["text"] = text
|
|
129
|
+
if step.table:
|
|
130
|
+
step_details["table"] = self.make_table(step.table)
|
|
131
|
+
element = self.current_feature_element
|
|
132
|
+
|
|
133
|
+
if index == -1:
|
|
134
|
+
self.steps.append(step)
|
|
135
|
+
element["steps"].append(step_details)
|
|
136
|
+
else:
|
|
137
|
+
self.steps.insert(index, step)
|
|
138
|
+
element["steps"].insert(index, step_details)
|
|
139
|
+
|
|
140
|
+
def step(self, step):
|
|
141
|
+
self.insert_step(step, index=-1)
|
|
142
|
+
|
|
143
|
+
def match(self, match):
|
|
144
|
+
# nothing to do, but we need to implement the method
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
def result(self, step):
|
|
148
|
+
steps = self.current_feature_element["steps"]
|
|
149
|
+
step_index = 0
|
|
150
|
+
for other_step in self.steps:
|
|
151
|
+
if other_step.unique_id == step.unique_id:
|
|
152
|
+
break
|
|
153
|
+
step_index += 1
|
|
154
|
+
|
|
155
|
+
# keep the last step recorded result state
|
|
156
|
+
self.last_step = steps[step_index]
|
|
157
|
+
|
|
158
|
+
timestamp = None
|
|
159
|
+
if step.status.name in ["passed", "failed"]:
|
|
160
|
+
timestamp = step.start_timestamp
|
|
161
|
+
|
|
162
|
+
step_variables = CONFIG.expand(step.name)
|
|
163
|
+
|
|
164
|
+
if step.text:
|
|
165
|
+
step_variables.update(CONFIG.expand(step.text))
|
|
166
|
+
|
|
167
|
+
if step.table:
|
|
168
|
+
for row in step.table.original.rows:
|
|
169
|
+
for value in row:
|
|
170
|
+
step_variables.update(CONFIG.expand(value))
|
|
171
|
+
|
|
172
|
+
if step_variables:
|
|
173
|
+
expanded = " ".join(
|
|
174
|
+
[
|
|
175
|
+
f'{key}="{value}"'
|
|
176
|
+
for (key, value) in step_variables.items()
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
padding = f" {' '*(len('Given')-len(step.keyword))}"
|
|
180
|
+
step.stdout.insert(
|
|
181
|
+
0, f"{padding}# {CONFIG.hide_secrets(expanded)}\n"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
stdout = None
|
|
185
|
+
if "stdout" in step.__dict__ and step.stdout != []:
|
|
186
|
+
stdout = [CONFIG.hide_secrets("".join(step.stdout).rstrip())]
|
|
187
|
+
|
|
188
|
+
stderr = None
|
|
189
|
+
if "stderr" in step.__dict__ and step.stderr != []:
|
|
190
|
+
stderr = [CONFIG.hide_secrets("".join(step.stderr).rstrip())]
|
|
191
|
+
|
|
192
|
+
steps[step_index]["result"] = {
|
|
193
|
+
"stdout": stdout,
|
|
194
|
+
"stderr": stderr,
|
|
195
|
+
"status": step.status.name,
|
|
196
|
+
"duration": step.duration,
|
|
197
|
+
"timestamp": timestamp,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if step.error_message and step.status == Status.failed:
|
|
201
|
+
# -- OPTIONAL: Provided for failed steps.
|
|
202
|
+
error_message = CONFIG.hide_secrets(step.error_message)
|
|
203
|
+
if self.split_text_into_lines:
|
|
204
|
+
error_message = error_message.splitlines()
|
|
205
|
+
|
|
206
|
+
result_element = steps[step_index]["result"]
|
|
207
|
+
result_element["error_message"] = error_message
|
|
208
|
+
|
|
209
|
+
if error := step.exception:
|
|
210
|
+
if isinstance(error, RetryError):
|
|
211
|
+
error = error.last_attempt.exception()
|
|
212
|
+
|
|
213
|
+
if len(error.args) > 0 and isinstance(error.args[0], str):
|
|
214
|
+
error_class_name = error.__class__.__name__
|
|
215
|
+
redacted_error_msg = CONFIG.hide_secrets(error.args[0])
|
|
216
|
+
error_lines = redacted_error_msg.splitlines()
|
|
217
|
+
error_lines[0] = f"{error_class_name}: {error_lines[0]}"
|
|
218
|
+
else:
|
|
219
|
+
error_lines = [repr(error)]
|
|
220
|
+
|
|
221
|
+
result_element["exception"] = error_lines
|
|
222
|
+
|
|
223
|
+
def embedding(self, mime_type, data):
|
|
224
|
+
# nothing to do, but we need to implement the method
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
def eof(self):
|
|
228
|
+
"""
|
|
229
|
+
End of feature
|
|
230
|
+
"""
|
|
231
|
+
if not self.current_feature_data:
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# -- NORMAL CASE: Write collected data of current feature.
|
|
235
|
+
self.finish_current_scenario()
|
|
236
|
+
self.update_status_data()
|
|
237
|
+
|
|
238
|
+
self.write_json_feature(self.current_feature_data)
|
|
239
|
+
self.reset()
|
|
240
|
+
|
|
241
|
+
def close(self):
|
|
242
|
+
self.write_json_footer()
|
|
243
|
+
self.close_stream()
|
|
244
|
+
|
|
245
|
+
# -- JSON-DATA COLLECTION:
|
|
246
|
+
def add_feature_element(self, element):
|
|
247
|
+
assert self.current_feature_data is not None
|
|
248
|
+
if "elements" not in self.current_feature_data:
|
|
249
|
+
self.current_feature_data["elements"] = []
|
|
250
|
+
self.current_feature_data["elements"].append(element)
|
|
251
|
+
return element
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def current_feature_element(self):
|
|
255
|
+
assert self.current_feature_data is not None
|
|
256
|
+
return self.current_feature_data["elements"][-1]
|
|
257
|
+
|
|
258
|
+
def update_status_data(self):
|
|
259
|
+
assert self.current_feature
|
|
260
|
+
assert self.current_feature_data
|
|
261
|
+
self.current_feature_data["status"] = self.current_feature.status.name
|
|
262
|
+
|
|
263
|
+
def finish_current_scenario(self):
|
|
264
|
+
if self.current_scenario:
|
|
265
|
+
hook_failed = self.current_scenario.hook_failed
|
|
266
|
+
if hook_failed:
|
|
267
|
+
status_name = "errored"
|
|
268
|
+
else:
|
|
269
|
+
status_name = self.current_scenario.status.name
|
|
270
|
+
|
|
271
|
+
self.current_feature_element["status"] = status_name
|
|
272
|
+
|
|
273
|
+
if status_name in ["failed", "errored"]:
|
|
274
|
+
# we need to record the error_message and exc_traceback in the
|
|
275
|
+
# last executed step and mark it as failed so the reporting can
|
|
276
|
+
# show the result correctly
|
|
277
|
+
error_message = [self.current_scenario.error_message]
|
|
278
|
+
error_message += traceback.format_tb(
|
|
279
|
+
self.current_scenario.exc_traceback
|
|
280
|
+
)
|
|
281
|
+
# If a before scenario hook fails, last_step will be None.
|
|
282
|
+
if (
|
|
283
|
+
self.last_step is not None
|
|
284
|
+
and "error_message" not in self.last_step["result"]
|
|
285
|
+
):
|
|
286
|
+
self.last_step["result"]["error_message"] = error_message
|
|
287
|
+
|
|
288
|
+
# -- JSON-WRITER:
|
|
289
|
+
def write_json_header(self):
|
|
290
|
+
self.stream.write("[\n")
|
|
291
|
+
|
|
292
|
+
def write_json_footer(self):
|
|
293
|
+
self.stream.write("\n]\n")
|
|
294
|
+
|
|
295
|
+
def write_json_feature(self, feature_data):
|
|
296
|
+
if "elements" not in feature_data:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
filtered_scenarios = [
|
|
300
|
+
x
|
|
301
|
+
for x in feature_data["elements"]
|
|
302
|
+
if x["keyword"] == "Scenario"
|
|
303
|
+
and (
|
|
304
|
+
CONFIG["CUCU_SHOW_SKIPS"] == "true" or x["status"] != "skipped"
|
|
305
|
+
)
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
if len(filtered_scenarios) == 0:
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
feature_data["elements"] = filtered_scenarios
|
|
312
|
+
|
|
313
|
+
if self.feature_count != 0:
|
|
314
|
+
self.write_json_feature_separator()
|
|
315
|
+
|
|
316
|
+
self.stream.write(json.dumps(feature_data, **self.dumps_kwargs))
|
|
317
|
+
self.stream.flush()
|
|
318
|
+
self.feature_count += 1
|
|
319
|
+
|
|
320
|
+
def write_json_feature_separator(self):
|
|
321
|
+
self.stream.write(",\n\n")
|