cucu 1.3.5__tar.gz → 1.3.7__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.5 → cucu-1.3.7}/PKG-INFO +3 -3
  2. {cucu-1.3.5 → cucu-1.3.7}/pyproject.toml +4 -4
  3. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/cli/core.py +25 -21
  4. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/cli/run.py +5 -1
  5. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/db.py +144 -69
  6. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/environment.py +24 -79
  7. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/formatter/cucu.py +2 -2
  8. cucu-1.3.7/src/cucu/formatter/rundb.py +207 -0
  9. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/html.py +114 -25
  10. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/templates/index.html +7 -7
  11. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/templates/scenario.html +1 -1
  12. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/flow_control_steps.py +4 -4
  13. {cucu-1.3.5 → cucu-1.3.7}/README.md +0 -0
  14. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/__init__.py +0 -0
  15. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/ansi_parser.py +0 -0
  16. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/behave_tweaks.py +0 -0
  17. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/browser/__init__.py +0 -0
  18. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/browser/core.py +0 -0
  19. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/browser/frames.py +0 -0
  20. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/browser/selenium.py +0 -0
  21. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/browser/selenium_tweaks.py +0 -0
  22. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/cli/__init__.py +0 -0
  23. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/cli/steps.py +0 -0
  24. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/cli/thread_dumper.py +0 -0
  25. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/config.py +0 -0
  26. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/edgedriver_autoinstaller/README.md +0 -0
  27. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/edgedriver_autoinstaller/__init__.py +0 -0
  28. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/edgedriver_autoinstaller/utils.py +0 -0
  29. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/external/jquery/jquery-3.5.1.min.js +0 -0
  30. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/formatter/__init__.py +0 -0
  31. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/formatter/json.py +0 -0
  32. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/formatter/junit.py +0 -0
  33. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/fuzzy/__init__.py +0 -0
  34. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/fuzzy/core.py +0 -0
  35. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/fuzzy/fuzzy.js +0 -0
  36. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/helpers.py +0 -0
  37. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/hooks.py +0 -0
  38. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/.gitignore +0 -0
  39. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/README.md +0 -0
  40. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/cucurc.yml +0 -0
  41. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/data/www/example.html +0 -0
  42. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/features/cucurc.yml +0 -0
  43. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/features/environment.py +0 -0
  44. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/features/example.feature +0 -0
  45. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/features/lint_rules/sid.yaml +0 -0
  46. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/features/steps/__init__.py +0 -0
  47. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/init_data/features/steps/my_steps.py +0 -0
  48. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/language_server/__init__.py +0 -0
  49. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/language_server/core.py +0 -0
  50. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/lint/__init__.py +0 -0
  51. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/lint/linter.py +0 -0
  52. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/lint/rules/format.yaml +0 -0
  53. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/logger.py +0 -0
  54. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/matcher/__init__.py +0 -0
  55. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/matcher/core.py +0 -0
  56. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/page_checks.py +0 -0
  57. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/__init__.py +0 -0
  58. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/external/bootstrap.min.css +0 -0
  59. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/external/bootstrap.min.js +0 -0
  60. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/external/dataTables.bootstrap.min.css +0 -0
  61. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/external/dataTables.bootstrap.min.js +0 -0
  62. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/external/jquery-3.5.1.min.js +0 -0
  63. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/external/jquery.dataTables.min.js +0 -0
  64. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/external/popper.min.js +0 -0
  65. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/favicon.png +0 -0
  66. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/templates/feature.html +0 -0
  67. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/templates/flat.html +0 -0
  68. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/reporter/templates/layout.html +0 -0
  69. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/__init__.py +0 -0
  70. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/base_steps.py +0 -0
  71. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/browser_steps.py +0 -0
  72. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/button_steps.py +0 -0
  73. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/checkbox_steps.py +0 -0
  74. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/command_steps.py +0 -0
  75. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/draggable_steps.py +0 -0
  76. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/dropdown_steps.py +0 -0
  77. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/file_input_steps.py +0 -0
  78. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/filesystem_steps.py +0 -0
  79. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/image_steps.py +0 -0
  80. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/input_steps.py +0 -0
  81. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/link_steps.py +0 -0
  82. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/menuitem_steps.py +0 -0
  83. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/platform_steps.py +0 -0
  84. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/radio_steps.py +0 -0
  85. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/section_steps.py +0 -0
  86. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/step_utils.py +0 -0
  87. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/tab_steps.py +0 -0
  88. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/table_steps.py +0 -0
  89. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/tables.js +0 -0
  90. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/text_steps.py +0 -0
  91. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/variable_steps.py +0 -0
  92. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/steps/webserver_steps.py +0 -0
  93. {cucu-1.3.5 → cucu-1.3.7}/src/cucu/utils.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cucu
