cucu 1.3.5__py3-none-any.whl → 1.3.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of cucu might be problematic. Click here for more details.

cucu/cli/core.py CHANGED
@@ -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
  )
cucu/cli/run.py CHANGED
@@ -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:
cucu/db.py CHANGED
@@ -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()
cucu/environment.py CHANGED
@@ -1,8 +1,6 @@
1
1
  import datetime
2
2
  import json
3
- import os
4
3
  import sys
5
- import time
6
4
  import traceback
7
5
  from functools import partial
8
6
  from pathlib import Path
@@ -11,23 +9,10 @@ import yaml
11
9
 
12
10
  from cucu import config, init_scenario_hook_variables, logger
13
11
  from cucu.config import CONFIG
14
- from cucu.db import (
15
- create_database_file,
16
- finish_cucu_run_record,
17
- finish_feature_record,
18
- finish_scenario_record,
19
- finish_step_record,
20
- finish_worker_record,
21
- record_cucu_run,
22
- record_feature,
23
- record_scenario,
24
- start_step_record,
25
- )
26
12
  from cucu.page_checks import init_page_checks
27
13
  from cucu.utils import (
28
14
  TeeStream,
29
15
  ellipsize_filename,
30
- generate_short_id,
31
16
  take_screenshot,
32
17
  )
33
18
 
@@ -61,27 +46,6 @@ def before_all(ctx):
61
46
  ctx.check_browser_initialized = partial(check_browser_initialized, ctx)
62
47
  ctx.worker_custom_data = {}
63
48
 
64
- if CONFIG["WORKER_RUN_ID"] != CONFIG["WORKER_PARENT_ID"]:
65
- logger.debug(
66
- "Create a new worker db since this isn't the parent process"
67
- )
68
- # use seed unique enough for multiple cucu_runs to be combined but predictable within the same run
69
- worker_id_seed = f"{CONFIG['WORKER_PARENT_ID']}_{os.getpid()}"
70
- CONFIG["WORKER_RUN_ID"] = generate_short_id(worker_id_seed)
71
-
72
- results_path = Path(CONFIG["CUCU_RESULTS_DIR"])
73
- worker_run_id = CONFIG["WORKER_RUN_ID"]
74
- cucu_run_id = CONFIG["CUCU_RUN_ID"]
75
- CONFIG["RUN_DB_PATH"] = run_db_path = (
76
- results_path / f"run_{cucu_run_id}_{worker_run_id}.db"
77
- )
78
- if not run_db_path.exists():
79
- logger.debug(
80
- f"Creating new run database file: {run_db_path} for {worker_id_seed}"
81
- )
82
- create_database_file(run_db_path)
83
- record_cucu_run()
84
-
85
49
  CONFIG.snapshot("before_all")
86
50
 
87
51
  for hook in CONFIG["__CUCU_BEFORE_ALL_HOOKS"]:
@@ -93,29 +57,22 @@ def after_all(ctx):
93
57
  for hook in CONFIG["__CUCU_AFTER_ALL_HOOKS"]:
94
58
  hook(ctx)
95
59
 
96
- finish_worker_record(ctx.worker_custom_data)
97
- finish_cucu_run_record()
98
60
  CONFIG.restore(with_pop=True)
99
61
 
100
62
 
101
63
  def before_feature(ctx, feature):
102
- feature_run_id_seed = f"{CONFIG['WORKER_RUN_ID']}_{time.perf_counter()}"
103
- feature.feature_run_id = generate_short_id(feature_run_id_seed)
104
- feature.custom_data = {}
105
- record_feature(feature)
106
-
107
64
  if config.CONFIG["CUCU_RESULTS_DIR"] is not None:
108
65
  results_dir = Path(config.CONFIG["CUCU_RESULTS_DIR"])
109
66
  ctx.feature_dir = results_dir / ellipsize_filename(feature.name)
110
67
 
111
68
 
112
69
  def after_feature(ctx, feature):
113
- finish_feature_record(feature)
70
+ pass
114
71
 
115
72
 
116
73
  def before_scenario(ctx, scenario):
117
74
  # we want every scenario to start with the exact same reinitialized config
118
- # values and not really bleed values between scenario runs
75
+ # values and not bleed values between scenario runs
119
76
  CONFIG.restore()
120
77
 
121
78
  # we should load any cucurc.yml files in the path to the feature file
@@ -125,10 +82,8 @@ def before_scenario(ctx, scenario):
125
82
 
126
83
  init_scenario_hook_variables()
127
84
 
128
- scenario.custom_data = {}
129
85
  ctx.scenario = scenario
130
86
  ctx.step_index = 0
131
- ctx.scenario_index = ctx.feature.scenarios.index(scenario) + 1
132
87
  ctx.browsers = []
133
88
  ctx.browser = None
134
89
  ctx.section_step_stack = []
@@ -172,14 +127,6 @@ def before_scenario(ctx, scenario):
172
127
  )
173
128
  ctx.browser_log_tee = TeeStream(ctx.browser_log_file)
174
129
 
