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