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 +15 -5
- cucu/cli/run.py +25 -26
- cucu/config.py +31 -11
- cucu/environment.py +106 -62
- cucu/formatter/cucu.py +1 -1
- cucu/formatter/json.py +1 -1
- cucu/steps/section_steps.py +21 -2
- cucu/utils.py +60 -2
- {cucu-1.2.4.dist-info → cucu-1.2.5.dist-info}/METADATA +1 -1
- {cucu-1.2.4.dist-info → cucu-1.2.5.dist-info}/RECORD +12 -12
- {cucu-1.2.4.dist-info → cucu-1.2.5.dist-info}/WHEEL +1 -1
- {cucu-1.2.4.dist-info → cucu-1.2.5.dist-info}/entry_points.txt +0 -0
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
|
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
|
|
152
|
-
log_filepath =
|
|
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(
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
187
|
+
if run_json_filepath.exists():
|
|
192
188
|
return
|
|
193
189
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
"""
|
|
263
|
-
self.
|
|
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(**
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
105
|
+
ctx.scenario_dir.mkdir(parents=True, exist_ok=True)
|
|
98
106
|
|
|
99
|
-
ctx.scenario_downloads_dir =
|
|
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
|
-
|
|
109
|
+
ctx.scenario_downloads_dir.mkdir(parents=True, exist_ok=True)
|
|
104
110
|
|
|
105
|
-
ctx.scenario_logs_dir =
|
|
111
|
+
ctx.scenario_logs_dir = ctx.scenario_dir / "logs"
|
|
106
112
|
CONFIG["SCENARIO_LOGS_DIR"] = ctx.scenario_logs_dir
|
|
107
|
-
|
|
113
|
+
ctx.scenario_logs_dir.mkdir(parents=True, exist_ok=True)
|
|
108
114
|
|
|
109
|
-
|
|
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
|
-
|
|
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.
|
|
121
|
-
sys.stderr.set_other_stream(ctx.
|
|
122
|
-
logger.init_debug_logger(ctx.
|
|
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
|
-
|
|
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,
|
|
192
|
+
run_after_scenario_hook(ctx, scenario, download_browser_log)
|
|
183
193
|
|
|
184
|
-
|
|
185
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
232
|
-
step.
|
|
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.
|
|
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
|
-
|
|
253
|
-
|
|
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.
|
|
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
cucu/steps/section_steps.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 = []
|
|
@@ -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=
|
|
11
|
-
cucu/cli/run.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
22
|
-
cucu/formatter/json.py,sha256=
|
|
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=
|
|
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=
|
|
89
|
-
cucu-1.2.
|
|
90
|
-
cucu-1.2.
|
|
91
|
-
cucu-1.2.
|
|
92
|
-
cucu-1.2.
|
|
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,,
|
|
File without changes
|