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.

Files changed (83) hide show
  1. cucu/__init__.py +38 -0
  2. cucu/ansi_parser.py +58 -0
  3. cucu/behave_tweaks.py +196 -0
  4. cucu/browser/__init__.py +0 -0
  5. cucu/browser/core.py +80 -0
  6. cucu/browser/frames.py +106 -0
  7. cucu/browser/selenium.py +323 -0
  8. cucu/browser/selenium_tweaks.py +27 -0
  9. cucu/cli/__init__.py +3 -0
  10. cucu/cli/core.py +788 -0
  11. cucu/cli/run.py +207 -0
  12. cucu/cli/steps.py +137 -0
  13. cucu/cli/thread_dumper.py +55 -0
  14. cucu/config.py +440 -0
  15. cucu/edgedriver_autoinstaller/README.md +1 -0
  16. cucu/edgedriver_autoinstaller/__init__.py +37 -0
  17. cucu/edgedriver_autoinstaller/utils.py +231 -0
  18. cucu/environment.py +283 -0
  19. cucu/external/jquery/jquery-3.5.1.min.js +2 -0
  20. cucu/formatter/__init__.py +0 -0
  21. cucu/formatter/cucu.py +261 -0
  22. cucu/formatter/json.py +321 -0
  23. cucu/formatter/junit.py +289 -0
  24. cucu/fuzzy/__init__.py +3 -0
  25. cucu/fuzzy/core.py +107 -0
  26. cucu/fuzzy/fuzzy.js +253 -0
  27. cucu/helpers.py +875 -0
  28. cucu/hooks.py +205 -0
  29. cucu/language_server/__init__.py +3 -0
  30. cucu/language_server/core.py +114 -0
  31. cucu/lint/__init__.py +0 -0
  32. cucu/lint/linter.py +397 -0
  33. cucu/lint/rules/format.yaml +125 -0
  34. cucu/logger.py +113 -0
  35. cucu/matcher/__init__.py +0 -0
  36. cucu/matcher/core.py +30 -0
  37. cucu/page_checks.py +63 -0
  38. cucu/reporter/__init__.py +3 -0
  39. cucu/reporter/external/bootstrap.min.css +7 -0
  40. cucu/reporter/external/bootstrap.min.js +7 -0
  41. cucu/reporter/external/dataTables.bootstrap.min.css +1 -0
  42. cucu/reporter/external/dataTables.bootstrap.min.js +14 -0
  43. cucu/reporter/external/jquery-3.5.1.min.js +2 -0
  44. cucu/reporter/external/jquery.dataTables.min.js +192 -0
  45. cucu/reporter/external/popper.min.js +5 -0
  46. cucu/reporter/favicon.png +0 -0
  47. cucu/reporter/html.py +452 -0
  48. cucu/reporter/templates/feature.html +72 -0
  49. cucu/reporter/templates/flat.html +48 -0
  50. cucu/reporter/templates/index.html +49 -0
  51. cucu/reporter/templates/layout.html +109 -0
  52. cucu/reporter/templates/scenario.html +200 -0
  53. cucu/steps/__init__.py +27 -0
  54. cucu/steps/base_steps.py +88 -0
  55. cucu/steps/browser_steps.py +337 -0
  56. cucu/steps/button_steps.py +91 -0
  57. cucu/steps/checkbox_steps.py +111 -0
  58. cucu/steps/command_steps.py +181 -0
  59. cucu/steps/comment_steps.py +17 -0
  60. cucu/steps/draggable_steps.py +168 -0
  61. cucu/steps/dropdown_steps.py +467 -0
  62. cucu/steps/file_input_steps.py +80 -0
  63. cucu/steps/filesystem_steps.py +144 -0
  64. cucu/steps/flow_control_steps.py +198 -0
  65. cucu/steps/image_steps.py +37 -0
  66. cucu/steps/input_steps.py +301 -0
  67. cucu/steps/link_steps.py +63 -0
  68. cucu/steps/menuitem_steps.py +39 -0
  69. cucu/steps/platform_steps.py +29 -0
  70. cucu/steps/radio_steps.py +187 -0
  71. cucu/steps/step_utils.py +55 -0
  72. cucu/steps/tab_steps.py +68 -0
  73. cucu/steps/table_steps.py +437 -0
  74. cucu/steps/tables.js +28 -0
  75. cucu/steps/text_steps.py +78 -0
  76. cucu/steps/variable_steps.py +100 -0
  77. cucu/steps/webserver_steps.py +40 -0
  78. cucu/utils.py +269 -0
  79. cucu-1.0.0.dist-info/METADATA +424 -0
  80. cucu-1.0.0.dist-info/RECORD +83 -0
  81. cucu-1.0.0.dist-info/WHEEL +4 -0
  82. cucu-1.0.0.dist-info/entry_points.txt +2 -0
  83. cucu-1.0.0.dist-info/licenses/LICENSE +32 -0
