cucu 1.2.8__tar.gz → 1.3.1__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 (92) hide show
  1. {cucu-1.2.8 → cucu-1.3.1}/PKG-INFO +2 -1
  2. {cucu-1.2.8 → cucu-1.3.1}/pyproject.toml +3 -1
  3. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/cli/core.py +13 -1
  4. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/cli/run.py +6 -0
  5. cucu-1.3.1/src/cucu/db.py +370 -0
  6. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/environment.py +53 -9
  7. {cucu-1.2.8 → cucu-1.3.1}/README.md +0 -0
  8. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/__init__.py +0 -0
  9. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/ansi_parser.py +0 -0
  10. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/behave_tweaks.py +0 -0
  11. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/browser/__init__.py +0 -0
  12. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/browser/core.py +0 -0
  13. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/browser/frames.py +0 -0
  14. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/browser/selenium.py +0 -0
  15. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/browser/selenium_tweaks.py +0 -0
  16. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/cli/__init__.py +0 -0
  17. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/cli/steps.py +0 -0
  18. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/cli/thread_dumper.py +0 -0
  19. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/config.py +0 -0
  20. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/edgedriver_autoinstaller/README.md +0 -0
  21. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/edgedriver_autoinstaller/__init__.py +0 -0
  22. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/edgedriver_autoinstaller/utils.py +0 -0
  23. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/external/jquery/jquery-3.5.1.min.js +0 -0
  24. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/formatter/__init__.py +0 -0
  25. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/formatter/cucu.py +0 -0
  26. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/formatter/json.py +0 -0
  27. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/formatter/junit.py +0 -0
  28. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/fuzzy/__init__.py +0 -0
  29. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/fuzzy/core.py +0 -0
  30. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/fuzzy/fuzzy.js +0 -0
  31. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/helpers.py +0 -0
  32. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/hooks.py +0 -0
  33. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/.gitignore +0 -0
  34. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/README.md +0 -0
  35. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/cucurc.yml +0 -0
  36. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/data/www/example.html +0 -0
  37. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/features/cucurc.yml +0 -0
  38. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/features/environment.py +0 -0
  39. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/features/example.feature +0 -0
  40. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/features/lint_rules/sid.yaml +0 -0
  41. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/features/steps/__init__.py +0 -0
  42. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/init_data/features/steps/my_steps.py +0 -0
  43. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/language_server/__init__.py +0 -0
  44. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/language_server/core.py +0 -0
  45. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/lint/__init__.py +0 -0
  46. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/lint/linter.py +0 -0
  47. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/lint/rules/format.yaml +0 -0
  48. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/logger.py +0 -0
  49. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/matcher/__init__.py +0 -0
  50. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/matcher/core.py +0 -0
  51. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/page_checks.py +0 -0
  52. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/__init__.py +0 -0
  53. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/external/bootstrap.min.css +0 -0
  54. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/external/bootstrap.min.js +0 -0
  55. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/external/dataTables.bootstrap.min.css +0 -0
  56. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/external/dataTables.bootstrap.min.js +0 -0
  57. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/external/jquery-3.5.1.min.js +0 -0
  58. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/external/jquery.dataTables.min.js +0 -0
  59. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/external/popper.min.js +0 -0
  60. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/favicon.png +0 -0
  61. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/html.py +0 -0
  62. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/templates/feature.html +0 -0
  63. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/templates/flat.html +0 -0
  64. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/templates/index.html +0 -0
  65. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/templates/layout.html +0 -0
  66. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/reporter/templates/scenario.html +0 -0
  67. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/__init__.py +0 -0
  68. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/base_steps.py +0 -0
  69. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/browser_steps.py +0 -0
  70. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/button_steps.py +0 -0
  71. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/checkbox_steps.py +0 -0
  72. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/command_steps.py +0 -0
  73. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/draggable_steps.py +0 -0
  74. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/dropdown_steps.py +0 -0
  75. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/file_input_steps.py +0 -0
  76. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/filesystem_steps.py +0 -0
  77. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/flow_control_steps.py +0 -0
  78. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/image_steps.py +0 -0
  79. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/input_steps.py +0 -0
  80. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/link_steps.py +0 -0
  81. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/menuitem_steps.py +0 -0
  82. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/platform_steps.py +0 -0
  83. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/radio_steps.py +0 -0
  84. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/section_steps.py +0 -0
  85. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/step_utils.py +0 -0
  86. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/tab_steps.py +0 -0
  87. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/table_steps.py +0 -0
  88. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/tables.js +0 -0
  89. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/text_steps.py +0 -0
  90. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/variable_steps.py +0 -0
  91. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/steps/webserver_steps.py +0 -0
  92. {cucu-1.2.8 → cucu-1.3.1}/src/cucu/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cucu