3
- Version: 1.3.5
3
+ Version: 1.3.7
4
4
  Summary: Easy BDD web testing
5
5
  Keywords: cucumber,selenium,behave
6
- Author: Domino Data Lab, Rodney Gomes, Cedric Young, Xin Dong, Kavya, Kevin Garton, Joy Liao
7
- Author-email: Domino Data Lab <open-source@dominodatalab.com>, Rodney Gomes <107359+rlgomes@users.noreply.github.com>, Cedric Young <4129217+ccedricyoung@users.noreply.github.com>, Xin Dong <104880864+ddl-xin@users.noreply.github.com>, Kavya <91882851+ddl-kavya@users.noreply.github.com>, Kevin Garton <71028750+ddl-kgarton@users.noreply.github.com>, Joy Liao <107583686+ddl-joy-liao@users.noreply.github.com>
6
+ Author: Domino Data Lab, Rodney Gomes, Cedric Young, Xin Dong, Kavya Yakkati, Kevin Garton, Joy Liao
7
+ Author-email: Domino Data Lab <open-source@dominodatalab.com>, Rodney Gomes <107359+rlgomes@users.noreply.github.com>, Cedric Young <4129217+ccedricyoung@users.noreply.github.com>, Xin Dong <104880864+ddl-xin@users.noreply.github.com>, Kavya Yakkati <91882851+ddl-kavya@users.noreply.github.com>, Kevin Garton <71028750+ddl-kgarton@users.noreply.github.com>, Joy Liao <107583686+ddl-joy-liao@users.noreply.github.com>
8
8
  License: The Clear BSD License
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Environment :: Console
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cucu"
3
- version = "1.3.5"
3
+ version = "1.3.7"
4
4
  description = "Easy BDD web testing"
5
5
  readme = "README.md"
6
6
  license = { text = "The Clear BSD License" }
@@ -10,7 +10,7 @@ authors = [
10
10
  {name = "Rodney Gomes", email = "107359+rlgomes@users.noreply.github.com"},
11
11
  {name = "Cedric Young", email = "4129217+ccedricyoung@users.noreply.github.com"},
12
12
  {name = "Xin Dong", email = "104880864+ddl-xin@users.noreply.github.com"},
13
- {name = "Kavya", email = "91882851+ddl-kavya@users.noreply.github.com"},
13
+ {name = "Kavya Yakkati", email = "91882851+ddl-kavya@users.noreply.github.com"},
14
14
  {name = "Kevin Garton", email = "71028750+ddl-kgarton@users.noreply.github.com"},
15
15
  {name = "Joy Liao", email = "107583686+ddl-joy-liao@users.noreply.github.com"},
16
16
  ]
@@ -80,8 +80,8 @@ exclude = [
80
80
  "/tests",
81
81
  ]
82
82
 