cucu/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ __version__ = "0.1.0"
3
+
4
+ import sys
5
+ from cucu import behave_tweaks
6
+ from cucu.browser import selenium_tweaks
7
+
8
+ # intercept the stdout/stderr so we can do things such as hiding secrets in logs
9
+ behave_tweaks.init_step_hooks(sys.stdout, sys.stderr)
10
+ selenium_tweaks.init()
11
+
12
+ from cucu.hooks import (
13
+ init_global_hook_variables,
14
+ init_scenario_hook_variables,
15
+ register_after_all_hook,
16
+ register_before_all_hook,
17
+ register_after_this_scenario_hook,
18
+ register_before_scenario_hook,
19
+ register_after_scenario_hook,
20
+ register_before_step_hook,
21
+ register_after_step_hook,
22
+ register_page_check_hook,
23
+ register_custom_variable_handling,
24
+ register_custom_tags_in_report_handling,
25
+ register_custom_scenario_subheader_in_report_handling,
26
+ register_custom_junit_failure_handler,
27
+ register_before_retry_hook,
28
+ )
29
+
30
+ from cucu.utils import (
31
+ format_gherkin_table,
32
+ run_steps,
33
+ retry,
34
+ StopRetryException,
35
+ )
36
+
37
+ from cucu import helpers
38
+ from behave import step
cucu/ansi_parser.py ADDED
@@ -0,0 +1,58 @@
1
+ import html
2
+ import re
3
+
4
+ from behave.formatter.ansi_escapes import colors, escapes
5
+
6
+ ESC_SEQ = r"\x1b["
7
+ TRANSLATION = (
8
+ {colors["grey"]: '<span style="color: grey;">\n'}
9
+ | { # grey indicates a new step, and hence add a line break
10
+ v: f'<span style="color: {k};">'
11
+ for k, v in colors.items()
12
+ if k != "grey"
13
+ }
14
+ | {
15
+ "\n" + escapes["reset"] + "\n": "</span>\n",
16
+ escapes["reset"]: "</span>",
17
+ escapes["up"] + "\n": "", # remove a line break with cursor up
18
+ ESC_SEQ + "46": "", # ignore DELETE (num keypad)
19
+ ESC_SEQ + "48": "", # ignore INSERT (num keypad)
20
+ ESC_SEQ + "49": "", # ignore END (num keypad)
21
+ ESC_SEQ + "50": "", # ignore DOWN ARROW (num keypad)
22
+ ESC_SEQ + "51": "", # ignore PAGE DOWN (num keypad)
23
+ ESC_SEQ + "52": "", # ignore LEFT ARROW (num keypad)
24
+ ESC_SEQ + "54": "", # ignore RIGHT ARROW (num keypad)
25
+ ESC_SEQ + "55": "", # ignore HOME (num keypad)
26
+ ESC_SEQ + "56": "", # ignore UP ARROW (num keypad)
27
+ ESC_SEQ + "57": "", # ignore PAGE UP (num keypad)
28
+ }
29
+ )
30
+ RE_TO_HTML = re.compile("|".join(map(re.escape, TRANSLATION)))
31
+
32
+ RE_TO_REMOVE = re.compile(
33
+ r"\x1b\[(0;)?[0-9A-F]{1,2}m"
34
+ ) # detect hex values, not just decimal digits
35
+
36
+
37
+ def remove_ansi(input: str) -> str:
38
+ return RE_TO_REMOVE.sub("", input)
39
+
40
+
41
+ def parse_log_to_html(input: str) -> str:
42
+ """
43
+ Parse an ansi color log to html
44
+ """
45
+ html_start = "<!DOCTYPE html>\n<html>\n"
46
+ html_end = "</html>\n"
47
+ head = '<head>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8">\n</head>\n'
48
+ body_start = '<body style="color: white; background-color: #100c08;">' # use dark bg since colors are from behave
49
+ body_end = "</body>\n"
50
+ result = f"{html_start}{head}{body_start}<pre>\n{RE_TO_HTML.sub(lambda match: TRANSLATION[match.group(0)], html.escape(input, quote=False))}\n</pre>{body_end}{html_end}"
51
+ if ESC_SEQ in result:
52
+ lines = "\n".join([x for x in result.split("\n") if ESC_SEQ in x])
53
+
54
+ print(
55
+ f"Detected unmapped ansi escape code!:\n{lines}"
56
+ ) # use print instead of logger to avoid circular import
57
+
58
+ return result
cucu/behave_tweaks.py ADDED
@@ -0,0 +1,196 @@
1
+ #
2
+ # XXX: a lot of convoluted logic to handle capturing per step stdou/stderr
3
+ # writing while not interfering with the way behave does its own log
4
+ # capturing
5
+ #
6
+ import sys
7
+ import warnings
8
+ from functools import wraps
9
+
10
+ import behave
11
+ from behave.__main__ import main as original_behave_main
12
+ from behave.model import Table
13
+
14
+ from cucu.config import CONFIG
15
+
16
+
17
+ def behave_main(args):
18
+ return original_behave_main(args)
19
+
20
+
21
+ def init_outputs(stdout, stderr):
22
+ # capturing stdout and stderr output to record in reporting
23
+ sys.stdout = CucuOutputStream(sys.stdout)
24
+ sys.stderr = CucuOutputStream(sys.stderr)
25
+
26
+
27
+ def init_step_hooks(stdout, stderr):
28
+ init_outputs(stdout, stderr)
29
+ #
30
+ # wrap the given, when, then, step decorators from behave so we can intercept
31
+ # the step arguments and do things such as replacing variable references that
32
+ # are wrapped with curly braces {...}.
33
+ #
34
+ for decorator_name in ["given", "when", "then", "step"]:
35
+ decorator = behave.__dict__[decorator_name]
36
+
37
+ def inner_step_func(func, *args, variable_passthru=False, **kwargs):
38
+ #
39
+ # replace the variable references in the args and kwargs passed to the
40
+ # step
41
+ #
42
+ # TODO: for tables and multiline strings there's a bit more
43
+ # work to be done here.
44
+ #
45
+ if not variable_passthru:
46
+ args = [CONFIG.resolve(value) for value in args]
47
+ kwargs = {
48
+ key: CONFIG.resolve(val) for (key, val) in kwargs.items()
49
+ }
50
+
51
+ # resolve variables in text and table data
52
+ ctx = args[0]
53
+
54
+ # we know what we're doing modifying this ctx value and so lets
55
+ # avoid the unnecessary warning int he logs
56
+ with warnings.catch_warnings():
57
+ warnings.simplefilter("ignore")
58
+
59
+ # resolve variables in the multiline string arguments
60
+ ctx.text = CONFIG.resolve(ctx.text)
61
+
62
+ # resolve variables in the table values
63
+ if ctx.table is not None:
64
+ ctx.table.original = Table(
65
+ ctx.table.headings, rows=ctx.table.rows
66
+ )
67
+ new_rows = []
68
+ for row in ctx.table.rows:
69
+ new_row = []
70
+
71
+ for value in row:
72
+ new_row.append(CONFIG.resolve(value))
73
+
74
+ new_rows.append(new_row)
75
+
76
+ ctx.table.rows = new_rows
77
+
78
+ func(*args, **kwargs)
79
+
80
+ def new_decorator(
81
+ step_text, fix_inner_step=lambda x: x, variable_passthru=False
82
+ ):
83
+ """
84
+ the new @step decorator
85
+
86
+ parameters:
87
+ step_text(string): the actual step string definition.
88
+ fix_inner_step(function): a function used to fix the inner_step
89
+ function code location and other
90
+ properties.
91
+ variable_passthru(bool): when set to true the variables are not
92
+ replaced in the arguments of the step
93
+ text and so variable references such as
94
+ {FOO} are passed as such so they can
95
+ be handled further down in `run_steps`
96
+ for example.
97
+ """
98
+
99
+ #
100
+ # IMPORTANT: if you add any additional line from the `wrapper`
101
+ # function name to the `inner_step` name then you HAVE to
102
+ # update the `src/cucu/helpers.py` fix_inner_step
103
+ # so it subtracts the lines between wrapper and
104
+ # inner_step functions (lines of code). More details
105
+ # at the `src/cucu/helpers.py` location.
106
+ #
107
+ def wrapper(func):
108
+ @decorator(step_text)
109
+ @wraps(func)
110
+ def inner_step(*args, **kwargs):
111
+ inner_step_func(
112
+ func,
113
+ *args,
114
+ variable_passthru=variable_passthru,
115
+ **kwargs,
116
+ )
117
+
118
+ fix_inner_step(inner_step)
119
+
120
+ return wrapper
121
+
122
+ behave.__dict__[decorator_name] = new_decorator
123
+
124
+
125
+ _stdout = sys.stdout
126
+
127
+
128
+ class CucuOutputStream:
129
+ """
130
+ encapsulates a lot of the logic to handle capturing step by step console
131
+ logging but also redirecting logging at runtime to another stream
132
+
133
+
134
+ FYI: in order to print to the screen you must use directly the sys.stdout object
135
+ and write to it here as doing something like `print(...)` will result
136
+ in an infinite recursive loop and break
137
+ """
138
+
139
+ def __init__(self, stream, other_stream=None):
140
+ self.captured_data = []
141
+ self.stream = stream
142
+ self.encoding = stream.encoding
143
+ self.other_stream = other_stream
144
+
145
+ def set_other_stream(self, other):
146
+ self.other_stream = other
147
+
148
+ def __getattr__(self, item):
149
+ return self.stream.__getattribute__(item)
150
+
151
+ def isatty(self, *args, **kwargs):
152
+ return self.stream.isatty(*args, **kwargs)
153
+
154
+ def fileno(self):
155
+ return self.stream.fileno()
156
+
157
+ def flush(self):
158
+ self.stream.flush()
159
+
160
+ if self.other_stream:
161
+ self.other_stream.flush()
162
+
163
+ def write(self, byte):
164
+ byte = CONFIG.hide_secrets(byte)
165
+
166
+ self.captured_data.append(byte)
167
+
168
+ if isinstance(byte, bytes):
169
+ self.stream.write(byte.decode("utf8"))
170
+
171
+ if self.other_stream:
172
+ self.other_stream.write(byte.decode("utf8"))
173
+ else:
174
+ self.stream.write(byte)
175
+
176
+ if self.other_stream:
177
+ self.other_stream.write(byte)
178
+
179
+ def writelines(self, lines):
180
+ lines = [CONFIG.hide_secrets(line) for line in lines]
181
+
182
+ for line in lines:
183
+ self.captured_data.append(line)
184
+
185
+ self.stream.writelines(lines)
186
+ if self.other_stream:
187
+ self.other_stream.writelines(lines)
188
+
189
+ def captured(self):
190
+ """
191
+ returns the data captured thus far to the stream and resets the internal
192
+ buffer to empty.
193
+ """
194
+ captured_data = self.captured_data
195
+ self.captured_data = []
196
+ return captured_data
File without changes
cucu/browser/core.py ADDED
@@ -0,0 +1,80 @@
1
+ #
2
+ # cucu's browser implementatoin is an abstraction to allow us to easily
3
+ # run the same tests against a browser being managed by selenium, cypress or
4
+ # any other future browser testing framework
5
+ #
6
+ import time
7
+
8
+ from cucu import logger
9
+ from cucu.config import CONFIG
10
+
11
+
12
+ class Browser:
13
+ def __init__(self):
14
+ pass
15
+
16
+ def open(self, *args, **kwargs):
17
+ raise RuntimeError("implement me")
18
+
19
+ def navigate(self, url):
20
+ raise RuntimeError("implement me")
21
+
22
+ def switch_to_next_tab(self):
23
+ raise RuntimeError("implement me")
24
+
25
+ def switch_to_previous_tab(self):
26
+ raise RuntimeError("implement me")
27
+
28
+ def back(self):
29
+ raise RuntimeError("implement me")
30
+
31
+ def refresh(self):
32
+ raise RuntimeError("implement me")
33
+
34
+ def get_current_url(self):
35
+ raise RuntimeError("implement me")
36
+
37
+ def title(self):
38
+ raise RuntimeError("implement me")
39
+
40
+ def execute(self, javascript, *args, **kwargs):
41
+ raise RuntimeError("implement me")
42
+
43
+ def click(self, element):
44
+ raise RuntimeError("implement me")
45
+
46
+ def switch_to_default_frame(self):
47
+ raise RuntimeError("implement me")
48
+
49
+ def switch_to_frame(self, frame):
50
+ raise RuntimeError("implement me")
51
+
52
+ def screenshot(self, filepath):
53
+ raise RuntimeError("implement me")
54
+
55
+ def close_window(self):
56
+ raise RuntimeError("implement me")
57
+
58
+ def quit(self):
59
+ raise RuntimeError("implement me")
60
+
61
+ # built in methods to be used by all browser implementations
62
+ def wait_for_page_to_load(self):
63
+ """
64
+ this method is to be used by all browser implementations and called
65
+ when after clicking something or refreshing the page and we'd like to
66
+ make sure the is "loaded" and ready to be interacted with.
67
+
68
+ this also gives us a place to fire off any additional page checks such
69
+ as a console log checker, broken image checker, etc.
70
+ """
71
+
72
+ # run the page checks
73
+ if CONFIG["__CUCU_PAGE_CHECK_HOOKS"]:
74
+ for name, hook in CONFIG["__CUCU_PAGE_CHECK_HOOKS"].items():
75
+ logger.debug(f'executing page check "{name}"')
76
+ start = time.time()
77
+ hook(self)
78
+ logger.debug(
79
+ f'executed page check "{name}" in {round(time.time()-start, 3)}s'
80
+ )
cucu/browser/frames.py ADDED
@@ -0,0 +1,106 @@
1
+ from cucu.browser.core import Browser
2
+
3
+
4
+ def search_in_all_frames(browser, search_function):
5
+ """
6
+ search all frames on the page for an element
7
+
8
+ Warning: This leaves the browser in whatever frame was last searched so that
9
+ users of this method are in that frame.
10
+
11
+ parameters:
12
+ browser - the cucu.browser.Browser object
13
+ search_function - function to search for the element (within a frame)
14
+ returns:
15
+ the WebElement that matches (if found)
16
+ """
17
+ # check whatever frame we're currently in
18
+ result = search_function()
19
+
20
+ if not result:
21
+ # we might have not been in the default frame so check again
22
+ browser.switch_to_default_frame()
23
+
24
+ result = search_function()
25
+ if result:
26
+ return result
27
+
28
+ frames = browser.execute('return document.querySelectorAll("iframe");')
29
+ for frame in frames:
30
+ # need to be in the default frame in order to switch to a child
31
+ # frame w/o getting a stale element exception
32
+ browser.switch_to_default_frame()
33
+ browser.switch_to_frame(frame)
34
+ result = search_function()
35
+
36
+ if result:
37
+ return result
38
+
39
+ return result
40
+
41
+
42
+ def run_in_all_frames(browser, search_function):
43
+ """
44
+ run the search function on all of the available frames and return the
45
+ appending of all the arrays.
46
+
47
+ Warning: This leaves the browser in whatever frame was last searched so that
48
+ users of this method are in that frame.
49
+
50
+ parameters:
51
+ browser - the cucu.browser.Browser object
52
+ search_function - function that returns an array of WebElements
53
+ returns:
54
+ the array of all of the WebElements found.
55
+ """
56
+ result = []
57
+
58
+ browser.switch_to_default_frame()
59
+ result += search_function()
60
+
61
+ frames = browser.execute('return document.querySelectorAll("iframe");')
62
+ for frame in frames:
63
+ # need to be in the default frame in order to switch to a child
64
+ # frame w/o getting a stale element exception
65
+ browser.switch_to_default_frame()
66
+ browser.switch_to_frame(frame)
67
+ result += search_function()
68
+
69
+ return result
70
+
71
+
72
+ def try_in_frames_until_success(browser: Browser, function_to_run) -> None:
73
+ """
74
+ Run the function on all of the possible frames one by one. It terminates
75
+ if the function doesn't raise an exception on a frame.
76
+
77
+ Warning: This leaves the browser in whatever frame the function is run successfully
78
+ so that users of the this method are in that frame.
79
+
80
+ Args:
81
+ browser (Browser): the browser session
82
+ function_to_run : a function that raises an exception if it fails
83
+
84
+ Raises:
85
+ RuntimeError: when the function fails in all frames
86
+ """
87
+ browser.switch_to_default_frame()
88
+ try:
89
+ function_to_run()
90
+ except Exception:
91
+ frames = browser.execute('return document.querySelectorAll("iframe");')
92
+ for frame in frames:
93
+ # need to be in the default frame in order to switch to a child
94
+ # frame w/o getting a stale element exception
95
+ browser.switch_to_default_frame()
96
+ browser.switch_to_frame(frame)
97
+ try:
98
+ function_to_run()
99
+ except Exception:
100
+ if frames.index(frame) < len(frames) - 1:
101
+ continue
102
+ else:
103
+ raise RuntimeError(
104
+ f"{function_to_run.__name__} failed in all frames"
105
+ )
106
+ return