cucu 1.3.16__py3-none-any.whl → 1.3.18__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/reporter/html.py CHANGED
@@ -1,14 +1,12 @@
1
- import glob
2
- import os
3
1
  import shutil
4
2
  import sys
5
3
  import traceback
6
- import urllib
7
- from datetime import datetime
4
+ from datetime import datetime, timezone
8
5
  from pathlib import Path
9
6
  from xml.sax.saxutils import escape as escape_
10
7
 
11
8
  import jinja2
9
+ from playhouse import shortcuts
12
10
 
13
11
  import cucu.db as db
14
12
  from cucu import format_gherkin_table, logger
@@ -16,8 +14,6 @@ from cucu.ansi_parser import parse_log_to_html
16
14
  from cucu.config import CONFIG
17
15
  from cucu.utils import (
18
16
  ellipsize_filename,
19
- generate_short_id,
20
- get_step_image_dir,
21
17
  )
22
18
 
23
19
 
@@ -52,387 +48,247 @@ def process_tags(element):
52
48
  element["tags"] = " ".join(prepared_tags)
53
49
 
54
50
 
55
- # function to left pad duration with '0' for better alphabetical sorting in html reports.
51
+ def urlencode(string):
52
+ """
53
+ handles encoding specific characters in the names of features/scenarios
54
+ so they can be used in a URL. NOTICE: we're not handling spaces since
55
+ the browser handles those already.
56
+
57
+ """
58
+ return string.replace('"', "%22").replace("'", "%27").replace("#", "%23")
59
+
60
+
56
61
  def left_pad_zeroes(elapsed_time):
62
+ """left pad duration with '0' for better alphabetical sorting in html reports"""
57
63
  int_decimal = str(round(elapsed_time, 3)).split(".")
58
64
  int_decimal[0] = int_decimal[0].zfill(3)
59
65
  padded_duration = ".".join(int_decimal)
60
66
  return padded_duration
61
67
 
62
68
 
63
- def generate(results, basepath, only_failures=False):
64
- """
65
- generate an HTML report for the results provided.
66
- """
69
+ def browser_timestamp_to_datetime(value):
70
+ """Convert a browser timestamp (in milliseconds since epoch) to a datetime object"""
71
+ try:
72
+ timestamp_sec = int(value) / 1000.0
73
+ return datetime.fromtimestamp(timestamp_sec).strftime(
74
+ "%Y-%m-%d %H:%M:%S,%f"
75
+ )[:-3]
76
+ except (ValueError, TypeError):
77
+ return None
78
+
79
+
80
+ def step_text_list_to_html(text):
81
+ """Convert a list of step text lines to an indented HTML heredoc format"""
82
+ text_indent = " " * 8
83
+ heredoc_quote = '"""'
84
+ return "\n".join(
85
+ [text_indent + heredoc_quote]
86
+ + [f"{text_indent}{x}" for x in text]
87
+ + [text_indent + heredoc_quote]
88
+ )
67
89
 
68
- features = []
69
90
 
70
- db_path = os.path.join(results, "run.db")
91
+ def step_table_to_html(table_data):
92
+ """Convert a step table data structure to an indented HTML table format"""
93
+ text_indent = " " * 8
94
+ return format_gherkin_table(
95
+ table_data["rows"],
96
+ table_data["headings"],
97
+ text_indent,
98
+ )
99
+
100
+
101
+ def generate(results: Path, basepath: Path):
102
+ ## Jinja2 templates setup
103
+ package_loader = jinja2.PackageLoader("cucu.reporter", "templates")
104
+ templates = jinja2.Environment(loader=package_loader) # nosec
105
+ templates.globals.update(
106
+ escape=escape,
107
+ urlencode=urlencode,
108
+ browser_timestamp_to_datetime=browser_timestamp_to_datetime,
109
+ step_text_list_to_html=step_text_list_to_html,
110
+ step_table_to_html=step_table_to_html,
111
+ )
112
+ feature_template = templates.get_template("feature.html")
113
+ scenario_template = templates.get_template("scenario.html")
114
+
115
+ ## prepare report directory
116
+ cucu_dir = Path(sys.modules["cucu"].__file__).parent
117
+ external_dir = cucu_dir / "reporter/external"
118
+ shutil.copytree(external_dir, basepath / "external")
119
+ shutil.copyfile(
120
+ cucu_dir / "reporter/favicon.png",
121
+ basepath / "favicon.png",
122
+ )
123
+
124
+ CONFIG.snapshot()
125
+
126
+ db_path = results / "run.db"
71
127
  try:
