cucu 1.3.15__py3-none-any.whl → 1.3.20__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.
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,250 @@ 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
  )
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
210
+ )
230
211
 
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
- )
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
+ scenario_dict["steps"] = sorted(
238
+ scenario_dict["steps"], key=lambda x: x["seq"]
239
+ )
240
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}"
296
- )
241
+ for step_dict in scenario_dict["steps"]:
242
+ # Handle section headings with different levels (# to ####)
243
+ if step_dict["name"].startswith("#"):
244
+ # Map the count to the appropriate HTML heading (h2-h5)
245
+ # We use h2-h5 instead of h1-h4 so h1 can be reserved for scenario/feature titles
246
+ step_dict["heading_level"] = (
247
+ f"h{step_dict['name'][:4].count('#') + 1}"
248
+ )
249
+
250
+ # process timestamps and time offsets
251
+ if not step_dict["end_at"]:
252
+ continue
297
253
 
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):
254
+ if not step_dict["start_at"]:
255
+ step_dict["timestamp"] = ""
256
+ step_dict["time_offset"] = ""
305
257
  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
258
 
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
- " ",
259
+ timestamp = step_dict["start_at"]
260
+ step_dict["timestamp"] = timestamp
261
+
262
+ time_offset = datetime.fromtimestamp(
263
+ (
264
+ timestamp - scenario_dict["start_at"]
265
+ ).total_seconds(),
266
+ timezone.utc,
391
267
  )
268
+ step_dict["time_offset"] = time_offset
392
269
 
393
- step_index += 1
394
- logs_dir = os.path.join(scenario_filepath, "logs")
270
+ logs_path = scenario_filepath / "logs"
395
271
 
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}/"
401
- )
272
+ log_files = []
273
+ for log_file in logs_path.glob("*.*"):
274
+ log_filepath = log_file.relative_to(scenario_filepath)
402
275
 
403
- if ".console." in log_filepath and scenario_started_at:
404
- log_filepath += ".html"
276
+ if (
277
+ scenario_dict["start_at"]
278
+ and ".console." in log_filepath.name
279
+ ):
280
+ log_filepath = Path(f"logs/{log_filepath.name}.html")
405
281
 
406
282
  log_files.append(
407
283
  {
408
284
  "filepath": log_filepath,
409
- "name": os.path.basename(log_file),
285
+ "name": log_file.name,
410
286
  }
411
287
  )
412
288
 
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
-
289
+ # generate html version of console log
427
290
  for log_file in [
428
291
  x for x in log_files if ".console." in x["name"]
429
292
  ]:
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")
293
+ input_file = scenario_filepath / "logs" / log_file["name"]
294
+ output_file = scenario_filepath / log_file["filepath"]
436
295
  output_file.write_text(
437
296
  parse_log_to_html(
438
297
  input_file.read_text(encoding="utf-8")
@@ -440,126 +299,82 @@ def generate(results, basepath, only_failures=False):
440
299
  encoding="utf-8",
441
300
  )
442
301
 
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)
302
+ scenario_dict["logs"] = log_files
303
+
304
+ # render scenario html
305
+ scenario_basepath = feature_path / scenario_dict["folder_name"]
306
+ scenario_basepath.mkdir(parents=True, exist_ok=True)
307
+ rendered_scenario_html = scenario_template.render(
308
+ basepath=results,
309
+ feature=feature_dict,
310
+ path_exists=lambda path: Path(path).exists(),
311
+ scenario=scenario_dict,
312
+ steps=scenario_dict["steps"],
313
+ title=scenario_dict["name"],
314
+ dir_depth="../../",
315
+ )
316
+ scenario_output_filepath = scenario_basepath / "index.html"
317
+ scenario_output_filepath.write_text(rendered_scenario_html)
318
+
319
+ # render feature html
320
+ rendered_feature_html = feature_template.render(
321
+ feature=feature_dict,
322
+ scenarios=feature_dict["scenarios"],
323
+ dir_depth="",
324
+ title=feature_dict["name"],
325
+ )
326
+ feature_output_filepath = basepath / f"{feature_dict['name']}.html"
327
+ feature_output_filepath.write_text(rendered_feature_html)
520
328
 
521
- scenarios = []
522
- if feature["status"] != "untested" and "elements" in feature:
523
- scenarios = feature["elements"]
329
+ feature_dict["total_steps"] = sum(
330
+ [x["total_steps"] for x in feature_dict["scenarios"]]
331
+ )
332
+ feature_dict["duration"] = left_pad_zeroes(
333
+ sum(
334
+ [
335
+ float(x["duration"])
336
+ for x in feature_dict["scenarios"]
337
+ if x["duration"]
338
+ ]
339
+ )
340
+ )
524
341
 
525
- rendered_feature_html = feature_template.render(
526
- feature=feature,
527
- scenarios=scenarios,
342
+ # query the database for stats
343
+ feature_stats_db = db.db.execute_sql("SELECT * FROM flat_feature")
344
+ keys = tuple([x[0] for x in feature_stats_db.description])
345
+ feature_stats = [
346
+ dict(zip(keys, x)) for x in feature_stats_db.fetchall()
347
+ ]
348
+
349
+ grand_totals_db = db.db.execute_sql("SELECT * FROM flat_all")
350
+ keys = tuple([x[0] for x in grand_totals_db.description])
351
+ grand_totals = dict(zip(keys, grand_totals_db.fetchone()))
352
+
353
+ ## Generate index.html and flat.html
354
+
355
+ index_template = templates.get_template("index.html")
356
+ rendered_index_html = index_template.render(
357
+ feature_stats=feature_stats,
358
+ grand_totals=grand_totals,
359
+ title="Cucu HTML Test Report",
360
+ basepath=basepath,
528
361
  dir_depth="",
529
- title=feature.get("name", "Cucu results"),
530
362
  )
531
-
532
- feature_output_filepath = os.path.join(
533
- basepath, f"{feature['name']}.html"
363
+ html_index_path = basepath / "index.html"
364
+ html_index_path.write_text(rendered_index_html)
365
+
366
+ flat_template = templates.get_template("flat.html")
367
+ rendered_flat_html = flat_template.render(
368
+ features=features,
369
+ grand_totals=grand_totals,
370
+ title="Flat HTML Test Report",
371
+ basepath=basepath,
372
+ dir_depth="",
534
373
  )
374
+ html_flat_path = basepath / "flat.html"
375
+ html_flat_path.write_text(rendered_flat_html)
535
376
 
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"))
377
+ finally:
378
+ db.close_html_report_db()
564
379
 
565
- return os.path.join(basepath, "flat.html")
380
+ return html_flat_path