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