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/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