175
- scenario_run_id_seed = (
176
- f"{ctx.feature.feature_run_id}_{time.perf_counter()}"
177
- )
178
- CONFIG["SCENARIO_RUN_ID"] = scenario.scenario_run_id = generate_short_id(
179
- scenario_run_id_seed
180
- )
181
- record_scenario(ctx)
182
-
183
130
  # run before all scenario hooks
184
131
  for hook in CONFIG["__CUCU_BEFORE_SCENARIO_HOOKS"]:
185
132
  try:
@@ -215,6 +162,23 @@ def after_scenario(ctx, scenario):
215
162
  for timer_name in ctx.step_timers:
216
163
  logger.warning(f'timer "{timer_name}" was never stopped/recorded')
217
164
 
165
+ browser_info = {"has_browser": False}
166
+
167
+ if len(ctx.browsers) != 0:
168
+ try:
169
+ tab_info = ctx.browser.get_tab_info()
170
+ all_tabs = ctx.browser.get_all_tabs_info()
171
+ browser_info = {
172
+ "has_browser": True,
173
+ "current_tab_index": tab_info["index"],
174
+ "all_tabs": all_tabs,
175
+ "browser_type": ctx.browser.driver.name,
176
+ }
177
+ except Exception as e:
178
+ logger.error(f"Error getting browser info: {e}")
179
+
180
+ scenario.browser_info = browser_info
181
+
218
182
  run_after_scenario_hook(ctx, scenario, download_mht_data)
219
183
 
220
184
  # run after all scenario hooks in 'lifo' order.
@@ -227,23 +191,12 @@ def after_scenario(ctx, scenario):
227
191
 
228
192
  CONFIG["__CUCU_AFTER_THIS_SCENARIO_HOOKS"] = []
229
193
 
230
- browser_info = {"has_browser": False}
231
194
  if CONFIG.true("CUCU_KEEP_BROWSER_ALIVE"):
232
195
  logger.debug("keeping browser alive between sessions")
233
196
  elif len(ctx.browsers) != 0:
234
- tab_info = ctx.browser.get_tab_info()
235
- all_tabs = ctx.browser.get_all_tabs_info()
236
- browser_info = {
237
- "current_tab_index": tab_info["index"],
238
- "all_tabs": all_tabs,
239
- "browser_type": ctx.browser.driver.name,
240
- }
241
-
242
197
  logger.debug("quitting browser between sessions")
243
198
  run_after_scenario_hook(ctx, scenario, cleanup_browsers)
244
199
 
245
- scenario.browser_info = browser_info
246
-
247
200
  cucu_config_path = ctx.scenario_logs_dir / "cucu.config.yaml.txt"
248
201
  with open(cucu_config_path, "w") as config_file:
249
202
  config_file.write(CONFIG.to_yaml_without_secrets())
@@ -253,7 +206,6 @@ def after_scenario(ctx, scenario):
253
206
  )
254
207
 
255
208
  scenario.end_at = datetime.datetime.now().isoformat()[:-3]
256
- finish_scenario_record(scenario)
257
209
 
258
210
 
259
211
  def download_mht_data(ctx):
@@ -285,10 +237,6 @@ def cleanup_browsers(ctx):
285
237
 
286
238
 
287
239
  def before_step(ctx, step):
288
- step_run_id_seed = f"{ctx.scenario.scenario_run_id}_{ctx.step_index}_{time.perf_counter()}"
289
- step.step_run_id = generate_short_id(
290
- step_run_id_seed, length=10
291
- ) # up to 10 characters to give two orders of magnitude less chance of collision
292
240
  step.start_at = datetime.datetime.now().isoformat()[:-3]
293
241
 
294
242
  sys.stdout.captured()
@@ -299,7 +247,8 @@ def before_step(ctx, step):
299
247
  ctx.scenario_debug_log_tee.clear()
300
248
 
301
249
  ctx.current_step = step
302
- ctx.current_step.has_substeps = False
250
+ step.is_substep = getattr(step, "is_substep", False)
251
+ step.has_substeps = getattr(step, "has_substeps", False)
303
252
  ctx.section_level = None
304
253
  step.seq = ctx.step_index + 1