83
- [tool.uv]
84
- dev-dependencies = [
83
+ [dependency-groups]
84
+ dev = [
85
85
  "pre-commit>=3.8.0",
86
86
  "pytest~=8.4.0",
87
87
  "pytest-check>=2.5.3",
@@ -33,7 +33,12 @@ from cucu.cli import thread_dumper
33
33
  from cucu.cli.run import behave, behave_init, create_run
34
34
  from cucu.cli.steps import print_human_readable_steps, print_json_steps
35
35
  from cucu.config import CONFIG
36
- from cucu.db import consolidate_database_files, finish_worker_record
36
+ from cucu.db import (
37
+ consolidate_database_files,
38
+ db,
39
+ finish_worker_record,
40
+ get_first_cucu_run_filepath,
41
+ )
37
42
  from cucu.lint import linter
38
43
  from cucu.utils import generate_short_id
39
44
 
@@ -259,12 +264,11 @@ def run(
259
264
  os.environ["CUCU_LOGGING_LEVEL"] = logging_level.upper()
260
265
  logger.init_logging(logging_level.upper())
261
266
 
262
- if not dry_run:
263
- if not preserve_results:
264
- if os.path.exists(results):
265
- shutil.rmtree(results)
267
+ if not preserve_results:
268
+ if os.path.exists(results):
269
+ shutil.rmtree(results)
266
270
 
267
- os.makedirs(results, exist_ok=True)
271
+ os.makedirs(results, exist_ok=True)
268
272
 
269
273
  if selenium_remote_url is not None:
270
274
  os.environ["CUCU_SELENIUM_REMOTE_URL"] = selenium_remote_url
@@ -299,8 +303,10 @@ def run(
299
303
  os.environ["WORKER_PARENT_ID"] = CONFIG["WORKER_RUN_ID"] = (
300
304
  generate_short_id(worker_id_seed)
301
305
  )
302
- if not dry_run:
303
- create_run(results, filepath)
306
+
307
+ os.environ["CUCU_FILEPATH"] = CONFIG["CUCU_FILEPATH"] = filepath
308
+
309
+ create_run(results, filepath)
304
310
 
305
311
  try:
306
312
  if workers is None or workers == 1:
@@ -491,7 +497,7 @@ def run(
491
497
  if dumper is not None:
492
498
  dumper.stop()
493
499
 
494
- if not dry_run and os.path.exists(results):
500
+ if os.path.exists(results):
495
501
  finish_worker_record(worker_run_id=CONFIG.get("WORKER_PARENT_ID"))
496
502
  consolidate_database_files(results)
497
503
 
@@ -510,18 +516,6 @@ def _generate_report(
510
516
  only_failures: False,
511
517
  junit: str | None = None,
512
518
  ):
513
- """
514
- helper method to handle report generation so it can be used by the `cucu report`
515
- command also the `cucu run` when told to generate a report. If junit is provided, it adds report
516
- path to the JUnit files.
517
-
518
-
519
- parameters:
520
- results_dir(string): the results directory containing the previous test run
521
- output(string): the directory where we'll generate the report
522
- only_failures(bool, optional): if only report failures. The default is False.
523
- junit(str|None, optional): the directory of the JUnit files. The default if None.
524
- """
525
519
  if os.path.exists(output):
526
520
  shutil.rmtree(output)
527
521
 
@@ -530,6 +524,16 @@ def _generate_report(
530
524
  if os.path.exists(results_dir):
531
525
  consolidate_database_files(results_dir)
532
526
 
527
+ db_path = os.path.join(results_dir, "run.db")
528
+
529
+ try:
530
+ db.init(db_path)
531
+ db.connect(reuse_if_open=True)
532
+ filepath = get_first_cucu_run_filepath()
533
+ behave_init(filepath)
534
+ finally:
535
+ db.close()
536
+
533
537
  report_location = reporter.generate(
534
538
  results_dir, output, only_failures=only_failures
535
539
  )
@@ -110,8 +110,10 @@ def behave(
110
110
  if dry_run:
111
111
  args += [
112
112
  "--dry-run",
113
- # console formater
113
+ # console formatter
114
114
  "--format=cucu.formatter.cucu:CucuFormatter",
115
+ # run.db formatter
116
+ "--format=cucu.formatter.rundb:RundbFormatter",
115
117
  ]
116
118
 
117
119
  else:
@@ -128,6 +130,8 @@ def behave(
128
130
  # disable behave's junit output in favor of our own formatter
129
131
  "--no-junit",
130
132
  "--format=cucu.formatter.junit:CucuJUnitFormatter",
133
+ # run.db formatter
134
+ "--format=cucu.formatter.rundb:RundbFormatter",
131
135
  ]
132
136
 
133
137
  for tag in tags:
@@ -18,6 +18,7 @@ from peewee import (
18
18
  TextField,
19
19
  )
20
20
  from playhouse.sqlite_ext import JSONField, SqliteExtDatabase
21
+ from tenacity import RetryError
21
22
 
22
23
  from cucu.config import CONFIG
23
24
 
@@ -36,6 +37,7 @@ class BaseModel(Model):
36
37
  class cucu_run(BaseModel):
37
38
  cucu_run_id = TextField(primary_key=True)
38
39
  full_arguments = JSONField()
40
+ filepath = TextField()
39
41
  date = TextField()
40
42
  start_at = DateTimeField()
41
43
  end_at = DateTimeField(null=True)
@@ -71,12 +73,13 @@ class feature(BaseModel):
71
73
  column_name="worker_run_id",
72
74
  )
73
75
  name = TextField()
74
- filename = TextField()
75
- description = TextField()
76
- tags = TextField()
76
+ status = TextField(null=True)
77
77
  start_at = DateTimeField()
78
78
  end_at = DateTimeField(null=True)
79
79
  custom_data = JSONField(null=True)
80
+ tags = JSONField()
81
+ filename = TextField()
82
+ description = TextField()
80
83
 
81
84
 
82
85
  class scenario(BaseModel):
@@ -88,17 +91,17 @@ class scenario(BaseModel):
88
91
  column_name="feature_run_id",
89
92
  )
90
93
  name = TextField()
91
- line_number = IntegerField()
92
- seq = IntegerField()
93
- tags = TextField()
94
+ seq = FloatField(null=True)
94
95
  status = TextField(null=True)
95
96
  duration = FloatField(null=True)
96
- start_at = DateTimeField()
97
+ start_at = DateTimeField(null=True)
97
98
  end_at = DateTimeField(null=True)
98
- log_files = JSONField(null=True)
99
- cucu_config = JSONField(null=True)
100
- browser_info = JSONField(null=True)
99
+ tags = JSONField()
101
100
  custom_data = JSONField(null=True)
101
+ browser_info = JSONField(null=True)
102
+ cucu_config = JSONField(null=True)
103
+ line_number = IntegerField()
104
+ log_files = JSONField(null=True)
102
105
 
103
106
 
104
107
  class step(BaseModel):
@@ -109,27 +112,32 @@ class step(BaseModel):
109
112
  backref="steps",
110
113
  column_name="scenario_run_id",
111
114
  )
112
- seq = IntegerField()
113
115
  section_level = IntegerField(null=True)
116
+ seq = IntegerField()
114
117
  parent_seq = IntegerField(null=True)
115
- keyword = TextField()
116
- name = TextField()
117
- text = TextField(null=True)
118
- table_data = JSONField(null=True)
119
- location = TextField()
120
118
  is_substep = BooleanField(null=True) # info available after step ends
121
119
  has_substeps = BooleanField()
120
+ keyword = TextField()
121
+ name = TextField()
122
122
  status = TextField(null=True)
123
123
  duration = FloatField(null=True)
124
- start_at = DateTimeField()
124
+ start_at = DateTimeField(null=True)
125
125
  end_at = DateTimeField(null=True)
126
+ stdout = JSONField(null=True)
127
+ stderr = JSONField(null=True)
128
+ error_message = JSONField(null=True)
129
+ exception = JSONField(null=True)
126
130
  debug_output = TextField(null=True)
127
- browser_logs = TextField(null=True)
128
131
  browser_info = JSONField(null=True)
132
+ text = JSONField(null=True)
133
+ table_data = JSONField(null=True)
134
+ location = TextField()
135
+ browser_logs = TextField(null=True)
129
136
  screenshots = JSONField(null=True)
130
137
 
131
138
 
132
139
  def record_cucu_run():
140
+ filepath = CONFIG["CUCU_FILEPATH"]
133
141
  cucu_run_id_val = CONFIG["CUCU_RUN_ID"]
134
142
  worker_run_id = CONFIG["WORKER_RUN_ID"]
135
143
 
@@ -138,6 +146,7 @@ def record_cucu_run():
138
146
  cucu_run.create(
139
147
  cucu_run_id=cucu_run_id_val,
140
148
  full_arguments=sys.argv,
149
+ filepath=filepath,
141
150
  date=start_at,
142
151
  start_at=start_at,
143
152
  )
@@ -150,7 +159,6 @@ def record_cucu_run():
150
159
  else None,
151
160
  start_at=datetime.now().isoformat(),
152
161
  )
153
- db.close()
154
162
  return str(db_filepath)
155
163
 
156
164
 
@@ -164,47 +172,48 @@ def record_feature(feature_obj):
164
172
  description="\n".join(feature_obj.description)
165
173
  if isinstance(feature_obj.description, list)
166
174
  else str(feature_obj.description),
167
- tags=" ".join(feature_obj.tags),
175
+ tags=feature_obj.tags,
168
176
  start_at=datetime.now().isoformat(),
169
177
  )
170
- db.close()
171
178
 
172
179
 
173
- def record_scenario(ctx):
180
+ def record_scenario(scenario_obj):
174
181
  db.connect(reuse_if_open=True)
175
182
  scenario.create(
176
- scenario_run_id=ctx.scenario.scenario_run_id,
177
- feature_run_id=ctx.scenario.feature.feature_run_id,
178
- name=ctx.scenario.name,
179
- line_number=ctx.scenario.line,
180
- seq=ctx.scenario_index,
181
- tags=" ".join(ctx.scenario.tags),
182
- start_at=ctx.scenario.start_at,
183
+ scenario_run_id=scenario_obj.scenario_run_id,
184
+ feature_run_id=scenario_obj.feature.feature_run_id,
185
+ name=scenario_obj.name,
186
+ line_number=scenario_obj.line,
187
+ seq=scenario_obj.seq,
188
+ tags=scenario_obj.tags,
189
+ start_at=getattr(scenario_obj, "start_at", None),
183
190
  )
184
- db.close()
185
191
 
186
192
 
187
- def start_step_record(ctx, step_obj):
193
+ def start_step_record(step_obj, scenario_run_id):
188
194
  db.connect(reuse_if_open=True)
189
- if not step_obj.table:
190
- table = None
191
- else:
192
- table = [step_obj.table.headings]
193
- table.extend([row.cells for row in step_obj.table.rows])
195
+
196
+ table = None
197
+ if step_obj.table:
198
+ table = {
199
+ "headings": step_obj.table.headings,
200
+ "rows": [list(row) for row in step_obj.table.rows],
201
+ }
202
+
194
203
  step.create(
195
204
  step_run_id=step_obj.step_run_id,
196
- scenario_run_id=ctx.scenario.scenario_run_id,
197
- seq=step_obj.seq,
205
+ scenario_run_id=scenario_run_id,
206
+ seq=getattr(step_obj, "seq", -1),
198
207
  keyword=step_obj.keyword,
199
208
  name=step_obj.name,
200
- text=step_obj.text if step_obj.text else None,
201
- table_data=table if step_obj.table else None,
209
+ status=step_obj.status.name,
210
+ text=step_obj.text.splitlines() if step_obj.text else [],
211
+ table_data=table,
202
212
  location=str(step_obj.location),
203
- has_substeps=step_obj.has_substeps,
213
+ is_substep=getattr(step_obj, "is_substep", False),
214
+ has_substeps=getattr(step_obj, "has_substeps", False),
204
215
  section_level=getattr(step_obj, "section_level", None),
205
- start_at=step_obj.start_at,
206
216
  )
207
- db.close()
208
217
 
209
218
 
210
219
  def finish_step_record(step_obj, duration):
@@ -221,27 +230,60 @@ def finish_step_record(step_obj, duration):
221
230
  }
222
231
  screenshot_infos.append(screenshot_info)
223
232
 
233
+ error_message = None
234
+ exception = []
235
+ if step.error_message and step_obj.status.name == "failed":
236
+ error_message = CONFIG.hide_secrets(step_obj.error_message)
237
+
238
+ if error := step_obj.exception:
239
+ if isinstance(error, RetryError):
240
+ error = error.last_attempt.exception()
241
+
242
+ if len(error.args) > 0 and isinstance(error.args[0], str):
243
+ error_class_name = error.__class__.__name__
244
+ redacted_error_msg = CONFIG.hide_secrets(error.args[0])
245
+ error_lines = redacted_error_msg.splitlines()
246
+ error_lines[0] = f"{error_class_name}: {error_lines[0]}"
247
+ else:
248
+ error_lines = [repr(error)]
249
+
250
+ exception = error_lines
251
+
224
252
  step.update(
225
- section_level=getattr(step_obj, "section_level", None),
226
- parent_seq=step_obj.parent_seq,
227
- has_substeps=step_obj.has_substeps,
228
- status=step_obj.status.name,
229
- is_substep=step_obj.is_substep,
253
+ browser_info=getattr(step_obj, "browser_info", ""),
254
+ browser_logs=getattr(step_obj, "browser_logs", ""),
255
+ debug_output=getattr(step_obj, "debug_output", ""),
230
256
  duration=duration,
231
- end_at=step_obj.end_at,
232
- debug_output=step_obj.debug_output,
233
- browser_logs=step_obj.browser_logs,
234
- browser_info=step_obj.browser_info,
257
+ end_at=getattr(step_obj, "end_at", None),
258
+ error_message=error_message,
259
+ exception=exception,
260
+ has_substeps=getattr(step_obj, "has_substeps", False),
261
+ parent_seq=getattr(step_obj, "parent_seq", None),
235
262
  screenshots=screenshot_infos,
263
+ section_level=getattr(step_obj, "section_level", None),
264
+ seq=step_obj.seq,
265
+ start_at=getattr(step_obj, "start_at", None),
266
+ status=step_obj.status.name,
267
+ stderr=getattr(step_obj, "stderr", []),
268
+ stdout=getattr(step_obj, "stdout", []),
236
269
  ).where(step.step_run_id == step_obj.step_run_id).execute()
237
- db.close()
238
270
 
239
271
 
240
272
  def finish_scenario_record(scenario_obj):
241
273
  db.connect(reuse_if_open=True)
242
- start_dt = datetime.fromisoformat(scenario_obj.start_at)
243
- end_dt = datetime.fromisoformat(scenario_obj.end_at)
244
- duration = (end_dt - start_dt).total_seconds()
274
+ if getattr(scenario_obj, "start_at", None):
275
+ start_at = datetime.fromisoformat(scenario_obj.start_at)
276
+ else:
277
+ start_at = None
278
+ if getattr(scenario_obj, "end_at", None):
279
+ end_at = datetime.fromisoformat(scenario_obj.end_at)
280
+ else:
281
+ end_at = None
282
+ if start_at and end_at:
283
+ duration = (end_at - start_at).total_seconds()
284
+ else:
285
+ duration = None
286
+
245
287
  scenario_logs_dir = CONFIG.get("SCENARIO_LOGS_DIR")
246
288
  if not scenario_logs_dir or not Path(scenario_logs_dir).exists():
247
289
  log_files_json = "[]"
@@ -253,26 +295,30 @@ def finish_scenario_record(scenario_obj):
253
295
  if file.is_file()
254
296
  ]