3
- Version: 1.2.8
3
+ Version: 1.3.1
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, Kevin Garton, Joy Liao
@@ -25,6 +25,7 @@ Requires-Dist: jellyfish>=1.1
25
25
  Requires-Dist: jinja2~=3.1.3
26
26
  Requires-Dist: lsprotocol~=2023.0.1
27
27
  Requires-Dist: mpire~=2.10.2
28
+ Requires-Dist: peewee>=3.18.2
28
29
  Requires-Dist: psutil>=6.0
29
30
  Requires-Dist: pygls~=1.3.1
30
31
  Requires-Dist: pyyaml~=6.0.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cucu"
3
- version = "1.2.8"
3
+ version = "1.3.1"
4
4
  description = "Easy BDD web testing"
5
5
  readme = "README.md"
6
6
  license = { text = "The Clear BSD License" }
@@ -47,6 +47,7 @@ dependencies = [
47
47
  "jinja2~=3.1.3",
48
48
  "lsprotocol~=2023.0.1",
49
49
  "mpire~=2.10.2",
50
+ "peewee>=3.18.2",
50
51
  "psutil>=6.0",
51
52
  "pygls~=1.3.1",
52
53
  "pyyaml~=6.0.1",
@@ -84,6 +85,7 @@ dev-dependencies = [
84
85
  "pre-commit>=3.8.0",
85
86
  "pytest~=8.4.0",
86
87
  "pytest-check>=2.5.3",
88
+ "pytest-sugar>=1.0.0",
87
89
  "ruff>=0.6.4",
88
90
  ]
89
91
 
@@ -33,6 +33,7 @@ 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
37
  from cucu.lint import linter
37
38
  from cucu.utils import generate_short_id
38
39
 
@@ -240,6 +241,8 @@ def run(
240
241
  ):
241
242
  """
242
243
  run a set of feature files
244
+
245
+ Note: All the os.environ variables are set to be available in the child processes
243
246
  """
244
247
  init_global_hook_variables()
245
248
  dumper = None
@@ -299,7 +302,9 @@ def run(
299
302
  os.environ["CUCU_RECORD_ENV_VARS"] = "true"
300
303
 
301
304
  os.environ["CUCU_RUN_ID"] = CONFIG["CUCU_RUN_ID"] = generate_short_id()
302
- CONFIG["WORKER_RUN_ID"] = "parent"
305
+ os.environ["WORKER_PARENT_ID"] = CONFIG["WORKER_RUN_ID"] = (
306
+ generate_short_id()
307
+ )
303
308
  if not dry_run:
304
309
  create_run(results, filepath)
305
310
 
@@ -492,6 +497,10 @@ def run(
492
497
  if dumper is not None:
493
498
  dumper.stop()
494
499
 
500
+ if not dry_run and os.path.exists(results):
501
+ finish_worker_record(worker_run_id=CONFIG.get("WORKER_PARENT_ID"))
502
+ consolidate_database_files(results)
503
+
495
504
  if generate_report:
496
505
  _generate_report(
497
506
  results,
@@ -524,6 +533,9 @@ def _generate_report(
524
533
 
525
534
  os.makedirs(output)
526
535
 
536
+ if os.path.exists(results_dir):
537
+ consolidate_database_files(results_dir)
538
+
527
539
  report_location = reporter.generate(
528
540
  results_dir, output, only_failures=only_failures
529
541
  )
@@ -13,6 +13,7 @@ from cucu import (
13
13
  )
14
14
  from cucu.browser import selenium
15
15
  from cucu.config import CONFIG
16
+ from cucu.db import create_database_file, record_cucu_run
16
17
  from cucu.page_checks import init_page_checks
17
18
 
18
19
 
@@ -204,3 +205,8 @@ def create_run(results, filepath):
204
205
  run_json_filepath.write_text(
205
206
  json.dumps(run_details, indent=2, sort_keys=True), encoding="utf8"
206
207
  )
208
+ CONFIG["RUN_DB_PATH"] = run_db_path = results_path / "run.db"
209
+ if not run_db_path.exists():
210
+ create_database_file(run_db_path)
211
+
212
+ record_cucu_run()
@@ -0,0 +1,370 @@
1
+ """
2
+ Database creation and management utilities for cucu.
3
+ """
4
+
5
+ import logging
6
+ import sqlite3
7
+ import sys
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from peewee import (
12
+ BooleanField,
13
+ DateTimeField,
14
+ FloatField,
15
+ ForeignKeyField,
16
+ IntegerField,
17
+ Model,
18
+ TextField,
19
+ )
20
+ from playhouse.sqlite_ext import JSONField, SqliteExtDatabase
21
+
22
+ from cucu.config import CONFIG
23
+
24
+ db_filepath = CONFIG["RUN_DB_PATH"]
25
+ db = SqliteExtDatabase(db_filepath)
26
+
27
+ logger = logging.getLogger("peewee")
28
+ logger.setLevel(logging.WARNING) # Only show warnings and errors
29
+
30
+
31
+ class BaseModel(Model):
32
+ class Meta:
33
+ database = db
34
+
35
+
36
+ class cucu_run(BaseModel):
37
+ cucu_run_id = TextField(primary_key=True)
38
+ full_arguments = JSONField()
39
+ date = TextField()
40
+ start_at = DateTimeField()
41
+ end_at = DateTimeField(null=True)
42
+
43
+
44
+ class worker(BaseModel):
45
+ worker_run_id = TextField(primary_key=True)
46
+ cucu_run_id = ForeignKeyField(
47
+ cucu_run,
48
+ field="cucu_run_id",
49
+ backref="workers",
50
+ column_name="cucu_run_id",
51
+ null=True,
52
+ )
53
+ parent_id = ForeignKeyField(
54
+ "self",
55
+ field="worker_run_id",
56
+ backref="child_workers",
57
+ column_name="parent_id",
58
+ null=True,
59
+ )
60
+ start_at = DateTimeField()
61
+ end_at = DateTimeField(null=True)
62
+ custom_data = JSONField(null=True)
63
+
64
+
65
+ class feature(BaseModel):
66
+ feature_run_id = TextField(primary_key=True)
67
+ worker_run_id = ForeignKeyField(
68
+ worker,
69
+ field="worker_run_id",
70
+ backref="features",
71
+ column_name="worker_run_id",
72
+ )
73
+ name = TextField()
74
+ filename = TextField()
75
+ description = TextField()
76
+ tags = TextField()
77
+ start_at = DateTimeField()
78
+ end_at = DateTimeField(null=True)
79
+ custom_data = JSONField(null=True)
80
+
81
+
82
+ class scenario(BaseModel):
83
+ scenario_run_id = TextField(primary_key=True)
84
+ feature_run_id = ForeignKeyField(
85
+ feature,
86
+ field="feature_run_id",
87
+ backref="scenarios",
88
+ column_name="feature_run_id",
89
+ )
90
+ name = TextField()
91
+ line_number = IntegerField()
92
+ seq = IntegerField()
93
+ tags = TextField()
94
+ status = TextField(null=True)
95
+ duration = FloatField(null=True)
96
+ start_at = DateTimeField()
97
+ end_at = DateTimeField(null=True)
98
+ log_files = JSONField(null=True)
99
+ cucu_config = JSONField(null=True)
100
+ browser_info = JSONField(null=True)
101
+ custom_data = JSONField(null=True)
102
+
103
+
104
+ class step(BaseModel):
105
+ step_run_id = TextField(primary_key=True)
106
+ scenario_run_id = ForeignKeyField(
107
+ scenario,
108
+ field="scenario_run_id",
109
+ backref="steps",
110
+ column_name="scenario_run_id",
111
+ )
112
+ seq = IntegerField()
113
+ section_level = IntegerField(null=True)
114
+ 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
+ is_substep = BooleanField()
121
+ has_substeps = BooleanField()
122
+ status = TextField(null=True)
123
+ duration = FloatField(null=True)
124
+ start_at = DateTimeField()
125
+ end_at = DateTimeField(null=True)
126
+ debug_output = TextField(null=True)
127
+ browser_logs = TextField(null=True)
128
+ browser_info = JSONField(null=True)
129
+ screenshots = JSONField(null=True)
130
+
131
+
132
+ def record_cucu_run():
133
+ cucu_run_id_val = CONFIG["CUCU_RUN_ID"]
134
+ worker_run_id = CONFIG["WORKER_RUN_ID"]
135
+
136
+ db.connect(reuse_if_open=True)
137
+ start_at = datetime.now().isoformat()
138
+ cucu_run.create(
139
+ cucu_run_id=cucu_run_id_val,
140
+ full_arguments=sys.argv,
141
+ date=start_at,
142
+ start_at=start_at,
143
+ )
144
+
145
+ worker.create(
146
+ worker_run_id=worker_run_id,
147
+ cucu_run_id=cucu_run_id_val,
148
+ parent_id=CONFIG.get("WORKER_PARENT_ID")
149
+ if CONFIG.get("WORKER_PARENT_ID") != worker_run_id
150
+ else None,
151
+ start_at=datetime.now().isoformat(),
152
+ )
153
+ db.close()
154
+ return str(db_filepath)
155
+
156
+
157
+ def record_feature(feature_obj):
158
+ db.connect(reuse_if_open=True)
159
+ feature.create(
160
+ feature_run_id=feature_obj.feature_run_id,
161
+ worker_run_id=CONFIG["WORKER_RUN_ID"],
162
+ name=feature_obj.name,
163
+ filename=feature_obj.filename,
164
+ description="\n".join(feature_obj.description)
165
+ if isinstance(feature_obj.description, list)
166
+ else str(feature_obj.description),
167
+ tags=" ".join(feature_obj.tags),
168
+ start_at=datetime.now().isoformat(),
169
+ )
170
+ db.close()
171
+
172
+
173
+ def record_scenario(ctx):
174
+ db.connect(reuse_if_open=True)
175
+ 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
+ )
184
+ db.close()
185
+
186
+
187
+ def start_step_record(ctx, step_obj):
188
+ 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])
194
+ step.create(
195
+ step_run_id=step_obj.step_run_id,
196
+ scenario_run_id=ctx.scenario.scenario_run_id,
197
+ seq=step_obj.seq,
198
+ keyword=step_obj.keyword,
199
+ 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,
202
+ location=str(step_obj.location),
203
+ is_substep=step_obj.is_substep,
204
+ has_substeps=step_obj.has_substeps,
205
+ section_level=getattr(step_obj, "section_level", None),
206
+ start_at=step_obj.start_at,
207
+ )
208
+ db.close()
209
+
210
+
211
+ def finish_step_record(step_obj, duration):
212
+ db.connect(reuse_if_open=True)
213
+ screenshot_infos = []
214
+ if hasattr(step_obj, "screenshots") and step_obj.screenshots:
215
+ for screenshot in step_obj.screenshots:
216
+ screenshot_info = {
217
+ "step_name": screenshot.get("step_name"),
218
+ "label": screenshot.get("label"),
219
+ "location": screenshot.get("location"),
220
+ "size": screenshot.get("size"),
221
+ "filepath": screenshot.get("filepath"),
222
+ }
223
+ screenshot_infos.append(screenshot_info)
224
+
225
+ step.update(
226
+ section_level=getattr(step_obj, "section_level", None),
227
+ parent_seq=step_obj.parent_seq,
228
+ has_substeps=step_obj.has_substeps,
229
+ status=step_obj.status.name,
230
+ 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,
235
+ screenshots=screenshot_infos,
236
+ ).where(step.step_run_id == step_obj.step_run_id).execute()
237
+ db.close()
238
+
239
+
240
+ def finish_scenario_record(scenario_obj):
241
+ 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()
245
+ scenario_logs_dir = CONFIG.get("SCENARIO_LOGS_DIR")
246
+ if not scenario_logs_dir or not Path(scenario_logs_dir).exists():
247
+ log_files_json = "[]"
248
+ else:
249
+ logs_path = Path(scenario_logs_dir)
250
+ log_files = [
251
+ str(file.relative_to(logs_path))
252
+ for file in logs_path.rglob("*")
253
+ if file.is_file()
254
+ ]
255
+ log_files_json = sorted(log_files)
256
+ custom_data_json = scenario_obj.custom_data
257
+ scenario.update(
258
+ status=scenario_obj.status.name,
259
+ duration=duration,
260
+ end_at=scenario_obj.end_at,
261
+ 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,
265
+ ).where(scenario.scenario_run_id == scenario_obj.scenario_run_id).execute()
266
+ db.close()
267
+
268
+
269
+ def finish_feature_record(feature_obj):
270
+ db.connect(reuse_if_open=True)
271
+ feature.update(
272
+ end_at=datetime.now().isoformat(),
273
+ custom_data=feature_obj.custom_data,
274
+ ).where(feature.feature_run_id == feature_obj.feature_run_id).execute()
275
+ db.close()
276
+
277
+
278
+ def finish_worker_record(custom_data=None, worker_run_id=None):
279
+ db.connect(reuse_if_open=True)
280
+ target_worker_run_id = worker_run_id or CONFIG["WORKER_RUN_ID"]
281
+ worker.update(
282
+ end_at=datetime.now().isoformat(),
283
+ custom_data=custom_data,
284
+ ).where(worker.worker_run_id == target_worker_run_id).execute()
285
+ db.close()
286
+
287
+
288
+ def finish_cucu_run_record():
289
+ db.connect(reuse_if_open=True)
290
+ cucu_run.update(
291
+ end_at=datetime.now().isoformat(),
292
+ ).where(cucu_run.cucu_run_id == CONFIG["CUCU_RUN_ID"]).execute()
293
+ db.close()
294
+
295
+
296
+ def create_database_file(db_filepath):
297
+ db.init(db_filepath)
298
+ db.connect(reuse_if_open=True)
299
+ db.create_tables([cucu_run, worker, feature, scenario, step])
300
+ db.execute_sql("""
301
+ CREATE VIEW IF NOT EXISTS flat AS
302
+ SELECT
303
+ w.cucu_run_id,
304
+ s.start_at,
305
+ s.duration,
306
+ f.name AS feature_name,
307
+ s.name AS scenario_name,
308
+ f.tags || ' ' || s.tags AS tags,
309
+ s.log_files
310
+ FROM scenario s
311
+ JOIN feature f ON s.feature_run_id = f.feature_run_id
312
+ JOIN worker w ON f.worker_run_id = w.worker_run_id
313
+ """)
314
+ db.execute_sql("""
315
+ CREATE VIEW IF NOT EXISTS flat_scenario AS
316
+ SELECT
317
+ s.scenario_run_id,
318
+ f.name AS feature_name,
319
+ s.name AS scenario_name,
320
+ f.tags || ' ' || s.tags AS tags,
321
+ f.filename || ':' || s.line_number AS feature_file_line,
322
+ s.status,
323
+ (
324
+ SELECT json_group_array(json_object(
325
+ 'status', st.status,
326
+ 'duration', st.duration,
327
+ 'name', st.name
328
+ ))
329
+ FROM step st
330
+ WHERE st.scenario_run_id = s.scenario_run_id
331
+ ORDER BY st.seq
332
+ ) AS steps,
333
+ (
334
+ SELECT st.debug_output
335
+ FROM step st
336
+ WHERE st.scenario_run_id = s.scenario_run_id
337
+ ORDER BY st.seq DESC
338
+ LIMIT 1
339
+ ) AS last_step_debug_log
340
+ FROM scenario s
341
+ JOIN feature f ON s.feature_run_id = f.feature_run_id
342
+ """)
343
+ db.close()
344
+
345
+
346
+ def consolidate_database_files(results_dir):
347
+ # This function would need a more advanced approach with peewee, so for now, keep using sqlite3 for consolidation
348
+ results_path = Path(results_dir)
349
+ target_db_path = results_path / "run.db"
350
+ db_files = [
351
+ db for db in results_path.glob("**/*.db") if db.name != "run.db"
352
+ ]
353
+ tables_to_copy = ["cucu_run", "worker", "feature", "scenario", "step"]
354
+ with sqlite3.connect(target_db_path) as target_conn:
355
+ target_cursor = target_conn.cursor()
356
+ for db_file in db_files:
357
+ with sqlite3.connect(db_file) as source_conn:
358
+ source_cursor = source_conn.cursor()
359
+ for table_name in tables_to_copy:
360
+ source_cursor.execute(f"SELECT * FROM {table_name}")
361
+ rows = source_cursor.fetchall()
362
+ source_cursor.execute(f"PRAGMA table_info({table_name})")
363
+ columns = [col[1] for col in source_cursor.fetchall()]
364
+ placeholders = ",".join(["?" for _ in columns])
365
+ target_cursor.executemany(
366
+ f"INSERT OR REPLACE INTO {table_name} VALUES ({placeholders})",
367
+ rows,
368
+ )
369
+ target_conn.commit()
370
+ db_file.unlink()
@@ -9,6 +9,18 @@ import yaml
9
9
 
10
10
  from cucu import config, init_scenario_hook_variables, logger
11
11
  from cucu.config import CONFIG
12
+ from cucu.db import (
13
+ create_database_file,
14
+ finish_cucu_run_record,
15
+ finish_feature_record,
16
+ finish_scenario_record,
17
+ finish_step_record,
18
+ finish_worker_record,
19
+ record_cucu_run,
20
+ record_feature,
21
+ record_scenario,
22
+ start_step_record,
23
+ )
12
24
  from cucu.page_checks import init_page_checks
13
25
  from cucu.utils import (
14
26
  TeeStream,
@@ -47,7 +59,21 @@ def before_all(ctx):
47
59
  ctx.check_browser_initialized = partial(check_browser_initialized, ctx)
48
60
  ctx.worker_custom_data = {}
49
61
 
50
- CONFIG["WORKER_RUN_ID"] = generate_short_id()
62
+ if CONFIG["WORKER_RUN_ID"] != CONFIG["WORKER_PARENT_ID"]:
63
+ logger.debug(
64
+ "Create a new worker db since this isn't the parent process"
65
+ )
66
+ CONFIG["WORKER_RUN_ID"] = generate_short_id()
67
+
68
+ results_path = Path(CONFIG["CUCU_RESULTS_DIR"])
69
+ worker_run_id = CONFIG["WORKER_RUN_ID"]
70
+ cucu_run_id = CONFIG["CUCU_RUN_ID"]
71
+ CONFIG["RUN_DB_PATH"] = run_db_path = (
72
+ results_path / f"run_{cucu_run_id}_{worker_run_id}.db"
73
+ )
74
+ create_database_file(run_db_path)
75
+ record_cucu_run()
76
+
51
77
  CONFIG.snapshot("before_all")
52
78
 
53
79
  for hook in CONFIG["__CUCU_BEFORE_ALL_HOOKS"]:
@@ -59,12 +85,15 @@ def after_all(ctx):
59
85
  for hook in CONFIG["__CUCU_AFTER_ALL_HOOKS"]:
60
86
  hook(ctx)
61
87
 
88
+ finish_worker_record(ctx.worker_custom_data)
89
+ finish_cucu_run_record()
62
90
  CONFIG.restore(with_pop=True)
63
91
 
64
92
 
65
93
  def before_feature(ctx, feature):
66
94
  feature.feature_run_id = generate_short_id()
67
95
  feature.custom_data = {}
96
+ record_feature(feature)
68
97
 
69
98
  if config.CONFIG["CUCU_RESULTS_DIR"] is not None:
70
99
  results_dir = Path(config.CONFIG["CUCU_RESULTS_DIR"])
@@ -72,7 +101,7 @@ def before_feature(ctx, feature):
72
101
 
73
102
 
74
103
  def after_feature(ctx, feature):
75
- pass
104
+ finish_feature_record(feature)
76
105
 
77
106
 
78
107
  def before_scenario(ctx, scenario):
@@ -135,6 +164,7 @@ def before_scenario(ctx, scenario):
135
164
  ctx.browser_log_tee = TeeStream(ctx.browser_log_file)
136
165
 
137
166
  CONFIG["SCENARIO_RUN_ID"] = scenario.scenario_run_id = generate_short_id()
167
+ record_scenario(ctx)
138
168
 
139
169
  # run before all scenario hooks
140
170
  for hook in CONFIG["__CUCU_BEFORE_SCENARIO_HOOKS"]:
@@ -183,23 +213,33 @@ def after_scenario(ctx, scenario):
183
213
 
184
214
  CONFIG["__CUCU_AFTER_THIS_SCENARIO_HOOKS"] = []
185
215
 
216
+ browser_info = {"has_browser": False}
186
217
  if CONFIG.true("CUCU_KEEP_BROWSER_ALIVE"):
187
218
  logger.debug("keeping browser alive between sessions")
188
- else:
189
- if len(ctx.browsers) != 0:
190
- logger.debug("quitting browser between sessions")
219
+ elif len(ctx.browsers) != 0:
220
+ tab_info = ctx.browser.get_tab_info()
221
+ all_tabs = ctx.browser.get_all_tabs_info()
222
+ browser_info = {
223
+ "current_tab_index": tab_info["index"],
224
+ "all_tabs": all_tabs,
225
+ "browser_type": ctx.browser.driver.name,
226
+ }
191
227
 
192
- run_after_scenario_hook(ctx, scenario, download_browser_log)
228
+ logger.debug("quitting browser between sessions")
229
+ run_after_scenario_hook(ctx, scenario, cleanup_browsers)
230
+
231
+ scenario.browser_info = browser_info
193
232
 
194
233
  cucu_config_path = ctx.scenario_logs_dir / "cucu.config.yaml.txt"
195
234
  with open(cucu_config_path, "w") as config_file:
196
235
  config_file.write(CONFIG.to_yaml_without_secrets())
197
236
 
198
- scenario.cucu_config_json = json.dumps(
199
- yaml.safe_load(CONFIG.to_yaml_without_secrets())
237
+ scenario.cucu_config_json = yaml.safe_load(
238
+ CONFIG.to_yaml_without_secrets()
200
239
  )
201
240
 
202
241
  scenario.end_at = datetime.datetime.now().isoformat()[:-3]
242
+ finish_scenario_record(scenario)
203
243
 
204
244
 
205
245
  def download_mht_data(ctx):
@@ -217,7 +257,7 @@ def download_mht_data(ctx):
217
257
  browser.download_mht(mht_pathname)
218
258
 
219
259
 
220
- def download_browser_log(ctx):
260
+ def cleanup_browsers(ctx):
221
261
  # close the browser unless someone has set the keep browser alive
222
262
  # environment variable which allows tests to reuse the same browser
223
263
  # session
@@ -251,6 +291,8 @@ def before_step(ctx, step):
251
291
 
252
292
  CONFIG["__STEP_SCREENSHOT_COUNT"] = 0
253
293
 
294
+ start_step_record(ctx, step)
295
+
254
296
  # run before all step hooks
255
297
  for hook in CONFIG["__CUCU_BEFORE_STEP_HOOKS"]:
256
298
  hook(ctx)
@@ -341,3 +383,5 @@ def after_step(ctx, step):
341
383
  }
342
384
 
343
385
  step.browser_info = browser_info
386
+
387
+ finish_step_record(step, ctx.previous_step_duration)
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
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