305
254
  step.parent_seq = (
@@ -308,8 +257,6 @@ def before_step(ctx, step):
308
257
 
309
258
  CONFIG["__STEP_SCREENSHOT_COUNT"] = 0
310
259
 
311
- start_step_record(ctx, step)
312
-
313
260
  # run before all step hooks
314
261
  for hook in CONFIG["__CUCU_BEFORE_STEP_HOOKS"]:
315
262
  hook(ctx)
@@ -330,7 +277,7 @@ def after_step(ctx, step):
330
277
  # calculate duration from ISO timestamps
331
278
  start_at = datetime.datetime.fromisoformat(step.start_at)
332
279
  end_at = datetime.datetime.fromisoformat(step.end_at)
333
- ctx.previous_step_duration = (end_at - start_at).total_seconds()
280
+ ctx.scenario.previous_step_duration = (end_at - start_at).total_seconds()
334
281
 
335
282
  # when set this means we're running in parallel mode using --workers and
336
283
  # we want to see progress reported using simply dots
@@ -355,8 +302,8 @@ def after_step(ctx, step):
355
302
  logger.debug(log_message)
356
303
 
357
304
  # Add tab info to step.stdout so it shows up in the HTML report
358
- step.stdout += (
359
- f"\ntab({current_tab} of {total_tabs}): {title}\nurl: {url}\n"
305
+ step.stdout.extend(
306
+ [f"tab({current_tab} of {total_tabs}): {title}", "url: {url}"]
360
307
  )
361
308
 
362
309
  # if the step has substeps from using `run_steps` then we already moved
@@ -400,5 +347,3 @@ def after_step(ctx, step):
400
347
  }
401
348
 
402
349
  step.browser_info = browser_info
403
-
404
- finish_step_record(step, ctx.previous_step_duration)
cucu/formatter/cucu.py CHANGED
@@ -101,8 +101,8 @@ class CucuFormatter(Formatter):
101
101
 
102
102
  def insert_step(self, step, index=-1):
103
103
  # used to determine how to better handle console output
104
- step.has_substeps = False
105
- step.is_substep = False
104
+ step.has_substeps = getattr(step, "has_substeps", False)
105
+ step.is_substep = getattr(step, "is_substep", False)
106
106
 
107
107
  if index == -1:
108
108
  self.steps.append(step)
@@ -0,0 +1,207 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import absolute_import
3
+
4
+ import datetime
5
+ import os
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from behave.formatter.base import Formatter
10
+ from behave.model import ScenarioOutline
11
+
12
+ from cucu import logger
13
+ from cucu.config import CONFIG
14
+ from cucu.db import (
15
+ close_db,
16
+ create_database_file,
17
+ finish_cucu_run_record,
18
+ finish_feature_record,
19
+ finish_scenario_record,
20
+ finish_step_record,
21
+ finish_worker_record,
22
+ record_cucu_run,
23
+ record_feature,
24
+ record_scenario,
25
+ start_step_record,
26
+ )
27
+ from cucu.utils import (
28
+ generate_short_id,
29
+ )
30
+
31
+
32
+ class RundbFormatter(Formatter):
33
+ """
34
+ Record to the database using the Formatter Behave API.
35
+ This is different from hooks as we don't have access to context (ctx) here.
36
+ Instead we use this class's data and add some data to the passed in objects: feature, scenario, step.
37
+ Another difference is that we can't rely on hooks to execute, as the step/scenario/feature may be skipped.
38
+ Also, use cucu's CONFIG for global data since we don't have context (ctx).
39
+
40
+ ## Just for reference:
41
+ Processing Logic (simplified, without ScenarioOutline and skip logic)::
42
+
43
+ for feature in runner.features:
44
+ formatter = make_formatters(...)
45
+ formatter.uri(feature.filename)
46
+ formatter.feature(feature)
47
+ for scenario in feature.scenarios:
48
+ formatter.scenario(scenario)
49
+ for step in scenario.all_steps:
50
+ formatter.step(step)
51
+ step_match = step_registry.find_match(step)
52
+ formatter.match(step_match)
53
+ if step_match:
54
+ step_match.run()
55
+ else:
56
+ step.status = Status.undefined
57
+ formatter.result(step.status)
58
+ formatter.eof() # -- FEATURE-END
59
+ formatter.close()
60
+ """
61
+
62
+ # -- FORMATTER API:
63
+ name = "rundb"
64
+ description = "records the results of the run to the run.db database"
65
+
66
+ def __init__(self, stream_opener, config):
67
+ super(RundbFormatter, self).__init__(stream_opener, config)
68
+ # We don't actually use the stream provided by Behave, so don't open it.
69
+ # self.stream = self.open()
70
+ self.config = config
71
+
72
+ if (
73
+ not CONFIG["RUN_DB_PATH"]
74
+ or CONFIG["WORKER_RUN_ID"] != CONFIG["WORKER_PARENT_ID"]
75
+ ):
76
+ logger.debug(
77
+ "Create a new worker db since this isn't the parent process"
78
+ )
79
+ # use seed unique enough for multiple cucu_runs to be combined but predictable within the same run
80
+ worker_id_seed = f"{CONFIG['WORKER_PARENT_ID']}_{os.getpid()}"
81
+ CONFIG["WORKER_RUN_ID"] = generate_short_id(worker_id_seed)
82
+
83
+ results_path = Path(CONFIG["CUCU_RESULTS_DIR"])
84
+ worker_run_id = CONFIG["WORKER_RUN_ID"]
85
+ cucu_run_id = CONFIG["CUCU_RUN_ID"]
86
+ CONFIG["RUN_DB_PATH"] = run_db_path = (
87
+ results_path / f"run_{cucu_run_id}_{worker_run_id}.db"
88
+ )
89
+
90
+ if not run_db_path.exists():
91
+ logger.debug(
92
+ f"Creating new run database file: {run_db_path} for {worker_id_seed}"
93
+ )
94
+ create_database_file(run_db_path)
95
+ record_cucu_run()
96
+
97
+ def uri(self, uri):
98
+ # nothing to do, but we need to implement the method for behave
99
+ pass
100
+
101
+ def feature(self, feature):
102
+ """Called before a feature is executed."""
103
+ self.this_feature = feature
104
+ self.this_scenario = None
105
+ self.this_background = None
106
+ self.next_start_at = None
107
+
108
+ feature_run_id_seed = (
109
+ f"{CONFIG['WORKER_RUN_ID']}_{time.perf_counter()}"
110
+ )
111
+ feature.feature_run_id = generate_short_id(feature_run_id_seed)
112
+ feature.custom_data = {}
113
+
114
+ record_feature(feature)
115
+
116
+ def background(self, background):
117
+ # nothing to do, but we need to implement the method for behave
118
+ pass
119
+
120
+ def _finish_scenario(self):
121
+ if self.this_scenario is None:
122
+ return
123
+
124
+ # ensure non-executed steps have correct seq
125
+ for index, step in enumerate(self.this_steps):
126
+ if getattr(step, "seq", -1) == -1:
127
+ step.seq = index + 1 # 1-based sequence
128
+ finish_step_record(step, None)
129
+
130
+ finish_scenario_record(self.this_scenario)
131
+
132
+ def scenario(self, scenario):
133
+ """Called before a scenario is executed (or ScenarioOutline scenarios)."""
134
+ self._finish_scenario()
135
+
136
+ self.this_scenario = scenario
137
+ self.this_steps = []
138
+ self.next_start_at = datetime.datetime.now().isoformat()[:-3]
139
+ scenario_run_id_seed = (
140
+ f"{scenario.feature.feature_run_id}_{time.perf_counter()}"
141
+ )
142
+ scenario.scenario_run_id = generate_short_id(scenario_run_id_seed)
143
+ scenario.custom_data = {}
144
+
145
+ # feature.scenarios is a mix of Scenario and ScenarioOutline objects with their own scenarios list
146
+ for index, feature_scenario in enumerate(scenario.feature.scenarios):
147
+ if feature_scenario == scenario:
148
+ scenario.seq = index + 1
149
+ break
150
+
151
+ # Scenarios belonging to a Scenario Outline are included under their Scenario Outline sequence.
152
+ # Add a suffix to preserve ordering within the Scenario Outline.
153
+ if isinstance(feature_scenario, ScenarioOutline):
154
+ for sub_index, sub_scenario in enumerate(
155
+ feature_scenario._scenarios
156
+ ):
157
+ if sub_scenario == scenario:
158
+ scenario.seq = index + 1 + (sub_index + 1) / 10
159
+ break
160
+
161
+ record_scenario(scenario)
162
+
163
+ def step(self, step):
164
+ """Called before a step is executed (and matched)."""
165
+ self.insert_step(step, index=-1)
166
+
167
+ def insert_step(self, step, index):
168
+ """cucu specific step insertion method used to add steps here and dynamically"""
169
+ next_index = index if index != -1 else len(self.this_steps)
170
+ step_run_id_seed = f"{self.this_scenario.scenario_run_id}_{next_index}_{time.perf_counter()}"
171
+ step.step_run_id = generate_short_id(
172
+ step_run_id_seed, length=10
173
+ ) # up to 10 characters to give two orders of magnitude less chance of collision
174
+ self.this_steps.insert(next_index, step)
175
+ start_step_record(step, self.this_scenario.scenario_run_id)
176
+
177
+ def match(self, match):
178
+ # nothing to do, but we need to implement the method for behave
179
+ pass
180
+
181
+ def result(self, step):
182
+ """Called after processing a step result is known, applies to executed/skipped too."""
183
+ step.start_at = self.next_start_at
184
+ self.next_start_at = step.end_at = datetime.datetime.now().isoformat()[
185
+ :-3
186
+ ]
187
+ previous_step_duration = getattr(
188
+ self.this_scenario, "previous_step_duration", 0
189
+ )
190
+ if step.status.name in ("untested", "undefined"):
191
+ step.seq = self.this_steps.index(step) + 1
192
+
193
+ finish_step_record(step, previous_step_duration)
194
+
195
+ def eof(self):
196
+ """Called after processing a feature (or a feature file)."""
197
+ # need to finish the last scenario
198
+ self._finish_scenario()
199
+ finish_feature_record(self.this_feature)
200
+
201
+ def close(self):
202
+ """Called before the formatter is no longer used
203
+ (stream/io compatibility).
204
+ """
205
+ finish_worker_record(None)
206
+ finish_cucu_run_record()
207
+ close_db()
cucu/reporter/html.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import glob
2
- import json
3
2
  import os
4
3
  import shutil
5
4
  import sys
@@ -14,6 +13,10 @@ import jinja2
14
13
  from cucu import format_gherkin_table, logger
15
14
  from cucu.ansi_parser import parse_log_to_html
16
15
  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
17
20
  from cucu.utils import ellipsize_filename, get_step_image_dir
18
21
 
19
22
 
@@ -63,21 +66,101 @@ def generate(results, basepath, only_failures=False):
63
66
 
64
67
  features = []
65
68
 
66
- run_json_filepaths = list(glob.iglob(os.path.join(results, "*run.json")))
67
- logger.info(
68
- f"Starting to process {len(run_json_filepaths)} files for report"
69
- )
69
+ db_path = os.path.join(results, "run.db")
70
+ try:
71
+ init_html_report_db(db_path)
72
+ features = []
73
+
74
+ db_features = FeatureModel.select().order_by(FeatureModel.start_at)
75
+ logger.info(
76
+ f"Starting to process {len(db_features)} features for report"
77
+ )
78
+
79
+ for db_feature in db_features:
80
+ feature_dict = {
81
+ "name": db_feature.name,
82
+ "filename": db_feature.filename,
83
+ "description": db_feature.description,
84
+ "tags": db_feature.tags if db_feature.tags else [],
85
+ "status": db_feature.status,
86
+ "elements": [],
87
+ }
88
+
89
+ db_scenarios = (
90
+ ScenarioModel.select()
91
+ .where(
92
+ ScenarioModel.feature_run_id == db_feature.feature_run_id
93
+ )
94
+ .order_by(ScenarioModel.seq)
95
+ )
70
96
 
71
- for run_json_filepath in run_json_filepaths:
72
- with open(run_json_filepath, "rb") as index_input:
73
- try:
74
- features += json.loads(index_input.read())
75
- except Exception as exception:
76
- logger.warning(
77
- f"unable to read file {run_json_filepath}, got error: {exception}"
97
+ feature_has_failures = False
98
+
99
+ if len(db_scenarios) == 0:
100
+ logger.debug(f"Feature {db_feature.name} has no scenarios")
101
+ continue
102
+
103
+ for db_scenario in db_scenarios:
104
+ scenario_dict = {
105
+ "name": db_scenario.name,
106
+ "line": db_scenario.line_number,
107
+ "tags": db_scenario.tags if db_scenario.tags else [],
108
+ "status": db_scenario.status or "passed",
109
+ "steps": [],
110
+ }
111
+
112
+ if db_scenario.status == "failed":
113
+ feature_has_failures = True
114
+
115
+ db_steps = (
116
+ StepModel.select()
117
+ .where(
118
+ StepModel.scenario_run_id
119
+ == db_scenario.scenario_run_id
120
+ )
121
+ .order_by(StepModel.seq)
78
122
  )
79
123
 
80
- # copy the external dependencies to the reports destination directory
124
+ for db_step in db_steps:
125
+ step_dict = {
126
+ "keyword": db_step.keyword,
127
+ "name": db_step.name,
128
+ "result": {
129
+ "status": db_step.status or "passed",
130
+ "duration": db_step.duration or 0,
131
+ "timestamp": db_step.end_at or "",
132
+ },
133
+ "substep": db_step.is_substep,
134
+ }
135
+
136
+ if db_step.text:
137
+ step_dict["text"] = db_step.text
138
+
139
+ if db_step.table_data:
140
+ step_dict["table"] = db_step.table_data
141
+
142
+ step_dict["result"]["error_message"] = [
143
+ db_step.error_message
144
+ ]
145
+
146
+ step_dict["result"]["exception"] = db_step.exception
147
+
148
+ step_dict["result"]["stdout"] = db_step.stdout
149
+ step_dict["result"]["stderr"] = db_step.stderr
150
+ scenario_dict["steps"].append(step_dict)
151
+
152
+ feature_dict["elements"].append(scenario_dict)
153
+
154
+ if feature_has_failures:
155
+ feature_dict["status"] = "failed"
156
+ elif only_failures and not feature_has_failures:
157
+ continue
158
+
159
+ features.append(feature_dict)
160
+
161
+ finally:
162
+ close_html_report_db()
163
+
81
164
  cucu_dir = os.path.dirname(sys.modules["cucu"].__file__)
82
165
  external_dir = os.path.join(cucu_dir, "reporter", "external")
83
166
  shutil.copytree(external_dir, os.path.join(basepath, "external"))
@@ -223,18 +306,24 @@ def generate(results, basepath, only_failures=False):
223
306
 
224
307
  if "result" in step:
225
308
  if step["result"]["status"] in ["failed", "passed"]:
226
- timestamp = datetime.fromisoformat(
227
- step["result"]["timestamp"]
228
- )
229
- step["result"]["timestamp"] = timestamp
230
-
231
- if scenario_started_at is None:
232
- scenario_started_at = timestamp
233
- scenario["started_at"] = timestamp
234
- time_offset = datetime.utcfromtimestamp(
235
- (timestamp - scenario_started_at).total_seconds()
236
- )
237
- step["result"]["time_offset"] = time_offset
309
+ if step["result"]["timestamp"]:
310
+ timestamp = datetime.fromisoformat(
311
+ step["result"]["timestamp"]
312
+ )
313
+ step["result"]["timestamp"] = timestamp
314
+
315
+ if scenario_started_at is None:
316
+ scenario_started_at = timestamp
317
+ scenario["started_at"] = timestamp
318
+ time_offset = datetime.utcfromtimestamp(
319
+ (
320
+ timestamp - scenario_started_at
321
+ ).total_seconds()
322
+ )
323
+ step["result"]["time_offset"] = time_offset
324
+ else:
325
+ step["result"]["timestamp"] = ""
326
+ step["result"]["time_offset"] = ""
238
327
 
239
328
  scenario_duration += step["result"]["duration"]
240
329
 
@@ -19,13 +19,13 @@
19
19
  <tr class="align-text-top">
20
20
  <th class="text-center">Started at</th>
21
21
  <th>Feature</th>
22
- <th class="text-center">Total<br/>{{ grand_totals['total_scenarios'] }}</th>
23
- <th class="text-center">Passed<br/>{{ grand_totals['total_scenarios_passed'] }}</th>
24
- <th class="text-center">Failed<br/>{{ grand_totals['total_scenarios_failed'] }}</th>
25
- <th class="text-center">Skipped<br/>{{ grand_totals['total_scenarios_skipped'] }}</th>
26
- <th class="text-center">Errored<br/>{{ grand_totals['total_scenarios_errored'] }}</th>
22
+ <th class="text-center">Total<br/>{{ grand_totals['total_scenarios'] | int }}</th>
23
+ <th class="text-center">Passed<br/>{{ grand_totals['total_scenarios_passed'] | int}}</th>
24
+ <th class="text-center">Failed<br/>{{ grand_totals['total_scenarios_failed'] | int }}</th>
25
+ <th class="text-center">Skipped<br/>{{ grand_totals['total_scenarios_skipped'] | int }}</th>
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/>{{ '{:.3f}'.format(grand_totals['duration']) }}s</th>
28
+ <th class="text-center">Duration (s)<br/>{{ grand_totals['duration'] | int }}s</th>
29
29
  </tr>
30
30
  </thead>
31
31
  {% for feature in features %}
@@ -38,7 +38,7 @@
38
38
  <td class="text-center">{{ feature['total_scenarios_skipped'] }}</td>
39
39
  <td class="text-center">{{ feature['total_scenarios_errored'] }}</td>
40
40
  <td class="text-center"><span class="status-{{ feature['status'] }}">{{ feature['status'] }}</span></td>
41
- <td class="text-center">{{ feature['duration'] }}</td>
41
+ <td class="text-center">{{ '{:.1f}'.format(feature['duration'] | float ) }}</td>
42
42
  </tr>
43
43
  {% endfor %}
44
44
  </table>
@@ -87,7 +87,7 @@
87
87
  {% endif %}
88
88
  {% if step['result'] is defined %}
89
89
  {% set step_status = step['result']['status'] %}
90
- {% if step['result']['status'] == 'failed' or step['result']['status'] == 'passed' %}
90
+ {% if step['result']['status'] in ('failed', 'passed') and step["result"]["timestamp"] %}
91
91
  {% set step_timing = "{} for {:.3f}s".format(step["result"]["timestamp"].strftime("%H:%M:%S"), step["result"]["duration"]) %}
92
92
  {% set step_start = step["result"]["timestamp"] %}
93
93
  {% endif %}
@@ -32,17 +32,17 @@ def expect_the_following_step_to_fail(ctx, message):
32
32
 
33
33
  @step('I should see the previous step took less than "{seconds}" seconds')
34
34
  def should_see_previous_step_took_less_than(ctx, seconds):
35
- if ctx.previous_step_duration > float(seconds):
35
+ if ctx.scenario.previous_step_duration > float(seconds):
36
36
  raise RuntimeError(
37
- f"previous step took {ctx.previous_step_duration}, which is more than {seconds}"
37
+ f"previous step took {ctx.scenario.previous_step_duration}, which is more than {seconds}"
38
38
  )
39
39
 
40
40
 
41
41
  @step('I should see the previous step took more than "{seconds}" seconds')
42
42
  def should_see_previous_step_took_more_than(ctx, seconds):
43
- if ctx.previous_step_duration < float(seconds):
43
+ if ctx.scenario.previous_step_duration < float(seconds):
44
44
  raise RuntimeError(
45
- f"previous step took {ctx.previous_step_duration}, which is less than {seconds}"
45
+ f"previous step took {ctx.scenario.previous_step_duration}, which is less than {seconds}"
46
46
  )
47
47
 
48
48
 
@@ -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
@@ -7,21 +7,22 @@ cucu/browser/frames.py,sha256=IW7kzRJn5PkbMaovIelAeCWO-T-2sOTwqaYBw-0-LKU,3545
7
7
  cucu/browser/selenium.py,sha256=eUC2DZkhUIZi70sVTaNE_0AhanGTceyx_pCLIT7PN6o,13299
8
8
  cucu/browser/selenium_tweaks.py,sha256=oUIhWVhBZbc9qsmQUJMpIr9uUWKxtgZBcnySWU6Yttk,879
9
9
  cucu/cli/__init__.py,sha256=uXX5yVG1konJ_INdlrcfMg-Tt_5_cSx29Ed8R8v908A,62
10
- cucu/cli/core.py,sha256=pRKTQwwaT9Uv6DMXPRHspt970cfkkikRHqVi4ATS6fI,27244
11
- cucu/cli/run.py,sha256=pIymhrx3w-dCkvh-YqSH9HldCPEgMB0MxDNHgS-RvFY,5935
10
+ cucu/cli/core.py,sha256=On0i9QzGoJ6ZFEYwt1Ae50i8YZFAAqeB__ULW8jcXc4,26950
11
+ cucu/cli/run.py,sha256=XIGIACPieywM5Mi3k_CycM9eiRElzGNZdHlV0_TDSVY,6118
12
12
  cucu/cli/steps.py,sha256=lg5itVH_C-0_3RelWXv9X2qQUHggdxuxLCGwH5l1bf4,4210
13
13
  cucu/cli/thread_dumper.py,sha256=Z3XnYSxidx6pqjlQ7zu-TKMIYZWk4z9c5YLdPkcemiU,1593
14
14
  cucu/config.py,sha256=5AE6GrkqzjNhzzrB-eZrINgeztV7CCGuSdWJ-5GtWhk,14939
15
- cucu/db.py,sha256=hQg54M7LvCIO-5Pluw5CR0jvC_0MXxrUAyhctoLCGKU,11865
15
+ cucu/db.py,sha256=iJ5NXECcqVbPdqozuQrdRGpCX1EgEENwDEGEsLUxpRs,14513
16
16
  cucu/edgedriver_autoinstaller/README.md,sha256=tDkAWIqgRdCjt-oX1nYqikIC_FfiOEM2-pc5S5VbRLo,84
17
17
  cucu/edgedriver_autoinstaller/__init__.py,sha256=fo6xJJPvcc5Xvni8epXfxDoPxJH5_b6Vk2jD9JTwfRs,969
18
18
  cucu/edgedriver_autoinstaller/utils.py,sha256=iRKTww77CGaTAntt_QDvxlKPxpMU4otx95OeD97khcM,6802
19
- cucu/environment.py,sha256=snMpafsBsBGRQwzf2MwfccQuZjDGkzUtnOicHyy6hQw,13714
19
+ cucu/environment.py,sha256=eeUERL4nUaFLlEv-UftQ99SfSSs7-3oaFkFM7YPqET0,11728
20
20
  cucu/external/jquery/jquery-3.5.1.min.js,sha256=9_aliU8dGd2tb6OSsuzixeV4y_faTqgFtohetphbbj0,89476
21
21
  cucu/formatter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- cucu/formatter/cucu.py,sha256=jHi6W9STRQjDy0j1P_HLLlBTPsS6EvUyvqeUjtWs3vM,9291
22
+ cucu/formatter/cucu.py,sha256=NRLFsd6xl3uwUk45ihKN2OEioOeAqeTURrcQgscDGnU,9351
23
23
  cucu/formatter/json.py,sha256=fJ1dZBGwYD4OkhQFDE49MRKGNzsrhDzQYq-dUfsYh94,10589
24
24
  cucu/formatter/junit.py,sha256=aJ9dGLbamMH-wUi_msF66_-_c_YUq07-8_wCNEjUju4,10129
25
+ cucu/formatter/rundb.py,sha256=7gKVPbSg7bCuAt972XRs5vRjeowLzVZPaOHCIxUazrE,7668
25
26
  cucu/fuzzy/__init__.py,sha256=ce4JRmaBF6oab6U99Qbpt7DrD3elhH32__-ND6fw5xc,104
26
27
  cucu/fuzzy/core.py,sha256=tmQKX_Ni-2ohoxzctRUg2x7zMeEW8MlJJmpU3PfTmvQ,3153
27
28
  cucu/fuzzy/fuzzy.js,sha256=ee-TytISLyUo7cMAkuVI5qbLXdt0eoFWczTsoU4zYhg,11618
@@ -55,12 +56,12 @@ cucu/reporter/external/jquery-3.5.1.min.js,sha256=9_aliU8dGd2tb6OSsuzixeV4y_faTq
55
56
  cucu/reporter/external/jquery.dataTables.min.js,sha256=XNhaB1tBOSFMHu96BSAJpZOJzfZ4SZI1nwAbnwry2UY,90265
56
57
  cucu/reporter/external/popper.min.js,sha256=pS96pU17yq-gVu4KBQJi38VpSuKN7otMrDQprzf_DWY,19188
57
58
  cucu/reporter/favicon.png,sha256=9ikXLAmzfQzy2NQps_8CGaZog2FvQrOX8nnSZ0e1UmM,2161
58
- cucu/reporter/html.py,sha256=jaaLFi5KU2RYHvqJyLZJ6APJ4MQn1Nz_sOK2q_mYz7U,15967
59
+ cucu/reporter/html.py,sha256=2BAZMYbB-xMZdQXZeE8FgWJdWmScGY8JHWaQqiBP03I,19143
59
60
  cucu/reporter/templates/feature.html,sha256=IBkwGiul-sRO5lT8q8VFXMUJx1owsAd1YbdDzziSjKw,3645
60
61
  cucu/reporter/templates/flat.html,sha256=JGsMq-IWz6YUpJX9hcN65-15HxcX3NJclOmMDtW3HZE,2358
61
- cucu/reporter/templates/index.html,sha256=LFth3SS__9881NKvPIIFdnrQEIcDTXWvToSWKNtjyKI,2726
62
+ cucu/reporter/templates/index.html,sha256=xgPYNU-sozN-iOaEzyymoQ4LDRI75eHXngbAP0xDYls,2770
62
63
  cucu/reporter/templates/layout.html,sha256=2iDRbm8atO8mgHWgijIvDCrBMKvcP6YHrmr95WtJiE4,4561
63
- cucu/reporter/templates/scenario.html,sha256=vTDBDTftFGodtNozfVt7Ol-fppTqPh1bW31_ENc1CWQ,10115
64
+ cucu/reporter/templates/scenario.html,sha256=Vgl8A2O_hwG2W6JD5XtJi7xmTTWRe4XyqgfjfB1QmgM,10119
64
65
  cucu/steps/__init__.py,sha256=seSmASBlWu6-6wbFbvEbPwigBcRXiYP18C4X_2cW8Ng,753
65
66
  cucu/steps/base_steps.py,sha256=0fPvdaKoan8lMAKrDnK0-zrALpxm11P1zVAY5CN7iXA,1893
66
67
  cucu/steps/browser_steps.py,sha256=iTRl5ffpf2YrFk5qh655WFHAeSOwoE3HFhmXhjsZtao,12687
@@ -71,7 +72,7 @@ cucu/steps/draggable_steps.py,sha256=lnQLicp0GZJaxD_Qm2P13ruUZAsl3mptwaI5-SQ6XJ0
71
72
  cucu/steps/dropdown_steps.py,sha256=abykG--m79kDQ4LU1tm73fNLFPmrKDavyFzJb2MYCu0,15601
72
73
  cucu/steps/file_input_steps.py,sha256=LLMAozVpceLMD-kJOE-auKHAdWLbNprH8eCfVQuNoGg,5523
73
74
  cucu/steps/filesystem_steps.py,sha256=8l37A-yPxT4Mzdi1JNSTShZkwyFxgSwnh6C0V-hM_RA,4741
74
- cucu/steps/flow_control_steps.py,sha256=vlW0CsphVS9NvrOnpT8wSS2ngHmO3Z87H9siKIQwsAw,6365
75
+ cucu/steps/flow_control_steps.py,sha256=UV-rWKOF1OqLaDKhMfSAqAm5TF81T5_j_e3eYGpE6QE,6401
75
76
  cucu/steps/image_steps.py,sha256=4X6bdumsIybcJBuao83TURxWAIshZyCvKi1uTJEoy1k,941
76
77
  cucu/steps/input_steps.py,sha256=TYQhkmke-W5dJt4EsU-JHjRwnbd7zvXnhEjw5PQ4Wxs,9679
77
78
  cucu/steps/link_steps.py,sha256=MQLxyjCbiF2dOsmj6IrKKlRYhaqJjxDUNTf5Cau4J0w,1625
@@ -87,7 +88,7 @@ cucu/steps/text_steps.py,sha256=Jj_GHoHeemNwVdUOdqcehArNp7WM-WMjljA4w0pLXuw,2576
87
88
  cucu/steps/variable_steps.py,sha256=WSctH3_xcxjijGPYZlxp-foC_SIAAKtF__saNtgZJbk,2966
88
89
  cucu/steps/webserver_steps.py,sha256=wWkpSvcSMdiskPkh4cqlepWx1nkvEpTU2tRXQmPDbyo,1410
89
90
  cucu/utils.py,sha256=LCcs8sMzvdvH05N8P5QYO4lO6j-_PQC530mEAD96go8,10957
90
- cucu-1.3.5.dist-info/WHEEL,sha256=4n27za1eEkOnA7dNjN6C5-O2rUiw6iapszm14Uj-Qmk,79
91
- cucu-1.3.5.dist-info/entry_points.txt,sha256=11WRIhQM7LuUnQg1lAoZQoNvvBvYNN1maDgQS4djwJo,40
92
- cucu-1.3.5.dist-info/METADATA,sha256=Z21XbbuwgJdBjh_x_anWZZTrPDKnqCGTw31VVkmuJBQ,16705
93
- cucu-1.3.5.dist-info/RECORD,,
91
+ cucu-1.3.7.dist-info/WHEEL,sha256=-neZj6nU9KAMg2CnCY6T3w8J53nx1kFGw_9HfoSzM60,79
92
+ cucu-1.3.7.dist-info/entry_points.txt,sha256=11WRIhQM7LuUnQg1lAoZQoNvvBvYNN1maDgQS4djwJo,40
93
+ cucu-1.3.7.dist-info/METADATA,sha256=odrZlSjtaYwYeWjol9l24U6-UFjMCw3WbSQ_X5dgAEw,16721
94
+ cucu-1.3.7.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.8.13
2
+ Generator: uv 0.8.22
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any