255
297
  log_files_json = sorted(log_files)
256
- custom_data_json = scenario_obj.custom_data
298
+
299
+ if scenario_obj.hook_failed:
300
+ status = "errored"
301
+ else:
302
+ status = scenario_obj.status.name
303
+
257
304
  scenario.update(
258
- status=scenario_obj.status.name,
305
+ status=status,
259
306
  duration=duration,
260
- end_at=scenario_obj.end_at,
307
+ end_at=end_at,
261
308
  log_files=log_files_json,
262
- cucu_config=scenario_obj.cucu_config_json,
263
- browser_info=scenario_obj.browser_info,
264
- custom_data=custom_data_json,
309
+ cucu_config=getattr(scenario_obj, "cucu_config_json", dict()),
310
+ browser_info=getattr(scenario_obj, "browser_info", dict()),
311
+ custom_data=getattr(scenario_obj, "custom_data", dict()),
265
312
  ).where(scenario.scenario_run_id == scenario_obj.scenario_run_id).execute()
266
- db.close()
267
313
 
268
314
 
269
315
  def finish_feature_record(feature_obj):
270
316
  db.connect(reuse_if_open=True)
271
317
  feature.update(
318
+ status=feature_obj.status.name,
272
319
  end_at=datetime.now().isoformat(),
273
320
  custom_data=feature_obj.custom_data,
274
321
  ).where(feature.feature_run_id == feature_obj.feature_run_id).execute()
