cucu 1.3.12__tar.gz → 1.3.13__tar.gz

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.

Files changed (93) hide show
  1. {cucu-1.3.12 → cucu-1.3.13}/PKG-INFO +1 -1
  2. {cucu-1.3.12 → cucu-1.3.13}/pyproject.toml +1 -1
  3. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/cli/core.py +78 -59
  4. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/cli/run.py +6 -7
  5. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/cli/steps.py +1 -1
  6. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/config.py +2 -1
  7. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/db.py +32 -17
  8. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/fuzzy/core.py +2 -2
  9. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/html.py +29 -22
  10. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/templates/flat.html +4 -4
  11. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/templates/index.html +3 -3
  12. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/templates/scenario.html +28 -14
  13. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/table_steps.py +4 -7
  14. {cucu-1.3.12 → cucu-1.3.13}/README.md +0 -0
  15. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/__init__.py +0 -0
  16. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/ansi_parser.py +0 -0
  17. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/behave_tweaks.py +0 -0
  18. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/browser/__init__.py +0 -0
  19. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/browser/core.py +0 -0
  20. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/browser/frames.py +0 -0
  21. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/browser/selenium.py +0 -0
  22. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/browser/selenium_tweaks.py +0 -0
  23. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/cli/__init__.py +0 -0
  24. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/cli/thread_dumper.py +0 -0
  25. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/edgedriver_autoinstaller/README.md +0 -0
  26. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/edgedriver_autoinstaller/__init__.py +0 -0
  27. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/edgedriver_autoinstaller/utils.py +0 -0
  28. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/environment.py +0 -0
  29. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/external/jquery/jquery-3.5.1.min.js +0 -0
  30. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/formatter/__init__.py +0 -0
  31. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/formatter/cucu.py +0 -0
  32. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/formatter/json.py +0 -0
  33. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/formatter/junit.py +0 -0
  34. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/formatter/rundb.py +0 -0
  35. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/fuzzy/__init__.py +0 -0
  36. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/fuzzy/fuzzy.js +0 -0
  37. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/helpers.py +0 -0
  38. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/hooks.py +0 -0
  39. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/.gitignore +0 -0
  40. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/README.md +0 -0
  41. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/cucurc.yml +0 -0
  42. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/data/www/example.html +0 -0
  43. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/features/cucurc.yml +0 -0
  44. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/features/environment.py +0 -0
  45. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/features/example.feature +0 -0
  46. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/features/lint_rules/sid.yaml +0 -0
  47. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/features/steps/__init__.py +0 -0
  48. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/init_data/features/steps/my_steps.py +0 -0
  49. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/language_server/__init__.py +0 -0
  50. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/language_server/core.py +0 -0
  51. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/lint/__init__.py +0 -0
  52. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/lint/linter.py +0 -0
  53. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/lint/rules/format.yaml +0 -0
  54. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/logger.py +0 -0
  55. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/matcher/__init__.py +0 -0
  56. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/matcher/core.py +0 -0
  57. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/page_checks.py +0 -0
  58. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/__init__.py +0 -0
  59. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/external/bootstrap.min.css +0 -0
  60. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/external/bootstrap.min.js +0 -0
  61. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/external/dataTables.bootstrap.min.css +0 -0
  62. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/external/dataTables.bootstrap.min.js +0 -0
  63. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/external/jquery-3.5.1.min.js +0 -0
  64. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/external/jquery.dataTables.min.js +0 -0
  65. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/external/popper.min.js +0 -0
  66. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/favicon.png +0 -0
  67. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/templates/feature.html +0 -0
  68. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/reporter/templates/layout.html +0 -0
  69. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/__init__.py +0 -0
  70. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/base_steps.py +0 -0
  71. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/browser_steps.py +0 -0
  72. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/button_steps.py +0 -0
  73. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/checkbox_steps.py +0 -0
  74. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/command_steps.py +0 -0
  75. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/draggable_steps.py +0 -0
  76. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/dropdown_steps.py +0 -0
  77. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/file_input_steps.py +0 -0
  78. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/filesystem_steps.py +0 -0
  79. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/flow_control_steps.py +0 -0
  80. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/image_steps.py +0 -0
  81. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/input_steps.py +0 -0
  82. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/link_steps.py +0 -0
  83. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/menuitem_steps.py +0 -0
  84. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/platform_steps.py +0 -0
  85. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/radio_steps.py +0 -0
  86. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/section_steps.py +0 -0
  87. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/step_utils.py +0 -0
  88. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/tab_steps.py +0 -0
  89. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/tables.js +0 -0
  90. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/text_steps.py +0 -0
  91. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/variable_steps.py +0 -0
  92. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/steps/webserver_steps.py +0 -0
  93. {cucu-1.3.12 → cucu-1.3.13}/src/cucu/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cucu
