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/core.py
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import glob
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import signal
|
|
7
|
+
import time
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
9
|
+
from importlib.metadata import version
|
|
10
|
+
from threading import Timer
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
import coverage
|
|
14
|
+
import psutil
|
|
15
|
+
from click import ClickException
|
|
16
|
+
from mpire import WorkerPool
|
|
17
|
+
from tabulate import tabulate
|
|
18
|
+
|
|
19
|
+
from cucu import (
|
|
20
|
+
fuzzy,
|
|
21
|
+
init_global_hook_variables,
|
|
22
|
+
language_server,
|
|
23
|
+
logger,
|
|
24
|
+
register_after_all_hook,
|
|
25
|
+
reporter,
|
|
26
|
+
)
|
|
27
|
+
from cucu.cli import thread_dumper
|
|
28
|
+
from cucu.cli.run import behave, behave_init, write_run_details
|
|
29
|
+
from cucu.cli.steps import print_human_readable_steps, print_json_steps
|
|
30
|
+
from cucu.config import CONFIG
|
|
31
|
+
from cucu.lint import linter
|
|
32
|
+
|
|
33
|
+
# will start coverage tracking once COVERAGE_PROCESS_START is set
|
|
34
|
+
coverage.process_startup()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@click.group()
|
|
38
|
+
@click.version_option(version("cucu"), message="%(version)s")
|
|
39
|
+
def main():
|
|
40
|
+
"""
|
|
41
|
+
cucu e2e testing framework
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@main.command()
|
|
47
|
+
@click.argument("filepath")
|
|
48
|
+
@click.option(
|
|
49
|
+
"-b",
|
|
50
|
+
"--browser",
|
|
51
|
+
default=os.environ.get("CUCU_BROWSER") or "chrome",
|
|
52
|
+
help="browser name to use default: chrome",
|
|
53
|
+
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"-c",
|
|
56
|
+
"--color-output/--no-color-output",
|
|
57
|
+
default=True,
|
|
58
|
+
help="produce output with colors or not",
|
|
59
|
+
)
|
|
60
|
+
@click.option(
|
|
61
|
+
"--dry-run/--no-dry-run",
|
|
62
|
+
default=False,
|
|
63
|
+
help="invokes output formatters without running the steps",
|
|
64
|
+
)
|
|
65
|
+
@click.option(
|
|
66
|
+
"-e",
|
|
67
|
+
"--env",
|
|
68
|
+
default=[],
|
|
69
|
+
multiple=True,
|
|
70
|
+
help="set environment variable which can be referenced with",
|
|
71
|
+
)
|
|
72
|
+
@click.option(
|
|
73
|
+
"-g",
|
|
74
|
+
"--generate-report/--no-generate-report",
|
|
75
|
+
default=False,
|
|
76
|
+
help="automatically generate a report at the end of the test run",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"-x",
|
|
80
|
+
"--fail-fast/--no-fail-fast",
|
|
81
|
+
default=False,
|
|
82
|
+
help="stop running tests on the first failure",
|
|
83
|
+
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"-h",
|
|
86
|
+
"--headless/--no-headless",
|
|
87
|
+
default=True,
|
|
88
|
+
help="controls if the browser is run in headless mode",
|
|
89
|
+
)
|
|
90
|
+
@click.option("-n", "--name", help="used to specify the exact scenario to run")
|
|
91
|
+
@click.option(
|
|
92
|
+
"-i",
|
|
93
|
+
"--ipdb-on-failure/--no-ipdb-on-failure",
|
|
94
|
+
default=False,
|
|
95
|
+
help="on failure drop into the ipdb debug shell",
|
|
96
|
+
)
|
|
97
|
+
@click.option(
|
|
98
|
+
"-j",
|
|
99
|
+
"--junit",
|
|
100
|
+
default=None,
|
|
101
|
+
help="specify the output directory for JUnit XML files, default is "
|
|
102
|
+
"the same location as --results",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--junit-with-stacktrace",
|
|
106
|
+
is_flag=True,
|
|
107
|
+
default=False,
|
|
108
|
+
help="when set to true the JUnit XML output will contain the stacktrace",
|
|
109
|
+
)
|
|
110
|
+
@click.option(
|
|
111
|
+
"-l",
|
|
112
|
+
"--logging-level",
|
|
113
|
+
default="INFO",
|
|
114
|
+
help="set logging level to one of debug, warn or info (default)",
|
|
115
|
+
)
|
|
116
|
+
@click.option(
|
|
117
|
+
"--show-skips",
|
|
118
|
+
default=False,
|
|
119
|
+
is_flag=True,
|
|
120
|
+
help="when set skips are shown",
|
|
121
|
+
)
|
|
122
|
+
@click.option(
|
|
123
|
+
"--show-status",
|
|
124
|
+
default=False,
|
|
125
|
+
is_flag=True,
|
|
126
|
+
help="when set status output is shown (helpful for CI that wants stdout updates)",
|
|
127
|
+
)
|
|
128
|
+
@click.option(
|
|
129
|
+
"--periodic-thread-dumper",
|
|
130
|
+
default=None,
|
|
131
|
+
help="sets the interval in minutes of when to run the periodic thread dumper",
|
|
132
|
+
)
|
|
133
|
+
@click.option(
|
|
134
|
+
"-p",
|
|
135
|
+
"--preserve-results/--no-preserve-results",
|
|
136
|
+
default=False,
|
|
137
|
+
help="when set we will not remove any existing results directory",
|
|
138
|
+
)
|
|
139
|
+
@click.option(
|
|
140
|
+
"--record-env-vars",
|
|
141
|
+
default=False,
|
|
142
|
+
is_flag=True,
|
|
143
|
+
help="when set will record shell environment variables to debug file: run_details.json",
|
|
144
|
+
)
|
|
145
|
+
@click.option(
|
|
146
|
+
"--report",
|
|
147
|
+
default="report",
|
|
148
|
+
help="the location to put the test report when --generate-report is used",
|
|
149
|
+
)
|
|
150
|
+
@click.option(
|
|
151
|
+
"--report-only-failures",
|
|
152
|
+
default=False,
|
|
153
|
+
is_flag=True,
|
|
154
|
+
help="when set the HTML test report will only contain the failed test results",
|
|
155
|
+
)
|
|
156
|
+
@click.option(
|
|
157
|
+
"-r",
|
|
158
|
+
"--results",
|
|
159
|
+
default="results",
|
|
160
|
+
help="the results directory used by cucu",
|
|
161
|
+
)
|
|
162
|
+
@click.option(
|
|
163
|
+
"--runtime-timeout",
|
|
164
|
+
default=None,
|
|
165
|
+
type=int,
|
|
166
|
+
help="the runtime timeout in seconds after which the current run will terminate any running tests and exit",
|
|
167
|
+
)
|
|
168
|
+
@click.option(
|
|
169
|
+
"--feature-timeout",
|
|
170
|
+
default=1800,
|
|
171
|
+
help="When run tests in parallel, the maximum amount of time (seconds) a feature can run",
|
|
172
|
+
)
|
|
173
|
+
@click.option(
|
|
174
|
+
"--secrets",
|
|
175
|
+
default=None,
|
|
176
|
+
help="coma separated list of variable names that we should hide"
|
|
177
|
+
" their value all of the output produced by cucu",
|
|
178
|
+
)
|
|
179
|
+
@click.option(
|
|
180
|
+
"-t",
|
|
181
|
+
"--tags",
|
|
182
|
+
default=[],
|
|
183
|
+
multiple=True,
|
|
184
|
+
help="Only execute features or scenarios with tags matching "
|
|
185
|
+
"expression provided. example: --tags @dev, --tags ~@dev",
|
|
186
|
+
)
|
|
187
|
+
@click.option(
|
|
188
|
+
"-w",
|
|
189
|
+
"--workers",
|
|
190
|
+
default=None,
|
|
191
|
+
help="Specifies the number of workers to use to run tests in parallel",
|
|
192
|
+
)
|
|
193
|
+
@click.option(
|
|
194
|
+
"--verbose/--no-verbose",
|
|
195
|
+
default=False,
|
|
196
|
+
help="runs with verbose logging and shows additional stacktrace",
|
|
197
|
+
)
|
|
198
|
+
@click.option(
|
|
199
|
+
"-s",
|
|
200
|
+
"--selenium-remote-url",
|
|
201
|
+
default=None,
|
|
202
|
+
help="the HTTP url for a selenium hub setup to run the browser tests on",
|
|
203
|
+
)
|
|
204
|
+
def run(
|
|
205
|
+
filepath,
|
|
206
|
+
browser,
|
|
207
|
+
color_output,
|
|
208
|
+
dry_run,
|
|
209
|
+
env,
|
|
210
|
+
generate_report,
|
|
211
|
+
fail_fast,
|
|
212
|
+
headless,
|
|
213
|
+
name,
|
|
214
|
+
ipdb_on_failure,
|
|
215
|
+
junit,
|
|
216
|
+
junit_with_stacktrace,
|
|
217
|
+
logging_level,
|
|
218
|
+
periodic_thread_dumper,
|
|
219
|
+
preserve_results,
|
|
220
|
+
record_env_vars,
|
|
221
|
+
report,
|
|
222
|
+
report_only_failures,
|
|
223
|
+
results,
|
|
224
|
+
runtime_timeout,
|
|
225
|
+
feature_timeout,
|
|
226
|
+
secrets,
|
|
227
|
+
show_skips,
|
|
228
|
+
show_status,
|
|
229
|
+
tags,
|
|
230
|
+
selenium_remote_url,
|
|
231
|
+
workers,
|
|
232
|
+
verbose,
|
|
233
|
+
):
|
|
234
|
+
"""
|
|
235
|
+
run a set of feature files
|
|
236
|
+
"""
|
|
237
|
+
init_global_hook_variables()
|
|
238
|
+
dumper = None
|
|
239
|
+
|
|
240
|
+
if os.environ.get("CUCU") == "true":
|
|
241
|
+
# when cucu is already running it means that we're running inside
|
|
242
|
+
# another cucu process and therefore we should make sure the results
|
|
243
|
+
# directory isn't the default one and throw an exception otherwise
|
|
244
|
+
if results == "results":
|
|
245
|
+
raise Exception(
|
|
246
|
+
"running within cucu but --results was not used, "
|
|
247
|
+
"this would lead to some very difficult to debug "
|
|
248
|
+
"failures as this process would clobber the "
|
|
249
|
+
"parent results directory"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# set for testing cucu itself but basically allows you to know when cucu
|
|
253
|
+
# is running itself as part of internal testing
|
|
254
|
+
os.environ["CUCU"] = "true"
|
|
255
|
+
|
|
256
|
+
os.environ["CUCU_LOGGING_LEVEL"] = logging_level.upper()
|
|
257
|
+
logger.init_logging(logging_level.upper())
|
|
258
|
+
|
|
259
|
+
if not dry_run:
|
|
260
|
+
if not preserve_results:
|
|
261
|
+
if os.path.exists(results):
|
|
262
|
+
shutil.rmtree(results)
|
|
263
|
+
|
|
264
|
+
os.makedirs(results, exist_ok=True)
|
|
265
|
+
|
|
266
|
+
if selenium_remote_url is not None:
|
|
267
|
+
os.environ["CUCU_SELENIUM_REMOTE_URL"] = selenium_remote_url
|
|
268
|
+
|
|
269
|
+
if periodic_thread_dumper is not None:
|
|
270
|
+
interval_min = float(periodic_thread_dumper)
|
|
271
|
+
dumper = thread_dumper.start(interval_min)
|
|
272
|
+
|
|
273
|
+
# need to set this before initializing any browsers below
|
|
274
|
+
os.environ["CUCU_BROWSER"] = browser.lower()
|
|
275
|
+
|
|
276
|
+
if junit is None:
|
|
277
|
+
junit = results
|
|
278
|
+
|
|
279
|
+
if show_skips:
|
|
280
|
+
os.environ["CUCU_SHOW_SKIPS"] = "true"
|
|
281
|
+
|
|
282
|
+
if show_status:
|
|
283
|
+
os.environ["CUCU_SHOW_STATUS"] = "true"
|
|
284
|
+
|
|
285
|
+
if junit_with_stacktrace:
|
|
286
|
+
os.environ["CUCU_JUNIT_WITH_STACKTRACE"] = "true"
|
|
287
|
+
|
|
288
|
+
if report_only_failures:
|
|
289
|
+
os.environ["CUCU_REPORT_ONLY_FAILURES"] = "true"
|
|
290
|
+
|
|
291
|
+
if record_env_vars:
|
|
292
|
+
os.environ["CUCU_RECORD_ENV_VARS"] = "true"
|
|
293
|
+
|
|
294
|
+
if not dry_run:
|
|
295
|
+
write_run_details(results, filepath)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
if workers is None or workers == 1:
|
|
299
|
+
if runtime_timeout:
|
|
300
|
+
logger.debug("setting up runtime timeout timer")
|
|
301
|
+
|
|
302
|
+
def runtime_exit():
|
|
303
|
+
logger.error("runtime timeout reached, aborting run")
|
|
304
|
+
CONFIG["__CUCU_CTX"]._runner.aborted = True
|
|
305
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
306
|
+
|
|
307
|
+
timer = Timer(runtime_timeout, runtime_exit)
|
|
308
|
+
timer.start()
|
|
309
|
+
|
|
310
|
+
def cancel_timer(_):
|
|
311
|
+
logger.debug("cancelled runtime timeout timer")
|
|
312
|
+
timer.cancel()
|
|
313
|
+
|
|
314
|
+
register_after_all_hook(cancel_timer)
|
|
315
|
+
|
|
316
|
+
exit_code = behave(
|
|
317
|
+
filepath,
|
|
318
|
+
color_output,
|
|
319
|
+
dry_run,
|
|
320
|
+
env,
|
|
321
|
+
fail_fast,
|
|
322
|
+
headless,
|
|
323
|
+
name,
|
|
324
|
+
ipdb_on_failure,
|
|
325
|
+
junit,
|
|
326
|
+
results,
|
|
327
|
+
secrets,
|
|
328
|
+
show_skips,
|
|
329
|
+
tags,
|
|
330
|
+
verbose,
|
|
331
|
+
skip_init_global_hook_variables=True,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if exit_code != 0:
|
|
335
|
+
raise ClickException("test run failed, see above for details")
|
|
336
|
+
|
|
337
|
+
else:
|
|
338
|
+
if os.path.isdir(filepath):
|
|
339
|
+
basepath = os.path.join(filepath, "**/*.feature")
|
|
340
|
+
feature_filepaths = list(glob.iglob(basepath, recursive=True))
|
|
341
|
+
|
|
342
|
+
else:
|
|
343
|
+
feature_filepaths = [filepath]
|
|
344
|
+
|
|
345
|
+
with WorkerPool(n_jobs=int(workers), start_method="spawn") as pool:
|
|
346
|
+
# Each feature file is applied to the pool as an async task.
|
|
347
|
+
# It then polls the async result of each task. It the result
|
|
348
|
+
# is ready, it removes the result from the list of results that
|
|
349
|
+
# need to be checked again until all the results are checked.
|
|
350
|
+
# If the timer is triggered, it stops the while loop and
|
|
351
|
+
# logs all the unfinished features.
|
|
352
|
+
# The pool is terminated automatically when it exits the
|
|
353
|
+
# context.
|
|
354
|
+
timer = None
|
|
355
|
+
timeout_reached = False
|
|
356
|
+
if runtime_timeout:
|
|
357
|
+
logger.debug("setting up runtime timeout timer")
|
|
358
|
+
|
|
359
|
+
def runtime_exit():
|
|
360
|
+
nonlocal timeout_reached
|
|
361
|
+
logger.error("runtime timeout reached, aborting run")
|
|
362
|
+
timeout_reached = True
|
|
363
|
+
|
|
364
|
+
timer = Timer(runtime_timeout, runtime_exit)
|
|
365
|
+
timer.start()
|
|
366
|
+
|
|
367
|
+
async_results = {}
|
|
368
|
+
for feature_filepath in feature_filepaths:
|
|
369
|
+
async_results[feature_filepath] = pool.apply_async(
|
|
370
|
+
behave,
|
|
371
|
+
[
|
|
372
|
+
feature_filepath,
|
|
373
|
+
color_output,
|
|
374
|
+
dry_run,
|
|
375
|
+
env,
|
|
376
|
+
fail_fast,
|
|
377
|
+
headless,
|
|
378
|
+
name,
|
|
379
|
+
ipdb_on_failure,
|
|
380
|
+
junit,
|
|
381
|
+
results,
|
|
382
|
+
secrets,
|
|
383
|
+
show_skips,
|
|
384
|
+
tags,
|
|
385
|
+
verbose,
|
|
386
|
+
],
|
|
387
|
+
{
|
|
388
|
+
"redirect_output": True,
|
|
389
|
+
},
|
|
390
|
+
task_timeout=float(feature_timeout),
|
|
391
|
+
)
|
|
392
|
+
logger.info(f"scheduled feature file {feature_filepath}")
|
|
393
|
+
|
|
394
|
+
# poll while we have running tasks until the overall time limit
|
|
395
|
+
task_failed = {}
|
|
396
|
+
while not timeout_reached:
|
|
397
|
+
remaining = {}
|
|
398
|
+
for feature, result in async_results.items():
|
|
399
|
+
if timeout_reached:
|
|
400
|
+
break
|
|
401
|
+
|
|
402
|
+
if result.ready():
|
|
403
|
+
try:
|
|
404
|
+
# wait 0.1s max for interprocess communication
|
|
405
|
+
exit_code = result.get(0.1)
|
|
406
|
+
if exit_code != 0:
|
|
407
|
+
task_failed[feature] = result
|
|
408
|
+
except TimeoutError as err:
|
|
409
|
+
if f"timeout={feature_timeout}" in str(err):
|
|
410
|
+
print(f"{err}")
|
|
411
|
+
task_failed[feature] = result
|
|
412
|
+
# ignore timeout errors from interprocess communication slowness
|
|
413
|
+
except Exception:
|
|
414
|
+
logger.exception(
|
|
415
|
+
f"an exception is raised during feature {feature}"
|
|
416
|
+
)
|
|
417
|
+
task_failed[feature] = result
|
|
418
|
+
else:
|
|
419
|
+
remaining[feature] = result
|
|
420
|
+
|
|
421
|
+
async_results = remaining
|
|
422
|
+
|
|
423
|
+
if len(remaining) == 0:
|
|
424
|
+
if timer:
|
|
425
|
+
# we're done so cancel any outstanding overall time limit
|
|
426
|
+
timer.cancel()
|
|
427
|
+
break
|
|
428
|
+
|
|
429
|
+
time.sleep(1)
|
|
430
|
+
|
|
431
|
+
if timeout_reached:
|
|
432
|
+
logger.warn("Timeout reached, send kill signal to workers")
|
|
433
|
+
for worker in pool._workers:
|
|
434
|
+
try:
|
|
435
|
+
worker_proc = psutil.Process(worker.pid)
|
|
436
|
+
for child in worker_proc.children():
|
|
437
|
+
child.kill()
|
|
438
|
+
|
|
439
|
+
worker_proc.kill()
|
|
440
|
+
except psutil.NoSuchProcess:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
task_failed.update(async_results)
|
|
444
|
+
|
|
445
|
+
if task_failed:
|
|
446
|
+
failing_features = "\n".join(task_failed.keys())
|
|
447
|
+
logger.error(f"Failing Features:\n{failing_features}")
|
|
448
|
+
raise RuntimeError(
|
|
449
|
+
"there are failures, see above for details"
|
|
450
|
+
)
|
|
451
|
+
finally:
|
|
452
|
+
if dumper is not None:
|
|
453
|
+
dumper.stop()
|
|
454
|
+
|
|
455
|
+
if generate_report:
|
|
456
|
+
_generate_report(
|
|
457
|
+
results,
|
|
458
|
+
report,
|
|
459
|
+
only_failures=report_only_failures,
|
|
460
|
+
junit=junit,
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _generate_report(
|
|
465
|
+
filepath: str, output: str, only_failures: False, junit: str | None = None
|
|
466
|
+
):
|
|
467
|
+
"""
|
|
468
|
+
helper method to handle report generation so it can be used by the `cucu report`
|
|
469
|
+
command also the `cucu run` when told to generate a report. If junit is provided, it adds report
|
|
470
|
+
path to the JUnit files.
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
parameters:
|
|
474
|
+
filepath(string): the results directory containing the previous test run
|
|
475
|
+
output(string): the directory where we'll generate the report
|
|
476
|
+
only_failures(bool, optional): if only report failures. The default is False.
|
|
477
|
+
junit(str|None, optional): the directory of the JUnit files. The default if None.
|
|
478
|
+
"""
|
|
479
|
+
if os.path.exists(output):
|
|
480
|
+
shutil.rmtree(output)
|
|
481
|
+
|
|
482
|
+
os.makedirs(output)
|
|
483
|
+
|
|
484
|
+
report_location = reporter.generate(
|
|
485
|
+
filepath, output, only_failures=only_failures
|
|
486
|
+
)
|
|
487
|
+
print(f"HTML test report at {report_location}")
|
|
488
|
+
|
|
489
|
+
if junit:
|
|
490
|
+
_add_report_path_in_junit(junit, output)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _add_report_path_in_junit(junit_folder, report_folder):
|
|
494
|
+
for junit_file in glob.glob(f"{junit_folder}/*.xml", recursive=True):
|
|
495
|
+
junit = ET.parse(junit_file)
|
|
496
|
+
test_suite = junit.getroot()
|
|
497
|
+
ts_folder = test_suite.get("foldername")
|
|
498
|
+
for test_case in test_suite.iter("testcase"):
|
|
499
|
+
report_path = os.path.join(
|
|
500
|
+
report_folder,
|
|
501
|
+
ts_folder,
|
|
502
|
+
test_case.get("foldername"),
|
|
503
|
+
"index.html",
|
|
504
|
+
)
|
|
505
|
+
test_case.set("report_path", report_path)
|
|
506
|
+
junit.write(junit_file, encoding="utf-8", xml_declaration=False)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@main.command()
|
|
510
|
+
@click.argument("filepath", default="results")
|
|
511
|
+
@click.option(
|
|
512
|
+
"--only-failures",
|
|
513
|
+
default=False,
|
|
514
|
+
is_flag=True,
|
|
515
|
+
help="when set the HTML test report will only contain the failed test results",
|
|
516
|
+
)
|
|
517
|
+
@click.option(
|
|
518
|
+
"-l",
|
|
519
|
+
"--logging-level",
|
|
520
|
+
default="INFO",
|
|
521
|
+
help="set logging level to one of debug, warn or info (default)",
|
|
522
|
+
)
|
|
523
|
+
@click.option(
|
|
524
|
+
"--show-skips",
|
|
525
|
+
default=False,
|
|
526
|
+
is_flag=True,
|
|
527
|
+
help="when set skips are shown",
|
|
528
|
+
)
|
|
529
|
+
@click.option(
|
|
530
|
+
"--show-status",
|
|
531
|
+
default=False,
|
|
532
|
+
is_flag=True,
|
|
533
|
+
help="when set status output is shown (helpful for CI that wants stdout updates)",
|
|
534
|
+
)
|
|
535
|
+
@click.option("-o", "--output", default="report")
|
|
536
|
+
@click.option(
|
|
537
|
+
"-j",
|
|
538
|
+
"--junit",
|
|
539
|
+
default=None,
|
|
540
|
+
help="specify the output directory for JUnit XML files, default is "
|
|
541
|
+
"the same location as --results",
|
|
542
|
+
)
|
|
543
|
+
def report(
|
|
544
|
+
filepath,
|
|
545
|
+
only_failures,
|
|
546
|
+
logging_level,
|
|
547
|
+
show_skips,
|
|
548
|
+
show_status,
|
|
549
|
+
output,
|
|
550
|
+
junit,
|
|
551
|
+
):
|
|
552
|
+
"""
|
|
553
|
+
generate a test report from a results directory
|
|
554
|
+
"""
|
|
555
|
+
init_global_hook_variables()
|
|
556
|
+
|
|
557
|
+
os.environ["CUCU_LOGGING_LEVEL"] = logging_level.upper()
|
|
558
|
+
logger.init_logging(logging_level.upper())
|
|
559
|
+
|
|
560
|
+
if show_skips:
|
|
561
|
+
os.environ["CUCU_SHOW_SKIPS"] = "true"
|
|
562
|
+
|
|
563
|
+
if show_status:
|
|
564
|
+
os.environ["CUCU_SHOW_STATUS"] = "true"
|
|
565
|
+
|
|
566
|
+
run_details_filepath = os.path.join(filepath, "run_details.json")
|
|
567
|
+
|
|
568
|
+
if os.path.exists(run_details_filepath):
|
|
569
|
+
# load the run details at the time of execution for the provided results
|
|
570
|
+
# directory
|
|
571
|
+
run_details = {}
|
|
572
|
+
|
|
573
|
+
with open(run_details_filepath, encoding="utf8") as _input:
|
|
574
|
+
run_details = json.loads(_input.read())
|
|
575
|
+
|
|
576
|
+
# initialize any underlying custom step code things
|
|
577
|
+
behave_init(run_details["filepath"])
|
|
578
|
+
|
|
579
|
+
_generate_report(
|
|
580
|
+
filepath, output, only_failures=only_failures, junit=junit
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
@main.command()
|
|
585
|
+
@click.argument("filepath", default="features")
|
|
586
|
+
@click.option(
|
|
587
|
+
"-f",
|
|
588
|
+
"--format",
|
|
589
|
+
default="human",
|
|
590
|
+
help="output format to use, available: human, json."
|
|
591
|
+
"default: human. PRO TIP: `brew install fzf` and then "
|
|
592
|
+
"`cucu steps | fzf` and easily find the step you need.",
|
|
593
|
+
)
|
|
594
|
+
def steps(filepath, format):
|
|
595
|
+
"""
|
|
596
|
+
print available cucu steps
|
|
597
|
+
"""
|
|
598
|
+
init_global_hook_variables()
|
|
599
|
+
|
|
600
|
+
if format == "human":
|
|
601
|
+
print_human_readable_steps(filepath=filepath)
|
|
602
|
+
|
|
603
|
+
elif format == "json":
|
|
604
|
+
print_json_steps(filepath=filepath)
|
|
605
|
+
|
|
606
|
+
else:
|
|
607
|
+
raise RuntimeError(f'unsupported format "{format}"')
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@main.command()
|
|
611
|
+
@click.argument("filepath", nargs=-1)
|
|
612
|
+
@click.option(
|
|
613
|
+
"--fix/--no-fix", default=False, help="fix lint violations, default: False"
|
|
614
|
+
)
|
|
615
|
+
@click.option(
|
|
616
|
+
"-l",
|
|
617
|
+
"--logging-level",
|
|
618
|
+
default="INFO",
|
|
619
|
+
help="set logging level to one of debug, warn or info (default)",
|
|
620
|
+
)
|
|
621
|
+
def lint(filepath, fix, logging_level):
|
|
622
|
+
"""
|
|
623
|
+
lint feature files
|
|
624
|
+
"""
|
|
625
|
+
os.environ["CUCU_LOGGING_LEVEL"] = logging_level.upper()
|
|
626
|
+
logger.init_logging(logging_level.upper())
|
|
627
|
+
|
|
628
|
+
init_global_hook_variables()
|
|
629
|
+
|
|
630
|
+
logger.init_logging("INFO")
|
|
631
|
+
filepaths = list(filepath)
|
|
632
|
+
|
|
633
|
+
if filepaths == []:
|
|
634
|
+
filepaths = ["features"]
|
|
635
|
+
|
|
636
|
+
violations_found = 0
|
|
637
|
+
violations_fixed = 0
|
|
638
|
+
|
|
639
|
+
for filepath in filepaths:
|
|
640
|
+
# initialize any underlying custom step code things
|
|
641
|
+
behave_init(filepath)
|
|
642
|
+
|
|
643
|
+
all_violations = linter.lint(filepath)
|
|
644
|
+
|
|
645
|
+
for violations in all_violations:
|
|
646
|
+
if fix:
|
|
647
|
+
violations = linter.fix(violations)
|
|
648
|
+
|
|
649
|
+
if violations:
|
|
650
|
+
for violation in violations:
|
|
651
|
+
violations_found += 1
|
|
652
|
+
|
|
653
|
+
if violation["type"] == "steps_error":
|
|
654
|
+
print(violation["message"])
|
|
655
|
+
print(
|
|
656
|
+
"failure loading some steps, see above for details"
|
|
657
|
+
)
|
|
658
|
+
print("")
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
location = violation["location"]
|
|
662
|
+
_type = violation["type"][0].upper()
|
|
663
|
+
message = violation["message"]
|
|
664
|
+
suffix = ""
|
|
665
|
+
|
|
666
|
+
if fix:
|
|
667
|
+
if violation["fixed"]:
|
|
668
|
+
suffix = " ✓"
|
|
669
|
+
violations_fixed += 1
|
|
670
|
+
else:
|
|
671
|
+
suffix = " ✗ (must be fixed manually)"
|
|
672
|
+
|
|
673
|
+
filepath = location["filepath"]
|
|
674
|
+
line_number = location["line"] + 1
|
|
675
|
+
print(
|
|
676
|
+
f"{filepath}:{line_number}: {_type} {message}{suffix}"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
if violations_found != 0:
|
|
680
|
+
if violations_found == violations_fixed:
|
|
681
|
+
print("\nlinting errors found and fixed, see above for details")
|
|
682
|
+
|
|
683
|
+
else:
|
|
684
|
+
raise ClickException(
|
|
685
|
+
"linting errors found, but not fixed, see above for details"
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@main.command()
|
|
690
|
+
@click.option(
|
|
691
|
+
"-l",
|
|
692
|
+
"--logging-level",
|
|
693
|
+
default="INFO",
|
|
694
|
+
help="set logging level to one of debug, warn or info (default)",
|
|
695
|
+
)
|
|
696
|
+
@click.option(
|
|
697
|
+
"-p",
|
|
698
|
+
"--port",
|
|
699
|
+
default=None,
|
|
700
|
+
help="when the port is set the lsp will run in TCP mode and not STDIO mode",
|
|
701
|
+
)
|
|
702
|
+
def lsp(logging_level, port):
|
|
703
|
+
"""
|
|
704
|
+
start the cucu language server
|
|
705
|
+
"""
|
|
706
|
+
os.environ["CUCU_LOGGING_LEVEL"] = logging_level.upper()
|
|
707
|
+
logger.init_logging(logging_level.upper())
|
|
708
|
+
|
|
709
|
+
language_server.start(port=port)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
@main.command()
|
|
713
|
+
@click.argument("filepath", default="features")
|
|
714
|
+
def vars(filepath):
|
|
715
|
+
"""
|
|
716
|
+
print built-in cucu variables
|
|
717
|
+
"""
|
|
718
|
+
init_global_hook_variables()
|
|
719
|
+
|
|
720
|
+
# loading the steps make it so the code that registers config variables
|
|
721
|
+
# elsewhere get to execute
|
|
722
|
+
behave_init(filepath)
|
|
723
|
+
|
|
724
|
+
variables = []
|
|
725
|
+
variables.append(["Name", "Description", "Default"])
|
|
726
|
+
|
|
727
|
+
variables.extend(
|
|
728
|
+
[
|
|
729
|
+
[name, definition["description"], definition["default"]]
|
|
730
|
+
for name, definition in CONFIG.defined_variables.items()
|
|
731
|
+
]
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
print(tabulate(variables, tablefmt="fancy_grid"))
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
@main.command()
|
|
738
|
+
@click.option(
|
|
739
|
+
"-b",
|
|
740
|
+
"--browser",
|
|
741
|
+
default="chrome",
|
|
742
|
+
help="when specified the browser will be opened with the fuzzy "
|
|
743
|
+
"js library preloaded.",
|
|
744
|
+
)
|
|
745
|
+
@click.option(
|
|
746
|
+
"-u",
|
|
747
|
+
"--url",
|
|
748
|
+
default="https://www.google.com",
|
|
749
|
+
help="URL to open the browser at for debugging",
|
|
750
|
+
)
|
|
751
|
+
@click.option(
|
|
752
|
+
"--detach",
|
|
753
|
+
default=False,
|
|
754
|
+
help="when set to detach the browser will continue to run and "
|
|
755
|
+
"the cucu process will exit",
|
|
756
|
+
)
|
|
757
|
+
@click.option(
|
|
758
|
+
"-l",
|
|
759
|
+
"--logging-level",
|
|
760
|
+
default="INFO",
|
|
761
|
+
help="set logging level to one of debug, warn or info (default)",
|
|
762
|
+
)
|
|
763
|
+
def debug(browser, url, detach, logging_level):
|
|
764
|
+
"""
|
|
765
|
+
debug cucu library
|
|
766
|
+
"""
|
|
767
|
+
os.environ["CUCU_LOGGING_LEVEL"] = logging_level.upper()
|
|
768
|
+
logger.init_logging(logging_level.upper())
|
|
769
|
+
|
|
770
|
+
fuzzy_js = fuzzy.load_jquery_lib() + fuzzy.load_fuzzy_lib()
|
|
771
|
+
# XXX: need to make this more generic once we make the underlying
|
|
772
|
+
# browser framework swappable.
|
|
773
|
+
from cucu.browser.selenium import Selenium
|
|
774
|
+
|
|
775
|
+
selenium = Selenium()
|
|
776
|
+
selenium.open(browser, detach=detach)
|
|
777
|
+
selenium.navigate(url)
|
|
778
|
+
selenium.execute(fuzzy_js)
|
|
779
|
+
|
|
780
|
+
if not detach:
|
|
781
|
+
while True:
|
|
782
|
+
# detect when there are changes to the cucu javascript library
|
|
783
|
+
# and reload it in the currently running browser.
|
|
784
|
+
time.sleep(5)
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
if __name__ == "__main__":
|
|
788
|
+
main()
|