275
- db.close()
276
322
 
277
323
 
278
324
  def finish_worker_record(custom_data=None, worker_run_id=None):
@@ -282,7 +328,6 @@ def finish_worker_record(custom_data=None, worker_run_id=None):
282
328
  end_at=datetime.now().isoformat(),
283
329
  custom_data=custom_data,
284
330
  ).where(worker.worker_run_id == target_worker_run_id).execute()
285
- db.close()
286
331
 
287
332
 
288
333
  def finish_cucu_run_record():
@@ -290,7 +335,11 @@ def finish_cucu_run_record():
290
335
  cucu_run.update(
291
336
  end_at=datetime.now().isoformat(),
292
337
  ).where(cucu_run.cucu_run_id == CONFIG["CUCU_RUN_ID"]).execute()
293
- db.close()
338
+
339
+
340
+ def close_db():
341
+ if not db.is_closed():
342
+ db.close()
294
343
 
295
344
 
296
345
  def create_database_file(db_filepath):
@@ -305,7 +354,12 @@ def create_database_file(db_filepath):
305
354
  s.duration,
306
355
  f.name AS feature_name,
307
356
  s.name AS scenario_name,
308
- f.tags || ' ' || s.tags AS tags,
357
+ CASE
358
+ WHEN f.tags = '[]' AND s.tags = '[]' THEN JSON('[]')
359
+ WHEN f.tags = '[]' THEN s.tags
360
+ WHEN s.tags = '[]' THEN f.tags
361
+ ELSE JSON(REPLACE(f.tags, ']', '') || ',' || REPLACE(s.tags, '[', ''))
362
+ END as tags,
309
363
  s.log_files