3
- Version: 1.3.12
3
+ Version: 1.3.13
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cucu"
3
- version = "1.3.12"
3
+ version = "1.3.13"
4
4
  description = "Easy BDD web testing"
5
5
  readme = "README.md"
6
6
  license = { text = "The Clear BSD License" }
@@ -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,58 @@ 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,
516
527
  ):
517
- if os.path.exists(output):
518
- shutil.rmtree(output)
528
+ if report_folder.exists():
529
+ shutil.rmtree(report_folder)
519
530
 
520
- os.makedirs(output)
531
+ report_folder.mkdir(parents=True, exist_ok=True)
521
532
 
522
- if os.path.exists(results_dir):
533
+ if results_dir.exists():
523
534
  consolidate_database_files(results_dir)
524
535
 
525
536
  report_location = reporter.generate(
526
- results_dir, output, only_failures=only_failures
537
+ results_dir, report_folder, only_failures=only_failures
527
538
  )
528
539
  print(f"HTML test report at {report_location}")
529
540
 
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)
541
+ if junit_folder:
542
+ for junit_file in junit_folder.rglob("*.xml"):
543
+ junit = ET.parse(junit_file)
544
+ test_suite = junit.getroot()
545
+ ts_folder = test_suite.get("foldername")
546
+ for test_case in test_suite.iter("testcase"):
547
+ report_path = os.path.join(
548
+ report_folder,
549
+ ts_folder,
550
+ test_case.get("foldername"),
551
+ "index.html",
552
+ )
553
+ test_case.set("report_path", report_path)
554
+ junit.write(junit_file, encoding="utf-8", xml_declaration=False)
548
555
 
549
556
 
550
557
  @main.command()
551
- @click.argument("results_dir", default="results")
558
+ @click.argument(
559
+ "results_dir", default="results", type=click.Path(path_type=Path)
560
+ )
552
561
  @click.option(
553
562
  "--only-failures",
554
563
  default=False,
@@ -567,21 +576,27 @@ def _add_report_path_in_junit(junit_folder, report_folder):
567
576
  is_flag=True,
568
577
  help="when set skips are shown",
569
578
  )
570
- @click.option("-o", "--output", default="report")
579
+ @click.option(
580
+ "-o",
581
+ "--output",
582
+ default="report",
583
+ type=click.Path(path_type=Path),
584
+ )
571
585
  @click.option(
572
586
  "-j",
573
587
  "--junit",
574
588
  default=None,
575
589
  help="specify the output directory for JUnit XML files, default is "
576
590
  "the same location as --results",
591
+ type=click.Path(path_type=Path),
577
592
  )
578
593
  def report(
579
- results_dir,
594
+ results_dir: Path,
580
595
  only_failures,
581
596
  logging_level,
582
597
  show_skips,
583
- output,
584
- junit,
598
+ output: Path,
599
+ junit: Path,
585
600
  ):
586
601
  """
587
602
  generate a test report from a results directory
@@ -594,7 +609,7 @@ def report(
594
609
  if show_skips:
595
610
  os.environ["CUCU_SHOW_SKIPS"] = "true"
596
611
 
597
- run_details_filepath = os.path.join(results_dir, "run_details.json")
612
+ run_details_filepath = results_dir / "run_details.json"
598
613
 
599
614
  if os.path.exists(run_details_filepath):
600
615
  # load the run details at the time of execution for the provided results
@@ -608,12 +623,17 @@ def report(
608
623
  behave_init(run_details["filepath"])
609
624
 
610
625
  _generate_report(
611
- results_dir, output, only_failures=only_failures, junit=junit
626
+ results_dir=results_dir,
627
+ report_folder=output,
628
+ only_failures=only_failures,
629
+ junit_folder=junit,
612
630
  )
613
631
 
614
632
 
615
633
  @main.command()
616
- @click.argument("filepath", default="features")
634
+ @click.argument(
635
+ "filepath", default="features", type=click.Path(path_type=Path)
636
+ )
617
637
  @click.option(
618
638
  "-f",
619
639
  "--format",
@@ -639,7 +659,7 @@ def steps(filepath, format):
639
659
 
640
660
 
641
661
  @main.command()
642
- @click.argument("filepath", nargs=-1)
662
+ @click.argument("filepath", type=click.Path(path_type=Path), nargs=-1)
643
663
  @click.option(
644
664
  "--fix/--no-fix", default=False, help="fix lint violations, default: False"
645
665
  )
@@ -741,7 +761,9 @@ def lsp(logging_level, port):
741
761
 
742
762
 
743
763
  @main.command()
744
- @click.argument("filepath", default="features")
764
+ @click.argument(
765
+ "filepath", default="features", type=click.Path(path_type=Path)
766
+ )
745
767
  def vars(filepath):
746
768
  """
