cucu 1.2.4__py3-none-any.whl → 1.2.5__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
@@ -30,10 +30,11 @@ from cucu import (
30
30
  reporter,
31
31
  )
32
32
  from cucu.cli import thread_dumper
33
- from cucu.cli.run import behave, behave_init, write_run_details
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
36
  from cucu.lint import linter
37
+ from cucu.utils import generate_short_id
37
38
 
38
39
  # will start coverage tracking once COVERAGE_PROCESS_START is set
39
40
  coverage.process_startup()
@@ -297,8 +298,12 @@ def run(
297
298
  if record_env_vars:
298
299
  os.environ["CUCU_RECORD_ENV_VARS"] = "true"
299
300
 
301
+ os.environ["CUCU_RUN_ID"] = CONFIG["CUCU_RUN_ID"] = generate_short_id()
302
+ CONFIG["WORKER_RUN_ID"] = "parent"
300
303
  if not dry_run:
301
- write_run_details(results, filepath)
304
+ create_run(results, filepath)
305
+
306
+ CONFIG.snapshot("core_run")
302
307
 
303
308
  try:
304
309
  if workers is None or workers == 1:
@@ -486,6 +491,8 @@ def run(
486
491
  "there are failures, see above for details"
487
492
  )
488
493
  finally:
494
+ CONFIG.restore(with_pop=True)
495
+
489
496
  if dumper is not None:
490
497
  dumper.stop()
491
498
 
@@ -499,7 +506,10 @@ def run(
499
506
 
500
507
 
501
508
  def _generate_report(
502
- filepath: str, output: str, only_failures: False, junit: str | None = None
509
+ results_dir: str,
510
+ output: str,
511
+ only_failures: False,
512
+ junit: str | None = None,
503
513
  ):
504
514
  """
505
515
  helper method to handle report generation so it can be used by the `cucu report`
@@ -508,7 +518,7 @@ def _generate_report(
508
518
 
509
519
 
510
520
  parameters:
511
- filepath(string): the results directory containing the previous test run
521
+ results_dir(string): the results directory containing the previous test run
512
522
  output(string): the directory where we'll generate the report
513
523
  only_failures(bool, optional): if only report failures. The default is False.
514
524
  junit(str|None, optional): the directory of the JUnit files. The default if None.
@@ -519,7 +529,7 @@ def _generate_report(
519
529
  os.makedirs(output)
520
530
 
521
531
  report_location = reporter.generate(
522
- filepath, output, only_failures=only_failures
532
+ results_dir, output, only_failures=only_failures
523
533
  )
524
534
  print(f"HTML test report at {report_location}")
525
535
 
cucu/cli/run.py CHANGED
@@ -4,6 +4,7 @@ import os
4
4
  import socket
5
5
  import sys
6
6
  from datetime import datetime
7
+ from pathlib import Path
7
8
 
8
9
  from cucu import (
9
10
  behave_tweaks,
@@ -16,13 +17,12 @@ from cucu.page_checks import init_page_checks
16
17
 
17
18
 
18
19
  def get_feature_name(file_path):
19
- with open(file_path, "r") as file:
20
- text = file.read()
21
- lines = text.split("\n")
22
- for line in lines:
23
- if "Feature:" in line:
24
- feature_name = line.replace("Feature:", "").strip()
25
- return feature_name
20
+ text = Path(file_path).read_text(encoding="utf8")
21
+ lines = text.split("\n")
22
+ for line in lines:
23
+ if "Feature:" in line:
24
+ feature_name = line.replace("Feature:", "").strip()
25
+ return feature_name
26
26
 
27
27
 
28
28
  def behave_init(filepath="features"):
@@ -104,7 +104,7 @@ def behave(
104
104
  run_json_filename = "run.json"
105
105
  if redirect_output:
106
106
  feature_name = get_feature_name(filepath)
107
- run_json_filename = f"{feature_name + '-run.json'}"
107
+ run_json_filename = f"{feature_name}-run.json"
108
108
 
109
109
  if dry_run:
110
110
  args += [
@@ -120,7 +120,7 @@ def behave(
120
120
  "--no-logcapture",
121
121
  # generate a JSON file containing the exact details of the whole run
122
122
  "--format=cucu.formatter.json:CucuJSONFormatter",
123
- f"--outfile={results}/{run_json_filename}",
123
+ f"--outfile={Path(results) / run_json_filename}",
124
124
  # console formatter
125
125
  "--format=cucu.formatter.cucu:CucuFormatter",
126
126
  f"--logging-level={os.environ['CUCU_LOGGING_LEVEL'].upper()}",
@@ -148,8 +148,8 @@ def behave(
148
148
  try:
149
149
  if redirect_output:
150
150
  feature_name = get_feature_name(filepath)
151
- log_filename = f"{feature_name + '.log'}"
152
- log_filepath = os.path.join(results, log_filename)
151
+ log_filename = f"{feature_name}.log"
152
+ log_filepath = Path(results) / log_filename
153
153
 
154
154
  CONFIG["__CUCU_PARENT_STDOUT"] = sys.stdout
155
155
 
@@ -161,7 +161,7 @@ def behave(
161
161
  # provide progress feedback on screen
162
162
  register_before_retry_hook(retry_progress)
163
163
 
164
- with open(log_filepath, "w", encoding="utf8") as output:
164
+ with log_filepath.open("w", encoding="utf8") as output:
165
165
  with contextlib.redirect_stderr(output):
166
166
  with contextlib.redirect_stdout(output):
167
167
  # intercept the stdout/stderr so we can do things such
@@ -180,28 +180,27 @@ def behave(
180
180
  return result
181
181
 
182
182
 
183
- def write_run_details(results, filepath):
184
- """
185
- writes a JSON file with run details to the results directory which can be
186
- used to figure out any runtime details that would otherwise be lost and
187
- difficult to figure out.
188
- """
189
- run_details_filepath = os.path.join(results, "run_details.json")
183
+ def create_run(results, filepath):
184
+ results_path = Path(results)
185
+ run_json_filepath = results_path / "run_details.json"
190
186
 
191
- if os.path.exists(run_details_filepath):
187
+ if run_json_filepath.exists():
192
188
  return
193
189
 
194
- if CONFIG["CUCU_RECORD_ENV_VARS"]:
195
- env_values = dict(os.environ)
196
- else:
197
- env_values = "To enable use the --record-env-vars flag"
190
+ env_values = (
191
+ dict(os.environ)
192
+ if CONFIG["CUCU_RECORD_ENV_VARS"]
193
+ else "To enable use the --record-env-vars flag"
194
+ )
198
195
 
199
196
  run_details = {
197
+ "cucu_run_id": CONFIG["CUCU_RUN_ID"],
200
198
  "filepath": filepath,
201
199
  "full_arguments": sys.argv,
202
200
  "env": env_values,
203
201
  "date": datetime.now().isoformat(),
204
202
  }
205
203
 
206
- with open(run_details_filepath, "w", encoding="utf8") as output:
207
- output.write(json.dumps(run_details, indent=2, sort_keys=True))
204
+ run_json_filepath.write_text(
205
+ json.dumps(run_details, indent=2, sort_keys=True), encoding="utf8"
206
+ )
cucu/config.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import json
2
+ import logging
2
3
  import os
3
4
  import re
4
5
  import socket
@@ -15,6 +16,7 @@ class Config(dict):
15
16
  self.resolving = False
16
17
  self.defined_variables = {}
17
18
  self.variable_lookups = {}
19
+ self.snapshots = []
18
20
 
19
21
  def define(self, name, description, default=None):
20
22
  """
@@ -255,19 +257,37 @@ class Config(dict):
255
257
 
256
258
  return string
257
259
 
258
- def snapshot(self):
259
- """
260
- make a shallow copy of the current config values which can later be
261
- restored using the `restore` method.
262
- """
263
- self.snapshot_data = self.copy()
260
+ def snapshot(self, name=None):
261
+ if name is None:
262
+ name = f"snapshot_{len(self.snapshots)}"
263
+
264
+ snapshot_data = {"name": name, "config": self.copy()}
265
+ self.snapshots.append(snapshot_data)
266
+
267
+ logging.debug(
268
+ f"CONFIG: snapshot taken '{name}' (stack depth: {len(self.snapshots)})"
269
+ )
270
+
271
+ def restore(self, with_pop=False):
272
+ if not self.snapshots:
273
+ return
274
+
275
+ if with_pop:
276
+ latest_snapshot = self.snapshots.pop()
277
+ action = "popped and restored"
278
+ else:
279
+ latest_snapshot = self.snapshots[-1]
280
+ action = "restored to"
264
281
 
265
- def restore(self):
266
- """
267
- restore a previous `snapshot`
268
- """
269
282
  self.clear()
270
- self.update(**self.snapshot_data)
283
+ self.update(**latest_snapshot["config"])
284
+
285
+ logging.debug(
286
+ f"CONFIG: {action} snapshot '{latest_snapshot['name']}' (stack depth: {len(self.snapshots)})"
287
+ )
288
+
289
+ def list_snapshots(self):
290
+ return [snapshot["name"] for snapshot in self.snapshots]
271
291
 
272
292
  def register_custom_variable_handling(self, regex, lookup):
273
293
  """
cucu/environment.py CHANGED
@@ -1,22 +1,22 @@
1
- import datetime
2
- import hashlib
3
1
  import json
4
- import os
5
2
  import sys
6
- import time
7
3
  import traceback
4
+ from datetime import datetime
8
5
  from functools import partial
6
+ from pathlib import Path
7
+
8
+ import yaml
9
9
 
10
10
  from cucu import config, init_scenario_hook_variables, logger
11
11
  from cucu.config import CONFIG
12
12
  from cucu.page_checks import init_page_checks
13
- from cucu.utils import ellipsize_filename, take_screenshot
14
-
15
- CONFIG.define(
16
- "FEATURE_RESULTS_DIR",
17
- "the results directory for the currently executing feature",
18
- default=None,
13
+ from cucu.utils import (
14
+ TeeStream,
15
+ ellipsize_filename,
16
+ generate_short_id,
17
+ take_screenshot,
19
18
  )
19
+
20
20
  CONFIG.define(
21
21
  "SCENARIO_RESULTS_DIR",
22
22
  "the results directory for the currently executing scenario",
@@ -44,8 +44,12 @@ def check_browser_initialized(ctx):
44
44
 
45
45
  def before_all(ctx):
46
46
  CONFIG["__CUCU_CTX"] = ctx
47
- CONFIG.snapshot()
48
47
  ctx.check_browser_initialized = partial(check_browser_initialized, ctx)
48
+ ctx.worker_custom_data = {}
49
+
50
+ CONFIG["WORKER_RUN_ID"] = generate_short_id()
51
+ CONFIG.snapshot("before_all")
52
+
49
53
  for hook in CONFIG["__CUCU_BEFORE_ALL_HOOKS"]:
50
54
  hook(ctx)
51
55
 
@@ -55,14 +59,16 @@ def after_all(ctx):
55
59
  for hook in CONFIG["__CUCU_AFTER_ALL_HOOKS"]:
56
60
  hook(ctx)
57
61
 
62
+ CONFIG.restore(with_pop=True)
63
+
58
64
 
59
65
  def before_feature(ctx, feature):
66
+ feature.feature_run_id = generate_short_id()
67
+ feature.custom_data = {}
68
+
60
69
  if config.CONFIG["CUCU_RESULTS_DIR"] is not None:
61
- results_dir = config.CONFIG["CUCU_RESULTS_DIR"]
62
- ctx.feature_dir = os.path.join(
63
- results_dir, ellipsize_filename(feature.name)
64
- )
65
- CONFIG["FEATURE_RESULTS_DIR"] = ctx.feature_dir
70
+ results_dir = Path(config.CONFIG["CUCU_RESULTS_DIR"])
71
+ ctx.feature_dir = results_dir / ellipsize_filename(feature.name)
66
72
 
67
73
 
68
74
  def after_feature(ctx, feature):
@@ -81,50 +87,54 @@ def before_scenario(ctx, scenario):
81
87
 
82
88
  init_scenario_hook_variables()
83
89
 
90
+ scenario.custom_data = {}
84
91
  ctx.scenario = scenario
85
92
  ctx.step_index = 0
93
+ ctx.scenario_index = ctx.feature.scenarios.index(scenario) + 1
86
94
  ctx.browsers = []
87
95
  ctx.browser = None
96
+ ctx.section_step_stack = []
88
97
 
89
98
  # reset the step timer dictionary
90
99
  ctx.step_timers = {}
100
+ scenario.start_at = datetime.now().isoformat()[:-3]
91
101
 
92
102
  if config.CONFIG["CUCU_RESULTS_DIR"] is not None:
93
- ctx.scenario_dir = os.path.join(
94
- ctx.feature_dir, ellipsize_filename(scenario.name)
95
- )
103
+ ctx.scenario_dir = ctx.feature_dir / ellipsize_filename(scenario.name)
96
104
  CONFIG["SCENARIO_RESULTS_DIR"] = ctx.scenario_dir
97
- os.makedirs(ctx.scenario_dir, exist_ok=True)
105
+ ctx.scenario_dir.mkdir(parents=True, exist_ok=True)
98
106
 
99
- ctx.scenario_downloads_dir = os.path.join(
100
- ctx.scenario_dir, "downloads"
101
- )
107
+ ctx.scenario_downloads_dir = ctx.scenario_dir / "downloads"
102
108
  CONFIG["SCENARIO_DOWNLOADS_DIR"] = ctx.scenario_downloads_dir
103
- os.makedirs(ctx.scenario_downloads_dir, exist_ok=True)
109
+ ctx.scenario_downloads_dir.mkdir(parents=True, exist_ok=True)
104
110
 
105
- ctx.scenario_logs_dir = os.path.join(ctx.scenario_dir, "logs")
111
+ ctx.scenario_logs_dir = ctx.scenario_dir / "logs"
106
112
  CONFIG["SCENARIO_LOGS_DIR"] = ctx.scenario_logs_dir
107
- os.makedirs(ctx.scenario_logs_dir, exist_ok=True)
113
+ ctx.scenario_logs_dir.mkdir(parents=True, exist_ok=True)
108
114
 
109
- cucu_debug_filepath = os.path.join(
110
- ctx.scenario_logs_dir, "cucu.debug.console.log"
111
- )
115
+ cucu_debug_log_path = ctx.scenario_logs_dir / "cucu.debug.console.log"
112
116
  ctx.scenario_debug_log_file = open(
113
- cucu_debug_filepath, "w", encoding=sys.stdout.encoding
117
+ cucu_debug_log_path, "w", encoding=sys.stdout.encoding
114
118
  )
119
+ ctx.scenario_debug_log_tee = TeeStream(ctx.scenario_debug_log_file)
115
120
 
116
121
  # redirect stdout, stderr and setup a logger at debug level to fill
117
122
  # the scenario cucu.debug.log file which makes it possible to have
118
123
  # debug logging for every single scenario run without polluting the
119
124
  # console logs at runtime.
120
- sys.stdout.set_other_stream(ctx.scenario_debug_log_file)
121
- sys.stderr.set_other_stream(ctx.scenario_debug_log_file)
122
- logger.init_debug_logger(ctx.scenario_debug_log_file)
125
+ sys.stdout.set_other_stream(ctx.scenario_debug_log_tee)
126
+ sys.stderr.set_other_stream(ctx.scenario_debug_log_tee)
127
+ logger.init_debug_logger(ctx.scenario_debug_log_tee)
128
+
129
+ # capture browser logs using TeeStream since each call clears the log
130
+ ctx.browser_log_file = open(
131
+ ctx.scenario_logs_dir / "browser_console.log.txt",
132
+ "w",
133
+ encoding="utf-8",
134
+ )
135
+ ctx.browser_log_tee = TeeStream(ctx.browser_log_file)
123
136
 
124
- # internal cucu config variables
125
- CONFIG["SCENARIO_RUN_ID"] = hashlib.sha256(
126
- str(time.perf_counter()).encode("utf-8")
127
- ).hexdigest()[:7]
137
+ CONFIG["SCENARIO_RUN_ID"] = scenario.scenario_run_id = generate_short_id()
128
138
 
129
139
  # run before all scenario hooks
130
140
  for hook in CONFIG["__CUCU_BEFORE_SCENARIO_HOOKS"]:
@@ -179,14 +189,18 @@ def after_scenario(ctx, scenario):
179
189
  if len(ctx.browsers) != 0:
180
190
  logger.debug("quitting browser between sessions")
181
191
 
182
- run_after_scenario_hook(ctx, scenario, download_browser_logs)
192
+ run_after_scenario_hook(ctx, scenario, download_browser_log)
183
193
 
184
- cucu_config_filepath = os.path.join(
185
- ctx.scenario_logs_dir, "cucu.config.yaml.txt"
186
- )
187
- with open(cucu_config_filepath, "w") as config_file:
194
+ cucu_config_path = ctx.scenario_logs_dir / "cucu.config.yaml.txt"
195
+ with open(cucu_config_path, "w") as config_file:
188
196
  config_file.write(CONFIG.to_yaml_without_secrets())
189
197
 
198
+ scenario.cucu_config_json = json.dumps(
199
+ yaml.safe_load(CONFIG.to_yaml_without_secrets())
200
+ )
201
+
202
+ scenario.end_at = datetime.now().isoformat()[:-3]
203
+
190
204
 
191
205
  def download_mht_data(ctx):
192
206
  if not ctx.browsers:
@@ -198,45 +212,42 @@ def download_mht_data(ctx):
198
212
  mht_filename = (
199
213
  f"browser{index if len(ctx.browsers) > 1 else ''}_snapshot.mht"
200
214
  )
201
- mht_pathname = os.path.join(
202
- CONFIG["SCENARIO_LOGS_DIR"],
203
- mht_filename,
204
- )
215
+ mht_pathname = CONFIG["SCENARIO_LOGS_DIR"] / mht_filename
205
216
  logger.debug(f"Saving MHT webpage snapshot: {mht_filename}")
206
217
  browser.download_mht(mht_pathname)
207
218
 
208
219
 
209
- def download_browser_logs(ctx):
220
+ def download_browser_log(ctx):
210
221
  # close the browser unless someone has set the keep browser alive
211
222
  # environment variable which allows tests to reuse the same browser
212
223
  # session
213
224
 
214
225
  for browser in ctx.browsers:
215
- # save the browser logs to the current scenarios results directory
216
- browser_log_filepath = os.path.join(
217
- ctx.scenario_logs_dir, "browser_console.log.txt"
218
- )
219
-
220
- os.makedirs(os.path.dirname(browser_log_filepath), exist_ok=True)
221
- with open(browser_log_filepath, "w") as output:
222
- for log in browser.get_log():
223
- output.write(f"{json.dumps(log)}\n")
224
-
225
226
  browser.quit()
226
227
 
228
+ ctx.browser_log_file.close()
229
+
227
230
  ctx.browsers = []
228
231
 
229
232
 
230
233
  def before_step(ctx, step):
231
- # trims the last 3 digits of the microseconds
232
- step.start_timestamp = datetime.datetime.now().isoformat()[:-3]
234
+ step.step_run_id = generate_short_id()
235
+ step.start_at = datetime.now().isoformat()[:-3]
233
236
 
234
237
  sys.stdout.captured()
235
238
  sys.stderr.captured()
236
239
 
240
+ # Reset the debug log buffer for this step
241
+ if hasattr(ctx, "scenario_debug_log_tee"):
242
+ ctx.scenario_debug_log_tee.clear()
243
+
237
244
  ctx.current_step = step
238
245
  ctx.current_step.has_substeps = False
239
- ctx.start_time = time.monotonic()
246
+ ctx.section_level = None
247
+ step.seq = ctx.step_index + 1
248
+ step.parent_seq = (
249
+ ctx.section_step_stack[-1].seq if ctx.section_step_stack else 0
250
+ )
240
251
 
241
252
  CONFIG["__STEP_SCREENSHOT_COUNT"] = 0
242
253
 
@@ -249,8 +260,18 @@ def after_step(ctx, step):
249
260
  step.stdout = sys.stdout.captured()
250
261
  step.stderr = sys.stderr.captured()
251
262
 
252
- ctx.end_time = time.monotonic()
253
- ctx.previous_step_duration = ctx.end_time - ctx.start_time
263
+ # Capture debug output from the TeeStream for this step
264
+ if hasattr(ctx, "scenario_debug_log_tee"):
265
+ step.debug_output = ctx.scenario_debug_log_tee.getvalue()
266
+ else:
267
+ step.debug_output = ""
268
+
269
+ step.end_at = datetime.now().isoformat()[:-3]
270
+
271
+ # calculate duration from ISO timestamps
272
+ start_at = datetime.fromisoformat(step.start_at)
273
+ end_at = datetime.fromisoformat(step.end_at)
274
+ ctx.previous_step_duration = (end_at - start_at).total_seconds()
254
275
 
255
276
  # when set this means we're running in parallel mode using --workers and
256
277
  # we want to see progress reported using simply dots
@@ -296,3 +317,26 @@ def after_step(ctx, step):
296
317
  # run after all step hooks
297
318
  for hook in CONFIG["__CUCU_AFTER_STEP_HOOKS"]:
298
319
  hook(ctx)
320
+
321
+ # Capture browser logs and info for this step
322
+ step.browser_logs = ""
323
+
324
+ browser_info = {"has_browser": False}
325
+ if ctx.browser:
326
+ browser_logs = []
327
+ for log in ctx.browser.get_log():
328
+ log_entry = json.dumps(log)
329
+ browser_logs.append(log_entry)
330
+ ctx.browser_log_tee.write(f"{log_entry}\n")
331
+ step.browser_logs = "\n".join(browser_logs)
332
+
333
+ tab_info = ctx.browser.get_tab_info()
334
+ all_tabs = ctx.browser.get_all_tabs_info()
335
+
336
+ browser_info = {
337
+ "current_tab_index": tab_info["index"],
338
+ "all_tabs": all_tabs,
339
+ "browser_type": ctx.browser.driver.name,
340
+ }
341
+
342
+ step.browser_info = json.dumps(browser_info)
cucu/formatter/cucu.py CHANGED
@@ -185,7 +185,7 @@ class CucuFormatter(Formatter):
185
185
  max_line_length = self.calculate_max_line_length()
186
186
  status_text = ""
187
187
  if self.show_timings:
188
- start = step.start_timestamp
188
+ start = step.start_at
189
189
  duration = f"{step.duration:.3f}"
190
190
  status_text += f" # started at {start} took {duration}s"
191
191
 
cucu/formatter/json.py CHANGED
@@ -157,7 +157,7 @@ class CucuJSONFormatter(Formatter):
157
157
 
158
158
  timestamp = None
159
159
  if step.status.name in ["passed", "failed"]:
160
- timestamp = step.start_timestamp
160
+ timestamp = step.start_at
161
161
 
162
162
  step_variables = CONFIG.expand(step.name)
163
163
 
@@ -1,3 +1,5 @@
1
+ import logging
2
+
1
3
  from behave import use_step_matcher
2
4
 
3
5
  from cucu import step
@@ -6,7 +8,7 @@ use_step_matcher("re") # use regex to match section heading patterns
6
8
 
7
9
 
8
10
  @step("(#{1,4})\\s*(.*)")
9
- def section_step(ctx, heading_level, section_text):
11
+ def section_step(ctx, section_level, section_text):
10
12
  """
11
13
  A section heading step that organizes scenarios into logical sections.
12
14
 
@@ -19,7 +21,24 @@ def section_step(ctx, heading_level, section_text):
19
21
  The number of # characters determines the heading level (1-4).
20
22
  This step is a no-op but provides structure in the HTML report.
21
23
  """
22
- pass
24
+ step = ctx.current_step
25
+ step.section_level = len(section_level)
26
+ step.parent_seq = 0
27
+
28
+ while len(ctx.section_step_stack):
29
+ latest_section = ctx.section_step_stack[-1]
30
+ if latest_section.section_level < step.section_level:
31
+ step.parent_seq = latest_section.seq
32
+ break
33
+ ctx.section_step_stack.pop()
34
+ logging.debug(
35
+ f"Section: exited '{latest_section.name}' (level {latest_section.section_level})"
36
+ )
37
+
38
+ ctx.section_step_stack.append(ctx.current_step)
39
+ logging.debug(
40
+ f"Section: entering '{step.name}' (level {step.section_level})"
41
+ )
23
42
 
24
43
 
25
44
  use_step_matcher("parse") # set this back to cucu's default matcher parser
cucu/utils.py CHANGED
@@ -3,10 +3,12 @@ various cucu utilities can be placed here and then exposed publicly through
3
3
  the src/cucu/__init__.py
4
4
  """
5
5
 
6
+ import hashlib
6
7
  import logging
7
8
  import os
8
9
  import pkgutil
9
10
  import shutil
11
+ import time
10
12
 
11
13
  import humanize
12
14
  from selenium.webdriver.common.by import By
@@ -41,6 +43,16 @@ class StopRetryException(Exception):
41
43
  pass
42
44
 
43
45
 
46
+ def generate_short_id():
47
+ """
48
+ Generate a short 7-character ID based on current performance counter.
49
+ Used for both cucu_run_id and scenario_run_id.
50
+ """
51
+ return hashlib.sha256(
52
+ str(time.perf_counter()).encode("utf-8")
53
+ ).hexdigest()[:7]
54
+
55
+
44
56
  def format_gherkin_table(table, headings=[], prefix=""):
45
57
  formatted = tabulate(table, headings, tablefmt=GHERKIN_TABLEFORMAT)
46
58
  if prefix == "":
@@ -76,7 +88,7 @@ def run_steps(ctx, steps_text):
76
88
  steps = ctx.feature.parser.parse_steps(steps_text)
77
89
 
78
90
  current_step = ctx.current_step
79
- current_step_start_time = ctx.start_time
91
+ current_step_start_at = current_step.start_at
80
92
 
81
93
  # XXX: I want to get back to this and find a slightly better way to handle
82
94
  # these substeps without mucking around with so much state in behave
@@ -101,7 +113,7 @@ def run_steps(ctx, steps_text):
101
113
  ctx.text = original_text
102
114
  finally:
103
115
  ctx.current_step = current_step
104
- ctx.start_time = current_step_start_time
116
+ ctx.current_step.start_at = current_step_start_at
105
117
 
106
118
  return True
107
119
 
@@ -222,6 +234,10 @@ def take_saw_element_screenshot(ctx, thing, name, index, element=None):
222
234
 
223
235
 
224
236
  def take_screenshot(ctx, step_name, label="", element=None):
237
+ step = ctx.current_step
238
+ if not hasattr(step, "screenshots"):
239
+ step.screenshots = []
240
+
225
241
  screenshot_dir = os.path.join(
226
242
  ctx.scenario_dir, get_step_image_dir(ctx.step_index, step_name)
227
243
  )
@@ -268,6 +284,20 @@ def take_screenshot(ctx, step_name, label="", element=None):
268
284
  """
269
285
  ctx.browser.execute(clear_highlight, element)
270
286
 
287
+ screenshot = {
288
+ "step_name": step_name,
289
+ "label": label,
290
+ "element": element,
291
+ "location": f"({element.location['x']},{element.location['y']})"
292
+ if element
293
+ else "",
294
+ "size": f"({element.size['width']},{element.size['height']})"
295
+ if element
296
+ else "",
297
+ "filepath": filepath,
298
+ }
299
+ step.screenshots.append(screenshot)
300
+
271
301
  if CONFIG["CUCU_MONITOR_PNG"]:
272
302
  shutil.copyfile(filepath, CONFIG["CUCU_MONITOR_PNG"])
273
303
 
@@ -297,3 +327,31 @@ def find_n_click_input_parent_label(ctx, input_element):
297
327
  def is_element_size_zero(element):
298
328
  size = element.size
299
329
  return size["width"] == 0 and size["height"] == 0
330
+
331
+
332
+ class TeeStream:
333
+ """
334
+ A stream that writes to both a file stream and captures content in an internal buffer.
335
+ Provides file-like accessors to read the captured content.
336
+ """
337
+
338
+ def __init__(self, file_stream):
339
+ self.file_stream = file_stream
340
+ self.string_buffer = []
341
+
342
+ def write(self, data):
343
+ self.file_stream.write(data)
344
+ self.string_buffer.append(data)
345
+
346
+ def flush(self):
347
+ self.file_stream.flush()
348
+
349
+ def getvalue(self):
350
+ return "".join(self.string_buffer)
351
+
352
+ def read(self):
353
+ return self.getvalue()
354
+
355
+ def clear(self):
356
+ """Clear the internal buffer."""
357
+ self.string_buffer = []
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cucu
3
- Version: 1.2.4
3
+ Version: 1.2.5
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
@@ -7,19 +7,19 @@ cucu/browser/frames.py,sha256=216ee4cd1267e4f91b31aa2f21e94078258ef93fb6b0e4f0a9
7
7
  cucu/browser/selenium.py,sha256=7940b60d9921508662ef4b154da344ff40216a719371ecb1fe908b213ecf37aa,13299
8
8
  cucu/browser/selenium_tweaks.py,sha256=a1422159584165b73daac99050932922bf6e5162b1b60641727c92594e98b6d9,879
9
9
  cucu/cli/__init__.py,sha256=b975f9c951b59289c9fc835d96b71f320f93b7fe7f712c76f4477c47cbfdd3c0,62
10
- cucu/cli/core.py,sha256=b00bd1222ff671de0e42228a21293b59e6f9faee4a58ad4335b6b2a9abfcb696,26915
11
- cucu/cli/run.py,sha256=b9939fd5ce730174f44e1690beb6f99205c6c25f34940b476939b9743c289678,5909
10
+ cucu/cli/core.py,sha256=23014432183140fe2f1adfe644205151f8d367a0b6e2dd0ee18800c9aad9d8a7,27151
11
+ cucu/cli/run.py,sha256=9e30943b42f71c2f16113ffb913ce07d36a721938420b30c28cd9d08ea7dae42,5713
12
12
  cucu/cli/steps.py,sha256=960e62b551ff0bed3fdd17a5597bfd5f6a90507820771bb12c21b01f99756dfe,4210
13
13
  cucu/cli/thread_dumper.py,sha256=6775e7612c62771ea9aa3950ef3bbe4ca3086195a4e33f5ce582dd3e471e9a25,1593
14
- cucu/config.py,sha256=b0096acc17efe448b00b89ba04a70c264224906228cf67867760be2643f27f24,14305
14
+ cucu/config.py,sha256=e4013a1ab92ace3361cf3ac1f9e66b20d81eced57b0821ae49d589fb91ad5a19,14939
15
15
  cucu/edgedriver_autoinstaller/README.md,sha256=b43900588aa045d0a3b7ea17d6762a8a4202fc57e2384336fa97394b955b44ba,84
16
16
  cucu/edgedriver_autoinstaller/__init__.py,sha256=7e8eb12493ef71ce57be78bc7a95dfc43a0fc491f9fdbe959368c3f494f07d1b,969
17
17
  cucu/edgedriver_autoinstaller/utils.py,sha256=891293c30efb086693027b6dfd00efc6528fc69314e28b71f7939e0fdee485c3,6802
18
- cucu/environment.py,sha256=353aaf8dd747aefe133e1f4536aab9c67c2adb5a297f91eb29925bdce8b6720e,9950
18
+ cucu/environment.py,sha256=1ec72f24abb3ae9ff214a3b6a44f00b0e42e0258399b13d8f21d5d1ab39a8d0f,11426
19
19
  cucu/external/jquery/jquery-3.5.1.min.js,sha256=f7f6a5894f1d19ddad6fa392b2ece2c5e578cbf7da4ea805b6885eb6985b6e3d,89476
20
20
  cucu/formatter/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
21
- cucu/formatter/cucu.py,sha256=2424ab0e3b9933b8aef37b35fa436e3f6bac0e43b1fd4e94d2fd797959a6cab2,9298
22
- cucu/formatter/json.py,sha256=f17d67de3040973bfcac2fd2f89a7e5377233e7bc6cd733a3018c0b9d82b00f5,10596
21
+ cucu/formatter/cucu.py,sha256=8c78ba5bd4934508c3cb48f53ff1cb2e50533ec4ba12f532bea7948ed5acdef3,9291
22
+ cucu/formatter/json.py,sha256=7c9d5d6411b0603e0e9214050c4e3d311286373b2b843cd062af9d51fb1887de,10589
23
23
  cucu/formatter/junit.py,sha256=689f5d18b6da98c1fec148bf9ac17aebffbf73f614ab4efef3fc023448d48eee,10129
24
24
  cucu/fuzzy/__init__.py,sha256=71ee0946668117aa1a6fa53df506e9b7b0eb0f77a5847df6ffff8d0fa7f0e717,104
25
25
  cucu/fuzzy/core.py,sha256=b6640a5ff362fb6a21a31cdcb51520db1ef331e116f0c949266a54dcf7d39af4,3153
@@ -77,7 +77,7 @@ cucu/steps/link_steps.py,sha256=3102f1ca309b885d9d3ac9a3e88aca2a545885aa898f10d4
77
77
  cucu/steps/menuitem_steps.py,sha256=f4995a3cee814a65b618f9a4e378311b2e52d37ac4b44c78284d2e0850648e4d,1133
78
78
  cucu/steps/platform_steps.py,sha256=1bb1ed04c85d46ee7cfb6fcb7570b9ae424cf45d62bc676545af41cd388769a6,754
79
79
  cucu/steps/radio_steps.py,sha256=17281434f084057ce6cc63072fc0d836d2def6ae2a8dec884e244b1fee4d6da8,5931
80
- cucu/steps/section_steps.py,sha256=5de4af2e47fba3f548a15830cb1037ebef47ffd5c852200027a11b22a7ac8481,645
80
+ cucu/steps/section_steps.py,sha256=041b11b494ee6ff2f582b2a78110a3eedf9ff12e2284e421b7af130b33c7671c,1283
81
81
  cucu/steps/step_utils.py,sha256=0a17743506cc9df004266424a1541131b5516c29c9201bd5787ec2997ac2326d,1417
82
82
  cucu/steps/tab_steps.py,sha256=4d5572b648a1bc9d86610f5bc00b35095cdbfadc3352ad7540e36511b77ab8ed,1818
83
83
  cucu/steps/table_steps.py,sha256=e5b053d8772e2f2fb027cbd4b1c86a9071040fefc63cf270a9da4b1654b87bd7,13744
@@ -85,8 +85,8 @@ cucu/steps/tables.js,sha256=3acd9aec5a3e720d375d5962ed44af72705569de0dee2757afe1
85
85
  cucu/steps/text_steps.py,sha256=263fc61e81de7a637055d50e76a71e840acda7b58cf96323963038c34a4b5eec,2576
86
86
  cucu/steps/variable_steps.py,sha256=59272d1f7ff17318e28c63d8665c69f9fa02fd220000ab45fffb1a36d81925b9,2966
87
87
  cucu/steps/webserver_steps.py,sha256=c169294af71231d8ac90f921e1caa57a95b1d6792f1294d4dad4574263c36f2a,1410
88
- cucu/utils.py,sha256=60c04926b4065bb4da17e3a0d5cb4001af1150d48dcd0f602570f9930b2858c0,9464
89
- cucu-1.2.4.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
90
- cucu-1.2.4.dist-info/entry_points.txt,sha256=d7559122140cecbb949d0835940a1942836fbc1bd834dd666838104b8763c09a,40
91
- cucu-1.2.4.dist-info/METADATA,sha256=848ee398aae88549ac768f9a21e9d05b52eb6f8e6a60dcffaaa28f81b1b5cb0e,16675
92
- cucu-1.2.4.dist-info/RECORD,,
88
+ cucu/utils.py,sha256=a79b12b89cc67409bda959310a384cacbe13f87e54241720f88bf303b7fcb9bb,10939
89
+ cucu-1.2.5.dist-info/WHEEL,sha256=03f80d698bb4300c27bcafc3987ea73ef427fcb365e59e808e50d041a7f87b89,78
90
+ cucu-1.2.5.dist-info/entry_points.txt,sha256=d7559122140cecbb949d0835940a1942836fbc1bd834dd666838104b8763c09a,40
91
+ cucu-1.2.5.dist-info/METADATA,sha256=2d068e4f9b75fafa2cdae5f25a4bb612e24fce621847dfeef55eac3bc73c3924,16675
92
+ cucu-1.2.5.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.7.22
2
+ Generator: uv 0.8.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any