310
364
  FROM scenario s
311
365
  JOIN feature f ON s.feature_run_id = f.feature_run_id
@@ -317,7 +371,12 @@ def create_database_file(db_filepath):
317
371
  s.scenario_run_id,
318
372
  f.name AS feature_name,
319
373
  s.name AS scenario_name,
320
- f.tags || ' ' || s.tags AS tags,
374
+ CASE
375
+ WHEN f.tags = '[]' AND s.tags = '[]' THEN JSON('[]')
376
+ WHEN f.tags = '[]' THEN s.tags
377
+ WHEN s.tags = '[]' THEN f.tags
378
+ ELSE JSON(REPLACE(f.tags, ']', '') || ',' || REPLACE(s.tags, '[', ''))
379
+ END as tags,
321
380
  f.filename || ':' || s.line_number AS feature_file_line,
322
381
  s.status,
323
382
  (
@@ -340,13 +399,20 @@ def create_database_file(db_filepath):
340
399
  FROM scenario s
341
400
  JOIN feature f ON s.feature_run_id = f.feature_run_id
342
401
  """)
343
- db.close()
402
+
403
+
404
+ def get_first_cucu_run_filepath():
405
+ run_record = cucu_run.select().first()
406
+ return run_record.filepath
344
407
 
345
408
 
346
409
  def consolidate_database_files(results_dir):
347
410
  # This function would need a more advanced approach with peewee, so for now, keep using sqlite3 for consolidation
348
411
  results_path = Path(results_dir)
349
412
  target_db_path = results_path / "run.db"
413
+ if not target_db_path.exists():
414
+ create_database_file(target_db_path)
415
+
350
416
  db_files = [
351
417
  db for db in results_path.glob("**/*.db") if db.name != "run.db"
352
418
  ]
@@ -368,3 +434,12 @@ def consolidate_database_files(results_dir):
368
434
  )
369
435
  target_conn.commit()
370
436
  db_file.unlink()
437
+
438
+
439
+ def init_html_report_db(db_path):
440
+ db.init(db_path)
441
+ db.connect(reuse_if_open=True)
442
+
443
+
444
+ def close_html_report_db():
445
+ db.close()