747
769
  print built-in cucu variables
@@ -766,14 +788,14 @@ def vars(filepath):
766
788
 
767
789
 
768
790
  @main.command()
769
- @click.argument("filepath", default="")
791
+ @click.argument("repo_dir", default="", type=click.Path(path_type=Path))
770
792
  @click.option(
771
793
  "-l",
772
794
  "--logging-level",
773
795
  default="INFO",
774
796
  help="set logging level to one of debug, warn or info (default)",
775
797
  )
776
- def init(filepath, logging_level):
798
+ def init(repo_dir, logging_level):
777
799
  """
778
800
  initialize cucu in the current directory
779
801
 
@@ -785,10 +807,9 @@ def init(filepath, logging_level):
785
807
  init_data_dir = Path(__file__).parent.parent / "init_data"
786
808
 
787
809
  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
810
 
790
- features_dir = os.path.join(repo_dir, "features")
791
- if os.path.exists(features_dir):
811
+ features_dir = repo_dir / "features"
812
+ if features_dir.exists():
792
813
  answer = input("Overwrite existing files? [y/N]:")
793
814
  if answer.lower() != "y":
794
815
  print("Aborted!")
@@ -874,9 +895,7 @@ def tags(filepath, logging_level):
874
895
  if not filepath.exists() or not feature_files:
875
896
  raise ClickException("No feature files found.")
876
897
 
877
- file_locations = [
878
- FileLocation(os.path.abspath(str(f))) for f in feature_files
879
- ]
898
+ file_locations = [FileLocation(f.absolute()) for f in feature_files]
880
899
  features = parse_features(file_locations)
881
900
  tag_scenarios = Counter()
882
901
 
@@ -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(),
@@ -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}")
@@ -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
@@ -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)
@@ -419,7 +419,7 @@ def consolidate_database_files(results_dir):
419
419
  create_database_file(target_db_path)
420
420
 
421
421
  db_files = [
422
- db for db in results_path.glob("**/*.db") if db.name != "run.db"
422
+ db for db in results_path.glob("**/run*.db") if db.name != "run.db"
423
423
  ]
424
424
  tables_to_copy = ["cucu_run", "worker", "feature", "scenario", "step"]
425
425
  with sqlite3.connect(target_db_path) as target_conn:
@@ -432,13 +432,28 @@ def consolidate_database_files(results_dir):
432
432
  rows = source_cursor.fetchall()
433
433
  source_cursor.execute(f"PRAGMA table_info({table_name})")
434
434
  columns = [col[1] for col in source_cursor.fetchall()]
435
+
436
+ # prep cucu_run for combining multiple runs
437
+ if table_name == "cucu_run":
438
+ db_path_index = columns.index("db_path")
439
+ rows = [
440
+ tuple(
441
+ item if idx != db_path_index else str(db_file)
442
+ for idx, item in enumerate(row)
443
+ )
444
+ for row in rows
445
+ ]
446
+
435
447
  placeholders = ",".join(["?" for _ in columns])
436
448
  target_cursor.executemany(
437
449
  f"INSERT OR REPLACE INTO {table_name} VALUES ({placeholders})",
438
450
  rows,
439
451
  )
440
452
  target_conn.commit()
441
- db_file.unlink()
453
+
454
+ if db_file.name != "run.db":
455
+ # remove the worker db files
456
+ db_file.unlink()
442
457
 
443
458
 
444
459
  def init_html_report_db(db_path):
@@ -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]
@@ -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>
@@ -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):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes