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/formatter/cucu.py ADDED
@@ -0,0 +1,261 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import absolute_import
3
+
4
+ import traceback
5
+
6
+ from behave.formatter.ansi_escapes import colors, escapes, up
7
+ from behave.formatter.base import Formatter
8
+ from behave.model_core import Status
9
+ from behave.model_describe import ModelPrinter
10
+ from behave.textutil import make_indentation
11
+
12
+ from cucu.config import CONFIG
13
+
14
+
15
+ class CucuFormatter(Formatter):
16
+ """ """
17
+
18
+ name = "cucu"
19
+ description = "cucu specific formatter to make the logs more readable"
20
+
21
+ DEFAULT_INDENT_SIZE = 2
22
+ SUBSTEP_PREFIX = " ⤷"
23
+
24
+ def __init__(self, stream_opener, config, **kwargs):
25
+ super(CucuFormatter, self).__init__(stream_opener, config)
26
+ self.current_scenario = None
27
+ self.steps = []
28
+ self.match_step_index = 0
29
+ self.show_timings = config.show_timings
30
+ self.indent_size = self.DEFAULT_INDENT_SIZE
31
+ # -- ENSURE: Output stream is open.
32
+ self.stream = self.open()
33
+ self.printer = ModelPrinter(self.stream)
34
+ # -- LAZY-EVALUATE:
35
+ self._multiline_indentation = None
36
+
37
+ color_output = CONFIG["CUCU_COLOR_OUTPUT"]
38
+ self.monochrome = color_output != "true"
39
+
40
+ @property
41
+ def multiline_indentation(self):
42
+ if self._multiline_indentation is None:
43
+ # align keywords
44
+ offset = 2
45
+ indentation = make_indentation(3 * self.indent_size + offset)
46
+ self._multiline_indentation = indentation
47
+
48
+ return self._multiline_indentation
49
+
50
+ def write_tags(self, tags, indent=None, for_feature=False):
51
+ """
52
+ writes the tags for a scenario or feature and takes extra care to be
53
+ sure to indent corectly for scenarios when `indent` is specified. We
54
+ also handle putting new lines in the right locations if its a scenario
55
+ or feature based on `for_feature` boolean flag.
56
+ """
57
+ indent = indent or ""
58
+ if tags:
59
+ text = " @".join(tags)
60
+ line = self.colorize(f"{indent}@{text}", "cyan")
61
+ if for_feature:
62
+ self.stream.write(f"{line}\n")
63
+ else:
64
+ self.stream.write(f"\n{line}")
65
+
66
+ # -- IMPLEMENT-INTERFACE FOR: Formatter
67
+ def feature(self, feature):
68
+ self.write_tags(feature.tags, for_feature=True)
69
+ text = f'{self.colorize(feature.keyword, "magenta")}: {feature.name}\n'
70
+ self.stream.write(text)
71
+
72
+ def colorize(self, text, color):
73
+ if self.monochrome:
74
+ return text
75
+ else:
76
+ return colors[color] + text + escapes["reset"]
77
+
78
+ def background(self, background):
79
+ if not self.monochrome:
80
+ indent = make_indentation(self.indent_size)
81
+ keyword = self.colorize(background.keyword, "magenta")
82
+ text = f"\n{indent}{keyword}: {background.name}\n"
83
+ self.stream.write(text)
84
+
85
+ def scenario(self, scenario):
86
+ self.current_scenario = scenario
87
+
88
+ indent = make_indentation(self.indent_size)
89
+ text = "\n%s%s: %s\n" % (
90
+ indent,
91
+ self.colorize(scenario.keyword, "magenta"),
92
+ scenario.name,
93
+ )
94
+ self.write_tags(scenario.tags, indent)
95
+ self.stream.write(text)
96
+ self.steps = []
97
+ self.match_step_index = 0
98
+
99
+ def step(self, step):
100
+ self.insert_step(step)
101
+
102
+ def insert_step(self, step, index=-1):
103
+ # used to determine how to better handle console output
104
+ step.has_substeps = False
105
+ step.is_substep = False
106
+
107
+ if index == -1:
108
+ self.steps.append(step)
109
+ else:
110
+ self.steps.insert(index, step)
111
+
112
+ def match(self, match):
113
+ if self.monochrome:
114
+ return
115
+
116
+ # we'll write a step line in grey and not hit the carriage return
117
+ step = self.steps[self.match_step_index]
118
+ self.match_step_index += 1
119
+
120
+ indent = make_indentation(2 * self.indent_size)
121
+ keyword = step.keyword.rjust(5)
122
+
123
+ prefix = ""
124
+ if step.is_substep:
125
+ prefix = self.SUBSTEP_PREFIX
126
+
127
+ text = self.colorize(
128
+ f"{indent}{prefix}{keyword} {step.name}\n", "grey"
129
+ )
130
+ self.stream.write(text)
131
+ self.stream.flush()
132
+
133
+ def calculate_max_line_length(self):
134
+ line_lengths = [
135
+ len(f"{step.keyword.rjust(5)} {step.name}") for step in self.steps
136
+ ]
137
+
138
+ return max(line_lengths) + 4
139
+
140
+ def result(self, step):
141
+ indent = make_indentation(2 * self.indent_size)
142
+ keyword = step.keyword.rjust(5)
143
+
144
+ if not (
145
+ self.monochrome
146
+ or step.has_substeps
147
+ or CONFIG["__CUCU_WROTE_TO_OUTPUT"]
148
+ ):
149
+ self.stream.write(up(1))
150
+
151
+ prefix = ""
152
+ if step.is_substep:
153
+ prefix = self.SUBSTEP_PREFIX
154
+
155
+ if step.status == Status.passed:
156
+ keyword = self.colorize(keyword, "green")
157
+ text = f"{indent}{prefix}{keyword} {step.name}"
158
+ elif step.status == Status.failed:
159
+ text = self.colorize(
160
+ f"{indent}{prefix}{keyword} {step.name}", "red"
161
+ )
162
+ elif step.status == Status.undefined:
163
+ text = self.colorize(
164
+ f"{indent}{prefix}{keyword} {step.name}", "yellow"
165
+ )
166
+ elif step.status == Status.skipped:
167
+ text = self.colorize(
168
+ f"{indent}{prefix}{keyword} {step.name}\n", "cyan"
169
+ )
170
+ elif step.status == Status.untested:
171
+ text = self.colorize(
172
+ f"{indent}{prefix}{keyword} {step.name}\n", "cyan"
173
+ )
174
+
175
+ if self.monochrome:
176
+ self.stream.write(f"{text}")
177
+ else:
178
+ self.stream.write(f"\r{text}")
179
+
180
+ if step.status in (Status.passed, Status.failed):
181
+ max_line_length = self.calculate_max_line_length()
182
+ status_text = ""
183
+ if self.show_timings:
184
+ start = step.start_timestamp
185
+ duration = f"{step.duration:.3f}"
186
+ status_text += f" # started at {start} took {duration}s"
187
+
188
+ current_step_text = f"{step.keyword.rjust(5)} {step.name}"
189
+ status_text_padding = (
190
+ max_line_length - len(current_step_text) - len(prefix)
191
+ )
192
+ status_text = f'{" " * status_text_padding}{status_text}'
193
+ status_text = self.colorize(status_text, "yellow")
194
+
195
+ self.stream.write(f"{status_text}\n")
196
+ if step.error_message:
197
+ self.stream.write(f"{step.error_message}\n")
198
+
199
+ if step.text:
200
+ self.doc_string(step.text)
201
+
202
+ if step.table:
203
+ self.table(getattr(step.table, "original", step.table))
204
+
205
+ if step.status in (Status.passed, Status.failed):
206
+ # print the variable values in step name, multiline/table arguments
207
+ # as a comment to ease debugging
208
+ step_variables = CONFIG.expand(step.name)
209
+
210
+ if step.text:
211
+ step_variables.update(CONFIG.expand(step.text))
212
+
213
+ if step.table:
214
+ for row in step.table.original.rows:
215
+ for value in row:
216
+ step_variables.update(CONFIG.expand(value))
217
+
218
+ if step_variables:
219
+ expanded = " ".join(
220
+ [
221
+ f'{key}="{value}"'
222
+ for (key, value) in step_variables.items()
223
+ ]
224
+ )
225
+
226
+ padding = f" {' '*(len('Given')-len(step.keyword))}"
227
+ variable_line = f"{padding}# {expanded}\n"
228
+ # hide secrets before we do anything to add color which could
229
+ # modify the output and result in not being able to correctly
230
+ # parse
231
+ # TODO: I'd like to move this out of here as we should be able
232
+ # to intercept all of the stdout/stderr writes but seems
233
+ # behaves underlying self.stream here is getting around
234
+ # that by accessing stdout/stderr another way.
235
+ variable_line = CONFIG.hide_secrets(variable_line)
236
+ colored_variable_line = self.colorize(variable_line, "blue")
237
+ self.stream.write(colored_variable_line)
238
+ self.stream.flush()
239
+
240
+ self.previous_step = step
241
+ CONFIG["__CUCU_WROTE_TO_OUTPUT"] = False
242
+
243
+ def eof(self):
244
+ self.stream.write("\n")
245
+
246
+ if self.current_scenario:
247
+ if self.current_scenario.status.name == "failed":
248
+ # we need to record the error_message and exc_traceback in the
249
+ # last executed step and mark it as failed so the reporting can
250
+ # show the result correctly
251
+ error_message = traceback.format_tb(
252
+ self.current_scenario.exc_traceback
253
+ )
254
+ self.stream.write("\n".join(error_message))
255
+
256
+ # -- MORE: Formatter helpers
257
+ def doc_string(self, doc_string):
258
+ self.printer.print_docstring(doc_string, self.multiline_indentation)
259
+
260
+ def table(self, table):
261
+ self.printer.print_table(table, self.multiline_indentation)
cucu/formatter/json.py ADDED
@@ -0,0 +1,321 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # adapted from:
4
+ # https://github.com/behave/behave/blob/master/behave/formatter/json.py
5
+ #
6
+ from __future__ import absolute_import
7
+
8
+ import json
9
+ import traceback
10
+ import uuid
11
+
12
+ import six
13
+ from behave.formatter.base import Formatter
14
+ from behave.model_core import Status
15
+ from tenacity import RetryError
16
+
17
+ from cucu.config import CONFIG
18
+
19
+
20
+ # -----------------------------------------------------------------------------
21
+ # CLASS: JSONFormatter
22
+ # -----------------------------------------------------------------------------
23
+ class CucuJSONFormatter(Formatter):
24
+ name = "cucu-json"
25
+ description = "JSON dump of test run"
26
+ dumps_kwargs = {"indent": 2, "sort_keys": True}
27
+
28
+ split_text_into_lines = True # EXPERIMENT for better readability.
29
+
30
+ json_number_types = six.integer_types + (float,)
31
+ json_scalar_types = json_number_types + (six.text_type, bool, type(None))
32
+
33
+ def __init__(self, stream_opener, config):
34
+ super(CucuJSONFormatter, self).__init__(stream_opener, config)
35
+ # -- ENSURE: Output stream is open.
36
+ self.stream = self.open()
37
+ self.feature_count = 0
38
+ self.current_feature = None
39
+ self.current_feature_data = None
40
+ self.current_scenario = None
41
+ self.last_step = None
42
+ self.steps = []
43
+ self.write_json_header()
44
+
45
+ def reset(self):
46
+ self.current_feature = None
47
+ self.current_feature_data = None
48
+ self.current_scenario = None
49
+
50
+ # -- FORMATTER API:
51
+ def uri(self, uri):
52
+ pass
53
+
54
+ def feature(self, feature):
55
+ self.reset()
56
+ self.current_feature = feature
57
+ self.current_feature_data = {
58
+ "keyword": feature.keyword,
59
+ "name": feature.name,
60
+ "tags": list(feature.tags),
61
+ "location": six.text_type(feature.location),
62
+ "status": None, # Not known before feature run.
63
+ }
64
+ element = self.current_feature_data
65
+ if feature.description:
66
+ element["description"] = feature.description
67
+
68
+ def background(self, background):
69
+ element = self.add_feature_element(
70
+ {
71
+ "type": "background",
72
+ "keyword": background.keyword,
73
+ "name": background.name,
74
+ "location": six.text_type(background.location),
75
+ "steps": [],
76
+ }
77
+ )
78
+ if background.name:
79
+ element["name"] = background.name
80
+
81
+ # -- ADD BACKGROUND STEPS: Support *.feature file regeneration.
82
+ for step_ in background.steps:
83
+ self.step(step_)
84
+
85
+ def scenario(self, scenario):
86
+ self.finish_current_scenario()
87
+ self.current_scenario = scenario
88
+
89
+ element = self.add_feature_element(
90
+ {
91
+ "type": "scenario",
92
+ "keyword": scenario.keyword,
93
+ "name": scenario.name,
94
+ "tags": scenario.tags,
95
+ "location": six.text_type(scenario.location),
96
+ "steps": [],
97
+ "status": None,
98
+ }
99
+ )
100
+ self.steps = []
101
+ if scenario.description:
102
+ element["description"] = scenario.description
103
+
104
+ @classmethod
105
+ def make_table(cls, table):
106
+ table_data = {
107
+ "headings": table.headings,
108
+ "rows": [list(row) for row in table.rows],
109
+ }
110
+ return table_data
111
+
112
+ def insert_step(self, step, index=-1):
113
+ step.unique_id = uuid.uuid1()
114
+ is_substep = getattr(step, "is_substep", False)
115
+
116
+ step_details = {
117
+ "keyword": step.keyword,
118
+ "step_type": step.step_type,
119
+ "name": step.name,
120
+ "location": six.text_type(step.location),
121
+ "substep": is_substep,
122
+ }
123
+
124
+ if step.text:
125
+ text = step.text
126
+ if self.split_text_into_lines and "\n" in text:
127
+ text = text.splitlines()
128
+ step_details["text"] = text
129
+ if step.table:
130
+ step_details["table"] = self.make_table(step.table)
131
+ element = self.current_feature_element
132
+
133
+ if index == -1:
134
+ self.steps.append(step)
135
+ element["steps"].append(step_details)
136
+ else:
137
+ self.steps.insert(index, step)
138
+ element["steps"].insert(index, step_details)
139
+
140
+ def step(self, step):
141
+ self.insert_step(step, index=-1)
142
+
143
+ def match(self, match):
144
+ # nothing to do, but we need to implement the method
145
+ pass
146
+
147
+ def result(self, step):
148
+ steps = self.current_feature_element["steps"]
149
+ step_index = 0
150
+ for other_step in self.steps:
151
+ if other_step.unique_id == step.unique_id:
152
+ break
153
+ step_index += 1
154
+
155
+ # keep the last step recorded result state
156
+ self.last_step = steps[step_index]
157
+
158
+ timestamp = None
159
+ if step.status.name in ["passed", "failed"]:
160
+ timestamp = step.start_timestamp
161
+
162
+ step_variables = CONFIG.expand(step.name)
163
+
164
+ if step.text:
165
+ step_variables.update(CONFIG.expand(step.text))
166
+
167
+ if step.table:
168
+ for row in step.table.original.rows:
169
+ for value in row:
170
+ step_variables.update(CONFIG.expand(value))
171
+
172
+ if step_variables:
173
+ expanded = " ".join(
174
+ [
175
+ f'{key}="{value}"'
176
+ for (key, value) in step_variables.items()
177
+ ]
178
+ )
179
+ padding = f" {' '*(len('Given')-len(step.keyword))}"
180
+ step.stdout.insert(
181
+ 0, f"{padding}# {CONFIG.hide_secrets(expanded)}\n"
182
+ )
183
+
184
+ stdout = None
185
+ if "stdout" in step.__dict__ and step.stdout != []:
186
+ stdout = [CONFIG.hide_secrets("".join(step.stdout).rstrip())]
187
+
188
+ stderr = None
189
+ if "stderr" in step.__dict__ and step.stderr != []:
190
+ stderr = [CONFIG.hide_secrets("".join(step.stderr).rstrip())]
191
+
192
+ steps[step_index]["result"] = {
193
+ "stdout": stdout,
194
+ "stderr": stderr,
195
+ "status": step.status.name,
196
+ "duration": step.duration,
197
+ "timestamp": timestamp,
198
+ }
199
+
200
+ if step.error_message and step.status == Status.failed:
201
+ # -- OPTIONAL: Provided for failed steps.
202
+ error_message = CONFIG.hide_secrets(step.error_message)
203
+ if self.split_text_into_lines:
204
+ error_message = error_message.splitlines()
205
+
206
+ result_element = steps[step_index]["result"]
207
+ result_element["error_message"] = error_message
208
+
209
+ if error := step.exception:
210
+ if isinstance(error, RetryError):
211
+ error = error.last_attempt.exception()
212
+
213
+ if len(error.args) > 0 and isinstance(error.args[0], str):
214
+ error_class_name = error.__class__.__name__
215
+ redacted_error_msg = CONFIG.hide_secrets(error.args[0])
216
+ error_lines = redacted_error_msg.splitlines()
217
+ error_lines[0] = f"{error_class_name}: {error_lines[0]}"
218
+ else:
219
+ error_lines = [repr(error)]
220
+
221
+ result_element["exception"] = error_lines
222
+
223
+ def embedding(self, mime_type, data):
224
+ # nothing to do, but we need to implement the method
225
+ pass
226
+
227
+ def eof(self):
228
+ """
229
+ End of feature
230
+ """
231
+ if not self.current_feature_data:
232
+ return
233
+
234
+ # -- NORMAL CASE: Write collected data of current feature.
235
+ self.finish_current_scenario()
236
+ self.update_status_data()
237
+
238
+ self.write_json_feature(self.current_feature_data)
239
+ self.reset()
240
+
241
+ def close(self):
242
+ self.write_json_footer()
243
+ self.close_stream()
244
+
245
+ # -- JSON-DATA COLLECTION:
246
+ def add_feature_element(self, element):
247
+ assert self.current_feature_data is not None
248
+ if "elements" not in self.current_feature_data:
249
+ self.current_feature_data["elements"] = []
250
+ self.current_feature_data["elements"].append(element)
251
+ return element
252
+
253
+ @property
254
+ def current_feature_element(self):
255
+ assert self.current_feature_data is not None
256
+ return self.current_feature_data["elements"][-1]
257
+
258
+ def update_status_data(self):
259
+ assert self.current_feature
260
+ assert self.current_feature_data
261
+ self.current_feature_data["status"] = self.current_feature.status.name
262
+
263
+ def finish_current_scenario(self):
264
+ if self.current_scenario:
265
+ hook_failed = self.current_scenario.hook_failed
266
+ if hook_failed:
267
+ status_name = "errored"
268
+ else:
269
+ status_name = self.current_scenario.status.name
270
+
271
+ self.current_feature_element["status"] = status_name
272
+
273
+ if status_name in ["failed", "errored"]:
274
+ # we need to record the error_message and exc_traceback in the
275
+ # last executed step and mark it as failed so the reporting can
276
+ # show the result correctly
277
+ error_message = [self.current_scenario.error_message]
278
+ error_message += traceback.format_tb(
279
+ self.current_scenario.exc_traceback
280
+ )
281
+ # If a before scenario hook fails, last_step will be None.
282
+ if (
283
+ self.last_step is not None
284
+ and "error_message" not in self.last_step["result"]
285
+ ):
286
+ self.last_step["result"]["error_message"] = error_message
287
+
288
+ # -- JSON-WRITER:
289
+ def write_json_header(self):
290
+ self.stream.write("[\n")
291
+
292
+ def write_json_footer(self):
293
+ self.stream.write("\n]\n")
294
+
295
+ def write_json_feature(self, feature_data):
296
+ if "elements" not in feature_data:
297
+ return
298
+
299
+ filtered_scenarios = [
300
+ x
301
+ for x in feature_data["elements"]
302
+ if x["keyword"] == "Scenario"
303
+ and (
304
+ CONFIG["CUCU_SHOW_SKIPS"] == "true" or x["status"] != "skipped"
305
+ )
306
+ ]
307
+
308
+ if len(filtered_scenarios) == 0:
309
+ return
310
+
311
+ feature_data["elements"] = filtered_scenarios
312
+
313
+ if self.feature_count != 0:
314
+ self.write_json_feature_separator()
315
+
316
+ self.stream.write(json.dumps(feature_data, **self.dumps_kwargs))
317
+ self.stream.flush()
318
+ self.feature_count += 1
319
+
320
+ def write_json_feature_separator(self):
321
+ self.stream.write(",\n\n")