cucu 1.3.12__py3-none-any.whl → 1.3.14__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/cli/core.py CHANGED
@@ -1,5 +1,4 @@
1
1
  # -*- coding: utf-8 -*-
2
- import glob
3
2
  import json
4
3
  import os
5
4
  import shutil
@@ -54,7 +53,9 @@ def main():
54
53
 
55
54
 
56
55
  @main.command()
57
- @click.argument("filepath")
56
+ @click.argument(
57
+ "filepath", default="features", type=click.Path(path_type=Path)
58
+ )
58
59
  @click.option(
59
60
  "-b",
60
61
  "--browser",
@@ -110,6 +111,7 @@ def main():
110
111
  default=None,
111
112
  help="specify the output directory for JUnit XML files, default is "
112
113
  "the same location as --results",
114
+ type=click.Path(path_type=Path),
113
115
  )
114
116
  @click.option(
115
117
  "--junit-with-stacktrace",
@@ -150,6 +152,7 @@ def main():
150
152
  "--report",
151
153
  default="report",
152
154
  help="the location to put the test report when --generate-report is used",
155
+ type=click.Path(path_type=Path),
153
156
  )
154
157
  @click.option(
155
158
  "--report-only-failures",
@@ -162,6 +165,7 @@ def main():
162
165
  "--results",
163
166
  default="results",
164
167
  help="the results directory used by cucu",
168
+ type=click.Path(path_type=Path),
165
169
  )