72
128
  db.init_html_report_db(db_path)
73
- features = []
74
129
 
75
- db_features = db.feature.select().order_by(db.feature.start_at)
130
+ feature_count = db.feature.select().count()
131
+ scenario_count = db.scenario.select().count()
132
+ step_count = db.step.select().count()
76
133
  logger.info(
77
- f"Starting to process {len(db_features)} features for report"
134
+ f"Starting to process {feature_count} features, {scenario_count} scenarios, and {step_count} steps for report"
78
135
  )
79
136
 
137
+ db_features = db.feature.select().order_by(db.feature.start_at)
138
+
139
+ features = []
80
140
  for db_feature in db_features:
141
+ if db_feature.status == "untested":
142
+ logger.debug(f"Skipping untested feature: {db_feature.name}")
143
+ continue
144
+
145
+ feature_dict = shortcuts.model_to_dict(db_feature, backrefs=True)
146
+ features.append(feature_dict)
147
+
81
148
  feature_results_dir = results
82
149
  if db_path := db_feature.worker.cucu_run.db_path:
83
- feature_results_dir = os.path.dirname(db_path)
84
-
85
- feature_dict = {
86
- "name": db_feature.name,
87
- "filename": db_feature.filename,
88
- "description": db_feature.description,
89
- "tags": db_feature.tags if db_feature.tags else [],
90
- "status": db_feature.status,
91
- "elements": [],
92
- "results_dir": feature_results_dir,
93
- }
94
-
95
- db_scenarios = (
96
- db.scenario.select()
97
- .where(db.scenario.feature_run_id == db_feature.feature_run_id)
98
- .order_by(db.scenario.seq)
99
- )
150
+ logger.debug(
151
+ f"Combining cucu_runs, using db_path from worker: {db_path}"
152
+ )
153
+ feature_results_dir = Path(db_path).parent
100
154
 
101
- feature_has_failures = False
155
+ feature_dict["results_dir"] = feature_results_dir
156
+ feature_dict["folder_name"] = ellipsize_filename(db_feature.name)
157
+ feature_dict["duration"] = (
158
+ feature_dict["start_at"] - feature_dict["start_at"]
159
+ ).total_seconds()
102
160
 
103
- if len(db_scenarios) == 0:
104
- logger.debug(f"Feature {db_feature.name} has no scenarios")
105
- continue
161
+ process_tags(feature_dict)
106
162
 
