cucu 1.0.0__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/__init__.py +38 -0
- cucu/ansi_parser.py +58 -0
- cucu/behave_tweaks.py +196 -0
- cucu/browser/__init__.py +0 -0
- cucu/browser/core.py +80 -0
- cucu/browser/frames.py +106 -0
- cucu/browser/selenium.py +323 -0
- cucu/browser/selenium_tweaks.py +27 -0
- cucu/cli/__init__.py +3 -0
- cucu/cli/core.py +788 -0
- cucu/cli/run.py +207 -0
- cucu/cli/steps.py +137 -0
- cucu/cli/thread_dumper.py +55 -0
- cucu/config.py +440 -0
- cucu/edgedriver_autoinstaller/README.md +1 -0
- cucu/edgedriver_autoinstaller/__init__.py +37 -0
- cucu/edgedriver_autoinstaller/utils.py +231 -0
- cucu/environment.py +283 -0
- cucu/external/jquery/jquery-3.5.1.min.js +2 -0
- cucu/formatter/__init__.py +0 -0
- cucu/formatter/cucu.py +261 -0
- cucu/formatter/json.py +321 -0
- cucu/formatter/junit.py +289 -0
- cucu/fuzzy/__init__.py +3 -0
- cucu/fuzzy/core.py +107 -0
- cucu/fuzzy/fuzzy.js +253 -0
- cucu/helpers.py +875 -0
- cucu/hooks.py +205 -0
- cucu/language_server/__init__.py +3 -0
- cucu/language_server/core.py +114 -0
- cucu/lint/__init__.py +0 -0
- cucu/lint/linter.py +397 -0
- cucu/lint/rules/format.yaml +125 -0
- cucu/logger.py +113 -0
- cucu/matcher/__init__.py +0 -0
- cucu/matcher/core.py +30 -0
- cucu/page_checks.py +63 -0
- cucu/reporter/__init__.py +3 -0
- cucu/reporter/external/bootstrap.min.css +7 -0
- cucu/reporter/external/bootstrap.min.js +7 -0
- cucu/reporter/external/dataTables.bootstrap.min.css +1 -0
- cucu/reporter/external/dataTables.bootstrap.min.js +14 -0
- cucu/reporter/external/jquery-3.5.1.min.js +2 -0
- cucu/reporter/external/jquery.dataTables.min.js +192 -0
- cucu/reporter/external/popper.min.js +5 -0
- cucu/reporter/favicon.png +0 -0
- cucu/reporter/html.py +452 -0
- cucu/reporter/templates/feature.html +72 -0
- cucu/reporter/templates/flat.html +48 -0
- cucu/reporter/templates/index.html +49 -0
- cucu/reporter/templates/layout.html +109 -0
- cucu/reporter/templates/scenario.html +200 -0
- cucu/steps/__init__.py +27 -0
- cucu/steps/base_steps.py +88 -0
- cucu/steps/browser_steps.py +337 -0
- cucu/steps/button_steps.py +91 -0
- cucu/steps/checkbox_steps.py +111 -0
- cucu/steps/command_steps.py +181 -0
- cucu/steps/comment_steps.py +17 -0
- cucu/steps/draggable_steps.py +168 -0
- cucu/steps/dropdown_steps.py +467 -0
- cucu/steps/file_input_steps.py +80 -0
- cucu/steps/filesystem_steps.py +144 -0
- cucu/steps/flow_control_steps.py +198 -0
- cucu/steps/image_steps.py +37 -0
- cucu/steps/input_steps.py +301 -0
- cucu/steps/link_steps.py +63 -0
- cucu/steps/menuitem_steps.py +39 -0
- cucu/steps/platform_steps.py +29 -0
- cucu/steps/radio_steps.py +187 -0
- cucu/steps/step_utils.py +55 -0
- cucu/steps/tab_steps.py +68 -0
- cucu/steps/table_steps.py +437 -0
- cucu/steps/tables.js +28 -0
- cucu/steps/text_steps.py +78 -0
- cucu/steps/variable_steps.py +100 -0
- cucu/steps/webserver_steps.py +40 -0
- cucu/utils.py +269 -0
- cucu-1.0.0.dist-info/METADATA +424 -0
- cucu-1.0.0.dist-info/RECORD +83 -0
- cucu-1.0.0.dist-info/WHEEL +4 -0
- cucu-1.0.0.dist-info/entry_points.txt +2 -0
- cucu-1.0.0.dist-info/licenses/LICENSE +32 -0
cucu/cli/run.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import socket
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from cucu import (
|
|
9
|
+
behave_tweaks,
|
|
10
|
+
init_global_hook_variables,
|
|
11
|
+
register_before_retry_hook,
|
|
12
|
+
)
|
|
13
|
+
from cucu.browser import selenium
|
|
14
|
+
from cucu.config import CONFIG
|
|
15
|
+
from cucu.page_checks import init_page_checks
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
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
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def behave_init(filepath="features"):
|
|
29
|
+
"""
|
|
30
|
+
behave internal init method used to load the various parts of set of
|
|
31
|
+
feature files and supporting code without executing any of it.
|
|
32
|
+
|
|
33
|
+
parameters:
|
|
34
|
+
filepath(string): the file system path of the features directory to load
|
|
35
|
+
"""
|
|
36
|
+
behave_tweaks.behave_main(
|
|
37
|
+
["--dry-run", "--format=null", "--no-summary", filepath]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def behave(
|
|
42
|
+
filepath,
|
|
43
|
+
color_output,
|
|
44
|
+
dry_run,
|
|
45
|
+
env,
|
|
46
|
+
fail_fast,
|
|
47
|
+
headless,
|
|
48
|
+
name,
|
|
49
|
+
ipdb_on_failure,
|
|
50
|
+
junit,
|
|
51
|
+
results,
|
|
52
|
+
secrets,
|
|
53
|
+
show_skips,
|
|
54
|
+
tags,
|
|
55
|
+
verbose,
|
|
56
|
+
redirect_output=False,
|
|
57
|
+
skip_init_global_hook_variables=False,
|
|
58
|
+
):
|
|
59
|
+
# load all them configs
|
|
60
|
+
CONFIG.load_cucurc_files(filepath)
|
|
61
|
+
|
|
62
|
+
if CONFIG["CUCU_SELENIUM_REMOTE_URL"] is None:
|
|
63
|
+
selenium.init()
|
|
64
|
+
|
|
65
|
+
# general socket timeout instead of letting the framework ever get stuck on a
|
|
66
|
+
# socket connect/read call
|
|
67
|
+
timeout = float(CONFIG["CUCU_SOCKET_DEFAULT_TIMEOUT_S"])
|
|
68
|
+
socket.setdefaulttimeout(timeout)
|
|
69
|
+
|
|
70
|
+
if not skip_init_global_hook_variables:
|
|
71
|
+
init_global_hook_variables()
|
|
72
|
+
|
|
73
|
+
init_page_checks()
|
|
74
|
+
|
|
75
|
+
os.environ["CUCU_COLOR_OUTPUT"] = str(color_output).lower()
|
|
76
|
+
|
|
77
|
+
if headless:
|
|
78
|
+
os.environ["CUCU_BROWSER_HEADLESS"] = "True"
|
|
79
|
+
|
|
80
|
+
for variable in list(env):
|
|
81
|
+
key, value = variable.split("=")
|
|
82
|
+
os.environ[key] = value
|
|
83
|
+
|
|
84
|
+
if ipdb_on_failure:
|
|
85
|
+
os.environ["CUCU_IPDB_ON_FAILURE"] = "true"
|
|
86
|
+
|
|
87
|
+
os.environ["CUCU_RESULTS_DIR"] = results
|
|
88
|
+
os.environ["CUCU_JUNIT_DIR"] = junit
|
|
89
|
+
|
|
90
|
+
if secrets:
|
|
91
|
+
os.environ["CUCU_SECRETS"] = secrets
|
|
92
|
+
|
|
93
|
+
args = [
|
|
94
|
+
# don't run disabled tests
|
|
95
|
+
"--tags",
|
|
96
|
+
"~@disabled",
|
|
97
|
+
# always print the skipped steps and scenarios
|
|
98
|
+
"--show-skipped",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
if verbose:
|
|
102
|
+
args.append("--verbose")
|
|
103
|
+
|
|
104
|
+
run_json_filename = "run.json"
|
|
105
|
+
if redirect_output:
|
|
106
|
+
feature_name = get_feature_name(filepath)
|
|
107
|
+
run_json_filename = f"{feature_name + '-run.json'}"
|
|
108
|
+
|
|
109
|
+
if dry_run:
|
|
110
|
+
args += [
|
|
111
|
+
"--dry-run",
|
|
112
|
+
# console formater
|
|
113
|
+
"--format=cucu.formatter.cucu:CucuFormatter",
|
|
114
|
+
]
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
args += [
|
|
118
|
+
"--no-capture",
|
|
119
|
+
"--no-capture-stderr",
|
|
120
|
+
"--no-logcapture",
|
|
121
|
+
# generate a JSON file containing the exact details of the whole run
|
|
122
|
+
"--format=cucu.formatter.json:CucuJSONFormatter",
|
|
123
|
+
f"--outfile={results}/{run_json_filename}",
|
|
124
|
+
# console formatter
|
|
125
|
+
"--format=cucu.formatter.cucu:CucuFormatter",
|
|
126
|
+
f"--logging-level={os.environ['CUCU_LOGGING_LEVEL'].upper()}",
|
|
127
|
+
# disable behave's junit output in favor of our own formatter
|
|
128
|
+
"--no-junit",
|
|
129
|
+
"--format=cucu.formatter.junit:CucuJUnitFormatter",
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
for tag in tags:
|
|
133
|
+
args.append("--tags")
|
|
134
|
+
args.append(tag)
|
|
135
|
+
|
|
136
|
+
if name is not None:
|
|
137
|
+
args += ["--name", name]
|
|
138
|
+
|
|
139
|
+
if fail_fast:
|
|
140
|
+
args.append("--stop")
|
|
141
|
+
|
|
142
|
+
if not show_skips:
|
|
143
|
+
args.append("--no-skipped")
|
|
144
|
+
|
|
145
|
+
args.append(filepath)
|
|
146
|
+
|
|
147
|
+
result = 0
|
|
148
|
+
try:
|
|
149
|
+
if redirect_output:
|
|
150
|
+
feature_name = get_feature_name(filepath)
|
|
151
|
+
log_filename = f"{feature_name + '.log'}"
|
|
152
|
+
log_filepath = os.path.join(results, log_filename)
|
|
153
|
+
|
|
154
|
+
CONFIG["__CUCU_PARENT_STDOUT"] = sys.stdout
|
|
155
|
+
|
|
156
|
+
def retry_progress(ctx):
|
|
157
|
+
CONFIG["__CUCU_PARENT_STDOUT"].write(".")
|
|
158
|
+
CONFIG["__CUCU_PARENT_STDOUT"].flush()
|
|
159
|
+
|
|
160
|
+
# this allows steps that are stuck in a retry to loop to still
|
|
161
|
+
# provide progress feedback on screen
|
|
162
|
+
register_before_retry_hook(retry_progress)
|
|
163
|
+
|
|
164
|
+
with open(log_filepath, "w", encoding="utf8") as output:
|
|
165
|
+
with contextlib.redirect_stderr(output):
|
|
166
|
+
with contextlib.redirect_stdout(output):
|
|
167
|
+
# intercept the stdout/stderr so we can do things such
|
|
168
|
+
# as hiding secrets in logs
|
|
169
|
+
behave_tweaks.init_outputs(sys.stdout, sys.stderr)
|
|
170
|
+
result = behave_tweaks.behave_main(args)
|
|
171
|
+
else:
|
|
172
|
+
# intercept the stdout/stderr so we can do things such
|
|
173
|
+
# as hiding secrets in logs
|
|
174
|
+
behave_tweaks.init_outputs(sys.stdout, sys.stderr)
|
|
175
|
+
result = behave_tweaks.behave_main(args)
|
|
176
|
+
except:
|
|
177
|
+
result = -1
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
return result
|
|
181
|
+
|
|
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")
|
|
190
|
+
|
|
191
|
+
if os.path.exists(run_details_filepath):
|
|
192
|
+
return
|
|
193
|
+
|
|
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"
|
|
198
|
+
|
|
199
|
+
run_details = {
|
|
200
|
+
"filepath": filepath,
|
|
201
|
+
"full_arguments": sys.argv,
|
|
202
|
+
"env": env_values,
|
|
203
|
+
"date": datetime.now().isoformat(),
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
with open(run_details_filepath, "w", encoding="utf8") as output:
|
|
207
|
+
output.write(json.dumps(run_details, indent=2, sort_keys=True))
|
cucu/cli/steps.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from cucu import behave_tweaks
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_cucu_steps(filepath=None):
|
|
10
|
+
"""
|
|
11
|
+
loads the cucu steps definition using behave and returns an array of
|
|
12
|
+
hashmaps that have the following structure:
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
[step_name]: {
|
|
16
|
+
"location": {
|
|
17
|
+
"filepath": "...",
|
|
18
|
+
"line": "...",
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
undefined steps are marked with a value of None instead of a location. ie:
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
[undefined_step_name]: None
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
an array of hashmaps
|
|
31
|
+
"""
|
|
32
|
+
steps_cache = {}
|
|
33
|
+
args = ["--dry-run", "--no-summary", "--format", "steps.doc"]
|
|
34
|
+
|
|
35
|
+
if filepath is not None:
|
|
36
|
+
args.append(filepath)
|
|
37
|
+
|
|
38
|
+
error = None
|
|
39
|
+
|
|
40
|
+
stdout = io.StringIO()
|
|
41
|
+
stderr = io.StringIO()
|
|
42
|
+
|
|
43
|
+
with contextlib.redirect_stderr(stderr):
|
|
44
|
+
with contextlib.redirect_stdout(stdout):
|
|
45
|
+
error = behave_tweaks.behave_main(args)
|
|
46
|
+
|
|
47
|
+
stdout = stdout.getvalue()
|
|
48
|
+
stderr = stderr.getvalue()
|
|
49
|
+
|
|
50
|
+
if stdout.startswith("ParserError"):
|
|
51
|
+
print(stdout)
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
"unable to parse feature files, see above for details"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
for cucu_step in stdout.split("@step"):
|
|
57
|
+
# Each line of a step definition looks like so:
|
|
58
|
+
#
|
|
59
|
+
# @step('I should see "{this}" matches "{that}"')
|
|
60
|
+
# Function: inner_step()
|
|
61
|
+
# Location: src/cucu/behave_tweaks.py:64
|
|
62
|
+
# possibly a doc string here on the function that can be used
|
|
63
|
+
# to add a little documentation to each step definition
|
|
64
|
+
#
|
|
65
|
+
# @step('I should see "{this}" matches the following')
|
|
66
|
+
# Function: inner_step()
|
|
67
|
+
# Location: src/cucu/behave_tweaks.py:64
|
|
68
|
+
#
|
|
69
|
+
if cucu_step.strip() == "":
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if not cucu_step.startswith("("):
|
|
73
|
+
#
|
|
74
|
+
# any block of lines between the `@step` that doesn't start with the
|
|
75
|
+
# character ( is an error being reported behave when loading steps
|
|
76
|
+
# and we'll ignore it when processing the step definitions and then
|
|
77
|
+
# report the actual underlying trace reported in STDERR below
|
|
78
|
+
#
|
|
79
|
+
print("unable to parse some step lines")
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
lines = cucu_step.split("\n")
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# parts[1] is the function name while parts[3:] is the docstring
|
|
86
|
+
# of the step which we can use for documenting usage of the step
|
|
87
|
+
# in the language server
|
|
88
|
+
#
|
|
89
|
+
step_name = lines[0]
|
|
90
|
+
location = lines[2]
|
|
91
|
+
|
|
92
|
+
step_name = re.match(r"\('(.*)'\)", step_name).groups()[0]
|
|
93
|
+
_, filepath, line_number = location.split(":")
|
|
94
|
+
|
|
95
|
+
steps_cache[step_name] = {
|
|
96
|
+
"location": {
|
|
97
|
+
"filepath": filepath.strip(),
|
|
98
|
+
"line": line_number.strip(),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# collect any undefined steps
|
|
103
|
+
for line in stderr.split("\n"):
|
|
104
|
+
# @when(u'I close the current browser caca')
|
|
105
|
+
match = re.match(r"@[a-z]+\(u'([^']+)'\)", line)
|
|
106
|
+
if match:
|
|
107
|
+
step_name = match.group(1)
|
|
108
|
+
steps_cache[step_name] = None
|
|
109
|
+
|
|
110
|
+
if error == 0:
|
|
111
|
+
return (steps_cache, None)
|
|
112
|
+
|
|
113
|
+
else:
|
|
114
|
+
return (steps_cache, stderr)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def print_json_steps(filepath=None):
|
|
118
|
+
"""
|
|
119
|
+
pretty print the steps in a JSON fart
|
|
120
|
+
"""
|
|
121
|
+
steps, steps_error = load_cucu_steps(filepath=filepath)
|
|
122
|
+
print(json.dumps(steps, indent=2, sort_keys=True))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def print_human_readable_steps(filepath=None):
|
|
126
|
+
steps, steps_error = load_cucu_steps(filepath=filepath)
|
|
127
|
+
|
|
128
|
+
for step_name in steps:
|
|
129
|
+
if steps[step_name] is not None:
|
|
130
|
+
if filepath in steps[step_name]["location"]["filepath"]:
|
|
131
|
+
print(f"custom: {step_name}")
|
|
132
|
+
else:
|
|
133
|
+
print(f"cucu: {step_name}")
|
|
134
|
+
|
|
135
|
+
if steps_error is not None:
|
|
136
|
+
print(steps_error)
|
|
137
|
+
raise RuntimeError("Failure loading some steps, see above for details")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import threading
|
|
3
|
+
import traceback
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ThreadDumper(threading.Thread):
|
|
7
|
+
"""
|
|
8
|
+
thread dumping class that can be easily stopped by calling the `.stop()`
|
|
9
|
+
method.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, interval_min, stdout, *args, **kwargs):
|
|
13
|
+
super(ThreadDumper, self).__init__(*args, **kwargs)
|
|
14
|
+
self.interval_min = interval_min
|
|
15
|
+
self.stdout = stdout
|
|
16
|
+
self.running = False
|
|
17
|
+
self.event = threading.Event()
|
|
18
|
+
|
|
19
|
+
def stop(self):
|
|
20
|
+
self.running = False
|
|
21
|
+
self.event.set()
|
|
22
|
+
|
|
23
|
+
def start(self):
|
|
24
|
+
self.running = True
|
|
25
|
+
super().start()
|
|
26
|
+
|
|
27
|
+
def run(self):
|
|
28
|
+
while self.running:
|
|
29
|
+
self.stdout.write(f"\n{'*' * 80}\n")
|
|
30
|
+
for thread in threading.enumerate():
|
|
31
|
+
self.stdout.write(f"{thread}\n")
|
|
32
|
+
traceback.print_stack(
|
|
33
|
+
sys._current_frames()[thread.ident], file=self.stdout
|
|
34
|
+
)
|
|
35
|
+
self.stdout.write("\n")
|
|
36
|
+
self.stdout.write(f"{'*' * 80}\n")
|
|
37
|
+
self.event.wait(self.interval_min * 60)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def start(interval_min):
|
|
41
|
+
"""
|
|
42
|
+
start the thread dumper and return it so one can call the `.stop()` method
|
|
43
|
+
on it to get it to quit
|
|
44
|
+
|
|
45
|
+
parameters:
|
|
46
|
+
interval_min(float): number of minutes between each thread stacktrace
|
|
47
|
+
dump to print
|
|
48
|
+
|
|
49
|
+
returns:
|
|
50
|
+
a ThreadDumper object that is a child of the threading.Thread class with
|
|
51
|
+
the method `.stop()` to get the thread to quit
|
|
52
|
+
"""
|
|
53
|
+
thread = ThreadDumper(interval_min, sys.stdout)
|
|
54
|
+
thread.start()
|
|
55
|
+
return thread
|