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/config.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import socket
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Config(dict):
|
|
10
|
+
# only match variables {...}
|
|
11
|
+
__VARIABLE_REGEX = re.compile(r"\{(?<!\\{)([^{}]+)\}(?<!\\})")
|
|
12
|
+
|
|
13
|
+
def __init__(self, **kwargs):
|
|
14
|
+
self.update(**kwargs)
|
|
15
|
+
self.resolving = False
|
|
16
|
+
self.defined_variables = {}
|
|
17
|
+
self.variable_lookups = {}
|
|
18
|
+
|
|
19
|
+
def define(self, name, description, default=None):
|
|
20
|
+
"""
|
|
21
|
+
used to define variables and set their default so that we can then
|
|
22
|
+
provide the info using the `cucu vars` command for test writers to know
|
|
23
|
+
where and how to use various built-in variables.
|
|
24
|
+
|
|
25
|
+
parameters:
|
|
26
|
+
name(string): the name of the variable
|
|
27
|
+
description(string): a succinct description of variables purpose
|
|
28
|
+
default(string): an optional default to set the variable to
|
|
29
|
+
"""
|
|
30
|
+
self.defined_variables[name] = {
|
|
31
|
+
"description": description,
|
|
32
|
+
"default": default,
|
|
33
|
+
}
|
|
34
|
+
# set the default
|
|
35
|
+
self.__setitem__(name, default)
|
|
36
|
+
|
|
37
|
+
def escape(self, string):
|
|
38
|
+
"""
|
|
39
|
+
utility method used to escape strings that would be otherwise problematic
|
|
40
|
+
if the values were passed around as is in cucu steps.
|
|
41
|
+
"""
|
|
42
|
+
if isinstance(string, str):
|
|
43
|
+
string = string.replace("{", "\\{")
|
|
44
|
+
string = string.replace("}", "\\}")
|
|
45
|
+
|
|
46
|
+
return string
|
|
47
|
+
|
|
48
|
+
def __getitem__(self, key):
|
|
49
|
+
try:
|
|
50
|
+
# environment always takes precedence
|
|
51
|
+
value = os.environ.get(key)
|
|
52
|
+
|
|
53
|
+
if value is None:
|
|
54
|
+
value = dict.__getitem__(self, key)
|
|
55
|
+
|
|
56
|
+
return value
|
|
57
|
+
except KeyError:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def __setitem__(self, key, val):
|
|
61
|
+
dict.__setitem__(self, key, val)
|
|
62
|
+
|
|
63
|
+
def get(self, key, default=None):
|
|
64
|
+
try:
|
|
65
|
+
for regex, lookup in self.variable_lookups.items():
|
|
66
|
+
if regex.match(key):
|
|
67
|
+
return lookup(key)
|
|
68
|
+
|
|
69
|
+
return self[key]
|
|
70
|
+
except KeyError:
|
|
71
|
+
return default
|
|
72
|
+
|
|
73
|
+
def update(self, **kwargs):
|
|
74
|
+
for k, v in kwargs.items():
|
|
75
|
+
self[k] = v
|
|
76
|
+
|
|
77
|
+
def bool(self, key):
|
|
78
|
+
return self[key] in [True, "True", "true", "yes", "enabled"]
|
|
79
|
+
|
|
80
|
+
def true(self, key):
|
|
81
|
+
return self.bool(key)
|
|
82
|
+
|
|
83
|
+
def false(self, key):
|
|
84
|
+
return not self.true(key)
|
|
85
|
+
|
|
86
|
+
def load(self, filepath):
|
|
87
|
+
"""
|
|
88
|
+
loads configuration values from a YAML file at the filepath provided
|
|
89
|
+
"""
|
|
90
|
+
config = yaml.safe_load(open(filepath, "rb"))
|
|
91
|
+
|
|
92
|
+
if config:
|
|
93
|
+
for key in config.keys():
|
|
94
|
+
if key == "CUCU_SECRETS": # security: only add restrictions
|
|
95
|
+
vals = self.get(key, "").split(",")
|
|
96
|
+
vals = vals + config[key].split(",")
|
|
97
|
+
self[key] = ",".join(set(vals) ^ {""})
|
|
98
|
+
else:
|
|
99
|
+
self[key] = config[key]
|
|
100
|
+
|
|
101
|
+
def load_cucurc_files(self, filepath):
|
|
102
|
+
"""
|
|
103
|
+
load in order the ~/.cucurc.yml and then subsequent config files
|
|
104
|
+
starting from the current working directory to the filepath provided
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# load the ~/.cucurc.yml first
|
|
108
|
+
home_cucurc_filepath = os.path.join(
|
|
109
|
+
os.path.expanduser("~"), ".cucurc.yml"
|
|
110
|
+
)
|
|
111
|
+
if os.path.exists(home_cucurc_filepath):
|
|
112
|
+
self.load(home_cucurc_filepath)
|
|
113
|
+
|
|
114
|
+
filepath = os.path.abspath(filepath)
|
|
115
|
+
if os.path.isfile(filepath):
|
|
116
|
+
basename = os.path.dirname(filepath)
|
|
117
|
+
else:
|
|
118
|
+
basename = filepath
|
|
119
|
+
|
|
120
|
+
# create the inverse list of the directories starting from cwd to the one
|
|
121
|
+
# containing the feature file we want to run to load the cucrc.yml files in
|
|
122
|
+
# the correct order
|
|
123
|
+
dirnames = [os.getcwd()]
|
|
124
|
+
while basename != os.getcwd():
|
|
125
|
+
dirnames.append(basename)
|
|
126
|
+
basename = os.path.dirname(basename)
|
|
127
|
+
|
|
128
|
+
for dirname in dirnames:
|
|
129
|
+
cucurc_filepath = os.path.join(dirname, "cucurc.yml")
|
|
130
|
+
if os.path.exists(cucurc_filepath):
|
|
131
|
+
self.load(cucurc_filepath)
|
|
132
|
+
|
|
133
|
+
def expand(self, string):
|
|
134
|
+
"""
|
|
135
|
+
find any variable references and return an dictionary of all of the
|
|
136
|
+
variable names and values within the string provided.
|
|
137
|
+
|
|
138
|
+
returns:
|
|
139
|
+
a dictionary of all of the variable names found in the string
|
|
140
|
+
provided with the exact value of the variable at runtime.
|
|
141
|
+
"""
|
|
142
|
+
references = {}
|
|
143
|
+
|
|
144
|
+
variables = re.findall("{([^{}]+)}", string)
|
|
145
|
+
|
|
146
|
+
for variable in variables:
|
|
147
|
+
value = self.resolve(f"{{{variable}}}")
|
|
148
|
+
|
|
149
|
+
# if it didn't resolve to anything then
|
|
150
|
+
if value:
|
|
151
|
+
value = str(value).replace("\n", "\\n")
|
|
152
|
+
value = value[:80] + "..." * (len(value) > 80)
|
|
153
|
+
else:
|
|
154
|
+
value = None
|
|
155
|
+
|
|
156
|
+
references[variable] = value
|
|
157
|
+
|
|
158
|
+
return references
|
|
159
|
+
|
|
160
|
+
def hide_secrets(self, text: str | bytes):
|
|
161
|
+
secret_keys = [x for x in self.get("CUCU_SECRETS", "").split(",") if x]
|
|
162
|
+
secret_values = [self.get(x) for x in secret_keys if self.get(x)]
|
|
163
|
+
secret_values = [x for x in secret_values if isinstance(x, str)]
|
|
164
|
+
|
|
165
|
+
is_bytes = isinstance(text, bytes)
|
|
166
|
+
if is_bytes:
|
|
167
|
+
text = text.decode()
|
|
168
|
+
|
|
169
|
+
result = None
|
|
170
|
+
if text.startswith("{"):
|
|
171
|
+
try:
|
|
172
|
+
result = self._hide_secrets_json(secret_values, text)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(
|
|
175
|
+
f"Couldn't parse json, falling back to text processing: {e}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if result is None:
|
|
179
|
+
result = self._hide_secrets_text(secret_values, text)
|
|
180
|
+
|
|
181
|
+
if is_bytes:
|
|
182
|
+
result = result.encode()
|
|
183
|
+
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
def _hide_secrets_json(self, secret_values, text):
|
|
187
|
+
data = json.loads(text)
|
|
188
|
+
|
|
189
|
+
def hide_node(value, parent, key):
|
|
190
|
+
if not isinstance(value, str):
|
|
191
|
+
return value
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
key == "name"
|
|
195
|
+
and isinstance(parent, dict)
|
|
196
|
+
and parent.get("keyword", "") in ["Feature", "Scenario"]
|
|
197
|
+
):
|
|
198
|
+
return value
|
|
199
|
+
|
|
200
|
+
return self._hide_secrets_text(secret_values, value)
|
|
201
|
+
|
|
202
|
+
leaf_map(data, hide_node)
|
|
203
|
+
return json.dumps(data, indent=2, sort_keys=True)
|
|
204
|
+
|
|
205
|
+
def _hide_secrets_text(self, secret_values, text):
|
|
206
|
+
lines = text.split("\n")
|
|
207
|
+
|
|
208
|
+
for x in range(len(lines)):
|
|
209
|
+
# here's where we can hide secrets
|
|
210
|
+
for value in secret_values:
|
|
211
|
+
lines[x] = lines[x].replace(value, "*" * len(value))
|
|
212
|
+
|
|
213
|
+
return "\n".join(lines)
|
|
214
|
+
|
|
215
|
+
def resolve(self, string):
|
|
216
|
+
"""
|
|
217
|
+
resolve any variable references {...} in the string provided using
|
|
218
|
+
the values currently stored in the config object. We also unescape any
|
|
219
|
+
characters preceding by a backslash as to allow for test writers to
|
|
220
|
+
escape special characters
|
|
221
|
+
"""
|
|
222
|
+
if isinstance(string, str):
|
|
223
|
+
previouses = [None]
|
|
224
|
+
while previouses[-1] != string:
|
|
225
|
+
# if any of the previous iterations of replacing variables looks
|
|
226
|
+
# like the exact string we have now then break out of the loop
|
|
227
|
+
# as there's an infinite recursion of variable replacing values
|
|
228
|
+
if string in previouses:
|
|
229
|
+
raise RuntimeError("infinite replacement loop detected")
|
|
230
|
+
|
|
231
|
+
previouses.append(string)
|
|
232
|
+
|
|
233
|
+
for var_name in Config.__VARIABLE_REGEX.findall(string):
|
|
234
|
+
value = self.get(var_name)
|
|
235
|
+
|
|
236
|
+
if value is None:
|
|
237
|
+
value = ""
|
|
238
|
+
# print directly to the output stream, which was taken over in behave_tweaks
|
|
239
|
+
print(f'WARNING variable "{var_name}" is undefined')
|
|
240
|
+
|
|
241
|
+
string = string.replace("{" + var_name + "}", str(value))
|
|
242
|
+
|
|
243
|
+
# we are only going to allow escaping of { and " characters for the
|
|
244
|
+
# time being as they're part of the language:
|
|
245
|
+
#
|
|
246
|
+
# * " are used around step arguments
|
|
247
|
+
# * {} are used by variable references
|
|
248
|
+
#
|
|
249
|
+
# regex below uses negative lookbehind to assert we're not replacing
|
|
250
|
+
# any escapes that have the backslash itself escaped.
|
|
251
|
+
#
|
|
252
|
+
string = re.sub(r"(?<!\\)\\{", "{", string)
|
|
253
|
+
string = re.sub(r"(?<!\\)\\}", "}", string)
|
|
254
|
+
string = re.sub(r'(?<!\\)\\"', '"', string)
|
|
255
|
+
|
|
256
|
+
return string
|
|
257
|
+
|
|
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()
|
|
264
|
+
|
|
265
|
+
def restore(self):
|
|
266
|
+
"""
|
|
267
|
+
restore a previous `snapshot`
|
|
268
|
+
"""
|
|
269
|
+
self.clear()
|
|
270
|
+
self.update(**self.snapshot_data)
|
|
271
|
+
|
|
272
|
+
def register_custom_variable_handling(self, regex, lookup):
|
|
273
|
+
"""
|
|
274
|
+
register a regex to match variable names on and allow the lookup
|
|
275
|
+
function provided to do the handling of the resolution of the variable
|
|
276
|
+
name.
|
|
277
|
+
|
|
278
|
+
parameters:
|
|
279
|
+
regex(string): regular expression to match on any config variable
|
|
280
|
+
name when doing lookups
|
|
281
|
+
lookup(func): a function that accepts a variable name to return its
|
|
282
|
+
value at runtime.
|
|
283
|
+
|
|
284
|
+
def lookup(name):
|
|
285
|
+
return [value of the variable at runtime]
|
|
286
|
+
"""
|
|
287
|
+
self.variable_lookups[re.compile(regex)] = lookup
|
|
288
|
+
|
|
289
|
+
def to_yaml_without_secrets(self):
|
|
290
|
+
omit_keys = [
|
|
291
|
+
"CWD",
|
|
292
|
+
"STDOUT",
|
|
293
|
+
"STDERR",
|
|
294
|
+
"SCENARIO_DOWNLOADS_DIR",
|
|
295
|
+
"SCENARIO_LOGS_DIR",
|
|
296
|
+
"SCENARIO_RESULTS_DIR",
|
|
297
|
+
]
|
|
298
|
+
secret_keys = [x for x in self.get("CUCU_SECRETS", "").split(",") if x]
|
|
299
|
+
keys = [
|
|
300
|
+
k
|
|
301
|
+
for k in self.keys()
|
|
302
|
+
if not k.startswith("__")
|
|
303
|
+
and k not in secret_keys
|
|
304
|
+
and k not in omit_keys
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
config = {k: self[k] for k in sorted(keys)}
|
|
308
|
+
for k, v in config.items():
|
|
309
|
+
if isinstance(v, str):
|
|
310
|
+
v = self.hide_secrets(v)
|
|
311
|
+
# fix leading '*' issue
|
|
312
|
+
if v.startswith("*"):
|
|
313
|
+
v = f"'{v}'"
|
|
314
|
+
config[k] = v
|
|
315
|
+
|
|
316
|
+
return yaml.dump(config)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
# global config object
|
|
320
|
+
CONFIG = Config()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _get_local_address():
|
|
324
|
+
"""
|
|
325
|
+
internal method to get the current host address
|
|
326
|
+
"""
|
|
327
|
+
google_dns_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
328
|
+
google_dns_socket.connect(("8.8.8.8", 80))
|
|
329
|
+
return google_dns_socket.getsockname()[0]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
CONFIG.define(
|
|
333
|
+
"HOST_ADDRESS",
|
|
334
|
+
"host address of the current machine cucu is running on",
|
|
335
|
+
default=_get_local_address(),
|
|
336
|
+
)
|
|
337
|
+
CONFIG.define(
|
|
338
|
+
"CWD",
|
|
339
|
+
"the current working directory of the cucu process",
|
|
340
|
+
default=os.getcwd(),
|
|
341
|
+
)
|
|
342
|
+
CONFIG.define(
|
|
343
|
+
"CUCU_SECRETS",
|
|
344
|
+
"a comma separated list of VARIABLE names that we want to hide "
|
|
345
|
+
"their values in the various outputs of cucu by replacing with asterisks",
|
|
346
|
+
default="",
|
|
347
|
+
)
|
|
348
|
+
CONFIG.define(
|
|
349
|
+
"CUCU_SHORT_UI_RETRY_AFTER_S",
|
|
350
|
+
"the amount of time to wait between retries in seconds for non-wait ui steps",
|
|
351
|
+
default=0.25,
|
|
352
|
+
)
|
|
353
|
+
CONFIG.define(
|
|
354
|
+
"CUCU_SHORT_UI_WAIT_TIMEOUT_S",
|
|
355
|
+
"the total amount of wait time in seconds for non-wait ui steps",
|
|
356
|
+
default=2.25,
|
|
357
|
+
)
|
|
358
|
+
CONFIG.define(
|
|
359
|
+
"CUCU_STEP_WAIT_TIMEOUT_S",
|
|
360
|
+
"the total amount of wait time in seconds for `wait for` steps",
|
|
361
|
+
default=20.0,
|
|
362
|
+
)
|
|
363
|
+
CONFIG.define(
|
|
364
|
+
"CUCU_STEP_RETRY_AFTER_S",
|
|
365
|
+
"the amount of time to wait between retries in seconds for `wait for` steps",
|
|
366
|
+
default=0.5,
|
|
367
|
+
)
|
|
368
|
+
CONFIG.define(
|
|
369
|
+
"CUCU_KEEP_BROWSER_ALIVE",
|
|
370
|
+
"when set to true we'll reuse the browser between scenario runs",
|
|
371
|
+
default=False,
|
|
372
|
+
)
|
|
373
|
+
CONFIG.define(
|
|
374
|
+
"CUCU_BROWSER_WINDOW_HEIGHT",
|
|
375
|
+
"the browser window height when running browser tests",
|
|
376
|
+
default=768,
|
|
377
|
+
)
|
|
378
|
+
CONFIG.define(
|
|
379
|
+
"CUCU_BROWSER_WINDOW_WIDTH",
|
|
380
|
+
"the browser window width when running browser tests",
|
|
381
|
+
default=1366,
|
|
382
|
+
)
|
|
383
|
+
CONFIG.define(
|
|
384
|
+
"CUCU_BROWSER_DOWNLOADS_DIR",
|
|
385
|
+
"the browser download directory when running browser tests",
|
|
386
|
+
default="/tmp/cucu-browser-downloads",
|
|
387
|
+
)
|
|
388
|
+
CONFIG.define(
|
|
389
|
+
"CUCU_SOCKET_DEFAULT_TIMEOUT_S",
|
|
390
|
+
"the default timeout (seconds) for socket connect/read in cucu",
|
|
391
|
+
default=10,
|
|
392
|
+
)
|
|
393
|
+
CONFIG.define(
|
|
394
|
+
"CUCU_SELENIUM_DEFAULT_TIMEOUT_S",
|
|
395
|
+
"the default timeout (seconds) for selenium connect/read in selenium",
|
|
396
|
+
default=10,
|
|
397
|
+
)
|
|
398
|
+
CONFIG.define(
|
|
399
|
+
"CUCU_MONITOR_PNG",
|
|
400
|
+
"when set to a filename `cucu` will update the image to match "
|
|
401
|
+
"the exact image step at runtime.",
|
|
402
|
+
default=".monitor.png",
|
|
403
|
+
)
|
|
404
|
+
CONFIG.define(
|
|
405
|
+
"CUCU_LINT_RULES_PATH",
|
|
406
|
+
"comma separated list of paths to load cucu lint rules from .yaml files",
|
|
407
|
+
default="",
|
|
408
|
+
)
|
|
409
|
+
CONFIG.define(
|
|
410
|
+
"CUCU_JUNIT_WITH_STACKTRACE",
|
|
411
|
+
"when set to 'true' results in stacktraces showing in the JUnit XML failure output",
|
|
412
|
+
default="false",
|
|
413
|
+
)
|
|
414
|
+
CONFIG.define(
|
|
415
|
+
"CUCU_SKIP_HIGHLIGHT_BORDER",
|
|
416
|
+
"when set to 'True' skips adding a border to highlight found element in screenshots",
|
|
417
|
+
default=True,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
# define re_map here instead of in utils.py to avoid circular import
|
|
422
|
+
def leaf_map(data, value_func, parent=None, key=None):
|
|
423
|
+
"""
|
|
424
|
+
Utility to apply a map function recursively to a dict or list.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
data: The dict or list or value to use
|
|
428
|
+
value_func: Callable function that accepts data and parent
|
|
429
|
+
parent: The parent object (or None)
|
|
430
|
+
"""
|
|
431
|
+
if isinstance(data, dict):
|
|
432
|
+
for key, value in data.items():
|
|
433
|
+
data[key] = leaf_map(value, value_func, data, key)
|
|
434
|
+
return data
|
|
435
|
+
elif isinstance(data, list):
|
|
436
|
+
for x, value in enumerate(data):
|
|
437
|
+
data[x] = leaf_map(value, value_func, data, key)
|
|
438
|
+
return data
|
|
439
|
+
else:
|
|
440
|
+
return value_func(data, parent, key)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
taken and adapted from https://github.com/celedev97/python-edgedriver-autoinstaller
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
from . import utils
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def install(cwd=False):
|
|
9
|
+
"""
|
|
10
|
+
Appends the directory of the edgedriver binary file to PATH.
|
|
11
|
+
|
|
12
|
+
:param cwd: Flag indicating whether to download to current working directory
|
|
13
|
+
:return: The file path of edgedriver
|
|
14
|
+
"""
|
|
15
|
+
edgedriver_filepath = utils.download_edgedriver(cwd)
|
|
16
|
+
if not edgedriver_filepath:
|
|
17
|
+
logging.debug("Can not download edgedriver.")
|
|
18
|
+
return
|
|
19
|
+
edgedriver_dir = os.path.dirname(edgedriver_filepath)
|
|
20
|
+
if "PATH" not in os.environ:
|
|
21
|
+
os.environ["PATH"] = edgedriver_dir
|
|
22
|
+
elif edgedriver_dir not in os.environ["PATH"]:
|
|
23
|
+
os.environ["PATH"] = (
|
|
24
|
+
edgedriver_dir
|
|
25
|
+
+ utils.get_variable_separator()
|
|
26
|
+
+ os.environ["PATH"]
|
|
27
|
+
)
|
|
28
|
+
return edgedriver_filepath
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_edge_version():
|
|
32
|
+
"""
|
|
33
|
+
Get installed version of edge on client
|
|
34
|
+
|
|
35
|
+
:return: The version of edge
|
|
36
|
+
"""
|
|
37
|
+
return utils.get_edge_version()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
"""
|
|
3
|
+
Helper functions for filename and URL generation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import xml.etree.ElementTree as elemTree
|
|
12
|
+
import zipfile
|
|
13
|
+
from io import BytesIO
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
__author__ = "Yeongbin Jo <iam.yeongbin.jo@gmail.com>"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_edgedriver_filename():
|
|
21
|
+
"""
|
|
22
|
+
Returns the filename of the binary for the current platform.
|
|
23
|
+
:return: Binary filename
|
|
24
|
+
"""
|
|
25
|
+
if sys.platform.startswith("win"):
|
|
26
|
+
return "msedgedriver.exe"
|
|
27
|
+
return "msedgedriver"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_variable_separator():
|
|
31
|
+
"""
|
|
32
|
+
Returns the environment variable separator for the current platform.
|
|
33
|
+
:return: Environment variable separator
|
|
34
|
+
"""
|
|
35
|
+
if sys.platform.startswith("win"):
|
|
36
|
+
return ";"
|
|
37
|
+
return ":"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_platform_architecture():
|
|
41
|
+
if sys.platform.startswith("linux") and sys.maxsize > 2**32:
|
|
42
|
+
platform = "linux"
|
|
43
|
+
architecture = "64"
|
|
44
|
+
elif sys.platform == "darwin":
|
|
45
|
+
platform = "mac"
|
|
46
|
+
architecture = "64"
|
|
47
|
+
elif sys.platform.startswith("win"):
|
|
48
|
+
platform = "win"
|
|
49
|
+
architecture = "32"
|
|
50
|
+
else:
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"Could not determine edgedriver download URL for this platform."
|
|
53
|
+
)
|
|
54
|
+
return platform, architecture
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_edgedriver_url(version):
|
|
58
|
+
"""
|
|
59
|
+
Generates the download URL for current platform , architecture and the given version.
|
|
60
|
+
Supports Linux, MacOS and Windows.
|
|
61
|
+
:param version: edgedriver version string
|
|
62
|
+
:return: Download URL for edgedriver
|
|
63
|
+
"""
|
|
64
|
+
base_url = "https://msedgedriver.azureedge.net//"
|
|
65
|
+
platform, architecture = get_platform_architecture()
|
|
66
|
+
return (
|
|
67
|
+
base_url + version + "/edgedriver_" + platform + architecture + ".zip"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def find_binary_in_path(filename):
|
|
72
|
+
"""
|
|
73
|
+
Searches for a binary named `filename` in the current PATH. If an executable is found, its absolute path is returned
|
|
74
|
+
else None.
|
|
75
|
+
:param filename: Filename of the binary
|
|
76
|
+
:return: Absolute path or None
|
|
77
|
+
"""
|
|
78
|
+
if "PATH" not in os.environ:
|
|
79
|
+
return None
|
|
80
|
+
for directory in os.environ["PATH"].split(get_variable_separator()):
|
|
81
|
+
binary = os.path.abspath(os.path.join(directory, filename))
|
|
82
|
+
if os.path.isfile(binary) and os.access(binary, os.X_OK):
|
|
83
|
+
return binary
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_version(binary, required_version):
|
|
88
|
+
try:
|
|
89
|
+
version = subprocess.check_output([binary, "-v"])
|
|
90
|
+
version = re.match(r".*?([\d.]+).*?", version.decode("utf-8"))[1]
|
|
91
|
+
if version == required_version:
|
|
92
|
+
return True
|
|
93
|
+
except Exception:
|
|
94
|
+
return False
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_edge_version():
|
|
99
|
+
"""
|
|
100
|
+
:return: the version of edge installed on client
|
|
101
|
+
"""
|
|
102
|
+
platform, _ = get_platform_architecture()
|
|
103
|
+
if platform == "linux":
|
|
104
|
+
return # Edge for linux still doesn't exists
|
|
105
|
+
elif platform == "mac":
|
|
106
|
+
process = subprocess.Popen(
|
|
107
|
+
[
|
|
108
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
109
|
+
"--version",
|
|
110
|
+
],
|
|
111
|
+
stdout=subprocess.PIPE,
|
|
112
|
+
)
|
|
113
|
+
version = (
|
|
114
|
+
process.communicate()[0]
|
|
115
|
+
.decode("UTF-8")
|
|
116
|
+
.replace("Microsoft Edge", "")
|
|
117
|
+
.strip()
|
|
118
|
+
)
|
|
119
|
+
elif platform == "win":
|
|
120
|
+
process = subprocess.Popen(
|
|
121
|
+
[
|
|
122
|
+
"reg",
|
|
123
|
+
"query",
|
|
124
|
+
"HKEY_CURRENT_USER\\Software\\Microsoft\\Edge\\BLBeacon",
|
|
125
|
+
"/v",
|
|
126
|
+
"version",
|
|
127
|
+
],
|
|
128
|
+
stdout=subprocess.PIPE,
|
|
129
|
+
stderr=subprocess.DEVNULL,
|
|
130
|
+
stdin=subprocess.DEVNULL,
|
|
131
|
+
)
|
|
132
|
+
version = process.communicate()[0].decode("UTF-8").strip().split()[-1]
|
|
133
|
+
else:
|
|
134
|
+
return
|
|
135
|
+
return version
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_major_version(version):
|
|
139
|
+
"""
|
|
140
|
+
:param version: the version of edge
|
|
141
|
+
:return: the major version of edge
|
|
142
|
+
"""
|
|
143
|
+
return version.split(".")[0]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_matched_edgedriver_version(version):
|
|
147
|
+
"""
|
|
148
|
+
:param version: the version of edge
|
|
149
|
+
:return: the version of edgedriver
|
|
150
|
+
"""
|
|
151
|
+
doc = requests.get(
|
|
152
|
+
"https://msedgedriver.azureedge.net/",
|
|
153
|
+
headers={"accept-encoding": "gzip, deflate, br"},
|
|
154
|
+
).text
|
|
155
|
+
root = elemTree.fromstring(doc)
|
|
156
|
+
for k in root.iter("Name"):
|
|
157
|
+
if k.text.find(get_major_version(version) + ".") == 0:
|
|
158
|
+
return k.text.split("/")[0]
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def get_edgedriver_path():
|
|
163
|
+
"""
|
|
164
|
+
:return: path of the edgedriver binary
|
|
165
|
+
"""
|
|
166
|
+
return os.path.abspath(os.path.dirname(__file__))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def print_edgedriver_path():
|
|
170
|
+
"""
|
|
171
|
+
Print the path of the edgedriver binary.
|
|
172
|
+
"""
|
|
173
|
+
print(get_edgedriver_path())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def download_edgedriver(cwd=False):
|
|
177
|
+
"""
|
|
178
|
+
Downloads, unzips and installs edgedriver.
|
|
179
|
+
If a edgedriver binary is found in PATH it will be copied, otherwise downloaded.
|
|
180
|
+
|
|
181
|
+
:param cwd: Flag indicating whether to download to current working directory
|
|
182
|
+
:return: The file path of edgedriver
|
|
183
|
+
"""
|
|
184
|
+
edge_version = get_edge_version()
|
|
185
|
+
if not edge_version:
|
|
186
|
+
logging.debug("edge is not installed.")
|
|
187
|
+
return
|
|
188
|
+
edgedriver_version = get_matched_edgedriver_version(edge_version)
|
|
189
|
+
if not edgedriver_version:
|
|
190
|
+
logging.debug(
|
|
191
|
+
"Can not find edgedriver for currently installed edge version."
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
major_version = get_major_version(edgedriver_version)
|
|
195
|
+
|
|
196
|
+
if cwd:
|
|
197
|
+
edgedriver_dir = os.path.join(
|
|
198
|
+
os.path.abspath(os.getcwd()), major_version
|
|
199
|
+
)
|
|
200
|
+
else:
|
|
201
|
+
edgedriver_dir = os.path.join(
|
|
202
|
+
os.path.abspath(os.path.dirname(__file__)), major_version
|
|
203
|
+
)
|
|
204
|
+
edgedriver_filename = get_edgedriver_filename()
|
|
205
|
+
edgedriver_filepath = os.path.join(edgedriver_dir, edgedriver_filename)
|
|
206
|
+
if not os.path.isfile(edgedriver_filepath) or not check_version(
|
|
207
|
+
edgedriver_filepath, edgedriver_version
|
|
208
|
+
):
|
|
209
|
+
logging.debug(f"Downloading edgedriver ({edgedriver_version})...")
|
|
210
|
+
if not os.path.isdir(edgedriver_dir):
|
|
211
|
+
os.makedirs(edgedriver_dir)
|
|
212
|
+
url = get_edgedriver_url(version=edgedriver_version)
|
|
213
|
+
try:
|
|
214
|
+
response = requests.get(url)
|
|
215
|
+
if response.status_code != 200:
|
|
216
|
+
raise Exception("URL Not Found")
|
|
217
|
+
except Exception:
|
|
218
|
+
raise RuntimeError(f"Failed to download edgedriver archive: {url}")
|
|
219
|
+
archive = BytesIO(response.content)
|
|
220
|
+
with zipfile.ZipFile(archive) as zip_file:
|
|
221
|
+
zip_file.extract(edgedriver_filename, edgedriver_dir)
|
|
222
|
+
else:
|
|
223
|
+
logging.debug("edgedriver is already installed.")
|
|
224
|
+
if not os.access(edgedriver_filepath, os.X_OK):
|
|
225
|
+
os.chmod(edgedriver_filepath, 0o744)
|
|
226
|
+
return edgedriver_filepath
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
print(get_edge_version())
|
|
231
|
+
print(download_edgedriver())
|