1# -*- coding: UTF-8 -*- 2""" 3Provides a formatter that provides an overview of available step definitions 4(step implementations). 5""" 6 7from __future__ import absolute_import 8from operator import attrgetter 9import inspect 10from six.moves import zip 11from behave.formatter.base import Formatter 12from behave.step_registry import StepRegistry, registry 13from behave.textutil import \ 14 compute_words_maxsize, indent, make_indentation, text as _text 15from behave import i18n 16 17 18# ----------------------------------------------------------------------------- 19# CLASS: AbstractStepsFormatter 20# ----------------------------------------------------------------------------- 21class AbstractStepsFormatter(Formatter): 22 """ 23 Provides a formatter base class that provides the common functionality 24 for formatter classes that operate on step definitions (implementations). 25 26 .. note:: 27 Supports behave dry-run mode. 28 """ 29 step_types = ("given", "when", "then", "step") 30 31 def __init__(self, stream_opener, config): 32 super(AbstractStepsFormatter, self).__init__(stream_opener, config) 33 self.step_registry = None 34 self.current_feature = None 35 self.shows_location = config.show_source 36 37 def reset(self): 38 self.step_registry = None 39 self.current_feature = None 40 41 def discover_step_definitions(self): 42 if self.step_registry is None: 43 self.step_registry = StepRegistry() 44 45 for step_type in registry.steps: 46 step_definitions = tuple(registry.steps[step_type]) 47 for step_definition in step_definitions: 48 step_definition.step_type = step_type 49 self.step_registry.steps[step_type] = step_definitions 50 51 # -- FORMATTER API: 52 def feature(self, feature): 53 self.current_feature = feature 54 if not self.step_registry: 55 # -- ONLY-ONCE: 56 self.discover_step_definitions() 57 58 def eof(self): 59 """Called at end of a feature.""" 60 self.current_feature = None 61 62 def close(self): 63 """Called at end of test run.""" 64 if not self.step_registry: 65 self.discover_step_definitions() 66 67 if self.step_registry: 68 # -- ENSURE: Output stream is open. 69 self.stream = self.open() 70 self.report() 71 72 # -- FINALLY: 73 self.close_stream() 74 75 # -- REPORT SPECIFIC-API: 76 def report(self): 77 raise NotImplementedError() 78 79 # pylint: disable=no-self-use 80 def describe_step_definition(self, step_definition, step_type=None): 81 if not step_type: 82 step_type = step_definition.step_type 83 assert step_type 84 return u"@%s('%s')" % (step_type, step_definition.pattern) 85 86 87# ----------------------------------------------------------------------------- 88# CLASS: StepsFormatter 89# ----------------------------------------------------------------------------- 90class StepsFormatter(AbstractStepsFormatter): 91 """ 92 Provides formatter class that provides an overview 93 which step definitions are available. 94 95 EXAMPLE: 96 $ behave --dry-run -f steps features/ 97 GIVEN STEP DEFINITIONS[21]: 98 Given a new working directory 99 Given I use the current directory as working directory 100 Given a file named "{filename}" with 101 ... 102 Given a step passes 103 Given a step fails 104 105 WHEN STEP DEFINITIONS[14]: 106 When I run "{command}" 107 ... 108 When a step passes 109 When a step fails 110 111 THEN STEP DEFINITIONS[45]: 112 Then the command should fail with returncode="{result:int}" 113 Then it should pass with 114 Then it should fail with 115 Then the command output should contain "{text}" 116 ... 117 Then a step passes 118 Then a step fails 119 120 GENERIC STEP DEFINITIONS[13]: 121 * I remove the directory "{directory}" 122 * a file named "{filename}" exists 123 * a file named "{filename}" does not exist 124 ... 125 * a step passes 126 * a step fails 127 128 .. note:: 129 Supports behave dry-run mode. 130 """ 131 name = "steps" 132 description = "Shows step definitions (step implementations)." 133 shows_location = True 134 min_location_column = 40 135 136 # -- REPORT SPECIFIC-API: 137 def report(self): 138 self.report_steps_by_type() 139 140 def report_steps_by_type(self): 141 """Show an overview of the existing step implementations per step type. 142 """ 143 # pylint: disable=too-many-branches 144 assert set(self.step_types) == set(self.step_registry.steps.keys()) 145 language = self.config.lang or "en" 146 language_keywords = i18n.languages[language] 147 148 for step_type in self.step_types: 149 steps = list(self.step_registry.steps[step_type]) 150 if step_type != "step": 151 steps.extend(self.step_registry.steps["step"]) 152 if not steps: 153 continue 154 155 # -- PREPARE REPORT: For a step-type. 156 step_type_name = step_type.upper() 157 if step_type == "step": 158 step_keyword = "*" 159 step_type_name = "GENERIC" 160 else: 161 # step_keyword = step_type.capitalize() 162 keywords = language_keywords[step_type] 163 if keywords[0] == u"*": 164 assert len(keywords) > 1 165 step_keyword = keywords[1] 166 else: 167 step_keyword = keywords[0] 168 169 steps_text = [u"%s %s" % (step_keyword, step.pattern) 170 for step in steps] 171 if self.shows_location: 172 max_size = compute_words_maxsize(steps_text) 173 if max_size < self.min_location_column: 174 max_size = self.min_location_column 175 schema = u" %-" + _text(max_size) + "s # %s\n" 176 else: 177 schema = u" %s\n" 178 179 # -- REPORT: 180 message = "%s STEP DEFINITIONS[%s]:\n" 181 self.stream.write(message % (step_type_name, len(steps))) 182 for step, step_text in zip(steps, steps_text): 183 if self.shows_location: 184 self.stream.write(schema % (step_text, step.location)) 185 else: 186 self.stream.write(schema % step_text) 187 self.stream.write("\n") 188 189 190# ----------------------------------------------------------------------------- 191# CLASS: StepsDocFormatter 192# ----------------------------------------------------------------------------- 193class StepsDocFormatter(AbstractStepsFormatter): 194 """ 195 Provides formatter class that shows the documentation of all registered 196 step definitions. The primary purpose is to provide help for a test writer. 197 198 EXAMPLE: 199 $ behave --dry-run -f steps.doc features/ 200 @given('a file named "{filename}" with') 201 Function: step_a_file_named_filename_with() 202 Location: behave4cmd0/command_steps.py:50 203 Creates a textual file with the content provided as docstring. 204 205 @when('I run "{command}"') 206 Function: step_i_run_command() 207 Location: behave4cmd0/command_steps.py:80 208 Run a command as subprocess, collect its output and returncode. 209 210 @step('a file named "{filename}" exists') 211 Function: step_file_named_filename_exists() 212 Location: behave4cmd0/command_steps.py:305 213 Verifies that a file with this filename exists. 214 215 .. code-block:: gherkin 216 217 Given a file named "abc.txt" exists 218 When a file named "abc.txt" exists 219 ... 220 221 .. note:: 222 Supports behave dry-run mode. 223 """ 224 name = "steps.doc" 225 description = "Shows documentation for step definitions." 226 shows_location = True 227 shows_function_name = True 228 ordered_by_location = True 229 doc_prefix = make_indentation(4) 230 231 # -- REPORT SPECIFIC-API: 232 def report(self): 233 self.report_step_definition_docs() 234 self.stream.write("\n") 235 236 def report_step_definition_docs(self): 237 step_definitions = [] 238 for step_type in self.step_types: 239 for step_definition in self.step_registry.steps[step_type]: 240 # step_definition.step_type = step_type 241 assert step_definition.step_type is not None 242 step_definitions.append(step_definition) 243 244 if self.ordered_by_location: 245 step_definitions = sorted(step_definitions, 246 key=attrgetter("location")) 247 248 for step_definition in step_definitions: 249 self.write_step_definition(step_definition) 250 251 def write_step_definition(self, step_definition): 252 step_definition_text = self.describe_step_definition(step_definition) 253 self.stream.write(u"%s\n" % step_definition_text) 254 doc = inspect.getdoc(step_definition.func) 255 func_name = step_definition.func.__name__ 256 if self.shows_function_name and func_name not in ("step", "impl"): 257 self.stream.write(u" Function: %s()\n" % func_name) 258 if self.shows_location: 259 self.stream.write(u" Location: %s\n" % step_definition.location) 260 if doc: 261 doc = doc.strip() 262 self.stream.write(indent(doc, self.doc_prefix)) 263 self.stream.write("\n") 264 self.stream.write("\n") 265 266 267# ----------------------------------------------------------------------------- 268# CLASS: StepsCatalogFormatter 269# ----------------------------------------------------------------------------- 270class StepsCatalogFormatter(StepsDocFormatter): 271 """ 272 Provides formatter class that shows the documentation of all registered 273 step definitions. The primary purpose is to provide help for a test writer. 274 275 In order to ease work for non-programmer testers, the technical details of 276 the steps (i.e. function name, source location) are ommited and the 277 steps are shown as they would apprear in a feature file (no noisy '@', 278 or '(', etc.). 279 280 Also, the output is sorted by step type (Given, When, Then) 281 282 Generic step definitions are listed with all three step types. 283 284 EXAMPLE: 285 $ behave --dry-run -f steps.catalog features/ 286 Given a file named "{filename}" with 287 Creates a textual file with the content provided as docstring. 288 289 When I run "{command}" 290 Run a command as subprocess, collect its output and returncode. 291 292 Given a file named "{filename}" exists 293 When a file named "{filename}" exists 294 Then a file named "{filename}" exists 295 Verifies that a file with this filename exists. 296 297 .. code-block:: gherkin 298 299 Given a file named "abc.txt" exists 300 When a file named "abc.txt" exists 301 ... 302 303 .. note:: 304 Supports behave dry-run mode. 305 """ 306 name = "steps.catalog" 307 description = "Shows non-technical documentation for step definitions." 308 shows_location = False 309 shows_function_name = False 310 ordered_by_location = False 311 doc_prefix = make_indentation(4) 312 313 314 def describe_step_definition(self, step_definition, step_type=None): 315 if not step_type: 316 step_type = step_definition.step_type 317 assert step_type 318 desc = [] 319 if step_type == "step": 320 for step_type1 in self.step_types[:-1]: 321 text = u"%5s %s" % (step_type1.title(), step_definition.pattern) 322 desc.append(text) 323 else: 324 desc.append(u"%s %s" % (step_type.title(), step_definition.pattern)) 325 326 return '\n'.join(desc) 327 328 329# ----------------------------------------------------------------------------- 330# CLASS: StepsUsageFormatter 331# ----------------------------------------------------------------------------- 332class StepsUsageFormatter(AbstractStepsFormatter): 333 """ 334 Provides formatter class that shows how step definitions are used by steps. 335 336 EXAMPLE: 337 $ behave --dry-run -f steps.usage features/ 338 ... 339 340 .. note:: 341 Supports behave dry-run mode. 342 """ 343 name = "steps.usage" 344 description = "Shows how step definitions are used by steps." 345 doc_prefix = make_indentation(4) 346 min_location_column = 40 347 348 def __init__(self, stream_opener, config): 349 super(StepsUsageFormatter, self).__init__(stream_opener, config) 350 self.step_usage_database = {} 351 self.undefined_steps = [] 352 353 def reset(self): 354 super(StepsUsageFormatter, self).reset() 355 self.step_usage_database = {} 356 self.undefined_steps = [] 357 358 # pylint: disable=invalid-name 359 def get_step_type_for_step_definition(self, step_definition): 360 step_type = step_definition.step_type 361 if not step_type: 362 # -- DETERMINE STEP-TYPE FROM STEP-REGISTRY: 363 assert self.step_registry 364 for step_type, values in self.step_registry.steps.items(): 365 if step_definition in values: 366 return step_type 367 # -- OTHERWISE: 368 step_type = "step" 369 return step_type 370 # pylint: enable=invalid-name 371 372 def select_unused_step_definitions(self): 373 step_definitions = set() 374 for step_type, values in self.step_registry.steps.items(): 375 step_definitions.update(values) 376 used_step_definitions = set(self.step_usage_database.keys()) 377 unused_step_definitions = step_definitions - used_step_definitions 378 return unused_step_definitions 379 380 def update_usage_database(self, step_definition, step): 381 matching_steps = self.step_usage_database.get(step_definition, None) 382 if matching_steps is None: 383 assert step_definition.step_type is not None 384 matching_steps = self.step_usage_database[step_definition] = [] 385 # -- AVOID DUPLICATES: From Scenario Outlines 386 if not steps_contain(matching_steps, step): 387 matching_steps.append(step) 388 389 def update_usage_database_for_step(self, step): 390 step_definition = self.step_registry.find_step_definition(step) 391 if step_definition: 392 self.update_usage_database(step_definition, step) 393 # elif step not in self.undefined_steps: 394 elif not steps_contain(self.undefined_steps, step): 395 # -- AVOID DUPLICATES: From Scenario Outlines 396 self.undefined_steps.append(step) 397 398 # pylint: disable=invalid-name 399 def update_usage_database_for_feature(self, feature): 400 # -- PROCESS BACKGROUND (if exists): Use Background steps only once. 401 if feature.background: 402 for step in feature.background.steps: 403 self.update_usage_database_for_step(step) 404 405 # -- PROCESS SCENARIOS: Without background steps. 406 for scenario in feature.walk_scenarios(): 407 for step in scenario.steps: 408 self.update_usage_database_for_step(step) 409 # pylint: enable=invalid-name 410 411 # -- FORMATTER API: 412 def feature(self, feature): 413 super(StepsUsageFormatter, self).feature(feature) 414 self.update_usage_database_for_feature(feature) 415 416 # -- REPORT API: 417 def report(self): 418 self.report_used_step_definitions() 419 self.report_unused_step_definitions() 420 self.report_undefined_steps() 421 self.stream.write("\n") 422 423 # -- REPORT SPECIFIC-API: 424 def report_used_step_definitions(self): 425 # -- STEP: Used step definitions. 426 # ORDERING: Sort step definitions by file location. 427 get_location = lambda x: x[0].location 428 step_definition_items = self.step_usage_database.items() 429 step_definition_items = sorted(step_definition_items, key=get_location) 430 431 for step_definition, steps in step_definition_items: 432 stepdef_text = self.describe_step_definition(step_definition) 433 steps_text = [u" %s %s" % (step.keyword, step.name) 434 for step in steps] 435 steps_text.append(stepdef_text) 436 max_size = compute_words_maxsize(steps_text) 437 if max_size < self.min_location_column: 438 max_size = self.min_location_column 439 440 schema = u"%-" + _text(max_size) + "s # %s\n" 441 self.stream.write(schema % (stepdef_text, step_definition.location)) 442 schema = u"%-" + _text(max_size) + "s # %s\n" 443 for step, step_text in zip(steps, steps_text): 444 self.stream.write(schema % (step_text, step.location)) 445 self.stream.write("\n") 446 447 def report_unused_step_definitions(self): 448 unused_step_definitions = self.select_unused_step_definitions() 449 if not unused_step_definitions: 450 return 451 452 # -- STEP: Prepare report for unused step definitions. 453 # ORDERING: Sort step definitions by file location. 454 get_location = lambda x: x.location 455 step_definitions = sorted(unused_step_definitions, key=get_location) 456 step_texts = [self.describe_step_definition(step_definition) 457 for step_definition in step_definitions] 458 459 max_size = compute_words_maxsize(step_texts) 460 if max_size < self.min_location_column-2: 461 max_size = self.min_location_column-2 462 463 # -- STEP: Write report. 464 schema = u" %-" + _text(max_size) + "s # %s\n" 465 self.stream.write("UNUSED STEP DEFINITIONS[%d]:\n" % len(step_texts)) 466 for step_definition, step_text in zip(step_definitions, step_texts): 467 self.stream.write(schema % (step_text, step_definition.location)) 468 469 def report_undefined_steps(self): 470 if not self.undefined_steps: 471 return 472 473 # -- STEP: Undefined steps. 474 undefined_steps = sorted(self.undefined_steps, 475 key=attrgetter("location")) 476 477 steps_text = [u" %s %s" % (step.keyword, step.name) 478 for step in undefined_steps] 479 max_size = compute_words_maxsize(steps_text) 480 if max_size < self.min_location_column: 481 max_size = self.min_location_column 482 483 self.stream.write("\nUNDEFINED STEPS[%d]:\n" % len(steps_text)) 484 schema = u"%-" + _text(max_size) + "s # %s\n" 485 for step, step_text in zip(undefined_steps, steps_text): 486 self.stream.write(schema % (step_text, step.location)) 487 488# ----------------------------------------------------------------------------- 489# UTILITY FUNCTIONS: 490# ----------------------------------------------------------------------------- 491def steps_contain(steps, step): 492 for other_step in steps: 493 if step == other_step and step.location == other_step.location: 494 # -- NOTE: Step comparison does not take location into account. 495 return True 496 # -- OTHERWISE: Not contained yet (or step in other location). 497 return False 498