PK!)morelia/__init__.py# -*- coding: utf-8 -*- """ Running ======= To run scenarios Morelia's :py:func:`run` method needs to be given at least two parameters: * file name with scenarios description * TestCase with defined steps Then running is as simple as: .. code-block:: python run('calculator.feature', test_case_with_steps) """ import sys from morelia.formatters import PlainTextFormatter, ColorTextFormatter from morelia.parser import Parser, execute_script # noqa __version__ = "0.8.3" def has_color_support(): """Check if color in terminal is supported.""" return sys.platform != "win32" # pragma: nocover def run( filename, suite, as_str=None, scenario=r".*", verbose=False, show_all_missing=True, **kwargs ): # NOQA """Parse file and run tests on given suite. :param str filename: file name :param unittest.TestCase suite: TestCase instance :param string as_str: None to use file or a string containing the feature to parse :param string scenario: a regex pattern to match the scenario to run :param boolean verbose: be verbose :param boolean show_all_missing: show all missing steps """ formatter = kwargs.get("formatter", None) if verbose and not formatter: if has_color_support(): formatter = ColorTextFormatter() else: formatter = PlainTextFormatter() kwargs["formatter"] = formatter kwargs["show_all_missing"] = show_all_missing parser = Parser() feature = ( parser.parse_file(filename, scenario=scenario) if as_str is None else parser.parse_as_str(filename, as_str, scenario=scenario) ) return execute_script(feature, suite, **kwargs) __all__ = ("Parser", "run") PK!t morelia/config.py""" Configuration ------------- """ import os from configparser import SafeConfigParser, NoSectionError, NoOptionError DEFAULT_CONFIG_FILES = [".moreliarc", "~/.moreliarc", "/etc/morelia.rc"] def expand_all(path): """Expand path.""" return os.path.abspath(os.path.expandvars(os.path.expanduser(path))) class Config: """Configuration object. Configuration is read from ini-style files and environment variables prefixed with `MORELIA_`. By default Morelia search for files: * .moreliarc * ~/.moreliarc * /etc/morelia.rc """ def __init__(self, config_files=None, config_parser_class=None): self._env_prefix = "MORELIA_" self._items = { "tags": None, "formatter": None, "matchers": None, "show_all_missing": False, } if config_files is None: config_files = DEFAULT_CONFIG_FILES self._default_section = "morelia" self._config_files = [expand_all(config_file) for config_file in config_files] if config_parser_class is None: config_parser_class = SafeConfigParser self._config_parser_class = config_parser_class def load(self): """Load configuration.""" self._update_from_file() self._update_from_environ() def _update_from_file(self): """Update config on settings from *.ini file.""" config_parser = self._config_parser_class() config_parser.read(self._config_files) for key in self._items.keys(): try: value = config_parser.get("morelia", key) except (NoOptionError, NoSectionError): pass else: self._items[key] = value def _update_from_environ(self): """Update config on environment variables.""" for key in self._items.keys(): try: value = os.environ[self._env_prefix + key.upper()] except KeyError: pass else: self._items[key] = value def get_tags_pattern(self): """Return tags pattern.""" tags = self._items.get("tags", "") return tags if tags is not None else "" def get_config(_memo={}): """Return config object.""" try: return _memo["config"] except KeyError: config = Config() config.load() _memo["config"] = config return _memo["config"] PK!  morelia/decorators.py# -*- coding: utf-8 -*- """ Decorators ---------- Sometimes you need selectively run tests. For that reason you can tag your tests: .. code-block:: python # test_acceptance.py import unittest from morelia import run from morelia.decorators import tags class CalculatorTestCase(unittest.TestCase): @tags(['basic']) def test_addition(self): ''' Addition feature ''' filename = os.path.join(os.path.dirname(__file__), 'add.feature') run(filename, self) # ... @tags(['advanced']) def test_substraction(self): ''' Substraction feature ''' filename = os.path.join(os.path.dirname(__file__), 'substract.feature') run(filename, self) # ... @tags(['slow', 'advanced']) def test_multiplication(self): ''' Multiplication feature ''' filename = os.path.join(os.path.dirname(__file__), 'multiplication.feature') run(filename, self) # ... And run tests only for selected features: .. code-block:: console $ MORELIA_TAGS=basic python -m unittest test_acceptance .ss ---------------------------------------------------------------------- Ran 3 test in 0.018s OK (skipped=2) $ MORELIA_TAGS=advanced python -m unittest test_acceptance s.. ---------------------------------------------------------------------- Ran 3 test in 0.048s OK (skipped=2) $ MORELIA_TAGS=-slow python -m unittest test_acceptance ..s ---------------------------------------------------------------------- Ran 3 test in 0.028s OK (skipped=1) $ MORELIA_TAGS=advanced,-slow python -m unittest test_acceptance s.s ---------------------------------------------------------------------- Ran 3 test in 0.022s OK (skipped=2) """ import unittest from morelia.config import get_config def should_skip(tags_list, pattern): tags_list = set(tags_list) matching_tags = pattern.split() negative_tags = [tag[1:] for tag in matching_tags if tag.startswith("-")] positive_tags = [tag for tag in matching_tags if not tag.startswith("-")] if negative_tags: return bool(set(negative_tags) & tags_list) if positive_tags: return not set(positive_tags).issubset(tags_list) return False def tags(tags_list, config=None): """Skip decorated test methods or classes if tags matches. Tags are matched to patterns provided by config object. :param list tags_list: list of tags for test :param morelia.config.Config config: optional configuration object """ if config is None: config = get_config() pattern = config.get_tags_pattern() return unittest.skipIf(should_skip(tags_list, pattern), "Tags not matched") PK!jhrrmorelia/exceptions.pyclass MoreliaError(Exception): pass class MissingStepError(MoreliaError): def __init__(self, predicate, suggest, method_name, docstring, *args, **kwargs): self.args = (suggest, predicate, method_name, docstring) self.predicate = predicate self.suggest = suggest self.method_name = method_name self.docstring = docstring PK!и9 morelia/formatters.py""" Formatting output ================= Morelia complies with Unix's `Rule of Silence` [#ROS]_ so when you hook it like this: .. code-block:: python run(filename, self) and all tests passes it would say nothing: .. code-block:: console $ python -m unittest test_acceptance . ---------------------------------------------------------------------- Ran 1 test in 0.028s OK (here's only information from test runner) But when something went wrong it would complie with Unix's `Rule of Repair` [#ROR]_ and fail noisily: .. code-block:: console F ====================================================================== FAIL: test_addition (test_acceptance.CalculatorTestCase) Addition feature ---------------------------------------------------------------------- Traceback (most recent call last): File "test_acceptance.py", line 45, in test_addition run(filename, self) File "(..)/morelia/__init__.py", line 22, in run return ast.evaluate(suite, **kwargs) File "(...)/morelia/grammar.py", line 36, in evaluate feature.evaluate_steps(test_visitor) File "(...)/morelia/grammar.py", line 74, in evaluate_steps self._evaluate_child_steps(visitor) File "(...)/morelia/grammar.py", line 80, in _evaluate_child_steps step.evaluate_steps(visitor) File "(...)/morelia/grammar.py", line 226, in evaluate_steps self.evaluate_test_case(visitor, step_indices) # note this works on reports too! File "(...)/morelia/grammar.py", line 237, in evaluate_test_case step.evaluate_steps(visitor) File "(...)/morelia/grammar.py", line 73, in evaluate_steps visitor.visit(self) File "(...)/morelia/visitors.py", line 53, in visit node.test_step(self._suite, self._matcher) File "(...)/morelia/grammar.py", line 366, in test_step self.evaluate(suite, matcher) File "(...)/morelia/grammar.py", line 362, in evaluate method(*args, **kwargs) File "test_acceptance.py", line 41, in step_the_result_should_be_on_the_screen self.assertEqual(int(number), self.calculator.get_result()) AssertionError: File "calculator.feature", line 11, in Scenario: Add two numbers Then: the result should be "121" on the screen 121 != 120 ---------------------------------------------------------------------- Ran 1 test in 0.020s FAILED (failures=1) Verbosity --------- In Behaviour Driven Development participate both programmers and non-programmers and the latter like animations and so on. So to make Morelia a little more verbose you can pass a `verbose=True` into :py:func:`morelia.run` method. .. code-block:: python run(filename, self, verbose=True) .. code-block:: console Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers Scenario: Add two numbers Given I have powered calculator on # pass 0.000s When I enter "50" into the calculator # pass 0.000s And I enter "70" into the calculator # pass 0.000s And I press add # pass 0.001s Then the result should be "120" on the screen # pass 0.001s Scenario: Subsequent additions Given I have powered calculator on # pass 0.000s When I enter "50" into the calculator # pass 0.000s And I enter "70" into the calculator # pass 0.000s And I press add # pass 0.001s And I enter "20" into the calculator # pass 0.000s And I press add # pass 0.001s Then the result should be "140" on the screen # pass 0.001s . ---------------------------------------------------------------------- Ran 1 test in 0.027s OK With ``verbose=True`` Morelia tries to use :py:class:`morelia.formatters.ColorTextFormatter` if avaiable in system and fallbacks to :py:class:`morelia.formatters.PlainTextFormatter` if can't show colors. You can explicity pass formatter you want use: .. code-block:: python from morelia.formatters import ColorTextFormatter run(filename, self, formatter=ColorTextFormatter()) .. code-block:: console Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers Scenario: Add two numbers Given I have powered calculator on # 0.000s When I enter "50" into the calculator # 0.000s And I enter "70" into the calculator # 0.000s And I press add # 0.001s Then the result should be "120" on the screen # 0.001s Scenario: Subsequent additions Given I have powered calculator on # 0.000s When I enter "50" into the calculator # 0.000s And I enter "70" into the calculator # 0.000s And I press add # 0.001s And I enter "20" into the calculator # 0.000s And I press add # 0.001s Then the result should be "140" on the screen # 0.001s . ---------------------------------------------------------------------- Ran 1 test in 0.027s OK (You have to run above for yourself to see colors - sorry). Or you can write your own formatter. Missing steps ------------- By default Morelia prints all missing steps if it finds out that some steps are missing. If you pass ``show_all_missing=False`` then only first missing step will be shown. It can be usefull when working with features with many steps. Formatter Classes ----------------- """ from abc import ABCMeta, abstractmethod import sys from morelia.grammar import Step, Feature colors = { "normal": "\x1b[30m", "fail": "\x1b[31m", "error": "\x1b[31m", "pass": "\x1b[32m", "reset": "\x1b[0m", } class IFormatter: """Abstract Base Class for all formatters.""" __metaclass__ = ABCMeta @abstractmethod def output(self, node, line, status, duration): """Method called after execution each step. :param node: node representing step :param str line: text of executed step :param str status: execution status :param float duration: step execution duration """ pass # pragma: nocover class NullFormatter(IFormatter): """Formatter that... do nothing.""" def output(self, node, line, status, duration): """See :py:meth:`IFormatter.output`.""" pass class PlainTextFormatter(IFormatter): """Formatter that prints all executed steps in plain text to a given stream.""" def __init__(self, stream=None): """Initialize formatter. :param file stream: file-like stream to output executed steps """ self._stream = stream if stream is not None else sys.stderr def output(self, node, line, status, duration): """See :py:meth:`IFormatter.output`.""" if isinstance(node, Feature): self._stream.write("\n") if isinstance(node, Step): status = status.lower() text = "{:<60} # {:<5} {:.3f}s\n".format(line.strip("\n"), status, duration) else: text = "%s\n" % line.strip("\n") self._stream.write(text) self._stream.flush() class ColorTextFormatter(PlainTextFormatter): """Formatter that prints all executed steps in color to a given stream.""" def output(self, node, line, status, duration): """See :py:meth:`IFormatter.output`.""" if isinstance(node, Feature): self._stream.write("\n") if isinstance(node, Step): status = status.lower() text = "{}{:<60} # {:.3f}s{}\n".format( colors[status], line.strip("\n"), duration, colors["reset"] ) else: text = "%s\n" % line.strip("\n") self._stream.write(text) self._stream.flush() PK!**morelia/grammar.pyimport copy import itertools import re from morelia.exceptions import MissingStepError from morelia.i18n import TRANSLATIONS PLACEHOLDER_RE = re.compile(r"\<(\w+)\>") class Node: allowed_parents = () def __init__( self, source="", line_number=0, language="en", labels=None, predecessors=[] ): self.__source = source self.__line_number = line_number self.__language = language self.__labels = labels if labels is not None else [] self.steps = [] self.parent = None self.__predicate = self.__extract_predicate() self.parent = self.__find_parent(predecessors) self.__connect_to_parent() self._validate_predicate() def __connect_to_parent(self): if self.parent: self.parent.add_child(self) def __find_parent(self, predecessors): allowed_parents = self.allowed_parents if not allowed_parents and predecessors: self.enforce(False, "Only one Feature per file") for step in predecessors[::-1]: if isinstance(step, allowed_parents): return step return None def __extract_predicate(self): node_re = self.__get_compiled_pattern(self.__language) return node_re.sub("", self.source).strip() @classmethod def match(cls, line, language): node_re = cls.__get_compiled_pattern(language) return node_re.match(line) @classmethod def __get_compiled_pattern(cls, language, __memo={}): try: return __memo[cls, language] except KeyError: pattern = cls._get_pattern(language) node_re = __memo[cls, language] = re.compile(pattern) return node_re @classmethod def _get_pattern(cls, language): class_name = cls.__name__ name = class_name.lower() name = TRANSLATIONS[language].get(name, class_name) return r"^\s*({name}):?(\s+|$)".format(name=name) @property def source(self): return self.__source @property def line_number(self): return self.__line_number @property def predicate(self): return self.__predicate def append_line(self, line): self.__source += "\n" + line self.__predicate = (self.__predicate + "\n" + line.strip()).strip() self._validate_predicate() def _validate_predicate(self): return # looks good! (-: def get_labels(self): labels = self.__labels[:] if self.parent: labels.extend(self.parent.get_labels()) return labels def find_method(self, matcher): pass def __iter__(self): for child in self.steps: yield child for descendant in child: yield descendant def add_child(self, child): self.steps.append(child) def enforce(self, condition, diagnostic): if not condition: text = "" offset = 1 if self.parent: text = self.parent.source offset = 5 text += self.source text = text.replace("\n\n", "\n").replace("\n", "\n\t") raise SyntaxError( diagnostic, (self.get_filename(), self.line_number, offset, text) ) def get_filename(self): try: return self.parent.get_filename() if self.parent else self.filename except AttributeError: return None def interpolated_source(self): return self.source + "\n" def format_fault(self, diagnostic): parent_reconstruction = "" if self.parent: parent_reconstruction = self.parent.source.strip("\n") args = ( self.get_filename(), self.line_number, parent_reconstruction, self.source, diagnostic, ) return '\n File "%s", line %s, in %s\n %s\n%s' % args class Feature(Node): def accept(self, visitor): visitor.visit_feature(self, self.steps) def prepend_steps(self, scenario): background = self.steps[0] try: background.prepend_steps(scenario) except AttributeError: pass class Scenario(Node): allowed_parents = (Feature,) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.row_indices = [0] def accept(self, visitor): self.parent.prepend_steps(self) self.enforce( 0 < len(self.steps), "Scenario without step(s) - Step, Given, When, Then, And, or #", ) schedule = self.permute_schedule() old_row_indices = self.row_indices try: for indices in schedule: self.row_indices = indices visitor.visit_scenario(self, self.steps) finally: self.row_indices = old_row_indices def permute_schedule(self): dims = self.count_Row_dimensions() return _permute_indices(dims) def count_Row_dimensions(self): return [step.rows_number for step in self.steps if isinstance(step, RowParent)] class Background(Node): allowed_parents = (Feature,) def accept(self, visitor): visitor.visit_background(self) def prepend_steps(self, scenario): try: return scenario.background_steps except AttributeError: background_steps = [] for step in self.steps: new_step = copy.copy(step) new_step.parent = scenario background_steps.append(new_step) scenario.steps = background_steps + scenario.steps scenario.background_steps = background_steps return background_steps def count_Row_dimensions(self): return [0] class RowParent(Node): @property def rows_number(self): rows_number = len(self.get_rows()) - 1 # do not count header return max(0, rows_number) def get_rows(self): return [step for step in self.steps if isinstance(step, Row)] class Step(RowParent): allowed_parents = (Scenario, Background) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.payload = "" def accept(self, visitor): visitor.visit_step(self, self.steps) def find_method(self, matcher): """Find method matching step. :param IStepMatcher matcher: object matching methods by given predicate :returns: (method, args, kwargs) tuple :rtype: tuple :raises MissingStepError: if method maching step not found """ predicate = self.predicate augmented_predicate = self.__get_interpolated_predicate() method, args, kwargs = matcher.find(predicate, augmented_predicate) if method: return method, args, kwargs suggest, method_name, docstring = matcher.suggest(predicate) raise MissingStepError(predicate, suggest, method_name, docstring) def interpolated_source(self): augmented_predicate = self.__get_interpolated_predicate() return self.source.replace(self.predicate, augmented_predicate) def __get_interpolated_predicate(self): if self.parent is None: return self.predicate if self.__parent_has_no_rows(): return self.predicate placeholders = PLACEHOLDER_RE.findall(self.predicate) if not placeholders: return self.predicate return self.__replace_placeholders_in_predicate(placeholders) def __parent_has_no_rows(self): dims = self.parent.count_Row_dimensions() return not any(dims) def __replace_placeholders_in_predicate(self, placeholders): copy = self.predicate[:] row_indices = self.parent.row_indices siblings = self.parent.steps for step_idx, row_idx in enumerate(row_indices): step = siblings[step_idx] table = step.get_rows() if len(table) > 1: header = table[0] body = table[1:] for column_idx, column_title in enumerate(header.values): value = body[row_idx][column_idx] copy = self.__replace_placeholders( column_title, value, placeholders, copy ) return copy def __replace_placeholders(self, column_title, table_value, placeholders, copy): for placeholder in placeholders: if column_title == placeholder: return self.__replace_placeholder(copy, placeholder, table_value) return copy def __replace_placeholder(self, copy, placeholder, table_value): table_value = table_value.replace("\n", "\\n") return copy.replace( "<{placeholder}>".format(placeholder=placeholder), table_value ) class Given(Step): pass class When(Step): pass class Then(Step): pass class And(Step): pass class But(And): pass class Examples(RowParent): allowed_parents = (Scenario,) def accept(self, visitor): visitor.visit_examples(self, self.steps) class Row(Node): allowed_parents = (Step, Examples) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._validate_predicate() def _validate_predicate(self): # TODO: validate that grandparent is not Background row = re.split(r" \|", re.sub(r"\|$", "", self.predicate)) self.__values = [s.strip() for s in row] def __getitem__(self, column_idx): return self.__values[column_idx] @property def values(self): return self.__values def accept(self, visitor): visitor.visit_row(self) @classmethod def _get_pattern(cls, language): return r"^\s*(\|):?\s+" class Comment(Node): allowed_parents = (Node,) def accept(self, visitor): visitor.visit_comment(self) @classmethod def _get_pattern(cls, language): return r"\s*(\#)" def _validate_predicate(self): self.enforce("\n" not in self.predicate, "linefeed in comment") def _permute_indices(arr): product_args = list(_imap(arr)) result = list(itertools.product(*product_args)) return result # tx to Chris Rebert, et al, on the Python newsgroup for curing my brainlock here!! def _imap(*iterables): iterables = [iter(i) for i in iterables] while True: try: args = [next(i) for i in iterables] yield _special_range(*args) except StopIteration: return def _special_range(n): return range(n) if n else [0] PK!)(Bvbvbmorelia/i18n.py# -*- coding: utf-8 -*- # source: https://github.com/cucumber/gherkin/blob/master/lib/gherkin/i18n.json TRANSLATIONS = { "en": { "name": "English", "native": "English", "feature": "Feature|Business Need|Ability", "background": "Background", "scenario": "Scenario Outline|Scenario Template|Scenario", "examples": "Examples|Scenarios", "given": "Given", "when": "When", "then": "Then", "and": "And", "but": "But", }, "af": { "name": "Afrikaans", "native": "Afrikaans", "feature": "Funksie|Besigheid Behoefte|Vermoë", "background": "Agtergrond", "scenario": "Situasie Uiteensetting|Situasie", "examples": "Voorbeelde", "given": "Gegewe", "when": "Wanneer", "then": "Dan", "and": "En", "but": "Maar", }, "ar": { "name": "Arabic", "native": "العربية", "feature": "خاصية", "background": "الخلفية", "scenario": "سيناريو مخطط|سيناريو", "examples": "امثلة", "given": "بفرض", "when": "متى|عندما", "then": "اذاً|ثم", "and": "و", "but": "لكن", }, "bm": { "name": "Malay", "native": "Bahasa Melayu", "feature": "Fungsi", "background": "Latar Belakang", "scenario": "Template Senario|Template Situai|Template Keadaan|Menggariskan Senario|Senario|Situai|Keadaan", "examples": "Contoh", "given": "Diberi|Bagi", "when": "Apabila", "then": "Maka|Kemudian", "and": "Dan", "but": "Tetapi|Tapi", }, "bg": { "name": "Bulgarian", "native": "български", "feature": "Функционалност", "background": "Предистория", "scenario": "Рамка на сценарий|Сценарий", "examples": "Примери", "given": "Дадено", "when": "Когато", "then": "То", "and": "И", "but": "Но", }, "ca": { "name": "Catalan", "native": "català", "background": "Rerefons|Antecedents", "feature": "Característica|Funcionalitat", "scenario": "Esquema de l'escenari|Escenari", "examples": "Exemples", "given": "Donat|Donada|Atès|Atesa", "when": "Quan", "then": "Aleshores|Cal", "and": "I", "but": "Però", }, "cy-GB": { "name": "Welsh", "native": "Cymraeg", "background": "Cefndir", "feature": "Arwedd", "scenario": "Scenario Amlinellol|Scenario", "examples": "Enghreifftiau", "given": "Anrhegedig a", "when": "Pryd", "then": "Yna", "and": "A", "but": "Ond", }, "cs": { "name": "Czech", "native": "Česky", "feature": "Požadavek", "background": "Pozadí|Kontext", "scenario": "Náčrt Scénáře|Osnova scénáře|Scénář", "examples": "Příklady", "given": "Pokud|Za předpokladu", "when": "Když", "then": "Pak", "and": "A také|A", "but": "Ale", }, "da": { "name": "Danish", "native": "dansk", "feature": "Egenskab", "background": "Baggrund", "scenario": "Abstrakt Scenario|Scenarie", "examples": "Eksempler", "given": "Givet", "when": "Når", "then": "Så", "and": "Og", "but": "Men", }, "de": { "name": "German", "native": "Deutsch", "feature": "Funktionalität", "background": "Grundlage", "scenario": "Szenariogrundriss|Szenario", "examples": "Beispiele", "given": "Angenommen|Gegeben sei|Gegeben seien", "when": "Wenn", "then": "Dann", "and": "Und", "but": "Aber", }, "el": { "name": "Greek", "native": "Ελληνικά", "feature": "Δυνατότητα|Λειτουργία", "background": "Υπόβαθρο", "scenario": "Περιγραφή Σεναρίου|Σενάριο", "examples": "Παραδείγματα|Σενάρια", "given": "Δεδομένου", "when": "Όταν", "then": "Τότε", "and": "Και", "but": "Αλλά", }, "en-au": { "name": "Australian", "native": "Australian", "feature": "Pretty much", "background": "First off", "scenario": "Reckon it's like|Awww, look mate", "examples": "You'll wanna", "given": "Y'know", "when": "It's just unbelievable", "then": "But at the end of the day I reckon", "and": "Too right", "but": "Yeah nah", }, "en-Scouse": { "name": "Scouse", "native": "Scouse", "feature": "Feature", "background": "Dis is what went down", "scenario": "Wharrimean is|The thing of it is", "examples": "Examples", "given": "Givun|Youse know when youse got", "when": "Wun|Youse know like when", "then": "Dun|Den youse gotta", "and": "An", "but": "Buh", }, "en-tx": { "name": "Texan", "native": "Texan", "feature": "Feature", "background": "Background", "scenario": "All y'all|Scenario", "examples": "Examples", "given": "Given y'all", "when": "When y'all", "then": "Then y'all", "and": "And y'all", "but": "But y'all", }, "eo": { "name": "Esperanto", "native": "Esperanto", "feature": "Trajto", "background": "Fono", "scenario": "Konturo de la scenaro|Scenaro", "examples": "Ekzemploj", "given": "Donitaĵo", "when": "Se", "then": "Do", "and": "Kaj", "but": "Sed", }, "es": { "name": "Spanish", "native": "español", "background": "Antecedentes", "feature": "Característica", "scenario": "Esquema del escenario|Escenario", "examples": "Ejemplos", "given": "Dado|Dada|Dados|Dadas", "when": "Cuando", "then": "Entonces", "and": "Y", "but": "Pero", }, "et": { "name": "Estonian", "native": "eesti keel", "feature": "Omadus", "background": "Taust", "scenario": "Raamstsenaarium|Stsenaarium", "examples": "Juhtumid", "given": "Eeldades", "when": "Kui", "then": "Siis", "and": "Ja", "but": "Kuid", }, "fa": { "name": "Persian", "native": "فارسی", "feature": "وِیژگی", "background": "زمینه", "scenario": "الگوی سناریو|سناریو", "examples": "نمونه ها", "given": "با فرض", "when": "هنگامی", "then": "آنگاه", "and": "و", "but": "اما", }, "fi": { "name": "Finnish", "native": "suomi", "feature": "Ominaisuus", "background": "Tausta", "scenario": "Tapausaihio|Tapaus", "examples": "Tapaukset", "given": "Oletetaan", "when": "Kun", "then": "Niin", "and": "Ja", "but": "Mutta", }, "fr": { "name": "French", "native": "français", "feature": "Fonctionnalité", "background": "Contexte", "scenario": "Plan du scénario|Plan du Scénario|Scénario", "examples": "Exemples", "given": "Soit|Etant donné|Etant donnée|Etant donnés|Etant données|Étant donné|Étant donnée|Étant donnés|Étant données", "when": "Quand|Lorsque|Lorsqu'<", "then": "Alors", "and": "Et", "but": "Mais", }, "gl": { "name": "Galician", "native": "galego", "background": "Contexto", "feature": "Característica", "scenario": "Esbozo do escenario|Escenario", "examples": "Exemplos", "given": "Dado|Dada|Dados|Dadas", "when": "Cando", "then": "Entón|Logo", "and": "E", "but": "Mais|Pero", }, "he": { "name": "Hebrew", "native": "עברית", "feature": "תכונה", "background": "רקע", "scenario": "תבנית תרחיש|תרחיש", "examples": "דוגמאות", "given": "בהינתן", "when": "כאשר", "then": "אז|אזי", "and": "וגם", "but": "אבל", }, "hi": { "name": "Hindi", "native": "हिंदी", "feature": "रूप लेख", "background": "पृष्ठभूमि", "scenario": "परिदृश्य रूपरेखा|परिदृश्य", "examples": "उदाहरण", "given": "अगर|यदि|चूंकि", "when": "जब|कदा", "then": "तब|तदा", "and": "और|तथा", "but": "पर|परन्तु|किन्तु", }, "hr": { "name": "Croatian", "native": "hrvatski", "feature": "Osobina|Mogućnost|Mogucnost", "background": "Pozadina", "scenario": "Skica|Koncept|Scenarij", "examples": "Primjeri|Scenariji", "given": "Zadan|Zadani|Zadano", "when": "Kada|Kad", "then": "Onda", "and": "I", "but": "Ali", }, "ht": { "name": "Creole", "native": "kreyòl", "feature": "Karakteristik|Mak|Fonksyonalite", "background": "Kontèks|Istorik", "scenario": "Plan senaryo|Plan Senaryo|Senaryo deskripsyon|Senaryo Deskripsyon|Dyagram senaryo|Dyagram Senaryo|Senaryo", "examples": "Egzanp", "given": "Sipoze|Sipoze ke|Sipoze Ke", "when": "Lè|Le", "then": "Lè sa a|Le sa a", "and": "Ak|Epi|E", "but": "Men", }, "hu": { "name": "Hungarian", "native": "magyar", "feature": "Jellemző", "background": "Háttér", "scenario": "Forgatókönyv vázlat|Forgatókönyv", "examples": "Példák", "given": "Amennyiben|Adott", "when": "Majd|Ha|Amikor", "then": "Akkor", "and": "És", "but": "De", }, "id": { "name": "Indonesian", "native": "Bahasa Indonesia", "feature": "Fitur", "background": "Dasar", "scenario": "Skenario konsep|Skenario", "examples": "Contoh", "given": "Dengan", "when": "Ketika", "then": "Maka", "and": "Dan", "but": "Tapi", }, "is": { "name": "Icelandic", "native": "Íslenska", "feature": "Eiginleiki", "background": "Bakgrunnur", "scenario": "Lýsing Atburðarásar|Lýsing Dæma|Atburðarás", "examples": "Dæmi|Atburðarásir", "given": "Ef", "when": "Þegar", "then": "Þá", "and": "Og", "but": "En", }, "it": { "name": "Italian", "native": "italiano", "feature": "Funzionalità", "background": "Contesto", "scenario": "Schema dello scenario|Scenario", "examples": "Esempi", "given": "Dato|Data|Dati|Date", "when": "Quando", "then": "Allora", "and": "E", "but": "Ma", }, "ja": { "name": "Japanese", "native": "日本語", "feature": "フィーチャ|機能", "background": "背景", "scenario": "シナリオアウトライン|シナリオテンプレート|テンプレ|シナリオテンプレ|シナリオ", "examples": "例|サンプル", "given": "前提<", "when": "もし<", "then": "ならば<", "and": "かつ<", "but": "しかし<|但し<|ただし<", }, "jv": { "name": "Javanese", "native": "Basa Jawa", "feature": "Fitur", "background": "Dasar", "scenario": "Konsep skenario|Skenario", "examples": "Conto|Contone", "given": "Nalika|Nalikaning", "when": "Manawa|Menawa", "then": "Njuk|Banjur", "and": "Lan", "but": "Tapi|Nanging|Ananging", }, "kn": { "name": "Kannada", "native": "ಕನ್ನಡ", "background": "ಹಿನ್ನೆಲೆ", "feature": "ಹೆಚ್ಚಳ", "scenario": "ವಿವರಣೆ|ಕಥಾಸಾರಾಂಶ", "examples": "ಉದಾಹರಣೆಗಳು", "given": "ನೀಡಿದ", "when": "ಸ್ಥಿತಿಯನ್ನು", "then": "ನಂತರ", "and": "ಮತ್ತು", "but": "ಆದರೆ", }, "ko": { "name": "Korean", "native": "한국어", "background": "배경", "feature": "기능", "scenario": "시나리오 개요|시나리오", "examples": "예", "given": "조건<|먼저<", "when": "만일<|만약<", "then": "그러면<", "and": "그리고<", "but": "하지만<|단<", }, "lt": { "name": "Lithuanian", "native": "lietuvių kalba", "feature": "Savybė", "background": "Kontekstas", "scenario": "Scenarijaus šablonas|Scenarijus", "examples": "Pavyzdžiai|Scenarijai|Variantai", "given": "Duota", "when": "Kai", "then": "Tada", "and": "Ir", "but": "Bet", }, "lu": { "name": "Luxemburgish", "native": "Lëtzebuergesch", "feature": "Funktionalitéit", "background": "Hannergrond", "scenario": "Plang vum Szenario|Szenario", "examples": "Beispiller", "given": "ugeholl", "when": "wann", "then": "dann", "and": "an|a", "but": "awer|mä", }, "lv": { "name": "Latvian", "native": "latviešu", "feature": "Funkcionalitāte|Fīča", "background": "Konteksts|Situācija", "scenario": "Scenārijs pēc parauga|Scenārijs", "examples": "Piemēri|Paraugs", "given": "Kad", "when": "Ja", "then": "Tad", "and": "Un", "but": "Bet", }, "nl": { "name": "Dutch", "native": "Nederlands", "feature": "Functionaliteit", "background": "Achtergrond", "scenario": "Abstract Scenario|Scenario", "examples": "Voorbeelden", "given": "Gegeven|Stel", "when": "Als", "then": "Dan", "and": "En", "but": "Maar", }, "no": { "name": "Norwegian", "native": "norsk", "feature": "Egenskap", "background": "Bakgrunn", "scenario": "Scenariomal|Abstrakt Scenario|Scenario", "examples": "Eksempler", "given": "Gitt", "when": "Når", "then": "Så", "and": "Og", "but": "Men", }, "pa": { "name": "Panjabi", "native": "ਪੰਜਾਬੀ", "feature": "ਖਾਸੀਅਤ|ਮੁਹਾਂਦਰਾ|ਨਕਸ਼ ਨੁਹਾਰ", "background": "ਪਿਛੋਕੜ", "scenario": "ਪਟਕਥਾ ਢਾਂਚਾ|ਪਟਕਥਾ ਰੂਪ ਰੇਖਾ|ਪਟਕਥਾ", "examples": "ਉਦਾਹਰਨਾਂ", "given": "ਜੇਕਰ|ਜਿਵੇਂ ਕਿ", "when": "ਜਦੋਂ", "then": "ਤਦ", "and": "ਅਤੇ", "but": "ਪਰ", }, "pl": { "name": "Polish", "native": "polski", "feature": "Właściwość|Funkcja|Aspekt|Potrzeba biznesowa", "background": "Założenia", "scenario": "Szablon scenariusza|Scenariusz", "examples": "Przykłady", "given": "Zakładając(?:,? że)?|Mając", "when": "Jeżeli|Jeśli|Gdy|Kiedy", "then": "Wtedy", "and": "Oraz|I", "but": "Ale", }, "pt": { "name": "Portuguese", "native": "português", "background": "Contexto|Cenário de Fundo|Cenario de Fundo|Fundo", "feature": "Funcionalidade|Característica|Caracteristica", "scenario": "Esquema do Cenário|Esquema do Cenario|Delineação do Cenário|Delineacao do Cenario|Cenário|Cenario", "examples": "Exemplos|Cenários|Cenarios", "given": "Dado|Dada|Dados|Dadas", "when": "Quando", "then": "Então|Entao", "and": "E", "but": "Mas", }, "ro": { "name": "Romanian", "native": "română", "background": "Context", "feature": "Functionalitate|Funcționalitate|Funcţionalitate", "scenario": "Structura scenariu|Structură scenariu|Scenariu", "examples": "Exemple", "given": "Date fiind|Dat fiind|Dati fiind|Dați fiind|Daţi fiind", "when": "Cand|Când", "then": "Atunci", "and": "Si|Și|Şi", "but": "Dar", }, "ru": { "name": "Russian", "native": "русский", "feature": "Функция|Функционал|Свойство", "background": "Предыстория|Контекст", "scenario": "Структура сценария|Сценарий", "examples": "Примеры", "given": "Допустим|Дано|Пусть", "when": "Если|Когда", "then": "То|Тогда", "and": "И|К тому же|Также", "but": "Но|А", }, "sv": { "name": "Swedish", "native": "Svenska", "feature": "Egenskap", "background": "Bakgrund", "scenario": "Abstrakt Scenario|Scenariomall|Scenario", "examples": "Exempel", "given": "Givet", "when": "När", "then": "Så", "and": "Och", "but": "Men", }, "sk": { "name": "Slovak", "native": "Slovensky", "feature": "Požiadavka|Funkcia|Vlastnosť", "background": "Pozadie", "scenario": "Náčrt Scenáru|Náčrt Scenára|Osnova Scenára|Scenár", "examples": "Príklady", "given": "Pokiaľ|Za predpokladu", "when": "Keď|Ak", "then": "Tak|Potom", "and": "A|A tiež|A taktiež|A zároveň", "but": "Ale", }, "sl": { "name": "Slovenian", "native": "Slovenski", "feature": "Funkcionalnost|Funkcija|Možnosti|Moznosti|Lastnost|Značilnost", "background": "Kontekst|Osnova|Ozadje", "scenario": "Struktura scenarija|Skica|Koncept|Oris scenarija|Osnutek|Scenarij|Primer", "examples": "Primeri|Scenariji", "given": "Dano|Podano|Zaradi|Privzeto", "when": "Ko|Ce|Če|Kadar", "then": "Nato|Potem|Takrat", "and": "In|Ter", "but": "Toda|Ampak|Vendar", }, "sr-Latn": { "name": "Serbian (Latin)", "native": "Srpski (Latinica)", "feature": "Funkcionalnost|Mogućnost|Mogucnost|Osobina", "background": "Kontekst|Osnova|Pozadina", "scenario": "Struktura scenarija|Skica|Koncept|Scenario|Primer", "examples": "Primeri|Scenariji", "given": "Zadato|Zadate|Zatati", "when": "Kada|Kad", "then": "Onda", "and": "I", "but": "Ali", }, "sr-Cyrl": { "name": "Serbian", "native": "Српски", "feature": "Функционалност|Могућност|Особина", "background": "Контекст|Основа|Позадина", "scenario": "Структура сценарија|Скица|Концепт|Сценарио|Пример", "examples": "Примери|Сценарији", "given": "Задато|Задате|Задати", "when": "Када|Кад", "then": "Онда", "and": "И", "but": "Али", }, "tl": { "name": "Telugu", "native": "తెలుగు", "feature": "గుణము", "background": "నేపథ్యం", "scenario": "కథనం|సన్నివేశం", "examples": "ఉదాహరణలు", "given": "చెప్పబడినది", "when": "ఈ పరిస్థితిలో", "then": "అప్పుడు", "and": "మరియు", "but": "కాని", }, "th": { "name": "Thai", "native": "ไทย", "feature": "โครงหลัก|ความต้องการทางธุรกิจ|ความสามารถ", "background": "แนวคิด", "scenario": "สรุปเหตุการณ์|โครงสร้างของเหตุการณ์|เหตุการณ์", "examples": "ชุดของตัวอย่าง|ชุดของเหตุการณ์", "given": "กำหนดให้", "when": "เมื่อ", "then": "ดังนั้น", "and": "และ", "but": "แต่", }, "tlh": { "name": "Klingon", "native": "tlhIngan", "feature": "Qap|Qu'meH 'ut|perbogh|poQbogh malja'|laH", "background": "mo'", "scenario": "lut chovnatlh|lut", "examples": "ghantoH|lutmey", "given": "ghu' noblu'|DaH ghu' bejlu'", "when": "qaSDI'", "then": "vaj", "and": "'ej|latlh", "but": "'ach|'a", }, "tr": { "name": "Turkish", "native": "Türkçe", "feature": "Özellik", "background": "Geçmiş", "scenario": "Senaryo taslağı|Senaryo", "examples": "Örnekler", "given": "Diyelim ki", "when": "Eğer ki", "then": "O zaman", "and": "Ve", "but": "Fakat|Ama", }, "tt": { "name": "Tatar", "native": "Татарча", "feature": "Мөмкинлек|Үзенчәлеклелек", "background": "Кереш", "scenario": "Сценарийның төзелеше|Сценарий", "examples": "Үрнәкләр|Мисаллар", "given": "Әйтик", "when": "Әгәр", "then": "Нәтиҗәдә", "and": "Һәм|Вә", "but": "Ләкин|Әмма", }, "uk": { "name": "Ukrainian", "native": "Українська", "feature": "Функціонал", "background": "Передумова", "scenario": "Структура сценарію|Сценарій", "examples": "Приклади", "given": "Припустимо|Припустимо, що|Нехай|Дано", "when": "Якщо|Коли", "then": "То|Тоді", "and": "І|А також|Та", "but": "Але", }, "uz": { "name": "Uzbek", "native": "Узбекча", "feature": "Функционал", "background": "Тарих", "scenario": "Сценарий структураси|Сценарий", "examples": "Мисоллар", "given": "Агар", "when": "Агар", "then": "Унда", "and": "Ва", "but": "Лекин|Бирок|Аммо", }, "vi": { "name": "Vietnamese", "native": "Tiếng Việt", "feature": "Tính năng", "background": "Bối cảnh", "scenario": "Khung tình huống|Khung kịch bản|Tình huống|Kịch bản", "examples": "Dữ liệu", "given": "Biết|Cho", "when": "Khi", "then": "Thì", "and": "Và", "but": "Nhưng", }, "zh-CN": { "name": "Chinese simplified", "native": "简体中文", "feature": "功能", "background": "背景", "scenario": "场景大纲|剧本大纲|场景|剧本", "examples": "例子", "given": "假如<|假设<|假定<", "when": "当<", "then": "那么<", "and": "而且<|并且<|同时<", "but": "但是<", }, "zh-TW": { "name": "Chinese traditional", "native": "繁體中文", "feature": "功能", "background": "背景", "scenario": "場景大綱|劇本大綱|場景|劇本", "examples": "例子", "given": "假如<|假設<|假定<", "when": "當<", "then": "那麼<", "and": "而且<|並且<|同時<", "but": "但是<", }, "ur": { "name": "Urdu", "native": "اردو", "feature": "صلاحیت|کاروبار کی ضرورت|خصوصیت", "background": "پس منظر", "scenario": "منظر نامے کا خاکہ|منظرنامہ", "examples": "مثالیں", "given": "اگر|بالفرض|فرض کیا", "when": "جب", "then": "پھر|تب", "and": "اور", "but": "لیکن", }, } PK!lEEmorelia/matchers.pyr""" Steps ===== .. _matching-steps: Matching steps -------------- When Morelia executes steps described in feature files it looks inside passed :py:class:`unittest.TestCase` object and search for methods which name starts with `step_`. Then it selects correct method using: * `Regular expressions`_ * `Format-like strings`_ * `Method names`_ If you look in example from :ref:`usage-guide`: .. code-block:: python # test_acceptance.py import unittest from morelia import run class CalculatorTestCase(unittest.TestCase): def test_addition(self): ''' Addition feature ''' filename = os.path.join(os.path.dirname(__file__), 'calculator.feature') run(filename, self, verbose=True) def step_I_have_powered_calculator_on(self): r'I have powered calculator on' self.stack = [] def step_I_enter_a_number_into_the_calculator(self, number): r'I enter "(\d+)" into the calculator' # match by regexp self.stack.append(int(number)) def step_I_press_add(self): # matched by method name self.result = sum(self.stack) def step_the_result_should_be_on_the_screen(self, number): r'the result should be "{number}" on the screen' # match by format-like string self.assertEqual(int(number), self.result) You'll see three types of matching. Regular expressions ^^^^^^^^^^^^^^^^^^^ Method ``step_I_enter_number_into_the_calculator`` from example is matched by :py:mod:`regular expression ` as it's docstring .. code-block:: python r'I enter "(\d+)" into the calculator' matches steps: .. code-block:: cucumber When I enter "50" into the calculator And I enter "70" into the calculator Regular expressions, such as ``(\d+)``, are expanded into positional step arguments, such as ``number`` in above example. If you would use named groups like ``(?P\d+)`` then capttured expressions from steps will be put as given keyword argument to method. Remember to use tight expressions, such as ``(\d+)``, not expressions like ``(\d*)`` or ``(.*)``, to validate your input. Format-like strings ^^^^^^^^^^^^^^^^^^^ Method ``step_the_result_should_be_on_the_screen`` from example is matched by :ref:`format-like strings ` as it's docstring .. code-block:: python r'the result should be "{number}" on the screen' matches step: .. code-block:: cucumber Then the result should be "120" on the screen Method names ^^^^^^^^^^^^ Method ``step_I_press_add`` from example is matched by method name which matches step: .. code-block:: cucumber And I press add Own matchers ^^^^^^^^^^^^ You can limit matchers for only some types or use your own matchers. Matcher classes can be passed to :py:func:`morelia.run` method as keyword parameter: .. code-block:: python from morelia.matchers import RegexpStepMatcher # ... run(filename, self, matchers=[MyOwnMatcher, RegexpStepMatcher]) .. _matching-tables: Tables ------ If you use Scenarios with tables and `` around the payload variable names: .. code-block:: cucumber Scenario: orders above $100.00 to the continental US get free ground shipping When we send an order totaling $, with a 12345 SKU, to our warehouse And the order will ship to Then the ground shipping cost is $ And delivery might be available | total | destination | cost | rapid | | 98.00 | Rhode Island | 8.25 | yes | | 101.00 | Rhode Island | 0.00 | yes | | 99.00 | Kansas | 8.25 | yes | | 101.00 | Kansas | 0.00 | yes | | 99.00 | Hawaii | 8.25 | yes | | 101.00 | Hawaii | 8.25 | yes | | 101.00 | Alaska | 8.25 | yes | | 99.00 | Ontario, Canada | 40.00 | no | | 99.00 | Brisbane, Australia | 55.00 | no | | 99.00 | London, United Kingdom | 55.00 | no | | 99.00 | Kuantan, Malaysia | 55.00 | no | | 101.00 | Tierra del Fuego | 55.00 | no | then that Scenario will unroll into a series of scenarios, each with one value from the table inserted into their placeholders ``, ``, and ``. So this step method will receive each line in the "destination" column: .. code-block:: python def step_the_order_will_ship_to_(self, location): r'the order will ship to (.*)' (And observe that naming the placeholder the same as the method argument is a *reeeally* good idea, but naturally unenforceable.) Morelia will take each line of the table, and construct a complete test case out of the Scenario steps, running :py:meth:`unittest.TestCase.setUp()` and :py:meth:`unittest.TestCase.tearDown()` around them. If you use many tables then Morelia would use permutation of all rows in all tables: .. code-block:: cucumber Scenario: orders above $100.00 to the continental US get free ground shipping When we send an order totaling $, with a 12345 SKU, to our warehouse And the order will ship to And we choose that delivery should be | speed | | rapid | | regular | Then the ground shipping cost is $ | total | destination | cost | | 98.00 | Rhode Island | 8.25 | | 101.00 | Rhode Island | 0.00 | | 99.00 | Kansas | 8.25 | In above example 2 * 3 = 6 different test cases would be generated. .. _matching-docstrings: Doc Strings ----------- Docstrings attached to steps are passed as keyword argument `_text` into method: .. code-block:: cucumber Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers Scenario: Add two numbers Given I have powered calculator on When I enter "50" into the calculator And I enter "70" into the calculator And I press add Then I would see on the screen ''' Calculator example ================== 50 +70 --- 120 ''' .. code-block:: python def step_i_would_see_on_the_screen(self, _text): pass # or def step_i_would_see_on_the_screen(self, **kwargs): _text = kwargs.pop('_text') Morelia is smart enough not to passing this argument if you don't name it. Below example won't raise exception: .. code-block:: python def step_i_would_see_on_the_screen(self): pass It'll be simply assumed that you ignore docstring. .. _labels-matching: Labels ------ Labels attached to features and scenarios are available as keyword argument `_label`: .. code-block:: cucumber @web @android @ios Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers @wip Scenario: Add two numbers Given I have powered calculator on When I enter "50" into the calculator And I enter "70" into the calculator And I press add Then the result should be "120" on the screen .. code-block:: python def step_I_enter_number_into_the_calculator(self, num, _label): pass As like with doc-strings you can ommit keyword parameter if you don't need it: .. code-block:: python def step_I_enter_number_into_the_calculator(self, num): pass Labels allows you to implement custom logic depending on labels given. .. note:: **Compatibility** Morelia does not connects any custom logic with labels as some other Behavior Driven Development tools do. You are put in the charge and should add logic if any. If you are looking for ability to selectivly running features and scenarios look at :py:func:`morelia.decorators.tags` decorator. Matchers Classes ---------------- """ from abc import ABCMeta, abstractmethod import re import unicodedata import parse class IStepMatcher: """Matches methods to steps. Subclasses should implement at least `match` and `suggest` methods. """ __metaclass__ = ABCMeta def __init__(self, suite, step_pattern="^step_"): self._suite = suite self._matcher = re.compile(step_pattern) self._next = None def _get_all_step_methods(self): match = self._matcher.match return [method_name for method_name in dir(self._suite) if match(method_name)] def add_matcher(self, matcher): """Add new matcher at end of CoR. :param IStepMatcher matcher: matcher to add :returns: self """ if self._next is None: self._next = matcher else: self._next.add_matcher(matcher) return self def find(self, predicate, augmented_predicate, step_methods=None): if step_methods is None: step_methods = self._get_all_step_methods() method, args, kwargs = self.match(predicate, augmented_predicate, step_methods) if method: return method, args, kwargs if self._next is not None: return self._next.find(predicate, augmented_predicate, step_methods) return None, (), {} @abstractmethod def match(self, predicate, augmented_predicate, step_methods): """Match method from suite to given predicate. :param str predicate: step predicate :param str augmented_predicate: step augmented_predicate :param list step_methods: list of all step methods from suite :returns: (method object, args, kwargs) :rtype: (method, tuple, dict) """ pass # pragma: nocover def suggest(self, predicate): """Suggest method definition. Method is used to suggest methods that should be implemented. :param str predicate: step predicate :returns: (suggested method definition, suggested method name, suggested docstring) :rtype: (str, str, str) """ docstring, extra_arguments = self._suggest_doc_string(predicate) method_name = self.slugify(predicate) suggest = " def step_{method_name}(self{args}):\n {docstring}\n\n raise NotImplementedError('{predicate}')\n\n".format( method_name=method_name, args=extra_arguments, docstring=docstring, predicate=predicate.replace("'", "\\'"), ) return suggest, method_name, docstring def _suggest_doc_string(self, predicate): predicate = predicate.replace("'", r"\'").replace("\n", r"\n") arguments = self._add_extra_args(r'["\<](.+?)["\>]', predicate) arguments = self._name_arguments(arguments) predicate = self.replace_placeholders(predicate, arguments) predicate = re.sub(r" \s+", r"\\s+", predicate) arguments = self._format_arguments(arguments) return "r'%s'" % predicate, arguments def _name_arguments(self, extra_arguments): if not extra_arguments: return "" arguments = [] number_arguments_count = sum( 1 for arg_type, arg in extra_arguments if arg_type == "number" ) if number_arguments_count < 2: num_suffixes = iter([""]) else: num_suffixes = iter(range(1, number_arguments_count + 1)) for arg_type, arg in extra_arguments: if arg_type == "number": arguments.append("number%s" % next(num_suffixes)) else: arguments.append(arg) return arguments def _format_arguments(self, arguments): if not arguments: return "" return ", " + ", ".join(arguments) def replace_placeholders(self, predicate, arguments): predicate = re.sub(r'".+?"', '"([^"]+)"', predicate) predicate = re.sub(r"\<.+?\>", "(.+)", predicate) return predicate def _add_extra_args(self, matcher, predicate): args = re.findall(matcher, predicate) result = [] for arg in args: try: float(arg) except ValueError: arg = ("id", self.slugify(arg)) else: arg = ("number", arg) result.append(arg) return result def slugify(self, predicate): result = [] for part in re.split(r"[^\w]+", predicate): part = ( unicodedata.normalize("NFD", part) .encode("ascii", "replace") .decode("utf-8") ) part = part.replace("??", "_").replace("?", "") try: float(part) except ValueError: pass else: part = "number" result.append(part) return "_".join(result).strip("_") class MethodNameStepMatcher(IStepMatcher): """Matcher that matches steps by method name.""" def match(self, predicate, augmented_predicate, step_methods): """See :py:meth:`IStepMatcher.match`.""" matches = self.__find_matching_methods(step_methods, predicate) return self.__select_best_match(matches) def __find_matching_methods(self, step_methods, predicate): clean = re.sub(r"[^\w]", "_?", predicate) pattern = "^step_" + clean + "$" regexp = re.compile(pattern) for method_name in step_methods: if regexp.match(method_name): method = self._suite.__getattribute__(method_name) yield (method, (), {}) def __select_best_match(self, matches): try: best_match = next(iter(matches)) except StopIteration: return None, (), {} else: method, args, kwargs = best_match return method, args, kwargs def suggest(self, predicate): """See :py:meth:`IStepMatcher.suggest`.""" method_name = self.slugify(predicate) suggest = " def step_{method_name}(self):\n\n raise NotImplementedError('{predicate}')\n\n".format( method_name=method_name, predicate=predicate.replace("'", "\\'") ) return suggest, method_name, "" def slugify(self, predicate): predicate = ( unicodedata.normalize("NFD", predicate) .encode("ascii", "replace") .decode("utf-8") ) predicate = predicate.replace("??", "_").replace("?", "") return re.sub(r"[^\w]+", "_", predicate, re.U).strip("_") class RegexpStepMatcher(IStepMatcher): """Matcher that matches steps by regexp in docstring.""" def match(self, predicate, augmented_predicate, step_methods): """See :py:meth:`IStepMatcher.match`.""" matches = self.__find_matching_methods(step_methods, augmented_predicate) return self.__select_best_match(matches) def __find_matching_methods(self, step_methods, augmented_predicate): for method, doc in self.__find_methods_with_docstring(step_methods): doc = re.compile("^" + doc + "$") match = doc.match(augmented_predicate) if match: kwargs = match.groupdict() if not kwargs: args = match.groups() else: args = () yield (method, args, kwargs) return None, (), {} def __find_methods_with_docstring(self, step_methods): for method_name in step_methods: method = self._suite.__getattribute__(method_name) doc = method.__doc__ if doc: yield method, doc def __select_best_match(self, matches): try: best_match = next(iter(matches)) except StopIteration: return None, (), {} else: method, args, kwargs = best_match return method, args, kwargs class ParseStepMatcher(IStepMatcher): """Matcher that matches steps by format-like string in docstring.""" def match(self, predicate, augmented_predicate, step_methods): """See :py:meth:`IStepMatcher.match`.""" matches = self.__find_matching_methods(step_methods, augmented_predicate) return self.__select_best_match(matches) def __find_matching_methods(self, step_methods, augmented_predicate): for method, doc in self.__find_methods_with_docstring(step_methods): match = parse.parse(doc, augmented_predicate) if match: args = match.fixed kwargs = match.named yield (len(args) + len(kwargs), method, tuple(args), kwargs) def __find_methods_with_docstring(self, step_methods): for method_name in step_methods: method = self._suite.__getattribute__(method_name) doc = method.__doc__ if doc: yield method, doc def __select_best_match(self, matches): matches = sorted(matches, reverse=True) try: best_match = next(iter(matches)) except StopIteration: return None, (), {} else: _, method, args, kwargs = best_match return method, args, kwargs def replace_placeholders(self, predicate, arguments): arguments = iter(arguments) def repl(match): if match.group(0).startswith('"'): return '"{%s}"' % next(arguments) return "{%s}" % next(arguments) predicate = re.sub(r'".+?"|\<.+?\>', repl, predicate) return predicate PK!P&&morelia/parser.py# -*- coding: utf-8 -*- # __ __ _ _ # | \/ | ___ _ __ ___| (_) __ _ # | |\/| |/ _ \| '__/ _ \ | |/ _` | # | | | | (_) | | | __/ | | (_| | # |_| |_|\___/|_| \___|_|_|\__,_| # o o | o # ,_ __| , # | |_| / | | / | | / \_ # \/ |_/ |_/|_/\_/|_/|_/ \/ from pathlib import Path import re import textwrap from morelia.exceptions import MissingStepError from morelia.formatters import NullFormatter from morelia.grammar import ( And, Background, But, Comment, Examples, Feature, Given, Row, Scenario, Step, Then, When, ) from morelia.i18n import TRANSLATIONS from morelia.matchers import MethodNameStepMatcher, ParseStepMatcher, RegexpStepMatcher from morelia.visitors import TestVisitor def execute_script(script_root, suite, formatter=None, matchers=None, show_all_missing=True): if formatter is None: formatter = NullFormatter() if matchers is None: matchers = [RegexpStepMatcher, ParseStepMatcher, MethodNameStepMatcher] matcher = _create_matchers_chain(suite, matchers) if show_all_missing: _find_and_report_missing(script_root, matcher) test_visitor = TestVisitor(suite, matcher, formatter) script_root.accept(test_visitor) def _create_matchers_chain(suite, matcher_classes): root_matcher = None for matcher_class in matcher_classes: matcher = matcher_class(suite) try: root_matcher.add_matcher(matcher) except AttributeError: root_matcher = matcher return root_matcher def _find_and_report_missing(feature, matcher): not_matched = set() for descendant in feature: try: descendant.find_method(matcher) except MissingStepError as e: not_matched.add(e.suggest) suggest = "".join(not_matched) if suggest: diagnostic = "Cannot match steps:\n\n{}".format(suggest) assert False, diagnostic class Parser: def __init__(self, language=None): self.__node_classes = [ Feature, Background, Scenario, Given, When, Then, And, But, Row, Comment, Examples, Step, ] self.nodes = [] if language is None: language = "en" self.__language = language self.__continuation_marker_re = re.compile(r"\\\s*$") def parse_as_str(self, filename, prose, scenario=None): feature = self.parse_features(prose, scenario=scenario) feature.filename = filename return feature def parse_file(self, filename, scenario=r".*"): with Path(filename).open("rb") as input_file: return self.parse_as_str( filename=filename, prose=input_file.read().decode("utf-8"), scenario=scenario, ) def parse_features(self, prose, scenario=r".*"): self.parse_feature(prose) # Filter steps to only include requested scenarios try: scenario_re = re.compile(scenario) except Exception as e: raise SyntaxError( 'Invalid scenario matching regex "{}": {}'.format(scenario, e) ) matched_feature_steps = [] matched_steps = [] matching = True for s in self.nodes: if isinstance(s, Background): matched_feature_steps.append(s) matching = True elif isinstance(s, Scenario): matching = scenario_re.match(s.predicate) is not None if matching is True: matched_feature_steps.append(s) if matching is True: matched_steps.append(s) self.nodes = matched_steps self.nodes[0].steps = matched_feature_steps feature = self.nodes[0] assert isinstance(feature, Feature), "Exactly one Feature per file" feature.enforce( any(isinstance(step, Scenario) for step in feature.steps), "Feature without Scenario(s)", ) return self.nodes[0] def parse_feature(self, lines): self.__line_producer = LineSource(lines) self.__docstring_parser = DocStringParser(self.__line_producer) self.__language_parser = LanguageParser(default_language=self.__language) self.__labels_parser = LabelParser() try: while True: line = self.__line_producer.get_line() if line: self.__parse_line(line) except StopIteration: pass return self.nodes def __parse_line(self, line): if self.__language_parser.parse(line): self.__language = self.__language_parser.language return if self.__labels_parser.parse(line): return if self.__docstring_parser.parse(line): previous = self.nodes[-1] previous.payload = self.__docstring_parser.payload return if self.__parse_node(line): return if 0 < len(self.nodes): self.__append_to_previous_node(line) else: line_number = self.__line_producer.line_number s = Step(line, line_number=line_number) feature_name = TRANSLATIONS[self.__language_parser.language].get( "feature", "Feature" ) feature_name = feature_name.replace("|", " or ") s.enforce(False, "feature files must start with a %s" % feature_name) def __parse_node(self, line): line_number = self.__line_producer.line_number folded_lines = self.__read_folded_lines(line) line = line.rstrip() source = line + folded_lines for node_class in self.__node_classes: if node_class.match(line, self.__language): labels = self.__labels_parser.pop_labels() node = node_class( source=source, line_number=line_number, labels=labels, predecessors=self.nodes, language=self.__language, ) self.nodes.append(node) return node def __read_folded_lines(self, line): folded_lines = [""] while self.__continuation_marker_re.search(line): line = self.__line_producer.get_line() folded_lines.append(line) return "\n".join(folded_lines) def __append_to_previous_node(self, line): previous = self.nodes[-1] previous.append_line(line) class LabelParser: def __init__(self, labels_pattern=r"@\w+"): self._labels = [] self._labels_re = re.compile(labels_pattern) self._labels_prefix_re = re.compile(r"^\s*@") def parse(self, line): """Parse labels. :param str line: line to parse :returns: True if line contains labels :side effects: sets self._labels to parsed labels """ if self._labels_prefix_re.match(line): matches = self._labels_re.findall(line) if matches: self._labels.extend(matches) return True return False def pop_labels(self): """Return labels. :returns: labels :side effects: clears current labels """ labels = [label.strip("@") for label in self._labels] self._labels = [] return labels class LanguageParser: def __init__(self, lang_pattern=r"^# language: (\w+)", default_language=None): if default_language is None: default_language = "en" self.__language = default_language self.__lang_re = re.compile(lang_pattern) def parse(self, line): """Parse language directive. :param str line: line to parse :returns: True if line contains language directive :side effects: sets self.language to parsed language """ match = self.__lang_re.match(line) if match: self.__language = match.groups()[0] return True return False @property def language(self): return self.__language class DocStringParser: def __init__(self, source, pattern=r'\s*"""\s*'): self.__source = source self.__docstring_re = re.compile(pattern) self.__payload = [] def parse(self, line): """Parse docstring payload. :param str line: first line to parse :returns: True if docstring parsed :side effects: sets self.payload to parsed docstring """ match = self.__docstring_re.match(line) if match: start_line = line self.__payload = [] line = self.__source.get_line() while line != start_line: self.__payload.append(line) line = self.__source.get_line() return True return False @property def payload(self): return textwrap.dedent("\n".join(self.__payload)) class LineSource: def __init__(self, text): self.__lines = iter(line for line in text.split("\n") if line) self.__line_number = 0 def get_line(self): """Return next line. :returns: next line of text """ self.__line_number += 1 return next(self.__lines) @property def line_number(self): return self.__line_number PK!morelia/utils.pyPK!morelia/visitors.pyimport inspect import sys import time import traceback from gettext import ngettext from morelia.exceptions import MissingStepError def noop(): pass class TestVisitor: """Visits all steps and run step methods.""" def __init__(self, suite, matcher, formatter): self._setUp = suite.setUp self._tearDown = suite.tearDown self._suite = suite self._suite.setUp = self._suite.tearDown = noop self._matcher = matcher self._formatter = formatter self._exceptions = [] self._scenarios_failed = 0 self._scenarios_passed = 0 self._scenarios_num = 0 self._scenario_exception = None self._steps_num = 0 def visit_feature(self, node, children=[]): try: self._feature_visit(node) self.__visit_children(children) finally: self._feature_after_visit(node) def _feature_visit(self, node): self._exceptions = [] self._scenarios_failed = 0 self._scenarios_passed = 0 self._scenarios_num = 0 line = node.interpolated_source() self._formatter.output(node, line, "", 0) def _feature_after_visit(self, node): if self._scenarios_failed: self._fail_feature() def _fail_feature(self): failed_msg = ngettext( "{} scenario failed", "{} scenarios failed", self._scenarios_failed ) passed_msg = ngettext( "{} scenario passed", "{} scenarios passed", self._scenarios_passed ) msg = "{}, {}".format(failed_msg, passed_msg).format( self._scenarios_failed, self._scenarios_passed ) prefix = "-" * 66 for step_line, tb, exc in self._exceptions: msg += "\n{}{}\n{}{}".format(prefix, step_line, tb, exc).replace( "\n", "\n " ) assert self._scenarios_failed == 0, msg def visit_scenario(self, node, children=[]): try: self._scenario_visit(node) self.__visit_children(children) finally: self._scenario_after_visit(node) def visit_step(self, node, children=[]): self._step_visit(node) self.__visit_children(children) def visit(self, node, children=[]): line = node.interpolated_source() self._formatter.output(node, line, "", 0) self.__visit_children(children) visit_background = visit_row = visit_examples = visit_comment = visit def __visit_children(self, children): for child in children: child.accept(self) def _scenario_visit(self, node): self._scenario_exception = None if self._scenarios_num != 0: self._setUp() self._scenarios_num += 1 line = node.interpolated_source() self._formatter.output(node, line, "", 0) def _scenario_after_visit(self, node): if self._scenario_exception: self._exceptions.append(self._scenario_exception) self._scenarios_failed += 1 else: self._scenarios_passed += 1 self._tearDown() def _step_visit(self, node): if self._scenario_exception: return self._suite.step = node self._steps_num += 1 reconstruction = node.interpolated_source() start_time = time.time() status = "pass" try: self.run_step(node) except (MissingStepError, AssertionError): status = "fail" etype, evalue, etraceback = sys.exc_info() tb = traceback.extract_tb(etraceback)[:-2] self._scenario_exception = ( node.parent.interpolated_source() + reconstruction, "".join(traceback.format_list(tb)), "".join(traceback.format_exception_only(etype, evalue)), ) except (SystemExit, Exception) as exc: status = "error" self._format_exception(node, exc) raise finally: end_time = time.time() duration = end_time - start_time self._formatter.output(node, reconstruction, status, duration) def _format_exception(self, node, exc): if len(exc.args): message = node.format_fault(exc.args[0]) exc.args = (message,) + exc.args[1:] def run_step(self, node): method, args, kwargs = node.find_method(self._matcher) spec = None arglist = [] spec = inspect.getfullargspec(method) arglist = spec.args + spec.kwonlyargs if "_labels" in arglist: kwargs["_labels"] = node.get_labels() if "_text" in arglist: kwargs["_text"] = node.payload method(*args, **kwargs) PK!pC@@morelia-0.8.3.dist-info/LICENSEThe MIT License (MIT) Copyright (c) 2009-2015, Morelia authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUmorelia-0.8.3.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H/2 " morelia-0.8.3.dist-info/METADATAZmw۶_9ݱI8MSsi^]N) H@ eKݹ;?{_6Rvoy&,SZͧy$(ŀf;/Dgi99;3"agg{cEīޘG20iYX+bEiM3K!> rӔ-TXp[jaT&z(,0~"mRHeQcۯ-#,~s~|/sc3`VqwT6QzbP#՛ȸLO!Xrm/zM2c)Z`O_iʍc)evavz<"ENvፕiŻsvRZ@:p{?%[򔽮/I*Mq禋"2], /+jyA çV(.U!#p8s-V[L;`BHW~@+O_Å** nUZ ȄREO)m{Q(#ҋrLTީвE!( m G TT ('Ih))Z, ٟ'B:4I ,a6,C'{bĮʓ;P,f)REJκN. XwQ:VAM/C}-ߌ4ϣ8@^\Z(e390"-uД+G(_HĵEiU[[sKpLAZłvbDçk%6ɍ`a쥺lR?<_0y[J"^犙BDĐ2@Fu$j`S%fQgu)J(ND]i''b@ch91 gG _&*m Ǐp)V!=ybf O}/ nFsٟj~!HpgϞW$s@(;\9n=NI)E(*Rvm3G?2PNJ |gV cvm%o>b訄J1`'q,fXg 72M,C@R47 OG Tvɀ͑:UX`< Sfz;+rQds[L[B#b-#JyӤT`dkٝ9M<$hLQ*el}<  8E͂?aK@+ 5C$JMY٤kS6y#FYerUVbHM1)7S3 7{GX$J*:FjU4ׄ6^,6eıK-w|K*)\Zna!#5N+n: 'upV{iz|߾QW2;w tްMRc.@}", @g3<9=X4ֹioƞ{{DZ԰}dl)"j"6Z;zڥ⹲%H[`' {8%Љ%Ww: MmZ$Z'ۘoc#QQcp}7Xs#z=Vy|MDFSլʡϽ "X$$͟Ěp# ɗ v6*f˳3ǜ'0^ӑ_sjX,1ҭ95UprB87]wqAܾp䎊3OeΖd^8Z|Vl04p:ՙ Y<abtݙ'U|%W7&|ȁkCo9"VQCYOgq],!L$]=DS:E|ȝž;nT}RC:j}A9ÐU]8Bga2bW;' PK!HRTcmorelia-0.8.3.dist-info/RECORDu˒J| @@@ !J)׏۶s#NdFfՐwQ>>C,X9e= 65 4e9d)W7uoqO0=KFD11؞ŃtXRR`^%oZ(o 9ȒLkMR+5}b%Ғ)N7& Wj0 WHwJʳϓPaCq<҆ThqT5oj0;^ӶY2]Ꙍ{iW 7jz+Kl֊/10wL"Z_窠~ !^G/.6ݹm =O$yxKS,`o/-m-¡x6x2\i} 1~EaH;Qk;}nGXn6cf%CI^zm.kKE]OH}7X>JG2[Z"E'(<IҐEwè7U궠?amjC˽o8^]b#@~KIHڅsJAuU$ޜΟE&..RU ]C:v*(hԃH-a# Ի٪ݼG_ \ @ޫW(_PK!)morelia/__init__.pyPK!t  morelia/config.pyPK!  morelia/decorators.pyPK!jhrr9morelia/exceptions.pyPK!и9 morelia/formatters.pyPK!**>morelia/grammar.pyPK!)(Bvbvb,imorelia/i18n.pyPK!lEEmorelia/matchers.pyPK!P&&morelia/parser.pyPK!/8morelia/utils.pyPK!]8morelia/visitors.pyPK!pC@@=Kmorelia-0.8.3.dist-info/LICENSEPK!HڽTUOmorelia-0.8.3.dist-info/WHEELPK!H/2 " IPmorelia-0.8.3.dist-info/METADATAPK!HRTc[morelia-0.8.3.dist-info/RECORDPK^