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/lint/linter.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from cucu import logger
|
|
9
|
+
from cucu.cli.steps import load_cucu_steps
|
|
10
|
+
from cucu.config import CONFIG
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_lint_rules(rules, filepath):
|
|
14
|
+
"""
|
|
15
|
+
load all of the lint rules from the filepath provided
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
hashmap of of lint rules
|
|
19
|
+
"""
|
|
20
|
+
filepath = os.path.abspath(filepath)
|
|
21
|
+
|
|
22
|
+
if os.path.isdir(filepath):
|
|
23
|
+
basepath = os.path.join(filepath, "**/*.yaml")
|
|
24
|
+
lint_rule_filepaths = glob.iglob(basepath, recursive=True)
|
|
25
|
+
|
|
26
|
+
else:
|
|
27
|
+
lint_rule_filepaths = [filepath]
|
|
28
|
+
|
|
29
|
+
for lint_rule_filepath in lint_rule_filepaths:
|
|
30
|
+
logger.debug(f"loading lint rules from {lint_rule_filepath}")
|
|
31
|
+
|
|
32
|
+
with open(lint_rule_filepath, "r", encoding="utf8") as _input:
|
|
33
|
+
rules_loaded = yaml.safe_load(_input.read())
|
|
34
|
+
|
|
35
|
+
for rule_name, rule in rules_loaded.items():
|
|
36
|
+
if rule_name in rules:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
f"found duplicate rule names {rule_name}, please correct one of the locations."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
rules[rule_name] = rule
|
|
42
|
+
|
|
43
|
+
return rules
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_matcher(name, rule_name, rule, line, state):
|
|
47
|
+
"""
|
|
48
|
+
parses the "matcher" from the rule provided and then returns the tuple:
|
|
49
|
+
(matched, extra_matcher_message) where matched is a boolean indicating that
|
|
50
|
+
the rule name matcher name and rule matched on the line provided and the
|
|
51
|
+
extra_matcher_message is used when reporting the linting failure upstream.
|
|
52
|
+
|
|
53
|
+
name(string): name of the line to match on, ie current_line, previous_line,
|
|
54
|
+
next_line are currently supported
|
|
55
|
+
rule_name(string): the actual name of the rule from the rules file, such as:
|
|
56
|
+
when_keyword_indented_correctly
|
|
57
|
+
rule(dict): the rule dictionary object which contains the matcher, fix, etc.
|
|
58
|
+
line(string): actual line to parse the matcher against
|
|
59
|
+
state(dict): state object passed in which contains a few things such as:
|
|
60
|
+
current_feature_filepath, current_feature_name, etc.
|
|
61
|
+
|
|
62
|
+
returns: a tuple where the first element is True if the matcher matches the
|
|
63
|
+
specified line. False if the matcher simply doesn't apply to this
|
|
64
|
+
line. The second part of the tuple is an string used to augment
|
|
65
|
+
the lint violation if the remainder of the matchers in a rule
|
|
66
|
+
matched their specific line (current_line, previous_line, etc).
|
|
67
|
+
"""
|
|
68
|
+
if name not in rule:
|
|
69
|
+
# when the name provided isn't in the rule then we simply have a no-op
|
|
70
|
+
# where this specific matcher name wasn't even used.
|
|
71
|
+
return (True, "")
|
|
72
|
+
|
|
73
|
+
if "match" in rule[name]:
|
|
74
|
+
if line is None:
|
|
75
|
+
return (False, "")
|
|
76
|
+
|
|
77
|
+
match = re.match(rule[name]["match"], line)
|
|
78
|
+
|
|
79
|
+
if match is None:
|
|
80
|
+
# no match on the specified rule means there's not violation to
|
|
81
|
+
# report
|
|
82
|
+
return (False, "")
|
|
83
|
+
|
|
84
|
+
cwd = f"{os.getcwd()}/"
|
|
85
|
+
|
|
86
|
+
# unique across all feature files
|
|
87
|
+
if "unique_per_all_features" in rule[name]:
|
|
88
|
+
value = match.groups()[0]
|
|
89
|
+
feature_filepath = state["current_feature_filepath"]
|
|
90
|
+
# make the path relative to the current working directory
|
|
91
|
+
feature_filepath = feature_filepath.replace(cwd, "")
|
|
92
|
+
|
|
93
|
+
if rule_name not in state["unique_per_all_features"]:
|
|
94
|
+
state["unique_per_all_features"][rule_name] = {}
|
|
95
|
+
|
|
96
|
+
if value in state["unique_per_all_features"][rule_name]:
|
|
97
|
+
# we have another feature which already has this value in use.
|
|
98
|
+
other_filepath = state["unique_per_all_features"][rule_name][
|
|
99
|
+
value
|
|
100
|
+
]
|
|
101
|
+
# make the path relative to the current working directory
|
|
102
|
+
other_filepath = other_filepath.replace(cwd, "")
|
|
103
|
+
|
|
104
|
+
if other_filepath != feature_filepath:
|
|
105
|
+
return (
|
|
106
|
+
True,
|
|
107
|
+
f', "{value}" also used in "{other_filepath}"',
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
state["unique_per_all_features"][rule_name][value] = (
|
|
111
|
+
feature_filepath
|
|
112
|
+
)
|
|
113
|
+
return (False, "")
|
|
114
|
+
|
|
115
|
+
# unique across all scenarios across all features
|
|
116
|
+
if "unique_per_all_scenarios" in rule[name]:
|
|
117
|
+
value = match.groups()[0]
|
|
118
|
+
feature_filepath = state["current_feature_filepath"]
|
|
119
|
+
# make the path relative to the current working directory
|
|
120
|
+
feature_filepath = feature_filepath.replace(cwd, "")
|
|
121
|
+
scenario_name = state["current_scenario_name"]
|
|
122
|
+
|
|
123
|
+
if rule_name not in state["unique_per_all_scenarios"]:
|
|
124
|
+
state["unique_per_all_scenarios"][rule_name] = {}
|
|
125
|
+
|
|
126
|
+
if value in state["unique_per_all_scenarios"][rule_name]:
|
|
127
|
+
# we have another scenario which already has this value in use.
|
|
128
|
+
other_file_path, other_line_number, other_scenario_name = (
|
|
129
|
+
state["unique_per_all_scenarios"][rule_name][value]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
True,
|
|
134
|
+
f', "{value}" also used in "{other_file_path}:{other_line_number}" Scenario: "{other_scenario_name}"',
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
state["unique_per_all_scenarios"][rule_name][value] = [
|
|
138
|
+
feature_filepath,
|
|
139
|
+
state["current_line_number"],
|
|
140
|
+
scenario_name,
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
return (False, "")
|
|
144
|
+
|
|
145
|
+
return (True, "")
|
|
146
|
+
|
|
147
|
+
raise RuntimeError(f"unsupported matcher for {name}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def lint_line(state, rules, steps, line_number, lines, filepath):
|
|
151
|
+
""" """
|
|
152
|
+
if line_number >= 1:
|
|
153
|
+
previous_line = lines[line_number - 1]
|
|
154
|
+
|
|
155
|
+
else:
|
|
156
|
+
previous_line = None
|
|
157
|
+
|
|
158
|
+
current_line = lines[line_number]
|
|
159
|
+
|
|
160
|
+
if line_number + 1 < len(lines):
|
|
161
|
+
next_line = lines[line_number + 1]
|
|
162
|
+
else:
|
|
163
|
+
next_line = None
|
|
164
|
+
|
|
165
|
+
logger.debug(f'linting line "{current_line}"')
|
|
166
|
+
|
|
167
|
+
violations = []
|
|
168
|
+
for rule_name in rules.keys():
|
|
169
|
+
logger.debug(f' * checking against rule "{rule_name}"')
|
|
170
|
+
rule = rules[rule_name]
|
|
171
|
+
|
|
172
|
+
# skip paths that match the exclude regex
|
|
173
|
+
if "exclude" in rule and re.match(rule["exclude"], filepath):
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
(current_matcher, current_message) = parse_matcher(
|
|
177
|
+
"current_line",
|
|
178
|
+
rule_name,
|
|
179
|
+
rule,
|
|
180
|
+
current_line,
|
|
181
|
+
state,
|
|
182
|
+
)
|
|
183
|
+
(previous_matcher, previous_message) = parse_matcher(
|
|
184
|
+
"previous_line",
|
|
185
|
+
rule_name,
|
|
186
|
+
rule,
|
|
187
|
+
previous_line,
|
|
188
|
+
state,
|
|
189
|
+
)
|
|
190
|
+
(next_matcher, next_message) = parse_matcher(
|
|
191
|
+
"next_line",
|
|
192
|
+
rule_name,
|
|
193
|
+
rule,
|
|
194
|
+
next_line,
|
|
195
|
+
state,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
logger.debug(
|
|
199
|
+
f"previous matcher {previous_matcher} current matcher {current_matcher} next matcher {next_matcher}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if current_matcher and previous_matcher and next_matcher:
|
|
203
|
+
type = rule["type"][0].upper()
|
|
204
|
+
message = rule["message"]
|
|
205
|
+
|
|
206
|
+
if "fix" in rule:
|
|
207
|
+
fix = rule["fix"]
|
|
208
|
+
else:
|
|
209
|
+
fix = None
|
|
210
|
+
|
|
211
|
+
violations.append(
|
|
212
|
+
{
|
|
213
|
+
"location": {
|
|
214
|
+
"filepath": os.path.relpath(filepath),
|
|
215
|
+
"line": line_number,
|
|
216
|
+
},
|
|
217
|
+
"type": type,
|
|
218
|
+
"message": f"{message}{previous_message}{current_message}{next_message}",
|
|
219
|
+
"fix": fix,
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# find any undefined steps and mark them as an unfixable violation
|
|
224
|
+
current_line = current_line.strip()
|
|
225
|
+
undefined_steps = [
|
|
226
|
+
{
|
|
227
|
+
"location": {
|
|
228
|
+
"filepath": os.path.relpath(filepath),
|
|
229
|
+
"line": line_number,
|
|
230
|
+
},
|
|
231
|
+
"type": "error",
|
|
232
|
+
"message": f'undefined step "{step_name}"',
|
|
233
|
+
"fix": None,
|
|
234
|
+
}
|
|
235
|
+
for step_name in steps
|
|
236
|
+
# step with no location/type/etc is an undefined step
|
|
237
|
+
if steps[step_name] is None and current_line.find(step_name) != -1
|
|
238
|
+
]
|
|
239
|
+
violations.extend(undefined_steps)
|
|
240
|
+
|
|
241
|
+
return violations
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def fix(violations):
|
|
245
|
+
"""
|
|
246
|
+
fix the violations found in a set of violations relating to a single
|
|
247
|
+
feature file.
|
|
248
|
+
"""
|
|
249
|
+
if not violations:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
deletions = []
|
|
253
|
+
filepath = violations[0]["location"]["filepath"]
|
|
254
|
+
lines = open(filepath, "r").read().split("\n")
|
|
255
|
+
|
|
256
|
+
for violation in violations:
|
|
257
|
+
if violation["fix"] is None:
|
|
258
|
+
violation["fixed"] = False
|
|
259
|
+
|
|
260
|
+
else:
|
|
261
|
+
line_number = violation["location"]["line"]
|
|
262
|
+
line_to_fix = lines[line_number]
|
|
263
|
+
|
|
264
|
+
if "delete" in violation["fix"]:
|
|
265
|
+
# store the deletions to do at the end
|
|
266
|
+
deletions.append(violation)
|
|
267
|
+
|
|
268
|
+
elif "match" in violation["fix"]:
|
|
269
|
+
match = violation["fix"]["match"]
|
|
270
|
+
replace = violation["fix"]["replace"]
|
|
271
|
+
fixed_line = re.sub(match, replace, line_to_fix)
|
|
272
|
+
lines[line_number] = fixed_line
|
|
273
|
+
violation["fixed"] = True
|
|
274
|
+
|
|
275
|
+
else:
|
|
276
|
+
raise RuntimeError(f"unknown fix type in {violation}")
|
|
277
|
+
|
|
278
|
+
# sort the deletions from bottom to the top of the file and then perform
|
|
279
|
+
# the deletions
|
|
280
|
+
deletions.sort(key=lambda x: x["location"]["line"], reverse=True)
|
|
281
|
+
for violation in deletions:
|
|
282
|
+
del lines[violation["location"]["line"]]
|
|
283
|
+
violation["fixed"] = True
|
|
284
|
+
|
|
285
|
+
with open(filepath, "w") as output:
|
|
286
|
+
output.write("\n".join(lines))
|
|
287
|
+
|
|
288
|
+
return violations
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def load_builtin_lint_rules(rules):
|
|
292
|
+
"""
|
|
293
|
+
load internal builtin lint rules and used primarily for unit testing
|
|
294
|
+
"""
|
|
295
|
+
cucu_path = os.path.dirname(importlib.util.find_spec("cucu").origin)
|
|
296
|
+
lint_rules_path = os.path.join(cucu_path, "lint", "rules")
|
|
297
|
+
load_lint_rules(rules, lint_rules_path)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def lint(filepath):
|
|
301
|
+
"""
|
|
302
|
+
lint the filepath provided which could be a base directory where many
|
|
303
|
+
feature files exists and we must traverse recursively or a specific file.
|
|
304
|
+
|
|
305
|
+
Params:
|
|
306
|
+
filepath(string): path to a directory or feature file to lint
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
a generator of violations per file, so each value yielded to the
|
|
310
|
+
generator is a list of violations within the same file
|
|
311
|
+
"""
|
|
312
|
+
rules = {}
|
|
313
|
+
|
|
314
|
+
logger.debug(f"linting {filepath}")
|
|
315
|
+
|
|
316
|
+
# load the base lint rules
|
|
317
|
+
load_builtin_lint_rules(rules)
|
|
318
|
+
|
|
319
|
+
if CONFIG["CUCU_LINT_RULES_PATH"]:
|
|
320
|
+
# load any other rules paths linked via CUCU_LINT_RULES_PATH variable
|
|
321
|
+
lint_rule_paths = CONFIG["CUCU_LINT_RULES_PATH"].split(",")
|
|
322
|
+
|
|
323
|
+
for lint_rule_path in lint_rule_paths:
|
|
324
|
+
logger.debug(f"loading custom rules from: {lint_rule_path}")
|
|
325
|
+
load_lint_rules(rules, lint_rule_path)
|
|
326
|
+
|
|
327
|
+
steps, steps_error = load_cucu_steps(filepath=filepath)
|
|
328
|
+
|
|
329
|
+
# state object used to carry state from the top level linting function down
|
|
330
|
+
# to the functions handling the lint rules and reporting on lint failures
|
|
331
|
+
state = {
|
|
332
|
+
"unique_per_all_features": {},
|
|
333
|
+
"unique_per_all_scenarios": {},
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if steps_error:
|
|
337
|
+
yield [
|
|
338
|
+
{
|
|
339
|
+
"type": "steps_error",
|
|
340
|
+
"message": steps_error,
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
filepath = os.path.abspath(filepath)
|
|
345
|
+
|
|
346
|
+
if os.path.isdir(filepath):
|
|
347
|
+
basepath = os.path.join(filepath, "**/*.feature")
|
|
348
|
+
# XXX: for now sorted by name... we could expose some options for other
|
|
349
|
+
# sorting orders if it makes sense
|
|
350
|
+
feature_filepaths = sorted(glob.iglob(basepath, recursive=True))
|
|
351
|
+
|
|
352
|
+
else:
|
|
353
|
+
feature_filepaths = [filepath]
|
|
354
|
+
|
|
355
|
+
for feature_filepath in feature_filepaths:
|
|
356
|
+
state["current_feature_filepath"] = feature_filepath
|
|
357
|
+
|
|
358
|
+
lines = open(feature_filepath).read().split("\n")
|
|
359
|
+
line_number = 0
|
|
360
|
+
|
|
361
|
+
violations = []
|
|
362
|
+
in_docstring = {
|
|
363
|
+
'"""': False,
|
|
364
|
+
"'''": False,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for line in lines:
|
|
368
|
+
state["current_line_number"] = line_number
|
|
369
|
+
|
|
370
|
+
feature_match = re.match(".*Feature: (.*)", line)
|
|
371
|
+
if feature_match is not None:
|
|
372
|
+
state["current_feature_name"] = feature_match.group(1)
|
|
373
|
+
|
|
374
|
+
scenario_match = re.match(" Scenario: (.*)", line)
|
|
375
|
+
|
|
376
|
+
if scenario_match is not None:
|
|
377
|
+
state["current_scenario_name"] = scenario_match.group(1)
|
|
378
|
+
else:
|
|
379
|
+
state["current_scenario_name"] = ""
|
|
380
|
+
|
|
381
|
+
# maintain state of if we're inside a docstring and if we are then
|
|
382
|
+
# do not apply any linting rules as its a freeform space for text
|
|
383
|
+
if line.strip() == '"""':
|
|
384
|
+
in_docstring['"""'] = not in_docstring['"""']
|
|
385
|
+
|
|
386
|
+
if line.strip() == "'''":
|
|
387
|
+
in_docstring["'''"] = not in_docstring["'''"]
|
|
388
|
+
|
|
389
|
+
if not (in_docstring['"""'] or in_docstring["'''"]):
|
|
390
|
+
for violation in lint_line(
|
|
391
|
+
state, rules, steps, line_number, lines, feature_filepath
|
|
392
|
+
):
|
|
393
|
+
violations.append(violation)
|
|
394
|
+
|
|
395
|
+
line_number += 1
|
|
396
|
+
|
|
397
|
+
yield violations
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# built in cucu linting rules
|
|
2
|
+
---
|
|
3
|
+
|
|
4
|
+
feature_name_must_not_contain_special_characters:
|
|
5
|
+
message: "feature name must not contain the characters '/\\:?'"
|
|
6
|
+
type: error
|
|
7
|
+
current_line:
|
|
8
|
+
match: '^\s*Feature: .*[/\:?].*'
|
|
9
|
+
|
|
10
|
+
scenario_name_must_not_contain_special_characters:
|
|
11
|
+
message: "scenario name must not contain the characters '/\\:?'"
|
|
12
|
+
type: error
|
|
13
|
+
current_line:
|
|
14
|
+
match: '^\s*Scenario: .*[/\:?].*'
|
|
15
|
+
|
|
16
|
+
feature_name_must_be_unique:
|
|
17
|
+
message: feature name must be unique
|
|
18
|
+
type: error
|
|
19
|
+
current_line:
|
|
20
|
+
match: '^\s*Feature: (.*)'
|
|
21
|
+
unique_per_all_features: true
|
|
22
|
+
|
|
23
|
+
scenario_name_must_be_unique:
|
|
24
|
+
message: scenario name must be unique
|
|
25
|
+
type: error
|
|
26
|
+
current_line:
|
|
27
|
+
match: '^\s*Scenario: (.*)'
|
|
28
|
+
unique_per_all_scenarios: true
|
|
29
|
+
|
|
30
|
+
feature_name_on_first_line:
|
|
31
|
+
message: feature name should not have any indentation
|
|
32
|
+
type: warning
|
|
33
|
+
current_line:
|
|
34
|
+
match: '^\s+Feature: .*'
|
|
35
|
+
fix:
|
|
36
|
+
match: '^\s*'
|
|
37
|
+
replace: ''
|
|
38
|
+
|
|
39
|
+
scenario_tags_with_appropriate_indentation:
|
|
40
|
+
message: scenario tags should be indented with 2 spaces
|
|
41
|
+
type: warning
|
|
42
|
+
# any tag line that has 0,1 or 3 or more spaces is a violation
|
|
43
|
+
current_line:
|
|
44
|
+
match: '^(\s{0,1}|\s{3,})@.*'
|
|
45
|
+
next_line:
|
|
46
|
+
match: '^\s*Scenario: (.*)'
|
|
47
|
+
fix:
|
|
48
|
+
match: '^\s*'
|
|
49
|
+
replace: ' '
|
|
50
|
+
|
|
51
|
+
feature_tags_with_appropriate_indentation:
|
|
52
|
+
message: feature tags should not be indented
|
|
53
|
+
type: warning
|
|
54
|
+
current_line:
|
|
55
|
+
match: '^(\s+)@.*'
|
|
56
|
+
next_line:
|
|
57
|
+
match: '^\s*Feature: (.*)'
|
|
58
|
+
fix:
|
|
59
|
+
match: '^\s*'
|
|
60
|
+
replace: ''
|
|
61
|
+
|
|
62
|
+
scenario_name_with_appropriate_indentation:
|
|
63
|
+
message: scenario name should be indented with 2 spaces
|
|
64
|
+
type: warning
|
|
65
|
+
# any scenario line that has 0,1 or 3 or more spaces is a violation
|
|
66
|
+
current_line:
|
|
67
|
+
match: '^(\s{0,1}|\s{3,})Scenario: .*'
|
|
68
|
+
fix:
|
|
69
|
+
match: '^\s*'
|
|
70
|
+
replace: ' '
|
|
71
|
+
|
|
72
|
+
given_keyword_indented_correctly:
|
|
73
|
+
message: given keyword should be indented with 4 spaces
|
|
74
|
+
type: warning
|
|
75
|
+
current_line:
|
|
76
|
+
match: '^(\s{0,3}|\s{5,})Given .*'
|
|
77
|
+
fix:
|
|
78
|
+
match: '^\s*'
|
|
79
|
+
replace: ' '
|
|
80
|
+
|
|
81
|
+
when_keyword_indented_correctly:
|
|
82
|
+
message: when keyword should be indented with 5 spaces
|
|
83
|
+
type: warning
|
|
84
|
+
current_line:
|
|
85
|
+
match: '^(\s{0,4}|\s{6,})When .*'
|
|
86
|
+
fix:
|
|
87
|
+
match: '^\s*'
|
|
88
|
+
replace: ' '
|
|
89
|
+
|
|
90
|
+
then_keyword_indented_correctly:
|
|
91
|
+
message: then keyword should be indented with 5 spaces
|
|
92
|
+
type: warning
|
|
93
|
+
current_line:
|
|
94
|
+
match: '^(\s{0,4}|\s{6,})Then .*'
|
|
95
|
+
fix:
|
|
96
|
+
match: '^\s*'
|
|
97
|
+
replace: ' '
|
|
98
|
+
|
|
99
|
+
and_keyword_indented_correctly:
|
|
100
|
+
message: and keyword should be indented with 6 spaces
|
|
101
|
+
type: warning
|
|
102
|
+
current_line:
|
|
103
|
+
match: '^(\s{0,5}|\s{7,})And .*'
|
|
104
|
+
fix:
|
|
105
|
+
match: '^\s*'
|
|
106
|
+
replace: ' '
|
|
107
|
+
|
|
108
|
+
line_with_extraneous_whitespace:
|
|
109
|
+
message: line has extraneous whitespace at the end
|
|
110
|
+
type: warning
|
|
111
|
+
current_line:
|
|
112
|
+
match: '^.*[ \t]+$'
|
|
113
|
+
fix:
|
|
114
|
+
match: '[ \t]+$'
|
|
115
|
+
replace: ''
|
|
116
|
+
|
|
117
|
+
too_many_blank_lines:
|
|
118
|
+
message: too many blank lines
|
|
119
|
+
type: warning
|
|
120
|
+
previous_line:
|
|
121
|
+
match: '^\s*$'
|
|
122
|
+
current_line:
|
|
123
|
+
match: '^\s*$'
|
|
124
|
+
fix:
|
|
125
|
+
delete: true
|
cucu/logger.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
from cucu.config import CONFIG
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def init_logging(logging_level):
|
|
9
|
+
"""
|
|
10
|
+
initialize the cucu logger
|
|
11
|
+
"""
|
|
12
|
+
formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
|
|
13
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
14
|
+
handler.setFormatter(formatter)
|
|
15
|
+
handler.setLevel(logging_level)
|
|
16
|
+
|
|
17
|
+
logging.getLogger().addHandler(handler)
|
|
18
|
+
# set the top level logger to DEBUG while the console stream handler is
|
|
19
|
+
# actually set to whatever you passed in using --logging-level which is
|
|
20
|
+
# INFO by default
|
|
21
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
22
|
+
|
|
23
|
+
logging.debug("logger initialized")
|
|
24
|
+
|
|
25
|
+
logging.getLogger("parse").setLevel(logging.WARNING)
|
|
26
|
+
logging.getLogger("selenium").setLevel(logging.WARNING)
|
|
27
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def init_debug_logger(output_file):
|
|
31
|
+
"""
|
|
32
|
+
initialize a debug logger handler that runs at debug level and pushes
|
|
33
|
+
all of the logs to the output file provided without affecting the logging
|
|
34
|
+
of the main root logger
|
|
35
|
+
"""
|
|
36
|
+
# assume that there's only 2 loggers the first one is the console one and
|
|
37
|
+
# the second one if present is a previously set debug logger for the
|
|
38
|
+
# previously executed scenario
|
|
39
|
+
if len(logging.getLogger().handlers) > 1:
|
|
40
|
+
logging.getLogger().removeHandler(logging.getLogger().handlers[1])
|
|
41
|
+
|
|
42
|
+
formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
|
|
43
|
+
handler = logging.StreamHandler(output_file)
|
|
44
|
+
handler.setFormatter(formatter)
|
|
45
|
+
handler.setLevel(logging.DEBUG)
|
|
46
|
+
logging.getLogger().addHandler(handler)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@wraps(logging.log)
|
|
50
|
+
def log(*args, **kwargs):
|
|
51
|
+
console_handler = logging.getLogger().handlers[0]
|
|
52
|
+
logging_level = console_handler.level
|
|
53
|
+
|
|
54
|
+
msg_level = args[0]
|
|
55
|
+
if logging_level <= msg_level:
|
|
56
|
+
CONFIG["__CUCU_WROTE_TO_OUTPUT"] = True
|
|
57
|
+
|
|
58
|
+
logging.getLogger().log(*args, **kwargs)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@wraps(logging.debug)
|
|
62
|
+
def debug(*args, **kwargs):
|
|
63
|
+
console_handler = logging.getLogger().handlers[0]
|
|
64
|
+
logging_level = console_handler.level
|
|
65
|
+
|
|
66
|
+
if logging_level <= logging.DEBUG:
|
|
67
|
+
CONFIG["__CUCU_WROTE_TO_OUTPUT"] = True
|
|
68
|
+
|
|
69
|
+
logging.getLogger().debug(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@wraps(logging.info)
|
|
73
|
+
def info(*args, **kwargs):
|
|
74
|
+
console_handler = logging.getLogger().handlers[0]
|
|
75
|
+
logging_level = console_handler.level
|
|
76
|
+
|
|
77
|
+
if logging_level <= logging.INFO:
|
|
78
|
+
CONFIG["__CUCU_WROTE_TO_OUTPUT"] = True
|
|
79
|
+
|
|
80
|
+
logging.info(*args, **kwargs)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@wraps(logging.warn)
|
|
84
|
+
def warn(*args, **kwargs):
|
|
85
|
+
console_handler = logging.getLogger().handlers[0]
|
|
86
|
+
logging_level = console_handler.level
|
|
87
|
+
|
|
88
|
+
if logging_level <= logging.WARN:
|
|
89
|
+
CONFIG["__CUCU_WROTE_TO_OUTPUT"] = True
|
|
90
|
+
|
|
91
|
+
logging.getLogger().warning(*args, **kwargs)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@wraps(logging.error)
|
|
95
|
+
def error(*args, **kwargs):
|
|
96
|
+
console_handler = logging.getLogger().handlers[0]
|
|
97
|
+
logging_level = console_handler.level
|
|
98
|
+
|
|
99
|
+
if logging_level <= logging.ERROR:
|
|
100
|
+
CONFIG["__CUCU_WROTE_TO_OUTPUT"] = True
|
|
101
|
+
|
|
102
|
+
logging.getLogger().error(*args, **kwargs)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@wraps(logging.exception)
|
|
106
|
+
def exception(*args, **kwargs):
|
|
107
|
+
console_handler = logging.getLogger().handlers[0]
|
|
108
|
+
logging_level = console_handler.level
|
|
109
|
+
|
|
110
|
+
if logging_level <= logging.ERROR:
|
|
111
|
+
CONFIG["__CUCU_WROTE_TO_OUTPUT"] = True
|
|
112
|
+
|
|
113
|
+
logging.getLogger().exception(*args, **kwargs)
|
cucu/matcher/__init__.py
ADDED
|
File without changes
|
cucu/matcher/core.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#
|
|
2
|
+
# cucu's matching sub system which allows for a pluggable matching architecture
|
|
3
|
+
#
|
|
4
|
+
|
|
5
|
+
# WIP: thinking of how to generalize the idea of finding elements on the
|
|
6
|
+
# currently opened browser while also being able to handle opening
|
|
7
|
+
# browsers through selenium, cypress or whatever.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ElementMatcher:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def find_button(self, execute_script, name, index: 0):
|
|
15
|
+
"""
|
|
16
|
+
return the button labeled by the name provided and use the index to
|
|
17
|
+
pick a specific one if there are duplicates.
|
|
18
|
+
|
|
19
|
+
arguments:
|
|
20
|
+
execute_script - function to execute javascript in the currently
|
|
21
|
+
running browsers javascript console.
|
|
22
|
+
name - name of the button we want to find on screen.
|
|
23
|
+
index - 0-base index of which element to return if there
|
|
24
|
+
are duplicates. default: 0
|
|
25
|
+
|
|
26
|
+
return:
|
|
27
|
+
return the Element found
|
|
28
|
+
|
|
29
|
+
"""
|
|
30
|
+
raise RuntimeError("implement me")
|