busser-behave 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.cane +0 -0
- data/.gitignore +17 -0
- data/.tailor +4 -0
- data/.travis.yml +11 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +3 -0
- data/LICENSE +15 -0
- data/README.md +41 -0
- data/Rakefile +68 -0
- data/busser-behave.gemspec +30 -0
- data/features/plugin_install_command.feature +11 -0
- data/features/plugin_list_command.feature +8 -0
- data/features/support/env.rb +13 -0
- data/features/test_command.feature +31 -0
- data/lib/busser/behave/version.rb +26 -0
- data/lib/busser/runner_plugin/behave.rb +37 -0
- data/vendor/behave/CHANGES.rst +483 -0
- data/vendor/behave/LICENSE +23 -0
- data/vendor/behave/MANIFEST.in +37 -0
- data/vendor/behave/PROJECT_INFO.rst +21 -0
- data/vendor/behave/README.rst +112 -0
- data/vendor/behave/VERSION.txt +1 -0
- data/vendor/behave/behave.ini +22 -0
- data/vendor/behave/behave/__init__.py +30 -0
- data/vendor/behave/behave/__main__.py +187 -0
- data/vendor/behave/behave/_stepimport.py +185 -0
- data/vendor/behave/behave/_types.py +134 -0
- data/vendor/behave/behave/api/__init__.py +7 -0
- data/vendor/behave/behave/api/async_step.py +283 -0
- data/vendor/behave/behave/capture.py +227 -0
- data/vendor/behave/behave/compat/__init__.py +5 -0
- data/vendor/behave/behave/compat/collections.py +20 -0
- data/vendor/behave/behave/configuration.py +788 -0
- data/vendor/behave/behave/contrib/__init__.py +0 -0
- data/vendor/behave/behave/contrib/scenario_autoretry.py +73 -0
- data/vendor/behave/behave/formatter/__init__.py +12 -0
- data/vendor/behave/behave/formatter/_builtins.py +39 -0
- data/vendor/behave/behave/formatter/_registry.py +135 -0
- data/vendor/behave/behave/formatter/ansi_escapes.py +91 -0
- data/vendor/behave/behave/formatter/base.py +200 -0
- data/vendor/behave/behave/formatter/formatters.py +57 -0
- data/vendor/behave/behave/formatter/json.py +253 -0
- data/vendor/behave/behave/formatter/null.py +12 -0
- data/vendor/behave/behave/formatter/plain.py +158 -0
- data/vendor/behave/behave/formatter/pretty.py +351 -0
- data/vendor/behave/behave/formatter/progress.py +287 -0
- data/vendor/behave/behave/formatter/rerun.py +114 -0
- data/vendor/behave/behave/formatter/sphinx_steps.py +372 -0
- data/vendor/behave/behave/formatter/sphinx_util.py +118 -0
- data/vendor/behave/behave/formatter/steps.py +497 -0
- data/vendor/behave/behave/formatter/tags.py +178 -0
- data/vendor/behave/behave/i18n.py +614 -0
- data/vendor/behave/behave/importer.py +102 -0
- data/vendor/behave/behave/json_parser.py +264 -0
- data/vendor/behave/behave/log_capture.py +233 -0
- data/vendor/behave/behave/matchers.py +402 -0
- data/vendor/behave/behave/model.py +1737 -0
- data/vendor/behave/behave/model_core.py +416 -0
- data/vendor/behave/behave/model_describe.py +105 -0
- data/vendor/behave/behave/parser.py +615 -0
- data/vendor/behave/behave/reporter/__init__.py +0 -0
- data/vendor/behave/behave/reporter/base.py +45 -0
- data/vendor/behave/behave/reporter/junit.py +473 -0
- data/vendor/behave/behave/reporter/summary.py +94 -0
- data/vendor/behave/behave/runner.py +753 -0
- data/vendor/behave/behave/runner_util.py +417 -0
- data/vendor/behave/behave/step_registry.py +112 -0
- data/vendor/behave/behave/tag_expression.py +111 -0
- data/vendor/behave/behave/tag_matcher.py +465 -0
- data/vendor/behave/behave/textutil.py +137 -0
- data/vendor/behave/behave/userdata.py +130 -0
- data/vendor/behave/behave4cmd0/__all_steps__.py +12 -0
- data/vendor/behave/behave4cmd0/__init__.py +5 -0
- data/vendor/behave/behave4cmd0/__setup.py +11 -0
- data/vendor/behave/behave4cmd0/command_shell.py +216 -0
- data/vendor/behave/behave4cmd0/command_shell_proc.py +256 -0
- data/vendor/behave/behave4cmd0/command_steps.py +532 -0
- data/vendor/behave/behave4cmd0/command_util.py +147 -0
- data/vendor/behave/behave4cmd0/failing_steps.py +49 -0
- data/vendor/behave/behave4cmd0/log/__init__.py +1 -0
- data/vendor/behave/behave4cmd0/log/steps.py +395 -0
- data/vendor/behave/behave4cmd0/note_steps.py +29 -0
- data/vendor/behave/behave4cmd0/passing_steps.py +36 -0
- data/vendor/behave/behave4cmd0/pathutil.py +146 -0
- data/vendor/behave/behave4cmd0/setup_command_shell.py +24 -0
- data/vendor/behave/behave4cmd0/textutil.py +304 -0
- data/vendor/behave/bin/behave +44 -0
- data/vendor/behave/bin/behave.cmd +10 -0
- data/vendor/behave/bin/behave.junit_filter.py +85 -0
- data/vendor/behave/bin/behave.step_durations.py +163 -0
- data/vendor/behave/bin/behave2cucumber_json.py +63 -0
- data/vendor/behave/bin/behave_cmd.py +44 -0
- data/vendor/behave/bin/convert_i18n_yaml.py +77 -0
- data/vendor/behave/bin/explore_platform_encoding.py +24 -0
- data/vendor/behave/bin/i18n.yml +621 -0
- data/vendor/behave/bin/invoke +8 -0
- data/vendor/behave/bin/invoke.cmd +9 -0
- data/vendor/behave/bin/json.format.py +167 -0
- data/vendor/behave/bin/jsonschema_validate.py +122 -0
- data/vendor/behave/bin/make_localpi.py +279 -0
- data/vendor/behave/bin/project_bootstrap.sh +30 -0
- data/vendor/behave/bin/toxcmd.py +270 -0
- data/vendor/behave/bin/toxcmd3.py +270 -0
- data/vendor/behave/conftest.py +27 -0
- data/vendor/behave/docs/Makefile +154 -0
- data/vendor/behave/docs/_static/agogo.css +501 -0
- data/vendor/behave/docs/_static/behave_logo.png +0 -0
- data/vendor/behave/docs/_static/behave_logo1.png +0 -0
- data/vendor/behave/docs/_static/behave_logo2.png +0 -0
- data/vendor/behave/docs/_static/behave_logo3.png +0 -0
- data/vendor/behave/docs/_themes/LICENSE +45 -0
- data/vendor/behave/docs/_themes/kr/layout.html +17 -0
- data/vendor/behave/docs/_themes/kr/relations.html +19 -0
- data/vendor/behave/docs/_themes/kr/static/flasky.css_t +480 -0
- data/vendor/behave/docs/_themes/kr/static/small_flask.css +90 -0
- data/vendor/behave/docs/_themes/kr/theme.conf +7 -0
- data/vendor/behave/docs/_themes/kr_small/layout.html +22 -0
- data/vendor/behave/docs/_themes/kr_small/static/flasky.css_t +287 -0
- data/vendor/behave/docs/_themes/kr_small/theme.conf +10 -0
- data/vendor/behave/docs/api.rst +408 -0
- data/vendor/behave/docs/appendix.rst +19 -0
- data/vendor/behave/docs/behave.rst +640 -0
- data/vendor/behave/docs/behave.rst-template +86 -0
- data/vendor/behave/docs/behave_ecosystem.rst +81 -0
- data/vendor/behave/docs/comparison.rst +85 -0
- data/vendor/behave/docs/conf.py +293 -0
- data/vendor/behave/docs/context_attributes.rst +66 -0
- data/vendor/behave/docs/django.rst +192 -0
- data/vendor/behave/docs/formatters.rst +61 -0
- data/vendor/behave/docs/gherkin.rst +673 -0
- data/vendor/behave/docs/index.rst +57 -0
- data/vendor/behave/docs/install.rst +60 -0
- data/vendor/behave/docs/more_info.rst +184 -0
- data/vendor/behave/docs/new_and_noteworthy.rst +18 -0
- data/vendor/behave/docs/new_and_noteworthy_v1.2.4.rst +11 -0
- data/vendor/behave/docs/new_and_noteworthy_v1.2.5.rst +814 -0
- data/vendor/behave/docs/new_and_noteworthy_v1.2.6.rst +255 -0
- data/vendor/behave/docs/parse_builtin_types.rst +59 -0
- data/vendor/behave/docs/philosophy.rst +235 -0
- data/vendor/behave/docs/regular_expressions.rst +71 -0
- data/vendor/behave/docs/related.rst +14 -0
- data/vendor/behave/docs/test_domains.rst +62 -0
- data/vendor/behave/docs/tutorial.rst +636 -0
- data/vendor/behave/docs/update_behave_rst.py +100 -0
- data/vendor/behave/etc/json/behave.json-schema +172 -0
- data/vendor/behave/etc/junit.xml/behave_junit.xsd +103 -0
- data/vendor/behave/etc/junit.xml/junit-4.xsd +92 -0
- data/vendor/behave/examples/async_step/README.txt +8 -0
- data/vendor/behave/examples/async_step/behave.ini +14 -0
- data/vendor/behave/examples/async_step/features/async_dispatch.feature +8 -0
- data/vendor/behave/examples/async_step/features/async_run.feature +6 -0
- data/vendor/behave/examples/async_step/features/environment.py +28 -0
- data/vendor/behave/examples/async_step/features/steps/async_dispatch_steps.py +26 -0
- data/vendor/behave/examples/async_step/features/steps/async_steps34.py +10 -0
- data/vendor/behave/examples/async_step/features/steps/async_steps35.py +10 -0
- data/vendor/behave/examples/async_step/testrun_example.async_dispatch.txt +11 -0
- data/vendor/behave/examples/async_step/testrun_example.async_run.txt +9 -0
- data/vendor/behave/examples/env_vars/README.rst +26 -0
- data/vendor/behave/examples/env_vars/behave.ini +15 -0
- data/vendor/behave/examples/env_vars/behave_run.output_example.txt +12 -0
- data/vendor/behave/examples/env_vars/features/env_var.feature +6 -0
- data/vendor/behave/examples/env_vars/features/steps/env_var_steps.py +38 -0
- data/vendor/behave/features/README.txt +12 -0
- data/vendor/behave/features/background.feature +392 -0
- data/vendor/behave/features/capture_stderr.feature +172 -0
- data/vendor/behave/features/capture_stdout.feature +125 -0
- data/vendor/behave/features/cmdline.lang_list.feature +33 -0
- data/vendor/behave/features/configuration.default_paths.feature +116 -0
- data/vendor/behave/features/context.global_params.feature +35 -0
- data/vendor/behave/features/context.local_params.feature +17 -0
- data/vendor/behave/features/directory_layout.advanced.feature +147 -0
- data/vendor/behave/features/directory_layout.basic.feature +75 -0
- data/vendor/behave/features/directory_layout.basic2.feature +87 -0
- data/vendor/behave/features/environment.py +53 -0
- data/vendor/behave/features/exploratory_testing.with_table.feature +141 -0
- data/vendor/behave/features/feature.description.feature +0 -0
- data/vendor/behave/features/feature.exclude_from_run.feature +96 -0
- data/vendor/behave/features/formatter.help.feature +30 -0
- data/vendor/behave/features/formatter.json.feature +420 -0
- data/vendor/behave/features/formatter.progress3.feature +235 -0
- data/vendor/behave/features/formatter.rerun.feature +296 -0
- data/vendor/behave/features/formatter.steps.feature +181 -0
- data/vendor/behave/features/formatter.steps_catalog.feature +100 -0
- data/vendor/behave/features/formatter.steps_doc.feature +140 -0
- data/vendor/behave/features/formatter.steps_usage.feature +404 -0
- data/vendor/behave/features/formatter.tags.feature +134 -0
- data/vendor/behave/features/formatter.tags_location.feature +183 -0
- data/vendor/behave/features/formatter.user_defined.feature +196 -0
- data/vendor/behave/features/i18n.unicode_problems.feature +445 -0
- data/vendor/behave/features/logcapture.clear_handlers.feature +114 -0
- data/vendor/behave/features/logcapture.feature +188 -0
- data/vendor/behave/features/logcapture.filter.feature +130 -0
- data/vendor/behave/features/logging.no_capture.feature +99 -0
- data/vendor/behave/features/logging.setup_format.feature +157 -0
- data/vendor/behave/features/logging.setup_level.feature +168 -0
- data/vendor/behave/features/logging.setup_with_configfile.feature +137 -0
- data/vendor/behave/features/parser.background.sad_cases.feature +129 -0
- data/vendor/behave/features/parser.feature.sad_cases.feature +144 -0
- data/vendor/behave/features/runner.abort_by_user.feature +305 -0
- data/vendor/behave/features/runner.continue_after_failed_step.feature +136 -0
- data/vendor/behave/features/runner.default_format.feature +175 -0
- data/vendor/behave/features/runner.dry_run.feature +184 -0
- data/vendor/behave/features/runner.feature_listfile.feature +223 -0
- data/vendor/behave/features/runner.hook_errors.feature +382 -0
- data/vendor/behave/features/runner.multiple_formatters.feature +285 -0
- data/vendor/behave/features/runner.scenario_autoretry.feature +131 -0
- data/vendor/behave/features/runner.select_files_by_regexp.example.feature +71 -0
- data/vendor/behave/features/runner.select_files_by_regexp.feature +84 -0
- data/vendor/behave/features/runner.select_scenarios_by_file_location.feature +403 -0
- data/vendor/behave/features/runner.select_scenarios_by_name.feature +289 -0
- data/vendor/behave/features/runner.select_scenarios_by_tag.feature +225 -0
- data/vendor/behave/features/runner.stop_after_failure.feature +122 -0
- data/vendor/behave/features/runner.tag_logic.feature +67 -0
- data/vendor/behave/features/runner.unknown_formatter.feature +23 -0
- data/vendor/behave/features/runner.use_stage_implementations.feature +126 -0
- data/vendor/behave/features/scenario.description.feature +171 -0
- data/vendor/behave/features/scenario.exclude_from_run.feature +217 -0
- data/vendor/behave/features/scenario_outline.basics.feature +100 -0
- data/vendor/behave/features/scenario_outline.improved.feature +177 -0
- data/vendor/behave/features/scenario_outline.name_annotation.feature +157 -0
- data/vendor/behave/features/scenario_outline.parametrized.feature +401 -0
- data/vendor/behave/features/scenario_outline.tagged_examples.feature +118 -0
- data/vendor/behave/features/step.async_steps.feature +225 -0
- data/vendor/behave/features/step.duplicated_step.feature +106 -0
- data/vendor/behave/features/step.execute_steps.feature +59 -0
- data/vendor/behave/features/step.execute_steps.with_table.feature +65 -0
- data/vendor/behave/features/step.import_other_step_module.feature +103 -0
- data/vendor/behave/features/step.pending_steps.feature +128 -0
- data/vendor/behave/features/step.undefined_steps.feature +307 -0
- data/vendor/behave/features/step.use_step_library.feature +44 -0
- data/vendor/behave/features/step_dialect.generic_steps.feature +189 -0
- data/vendor/behave/features/step_dialect.given_when_then.feature +89 -0
- data/vendor/behave/features/step_param.builtin_types.with_float.feature +239 -0
- data/vendor/behave/features/step_param.builtin_types.with_integer.feature +305 -0
- data/vendor/behave/features/step_param.custom_types.feature +134 -0
- data/vendor/behave/features/steps/behave_active_tags_steps.py +86 -0
- data/vendor/behave/features/steps/behave_context_steps.py +67 -0
- data/vendor/behave/features/steps/behave_model_tag_logic_steps.py +105 -0
- data/vendor/behave/features/steps/behave_model_util.py +105 -0
- data/vendor/behave/features/steps/behave_select_files_steps.py +83 -0
- data/vendor/behave/features/steps/behave_tag_expression_steps.py +166 -0
- data/vendor/behave/features/steps/behave_undefined_steps.py +101 -0
- data/vendor/behave/features/steps/use_steplib_behave4cmd.py +12 -0
- data/vendor/behave/features/summary.undefined_steps.feature +114 -0
- data/vendor/behave/features/tags.active_tags.feature +385 -0
- data/vendor/behave/features/tags.default_tags.feature +104 -0
- data/vendor/behave/features/tags.tag_expression.feature +105 -0
- data/vendor/behave/features/userdata.feature +331 -0
- data/vendor/behave/invoke.yaml +21 -0
- data/vendor/behave/issue.features/README.txt +17 -0
- data/vendor/behave/issue.features/environment.py +97 -0
- data/vendor/behave/issue.features/issue0030.feature +21 -0
- data/vendor/behave/issue.features/issue0031.feature +16 -0
- data/vendor/behave/issue.features/issue0032.feature +28 -0
- data/vendor/behave/issue.features/issue0035.feature +74 -0
- data/vendor/behave/issue.features/issue0040.feature +154 -0
- data/vendor/behave/issue.features/issue0041.feature +135 -0
- data/vendor/behave/issue.features/issue0042.feature +230 -0
- data/vendor/behave/issue.features/issue0044.feature +51 -0
- data/vendor/behave/issue.features/issue0046.feature +77 -0
- data/vendor/behave/issue.features/issue0052.feature +66 -0
- data/vendor/behave/issue.features/issue0059.feature +29 -0
- data/vendor/behave/issue.features/issue0063.feature +102 -0
- data/vendor/behave/issue.features/issue0064.feature +97 -0
- data/vendor/behave/issue.features/issue0065.feature +18 -0
- data/vendor/behave/issue.features/issue0066.feature +80 -0
- data/vendor/behave/issue.features/issue0067.feature +90 -0
- data/vendor/behave/issue.features/issue0069.feature +64 -0
- data/vendor/behave/issue.features/issue0072.feature +32 -0
- data/vendor/behave/issue.features/issue0073.feature +228 -0
- data/vendor/behave/issue.features/issue0075.feature +18 -0
- data/vendor/behave/issue.features/issue0077.feature +89 -0
- data/vendor/behave/issue.features/issue0080.feature +49 -0
- data/vendor/behave/issue.features/issue0081.feature +138 -0
- data/vendor/behave/issue.features/issue0083.feature +69 -0
- data/vendor/behave/issue.features/issue0084.feature +69 -0
- data/vendor/behave/issue.features/issue0085.feature +119 -0
- data/vendor/behave/issue.features/issue0092.feature +66 -0
- data/vendor/behave/issue.features/issue0096.feature +173 -0
- data/vendor/behave/issue.features/issue0099.feature +130 -0
- data/vendor/behave/issue.features/issue0109.feature +60 -0
- data/vendor/behave/issue.features/issue0111.feature +53 -0
- data/vendor/behave/issue.features/issue0112.feature +64 -0
- data/vendor/behave/issue.features/issue0114.feature +118 -0
- data/vendor/behave/issue.features/issue0116.feature +71 -0
- data/vendor/behave/issue.features/issue0125.feature +49 -0
- data/vendor/behave/issue.features/issue0127.feature +64 -0
- data/vendor/behave/issue.features/issue0139.feature +67 -0
- data/vendor/behave/issue.features/issue0142.feature +37 -0
- data/vendor/behave/issue.features/issue0143.feature +54 -0
- data/vendor/behave/issue.features/issue0145.feature +63 -0
- data/vendor/behave/issue.features/issue0148.feature +105 -0
- data/vendor/behave/issue.features/issue0152.feature +52 -0
- data/vendor/behave/issue.features/issue0159.feature +74 -0
- data/vendor/behave/issue.features/issue0162.feature +86 -0
- data/vendor/behave/issue.features/issue0171.feature +16 -0
- data/vendor/behave/issue.features/issue0172.feature +51 -0
- data/vendor/behave/issue.features/issue0175.feature +91 -0
- data/vendor/behave/issue.features/issue0177.feature +40 -0
- data/vendor/behave/issue.features/issue0181.feature +36 -0
- data/vendor/behave/issue.features/issue0184.feature +144 -0
- data/vendor/behave/issue.features/issue0186.feature +12 -0
- data/vendor/behave/issue.features/issue0188.feature +60 -0
- data/vendor/behave/issue.features/issue0191.feature +178 -0
- data/vendor/behave/issue.features/issue0194.feature +215 -0
- data/vendor/behave/issue.features/issue0197.feature +11 -0
- data/vendor/behave/issue.features/issue0216.feature +129 -0
- data/vendor/behave/issue.features/issue0226.feature +51 -0
- data/vendor/behave/issue.features/issue0228.feature +41 -0
- data/vendor/behave/issue.features/issue0230.feature +46 -0
- data/vendor/behave/issue.features/issue0231.feature +77 -0
- data/vendor/behave/issue.features/issue0238.feature +52 -0
- data/vendor/behave/issue.features/issue0251.feature +15 -0
- data/vendor/behave/issue.features/issue0280.feature +118 -0
- data/vendor/behave/issue.features/issue0288.feature +95 -0
- data/vendor/behave/issue.features/issue0300.feature +49 -0
- data/vendor/behave/issue.features/issue0302.feature +91 -0
- data/vendor/behave/issue.features/issue0309.feature +52 -0
- data/vendor/behave/issue.features/issue0330.feature +124 -0
- data/vendor/behave/issue.features/issue0349.feature +9 -0
- data/vendor/behave/issue.features/issue0361.feature +79 -0
- data/vendor/behave/issue.features/issue0383.feature +76 -0
- data/vendor/behave/issue.features/issue0384.feature +103 -0
- data/vendor/behave/issue.features/issue0385.feature +109 -0
- data/vendor/behave/issue.features/issue0424.feature +66 -0
- data/vendor/behave/issue.features/issue0446.feature +116 -0
- data/vendor/behave/issue.features/issue0449.feature +42 -0
- data/vendor/behave/issue.features/issue0453.feature +42 -0
- data/vendor/behave/issue.features/issue0457.feature +65 -0
- data/vendor/behave/issue.features/issue0462.feature +38 -0
- data/vendor/behave/issue.features/issue0476.feature +39 -0
- data/vendor/behave/issue.features/issue0487.feature +92 -0
- data/vendor/behave/issue.features/issue0506.feature +77 -0
- data/vendor/behave/issue.features/issue0510.feature +51 -0
- data/vendor/behave/issue.features/requirements.txt +12 -0
- data/vendor/behave/issue.features/steps/ansi_steps.py +20 -0
- data/vendor/behave/issue.features/steps/behave_hooks_steps.py +10 -0
- data/vendor/behave/issue.features/steps/use_steplib_behave4cmd.py +13 -0
- data/vendor/behave/more.features/formatter.json.validate_output.feature +37 -0
- data/vendor/behave/more.features/steps/tutorial_steps.py +16 -0
- data/vendor/behave/more.features/steps/use_steplib_behave4cmd.py +7 -0
- data/vendor/behave/more.features/tutorial.feature +6 -0
- data/vendor/behave/py.requirements/README.txt +5 -0
- data/vendor/behave/py.requirements/all.txt +16 -0
- data/vendor/behave/py.requirements/basic.txt +21 -0
- data/vendor/behave/py.requirements/develop.txt +28 -0
- data/vendor/behave/py.requirements/docs.txt +6 -0
- data/vendor/behave/py.requirements/json.txt +7 -0
- data/vendor/behave/py.requirements/more_py26.txt +8 -0
- data/vendor/behave/py.requirements/testing.txt +10 -0
- data/vendor/behave/pytest.ini +24 -0
- data/vendor/behave/setup.cfg +29 -0
- data/vendor/behave/setup.py +118 -0
- data/vendor/behave/setuptools_behave.py +130 -0
- data/vendor/behave/tasks/__behave.py +45 -0
- data/vendor/behave/tasks/__init__.py +55 -0
- data/vendor/behave/tasks/__main__.py +70 -0
- data/vendor/behave/tasks/_setup.py +135 -0
- data/vendor/behave/tasks/_vendor/README.rst +35 -0
- data/vendor/behave/tasks/_vendor/invoke.zip +0 -0
- data/vendor/behave/tasks/_vendor/path.py +1725 -0
- data/vendor/behave/tasks/_vendor/pathlib.py +1280 -0
- data/vendor/behave/tasks/_vendor/six.py +868 -0
- data/vendor/behave/tasks/clean.py +246 -0
- data/vendor/behave/tasks/docs.py +97 -0
- data/vendor/behave/tasks/requirements.txt +17 -0
- data/vendor/behave/tasks/test.py +192 -0
- data/vendor/behave/test/__init__.py +0 -0
- data/vendor/behave/test/_importer_candidate.py +3 -0
- data/vendor/behave/test/reporters/__init__.py +0 -0
- data/vendor/behave/test/reporters/test_summary.py +240 -0
- data/vendor/behave/test/test_ansi_escapes.py +73 -0
- data/vendor/behave/test/test_configuration.py +172 -0
- data/vendor/behave/test/test_formatter.py +265 -0
- data/vendor/behave/test/test_formatter_progress.py +39 -0
- data/vendor/behave/test/test_formatter_rerun.py +97 -0
- data/vendor/behave/test/test_formatter_tags.py +57 -0
- data/vendor/behave/test/test_importer.py +151 -0
- data/vendor/behave/test/test_log_capture.py +29 -0
- data/vendor/behave/test/test_matchers.py +236 -0
- data/vendor/behave/test/test_model.py +871 -0
- data/vendor/behave/test/test_parser.py +1590 -0
- data/vendor/behave/test/test_runner.py +1074 -0
- data/vendor/behave/test/test_step_registry.py +96 -0
- data/vendor/behave/test/test_tag_expression.py +506 -0
- data/vendor/behave/test/test_tag_expression2.py +462 -0
- data/vendor/behave/test/test_tag_matcher.py +729 -0
- data/vendor/behave/test/test_userdata.py +184 -0
- data/vendor/behave/tests/README.txt +12 -0
- data/vendor/behave/tests/__init__.py +0 -0
- data/vendor/behave/tests/api/__ONLY_PY34_or_newer.txt +0 -0
- data/vendor/behave/tests/api/__init__.py +0 -0
- data/vendor/behave/tests/api/_test_async_step34.py +130 -0
- data/vendor/behave/tests/api/_test_async_step35.py +75 -0
- data/vendor/behave/tests/api/test_async_step.py +18 -0
- data/vendor/behave/tests/api/testing_support.py +94 -0
- data/vendor/behave/tests/api/testing_support_async.py +21 -0
- data/vendor/behave/tests/issues/test_issue0336.py +66 -0
- data/vendor/behave/tests/issues/test_issue0449.py +55 -0
- data/vendor/behave/tests/issues/test_issue0453.py +62 -0
- data/vendor/behave/tests/issues/test_issue0458.py +54 -0
- data/vendor/behave/tests/issues/test_issue0495.py +65 -0
- data/vendor/behave/tests/unit/__init__.py +0 -0
- data/vendor/behave/tests/unit/test_behave4cmd_command_shell_proc.py +135 -0
- data/vendor/behave/tests/unit/test_capture.py +280 -0
- data/vendor/behave/tests/unit/test_model_core.py +56 -0
- data/vendor/behave/tests/unit/test_textutil.py +267 -0
- data/vendor/behave/tools/test-features/background.feature +9 -0
- data/vendor/behave/tools/test-features/environment.py +8 -0
- data/vendor/behave/tools/test-features/french.feature +11 -0
- data/vendor/behave/tools/test-features/outline.feature +39 -0
- data/vendor/behave/tools/test-features/parse.feature +10 -0
- data/vendor/behave/tools/test-features/step-data.feature +60 -0
- data/vendor/behave/tools/test-features/steps/steps.py +120 -0
- data/vendor/behave/tools/test-features/tags.feature +18 -0
- data/vendor/behave/tox.ini +159 -0
- metadata +562 -0
@@ -0,0 +1,402 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
"""
|
3
|
+
This module provides the step matchers functionality that matches a
|
4
|
+
step definition (as text) with step-functions that implement this step.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import absolute_import, print_function, with_statement
|
8
|
+
import copy
|
9
|
+
import re
|
10
|
+
import parse
|
11
|
+
import six
|
12
|
+
from parse_type import cfparse
|
13
|
+
from behave._types import ChainedExceptionUtil, ExceptionUtil
|
14
|
+
from behave.model_core import Argument, FileLocation, Replayable
|
15
|
+
|
16
|
+
|
17
|
+
# -----------------------------------------------------------------------------
|
18
|
+
# SECTION: Exceptions
|
19
|
+
# -----------------------------------------------------------------------------
|
20
|
+
class StepParseError(ValueError):
|
21
|
+
"""Exception class, used when step matching fails before a step is run.
|
22
|
+
This is normally the case when an error occurs during the type conversion
|
23
|
+
of step parameters.
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, text=None, exc_cause=None):
|
27
|
+
if not text and exc_cause:
|
28
|
+
text = six.text_type(exc_cause)
|
29
|
+
if exc_cause and six.PY2:
|
30
|
+
# -- NOTE: Python2 does not show chained-exception causes.
|
31
|
+
# Therefore, provide some hint (see also: PEP-3134).
|
32
|
+
cause_text = ExceptionUtil.describe(exc_cause,
|
33
|
+
use_traceback=True,
|
34
|
+
prefix="CAUSED-BY: ")
|
35
|
+
text += u"\n" + cause_text
|
36
|
+
|
37
|
+
ValueError.__init__(self, text)
|
38
|
+
if exc_cause:
|
39
|
+
# -- CHAINED EXCEPTION (see: PEP 3134)
|
40
|
+
ChainedExceptionUtil.set_cause(self, exc_cause)
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
# -----------------------------------------------------------------------------
|
45
|
+
# SECTION: Model Elements
|
46
|
+
# -----------------------------------------------------------------------------
|
47
|
+
class Match(Replayable):
|
48
|
+
"""An parameter-matched *feature file* step name extracted using
|
49
|
+
step decorator `parameters`_.
|
50
|
+
|
51
|
+
.. attribute:: func
|
52
|
+
|
53
|
+
The step function that this match will be applied to.
|
54
|
+
|
55
|
+
.. attribute:: arguments
|
56
|
+
|
57
|
+
A list of :class:`~behave.model_core.Argument` instances containing the
|
58
|
+
matched parameters from the step name.
|
59
|
+
"""
|
60
|
+
type = "match"
|
61
|
+
|
62
|
+
def __init__(self, func, arguments=None):
|
63
|
+
super(Match, self).__init__()
|
64
|
+
self.func = func
|
65
|
+
self.arguments = arguments
|
66
|
+
self.location = None
|
67
|
+
if func:
|
68
|
+
self.location = self.make_location(func)
|
69
|
+
|
70
|
+
def __repr__(self):
|
71
|
+
if self.func:
|
72
|
+
func_name = self.func.__name__
|
73
|
+
else:
|
74
|
+
func_name = '<no function>'
|
75
|
+
return '<Match %s, %s>' % (func_name, self.location)
|
76
|
+
|
77
|
+
def __eq__(self, other):
|
78
|
+
if not isinstance(other, Match):
|
79
|
+
return False
|
80
|
+
return (self.func, self.location) == (other.func, other.location)
|
81
|
+
|
82
|
+
def with_arguments(self, arguments):
|
83
|
+
match = copy.copy(self)
|
84
|
+
match.arguments = arguments
|
85
|
+
return match
|
86
|
+
|
87
|
+
def run(self, context):
|
88
|
+
args = []
|
89
|
+
kwargs = {}
|
90
|
+
for arg in self.arguments:
|
91
|
+
if arg.name is not None:
|
92
|
+
kwargs[arg.name] = arg.value
|
93
|
+
else:
|
94
|
+
args.append(arg.value)
|
95
|
+
|
96
|
+
with context.use_with_user_mode():
|
97
|
+
self.func(context, *args, **kwargs)
|
98
|
+
|
99
|
+
@staticmethod
|
100
|
+
def make_location(step_function):
|
101
|
+
"""Extracts the location information from the step function and
|
102
|
+
builds a FileLocation object with (filename, line_number) info.
|
103
|
+
|
104
|
+
:param step_function: Function whose location should be determined.
|
105
|
+
:return: FileLocation object for step function.
|
106
|
+
"""
|
107
|
+
return FileLocation.for_function(step_function)
|
108
|
+
|
109
|
+
|
110
|
+
class NoMatch(Match):
|
111
|
+
"""Used for an "undefined step" when it can not be matched with a
|
112
|
+
step definition.
|
113
|
+
"""
|
114
|
+
|
115
|
+
def __init__(self):
|
116
|
+
Match.__init__(self, func=None)
|
117
|
+
self.func = None
|
118
|
+
self.arguments = []
|
119
|
+
self.location = None
|
120
|
+
|
121
|
+
|
122
|
+
class MatchWithError(Match):
|
123
|
+
"""Match class when error occur during step-matching
|
124
|
+
|
125
|
+
REASON:
|
126
|
+
* Type conversion error occured.
|
127
|
+
* ...
|
128
|
+
"""
|
129
|
+
def __init__(self, func, error):
|
130
|
+
if not ExceptionUtil.has_traceback(error):
|
131
|
+
ExceptionUtil.set_traceback(error)
|
132
|
+
Match.__init__(self, func=func)
|
133
|
+
self.stored_error = error
|
134
|
+
|
135
|
+
def run(self, context):
|
136
|
+
"""Raises stored error from step matching phase (type conversion)."""
|
137
|
+
raise StepParseError(exc_cause=self.stored_error)
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
|
142
|
+
# -----------------------------------------------------------------------------
|
143
|
+
# SECTION: Matchers
|
144
|
+
# -----------------------------------------------------------------------------
|
145
|
+
class Matcher(object):
|
146
|
+
"""Pull parameters out of step names.
|
147
|
+
|
148
|
+
.. attribute:: string
|
149
|
+
|
150
|
+
The match pattern attached to the step function.
|
151
|
+
|
152
|
+
.. attribute:: func
|
153
|
+
|
154
|
+
The step function the pattern is being attached to.
|
155
|
+
"""
|
156
|
+
schema = u"@%s('%s')" # Schema used to describe step definition (matcher)
|
157
|
+
|
158
|
+
def __init__(self, func, string, step_type=None):
|
159
|
+
self.func = func
|
160
|
+
self.string = string
|
161
|
+
self.step_type = step_type
|
162
|
+
self._location = None
|
163
|
+
|
164
|
+
@property
|
165
|
+
def location(self):
|
166
|
+
if self._location is None:
|
167
|
+
self._location = Match.make_location(self.func)
|
168
|
+
return self._location
|
169
|
+
|
170
|
+
def describe(self, schema=None):
|
171
|
+
"""Provide a textual description of the step function/matcher object.
|
172
|
+
|
173
|
+
:param schema: Text schema to use.
|
174
|
+
:return: Textual description of this step definition (matcher).
|
175
|
+
"""
|
176
|
+
step_type = self.step_type or "step"
|
177
|
+
if not schema:
|
178
|
+
schema = self.schema
|
179
|
+
return schema % (step_type, self.string)
|
180
|
+
|
181
|
+
|
182
|
+
def check_match(self, step):
|
183
|
+
"""Match me against the "step" name supplied.
|
184
|
+
|
185
|
+
Return None, if I don't match otherwise return a list of matches as
|
186
|
+
:class:`~behave.model_core.Argument` instances.
|
187
|
+
|
188
|
+
The return value from this function will be converted into a
|
189
|
+
:class:`~behave.matchers.Match` instance by *behave*.
|
190
|
+
"""
|
191
|
+
raise NotImplementedError
|
192
|
+
|
193
|
+
def match(self, step):
|
194
|
+
# -- PROTECT AGAINST: Type conversion errors (with ParseMatcher).
|
195
|
+
try:
|
196
|
+
result = self.check_match(step)
|
197
|
+
except Exception as e: # pylint: disable=broad-except
|
198
|
+
return MatchWithError(self.func, e)
|
199
|
+
|
200
|
+
if result is None:
|
201
|
+
return None # -- NO-MATCH
|
202
|
+
return Match(self.func, result)
|
203
|
+
|
204
|
+
def __repr__(self):
|
205
|
+
return u"<%s: %r>" % (self.__class__.__name__, self.string)
|
206
|
+
|
207
|
+
|
208
|
+
class ParseMatcher(Matcher):
|
209
|
+
custom_types = {}
|
210
|
+
|
211
|
+
def __init__(self, func, string, step_type=None):
|
212
|
+
super(ParseMatcher, self).__init__(func, string, step_type)
|
213
|
+
self.parser = parse.compile(self.string, self.custom_types)
|
214
|
+
|
215
|
+
def check_match(self, step):
|
216
|
+
# -- FAILURE-POINT: Type conversion of parameters may fail here.
|
217
|
+
# NOTE: Type converter should raise ValueError in case of PARSE ERRORS.
|
218
|
+
result = self.parser.parse(step)
|
219
|
+
if not result:
|
220
|
+
return None
|
221
|
+
|
222
|
+
args = []
|
223
|
+
for index, value in enumerate(result.fixed):
|
224
|
+
start, end = result.spans[index]
|
225
|
+
args.append(Argument(start, end, step[start:end], value))
|
226
|
+
for name, value in result.named.items():
|
227
|
+
start, end = result.spans[name]
|
228
|
+
args.append(Argument(start, end, step[start:end], value, name))
|
229
|
+
args.sort(key=lambda x: x.start)
|
230
|
+
return args
|
231
|
+
|
232
|
+
class CFParseMatcher(ParseMatcher):
|
233
|
+
"""
|
234
|
+
Uses :class:`~parse_type.cfparse.Parser` instead of "parse.Parser".
|
235
|
+
Provides support for automatic generation of type variants
|
236
|
+
for fields with CardinalityField part.
|
237
|
+
"""
|
238
|
+
def __init__(self, func, string, step_type=None):
|
239
|
+
super(CFParseMatcher, self).__init__(func, string, step_type)
|
240
|
+
self.parser = cfparse.Parser(self.string, self.custom_types)
|
241
|
+
|
242
|
+
|
243
|
+
def register_type(**kw):
|
244
|
+
# pylint: disable=anomalous-backslash-in-string
|
245
|
+
# REQUIRED-BY: code example
|
246
|
+
"""Registers a custom type that will be available to "parse"
|
247
|
+
for type conversion during step matching.
|
248
|
+
|
249
|
+
Converters should be supplied as ``name=callable`` arguments (or as dict).
|
250
|
+
|
251
|
+
A type converter should follow :pypi:`parse` module rules.
|
252
|
+
In general, a type converter is a function that converts text (as string)
|
253
|
+
into a value-type (type converted value).
|
254
|
+
|
255
|
+
EXAMPLE:
|
256
|
+
|
257
|
+
.. code-block:: python
|
258
|
+
|
259
|
+
from behave import register_type, given
|
260
|
+
import parse
|
261
|
+
|
262
|
+
# -- TYPE CONVERTER: For a simple, positive integer number.
|
263
|
+
@parse.with_pattern(r"\d+")
|
264
|
+
def parse_number(text):
|
265
|
+
return int(text)
|
266
|
+
|
267
|
+
# -- REGISTER TYPE-CONVERTER: With behave
|
268
|
+
register_type(Number=parse_number)
|
269
|
+
|
270
|
+
# -- STEP DEFINITIONS: Use type converter.
|
271
|
+
@given('{amount:Number} vehicles')
|
272
|
+
def step_impl(context, amount):
|
273
|
+
assert isinstance(amount, int)
|
274
|
+
"""
|
275
|
+
ParseMatcher.custom_types.update(kw)
|
276
|
+
|
277
|
+
|
278
|
+
class RegexMatcher(Matcher):
|
279
|
+
def __init__(self, func, string, step_type=None):
|
280
|
+
super(RegexMatcher, self).__init__(func, string, step_type)
|
281
|
+
self.regex = re.compile(self.string)
|
282
|
+
|
283
|
+
def check_match(self, step):
|
284
|
+
m = self.regex.match(step)
|
285
|
+
if not m:
|
286
|
+
return None
|
287
|
+
|
288
|
+
groupindex = dict((y, x) for x, y in self.regex.groupindex.items())
|
289
|
+
args = []
|
290
|
+
for index, group in enumerate(m.groups()):
|
291
|
+
index += 1
|
292
|
+
name = groupindex.get(index, None)
|
293
|
+
args.append(Argument(m.start(index), m.end(index), group,
|
294
|
+
group, name))
|
295
|
+
|
296
|
+
return args
|
297
|
+
|
298
|
+
class SimplifiedRegexMatcher(RegexMatcher):
|
299
|
+
"""Simplified regular expression step-matcher that automatically adds
|
300
|
+
start-of-line/end-of-line matcher symbols to string:
|
301
|
+
|
302
|
+
.. code-block:: python
|
303
|
+
|
304
|
+
@when(u'a step passes') # re.pattern = "^a step passes$"
|
305
|
+
def step_impl(context): pass
|
306
|
+
"""
|
307
|
+
|
308
|
+
def __init__(self, func, string, step_type=None):
|
309
|
+
assert not (string.startswith("^") or string.endswith("$")), \
|
310
|
+
"Regular expression should not use begin/end-markers: "+ string
|
311
|
+
expression = "^%s$" % string
|
312
|
+
super(SimplifiedRegexMatcher, self).__init__(func, expression, step_type)
|
313
|
+
self.string = string
|
314
|
+
|
315
|
+
|
316
|
+
class CucumberRegexMatcher(RegexMatcher):
|
317
|
+
"""Compatible to (old) Cucumber style regular expressions.
|
318
|
+
Text must contain start-of-line/end-of-line matcher symbols to string:
|
319
|
+
|
320
|
+
.. code-block:: python
|
321
|
+
|
322
|
+
@when(u'^a step passes$') # re.pattern = "^a step passes$"
|
323
|
+
def step_impl(context): pass
|
324
|
+
"""
|
325
|
+
|
326
|
+
matcher_mapping = {
|
327
|
+
"parse": ParseMatcher,
|
328
|
+
"cfparse": CFParseMatcher,
|
329
|
+
"re": SimplifiedRegexMatcher,
|
330
|
+
|
331
|
+
# -- BACKWARD-COMPATIBLE REGEX MATCHER: Old Cucumber compatible style.
|
332
|
+
# To make it the default step-matcher use the following snippet:
|
333
|
+
# # -- FILE: features/environment.py
|
334
|
+
# from behave import use_step_matcher
|
335
|
+
# def before_all(context):
|
336
|
+
# use_step_matcher("re0")
|
337
|
+
"re0": CucumberRegexMatcher,
|
338
|
+
}
|
339
|
+
current_matcher = ParseMatcher # pylint: disable=invalid-name
|
340
|
+
|
341
|
+
|
342
|
+
def use_step_matcher(name):
|
343
|
+
"""Change the parameter matcher used in parsing step text.
|
344
|
+
|
345
|
+
The change is immediate and may be performed between step definitions in
|
346
|
+
your step implementation modules - allowing adjacent steps to use different
|
347
|
+
matchers if necessary.
|
348
|
+
|
349
|
+
There are several parsers available in *behave* (by default):
|
350
|
+
|
351
|
+
**parse** (the default, based on: :pypi:`parse`)
|
352
|
+
Provides a simple parser that replaces regular expressions for
|
353
|
+
step parameters with a readable syntax like ``{param:Type}``.
|
354
|
+
The syntax is inspired by the Python builtin ``string.format()``
|
355
|
+
function.
|
356
|
+
Step parameters must use the named fields syntax of :pypi:`parse`
|
357
|
+
in step definitions. The named fields are extracted,
|
358
|
+
optionally type converted and then used as step function arguments.
|
359
|
+
|
360
|
+
Supports type conversions by using type converters
|
361
|
+
(see :func:`~behave.register_type()`).
|
362
|
+
|
363
|
+
**cfparse** (extends: :pypi:`parse`, requires: :pypi:`parse_type`)
|
364
|
+
Provides an extended parser with "Cardinality Field" (CF) support.
|
365
|
+
Automatically creates missing type converters for related cardinality
|
366
|
+
as long as a type converter for cardinality=1 is provided.
|
367
|
+
Supports parse expressions like:
|
368
|
+
|
369
|
+
* ``{values:Type+}`` (cardinality=1..N, many)
|
370
|
+
* ``{values:Type*}`` (cardinality=0..N, many0)
|
371
|
+
* ``{value:Type?}`` (cardinality=0..1, optional)
|
372
|
+
|
373
|
+
Supports type conversions (as above).
|
374
|
+
|
375
|
+
**re**
|
376
|
+
This uses full regular expressions to parse the clause text. You will
|
377
|
+
need to use named groups "(?P<name>...)" to define the variables pulled
|
378
|
+
from the text and passed to your ``step()`` function.
|
379
|
+
|
380
|
+
Type conversion is **not supported**.
|
381
|
+
A step function writer may implement type conversion
|
382
|
+
inside the step function (implementation).
|
383
|
+
|
384
|
+
You may `define your own matcher`_.
|
385
|
+
|
386
|
+
.. _`define your own matcher`: api.html#step-parameters
|
387
|
+
"""
|
388
|
+
global current_matcher # pylint: disable=global-statement
|
389
|
+
current_matcher = matcher_mapping[name]
|
390
|
+
|
391
|
+
def step_matcher(name):
|
392
|
+
"""
|
393
|
+
DEPRECATED, use :func:`use_step_matcher()` instead.
|
394
|
+
"""
|
395
|
+
# -- BACKWARD-COMPATIBLE NAME: Mark as deprecated.
|
396
|
+
import warnings
|
397
|
+
warnings.warn("Use 'use_step_matcher()' instead",
|
398
|
+
PendingDeprecationWarning, stacklevel=2)
|
399
|
+
use_step_matcher(name)
|
400
|
+
|
401
|
+
def get_matcher(func, string):
|
402
|
+
return current_matcher(func, string)
|
@@ -0,0 +1,1737 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# pylint: disable=too-many-lines
|
3
|
+
"""
|
4
|
+
This module provides the model element class that represent a behave model:
|
5
|
+
|
6
|
+
* :class:`Feature`
|
7
|
+
* :class:`Scenario`
|
8
|
+
* :class:`ScenarioOutline`
|
9
|
+
* :class:`Step`
|
10
|
+
* ...
|
11
|
+
"""
|
12
|
+
|
13
|
+
from __future__ import absolute_import, with_statement
|
14
|
+
import copy
|
15
|
+
import difflib
|
16
|
+
import logging
|
17
|
+
import itertools
|
18
|
+
import time
|
19
|
+
import six
|
20
|
+
from six.moves import zip # pylint: disable=redefined-builtin
|
21
|
+
from behave.model_core import \
|
22
|
+
Status, BasicStatement, TagAndStatusStatement, TagStatement, Replayable
|
23
|
+
from behave.matchers import NoMatch
|
24
|
+
from behave.textutil import text as _text
|
25
|
+
if six.PY2:
|
26
|
+
# -- USE PYTHON3 BACKPORT: With unicode traceback support.
|
27
|
+
import traceback2 as traceback
|
28
|
+
else:
|
29
|
+
import traceback
|
30
|
+
|
31
|
+
|
32
|
+
class Feature(TagAndStatusStatement, Replayable):
|
33
|
+
"""A `feature`_ parsed from a *feature file*.
|
34
|
+
|
35
|
+
The attributes are:
|
36
|
+
|
37
|
+
.. attribute:: keyword
|
38
|
+
|
39
|
+
This is the keyword as seen in the *feature file*. In English this will
|
40
|
+
be "Feature".
|
41
|
+
|
42
|
+
.. attribute:: name
|
43
|
+
|
44
|
+
The name of the feature (the text after "Feature".)
|
45
|
+
|
46
|
+
.. attribute:: description
|
47
|
+
|
48
|
+
The description of the feature as seen in the *feature file*. This is
|
49
|
+
stored as a list of text lines.
|
50
|
+
|
51
|
+
.. attribute:: background
|
52
|
+
|
53
|
+
The :class:`~behave.model.Background` for this feature, if any.
|
54
|
+
|
55
|
+
.. attribute:: scenarios
|
56
|
+
|
57
|
+
A list of :class:`~behave.model.Scenario` making up this feature.
|
58
|
+
|
59
|
+
.. attribute:: tags
|
60
|
+
|
61
|
+
A list of @tags (as :class:`~behave.model.Tag` which are basically
|
62
|
+
glorified strings) attached to the feature.
|
63
|
+
See :ref:`controlling things with tags`.
|
64
|
+
|
65
|
+
.. attribute:: status
|
66
|
+
|
67
|
+
Read-Only. A summary status of the feature's run. If read before the
|
68
|
+
feature is fully tested it will return "untested" otherwise it will
|
69
|
+
return one of:
|
70
|
+
|
71
|
+
Status.untested
|
72
|
+
The feature was has not been completely tested yet.
|
73
|
+
Status.skipped
|
74
|
+
One or more steps of this feature was passed over during testing.
|
75
|
+
Status.passed
|
76
|
+
The feature was tested successfully.
|
77
|
+
Status.failed
|
78
|
+
One or more steps of this feature failed.
|
79
|
+
|
80
|
+
.. versionchanged:: 1.2.6
|
81
|
+
Use Status enum class (was: string).
|
82
|
+
|
83
|
+
.. attribute:: hook_failed
|
84
|
+
|
85
|
+
Indicates if a hook failure occured while running this feature.
|
86
|
+
|
87
|
+
.. versionadded:: 1.2.6
|
88
|
+
|
89
|
+
.. attribute:: duration
|
90
|
+
|
91
|
+
The time, in seconds, that it took to test this feature. If read before
|
92
|
+
the feature is tested it will return 0.0.
|
93
|
+
|
94
|
+
.. attribute:: filename
|
95
|
+
|
96
|
+
The file name (or "<string>") of the *feature file* where the feature
|
97
|
+
was found.
|
98
|
+
|
99
|
+
.. attribute:: line
|
100
|
+
|
101
|
+
The line number of the *feature file* where the feature was found.
|
102
|
+
|
103
|
+
.. attribute:: language
|
104
|
+
|
105
|
+
Indicates which spoken language (English, French, German, ..) was used
|
106
|
+
for parsing the feature file and its keywords. The I18N language code
|
107
|
+
indicates which language is used. This corresponds to the language tag
|
108
|
+
at the beginning of the feature file.
|
109
|
+
|
110
|
+
.. versionadded:: 1.2.6
|
111
|
+
|
112
|
+
.. _`feature`: gherkin.html#features
|
113
|
+
"""
|
114
|
+
|
115
|
+
type = "feature"
|
116
|
+
|
117
|
+
def __init__(self, filename, line, keyword, name, tags=None,
|
118
|
+
description=None, scenarios=None, background=None,
|
119
|
+
language=None):
|
120
|
+
tags = tags or []
|
121
|
+
super(Feature, self).__init__(filename, line, keyword, name, tags)
|
122
|
+
self.description = description or []
|
123
|
+
self.scenarios = []
|
124
|
+
self.background = background
|
125
|
+
self.language = language
|
126
|
+
self.parser = None
|
127
|
+
self.hook_failed = False
|
128
|
+
if scenarios:
|
129
|
+
for scenario in scenarios:
|
130
|
+
self.add_scenario(scenario)
|
131
|
+
|
132
|
+
def reset(self):
|
133
|
+
"""Reset to clean state before a test run."""
|
134
|
+
super(Feature, self).reset()
|
135
|
+
self.hook_failed = False
|
136
|
+
for scenario in self.scenarios:
|
137
|
+
scenario.reset()
|
138
|
+
|
139
|
+
def __repr__(self):
|
140
|
+
return '<Feature "%s": %d scenario(s)>' % \
|
141
|
+
(self.name, len(self.scenarios))
|
142
|
+
|
143
|
+
def __iter__(self):
|
144
|
+
return iter(self.scenarios)
|
145
|
+
|
146
|
+
def add_scenario(self, scenario):
|
147
|
+
scenario.feature = self
|
148
|
+
scenario.background = self.background
|
149
|
+
self.scenarios.append(scenario)
|
150
|
+
|
151
|
+
def compute_status(self):
|
152
|
+
"""Compute the status of this feature based on its:
|
153
|
+
* scenarios
|
154
|
+
* scenario outlines
|
155
|
+
* hook failures
|
156
|
+
|
157
|
+
:return: Computed status (as string-enum).
|
158
|
+
"""
|
159
|
+
if self.hook_failed:
|
160
|
+
return Status.failed
|
161
|
+
|
162
|
+
skipped = True
|
163
|
+
passed_count = 0
|
164
|
+
for scenario in self.scenarios:
|
165
|
+
scenario_status = scenario.status
|
166
|
+
if scenario_status == Status.failed:
|
167
|
+
return Status.failed
|
168
|
+
elif scenario_status == Status.untested:
|
169
|
+
if passed_count > 0:
|
170
|
+
return Status.failed # ABORTED: Some passed, now untested.
|
171
|
+
return Status.untested
|
172
|
+
if scenario_status != Status.skipped:
|
173
|
+
skipped = False
|
174
|
+
if scenario_status == Status.passed:
|
175
|
+
passed_count += 1
|
176
|
+
|
177
|
+
if skipped:
|
178
|
+
return Status.skipped
|
179
|
+
else:
|
180
|
+
return Status.passed
|
181
|
+
|
182
|
+
|
183
|
+
@property
|
184
|
+
def duration(self):
|
185
|
+
# -- NEW: Background is executed N times, now part of scenarios.
|
186
|
+
feature_duration = 0.0
|
187
|
+
for scenario in self.scenarios:
|
188
|
+
feature_duration += scenario.duration
|
189
|
+
return feature_duration
|
190
|
+
|
191
|
+
def walk_scenarios(self, with_outlines=False):
|
192
|
+
"""
|
193
|
+
Provides a flat list of all scenarios of this feature.
|
194
|
+
A ScenarioOutline element adds its scenarios to this list.
|
195
|
+
But the ScenarioOutline element itself is only added when specified.
|
196
|
+
|
197
|
+
A flat scenario list is useful when all scenarios of a features
|
198
|
+
should be processed.
|
199
|
+
|
200
|
+
:param with_outlines: If ScenarioOutline items should be added, too.
|
201
|
+
:return: List of all scenarios of this feature.
|
202
|
+
"""
|
203
|
+
all_scenarios = []
|
204
|
+
for scenario in self.scenarios:
|
205
|
+
if isinstance(scenario, ScenarioOutline):
|
206
|
+
scenario_outline = scenario
|
207
|
+
if with_outlines:
|
208
|
+
all_scenarios.append(scenario_outline)
|
209
|
+
all_scenarios.extend(scenario_outline.scenarios)
|
210
|
+
else:
|
211
|
+
all_scenarios.append(scenario)
|
212
|
+
return all_scenarios
|
213
|
+
|
214
|
+
def should_run(self, config=None):
|
215
|
+
"""
|
216
|
+
Determines if this Feature (and its scenarios) should run.
|
217
|
+
Implements the run decision logic for a feature.
|
218
|
+
The decision depends on:
|
219
|
+
|
220
|
+
* if the Feature is marked as skipped
|
221
|
+
* if the config.tags (tag expression) enable/disable this feature
|
222
|
+
|
223
|
+
:param config: Runner configuration to use (optional).
|
224
|
+
:return: True, if scenario should run. False, otherwise.
|
225
|
+
"""
|
226
|
+
answer = not self.should_skip
|
227
|
+
if answer and config:
|
228
|
+
answer = self.should_run_with_tags(config.tags)
|
229
|
+
return answer
|
230
|
+
|
231
|
+
def should_run_with_tags(self, tag_expression):
|
232
|
+
"""Determines if this feature should run when the tag expression is used.
|
233
|
+
A feature should run if:
|
234
|
+
* it should run according to its tags
|
235
|
+
* any of its scenarios should run according to its tags
|
236
|
+
|
237
|
+
:param tag_expression: Runner/config environment tags to use.
|
238
|
+
:return: True, if feature should run. False, otherwise (skip it).
|
239
|
+
"""
|
240
|
+
run_feature = tag_expression.check(self.tags)
|
241
|
+
if not run_feature:
|
242
|
+
for scenario in self:
|
243
|
+
if scenario.should_run_with_tags(tag_expression):
|
244
|
+
run_feature = True
|
245
|
+
break
|
246
|
+
return run_feature
|
247
|
+
|
248
|
+
def mark_skipped(self):
|
249
|
+
"""Marks this feature (and all its scenarios and steps) as skipped.
|
250
|
+
Note this function may be called before the feature is executed.
|
251
|
+
"""
|
252
|
+
self.skip(require_not_executed=True)
|
253
|
+
assert self.status == Status.skipped or self.hook_failed
|
254
|
+
|
255
|
+
def skip(self, reason=None, require_not_executed=False):
|
256
|
+
"""Skip executing this feature or the remaining parts of it.
|
257
|
+
Note that this feature may be already partly executed
|
258
|
+
when this function is called.
|
259
|
+
|
260
|
+
:param reason: Optional reason why feature should be skipped (as string).
|
261
|
+
:param require_not_executed: Optional, requires that feature is not
|
262
|
+
executed yet (default: false).
|
263
|
+
"""
|
264
|
+
if reason:
|
265
|
+
logger = logging.getLogger("behave")
|
266
|
+
logger.warning(u"SKIP FEATURE %s: %s", self.name, reason)
|
267
|
+
|
268
|
+
self.clear_status()
|
269
|
+
self.should_skip = True
|
270
|
+
self.skip_reason = reason
|
271
|
+
for scenario in self.scenarios:
|
272
|
+
scenario.skip(reason, require_not_executed)
|
273
|
+
if not self.scenarios:
|
274
|
+
# -- SPECIAL CASE: Feature without scenarios
|
275
|
+
self.set_status(Status.skipped)
|
276
|
+
assert self.status in self.final_status #< skipped, failed or passed.
|
277
|
+
|
278
|
+
def run(self, runner):
|
279
|
+
# pylint: disable=too-many-branches
|
280
|
+
# MAYBE: self.reset()
|
281
|
+
self.clear_status()
|
282
|
+
self.hook_failed = False
|
283
|
+
|
284
|
+
runner.context._push() # pylint: disable=protected-access
|
285
|
+
runner.context.feature = self
|
286
|
+
runner.context.tags = set(self.tags)
|
287
|
+
|
288
|
+
skip_feature_untested = runner.aborted
|
289
|
+
run_feature = self.should_run(runner.config)
|
290
|
+
failed_count = 0
|
291
|
+
hooks_called = False
|
292
|
+
if not runner.config.dry_run and run_feature:
|
293
|
+
hooks_called = True
|
294
|
+
for tag in self.tags:
|
295
|
+
runner.run_hook("before_tag", runner.context, tag)
|
296
|
+
runner.run_hook("before_feature", runner.context, self)
|
297
|
+
if self.hook_failed:
|
298
|
+
failed_count += 1
|
299
|
+
|
300
|
+
# -- RE-EVALUATE SHOULD-RUN STATE:
|
301
|
+
# Hook may call feature.mark_skipped() to exclude it.
|
302
|
+
skip_feature_untested = self.hook_failed or runner.aborted
|
303
|
+
run_feature = self.should_run()
|
304
|
+
|
305
|
+
# run this feature if the tags say so or any one of its scenarios
|
306
|
+
if run_feature or runner.config.show_skipped:
|
307
|
+
for formatter in runner.formatters:
|
308
|
+
formatter.feature(self)
|
309
|
+
if self.background:
|
310
|
+
for formatter in runner.formatters:
|
311
|
+
formatter.background(self.background)
|
312
|
+
|
313
|
+
if not skip_feature_untested:
|
314
|
+
for scenario in self.scenarios:
|
315
|
+
# -- OPTIONAL: Select scenario by name (regular expressions).
|
316
|
+
if (runner.config.name and
|
317
|
+
not scenario.should_run_with_name_select(runner.config)):
|
318
|
+
scenario.mark_skipped()
|
319
|
+
continue
|
320
|
+
|
321
|
+
failed = scenario.run(runner)
|
322
|
+
if failed:
|
323
|
+
failed_count += 1
|
324
|
+
if runner.config.stop or runner.aborted:
|
325
|
+
# -- FAIL-EARLY: Stop after first failure.
|
326
|
+
break
|
327
|
+
|
328
|
+
self.clear_status() # -- ENFORCE: compute_status() after run.
|
329
|
+
if not self.scenarios and not run_feature:
|
330
|
+
# -- SPECIAL CASE: Feature without scenarios
|
331
|
+
self.set_status(Status.skipped)
|
332
|
+
|
333
|
+
if hooks_called:
|
334
|
+
runner.run_hook("after_feature", runner.context, self)
|
335
|
+
for tag in self.tags:
|
336
|
+
runner.run_hook("after_tag", runner.context, tag)
|
337
|
+
if self.hook_failed:
|
338
|
+
failed_count += 1
|
339
|
+
self.set_status(Status.failed)
|
340
|
+
|
341
|
+
runner.context._pop() # pylint: disable=protected-access
|
342
|
+
|
343
|
+
if run_feature or runner.config.show_skipped:
|
344
|
+
for formatter in runner.formatters:
|
345
|
+
formatter.eof()
|
346
|
+
|
347
|
+
failed = (failed_count > 0)
|
348
|
+
return failed
|
349
|
+
|
350
|
+
|
351
|
+
class Background(BasicStatement, Replayable):
|
352
|
+
"""A `background`_ parsed from a *feature file*.
|
353
|
+
|
354
|
+
The attributes are:
|
355
|
+
|
356
|
+
.. attribute:: keyword
|
357
|
+
|
358
|
+
This is the keyword as seen in the *feature file*. In English this will
|
359
|
+
typically be "Background".
|
360
|
+
|
361
|
+
.. attribute:: name
|
362
|
+
|
363
|
+
The name of the background (the text after "Background:".)
|
364
|
+
|
365
|
+
.. attribute:: steps
|
366
|
+
|
367
|
+
A list of :class:`~behave.model.Step` making up this background.
|
368
|
+
|
369
|
+
.. attribute:: duration
|
370
|
+
|
371
|
+
The time, in seconds, that it took to run this background. If read
|
372
|
+
before the background is run it will return 0.0.
|
373
|
+
|
374
|
+
.. attribute:: filename
|
375
|
+
|
376
|
+
The file name (or "<string>") of the *feature file* where the background
|
377
|
+
was found.
|
378
|
+
|
379
|
+
.. attribute:: line
|
380
|
+
|
381
|
+
The line number of the *feature file* where the background was found.
|
382
|
+
|
383
|
+
.. _`background`: gherkin.html#backgrounds
|
384
|
+
"""
|
385
|
+
type = "background"
|
386
|
+
|
387
|
+
def __init__(self, filename, line, keyword, name, steps=None):
|
388
|
+
super(Background, self).__init__(filename, line, keyword, name)
|
389
|
+
self.steps = steps or []
|
390
|
+
|
391
|
+
def __repr__(self):
|
392
|
+
return '<Background "%s">' % self.name
|
393
|
+
|
394
|
+
def __iter__(self):
|
395
|
+
return iter(self.steps)
|
396
|
+
|
397
|
+
@property
|
398
|
+
def duration(self):
|
399
|
+
duration = 0
|
400
|
+
for step in self.steps:
|
401
|
+
duration += step.duration
|
402
|
+
return duration
|
403
|
+
|
404
|
+
|
405
|
+
class Scenario(TagAndStatusStatement, Replayable):
|
406
|
+
"""A `scenario`_ parsed from a *feature file*.
|
407
|
+
|
408
|
+
The attributes are:
|
409
|
+
|
410
|
+
.. attribute:: keyword
|
411
|
+
|
412
|
+
This is the keyword as seen in the *feature file*. In English this will
|
413
|
+
typically be "Scenario".
|
414
|
+
|
415
|
+
.. attribute:: name
|
416
|
+
|
417
|
+
The name of the scenario (the text after "Scenario:".)
|
418
|
+
|
419
|
+
.. attribute:: description
|
420
|
+
|
421
|
+
The description of the scenario as seen in the *feature file*.
|
422
|
+
This is stored as a list of text lines.
|
423
|
+
|
424
|
+
.. attribute:: feature
|
425
|
+
|
426
|
+
The :class:`~behave.model.Feature` this scenario belongs to.
|
427
|
+
|
428
|
+
.. attribute:: steps
|
429
|
+
|
430
|
+
A list of :class:`~behave.model.Step` making up this scenario.
|
431
|
+
|
432
|
+
.. attribute:: tags
|
433
|
+
|
434
|
+
A list of @tags (as :class:`~behave.model.Tag` which are basically
|
435
|
+
glorified strings) attached to the scenario.
|
436
|
+
See :ref:`controlling things with tags`.
|
437
|
+
|
438
|
+
.. attribute:: status
|
439
|
+
|
440
|
+
Read-Only. A summary status of the scenario's run. If read before the
|
441
|
+
scenario is fully tested it will return "untested" otherwise it will
|
442
|
+
return one of:
|
443
|
+
|
444
|
+
|
445
|
+
Status.untested
|
446
|
+
The scenario was has not been completely tested yet.
|
447
|
+
Status.skipped
|
448
|
+
One or more steps of this scenario was passed over during testing.
|
449
|
+
Status.passed
|
450
|
+
The scenario was tested successfully.
|
451
|
+
Status.failed
|
452
|
+
One or more steps of this scenario failed.
|
453
|
+
|
454
|
+
.. versionchanged:: 1.2.6
|
455
|
+
Use Status enum class (was: string)
|
456
|
+
|
457
|
+
.. attribute:: hook_failed
|
458
|
+
|
459
|
+
Indicates if a hook failure occured while running this scenario.
|
460
|
+
|
461
|
+
.. versionadded:: 1.2.6
|
462
|
+
|
463
|
+
.. attribute:: duration
|
464
|
+
|
465
|
+
The time, in seconds, that it took to test this scenario. If read before
|
466
|
+
the scenario is tested it will return 0.0.
|
467
|
+
|
468
|
+
.. attribute:: filename
|
469
|
+
|
470
|
+
The file name (or "<string>") of the *feature file* where the scenario
|
471
|
+
was found.
|
472
|
+
|
473
|
+
.. attribute:: line
|
474
|
+
|
475
|
+
The line number of the *feature file* where the scenario was found.
|
476
|
+
|
477
|
+
|
478
|
+
.. _`scenario`: gherkin.html#scenarios
|
479
|
+
"""
|
480
|
+
# pylint: disable=too-many-instance-attributes
|
481
|
+
type = "scenario"
|
482
|
+
continue_after_failed_step = False
|
483
|
+
|
484
|
+
def __init__(self, filename, line, keyword, name, tags=None, steps=None,
|
485
|
+
description=None):
|
486
|
+
tags = tags or []
|
487
|
+
super(Scenario, self).__init__(filename, line, keyword, name, tags)
|
488
|
+
self.description = description or []
|
489
|
+
self.steps = steps or []
|
490
|
+
self.background = None
|
491
|
+
self.feature = None # REFER-TO: owner=Feature
|
492
|
+
self.hook_failed = False
|
493
|
+
self._background_steps = None
|
494
|
+
self._row = None
|
495
|
+
self.was_dry_run = False
|
496
|
+
|
497
|
+
def reset(self):
|
498
|
+
"""Reset the internal data to reintroduce new-born state just after the
|
499
|
+
ctor was called.
|
500
|
+
"""
|
501
|
+
super(Scenario, self).reset()
|
502
|
+
self.hook_failed = False
|
503
|
+
self._row = None
|
504
|
+
self.was_dry_run = False
|
505
|
+
for step in self.all_steps:
|
506
|
+
step.reset()
|
507
|
+
|
508
|
+
@property
|
509
|
+
def background_steps(self):
|
510
|
+
"""Provide background steps if feature has a background.
|
511
|
+
Lazy init that copies the background steps.
|
512
|
+
|
513
|
+
Note that a copy of the background steps is needed to ensure
|
514
|
+
that the background step status is specific to the scenario.
|
515
|
+
|
516
|
+
:return: List of background steps or empty list
|
517
|
+
"""
|
518
|
+
if self._background_steps is None:
|
519
|
+
# -- LAZY-INIT (need copy of background.steps):
|
520
|
+
# Each scenario needs own background.steps.
|
521
|
+
# Otherwise, background step status of the last-run scenario is used.
|
522
|
+
steps = []
|
523
|
+
if self.background:
|
524
|
+
steps = [copy.copy(step) for step in self.background.steps]
|
525
|
+
self._background_steps = steps
|
526
|
+
return self._background_steps
|
527
|
+
|
528
|
+
@property
|
529
|
+
def all_steps(self):
|
530
|
+
"""Returns iterator to all steps, including background steps if any."""
|
531
|
+
if self.background is not None:
|
532
|
+
return itertools.chain(self.background_steps, self.steps)
|
533
|
+
else:
|
534
|
+
return iter(self.steps)
|
535
|
+
|
536
|
+
def __repr__(self):
|
537
|
+
return '<Scenario "%s">' % self.name
|
538
|
+
|
539
|
+
def __iter__(self):
|
540
|
+
return self.all_steps
|
541
|
+
|
542
|
+
def compute_status(self):
|
543
|
+
"""Compute the status of the scenario from its steps
|
544
|
+
(and hook failures).
|
545
|
+
|
546
|
+
:return: Computed status (as enum value).
|
547
|
+
"""
|
548
|
+
if self.hook_failed:
|
549
|
+
return Status.failed
|
550
|
+
|
551
|
+
for step in self.all_steps:
|
552
|
+
if step.status == Status.undefined:
|
553
|
+
if self.was_dry_run:
|
554
|
+
# -- SPECIAL CASE: In dry-run with undefined-step discovery
|
555
|
+
# Undefined steps should not cause failed scenario.
|
556
|
+
return Status.untested
|
557
|
+
else:
|
558
|
+
# -- NORMALLY: Undefined steps cause failed scenario.
|
559
|
+
return Status.failed
|
560
|
+
elif step.status != Status.passed:
|
561
|
+
# pylint: disable=line-too-long
|
562
|
+
assert step.status in (Status.failed, Status.skipped, Status.untested)
|
563
|
+
return step.status
|
564
|
+
return Status.passed
|
565
|
+
|
566
|
+
@property
|
567
|
+
def duration(self):
|
568
|
+
# -- ORIG: for step in self.steps: Background steps were excluded.
|
569
|
+
scenario_duration = 0
|
570
|
+
for step in self.all_steps:
|
571
|
+
scenario_duration += step.duration
|
572
|
+
return scenario_duration
|
573
|
+
|
574
|
+
@property
|
575
|
+
def effective_tags(self):
|
576
|
+
"""
|
577
|
+
Effective tags for this scenario:
|
578
|
+
* own tags
|
579
|
+
* tags inherited from its feature
|
580
|
+
"""
|
581
|
+
tags = self.tags
|
582
|
+
if self.feature:
|
583
|
+
tags = self.feature.tags + self.tags
|
584
|
+
return tags
|
585
|
+
|
586
|
+
def should_run(self, config=None):
|
587
|
+
"""
|
588
|
+
Determines if this Scenario (or ScenarioOutline) should run.
|
589
|
+
Implements the run decision logic for a scenario.
|
590
|
+
The decision depends on:
|
591
|
+
|
592
|
+
* if the Scenario is marked as skipped
|
593
|
+
* if the config.tags (tag expression) enable/disable this scenario
|
594
|
+
* if the scenario is selected by name
|
595
|
+
|
596
|
+
:param config: Runner configuration to use (optional).
|
597
|
+
:return: True, if scenario should run. False, otherwise.
|
598
|
+
"""
|
599
|
+
answer = not self.should_skip
|
600
|
+
if answer and config:
|
601
|
+
answer = (self.should_run_with_tags(config.tags) and
|
602
|
+
self.should_run_with_name_select(config))
|
603
|
+
return answer
|
604
|
+
|
605
|
+
def should_run_with_tags(self, tag_expression):
|
606
|
+
"""
|
607
|
+
Determines if this scenario should run when the tag expression is used.
|
608
|
+
|
609
|
+
:param tag_expression: Runner/config environment tags to use.
|
610
|
+
:return: True, if scenario should run. False, otherwise (skip it).
|
611
|
+
"""
|
612
|
+
return tag_expression.check(self.effective_tags)
|
613
|
+
|
614
|
+
def should_run_with_name_select(self, config):
|
615
|
+
"""Determines if this scenario should run when it is selected by name.
|
616
|
+
|
617
|
+
:param config: Runner/config environment name regexp (if any).
|
618
|
+
:return: True, if scenario should run. False, otherwise (skip it).
|
619
|
+
"""
|
620
|
+
# -- SELECT-ANY: If select by name is not specified (not config.name).
|
621
|
+
return not config.name or config.name_re.search(self.name)
|
622
|
+
|
623
|
+
def mark_skipped(self):
|
624
|
+
"""Marks this scenario (and all its steps) as skipped.
|
625
|
+
Note that this method can be called before the scenario is executed.
|
626
|
+
"""
|
627
|
+
self.skip(require_not_executed=True)
|
628
|
+
assert self.status == Status.skipped or self.hook_failed, \
|
629
|
+
"OOPS: scenario.status=%s" % self.status.name
|
630
|
+
|
631
|
+
def skip(self, reason=None, require_not_executed=False):
|
632
|
+
"""Skip from executing this scenario or the remaining parts of it.
|
633
|
+
Note that the scenario may be already partly executed
|
634
|
+
when this method is called.
|
635
|
+
|
636
|
+
:param reason: Optional reason why it should be skipped (as string).
|
637
|
+
"""
|
638
|
+
if reason:
|
639
|
+
scenario_type = self.__class__.__name__
|
640
|
+
logger = logging.getLogger("behave")
|
641
|
+
logger.warning(u"SKIP %s %s: %s", scenario_type, self.name, reason)
|
642
|
+
|
643
|
+
self.clear_status()
|
644
|
+
self.should_skip = True
|
645
|
+
self.skip_reason = reason
|
646
|
+
for step in self.all_steps:
|
647
|
+
not_executed = step.status in (Status.untested, Status.skipped)
|
648
|
+
if not_executed:
|
649
|
+
step.status = Status.skipped
|
650
|
+
else:
|
651
|
+
assert not require_not_executed, \
|
652
|
+
"REQUIRE NOT-EXECUTED, but step is %s" % step.status
|
653
|
+
|
654
|
+
scenario_without_steps = not self.steps and not self.background_steps
|
655
|
+
if scenario_without_steps:
|
656
|
+
self.set_status(Status.skipped)
|
657
|
+
assert self.status in self.final_status #< skipped, failed or passed
|
658
|
+
|
659
|
+
def run(self, runner):
|
660
|
+
# pylint: disable=too-many-branches, too-many-statements
|
661
|
+
self.clear_status()
|
662
|
+
self.captured.reset()
|
663
|
+
self.hook_failed = False
|
664
|
+
failed = False
|
665
|
+
skip_scenario_untested = runner.aborted
|
666
|
+
run_scenario = self.should_run(runner.config)
|
667
|
+
run_steps = run_scenario and not runner.config.dry_run
|
668
|
+
dry_run_scenario = run_scenario and runner.config.dry_run
|
669
|
+
self.was_dry_run = dry_run_scenario
|
670
|
+
|
671
|
+
runner.context._push() # pylint: disable=protected-access
|
672
|
+
runner.context.scenario = self
|
673
|
+
runner.context.tags = set(self.effective_tags)
|
674
|
+
|
675
|
+
hooks_called = False
|
676
|
+
if not runner.config.dry_run and run_scenario:
|
677
|
+
hooks_called = True
|
678
|
+
for tag in self.tags:
|
679
|
+
runner.run_hook("before_tag", runner.context, tag)
|
680
|
+
runner.run_hook("before_scenario", runner.context, self)
|
681
|
+
if self.hook_failed:
|
682
|
+
# -- SKIP: Scenario steps and behave like dry_run_scenario
|
683
|
+
failed = True
|
684
|
+
|
685
|
+
# -- RE-EVALUATE SHOULD-RUN STATE:
|
686
|
+
# Hook may call scenario.mark_skipped() to exclude it.
|
687
|
+
skip_scenario_untested = self.hook_failed or runner.aborted
|
688
|
+
run_scenario = self.should_run()
|
689
|
+
run_steps = run_scenario and not runner.config.dry_run
|
690
|
+
|
691
|
+
if run_scenario or runner.config.show_skipped:
|
692
|
+
for formatter in runner.formatters:
|
693
|
+
formatter.scenario(self)
|
694
|
+
|
695
|
+
# TODO: Reevaluate location => Move in front of hook-calls
|
696
|
+
runner.setup_capture()
|
697
|
+
|
698
|
+
if run_scenario or runner.config.show_skipped:
|
699
|
+
for step in self:
|
700
|
+
for formatter in runner.formatters:
|
701
|
+
formatter.step(step)
|
702
|
+
|
703
|
+
if not skip_scenario_untested:
|
704
|
+
for step in self.all_steps:
|
705
|
+
if run_steps:
|
706
|
+
if not step.run(runner):
|
707
|
+
# -- CASE: Failed or undefined step
|
708
|
+
# Optionally continue_after_failed_step if enabled.
|
709
|
+
# But disable run_steps after undefined-step.
|
710
|
+
run_steps = (self.continue_after_failed_step and
|
711
|
+
step.status == Status.failed)
|
712
|
+
failed = True
|
713
|
+
# pylint: disable=protected-access
|
714
|
+
runner.context._set_root_attribute("failed", True)
|
715
|
+
self.set_status(Status.failed)
|
716
|
+
elif self.should_skip:
|
717
|
+
# -- CASE: Step skipped remaining scenario.
|
718
|
+
# assert self.status == Status.skipped
|
719
|
+
run_steps = False
|
720
|
+
elif failed or dry_run_scenario:
|
721
|
+
# -- SKIP STEPS: After failure/undefined-step occurred.
|
722
|
+
# BUT: Detect all remaining undefined steps.
|
723
|
+
step.status = Status.skipped
|
724
|
+
if dry_run_scenario:
|
725
|
+
# pylint: disable=redefined-variable-type
|
726
|
+
step.status = Status.untested
|
727
|
+
found_step_match = runner.step_registry.find_match(step)
|
728
|
+
if not found_step_match:
|
729
|
+
step.status = Status.undefined
|
730
|
+
runner.undefined_steps.append(step)
|
731
|
+
elif dry_run_scenario:
|
732
|
+
# -- BETTER DIAGNOSTICS: Provide step file location
|
733
|
+
# (when --format=pretty is used).
|
734
|
+
assert step.status == Status.untested
|
735
|
+
for formatter in runner.formatters:
|
736
|
+
# -- EMULATE: Step.run() protocol w/o step execution.
|
737
|
+
formatter.match(found_step_match)
|
738
|
+
formatter.result(step)
|
739
|
+
else:
|
740
|
+
# -- SKIP STEPS: For disabled scenario.
|
741
|
+
# CASES:
|
742
|
+
# * Undefined steps are not detected (by intention).
|
743
|
+
# * Step skipped remaining scenario.
|
744
|
+
step.status = Status.skipped
|
745
|
+
|
746
|
+
self.clear_status() # -- ENFORCE: compute_status() after run.
|
747
|
+
if not run_scenario and not self.steps:
|
748
|
+
# -- SPECIAL CASE: Scenario without steps.
|
749
|
+
self.set_status(Status.skipped)
|
750
|
+
|
751
|
+
|
752
|
+
if hooks_called:
|
753
|
+
runner.run_hook("after_scenario", runner.context, self)
|
754
|
+
for tag in self.tags:
|
755
|
+
runner.run_hook("after_tag", runner.context, tag)
|
756
|
+
if self.hook_failed:
|
757
|
+
failed = True
|
758
|
+
self.set_status(Status.failed)
|
759
|
+
|
760
|
+
# -- CAPTURED-OUTPUT:
|
761
|
+
store_captured = (runner.config.junit or self.status == Status.failed)
|
762
|
+
if store_captured:
|
763
|
+
self.captured = runner.capture_controller.captured
|
764
|
+
|
765
|
+
runner.teardown_capture()
|
766
|
+
runner.context._pop() # pylint: disable=protected-access
|
767
|
+
return failed
|
768
|
+
|
769
|
+
|
770
|
+
class ScenarioOutlineBuilder(object):
|
771
|
+
"""Helper class to use a ScenarioOutline as a template and
|
772
|
+
build its scenarios (as template instances).
|
773
|
+
"""
|
774
|
+
|
775
|
+
def __init__(self, annotation_schema):
|
776
|
+
self.annotation_schema = annotation_schema
|
777
|
+
|
778
|
+
@staticmethod
|
779
|
+
def render_template(text, row=None, params=None):
|
780
|
+
"""Render a text template with placeholders, ala "Hello <name>".
|
781
|
+
|
782
|
+
:param row: As placeholder provider (dict-like).
|
783
|
+
:param params: As additional placeholder provider (as dict).
|
784
|
+
:return: Rendered text, known placeholders are substituted w/ values.
|
785
|
+
"""
|
786
|
+
if not ("<" in text and ">" in text):
|
787
|
+
return text
|
788
|
+
|
789
|
+
safe_values = False
|
790
|
+
for placeholders in (row, params):
|
791
|
+
if not placeholders:
|
792
|
+
continue
|
793
|
+
for name, value in placeholders.items():
|
794
|
+
if safe_values and ("<" in value and ">" in value):
|
795
|
+
continue # -- OOPS, value looks like placeholder.
|
796
|
+
text = text.replace("<%s>" % name, value)
|
797
|
+
return text
|
798
|
+
|
799
|
+
def make_scenario_name(self, outline_name, example, row, params=None):
|
800
|
+
"""Build a scenario name for an example row of this scenario outline.
|
801
|
+
Placeholders for row data are replaced by values.
|
802
|
+
|
803
|
+
SCHEMA: "{outline_name} -*- {examples.name}@{row.id}"
|
804
|
+
|
805
|
+
:param outline_name: ScenarioOutline's name (as template).
|
806
|
+
:param example: Examples object.
|
807
|
+
:param row: Row of this example.
|
808
|
+
:param params: Additional placeholders for example/row.
|
809
|
+
:return: Computed name for the scenario representing example/row.
|
810
|
+
"""
|
811
|
+
if params is None:
|
812
|
+
params = {}
|
813
|
+
params["examples.name"] = example.name or ""
|
814
|
+
params.setdefault("examples.index", example.index)
|
815
|
+
params.setdefault("row.index", row.index)
|
816
|
+
params.setdefault("row.id", row.id)
|
817
|
+
|
818
|
+
# -- STEP: Replace placeholders in scenario/example name (if any).
|
819
|
+
examples_name = self.render_template(example.name, row, params)
|
820
|
+
params["examples.name"] = examples_name
|
821
|
+
scenario_name = self.render_template(outline_name, row, params)
|
822
|
+
|
823
|
+
class Data(object):
|
824
|
+
def __init__(self, name, index):
|
825
|
+
self.name = name
|
826
|
+
self.index = index
|
827
|
+
self.id = name # pylint: disable=invalid-name
|
828
|
+
|
829
|
+
example_data = Data(examples_name, example.index)
|
830
|
+
row_data = Data(row.id, row.index)
|
831
|
+
return self.annotation_schema.format(name=scenario_name,
|
832
|
+
examples=example_data, row=row_data)
|
833
|
+
|
834
|
+
@classmethod
|
835
|
+
def make_row_tags(cls, outline_tags, row, params=None):
|
836
|
+
if not outline_tags:
|
837
|
+
return []
|
838
|
+
|
839
|
+
tags = []
|
840
|
+
for tag in outline_tags:
|
841
|
+
if "<" in tag and ">" in tag:
|
842
|
+
tag = cls.render_template(tag, row, params)
|
843
|
+
if "<" in tag or ">" in tag:
|
844
|
+
# -- OOPS: Unknown placeholder, drop tag.
|
845
|
+
continue
|
846
|
+
new_tag = Tag.make_name(tag, unescape=True)
|
847
|
+
tags.append(new_tag)
|
848
|
+
return tags
|
849
|
+
|
850
|
+
@classmethod
|
851
|
+
def make_step_for_row(cls, outline_step, row, params=None):
|
852
|
+
# -- BASED-ON: new_step = outline_step.set_values(row)
|
853
|
+
new_step = copy.deepcopy(outline_step)
|
854
|
+
new_step.name = cls.render_template(new_step.name, row, params)
|
855
|
+
if new_step.text:
|
856
|
+
new_step.text = cls.render_template(new_step.text, row)
|
857
|
+
if new_step.table:
|
858
|
+
for name, value in row.items():
|
859
|
+
for row in new_step.table:
|
860
|
+
for i, cell in enumerate(row.cells):
|
861
|
+
row.cells[i] = cell.replace("<%s>" % name, value)
|
862
|
+
return new_step
|
863
|
+
|
864
|
+
def build_scenarios(self, scenario_outline):
|
865
|
+
"""Build scenarios for a ScenarioOutline from its examples."""
|
866
|
+
# -- BUILD SCENARIOS (once): For this ScenarioOutline from examples.
|
867
|
+
params = {
|
868
|
+
"examples.name": None,
|
869
|
+
"examples.index": None,
|
870
|
+
"row.index": None,
|
871
|
+
"row.id": None,
|
872
|
+
}
|
873
|
+
scenarios = []
|
874
|
+
for example_index, example in enumerate(scenario_outline.examples):
|
875
|
+
example.index = example_index+1
|
876
|
+
params["examples.name"] = example.name
|
877
|
+
params["examples.index"] = _text(example.index)
|
878
|
+
for row_index, row in enumerate(example.table):
|
879
|
+
row.index = row_index+1
|
880
|
+
row.id = "%d.%d" % (example.index, row.index)
|
881
|
+
params["row.id"] = row.id
|
882
|
+
params["row.index"] = _text(row.index)
|
883
|
+
scenario_name = self.make_scenario_name(scenario_outline.name,
|
884
|
+
example, row, params)
|
885
|
+
row_tags = self.make_row_tags(scenario_outline.tags, row, params)
|
886
|
+
row_tags.extend(example.tags)
|
887
|
+
new_steps = []
|
888
|
+
for outline_step in scenario_outline.steps:
|
889
|
+
new_step = self.make_step_for_row(outline_step, row, params)
|
890
|
+
new_steps.append(new_step)
|
891
|
+
|
892
|
+
# -- STEP: Make Scenario name for this row.
|
893
|
+
# scenario_line = example.line + 2 + row_index
|
894
|
+
scenario_line = row.line
|
895
|
+
scenario = Scenario(scenario_outline.filename, scenario_line,
|
896
|
+
scenario_outline.keyword,
|
897
|
+
scenario_name, row_tags, new_steps)
|
898
|
+
scenario.feature = scenario_outline.feature
|
899
|
+
scenario.background = scenario_outline.background
|
900
|
+
scenario._row = row # pylint: disable=protected-access
|
901
|
+
scenarios.append(scenario)
|
902
|
+
return scenarios
|
903
|
+
|
904
|
+
|
905
|
+
class ScenarioOutline(Scenario):
|
906
|
+
"""A `scenario outline`_ parsed from a *feature file*.
|
907
|
+
|
908
|
+
A scenario outline extends the existing :class:`~behave.model.Scenario`
|
909
|
+
class with the addition of the :class:`~behave.model.Examples` tables of
|
910
|
+
data from the *feature file*.
|
911
|
+
|
912
|
+
The attributes are:
|
913
|
+
|
914
|
+
.. attribute:: keyword
|
915
|
+
|
916
|
+
This is the keyword as seen in the *feature file*. In English this will
|
917
|
+
typically be "Scenario Outline".
|
918
|
+
|
919
|
+
.. attribute:: name
|
920
|
+
|
921
|
+
The name of the scenario (the text after "Scenario Outline:".)
|
922
|
+
|
923
|
+
.. attribute:: description
|
924
|
+
|
925
|
+
The description of the `scenario outline`_ as seen in the *feature file*.
|
926
|
+
This is stored as a list of text lines.
|
927
|
+
|
928
|
+
.. attribute:: feature
|
929
|
+
|
930
|
+
The :class:`~behave.model.Feature` this scenario outline belongs to.
|
931
|
+
|
932
|
+
.. attribute:: steps
|
933
|
+
|
934
|
+
A list of :class:`~behave.model.Step` making up this scenario outline.
|
935
|
+
|
936
|
+
.. attribute:: examples
|
937
|
+
|
938
|
+
A list of :class:`~behave.model.Examples` used by this scenario outline.
|
939
|
+
|
940
|
+
.. attribute:: tags
|
941
|
+
|
942
|
+
A list of @tags (as :class:`~behave.model.Tag` which are basically
|
943
|
+
glorified strings) attached to the scenario.
|
944
|
+
See :ref:`controlling things with tags`.
|
945
|
+
|
946
|
+
.. attribute:: status
|
947
|
+
|
948
|
+
Read-Only. A summary status of the scenario outlines's run. If read
|
949
|
+
before the scenario is fully tested it will return "untested" otherwise
|
950
|
+
it will return one of:
|
951
|
+
|
952
|
+
Status.untested
|
953
|
+
The scenario was has not been completely tested yet.
|
954
|
+
Status.skipped
|
955
|
+
One or more scenarios of this outline was passed over during testing.
|
956
|
+
Status.passed
|
957
|
+
The scenario was tested successfully.
|
958
|
+
Status.failed
|
959
|
+
One or more scenarios of this outline failed.
|
960
|
+
|
961
|
+
.. versionchanged:: 1.2.6
|
962
|
+
Use Status enum class (was: string)
|
963
|
+
|
964
|
+
.. attribute:: duration
|
965
|
+
|
966
|
+
The time, in seconds, that it took to test the scenarios of this
|
967
|
+
outline. If read before the scenarios are tested it will return 0.0.
|
968
|
+
|
969
|
+
.. attribute:: filename
|
970
|
+
|
971
|
+
The file name (or "<string>") of the *feature file* where the scenario
|
972
|
+
was found.
|
973
|
+
|
974
|
+
.. attribute:: line
|
975
|
+
|
976
|
+
The line number of the *feature file* where the scenario was found.
|
977
|
+
|
978
|
+
.. _`scenario outline`: gherkin.html#scenario-outlines
|
979
|
+
"""
|
980
|
+
type = "scenario_outline"
|
981
|
+
annotation_schema = u"{name} -- @{row.id} {examples.name}"
|
982
|
+
|
983
|
+
def __init__(self, filename, line, keyword, name, tags=None,
|
984
|
+
steps=None, examples=None, description=None):
|
985
|
+
super(ScenarioOutline, self).__init__(filename, line, keyword, name,
|
986
|
+
tags, steps, description)
|
987
|
+
self.examples = examples or []
|
988
|
+
self._scenarios = []
|
989
|
+
|
990
|
+
def reset(self):
|
991
|
+
"""Reset runtime temporary data like before a test run."""
|
992
|
+
super(ScenarioOutline, self).reset()
|
993
|
+
for scenario in self._scenarios: # -- AVOID: BUILD-SCENARIOS
|
994
|
+
scenario.reset()
|
995
|
+
|
996
|
+
@property
|
997
|
+
def scenarios(self):
|
998
|
+
"""Return the scenarios with the steps altered to take the values from
|
999
|
+
the examples.
|
1000
|
+
"""
|
1001
|
+
if self._scenarios:
|
1002
|
+
return self._scenarios
|
1003
|
+
|
1004
|
+
# -- BUILD SCENARIOS (once): For this ScenarioOutline from examples.
|
1005
|
+
builder = ScenarioOutlineBuilder(self.annotation_schema)
|
1006
|
+
self._scenarios = builder.build_scenarios(self)
|
1007
|
+
return self._scenarios
|
1008
|
+
|
1009
|
+
def __repr__(self):
|
1010
|
+
return '<ScenarioOutline "%s">' % self.name
|
1011
|
+
|
1012
|
+
def __iter__(self):
|
1013
|
+
return iter(self.scenarios) # -- REQUIRE: BUILD-SCENARIOS
|
1014
|
+
|
1015
|
+
def compute_status(self):
|
1016
|
+
skipped_count = 0
|
1017
|
+
for scenario in self._scenarios: # -- AVOID: BUILD-SCENARIOS
|
1018
|
+
scenario_status = scenario.status
|
1019
|
+
if scenario_status in (Status.failed, Status.untested):
|
1020
|
+
return scenario_status
|
1021
|
+
elif scenario_status == Status.skipped:
|
1022
|
+
skipped_count += 1
|
1023
|
+
if skipped_count > 0 and skipped_count == len(self._scenarios):
|
1024
|
+
# -- ALL SKIPPED:
|
1025
|
+
return Status.skipped
|
1026
|
+
# -- OTHERWISE: ALL PASSED (some scenarios may have been excluded)
|
1027
|
+
return Status.passed
|
1028
|
+
|
1029
|
+
@property
|
1030
|
+
def duration(self):
|
1031
|
+
outline_duration = 0
|
1032
|
+
for scenario in self._scenarios: # -- AVOID: BUILD-SCENARIOS
|
1033
|
+
outline_duration += scenario.duration
|
1034
|
+
return outline_duration
|
1035
|
+
|
1036
|
+
def should_run_with_tags(self, tag_expression):
|
1037
|
+
"""Determines if this scenario outline (or one of its scenarios)
|
1038
|
+
should run when the tag expression is used.
|
1039
|
+
|
1040
|
+
:param tag_expression: Runner/config environment tags to use.
|
1041
|
+
:return: True, if scenario should run. False, otherwise (skip it).
|
1042
|
+
"""
|
1043
|
+
if tag_expression.check(self.effective_tags):
|
1044
|
+
return True
|
1045
|
+
|
1046
|
+
for scenario in self.scenarios: # -- REQUIRE: BUILD-SCENARIOS
|
1047
|
+
if scenario.should_run_with_tags(tag_expression):
|
1048
|
+
return True
|
1049
|
+
# -- NOTHING SELECTED:
|
1050
|
+
return False
|
1051
|
+
|
1052
|
+
def should_run_with_name_select(self, config):
|
1053
|
+
"""Determines if this scenario should run when it is selected by name.
|
1054
|
+
|
1055
|
+
:param config: Runner/config environment name regexp (if any).
|
1056
|
+
:return: True, if scenario should run. False, otherwise (skip it).
|
1057
|
+
"""
|
1058
|
+
if not config.name:
|
1059
|
+
return True # -- SELECT-ALL: Select by name is not specified.
|
1060
|
+
|
1061
|
+
for scenario in self.scenarios: # -- REQUIRE: BUILD-SCENARIOS
|
1062
|
+
if scenario.should_run_with_name_select(config):
|
1063
|
+
return True
|
1064
|
+
# -- NOTHING SELECTED:
|
1065
|
+
return False
|
1066
|
+
|
1067
|
+
|
1068
|
+
def mark_skipped(self):
|
1069
|
+
"""Marks this scenario outline (and all its scenarios/steps) as skipped.
|
1070
|
+
Note that this method may be called before the scenario outline
|
1071
|
+
is executed.
|
1072
|
+
"""
|
1073
|
+
self.skip(require_not_executed=True)
|
1074
|
+
assert self.status == Status.skipped
|
1075
|
+
|
1076
|
+
def skip(self, reason=None, require_not_executed=False):
|
1077
|
+
"""Skip from executing this scenario outline or its remaining parts.
|
1078
|
+
Note that the scenario outline may be already partly executed
|
1079
|
+
when this method is called.
|
1080
|
+
|
1081
|
+
:param reason: Optional reason why it should be skipped (as string).
|
1082
|
+
"""
|
1083
|
+
if reason:
|
1084
|
+
logger = logging.getLogger("behave")
|
1085
|
+
logger.warning(u"SKIP ScenarioOutline %s: %s", self.name, reason)
|
1086
|
+
|
1087
|
+
self.clear_status()
|
1088
|
+
self.should_skip = True
|
1089
|
+
for scenario in self.scenarios:
|
1090
|
+
scenario.skip(reason, require_not_executed)
|
1091
|
+
if not self.scenarios:
|
1092
|
+
# -- SPECIAL CASE: ScenarioOutline without scenarios/examples
|
1093
|
+
self.set_status(Status.skipped)
|
1094
|
+
assert self.status in self.final_status #< skipped, failed or passed
|
1095
|
+
|
1096
|
+
def run(self, runner):
|
1097
|
+
# pylint: disable=protected-access
|
1098
|
+
# REASON: context._set_root_attribute(), scenario._row
|
1099
|
+
self.clear_status()
|
1100
|
+
failed_count = 0
|
1101
|
+
for scenario in self.scenarios: # -- REQUIRE: BUILD-SCENARIOS
|
1102
|
+
runner.context._set_root_attribute("active_outline", scenario._row)
|
1103
|
+
failed = scenario.run(runner)
|
1104
|
+
if failed:
|
1105
|
+
failed_count += 1
|
1106
|
+
if runner.config.stop or runner.aborted:
|
1107
|
+
# -- FAIL-EARLY: Stop after first failure.
|
1108
|
+
break
|
1109
|
+
runner.context._set_root_attribute("active_outline", None)
|
1110
|
+
return failed_count > 0
|
1111
|
+
|
1112
|
+
class Examples(TagStatement, Replayable):
|
1113
|
+
"""A table parsed from a `scenario outline`_ in a *feature file*.
|
1114
|
+
|
1115
|
+
The attributes are:
|
1116
|
+
|
1117
|
+
.. attribute:: keyword
|
1118
|
+
|
1119
|
+
This is the keyword as seen in the *feature file*. In English this will
|
1120
|
+
typically be "Example".
|
1121
|
+
|
1122
|
+
.. attribute:: name
|
1123
|
+
|
1124
|
+
The name of the example (the text after "Example:".)
|
1125
|
+
|
1126
|
+
.. attribute:: table
|
1127
|
+
|
1128
|
+
An instance of :class:`~behave.model.Table` that came with the example
|
1129
|
+
in the *feature file*.
|
1130
|
+
|
1131
|
+
.. attribute:: filename
|
1132
|
+
|
1133
|
+
The file name (or "<string>") of the *feature file* where the example
|
1134
|
+
was found.
|
1135
|
+
|
1136
|
+
.. attribute:: line
|
1137
|
+
|
1138
|
+
The line number of the *feature file* where the example was found.
|
1139
|
+
|
1140
|
+
.. _`examples`: gherkin.html#examples
|
1141
|
+
"""
|
1142
|
+
type = "examples"
|
1143
|
+
|
1144
|
+
def __init__(self, filename, line, keyword, name, tags=None, table=None):
|
1145
|
+
super(Examples, self).__init__(filename, line, keyword, name, tags)
|
1146
|
+
self.table = table
|
1147
|
+
self.index = None
|
1148
|
+
|
1149
|
+
|
1150
|
+
class Step(BasicStatement, Replayable):
|
1151
|
+
"""A single `step`_ parsed from a *feature file*.
|
1152
|
+
|
1153
|
+
The attributes are:
|
1154
|
+
|
1155
|
+
.. attribute:: keyword
|
1156
|
+
|
1157
|
+
This is the keyword as seen in the *feature file*. In English this will
|
1158
|
+
typically be "Given", "When", "Then" or a number of other words.
|
1159
|
+
|
1160
|
+
.. attribute:: name
|
1161
|
+
|
1162
|
+
The name of the step (the text after "Given" etc.)
|
1163
|
+
|
1164
|
+
.. attribute:: step_type
|
1165
|
+
|
1166
|
+
The type of step as determined by the keyword. If the keyword is "and"
|
1167
|
+
then the previous keyword in the *feature file* will determine this
|
1168
|
+
step's step_type.
|
1169
|
+
|
1170
|
+
.. attribute:: text
|
1171
|
+
|
1172
|
+
An instance of :class:`~behave.model.Text` that came with the step
|
1173
|
+
in the *feature file*.
|
1174
|
+
|
1175
|
+
.. attribute:: table
|
1176
|
+
|
1177
|
+
An instance of :class:`~behave.model.Table` that came with the step
|
1178
|
+
in the *feature file*.
|
1179
|
+
|
1180
|
+
.. attribute:: status
|
1181
|
+
|
1182
|
+
Read-Only. A summary status of the step's run. If read before the
|
1183
|
+
step is tested it will return "untested" otherwise it will
|
1184
|
+
return one of:
|
1185
|
+
|
1186
|
+
Status.untested
|
1187
|
+
This step was not run (yet).
|
1188
|
+
Status.skipped
|
1189
|
+
This step was skipped during testing.
|
1190
|
+
Status.passed
|
1191
|
+
The step was tested successfully.
|
1192
|
+
Status.failed
|
1193
|
+
The step failed.
|
1194
|
+
Status.undefined
|
1195
|
+
The step has no matching step implementation.
|
1196
|
+
|
1197
|
+
.. versionchanged::
|
1198
|
+
Use Status enum class (was: string).
|
1199
|
+
|
1200
|
+
.. attribute:: hook_failed
|
1201
|
+
|
1202
|
+
Indicates if a hook failure occured while running this step.
|
1203
|
+
|
1204
|
+
.. versionadded:: 1.2.6
|
1205
|
+
|
1206
|
+
.. attribute:: duration
|
1207
|
+
|
1208
|
+
The time, in seconds, that it took to test this step. If read before the
|
1209
|
+
step is tested it will return 0.0.
|
1210
|
+
|
1211
|
+
.. attribute:: error_message
|
1212
|
+
|
1213
|
+
If the step failed then this will hold any error information, as a
|
1214
|
+
single string. It will otherwise be None.
|
1215
|
+
|
1216
|
+
.. versionchanged:: 1.2.6 (moved to base class)
|
1217
|
+
|
1218
|
+
.. attribute:: filename
|
1219
|
+
|
1220
|
+
The file name (or "<string>") of the *feature file* where the step was
|
1221
|
+
found.
|
1222
|
+
|
1223
|
+
.. attribute:: line
|
1224
|
+
|
1225
|
+
The line number of the *feature file* where the step was found.
|
1226
|
+
|
1227
|
+
.. _`step`: gherkin.html#steps
|
1228
|
+
"""
|
1229
|
+
type = "step"
|
1230
|
+
|
1231
|
+
def __init__(self, filename, line, keyword, step_type, name, text=None,
|
1232
|
+
table=None):
|
1233
|
+
super(Step, self).__init__(filename, line, keyword, name)
|
1234
|
+
self.step_type = step_type
|
1235
|
+
self.text = text
|
1236
|
+
self.table = table
|
1237
|
+
|
1238
|
+
self.status = Status.untested
|
1239
|
+
self.hook_failed = False
|
1240
|
+
self.duration = 0
|
1241
|
+
|
1242
|
+
def reset(self):
|
1243
|
+
"""Reset temporary runtime data to reach clean state again."""
|
1244
|
+
super(Step, self).reset()
|
1245
|
+
self.status = Status.untested
|
1246
|
+
self.hook_failed = False
|
1247
|
+
self.duration = 0
|
1248
|
+
# -- POSTCONDITION: assert self.status == Status.untested
|
1249
|
+
|
1250
|
+
def __repr__(self):
|
1251
|
+
return '<%s "%s">' % (self.step_type, self.name)
|
1252
|
+
|
1253
|
+
def __eq__(self, other):
|
1254
|
+
return (self.step_type, self.name) == (other.step_type, other.name)
|
1255
|
+
|
1256
|
+
def __hash__(self):
|
1257
|
+
return hash(self.step_type) + hash(self.name)
|
1258
|
+
|
1259
|
+
def set_values(self, table_row):
|
1260
|
+
"""Clone a new step from this one, used for ScenarioOutline.
|
1261
|
+
Replace ScenarioOutline placeholders w/ values.
|
1262
|
+
|
1263
|
+
:param table_row: Placeholder data for example row.
|
1264
|
+
:return: Cloned, adapted step object.
|
1265
|
+
|
1266
|
+
.. note:: Deprecating
|
1267
|
+
Use 'ScenarioOutlineBuilder.make_step_for_row()' instead.
|
1268
|
+
"""
|
1269
|
+
import warnings
|
1270
|
+
warnings.warn("Use 'ScenarioOutline.make_step_for_row()' instead",
|
1271
|
+
PendingDeprecationWarning, stacklevel=2)
|
1272
|
+
outline_step = self
|
1273
|
+
return ScenarioOutlineBuilder.make_step_for_row(outline_step, table_row)
|
1274
|
+
|
1275
|
+
def run(self, runner, quiet=False, capture=True):
|
1276
|
+
# pylint: disable=too-many-branches, too-many-statements
|
1277
|
+
# -- RESET: Run-time information.
|
1278
|
+
# self.status = Status.untested
|
1279
|
+
# self.hook_failed = False
|
1280
|
+
self.reset()
|
1281
|
+
|
1282
|
+
match = runner.step_registry.find_match(self)
|
1283
|
+
if match is None:
|
1284
|
+
runner.undefined_steps.append(self)
|
1285
|
+
if not quiet:
|
1286
|
+
for formatter in runner.formatters:
|
1287
|
+
formatter.match(NoMatch())
|
1288
|
+
|
1289
|
+
self.status = Status.undefined
|
1290
|
+
if not quiet:
|
1291
|
+
for formatter in runner.formatters:
|
1292
|
+
formatter.result(self)
|
1293
|
+
return False
|
1294
|
+
|
1295
|
+
keep_going = True
|
1296
|
+
error = u""
|
1297
|
+
|
1298
|
+
if not quiet:
|
1299
|
+
for formatter in runner.formatters:
|
1300
|
+
formatter.match(match)
|
1301
|
+
|
1302
|
+
if capture:
|
1303
|
+
runner.start_capture()
|
1304
|
+
|
1305
|
+
skip_step_untested = False
|
1306
|
+
runner.run_hook("before_step", runner.context, self)
|
1307
|
+
if self.hook_failed:
|
1308
|
+
skip_step_untested = True
|
1309
|
+
|
1310
|
+
start = time.time()
|
1311
|
+
if not skip_step_untested:
|
1312
|
+
try:
|
1313
|
+
# -- ENSURE:
|
1314
|
+
# * runner.context.text/.table attributes are reset (#66).
|
1315
|
+
# * Even EMPTY multiline text is available in context.
|
1316
|
+
runner.context.text = self.text
|
1317
|
+
runner.context.table = self.table
|
1318
|
+
match.run(runner.context)
|
1319
|
+
if self.status == Status.untested:
|
1320
|
+
# -- NOTE: Executed step may have skipped scenario and itself.
|
1321
|
+
# pylint: disable=redefined-variable-type
|
1322
|
+
self.status = Status.passed
|
1323
|
+
except KeyboardInterrupt as e:
|
1324
|
+
runner.aborted = True
|
1325
|
+
error = u"ABORTED: By user (KeyboardInterrupt)."
|
1326
|
+
self.status = Status.failed
|
1327
|
+
self.store_exception_context(e)
|
1328
|
+
except AssertionError as e:
|
1329
|
+
self.status = Status.failed
|
1330
|
+
self.store_exception_context(e)
|
1331
|
+
if e.args:
|
1332
|
+
message = _text(e)
|
1333
|
+
error = u"Assertion Failed: "+ message
|
1334
|
+
else:
|
1335
|
+
# no assertion text; format the exception
|
1336
|
+
error = _text(traceback.format_exc())
|
1337
|
+
except Exception as e: # pylint: disable=broad-except
|
1338
|
+
self.status = Status.failed
|
1339
|
+
error = _text(traceback.format_exc())
|
1340
|
+
self.store_exception_context(e)
|
1341
|
+
|
1342
|
+
self.duration = time.time() - start
|
1343
|
+
runner.run_hook("after_step", runner.context, self)
|
1344
|
+
if self.hook_failed:
|
1345
|
+
self.status = Status.failed
|
1346
|
+
|
1347
|
+
if capture:
|
1348
|
+
runner.stop_capture()
|
1349
|
+
|
1350
|
+
# flesh out the failure with details
|
1351
|
+
store_captured_always = False # PREPARED
|
1352
|
+
store_captured = self.status == Status.failed or store_captured_always
|
1353
|
+
if self.status == Status.failed:
|
1354
|
+
assert isinstance(error, six.text_type)
|
1355
|
+
if capture:
|
1356
|
+
# -- CAPTURE-ONLY: Non-nested step failures.
|
1357
|
+
self.captured = runner.capture_controller.captured
|
1358
|
+
error2 = self.captured.make_report()
|
1359
|
+
if error2:
|
1360
|
+
error += "\n" + error2
|
1361
|
+
self.error_message = error
|
1362
|
+
keep_going = False
|
1363
|
+
elif store_captured and capture:
|
1364
|
+
self.captured = runner.capture_controller.captured
|
1365
|
+
|
1366
|
+
if not quiet:
|
1367
|
+
for formatter in runner.formatters:
|
1368
|
+
formatter.result(self)
|
1369
|
+
|
1370
|
+
return keep_going
|
1371
|
+
|
1372
|
+
|
1373
|
+
class Table(Replayable):
|
1374
|
+
"""A `table`_ extracted from a *feature file*.
|
1375
|
+
|
1376
|
+
Table instance data is accessible using a number of methods:
|
1377
|
+
|
1378
|
+
**iteration**
|
1379
|
+
Iterating over the Table will yield the :class:`~behave.model.Row`
|
1380
|
+
instances from the .rows attribute.
|
1381
|
+
|
1382
|
+
**indexed access**
|
1383
|
+
Individual rows may be accessed directly by index on the Table instance;
|
1384
|
+
table[0] gives the first non-heading row and table[-1] gives the last
|
1385
|
+
row.
|
1386
|
+
|
1387
|
+
The attributes are:
|
1388
|
+
|
1389
|
+
.. attribute:: headings
|
1390
|
+
|
1391
|
+
The headings of the table as a list of strings.
|
1392
|
+
|
1393
|
+
.. attribute:: rows
|
1394
|
+
|
1395
|
+
An list of instances of :class:`~behave.model.Row` that make up the body
|
1396
|
+
of the table in the *feature file*.
|
1397
|
+
|
1398
|
+
Tables are also comparable, for what that's worth. Headings and row data
|
1399
|
+
are compared.
|
1400
|
+
|
1401
|
+
.. _`table`: gherkin.html#table
|
1402
|
+
"""
|
1403
|
+
type = "table"
|
1404
|
+
|
1405
|
+
def __init__(self, headings, line=None, rows=None):
|
1406
|
+
Replayable.__init__(self)
|
1407
|
+
self.headings = headings
|
1408
|
+
self.line = line
|
1409
|
+
self.rows = []
|
1410
|
+
if rows:
|
1411
|
+
for row in rows:
|
1412
|
+
self.add_row(row, line)
|
1413
|
+
|
1414
|
+
def add_row(self, row, line=None):
|
1415
|
+
self.rows.append(Row(self.headings, row, line))
|
1416
|
+
|
1417
|
+
def add_column(self, column_name, values=None, default_value=u""):
|
1418
|
+
"""Adds a new column to this table.
|
1419
|
+
Uses :param:`default_value` for new cells (if :param:`values` are
|
1420
|
+
not provided). param:`values` are extended with :param:`default_value`
|
1421
|
+
if values list is smaller than the number of table rows.
|
1422
|
+
|
1423
|
+
:param column_name: Name of new column (as string).
|
1424
|
+
:param values: Optional list of cell values in new column.
|
1425
|
+
:param default_value: Default value for cell (if values not provided).
|
1426
|
+
:returns: Index of new column (as number).
|
1427
|
+
"""
|
1428
|
+
# assert isinstance(column_name, unicode)
|
1429
|
+
assert not self.has_column(column_name)
|
1430
|
+
if values is None:
|
1431
|
+
values = [default_value] * len(self.rows)
|
1432
|
+
elif not isinstance(values, list):
|
1433
|
+
values = list(values)
|
1434
|
+
if len(values) < len(self.rows):
|
1435
|
+
more_size = len(self.rows) - len(values)
|
1436
|
+
more_values = [default_value] * more_size
|
1437
|
+
values.extend(more_values)
|
1438
|
+
|
1439
|
+
new_column_index = len(self.headings)
|
1440
|
+
self.headings.append(column_name)
|
1441
|
+
for row, value in zip(self.rows, values):
|
1442
|
+
assert len(row.cells) == new_column_index
|
1443
|
+
row.cells.append(value)
|
1444
|
+
return new_column_index
|
1445
|
+
|
1446
|
+
def remove_column(self, column_name):
|
1447
|
+
if not isinstance(column_name, int):
|
1448
|
+
try:
|
1449
|
+
column_index = self.get_column_index(column_name)
|
1450
|
+
except ValueError:
|
1451
|
+
raise KeyError("column=%s is unknown" % column_name)
|
1452
|
+
|
1453
|
+
assert isinstance(column_index, int)
|
1454
|
+
assert column_index < len(self.headings)
|
1455
|
+
del self.headings[column_index]
|
1456
|
+
for row in self.rows:
|
1457
|
+
assert column_index < len(row.cells)
|
1458
|
+
del row.cells[column_index]
|
1459
|
+
|
1460
|
+
def remove_columns(self, column_names):
|
1461
|
+
for column_name in column_names:
|
1462
|
+
self.remove_column(column_name)
|
1463
|
+
|
1464
|
+
def has_column(self, column_name):
|
1465
|
+
return column_name in self.headings
|
1466
|
+
|
1467
|
+
def get_column_index(self, column_name):
|
1468
|
+
return self.headings.index(column_name)
|
1469
|
+
|
1470
|
+
def require_column(self, column_name):
|
1471
|
+
"""Require that a column exists in the table.
|
1472
|
+
Raise an AssertionError if the column does not exist.
|
1473
|
+
|
1474
|
+
:param column_name: Name of new column (as string).
|
1475
|
+
:return: Index of column (as number) if it exists.
|
1476
|
+
"""
|
1477
|
+
if not self.has_column(column_name):
|
1478
|
+
columns = ", ".join(self.headings)
|
1479
|
+
msg = "REQUIRE COLUMN: %s (columns: %s)" % (column_name, columns)
|
1480
|
+
raise AssertionError(msg)
|
1481
|
+
return self.get_column_index(column_name)
|
1482
|
+
|
1483
|
+
def require_columns(self, column_names):
|
1484
|
+
for column_name in column_names:
|
1485
|
+
self.require_column(column_name)
|
1486
|
+
|
1487
|
+
def ensure_column_exists(self, column_name):
|
1488
|
+
"""Ensures that a column with the given name exists.
|
1489
|
+
If the column does not exist, the column is added.
|
1490
|
+
|
1491
|
+
:param column_name: Name of column (as string).
|
1492
|
+
:return: Index of column (as number).
|
1493
|
+
"""
|
1494
|
+
if self.has_column(column_name):
|
1495
|
+
return self.get_column_index(column_name)
|
1496
|
+
else:
|
1497
|
+
return self.add_column(column_name)
|
1498
|
+
|
1499
|
+
def __repr__(self):
|
1500
|
+
return "<Table: %dx%d>" % (len(self.headings), len(self.rows))
|
1501
|
+
|
1502
|
+
def __eq__(self, other):
|
1503
|
+
if isinstance(other, Table):
|
1504
|
+
if self.headings != other.headings:
|
1505
|
+
return False
|
1506
|
+
for my_row, their_row in zip(self.rows, other.rows):
|
1507
|
+
if my_row != their_row:
|
1508
|
+
return False
|
1509
|
+
else:
|
1510
|
+
# -- ASSUME: table <=> raw data comparison
|
1511
|
+
other_rows = other
|
1512
|
+
for my_row, their_row in zip(self.rows, other_rows):
|
1513
|
+
if my_row != their_row:
|
1514
|
+
return False
|
1515
|
+
return True
|
1516
|
+
|
1517
|
+
def __ne__(self, other):
|
1518
|
+
return not self.__eq__(other)
|
1519
|
+
|
1520
|
+
def __iter__(self):
|
1521
|
+
return iter(self.rows)
|
1522
|
+
|
1523
|
+
def __getitem__(self, index):
|
1524
|
+
return self.rows[index]
|
1525
|
+
|
1526
|
+
def assert_equals(self, data):
|
1527
|
+
"""Assert that this table's cells are the same as the supplied "data".
|
1528
|
+
|
1529
|
+
The data passed in must be a list of lists giving:
|
1530
|
+
|
1531
|
+
[
|
1532
|
+
[row 1],
|
1533
|
+
[row 2],
|
1534
|
+
[row 3],
|
1535
|
+
]
|
1536
|
+
|
1537
|
+
If the cells do not match then a useful AssertionError will be raised.
|
1538
|
+
"""
|
1539
|
+
assert self == data
|
1540
|
+
raise NotImplementedError
|
1541
|
+
|
1542
|
+
|
1543
|
+
class Row(object):
|
1544
|
+
"""One row of a `table`_ parsed from a *feature file*.
|
1545
|
+
|
1546
|
+
Row data is accessible using a number of methods:
|
1547
|
+
|
1548
|
+
**iteration**
|
1549
|
+
Iterating over the Row will yield the individual cells as strings.
|
1550
|
+
|
1551
|
+
**named access**
|
1552
|
+
Individual cells may be accessed by heading name; row["name"] would give
|
1553
|
+
the cell value for the column with heading "name".
|
1554
|
+
|
1555
|
+
**indexed access**
|
1556
|
+
Individual cells may be accessed directly by index on the Row instance;
|
1557
|
+
row[0] gives the first cell and row[-1] gives the last cell.
|
1558
|
+
|
1559
|
+
The attributes are:
|
1560
|
+
|
1561
|
+
.. attribute:: cells
|
1562
|
+
|
1563
|
+
The list of strings that form the cells of this row.
|
1564
|
+
|
1565
|
+
.. attribute:: headings
|
1566
|
+
|
1567
|
+
The headings of the table as a list of strings.
|
1568
|
+
|
1569
|
+
Rows are also comparable, for what that's worth. Only the cells are
|
1570
|
+
compared.
|
1571
|
+
|
1572
|
+
.. _`table`: gherkin.html#table
|
1573
|
+
"""
|
1574
|
+
def __init__(self, headings, cells, line=None, comments=None):
|
1575
|
+
self.headings = headings
|
1576
|
+
self.comments = comments
|
1577
|
+
for c in cells:
|
1578
|
+
assert isinstance(c, six.text_type)
|
1579
|
+
self.cells = cells
|
1580
|
+
self.line = line
|
1581
|
+
|
1582
|
+
def __getitem__(self, name):
|
1583
|
+
try:
|
1584
|
+
index = self.headings.index(name)
|
1585
|
+
except ValueError:
|
1586
|
+
if isinstance(name, int):
|
1587
|
+
index = name
|
1588
|
+
else:
|
1589
|
+
raise KeyError('"%s" is not a row heading' % name)
|
1590
|
+
return self.cells[index]
|
1591
|
+
|
1592
|
+
def __repr__(self):
|
1593
|
+
return "<Row %r>" % (self.cells,)
|
1594
|
+
|
1595
|
+
def __eq__(self, other):
|
1596
|
+
return self.cells == other.cells
|
1597
|
+
|
1598
|
+
def __ne__(self, other):
|
1599
|
+
return not self.__eq__(other)
|
1600
|
+
|
1601
|
+
def __len__(self):
|
1602
|
+
return len(self.cells)
|
1603
|
+
|
1604
|
+
def __iter__(self):
|
1605
|
+
return iter(self.cells)
|
1606
|
+
|
1607
|
+
def items(self):
|
1608
|
+
return zip(self.headings, self.cells)
|
1609
|
+
|
1610
|
+
def get(self, key, default=None):
|
1611
|
+
try:
|
1612
|
+
return self[key]
|
1613
|
+
except KeyError:
|
1614
|
+
return default
|
1615
|
+
|
1616
|
+
def as_dict(self):
|
1617
|
+
"""Converts the row and its cell data into a dictionary.
|
1618
|
+
:return: Row data as dictionary (without comments, line info).
|
1619
|
+
"""
|
1620
|
+
from behave.compat.collections import OrderedDict
|
1621
|
+
return OrderedDict(self.items())
|
1622
|
+
|
1623
|
+
|
1624
|
+
class Tag(six.text_type):
|
1625
|
+
"""Tags appear may be associated with Features or Scenarios.
|
1626
|
+
|
1627
|
+
They're a subclass of regular strings (unicode pre-Python 3) with an
|
1628
|
+
additional ``line`` number attribute (where the tag was seen in the source
|
1629
|
+
feature file.
|
1630
|
+
|
1631
|
+
See :ref:`controlling things with tags`.
|
1632
|
+
"""
|
1633
|
+
allowed_chars = u"._-=:" # In addition to aplha-numerical chars.
|
1634
|
+
quoting_chars = ("'", '"', "<", ">")
|
1635
|
+
|
1636
|
+
def __new__(cls, name, line):
|
1637
|
+
o = six.text_type.__new__(cls, name)
|
1638
|
+
o.line = line
|
1639
|
+
return o
|
1640
|
+
|
1641
|
+
@classmethod
|
1642
|
+
def make_name(cls, text, unescape=False, allowed_chars=None):
|
1643
|
+
"""Translate text into a "valid tag" without whitespace, etc.
|
1644
|
+
Translation rules are:
|
1645
|
+
* alnum chars => same, kept
|
1646
|
+
* space chars => "_"
|
1647
|
+
* other chars => deleted
|
1648
|
+
|
1649
|
+
Preserve following characters (in addition to alnums, like: A-z, 0-9):
|
1650
|
+
* dots => "." (support: dotted-names, active-tag name schema)
|
1651
|
+
* minus => "-" (support: dashed-names)
|
1652
|
+
* underscore => "_"
|
1653
|
+
* equal => "=" (support: active-tag name schema)
|
1654
|
+
* colon => ":" (support: active-tag name schema or similar)
|
1655
|
+
|
1656
|
+
:param text: Unicode text as input for name.
|
1657
|
+
:param unescape: Optional flag to unescape some chars (default: false)
|
1658
|
+
:param allowed_chars: Optional string with additional preserved chars.
|
1659
|
+
:return: Unicode name that can be used as tag.
|
1660
|
+
"""
|
1661
|
+
assert isinstance(text, six.text_type)
|
1662
|
+
if allowed_chars is None:
|
1663
|
+
allowed_chars = cls.allowed_chars
|
1664
|
+
|
1665
|
+
if unescape:
|
1666
|
+
# -- UNESCAPE: Some escaped sequences
|
1667
|
+
text = text.replace("\\t", "\t").replace("\\n", "\n")
|
1668
|
+
chars = []
|
1669
|
+
for char in text:
|
1670
|
+
if char.isalnum() or (allowed_chars and char in allowed_chars):
|
1671
|
+
chars.append(char)
|
1672
|
+
elif char.isspace():
|
1673
|
+
chars.append(u"_")
|
1674
|
+
elif char in cls.quoting_chars:
|
1675
|
+
pass # -- NORMALIZE: Remove any quoting chars.
|
1676
|
+
# -- MAYBE:
|
1677
|
+
# else:
|
1678
|
+
# # -- OTHERWISE: Accept gracefully any other character.
|
1679
|
+
# chars.append(char)
|
1680
|
+
return u"".join(chars)
|
1681
|
+
|
1682
|
+
|
1683
|
+
class Text(six.text_type):
|
1684
|
+
"""Store multiline text from a Step definition.
|
1685
|
+
|
1686
|
+
The attributes are:
|
1687
|
+
|
1688
|
+
.. attribute:: value
|
1689
|
+
|
1690
|
+
The actual text parsed from the *feature file*.
|
1691
|
+
|
1692
|
+
.. attribute:: content_type
|
1693
|
+
|
1694
|
+
Currently only "text/plain".
|
1695
|
+
"""
|
1696
|
+
def __new__(cls, value, content_type=u"text/plain", line=0):
|
1697
|
+
assert isinstance(value, six.text_type)
|
1698
|
+
assert isinstance(content_type, six.text_type)
|
1699
|
+
o = six.text_type.__new__(cls, value)
|
1700
|
+
o.content_type = content_type
|
1701
|
+
o.line = line
|
1702
|
+
return o
|
1703
|
+
|
1704
|
+
def line_range(self):
|
1705
|
+
line_count = len(self.splitlines())
|
1706
|
+
return (self.line, self.line + line_count + 1)
|
1707
|
+
|
1708
|
+
def replace(self, old, new, count=-1):
|
1709
|
+
return Text(super(Text, self).replace(old, new, count), self.content_type,
|
1710
|
+
self.line)
|
1711
|
+
|
1712
|
+
def assert_equals(self, expected):
|
1713
|
+
"""Assert that my text is identical to the "expected" text.
|
1714
|
+
|
1715
|
+
A nice context diff will be displayed if they do not match.
|
1716
|
+
"""
|
1717
|
+
if self == expected:
|
1718
|
+
return True
|
1719
|
+
diff = []
|
1720
|
+
for line in difflib.unified_diff(self.splitlines(),
|
1721
|
+
expected.splitlines()):
|
1722
|
+
diff.append(line)
|
1723
|
+
# strip unnecessary diff prefix
|
1724
|
+
diff = ["Text does not match:"] + diff[3:]
|
1725
|
+
raise AssertionError("\n".join(diff))
|
1726
|
+
|
1727
|
+
|
1728
|
+
# -----------------------------------------------------------------------------
|
1729
|
+
# UTILITY FUNCTIONS:
|
1730
|
+
# -----------------------------------------------------------------------------
|
1731
|
+
def reset_model(model_elements):
|
1732
|
+
"""Reset the test run information stored in model elements.
|
1733
|
+
|
1734
|
+
:param model_elements: List of model elements (Feature, Scenario, ...)
|
1735
|
+
"""
|
1736
|
+
for model_element in model_elements:
|
1737
|
+
model_element.reset()
|