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/reporter/html.py
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
import urllib
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from xml.sax.saxutils import escape as escape_
|
|
11
|
+
|
|
12
|
+
import jinja2
|
|
13
|
+
|
|
14
|
+
from cucu import format_gherkin_table, logger
|
|
15
|
+
from cucu.ansi_parser import parse_log_to_html
|
|
16
|
+
from cucu.config import CONFIG
|
|
17
|
+
from cucu.utils import ellipsize_filename, get_step_image_dir
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def escape(data):
|
|
21
|
+
if data is None:
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
return escape_(data, {'"': """}).rstrip()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def process_tags(element):
|
|
28
|
+
"""
|
|
29
|
+
process tags in the element provided (scenario or feature) and basically
|
|
30
|
+
convert the tags to a simple @xxx representation.
|
|
31
|
+
"""
|
|
32
|
+
prepared_tags = []
|
|
33
|
+
|
|
34
|
+
if "tags" not in element:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
for tag in element["tags"]:
|
|
38
|
+
tag = f"@{tag}"
|
|
39
|
+
|
|
40
|
+
# process custom tag handlers
|
|
41
|
+
tag_handlers = CONFIG["__CUCU_HTML_REPORT_TAG_HANDLERS"].items()
|
|
42
|
+
for regex, handler in tag_handlers:
|
|
43
|
+
if regex.match(tag):
|
|
44
|
+
tag = handler(tag)
|
|
45
|
+
|
|
46
|
+
prepared_tags.append(tag)
|
|
47
|
+
|
|
48
|
+
element["tags"] = " ".join(prepared_tags)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# function to left pad duration with '0' for better alphabetical sorting in html reports.
|
|
52
|
+
def left_pad_zeroes(elapsed_time):
|
|
53
|
+
int_decimal = str(round(elapsed_time, 3)).split(".")
|
|
54
|
+
int_decimal[0] = int_decimal[0].zfill(3)
|
|
55
|
+
padded_duration = ".".join(int_decimal)
|
|
56
|
+
return padded_duration
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def generate(results, basepath, only_failures=False):
|
|
60
|
+
"""
|
|
61
|
+
generate an HTML report for the results provided.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
show_status = CONFIG["CUCU_SHOW_STATUS"] == "true"
|
|
65
|
+
|
|
66
|
+
features = []
|
|
67
|
+
|
|
68
|
+
run_json_filepaths = list(glob.iglob(os.path.join(results, "*run.json")))
|
|
69
|
+
logger.info(
|
|
70
|
+
f"Starting to process {len(run_json_filepaths)} files for report"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for run_json_filepath in run_json_filepaths:
|
|
74
|
+
with open(run_json_filepath, "rb") as index_input:
|
|
75
|
+
try:
|
|
76
|
+
features += json.loads(index_input.read())
|
|
77
|
+
if show_status:
|
|
78
|
+
print("r", end="", flush=True)
|
|
79
|
+
except Exception as exception:
|
|
80
|
+
if show_status:
|
|
81
|
+
print("") # add a newline before logger
|
|
82
|
+
logger.warn(
|
|
83
|
+
f"unable to read file {run_json_filepath}, got error: {exception}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# copy the external dependencies to the reports destination directory
|
|
87
|
+
cucu_dir = os.path.dirname(sys.modules["cucu"].__file__)
|
|
88
|
+
external_dir = os.path.join(cucu_dir, "reporter", "external")
|
|
89
|
+
shutil.copytree(external_dir, os.path.join(basepath, "external"))
|
|
90
|
+
shutil.copyfile(
|
|
91
|
+
os.path.join(cucu_dir, "reporter", "favicon.png"),
|
|
92
|
+
os.path.join(basepath, "favicon.png"),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
#
|
|
96
|
+
# augment existing test run data with:
|
|
97
|
+
# * features & scenarios with `duration` attribute computed by adding all
|
|
98
|
+
# step durations.
|
|
99
|
+
# * add `image` attribute to a step if it has an underlying .png image.
|
|
100
|
+
#
|
|
101
|
+
CONFIG.snapshot()
|
|
102
|
+
reported_features = []
|
|
103
|
+
for feature in features:
|
|
104
|
+
feature["folder_name"] = ellipsize_filename(feature["name"])
|
|
105
|
+
if show_status:
|
|
106
|
+
print("F", end="", flush=True)
|
|
107
|
+
scenarios = []
|
|
108
|
+
|
|
109
|
+
if feature["status"] != "untested" and "elements" in feature:
|
|
110
|
+
scenarios = feature["elements"]
|
|
111
|
+
|
|
112
|
+
if only_failures and feature["status"] != "failed":
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
feature_duration = 0
|
|
116
|
+
total_scenarios = 0
|
|
117
|
+
total_scenarios_passed = 0
|
|
118
|
+
total_scenarios_failed = 0
|
|
119
|
+
total_scenarios_skipped = 0
|
|
120
|
+
total_scenarios_errored = 0
|
|
121
|
+
feature_started_at = None
|
|
122
|
+
|
|
123
|
+
reported_features.append(feature)
|
|
124
|
+
process_tags(feature)
|
|
125
|
+
|
|
126
|
+
if feature["status"] not in ["skipped", "untested"]:
|
|
127
|
+
# copy each feature directories contents over to the report directory
|
|
128
|
+
src_feature_filepath = os.path.join(
|
|
129
|
+
results, feature["folder_name"]
|
|
130
|
+
)
|
|
131
|
+
dst_feature_filepath = os.path.join(
|
|
132
|
+
basepath, feature["folder_name"]
|
|
133
|
+
)
|
|
134
|
+
shutil.copytree(
|
|
135
|
+
src_feature_filepath, dst_feature_filepath, dirs_exist_ok=True
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
for scenario in scenarios:
|
|
139
|
+
CONFIG.restore()
|
|
140
|
+
|
|
141
|
+
scenario["folder_name"] = ellipsize_filename(scenario["name"])
|
|
142
|
+
scenario_filepath = os.path.join(
|
|
143
|
+
basepath,
|
|
144
|
+
feature["folder_name"],
|
|
145
|
+
scenario["folder_name"],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
scenario_configpath = os.path.join(
|
|
149
|
+
scenario_filepath, "logs", "cucu.config.yaml.txt"
|
|
150
|
+
)
|
|
151
|
+
if os.path.exists(scenario_configpath):
|
|
152
|
+
try:
|
|
153
|
+
CONFIG.load(scenario_configpath)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.warn(
|
|
156
|
+
f"Could not reload config: {scenario_configpath}: {e}"
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
logger.info(f"No config to reload: {scenario_configpath}")
|
|
160
|
+
|
|
161
|
+
if show_status:
|
|
162
|
+
print("S", end="", flush=True)
|
|
163
|
+
|
|
164
|
+
process_tags(scenario)
|
|
165
|
+
|
|
166
|
+
sub_headers = []
|
|
167
|
+
for handler in CONFIG[
|
|
168
|
+
"__CUCU_HTML_REPORT_SCENARIO_SUBHEADER_HANDLER"
|
|
169
|
+
]:
|
|
170
|
+
try:
|
|
171
|
+
sub_headers.append(handler(scenario))
|
|
172
|
+
except Exception:
|
|
173
|
+
logger.warn(
|
|
174
|
+
f"Exception while trying to run sub_headers hook for scenario: \"{scenario['name']}\"\n{traceback.format_exc()}"
|
|
175
|
+
)
|
|
176
|
+
scenario["sub_headers"] = "<br/>".join(sub_headers)
|
|
177
|
+
|
|
178
|
+
scenario_duration = 0
|
|
179
|
+
total_scenarios += 1
|
|
180
|
+
total_steps = 0
|
|
181
|
+
|
|
182
|
+
if "status" not in scenario:
|
|
183
|
+
total_scenarios_skipped += 1
|
|
184
|
+
elif scenario["status"] == "passed":
|
|
185
|
+
total_scenarios_passed += 1
|
|
186
|
+
elif scenario["status"] == "failed":
|
|
187
|
+
total_scenarios_failed += 1
|
|
188
|
+
elif scenario["status"] == "skipped":
|
|
189
|
+
total_scenarios_skipped += 1
|
|
190
|
+
elif scenario["status"] == "errored":
|
|
191
|
+
total_scenarios_errored += 1
|
|
192
|
+
|
|
193
|
+
step_index = 0
|
|
194
|
+
scenario_started_at = None
|
|
195
|
+
for step in scenario["steps"]:
|
|
196
|
+
if show_status:
|
|
197
|
+
print("s", end="", flush=True)
|
|
198
|
+
total_steps += 1
|
|
199
|
+
image_dir = get_step_image_dir(step_index, step["name"])
|
|
200
|
+
image_dirpath = os.path.join(scenario_filepath, image_dir)
|
|
201
|
+
|
|
202
|
+
if step["name"].startswith("#"):
|
|
203
|
+
step["heading_level"] = "h4"
|
|
204
|
+
|
|
205
|
+
if os.path.exists(image_dirpath):
|
|
206
|
+
_, _, image_names = next(os.walk(image_dirpath))
|
|
207
|
+
images = []
|
|
208
|
+
for image_name in image_names:
|
|
209
|
+
words = image_name.split("-", 1)
|
|
210
|
+
index = words[0].strip()
|
|
211
|
+
try:
|
|
212
|
+
# Images with label should have a name in the form:
|
|
213
|
+
# 0000 - This is the image label.png
|
|
214
|
+
label, _ = os.path.splitext(words[1].strip())
|
|
215
|
+
except IndexError:
|
|
216
|
+
# Images with no label should instead look like:
|
|
217
|
+
# 0000.png
|
|
218
|
+
# so we default to the step name in this case.
|
|
219
|
+
label = step["name"]
|
|
220
|
+
|
|
221
|
+
images.append(
|
|
222
|
+
{
|
|
223
|
+
"src": urllib.parse.quote(
|
|
224
|
+
os.path.join(image_dir, image_name)
|
|
225
|
+
),
|
|
226
|
+
"index": index,
|
|
227
|
+
"label": label,
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
step["images"] = sorted(images, key=lambda x: x["index"])
|
|
231
|
+
|
|
232
|
+
if "result" in step:
|
|
233
|
+
if step["result"]["status"] in ["failed", "passed"]:
|
|
234
|
+
timestamp = datetime.fromisoformat(
|
|
235
|
+
step["result"]["timestamp"]
|
|
236
|
+
)
|
|
237
|
+
step["result"]["timestamp"] = timestamp
|
|
238
|
+
|
|
239
|
+
if scenario_started_at is None:
|
|
240
|
+
scenario_started_at = timestamp
|
|
241
|
+
scenario["started_at"] = timestamp
|
|
242
|
+
time_offset = datetime.utcfromtimestamp(
|
|
243
|
+
(timestamp - scenario_started_at).total_seconds()
|
|
244
|
+
)
|
|
245
|
+
step["result"]["time_offset"] = time_offset
|
|
246
|
+
|
|
247
|
+
scenario_duration += step["result"]["duration"]
|
|
248
|
+
|
|
249
|
+
if "error_message" in step["result"] and step["result"][
|
|
250
|
+
"error_message"
|
|
251
|
+
] == [None]:
|
|
252
|
+
step["result"]["error_message"] = [""]
|
|
253
|
+
|
|
254
|
+
if "text" in step and not isinstance(step["text"], list):
|
|
255
|
+
step["text"] = [step["text"]]
|
|
256
|
+
|
|
257
|
+
# prepare by joining into one big chunk here since we can't do it in the Jinja template
|
|
258
|
+
if "text" in step:
|
|
259
|
+
text_indent = " "
|
|
260
|
+
step["text"] = "\n".join(
|
|
261
|
+
[text_indent + '"""']
|
|
262
|
+
+ [f"{text_indent}{x}" for x in step["text"]]
|
|
263
|
+
+ [text_indent + '"""']
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# prepare by joining into one big chunk here since we can't do it in the Jinja template
|
|
267
|
+
if "table" in step:
|
|
268
|
+
step["table"] = format_gherkin_table(
|
|
269
|
+
step["table"]["rows"],
|
|
270
|
+
step["table"]["headings"],
|
|
271
|
+
" ",
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
step_index += 1
|
|
275
|
+
logs_dir = os.path.join(scenario_filepath, "logs")
|
|
276
|
+
|
|
277
|
+
if os.path.exists(logs_dir):
|
|
278
|
+
log_files = []
|
|
279
|
+
for log_file in glob.iglob(os.path.join(logs_dir, "*.*")):
|
|
280
|
+
if show_status:
|
|
281
|
+
print("l", end="", flush=True)
|
|
282
|
+
log_filepath = log_file.removeprefix(
|
|
283
|
+
f"{scenario_filepath}/"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if ".console." in log_filepath and scenario_started_at:
|
|
287
|
+
log_filepath += ".html"
|
|
288
|
+
|
|
289
|
+
log_files.append(
|
|
290
|
+
{
|
|
291
|
+
"filepath": log_filepath,
|
|
292
|
+
"name": os.path.basename(log_file),
|
|
293
|
+
}
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
scenario["logs"] = log_files
|
|
297
|
+
|
|
298
|
+
scenario["total_steps"] = total_steps
|
|
299
|
+
if scenario_started_at is None:
|
|
300
|
+
scenario["started_at"] = ""
|
|
301
|
+
else:
|
|
302
|
+
if feature_started_at is None:
|
|
303
|
+
feature_started_at = scenario_started_at
|
|
304
|
+
feature["started_at"] = feature_started_at
|
|
305
|
+
|
|
306
|
+
scenario["time_offset"] = datetime.utcfromtimestamp(
|
|
307
|
+
(scenario_started_at - feature_started_at).total_seconds()
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
for log_file in [
|
|
311
|
+
x for x in log_files if ".console." in x["name"]
|
|
312
|
+
]:
|
|
313
|
+
if show_status:
|
|
314
|
+
print("c", end="", flush=True)
|
|
315
|
+
|
|
316
|
+
log_file_filepath = os.path.join(
|
|
317
|
+
scenario_filepath, "logs", log_file["name"]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
input_file = Path(log_file_filepath)
|
|
321
|
+
output_file = Path(log_file_filepath + ".html")
|
|
322
|
+
output_file.write_text(
|
|
323
|
+
parse_log_to_html(
|
|
324
|
+
input_file.read_text(encoding="utf-8")
|
|
325
|
+
),
|
|
326
|
+
encoding="utf-8",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
scenario["duration"] = left_pad_zeroes(scenario_duration)
|
|
330
|
+
feature_duration += scenario_duration
|
|
331
|
+
|
|
332
|
+
if feature_started_at is None:
|
|
333
|
+
feature["started_at"] = ""
|
|
334
|
+
|
|
335
|
+
feature["total_steps"] = sum([x["total_steps"] for x in scenarios])
|
|
336
|
+
feature["duration"] = left_pad_zeroes(
|
|
337
|
+
sum([float(x["duration"]) for x in scenarios])
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
feature["total_scenarios"] = total_scenarios
|
|
341
|
+
feature["total_scenarios_passed"] = total_scenarios_passed
|
|
342
|
+
feature["total_scenarios_failed"] = total_scenarios_failed
|
|
343
|
+
feature["total_scenarios_skipped"] = total_scenarios_skipped
|
|
344
|
+
feature["total_scenarios_errored"] = total_scenarios_errored
|
|
345
|
+
|
|
346
|
+
keys = [
|
|
347
|
+
"total_scenarios",
|
|
348
|
+
"total_scenarios_passed",
|
|
349
|
+
"total_scenarios_failed",
|
|
350
|
+
"total_scenarios_skipped",
|
|
351
|
+
"total_scenarios_errored",
|
|
352
|
+
"duration",
|
|
353
|
+
]
|
|
354
|
+
grand_totals = {}
|
|
355
|
+
for k in keys:
|
|
356
|
+
grand_totals[k] = sum([float(x[k]) for x in reported_features])
|
|
357
|
+
|
|
358
|
+
package_loader = jinja2.PackageLoader("cucu.reporter", "templates")
|
|
359
|
+
templates = jinja2.Environment(loader=package_loader) # nosec
|
|
360
|
+
if show_status:
|
|
361
|
+
print("") # add a newline to end status
|
|
362
|
+
|
|
363
|
+
def urlencode(string):
|
|
364
|
+
"""
|
|
365
|
+
handles encoding specific characters in the names of features/scenarios
|
|
366
|
+
so they can be used in a URL. NOTICE: we're not handling spaces since
|
|
367
|
+
the browser handles those already.
|
|
368
|
+
|
|
369
|
+
"""
|
|
370
|
+
return (
|
|
371
|
+
string.replace('"', "%22").replace("'", "%27").replace("#", "%23")
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
templates.globals.update(escape=escape, urlencode=urlencode)
|
|
375
|
+
|
|
376
|
+
index_template = templates.get_template("index.html")
|
|
377
|
+
rendered_index_html = index_template.render(
|
|
378
|
+
features=reported_features,
|
|
379
|
+
grand_totals=grand_totals,
|
|
380
|
+
title="Cucu HTML Test Report",
|
|
381
|
+
basepath=basepath,
|
|
382
|
+
dir_depth="",
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
index_output_filepath = os.path.join(basepath, "index.html")
|
|
386
|
+
with open(index_output_filepath, "wb") as output:
|
|
387
|
+
output.write(rendered_index_html.encode("utf8"))
|
|
388
|
+
|
|
389
|
+
flat_template = templates.get_template("flat.html")
|
|
390
|
+
rendered_flat_html = flat_template.render(
|
|
391
|
+
features=reported_features,
|
|
392
|
+
grand_totals=grand_totals,
|
|
393
|
+
title="Flat HTML Test Report",
|
|
394
|
+
basepath=basepath,
|
|
395
|
+
dir_depth="",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
flat_output_filepath = os.path.join(basepath, "flat.html")
|
|
399
|
+
with open(flat_output_filepath, "wb") as output:
|
|
400
|
+
output.write(rendered_flat_html.encode("utf8"))
|
|
401
|
+
|
|
402
|
+
feature_template = templates.get_template("feature.html")
|
|
403
|
+
|
|
404
|
+
for feature in reported_features:
|
|
405
|
+
feature_basepath = os.path.join(basepath, feature["folder_name"])
|
|
406
|
+
os.makedirs(feature_basepath, exist_ok=True)
|
|
407
|
+
|
|
408
|
+
scenarios = []
|
|
409
|
+
if feature["status"] != "untested" and "elements" in feature:
|
|
410
|
+
scenarios = feature["elements"]
|
|
411
|
+
|
|
412
|
+
rendered_feature_html = feature_template.render(
|
|
413
|
+
feature=feature,
|
|
414
|
+
scenarios=scenarios,
|
|
415
|
+
dir_depth="",
|
|
416
|
+
title=feature.get("name", "Cucu results"),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
feature_output_filepath = os.path.join(
|
|
420
|
+
basepath, f'{feature["name"]}.html'
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
with open(feature_output_filepath, "wb") as output:
|
|
424
|
+
output.write(rendered_feature_html.encode("utf8"))
|
|
425
|
+
|
|
426
|
+
scenario_template = templates.get_template("scenario.html")
|
|
427
|
+
|
|
428
|
+
for scenario in scenarios:
|
|
429
|
+
steps = scenario["steps"]
|
|
430
|
+
scenario_basepath = os.path.join(
|
|
431
|
+
feature_basepath, scenario["folder_name"]
|
|
432
|
+
)
|
|
433
|
+
os.makedirs(scenario_basepath, exist_ok=True)
|
|
434
|
+
|
|
435
|
+
scenario_output_filepath = os.path.join(
|
|
436
|
+
scenario_basepath, "index.html"
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
rendered_scenario_html = scenario_template.render(
|
|
440
|
+
basepath=results,
|
|
441
|
+
feature=feature,
|
|
442
|
+
path_exists=os.path.exists,
|
|
443
|
+
scenario=scenario,
|
|
444
|
+
steps=steps,
|
|
445
|
+
title=scenario.get("name", "Cucu results"),
|
|
446
|
+
dir_depth="../../",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
with open(scenario_output_filepath, "wb") as output:
|
|
450
|
+
output.write(rendered_scenario_html.encode("utf8"))
|
|
451
|
+
|
|
452
|
+
return os.path.join(basepath, "flat.html")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
{% block nav %}
|
|
3
|
+
<nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top">
|
|
4
|
+
<div class="container-fluid">
|
|
5
|
+
<a class="navbar-brand" href="#">Feature HTML Test Report</a>
|
|
6
|
+
|
|
7
|
+
<div class="collapse navbar-collapse" id="navbarTogglerDemo02">
|
|
8
|
+
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
|
9
|
+
<li class="nav-item active">
|
|
10
|
+
<a class="nav-link" href="index.html" title="go to index report">Index</a>
|
|
11
|
+
</li>
|
|
12
|
+
<li class="align-middle">
|
|
13
|
+
<div style="padding: 8px">Started {{ feature['started_at'] }} for {{ feature['duration'] }}s</div>
|
|
14
|
+
</li>
|
|
15
|
+
</ul>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</nav>
|
|
19
|
+
{% endblock %}
|
|
20
|
+
{% block content %}
|
|
21
|
+
<table class="table table-hover">
|
|
22
|
+
<tr class="remove-table-hover"><span style="display: inline; color: mediumturquoise">{{ feature['tags'] }}</span><br/></tr>
|
|
23
|
+
<tr class="remove-table-hover">
|
|
24
|
+
<span style="display: inline; color: maroon">Feature: </span><span style="display: inline; color: grey" title="{{ escape(feature['name']) }}">{{ escape(feature['name']) }}</span>
|
|
25
|
+
<br/>
|
|
26
|
+
<span>Status: </span>
|
|
27
|
+
{% if feature['status'] == 'passed' %}
|
|
28
|
+
<span style="display: inline; color: green">{{ feature['status'] }}</span>
|
|
29
|
+
{% elif (feature['status'] == 'failed') or (feature['status'] == 'errored') %}
|
|
30
|
+
<span style="display: inline; color: red">{{ feature['status'] }}</span>
|
|
31
|
+
{% elif feature['status'] == 'skipped' %}
|
|
32
|
+
<span style="display: inline; color: blue">{{ feature['status'] }}</span>
|
|
33
|
+
{% elif feature['status'] == 'untested' %}
|
|
34
|
+
<span style="display: inline; color: gray">{{ feature['status'] }}</span>
|
|
35
|
+
{% endif %}
|
|
36
|
+
</tr>
|
|
37
|
+
</table>
|
|
38
|
+
<table class="table table-hover datatable">
|
|
39
|
+
<thead>
|
|
40
|
+
<tr class="align-text-top">
|
|
41
|
+
<th class="text-center">Offset</th>
|
|
42
|
+
<th>Scenario</th>
|
|
43
|
+
<th class="text-center">Steps</th>
|
|
44
|
+
<th class="text-center">Status</th>
|
|
45
|
+
<th class="text-center">Duration (s)</th>
|
|
46
|
+
</tr>
|
|
47
|
+
</thead>
|
|
48
|
+
{% for scenario in scenarios %}
|
|
49
|
+
<!--- ignore Backgrounds for the time being -->
|
|
50
|
+
{% if scenario['keyword'] != 'Background' %}
|
|
51
|
+
<tr>
|
|
52
|
+
<td class="text-center">
|
|
53
|
+
{% if scenario['time_offset'] %}
|
|
54
|
+
{{ scenario['time_offset'].strftime("%H:%M:%S") }}
|
|
55
|
+
{% endif %}
|
|
56
|
+
</td>
|
|
57
|
+
<td>
|
|
58
|
+
<a href="{{ urlencode(escape(feature['folder_name'])) }}/{{ urlencode(escape(scenario['folder_name'])) }}/index.html"><span style="display: inline; color: grey">{{ escape(scenario['name']) }}</span></a><br/>
|
|
59
|
+
<span style="display: inline; color: darkslateblue">{{ scenario['tags'] }}</span>
|
|
60
|
+
</td>
|
|
61
|
+
<td class="text-center">{{ scenario['total_steps'] }}</td>
|
|
62
|
+
<td class="text-center"><span class="status-{{ scenario['status'] }}">{{ scenario['status'] }}</span></td>
|
|
63
|
+
<td class="text-center">{{ scenario['duration'] }}</td>
|
|
64
|
+
</tr>
|
|
65
|
+
{% endif %}
|
|
66
|
+
{% endfor %}
|
|
67
|
+
</table>
|
|
68
|
+
|
|
69
|
+
<script>
|
|
70
|
+
setupReportTables([[1, 'asc']], [{type: 'timestamp', searchable: false}, {type: 'string'}, {type: 'num', searchable: false}, {type: 'html'} , {type: 'timestamp', searchable: false} ])
|
|
71
|
+
</script>
|
|
72
|
+
{% endblock %}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
{% block nav %}
|
|
3
|
+
<nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top">
|
|
4
|
+
<div class="container-fluid">
|
|
5
|
+
<a class="navbar-brand" href="#">Flat HTML Test Report</a>
|
|
6
|
+
<div class="collapse navbar-collapse">
|
|
7
|
+
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
|
8
|
+
<li class="nav-item active">
|
|
9
|
+
<a class="nav-link" href="index.html" title="go to all report">Index</a>
|
|
10
|
+
</li>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</nav>
|
|
15
|
+
{% endblock %}
|
|
16
|
+
{% block content %}
|
|
17
|
+
<table class="table table-hover datatable">
|
|
18
|
+
<thead>
|
|
19
|
+
<tr class="align-text-top">
|
|
20
|
+
<th class="text-center">Started at</th>
|
|
21
|
+
<th>Feature</th>
|
|
22
|
+
<th>Scenario</th>
|
|
23
|
+
<th class="text-center">Total Steps</th>
|
|
24
|
+
<th class="text-center">Status</th>
|
|
25
|
+
<th class="text-center">Duration (s)</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
{% for feature in features %}
|
|
29
|
+
{% for scenario in feature["elements"] %}
|
|
30
|
+
<!--- ignore Backgrounds for the time being -->
|
|
31
|
+
{% if scenario['keyword'] != 'Background' %}
|
|
32
|
+
<tr>
|
|
33
|
+
<td class="text-center">{{ scenario['started_at'] }}</td>
|
|
34
|
+
<td><a href="{{ urlencode(escape(feature['name'])) }}.html"><span>{{ escape(feature['name']) }}</span></a><br>{{ feature['tags'] }}</td>
|
|
35
|
+
<td><a href="{{ urlencode(escape(feature['folder_name'])) }}/{{ urlencode(escape(scenario['folder_name'])) }}/index.html"><span>{{ escape(scenario['name']) }}</span></a><br>{{ scenario['tags'] }}</td>
|
|
36
|
+
<td class="text-center">{{ scenario['total_steps'] }}</td>
|
|
37
|
+
<td class="text-center"><span class="status-{{ scenario['status'] }}">{{ scenario['status'] }}</span></td>
|
|
38
|
+
<td class="text-center">{{ scenario['duration'] }}</td>
|
|
39
|
+
</tr>
|
|
40
|
+
{% endif %}
|
|
41
|
+
{% endfor %}
|
|
42
|
+
{% endfor %}
|
|
43
|
+
</table>
|
|
44
|
+
|
|
45
|
+
<script>
|
|
46
|
+
setupReportTables([[2, 'asc']], [{type: 'timestamp', searchable: false}, {type: 'string'}, {type: 'string'}, {type: 'num', searchable: false}, {type: 'html'} , {type: 'timestamp', searchable: false} ])
|
|
47
|
+
</script>
|
|
48
|
+
{% endblock %}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
{% block nav %}
|
|
3
|
+
<nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top">
|
|
4
|
+
<div class="container-fluid">
|
|
5
|
+
<a class="navbar-brand" href="#">Index HTML Test Report</a>
|
|
6
|
+
<div class="collapse navbar-collapse">
|
|
7
|
+
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
|
|
8
|
+
<li class="nav-item active">
|
|
9
|
+
<a class="nav-link" href="flat.html" title="go to all report">Flat</a>
|
|
10
|
+
</li>
|
|
11
|
+
</ul>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
</nav>
|
|
15
|
+
{% endblock %}
|
|
16
|
+
{% block content %}
|
|
17
|
+
<table class="table table-hover datatable">
|
|
18
|
+
<thead>
|
|
19
|
+
<tr class="align-text-top">
|
|
20
|
+
<th class="text-center">Started at</th>
|
|
21
|
+
<th>Feature</th>
|
|
22
|
+
<th class="text-center">Total<br/>{{ grand_totals['total_scenarios'] }}</th>
|
|
23
|
+
<th class="text-center">Passed<br/>{{ grand_totals['total_scenarios_passed'] }}</th>
|
|
24
|
+
<th class="text-center">Failed<br/>{{ grand_totals['total_scenarios_failed'] }}</th>
|
|
25
|
+
<th class="text-center">Skipped<br/>{{ grand_totals['total_scenarios_skipped'] }}</th>
|
|
26
|
+
<th class="text-center">Errored<br/>{{ grand_totals['total_scenarios_errored'] }}</th>
|
|
27
|
+
<th class="text-center">Status<br/> </th>
|
|
28
|
+
<th class="text-center">Duration (s)<br/>{{ '{:.3f}'.format(grand_totals['duration']) }}s</th>
|
|
29
|
+
</tr>
|
|
30
|
+
</thead>
|
|
31
|
+
{% for feature in features %}
|
|
32
|
+
<tr>
|
|
33
|
+
<td class="text-center">{{ feature['started_at'] }}</td>
|
|
34
|
+
<td><a href="{{ urlencode(escape(feature['name'])) }}.html">{{ escape(feature['name']) }}</a></td>
|
|
35
|
+
<td class="text-center">{{ feature['total_scenarios'] }}</td>
|
|
36
|
+
<td class="text-center">{{ feature['total_scenarios_passed'] }}</td>
|
|
37
|
+
<td class="text-center">{{ feature['total_scenarios_failed'] }}</td>
|
|
38
|
+
<td class="text-center">{{ feature['total_scenarios_skipped'] }}</td>
|
|
39
|
+
<td class="text-center">{{ feature['total_scenarios_errored'] }}</td>
|
|
40
|
+
<td class="text-center"><span class="status-{{ feature['status'] }}">{{ feature['status'] }}</span></td>
|
|
41
|
+
<td class="text-center">{{ feature['duration'] }}</td>
|
|
42
|
+
</tr>
|
|
43
|
+
{% endfor %}
|
|
44
|
+
</table>
|
|
45
|
+
|
|
46
|
+
<script>
|
|
47
|
+
setupReportTables([[6, 'asc']], [{type: 'timestamp', searchable: false}, {type: 'string'}, {type: 'num', searchable: false}, {type: 'num', searchable: false}, {type: 'num', searchable: false}, {type: 'num', searchable: false}, {type: 'num', searchable: false}, {type: 'html'} , {type: 'timestamp', searchable: false} ])
|
|
48
|
+
</script>
|
|
49
|
+
{% endblock %}
|