107
- for db_scenario in db_scenarios:
108
- scenario_dict = {
109
- "name": db_scenario.name,
110
- "line": db_scenario.line_number,
111
- "tags": db_scenario.tags if db_scenario.tags else [],
112
- "status": db_scenario.status or "passed",
113
- "steps": [],
114
- }
115
-
116
- if db_scenario.status == "failed":
117
- feature_has_failures = True
118
-
119
- db_steps = (
120
- db.step.select()
121
- .where(
122
- db.step.scenario_run_id == db_scenario.scenario_run_id
123
- )
124
- .order_by(db.step.seq)
163
+ feature_path = basepath / feature_dict["folder_name"]
164
+
165
+ if feature_dict["status"] not in ["skipped", "untested"]:
166
+ # copy each feature directories contents over to the report directory
167
+ src_feature_filepath = (
168
+ Path(feature_dict["results_dir"])
169
+ / feature_dict["folder_name"]
125
170
  )
126
171
 
127
- for db_step in db_steps:
128
- step_dict = {
129
- "keyword": db_step.keyword,
130
- "name": db_step.name,
131
- "result": {
132
- "status": db_step.status or "passed",
133
- "duration": db_step.duration or 0,
134
- "timestamp": db_step.end_at or "",
135
- },
136
- "substep": db_step.is_substep,
137
- "screenshots": db_step.screenshots,
138
- }
139
-
140
- if db_step.text:
141
- step_dict["text"] = db_step.text
142
-
143
- if db_step.table_data:
144
- step_dict["table"] = db_step.table_data
145
-
146
- step_dict["result"]["error_message"] = (
147
- db_step.error_message.splitlines()
148
- if db_step.error_message
149
- else []
172
+ if src_feature_filepath.exists():
173
+ shutil.copytree(
174
+ src_feature_filepath,
175
+ feature_path,
176
+ dirs_exist_ok=True,
150
177
  )
151
- step_dict["result"]["exception"] = db_step.exception
152
- step_dict["result"]["stdout"] = db_step.stdout
153
- step_dict["result"]["stderr"] = db_step.stderr
154
- step_dict["result"]["browser_logs"] = (
155
- db_step.browser_logs.splitlines()
156
- )
157
- step_dict["result"]["debug_output"] = (
158
- db_step.debug_output.splitlines()
178
+ else:
179
+ logger.warning(
180
+ f"Feature directory not found, skipping copy: {src_feature_filepath}"
159
181
  )
160
182
 
161
- scenario_dict["steps"].append(step_dict)
162
-
163
- feature_dict["elements"].append(scenario_dict)
183
+ db_scenarios = db_feature.scenarios.select().order_by(
184
+ db.scenario.seq
185
+ )
164
186
 
165
- if feature_has_failures:
166
- feature_dict["status"] = "failed"
167
- elif only_failures and not feature_has_failures:
187
+ if len(db_scenarios) == 0:
188
+ logger.debug(f"Feature {db_feature.name} has no scenarios")
168
189
  continue
169
190
 
170
- features.append(feature_dict)
171
-
172
- finally:
173
- db.close_html_report_db()
174
-
175
- cucu_dir = os.path.dirname(sys.modules["cucu"].__file__)
176
- external_dir = os.path.join(cucu_dir, "reporter", "external")
177
- shutil.copytree(external_dir, os.path.join(basepath, "external"))
178
- shutil.copyfile(
179
- os.path.join(cucu_dir, "reporter", "favicon.png"),
180
- os.path.join(basepath, "favicon.png"),
181
- )
191
+ for scenario_dict in sorted(
192
+ feature_dict["scenarios"], key=lambda x: x["seq"]
193
+ ):
194
+ CONFIG.restore()
182
195
 
183
- #
184
- # augment existing test run data with:
185
- # * features & scenarios with `duration` attribute computed by adding all
186
- # step durations.
187
- # * add `image` attribute to a step if it has an underlying .png image.
188
- #
189
- CONFIG.snapshot()
190
- reported_features = []
191
- for feature in features:
192
- feature["folder_name"] = ellipsize_filename(feature["name"])
193
- scenarios = []
194
-
195
- if feature["status"] != "untested" and "elements" in feature:
196
- scenarios = feature["elements"]
197
-
198
- if only_failures and feature["status"] != "failed":
199
- continue
200
-
201
- feature_duration = 0
202
- total_scenarios = 0
203
- total_scenarios_passed = 0
204
- total_scenarios_failed = 0
205
- total_scenarios_skipped = 0
206
- total_scenarios_errored = 0
207
- feature_started_at = None
208
-
209
- reported_features.append(feature)
210
- process_tags(feature)
211
-
212
- if feature["status"] not in ["skipped", "untested"]:
213
- # copy each feature directories contents over to the report directory
214
- src_feature_filepath = os.path.join(
215
- feature["results_dir"], feature["folder_name"]
216
- )
217
- dst_feature_filepath = os.path.join(
218
- basepath, feature["folder_name"]
219
- )
220
- if os.path.exists(src_feature_filepath):
221
- shutil.copytree(
222
- src_feature_filepath,
223
- dst_feature_filepath,
224
- dirs_exist_ok=True,
196
+ scenario_dict["folder_name"] = ellipsize_filename(
197
+ scenario_dict["name"]
225
198
  )
226
- else:
227
- logger.warning(
228
- f"Feature directory not found, skipping copy: {src_feature_filepath}"
199
+ scenario_filepath = feature_path / scenario_dict["folder_name"]
200
+ scenario_configpath = (
201
+ scenario_filepath / "logs/cucu.config.yaml.txt"
229
202
  )
230
-
231
- for scenario in scenarios:
232
- CONFIG.restore()
233
-
234
- scenario["folder_name"] = ellipsize_filename(scenario["name"])
235
- scenario_filepath = os.path.join(
236
- basepath,
237
- feature["folder_name"],
238
- scenario["folder_name"],
239
- )
240
-
241
- scenario_configpath = os.path.join(
242
- scenario_filepath, "logs", "cucu.config.yaml.txt"
243
- )
244
- if os.path.exists(scenario_configpath):
245
- try:
246
- CONFIG.load(scenario_configpath)
247
- except Exception as e:
248
- logger.warning(
249
- f"Could not reload config: {scenario_configpath}: {e}"
250
- )
251
- else:
252
- logger.info(f"No config to reload: {scenario_configpath}")
253
-
254
- process_tags(scenario)
255
-
256
- sub_headers = []
257
- for handler in CONFIG[
258
- "__CUCU_HTML_REPORT_SCENARIO_SUBHEADER_HANDLER"
259
- ]:
260
- try:
261
- sub_header = handler(scenario, feature)
262
- if sub_header:
263
- sub_headers.append(sub_header)
264
- except Exception:
265
- logger.warning(
266
- f'Exception while trying to run sub_headers hook for scenario: "{scenario["name"]}"\n{traceback.format_exc()}'
267
- )
268
- scenario["sub_headers"] = "<br/>".join(sub_headers)
269
-
270
- scenario_duration = 0
271
- total_scenarios += 1
272
- total_steps = 0
273
-
274
- if "status" not in scenario:
275
- total_scenarios_skipped += 1
276
- elif scenario["status"] == "passed":
277
- total_scenarios_passed += 1
278
- elif scenario["status"] == "failed":
279
- total_scenarios_failed += 1
280
- elif scenario["status"] == "skipped":
281
- total_scenarios_skipped += 1
282
- elif scenario["status"] == "errored":
283
- total_scenarios_errored += 1
284
-
285
- step_index = 0
286
- scenario_started_at = None
287
- for step in scenario["steps"]:
288
- total_steps += 1
289
-
290
- # Handle section headings with different levels (# to ####)
291
- if step["name"].startswith("#"):
292
- # Map the count to the appropriate HTML heading (h2-h5)
293
- # We use h2-h5 instead of h1-h4 so h1 can be reserved for scenario/feature titles
294
- step["heading_level"] = (
295
- f"h{step['name'][:4].count('#') + 1}"
203
+ scenario_dict["total_steps"] = len(scenario_dict["steps"])
204
+ if scenario_dict["start_at"]:
205
+ offset_seconds = (
206
+ scenario_dict["start_at"] - feature_dict["start_at"]
207
+ ).total_seconds()
208
+ scenario_dict["time_offset"] = datetime.fromtimestamp(
209
+ offset_seconds, timezone.utc
296
210
  )
297
211
 
298
- images = []
299
- image_dir = get_step_image_dir(step_index, step["name"])
300
- image_dirpath = os.path.join(scenario_filepath, image_dir)
301
- for screenshot_index, screenshot in enumerate(step["screenshots"]):
302
- filename = os.path.split(screenshot["filepath"])[-1]
303
- filepath = os.path.join(image_dirpath, filename)
304
- if not os.path.exists(filepath):
212
+ if not scenario_configpath.exists():
213
+ logger.info(f"No config to reload: {scenario_configpath}")
214
+ else:
215
+ try:
216
+ CONFIG.load(scenario_configpath)
217
+ except Exception as e:
218
+ logger.warning(
219
+ f"Could not reload config: {scenario_configpath}: {e}"
220
+ )
221
+
222
+ process_tags(scenario_dict)
223
+
224
+ sub_headers = []
225
+ for handler in CONFIG[
226
+ "__CUCU_HTML_REPORT_SCENARIO_SUBHEADER_HANDLER"
227
+ ]:
228
+ try:
229
+ sub_header = handler(scenario_dict, feature_dict)
230
+ if sub_header:
231
+ sub_headers.append(sub_header)
232
+ except Exception:
233
+ logger.warning(
234
+ f'Exception while trying to run sub_headers hook for scenario: "{scenario_dict["name"]}"\n{traceback.format_exc()}'
235
+ )
236
+ scenario_dict["sub_headers"] = "<br/>".join(sub_headers)
237
+
238
+ for step_dict in scenario_dict["steps"]:
239
+ # Handle section headings with different levels (# to ####)
240
+ if step_dict["name"].startswith("#"):
241
+ # Map the count to the appropriate HTML heading (h2-h5)
242
+ # We use h2-h5 instead of h1-h4 so h1 can be reserved for scenario/feature titles
243
+ step_dict["heading_level"] = (
244
+ f"h{step_dict['name'][:4].count('#') + 1}"
245
+ )
246
+
247
+ # process timestamps and time offsets
248
+ if not step_dict["end_at"]:
305
249
  continue
306
- label = screenshot.get("label", step["name"])
307
- highlight = None
308
- if (
309
- screenshot["location"]
310
- and not CONFIG["CUCU_SKIP_HIGHLIGHT_BORDER"]
311
- ):
312
- window_height = screenshot["size"]["height"]
313
- window_width = screenshot["size"]["width"]
314
- try:
315
- highlight = {
316
- "height_ratio": screenshot["location"][
317
- "height"
318
- ]
319
- / window_height,
320
- "width_ratio": screenshot["location"]["width"]
321
- / window_width,
322
- "top_ratio": screenshot["location"]["y"]
323
- / window_height,
324
- "left_ratio": screenshot["location"]["x"]
325
- / window_width,
326
- }
327
- except TypeError:
328
- # If any of the necessary properties is absent,
329
- # then oh well, no highlight this time.
330
- pass
331
- screenshot_id = f"step-img-{screenshot.get("step_run_id", generate_short_id())}-{screenshot_index:0>4}"
332
- images.append(
333
- {
334
- "src": urllib.parse.quote(
335
- os.path.join(image_dir, filename)
336
- ),
337
- "index": screenshot_index,
338
- "label": label,
339
- "id": screenshot_id,
340
- "highlight": highlight,
341
- }
342
- )
343
- step["images"] = sorted(images, key=lambda x: x["index"])
344
-
345
- if "result" in step:
346
- if step["result"]["status"] in ["failed", "passed"]:
347
- if step["result"]["timestamp"]:
348
- timestamp = datetime.fromisoformat(
349
- step["result"]["timestamp"]
350
- )
351
- step["result"]["timestamp"] = timestamp
352
-
353
- if scenario_started_at is None:
354
- scenario_started_at = timestamp
355
- scenario["started_at"] = timestamp
356
- time_offset = datetime.utcfromtimestamp(
357
- (
358
- timestamp - scenario_started_at
359
- ).total_seconds()
360
- )
361
- step["result"]["time_offset"] = time_offset
362
- else:
363
- step["result"]["timestamp"] = ""
364
- step["result"]["time_offset"] = ""
365
-
366
- scenario_duration += step["result"]["duration"]
367
-
368
- if "error_message" in step["result"] and step["result"][
369
- "error_message"
370
- ] == [None]:
371
- step["result"]["error_message"] = [""]
372
-
373
- if "text" in step and not isinstance(step["text"], list):
374
- step["text"] = [step["text"]]
375
-
376
- # prepare by joining into one big chunk here since we can't do it in the Jinja template
377
- if "text" in step:
378
- text_indent = " "
379
- step["text"] = "\n".join(
380
- [text_indent + '"""']
381
- + [f"{text_indent}{x}" for x in step["text"]]
382
- + [text_indent + '"""']
383
- )
384
250
 
385
- # prepare by joining into one big chunk here since we can't do it in the Jinja template
386
- if "table" in step:
387
- step["table"] = format_gherkin_table(
388
- step["table"]["rows"],
389
- step["table"]["headings"],
390
- " ",
391
- )
251
+ if not step_dict["start_at"]:
252
+ step_dict["timestamp"] = ""
253
+ step_dict["time_offset"] = ""
254
+ continue
392
255
 
393
- step_index += 1
394
- logs_dir = os.path.join(scenario_filepath, "logs")
256
+ timestamp = step_dict["start_at"]
257
+ step_dict["timestamp"] = timestamp
395
258
 
396
- log_files = []
397
- if os.path.exists(logs_dir):
398
- for log_file in glob.iglob(os.path.join(logs_dir, "*.*")):
399
- log_filepath = log_file.removeprefix(
400
- f"{scenario_filepath}/"
259
+ time_offset = datetime.fromtimestamp(
260
+ (
261
+ timestamp - scenario_dict["start_at"]
262
+ ).total_seconds(),
263
+ timezone.utc,
401
264
  )
265
+ step_dict["time_offset"] = time_offset
266
+
267
+ logs_path = scenario_filepath / "logs"
402
268
 
403
- if ".console." in log_filepath and scenario_started_at:
404
- log_filepath += ".html"
269
+ log_files = []
270
+ for log_file in logs_path.glob("*.*"):
271
+ log_filepath = log_file.relative_to(scenario_filepath)
272
+
273
+ if (
274
+ scenario_dict["start_at"]
275
+ and ".console." in log_filepath.name
276
+ ):
277
+ log_filepath = Path(f"logs/{log_filepath.name}.html")
405
278
 
406
279
  log_files.append(
407
280
  {
408
281
  "filepath": log_filepath,
409
- "name": os.path.basename(log_file),
282
+ "name": log_file.name,
410
283
  }
411
284
  )
412
285
 
413
- scenario["logs"] = log_files
414
-
415
- scenario["total_steps"] = total_steps
416
- if scenario_started_at is None:
417
- scenario["started_at"] = ""
418
- else:
419
- if feature_started_at is None:
420
- feature_started_at = scenario_started_at
421
- feature["started_at"] = feature_started_at
422
-
423
- scenario["time_offset"] = datetime.utcfromtimestamp(
424
- (scenario_started_at - feature_started_at).total_seconds()
425
- )
426
-
286
+ # generate html version of console log
427
287
  for log_file in [
428
288
  x for x in log_files if ".console." in x["name"]
429
289
  ]:
430
- log_file_filepath = os.path.join(
431
- scenario_filepath, "logs", log_file["name"]
432
- )
433
-
434
- input_file = Path(log_file_filepath)
435
- output_file = Path(log_file_filepath + ".html")
290
+ input_file = scenario_filepath / "logs" / log_file["name"]
291
+ output_file = scenario_filepath / log_file["filepath"]
436
292
  output_file.write_text(
437
293
  parse_log_to_html(
438
294
  input_file.read_text(encoding="utf-8")
@@ -440,126 +296,82 @@ def generate(results, basepath, only_failures=False):
440
296
  encoding="utf-8",
441
297
  )
442
298
 
443
- scenario["duration"] = left_pad_zeroes(scenario_duration)
444
- feature_duration += scenario_duration
445
-
446
- if feature_started_at is None:
447
- feature["started_at"] = ""
448
-
449
- feature["total_steps"] = sum([x["total_steps"] for x in scenarios])
450
- feature["duration"] = left_pad_zeroes(
451
- sum([float(x["duration"]) for x in scenarios])
452
- )
453
-
454
- feature["total_scenarios"] = total_scenarios
455
- feature["total_scenarios_passed"] = total_scenarios_passed
456
- feature["total_scenarios_failed"] = total_scenarios_failed
457
- feature["total_scenarios_skipped"] = total_scenarios_skipped
458
- feature["total_scenarios_errored"] = total_scenarios_errored
459
-
460
- keys = [
461
- "total_scenarios",
462
- "total_scenarios_passed",
463
- "total_scenarios_failed",
464
- "total_scenarios_skipped",
465
- "total_scenarios_errored",
466
- "total_steps",
467
- "duration",
468
- ]
469
- grand_totals = {"total_features": len(reported_features)}
470
- for k in keys:
471
- grand_totals[k] = sum([float(x[k]) for x in reported_features])
472
-
473
- package_loader = jinja2.PackageLoader("cucu.reporter", "templates")
474
- templates = jinja2.Environment(loader=package_loader) # nosec
475
-
476
- def urlencode(string):
477
- """
478
- handles encoding specific characters in the names of features/scenarios
479
- so they can be used in a URL. NOTICE: we're not handling spaces since
480
- the browser handles those already.
481
-
482
- """
483
- return (
484
- string.replace('"', "%22").replace("'", "%27").replace("#", "%23")
485
- )
486
-
487
- templates.globals.update(escape=escape, urlencode=urlencode)
488
-
489
- index_template = templates.get_template("index.html")
490
- rendered_index_html = index_template.render(
491
- features=reported_features,
492
- grand_totals=grand_totals,
493
- title="Cucu HTML Test Report",
494
- basepath=basepath,
495
- dir_depth="",
496
- )
497
-
498
- index_output_filepath = os.path.join(basepath, "index.html")
499
- with open(index_output_filepath, "wb") as output:
500
- output.write(rendered_index_html.encode("utf8"))
501
-
502
- flat_template = templates.get_template("flat.html")
503
- rendered_flat_html = flat_template.render(
504
- features=reported_features,
505
- grand_totals=grand_totals,
506
- title="Flat HTML Test Report",
507
- basepath=basepath,
508
- dir_depth="",
509
- )
510
-
511
- flat_output_filepath = os.path.join(basepath, "flat.html")
512
- with open(flat_output_filepath, "wb") as output:
513
- output.write(rendered_flat_html.encode("utf8"))
514
-
515
- feature_template = templates.get_template("feature.html")
516
-
517
- for feature in reported_features:
518
- feature_basepath = os.path.join(basepath, feature["folder_name"])
519
- os.makedirs(feature_basepath, exist_ok=True)
299
+ scenario_dict["logs"] = log_files
300
+
301
+ # render scenario html
302
+ scenario_basepath = feature_path / scenario_dict["folder_name"]
303
+ scenario_basepath.mkdir(parents=True, exist_ok=True)
304
+ rendered_scenario_html = scenario_template.render(
305
+ basepath=results,
306
+ feature=feature_dict,
307
+ path_exists=lambda path: Path(path).exists(),
308
+ scenario=scenario_dict,
309
+ steps=scenario_dict["steps"],
310
+ title=scenario_dict["name"],
311
+ dir_depth="../../",
312
+ )
313
+ scenario_output_filepath = scenario_basepath / "index.html"
314
+ scenario_output_filepath.write_text(rendered_scenario_html)
315
+
316
+ # render feature html
317
+ rendered_feature_html = feature_template.render(
318
+ feature=feature_dict,
319
+ scenarios=feature_dict["scenarios"],
320
+ dir_depth="",
321
+ title=feature_dict["name"],
322
+ )
323
+ feature_output_filepath = basepath / f"{feature_dict['name']}.html"
324
+ feature_output_filepath.write_text(rendered_feature_html)
520
325
 
521
- scenarios = []
522
- if feature["status"] != "untested" and "elements" in feature:
523
- scenarios = feature["elements"]
326
+ feature_dict["total_steps"] = sum(
327
+ [x["total_steps"] for x in feature_dict["scenarios"]]
328
+ )
329
+ feature_dict["duration"] = left_pad_zeroes(
330
+ sum(
331
+ [
332
+ float(x["duration"])
333
+ for x in feature_dict["scenarios"]
334
+ if x["duration"]
335
+ ]
336
+ )
337
+ )
524
338
 
525
- rendered_feature_html = feature_template.render(
526
- feature=feature,
527
- scenarios=scenarios,
339
+ # query the database for stats
340
+ feature_stats_db = db.db.execute_sql("SELECT * FROM flat_feature")
341
+ keys = tuple([x[0] for x in feature_stats_db.description])
342
+ feature_stats = [
343
+ dict(zip(keys, x)) for x in feature_stats_db.fetchall()
344
+ ]
345
+
346
+ grand_totals_db = db.db.execute_sql("SELECT * FROM flat_all")
347
+ keys = tuple([x[0] for x in grand_totals_db.description])
348
+ grand_totals = dict(zip(keys, grand_totals_db.fetchone()))
349
+
350
+ ## Generate index.html and flat.html
351
+
352
+ index_template = templates.get_template("index.html")
353
+ rendered_index_html = index_template.render(
354
+ feature_stats=feature_stats,
355
+ grand_totals=grand_totals,
356
+ title="Cucu HTML Test Report",
357
+ basepath=basepath,
528
358
  dir_depth="",
529
- title=feature.get("name", "Cucu results"),
530
359
  )
531
-
532
- feature_output_filepath = os.path.join(
533
- basepath, f"{feature['name']}.html"
360
+ html_index_path = basepath / "index.html"
361
+ html_index_path.write_text(rendered_index_html)
362
+
363
+ flat_template = templates.get_template("flat.html")
364
+ rendered_flat_html = flat_template.render(
365
+ features=features,
366
+ grand_totals=grand_totals,
367
+ title="Flat HTML Test Report",
368
+ basepath=basepath,
369
+ dir_depth="",
534
370
  )
371
+ html_flat_path = basepath / "flat.html"
372
+ html_flat_path.write_text(rendered_flat_html)
535
373
 
536
- with open(feature_output_filepath, "wb") as output:
537
- output.write(rendered_feature_html.encode("utf8"))
538
-
539
- scenario_template = templates.get_template("scenario.html")
540
-
541
- for scenario in scenarios:
542
- steps = scenario["steps"]
543
- scenario_basepath = os.path.join(
544
- feature_basepath, scenario["folder_name"]
545
- )
546
- os.makedirs(scenario_basepath, exist_ok=True)
547
-
548
- scenario_output_filepath = os.path.join(
549
- scenario_basepath, "index.html"
550
- )
551
-
552
- rendered_scenario_html = scenario_template.render(
553
- basepath=results,
554
- feature=feature,
555
- path_exists=os.path.exists,
556
- scenario=scenario,
557
- steps=steps,
558
- title=scenario.get("name", "Cucu results"),
559
- dir_depth="../../",
560
- )
561
-
562
- with open(scenario_output_filepath, "wb") as output:
563
- output.write(rendered_scenario_html.encode("utf8"))
374
+ finally:
375
+ db.close_html_report_db()
564
376
 
565
- return os.path.join(basepath, "flat.html")
377
+ return html_flat_path