166
170
  @click.option(
167
171
  "--runtime-timeout",
@@ -247,7 +251,7 @@ def run(
247
251
  # when cucu is already running it means that we're running inside
248
252
  # another cucu process and therefore we should make sure the results
249
253
  # directory isn't the default one and throw an exception otherwise
250
- if results == "results":
254
+ if results == Path("results"):
251
255
  raise Exception(
252
256
  "running within cucu but --results was not used, "
253
257
  "this would lead to some very difficult to debug "
@@ -263,10 +267,10 @@ def run(
263
267
  logger.init_logging(logging_level.upper())
264
268
 
265
269
  if not preserve_results:
266
- if os.path.exists(results):
270
+ if results.exists():
267
271
  shutil.rmtree(results)
268
272
 
269
- os.makedirs(results, exist_ok=True)
273
+ results.mkdir(parents=True, exist_ok=True)
270
274
 
271
275
  if selenium_remote_url is not None:
272
276
  os.environ["CUCU_SELENIUM_REMOTE_URL"] = selenium_remote_url
@@ -302,12 +306,15 @@ def run(
302
306
  generate_short_id(worker_id_seed)
303
307
  )
304
308
 
305
- os.environ["CUCU_FILEPATH"] = CONFIG["CUCU_FILEPATH"] = filepath
309
+ os.environ["CUCU_FILEPATH"] = CONFIG["CUCU_FILEPATH"] = str(filepath)
306
310
 
307
311
  create_run(results, filepath)
308
312
 
309
313
  try:
310
314
  if workers is None or workers == 1:
315
+ logger.debug(
316
+ f"Starting cucu_run {CONFIG['CUCU_RUN_ID']} with single worker"
317
+ )
311
318
  if runtime_timeout:
312
319
  logger.debug("setting up runtime timeout timer")
313
320
 
@@ -347,9 +354,11 @@ def run(
347
354
  raise ClickException("test run failed, see above for details")
348
355
 
349
356
  else:
350
- if os.path.isdir(filepath):
351
- basepath = os.path.join(filepath, "**/*.feature")
352
- feature_filepaths = list(glob.iglob(basepath, recursive=True))
357
+ logger.debug(
358
+ f"Starting cucu_run {CONFIG['CUCU_RUN_ID']} with multiple workers: {workers}"
359
+ )
360
+ if filepath.is_dir():
361
+ feature_filepaths = list(filepath.rglob("*.feature"))
353
362
  else:
354
363
  feature_filepaths = [filepath]
355
364
 
@@ -486,8 +495,10 @@ def run(
486
495
  task_failed.update(async_results)
487
496
 
488
497
  if task_failed:
489
- failing_features = "\n".join(task_failed.keys())
490
- logger.error(f"Failing Features:\n{failing_features}")
498
+ failing_features = [str(x) for x in task_failed.keys()]
499
+ logger.error(
500
+ f"Failing Features:\n{'\n'.join(failing_features)}"
501
+ )
491
502
  raise RuntimeError(
492
503
  "there are failures, see above for details"
493
504
  )
@@ -495,60 +506,59 @@ def run(
495
506
  if dumper is not None:
496
507
  dumper.stop()
497
508
 
498
- if os.path.exists(results):
509
+ if results.exists():
499
510
  finish_worker_record(worker_run_id=CONFIG.get("WORKER_PARENT_ID"))
500
511
  consolidate_database_files(results)
501
512
 
502
513
  if generate_report:
503
514
  _generate_report(
504
- results,
505
- report,
515
+ results_dir=results,
516
+ report_folder=report,
506
517
  only_failures=report_only_failures,
507
- junit=junit,
518
+ junit_folder=junit,
508
519
  )
509
520
 
510
521
 
511
522
  def _generate_report(
512
- results_dir: str,
513
- output: str,
523
+ results_dir: Path,
524
+ report_folder: Path,
514
525
  only_failures: False,
515
- junit: str | None = None,
526
+ junit_folder: Path | None = None,
527
+ combine: bool = False,
516
528
  ):
517
- if os.path.exists(output):
518
- shutil.rmtree(output)
529
+ if report_folder.exists():
530
+ shutil.rmtree(report_folder)
519
531
 
520
- os.makedirs(output)
532
+ report_folder.mkdir(parents=True, exist_ok=True)
521
533
 
522
- if os.path.exists(results_dir):
523
- consolidate_database_files(results_dir)
534
+ if results_dir.exists():
535
+ consolidate_database_files(results_dir, combine)
524
536
 
525
537
  report_location = reporter.generate(
526
- results_dir, output, only_failures=only_failures
538
+ results_dir, report_folder, only_failures=only_failures
527
539
  )
528
540
  print(f"HTML test report at {report_location}")
529
541
 
530
- if junit:
531
- _add_report_path_in_junit(junit, output)
532
-
533
-
534
- def _add_report_path_in_junit(junit_folder, report_folder):
535
- for junit_file in glob.glob(f"{junit_folder}/*.xml", recursive=True):
536
- junit = ET.parse(junit_file)
537
- test_suite = junit.getroot()
538
- ts_folder = test_suite.get("foldername")
539
- for test_case in test_suite.iter("testcase"):
540
- report_path = os.path.join(
541
- report_folder,
542
- ts_folder,
543
- test_case.get("foldername"),
544
- "index.html",
545
- )
546
- test_case.set("report_path", report_path)
547
- junit.write(junit_file, encoding="utf-8", xml_declaration=False)
542
+ if junit_folder:
543
+ for junit_file in junit_folder.rglob("*.xml"):
544
+ junit = ET.parse(junit_file)
545
+ test_suite = junit.getroot()
546
+ ts_folder = test_suite.get("foldername")
547
+ for test_case in test_suite.iter("testcase"):
548
+ report_path = os.path.join(
549
+ report_folder,
550
+ ts_folder,
551
+ test_case.get("foldername"),
552
+ "index.html",
553
+ )
554
+ test_case.set("report_path", report_path)
555
+ junit.write(junit_file, encoding="utf-8", xml_declaration=False)
548
556
 
549
557
 
550
558
  @main.command()
551
- @click.argument("results_dir", default="results")
559
+ @click.argument(
560
+ "results_dir", default="results", type=click.Path(path_type=Path)
561
+ )
552
562
  @click.option(
553
563
  "--only-failures",
554
564
  default=False,
@@ -567,21 +577,34 @@ def _add_report_path_in_junit(junit_folder, report_folder):
567
577
  is_flag=True,
568
578
  help="when set skips are shown",
569
579
  )
570
- @click.option("-o", "--output", default="report")
580
+ @click.option(
581
+ "-o",
582
+ "--output",
583
+ default="report",
584
+ type=click.Path(path_type=Path),
585
+ )
571
586
  @click.option(
572
587
  "-j",
573
588
  "--junit",
574
589
  default=None,
575
590
  help="specify the output directory for JUnit XML files, default is "
576
591
  "the same location as --results",
592
+ type=click.Path(path_type=Path),
593
+ )
594
+ @click.option(
595
+ "--combine",
596
+ default=False,
597
+ is_flag=True,
598
+ help="combine multiple cucu_runs into a single report",
577
599
  )
578
600
  def report(
579
- results_dir,
601
+ results_dir: Path,
580
602
  only_failures,
581
603
  logging_level,
582
604
  show_skips,
583
- output,
584
- junit,
605
+ output: Path,
606
+ junit: Path,
607
+ combine: bool,
585
608
  ):
586
609
  """
587
610
  generate a test report from a results directory
@@ -594,7 +617,7 @@ def report(
594
617
  if show_skips:
595
618
  os.environ["CUCU_SHOW_SKIPS"] = "true"
596
619
 
597
- run_details_filepath = os.path.join(results_dir, "run_details.json")
620
+ run_details_filepath = results_dir / "run_details.json"
598
621
 
599
622
  if os.path.exists(run_details_filepath):
600
623
  # load the run details at the time of execution for the provided results
@@ -608,12 +631,18 @@ def report(
608
631
  behave_init(run_details["filepath"])
609
632
 
610
633
  _generate_report(
611
- results_dir, output, only_failures=only_failures, junit=junit
634
+ results_dir=results_dir,
635
+ report_folder=output,
636
+ only_failures=only_failures,
637
+ junit_folder=junit,
638
+ combine=combine,
612
639
  )
613
640
 
614
641
 
615
642
  @main.command()
616
- @click.argument("filepath", default="features")
643
+ @click.argument(
644
+ "filepath", default="features", type=click.Path(path_type=Path)
645
+ )
617
646
  @click.option(
618
647
  "-f",
619
648
  "--format",
@@ -639,7 +668,7 @@ def steps(filepath, format):
639
668
 
640
669
 
641
670
  @main.command()
642
- @click.argument("filepath", nargs=-1)
671
+ @click.argument("filepath", type=click.Path(path_type=Path), nargs=-1)
643
672
  @click.option(
644
673
  "--fix/--no-fix", default=False, help="fix lint violations, default: False"
645
674
  )
@@ -741,7 +770,9 @@ def lsp(logging_level, port):
741
770
 
742
771
 
743
772
  @main.command()
744
- @click.argument("filepath", default="features")
773
+ @click.argument(
774
+ "filepath", default="features", type=click.Path(path_type=Path)
775
+ )
745
776
  def vars(filepath):
746
777
  """
747
778
  print built-in cucu variables
@@ -766,14 +797,14 @@ def vars(filepath):
766
797
 
767
798
 
768
799
  @main.command()
769
- @click.argument("filepath", default="")
800
+ @click.argument("repo_dir", default="", type=click.Path(path_type=Path))
770
801
  @click.option(
771
802
  "-l",
772
803
  "--logging-level",
773
804
  default="INFO",
774
805
  help="set logging level to one of debug, warn or info (default)",
775
806
  )
776
- def init(filepath, logging_level):
807
+ def init(repo_dir, logging_level):
777
808
  """
778
809
  initialize cucu in the current directory
779
810
 
@@ -785,10 +816,9 @@ def init(filepath, logging_level):
785
816
  init_data_dir = Path(__file__).parent.parent / "init_data"
786
817
 
787
818
  logger.debug(f"cucu init: copy example directory from {init_data_dir=}")
788
- repo_dir = filepath if filepath.strip() else os.path.join(os.getcwd())
789
819
 
790
- features_dir = os.path.join(repo_dir, "features")
791
- if os.path.exists(features_dir):
820
+ features_dir = repo_dir / "features"
821
+ if features_dir.exists():
792
822
  answer = input("Overwrite existing files? [y/N]:")
793
823
  if answer.lower() != "y":
794
824
  print("Aborted!")
@@ -874,9 +904,7 @@ def tags(filepath, logging_level):
874
904
  if not filepath.exists() or not feature_files:
875
905
  raise ClickException("No feature files found.")
876
906
 
877
- file_locations = [
878
- FileLocation(os.path.abspath(str(f))) for f in feature_files
879
- ]
907
+ file_locations = [FileLocation(f.absolute()) for f in feature_files]
880
908
  features = parse_features(file_locations)
881
909
  tag_scenarios = Counter()
882
910
 
cucu/cli/run.py CHANGED
@@ -85,8 +85,8 @@ def behave(
85
85
  if debug_on_failure:
86
86
  os.environ["CUCU_DEBUG_ON_FAILURE"] = "true"
87
87
 
88
- os.environ["CUCU_RESULTS_DIR"] = results
89
- os.environ["CUCU_JUNIT_DIR"] = junit
88
+ os.environ["CUCU_RESULTS_DIR"] = str(results)
89
+ os.environ["CUCU_JUNIT_DIR"] = str(junit)
90
90
 
91
91
  if secrets:
92
92
  os.environ["CUCU_SECRETS"] = secrets
@@ -123,7 +123,7 @@ def behave(
123
123
  "--no-logcapture",
124
124
  # generate a JSON file containing the exact details of the whole run
125
125
  "--format=cucu.formatter.json:CucuJSONFormatter",
126
- f"--outfile={Path(results) / run_json_filename}",
126
+ f"--outfile={results / run_json_filename}",
127
127
  # console formatter
128
128
  "--format=cucu.formatter.cucu:CucuFormatter",
129
129
  f"--logging-level={os.environ['CUCU_LOGGING_LEVEL'].upper()}",
@@ -154,7 +154,7 @@ def behave(
154
154
  if redirect_output:
155
155
  feature_name = get_feature_name(filepath)
156
156
  log_filename = f"{feature_name}.log"
157
- log_filepath = Path(results) / log_filename
157
+ log_filepath = results / log_filename
158
158
 
159
159
  CONFIG["__CUCU_PARENT_STDOUT"] = sys.stdout
160
160
 
@@ -185,8 +185,7 @@ def behave(
185
185
  return result
186
186
 
187
187
 
188
- def create_run(results, filepath):
189
- results_path = Path(results)
188
+ def create_run(results_path: Path, filepath: Path):
190
189
  run_json_filepath = results_path / "run_details.json"
191
190
 
192
191
  if run_json_filepath.exists():
@@ -200,7 +199,7 @@ def create_run(results, filepath):
200
199
 
201
200
  run_details = {
202
201
  "cucu_run_id": CONFIG["CUCU_RUN_ID"],
203
- "filepath": filepath,
202
+ "filepath": str(filepath),
204
203
  "full_arguments": sys.argv,
205
204
  "env": env_values,
206
205
  "date": datetime.now().isoformat(),
cucu/cli/steps.py CHANGED
@@ -131,7 +131,7 @@ def print_human_readable_steps(filepath=None):
131
131
 
132
132
  for step_name in steps:
133
133
  if steps[step_name] is not None:
134
- if filepath in steps[step_name]["location"]["filepath"]:
134
+ if str(filepath) in steps[step_name]["location"]["filepath"]:
135
135
  print(f"custom: {step_name}")
136
136
  else:
137
137
  print(f"cucu: {step_name}")
cucu/config.py CHANGED
@@ -3,6 +3,7 @@ import logging
3
3
  import os
4
4
  import re
5
5
  import socket
6
+ from pathlib import Path
6
7
 
7
8
  import yaml
8
9
 
@@ -100,7 +101,7 @@ class Config(dict):
100
101
  else:
101
102
  self[key] = config[key]
102
103
 
103
- def load_cucurc_files(self, filepath):
104
+ def load_cucurc_files(self, filepath: Path):
104
105
  """
105
106
  load in order the ~/.cucurc.yml and then subsequent config files
106
107
  starting from the current working directory to the filepath provided
cucu/db.py CHANGED
@@ -38,16 +38,16 @@ class cucu_run(BaseModel):
38
38
  cucu_run_id = TextField(primary_key=True)
39
39
  full_arguments = JSONField()
40
40
  filepath = TextField()
41
- date = TextField()
42
41
  start_at = DateTimeField()
43
42
  end_at = DateTimeField(null=True)
43
+ db_path = TextField(null=True)
44
+ run_info = JSONField(null=True)
44
45
 
45
46
 
46
47
  class worker(BaseModel):
47
48
  worker_run_id = TextField(primary_key=True)
48
- cucu_run_id = ForeignKeyField(
49
+ cucu_run = ForeignKeyField(
49
50
  cucu_run,
50
- field="cucu_run_id",
51
51
  backref="workers",
52
52
  column_name="cucu_run_id",
53
53
  null=True,
@@ -56,7 +56,7 @@ class worker(BaseModel):
56
56
  "self",
57
57
  field="worker_run_id",
58
58
  backref="child_workers",
59
- column_name="parent_id",
59
+ column_name="parent_run_id",
60
60
  null=True,
61
61
  )
62
62
  start_at = DateTimeField()
@@ -66,9 +66,8 @@ class worker(BaseModel):
66
66
 
67
67
  class feature(BaseModel):
68
68
  feature_run_id = TextField(primary_key=True)
69
- worker_run_id = ForeignKeyField(
69
+ worker = ForeignKeyField(
70
70
  worker,
71
- field="worker_run_id",
72
71
  backref="features",
73
72
  column_name="worker_run_id",
74
73
  )
@@ -84,9 +83,8 @@ class feature(BaseModel):
84
83
 
85
84
  class scenario(BaseModel):
86
85
  scenario_run_id = TextField(primary_key=True)
87
- feature_run_id = ForeignKeyField(
86
+ feature = ForeignKeyField(
88
87
  feature,
89
- field="feature_run_id",
90
88
  backref="scenarios",
91
89
  column_name="feature_run_id",
92
90
  )
@@ -106,9 +104,8 @@ class scenario(BaseModel):
106
104
 
107
105
  class step(BaseModel):
108
106
  step_run_id = TextField(primary_key=True)
109
- scenario_run_id = ForeignKeyField(
107
+ scenario = ForeignKeyField(
110
108
  scenario,
111
- field="scenario_run_id",
112
109
  backref="steps",
113
110
  column_name="scenario_run_id",
114
111
  )
@@ -147,18 +144,21 @@ def record_cucu_run():
147
144
  cucu_run_id=cucu_run_id_val,
148
145
  full_arguments=sys.argv,
149
146
  filepath=filepath,
150
- date=start_at,
151
147
  start_at=start_at,
152
148
  )
153
149
 
150
+ parent_id = (
151
+ CONFIG.get("WORKER_PARENT_ID")
152
+ if CONFIG.get("WORKER_PARENT_ID") != worker_run_id
153
+ else None
154
+ )
154
155
  worker.create(
155
156
  worker_run_id=worker_run_id,
156
157
  cucu_run_id=cucu_run_id_val,
157
- parent_id=CONFIG.get("WORKER_PARENT_ID")
158
- if CONFIG.get("WORKER_PARENT_ID") != worker_run_id
159
- else None,
158
+ parent_id=parent_id,
160
159
  start_at=datetime.now().isoformat(),
161
160
  )
161
+
162
162
  return str(db_filepath)
163
163
 
164
164
 
@@ -166,7 +166,7 @@ def record_feature(feature_obj):
166
166
  db.connect(reuse_if_open=True)
167
167
  feature.create(
168
168
  feature_run_id=feature_obj.feature_run_id,
169
- worker_run_id=CONFIG["WORKER_RUN_ID"],
169
+ worker=CONFIG["WORKER_RUN_ID"],
170
170
  name=feature_obj.name,
171
171
  filename=feature_obj.filename,
172
172
  description="\n".join(feature_obj.description)
@@ -411,16 +411,23 @@ def get_first_cucu_run_filepath():
411
411
  return run_record.filepath
412
412
 
413
413
 
414
- def consolidate_database_files(results_dir):
414
+ def consolidate_database_files(results_dir, combine=False):
415
415
  # This function would need a more advanced approach with peewee, so for now, keep using sqlite3 for consolidation
416
416
  results_path = Path(results_dir)
417
417
  target_db_path = results_path / "run.db"
418
418
  if not target_db_path.exists():
419
419
  create_database_file(target_db_path)
420
420
 
421
- db_files = [
422
- db for db in results_path.glob("**/*.db") if db.name != "run.db"
423
- ]
421
+ if not combine:
422
+ db_files = [
423
+ db for db in results_path.glob("**/run*.db") if db.name != "run.db"
424
+ ]
425
+ else:
426
+ # include all run.db files in all subdirectories
427
+ db_files = [
428
+ db for db in results_path.rglob("run*.db") if db != Path("run.db")
429
+ ]
430
+
424
431
  tables_to_copy = ["cucu_run", "worker", "feature", "scenario", "step"]
425
432
  with sqlite3.connect(target_db_path) as target_conn:
426
433
  target_cursor = target_conn.cursor()
@@ -432,13 +439,28 @@ def consolidate_database_files(results_dir):
432
439
  rows = source_cursor.fetchall()
433
440
  source_cursor.execute(f"PRAGMA table_info({table_name})")
434
441
  columns = [col[1] for col in source_cursor.fetchall()]
442
+
443
+ # prep cucu_run for combining multiple runs
444
+ if table_name == "cucu_run":
445
+ db_path_index = columns.index("db_path")
446
+ rows = [
447
+ tuple(
448
+ item if idx != db_path_index else str(db_file)
449
+ for idx, item in enumerate(row)
450
+ )
451
+ for row in rows
452
+ ]
453
+
435
454
  placeholders = ",".join(["?" for _ in columns])
436
455
  target_cursor.executemany(
437
456
  f"INSERT OR REPLACE INTO {table_name} VALUES ({placeholders})",
438
457
  rows,
439
458
  )
440
459
  target_conn.commit()
441
- db_file.unlink()
460
+
461
+ if not combine and db_file.name != "run.db":
462
+ # remove the worker db files
463
+ db_file.unlink()
442
464
 
443
465
 
444
466
  def init_html_report_db(db_path):
cucu/fuzzy/core.py CHANGED
@@ -107,9 +107,9 @@ def find(
107
107
 
108
108
  fuzzy_return = search_in_all_frames(browser, execute_fuzzy_find)
109
109
  if fuzzy_return is None:
110
- logger.info("Fuzzy found no element.")
110
+ logger.debug("Fuzzy found no element.")
111
111
  return None
112
- logger.info(
112
+ logger.debug(
113
113
  "Fuzzy found element by search term {}".format(fuzzy_return[1])
114
114
  )
115
115
  return fuzzy_return[0]
cucu/reporter/html.py CHANGED
@@ -10,13 +10,10 @@ from xml.sax.saxutils import escape as escape_
10
10
 
11
11
  import jinja2
12
12
 
13
+ import cucu.db as db
13
14
  from cucu import format_gherkin_table, logger
14
15
  from cucu.ansi_parser import parse_log_to_html
15
16
  from cucu.config import CONFIG
16
- from cucu.db import close_html_report_db, init_html_report_db
17
- from cucu.db import feature as FeatureModel
18
- from cucu.db import scenario as ScenarioModel
19
- from cucu.db import step as StepModel
20
17
  from cucu.utils import ellipsize_filename, get_step_image_dir
21
18
 
22
19
 
@@ -68,15 +65,19 @@ def generate(results, basepath, only_failures=False):
68
65
 
69
66
  db_path = os.path.join(results, "run.db")
70
67
  try:
71
- init_html_report_db(db_path)
68
+ db.init_html_report_db(db_path)
72
69
  features = []
73
70
 
74
- db_features = FeatureModel.select().order_by(FeatureModel.start_at)
71
+ db_features = db.feature.select().order_by(db.feature.start_at)
75
72
  logger.info(
76
73
  f"Starting to process {len(db_features)} features for report"
77
74
  )
78
75
 
79
76
  for db_feature in db_features:
77
+ feature_results_dir = results
78
+ if db_path := db_feature.worker.cucu_run.db_path:
79
+ feature_results_dir = os.path.dirname(db_path)
80
+
80
81
  feature_dict = {
81
82
  "name": db_feature.name,
82
83
  "filename": db_feature.filename,
@@ -84,14 +85,13 @@ def generate(results, basepath, only_failures=False):
84
85
  "tags": db_feature.tags if db_feature.tags else [],
85
86
  "status": db_feature.status,
86
87
  "elements": [],
88
+ "results_dir": feature_results_dir,
87
89
  }
88
90
 
89
91
  db_scenarios = (
90
- ScenarioModel.select()
91
- .where(
92
- ScenarioModel.feature_run_id == db_feature.feature_run_id
93
- )
94
- .order_by(ScenarioModel.seq)
92
+ db.scenario.select()
93
+ .where(db.scenario.feature_run_id == db_feature.feature_run_id)
94
+ .order_by(db.scenario.seq)
95
95
  )
96
96
 
97
97
  feature_has_failures = False
@@ -113,12 +113,11 @@ def generate(results, basepath, only_failures=False):
113
113
  feature_has_failures = True
114
114
 
115
115
  db_steps = (
116
- StepModel.select()
116
+ db.step.select()
117
117
  .where(
118
- StepModel.scenario_run_id
119
- == db_scenario.scenario_run_id
118
+ db.step.scenario_run_id == db_scenario.scenario_run_id
120
119
  )
121
- .order_by(StepModel.seq)
120
+ .order_by(db.step.seq)
122
121
  )
123
122
 
124
123
  for db_step in db_steps:
@@ -166,7 +165,7 @@ def generate(results, basepath, only_failures=False):
166
165
  features.append(feature_dict)
167
166
 
168
167
  finally:
169
- close_html_report_db()
168
+ db.close_html_report_db()
170
169
 
171
170
  cucu_dir = os.path.dirname(sys.modules["cucu"].__file__)
172
171
  external_dir = os.path.join(cucu_dir, "reporter", "external")
@@ -208,14 +207,21 @@ def generate(results, basepath, only_failures=False):
208
207
  if feature["status"] not in ["skipped", "untested"]:
209
208
  # copy each feature directories contents over to the report directory
210
209
  src_feature_filepath = os.path.join(
211
- results, feature["folder_name"]
210
+ feature["results_dir"], feature["folder_name"]
212
211
  )
213
212
  dst_feature_filepath = os.path.join(
214
213
  basepath, feature["folder_name"]
215
214
  )
216
- shutil.copytree(
217
- src_feature_filepath, dst_feature_filepath, dirs_exist_ok=True
218
- )
215
+ if os.path.exists(src_feature_filepath):
216
+ shutil.copytree(
217
+ src_feature_filepath,
218
+ dst_feature_filepath,
219
+ dirs_exist_ok=True,
220
+ )
221
+ else:
222
+ logger.warning(
223
+ f"Feature directory not found, skipping copy: {src_feature_filepath}"
224
+ )
219
225
 
220
226
  for scenario in scenarios:
221
227
  CONFIG.restore()
@@ -364,8 +370,8 @@ def generate(results, basepath, only_failures=False):
364
370
  step_index += 1
365
371
  logs_dir = os.path.join(scenario_filepath, "logs")
366
372
 
373
+ log_files = []
367
374
  if os.path.exists(logs_dir):
368
- log_files = []
369
375
  for log_file in glob.iglob(os.path.join(logs_dir, "*.*")):
370
376
  log_filepath = log_file.removeprefix(
371
377
  f"{scenario_filepath}/"
@@ -434,9 +440,10 @@ def generate(results, basepath, only_failures=False):
434
440
  "total_scenarios_failed",
435
441
  "total_scenarios_skipped",
436
442
  "total_scenarios_errored",
443
+ "total_steps",
437
444
  "duration",
438
445
  ]
439
- grand_totals = {}
446
+ grand_totals = {"total_features": len(reported_features)}
440
447
  for k in keys:
441
448
  grand_totals[k] = sum([float(x[k]) for x in reported_features])
442
449
 
@@ -18,11 +18,11 @@
18
18
  <thead>
19
19
  <tr class="align-text-top">
20
20
  <th class="text-center">Started at</th>
21
- <th>Feature</th>
22
- <th>Scenario</th>
23
- <th class="text-center">Total Steps</th>
21
+ <th>Features<br/>{{ grand_totals['total_features'] | int }}</th>
22
+ <th>Scenarios<br/>{{ grand_totals['total_scenarios'] | int }}</th>
23
+ <th class="text-center">Steps<br/>{{ grand_totals['total_steps'] | int }}</th>
24
24
  <th class="text-center">Status</th>
25
- <th class="text-center">Duration (s)</th>
25
+ <th class="text-center">Duration<br/>{{ grand_totals['duration'] | int }}s</th>
26
26
  </tr>
27
27
  </thead>
28
28
  {% for feature in features %}
@@ -18,14 +18,14 @@
18
18
  <thead>
19
19
  <tr class="align-text-top">
20
20
  <th class="text-center">Started at</th>
21
- <th>Feature</th>
22
- <th class="text-center">Total<br/>{{ grand_totals['total_scenarios'] | int }}</th>
21
+ <th>Features<br/>{{ grand_totals['total_features'] | int }}</th>
22
+ <th class="text-center">Scenarios<br/>{{ grand_totals['total_scenarios'] | int }}</th>
23
23
  <th class="text-center">Passed<br/>{{ grand_totals['total_scenarios_passed'] | int}}</th>
24
24
  <th class="text-center">Failed<br/>{{ grand_totals['total_scenarios_failed'] | int }}</th>
25
25
  <th class="text-center">Skipped<br/>{{ grand_totals['total_scenarios_skipped'] | int }}</th>
26
26
  <th class="text-center">Errored<br/>{{ grand_totals['total_scenarios_errored'] | int }}</th>
27
27
  <th class="text-center">Status<br/>&nbsp;</th>
28
- <th class="text-center">Duration (s)<br/>{{ grand_totals['duration'] | int }}s</th>
28
+ <th class="text-center">Duration<br/>{{ grand_totals['duration'] | int }}s</th>
29
29
  </tr>
30
30
  </thead>
31
31
  {% for feature in features %}
@@ -134,30 +134,44 @@
134
134
  <tr class="row"><td style="min-width: 0;" class="col-12 collapse multi-collapse" id="collapsable-row-{{ loop.index }}" colspan="2">
135
135
 
136
136
  {% if step['result']['stdout'] %}
137
- <p>stdout ({{ step['result']['stdout']|length }} lines)</p>
138
- <pre style="color: darkgray; margin: 0;">{{ escape("\n".join(step['result']['stdout'])) }}</pre>
137
+ <details open>
138
+ <summary style="color: dimgray;">stdout ({{ step['result']['stdout']|length }} lines)</summary>
139
+ <pre style="color: darkgray;">{{ escape("\n".join(step['result']['stdout'])) }}</pre>
140
+ </details>
139
141
  {% endif %}
140
142
  {% if step['result']['stderr'] %}
141
- <p>stderr ({{ step['result']['stderr']|length }} lines)</p>
142
- <pre style="color: darkgray; margin: 0;">{{ escape("\n".join(step['result']['stderr'])) }}</pre>
143
+ <details open>
144
+ <summary style="color: dimgray;">stderr ({{ step['result']['stderr']|length }} lines)</summary>
145
+ <pre style="color: darkgray;">{{ escape("\n".join(step['result']['stderr'])) }}</pre>
146
+ </details>
143
147
  {% endif %}
144
148
  {% if step['images'] %}
145
- <p>images ({{ step['images']|length }} images)</p>
146
- {% for image in step['images'] %}
147
- <img class="mx-auto d-block img-fluid shadow bg-white rounded" style="margin-bottom:15px" alt='{{ image["label"] }}' title='{{ image["label"] }}' src='{{ image["src"] }}'></img>
148
- {% endfor %}
149
+ <details open>
150
+ <summary style="color: dimgray;">images ({{ step['images']|length }} images)</summary>
151
+ <div style="margin: 10px 0 0 0;">
152
+ {% for image in step['images'] %}
153
+ <img class="mx-auto d-block img-fluid shadow bg-white rounded" style="margin-bottom:15px" alt='{{ image["label"] }}' title='{{ image["label"] }}' src='{{ image["src"] }}'></img>
154
+ {% endfor %}
155
+ </div>
156
+ </details>
149
157
  {% endif %}
150
158
  {% if step['result']['error_message'] %}
151
- <p>error message ({{ step['result']['error_message']|length }} lines)</p>
152
- <pre style="color: gray; margin: 0">{{ escape("\n".join(step['result']['error_message'])) }}</pre>
159
+ <details open>
160
+ <summary style="color: dimgray;">error message ({{ step['result']['error_message']|length }} lines)</summary>
161
+ <pre style="color: darkgray;">{{ escape("\n".join(step['result']['error_message'])) }}</pre>
162
+ </details>
153
163
  {% endif %}
154
164
  {% if step['result']['browser_logs'] %}
155
- <p>browser logs ({{ step['result']['browser_logs']|length }} lines)</p>
156
- <pre style="color: gray; margin: 0;">{{ escape("\n".join(step['result']['browser_logs'])) }}</pre>
165
+ <details open>
166
+ <summary style="color: dimgray;">browser logs ({{ step['result']['browser_logs']|length }} lines)</summary>
167
+ <pre style="color: darkgray;">{{ escape("\n".join(step['result']['browser_logs'])) }}</pre>
168
+ </details>
157
169
  {% endif %}
158
170
  {% if step['result']['debug_output'] %}
159
- <p>debug output ({{ step['result']['debug_output']|length }} lines)</p>
160
- <pre style="color: gray; margin: 0;">{{ escape("\n".join(step['result']['debug_output'])) }}</pre>
171
+ <details open>
172
+ <summary style="color: dimgray;">debug output ({{ step['result']['debug_output']|length }} lines)</summary>
173
+ <pre style="color: darkgray;">{{ escape("\n".join(step['result']['debug_output'])) }}</pre>
174
+ </details>
161
175
  {% endif %}
162
176
 
163
177
  </td></tr>
cucu/steps/table_steps.py CHANGED
@@ -70,18 +70,15 @@ def check_table_matches_table(table, expected_table):
70
70
  check if table matches the regex patterns in expected table
71
71
  """
72
72
 
73
- if len(table) == len(expected_table):
74
- table_matched = True
75
-
73
+ table_matched = bool(len(table) == len(expected_table))
74
+ if table_matched:
76
75
  for expected_row, row in zip(expected_table, table):
77
76
  for expected_value, value in zip(expected_row, row):
78
77
  if not re.match(expected_value, value):
79
78
  table_matched = False
79
+ break
80
80
 
81
- if table_matched:
82
- return True
83
-
84
- return False
81
+ return table_matched
85
82
 
86
83
 
87
84
  def check_table_contains_table(table, expected_table):
@@ -35,7 +35,7 @@ def run_webserver_for_scenario(ctx, directory, variable):
35
35
  CONFIG[variable] = str(port)
36
36
 
37
37
  with socket.create_connection(("localhost", port), timeout=5):
38
- logger.debug(f"Webserver is running at {port=}port")
38
+ logger.debug(f"Webserver is running at {port=}")
39
39
 
40
40
  def shutdown_webserver(_):
41
41
  httpd.shutdown()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cucu
3
- Version: 1.3.12
3
+ Version: 1.3.14
4
4
  Summary: Easy BDD web testing
5
5
  Keywords: cucumber,selenium,behave
6
6
  Author: Domino Data Lab, Rodney Gomes, Cedric Young, Xin Dong, Kavya Yakkati, Kevin Garton, Joy Liao
@@ -7,12 +7,12 @@ cucu/browser/frames.py,sha256=IW7kzRJn5PkbMaovIelAeCWO-T-2sOTwqaYBw-0-LKU,3545
7
7
  cucu/browser/selenium.py,sha256=eUC2DZkhUIZi70sVTaNE_0AhanGTceyx_pCLIT7PN6o,13299
8
8
  cucu/browser/selenium_tweaks.py,sha256=oUIhWVhBZbc9qsmQUJMpIr9uUWKxtgZBcnySWU6Yttk,879
9
9
  cucu/cli/__init__.py,sha256=uXX5yVG1konJ_INdlrcfMg-Tt_5_cSx29Ed8R8v908A,62
10
- cucu/cli/core.py,sha256=gIc_r6qjfLxVvGiiJeSErdsXsoKWlhi9ca_EaBUx6fM,26673
11
- cucu/cli/run.py,sha256=XIGIACPieywM5Mi3k_CycM9eiRElzGNZdHlV0_TDSVY,6118
12
- cucu/cli/steps.py,sha256=lg5itVH_C-0_3RelWXv9X2qQUHggdxuxLCGwH5l1bf4,4210
10
+ cucu/cli/core.py,sha256=-3TWxedDNMR1Jg7qNF5GLdvgn7KgthMwQ0hfeyJi1eg,27509
11
+ cucu/cli/run.py,sha256=6w7lkgf3iWsg9lqrmCJEWmOHGRXJFbVyZItVGGk_9gM,6105
12
+ cucu/cli/steps.py,sha256=5-aOGf3fmcnge4pcFM__4shcA3PZwjKe6oZN6XKY1pM,4215
13
13
  cucu/cli/thread_dumper.py,sha256=Z3XnYSxidx6pqjlQ7zu-TKMIYZWk4z9c5YLdPkcemiU,1593
14
- cucu/config.py,sha256=5AE6GrkqzjNhzzrB-eZrINgeztV7CCGuSdWJ-5GtWhk,14939
15
- cucu/db.py,sha256=0aT4h1hGeToIBXlPoxkuB7dBpQ3lMlUHWfq-J3WJINA,14581
14
+ cucu/config.py,sha256=Pi59JiRcCdzugDwraM4R1hXRkP52123z0hSP8X6lyzI,14970
15
+ cucu/db.py,sha256=ZadBBvwwzlHTi-MXMrHlSudpn61zxaTQsDMfCjSGnNk,15323
16
16
  cucu/edgedriver_autoinstaller/README.md,sha256=tDkAWIqgRdCjt-oX1nYqikIC_FfiOEM2-pc5S5VbRLo,84
17
17
  cucu/edgedriver_autoinstaller/__init__.py,sha256=fo6xJJPvcc5Xvni8epXfxDoPxJH5_b6Vk2jD9JTwfRs,969
18
18
  cucu/edgedriver_autoinstaller/utils.py,sha256=iRKTww77CGaTAntt_QDvxlKPxpMU4otx95OeD97khcM,6802
@@ -24,7 +24,7 @@ cucu/formatter/json.py,sha256=fJ1dZBGwYD4OkhQFDE49MRKGNzsrhDzQYq-dUfsYh94,10589
24
24
  cucu/formatter/junit.py,sha256=dCyS47iHOqn5AZjsRpWCsDRkxxJ67IZy3wIlE5jjhoM,10249
25
25
  cucu/formatter/rundb.py,sha256=dKNlD-LXmrJ1Gm4OHI7Cs49eMuBGlBfwLz7NLISF5sg,7857
26
26
  cucu/fuzzy/__init__.py,sha256=ce4JRmaBF6oab6U99Qbpt7DrD3elhH32__-ND6fw5xc,104
27
- cucu/fuzzy/core.py,sha256=CMxS5XifmWNtuDacGA8SA6dAUNuFR71WrNzZ2uK0_vc,3395
27
+ cucu/fuzzy/core.py,sha256=TnUFaBgtJ4QeXXxW5E8Eviwy3zYpQuBPefAdNZzzjkM,3397
28
28
  cucu/fuzzy/fuzzy.js,sha256=7ppPmR8szoEQ2rfIsyTmtYDgTrNwXrP_g11y-qEITGk,13882
29
29
  cucu/helpers.py,sha256=l_YMmbuXjtBRo-MER-qe6soUIyjt0ey2BoSgWs4zYwA,36285
30
30
  cucu/hooks.py,sha256=3Z1mavU42XMQ0DZ7lVWwTB-BJYHRyYUOzzOtmkdIsow,7117
@@ -56,12 +56,12 @@ cucu/reporter/external/jquery-3.5.1.min.js,sha256=9_aliU8dGd2tb6OSsuzixeV4y_faTq
56
56
  cucu/reporter/external/jquery.dataTables.min.js,sha256=XNhaB1tBOSFMHu96BSAJpZOJzfZ4SZI1nwAbnwry2UY,90265
57
57
  cucu/reporter/external/popper.min.js,sha256=pS96pU17yq-gVu4KBQJi38VpSuKN7otMrDQprzf_DWY,19188
58
58
  cucu/reporter/favicon.png,sha256=9ikXLAmzfQzy2NQps_8CGaZog2FvQrOX8nnSZ0e1UmM,2161
59
- cucu/reporter/html.py,sha256=I0eRj3FDQO6Oo1dWVmQ3wrDKqEuKC8mpUwdp5IJpDQU,19608
59
+ cucu/reporter/html.py,sha256=MpTwYmd-_GycFh3zqQrmSp6Wp-FBJ1CvfatV5bBxpbs,19925
60
60
  cucu/reporter/templates/feature.html,sha256=IBkwGiul-sRO5lT8q8VFXMUJx1owsAd1YbdDzziSjKw,3645
61
- cucu/reporter/templates/flat.html,sha256=JGsMq-IWz6YUpJX9hcN65-15HxcX3NJclOmMDtW3HZE,2358
62
- cucu/reporter/templates/index.html,sha256=xgPYNU-sozN-iOaEzyymoQ4LDRI75eHXngbAP0xDYls,2770
61
+ cucu/reporter/templates/flat.html,sha256=inx9wBo23SKsETA5BqU3GAxM7WaLTsDgCyL_D7TPpdA,2531
62
+ cucu/reporter/templates/index.html,sha256=pJ1eojL19EIUuIiqtALPm3atTabKJb7M1FwGzWpGkdg,2818
63
63
  cucu/reporter/templates/layout.html,sha256=2iDRbm8atO8mgHWgijIvDCrBMKvcP6YHrmr95WtJiE4,4561
64
- cucu/reporter/templates/scenario.html,sha256=zxVCBLHqaeai6mcRGbpXmREvNeSAyQrygXLzdF30xb0,11120
64
+ cucu/reporter/templates/scenario.html,sha256=yOAVb3cHMDvf1xzHatbXdpJnhClVMwMMyZrTSU9Nz9o,11735
65
65
  cucu/steps/__init__.py,sha256=seSmASBlWu6-6wbFbvEbPwigBcRXiYP18C4X_2cW8Ng,753
66
66
  cucu/steps/base_steps.py,sha256=0fPvdaKoan8lMAKrDnK0-zrALpxm11P1zVAY5CN7iXA,1893
67
67
  cucu/steps/browser_steps.py,sha256=iTRl5ffpf2YrFk5qh655WFHAeSOwoE3HFhmXhjsZtao,12687
@@ -82,13 +82,13 @@ cucu/steps/radio_steps.py,sha256=FygUNPCEBXzmzGMHL8DYNtLe9q4qjeyITiRLH-5Nbag,593
82
82
  cucu/steps/section_steps.py,sha256=BBsRtJTub_L1grKngRCj7t-f8S4ihOQht68TCzPHZxw,1283
83
83
  cucu/steps/step_utils.py,sha256=Chd0NQbMnfAEJmQkoVQRMbVRbCnJIBvVeH7CmXrCMm0,1417
84
84
  cucu/steps/tab_steps.py,sha256=TVVytkihvJ2GYQ9bwAs1CVzb-twzUq11QONlEbd6uO0,1818
85
- cucu/steps/table_steps.py,sha256=5bBT2HcuLy-wJ8vUschqkHEED-_GPPJwqdpLFlS4e9c,13744
85
+ cucu/steps/table_steps.py,sha256=Xf_9sMZXJRaO64Sd12vCnBm9Olxq1qyyfHHpy_hveWI,13737
86
86
  cucu/steps/tables.js,sha256=Os2a7Fo-cg03XVli7USvcnBVad4N7idXr-HBuzdLvVQ,945
87
87
  cucu/steps/text_steps.py,sha256=Jj_GHoHeemNwVdUOdqcehArNp7WM-WMjljA4w0pLXuw,2576
88
88
  cucu/steps/variable_steps.py,sha256=WSctH3_xcxjijGPYZlxp-foC_SIAAKtF__saNtgZJbk,2966
89
- cucu/steps/webserver_steps.py,sha256=wWkpSvcSMdiskPkh4cqlepWx1nkvEpTU2tRXQmPDbyo,1410
89
+ cucu/steps/webserver_steps.py,sha256=i11xOmSjhhrQ-2QrDfpjDhWroeJuuGKvbYEsHV1cioI,1406
90
90
  cucu/utils.py,sha256=LCcs8sMzvdvH05N8P5QYO4lO6j-_PQC530mEAD96go8,10957
91
- cucu-1.3.12.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
92
- cucu-1.3.12.dist-info/entry_points.txt,sha256=11WRIhQM7LuUnQg1lAoZQoNvvBvYNN1maDgQS4djwJo,40
93
- cucu-1.3.12.dist-info/METADATA,sha256=Dll_GwbkRROSj58VKzcldTeZYm9LY76Vjuer6EOWInI,16722
94
- cucu-1.3.12.dist-info/RECORD,,
91
+ cucu-1.3.14.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
92
+ cucu-1.3.14.dist-info/entry_points.txt,sha256=11WRIhQM7LuUnQg1lAoZQoNvvBvYNN1maDgQS4djwJo,40
93
+ cucu-1.3.14.dist-info/METADATA,sha256=O06K0LOySNXhSXlxm1DR8pXT9M5bgMTMEwKiGIa5Bj0,16722
94
+ cucu-1.3.14.dist-info/RECORD,,
File without changes