PageRenderTime 56ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/lettuce/core.py

https://github.com/OddBloke/lettuce
Python | 1322 lines | 1271 code | 33 blank | 18 comment | 14 complexity | 9aa9a9e99f37857b31b42722d13af88c MD5 | raw file
Possible License(s): GPL-3.0, BSD-3-Clause
  1. # -*- coding: utf-8 -*-
  2. # <Lettuce - Behaviour Driven Development for python>
  3. # Copyright (C) <2010-2012> Gabriel Falc達o <gabriel@nacaolivre.org>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. import re
  18. import codecs
  19. import unicodedata
  20. from copy import deepcopy
  21. from fuzzywuzzy import fuzz
  22. from itertools import chain
  23. from random import shuffle
  24. from lettuce import strings
  25. from lettuce import languages
  26. from lettuce.fs import FileSystem
  27. from lettuce.registry import STEP_REGISTRY
  28. from lettuce.registry import call_hook
  29. from lettuce.exceptions import ReasonToFail
  30. from lettuce.exceptions import NoDefinitionFound
  31. from lettuce.exceptions import LettuceSyntaxError
  32. fs = FileSystem()
  33. class REP(object):
  34. "RegEx Pattern"
  35. first_of = re.compile(ur'^first_of_')
  36. last_of = re.compile(ur'^last_of_')
  37. language = re.compile(u"language:[ ]*([^\s]+)")
  38. within_double_quotes = re.compile(r'("[^"]+")')
  39. within_single_quotes = re.compile(r"('[^']+')")
  40. only_whitespace = re.compile('^\s*$')
  41. tag_extraction_regex = re.compile(r'(?:(?:^|\s+)[@]([^@\s]+))')
  42. tag_strip_regex = re.compile(ur'(?:(?:^\s*|\s+)[@]\S+\s*)+$', re.DOTALL)
  43. comment_strip1 = re.compile(ur'(^[^\'"]*)[#]([^\'"]*)$')
  44. comment_strip2 = re.compile(ur'(^[^\'"]+)[#](.*)$')
  45. class HashList(list):
  46. __base_msg = 'The step "%s" have no table defined, so ' \
  47. 'that you can\'t use step.hashes.%s'
  48. def __init__(self, step, *args, **kw):
  49. self.step = step
  50. super(HashList, self).__init__(*args, **kw)
  51. def values_under(self, key):
  52. msg = 'The step "%s" have no table column with the key "%s". ' \
  53. 'Could you check your step definition for that ? ' \
  54. 'Maybe there is a typo :)'
  55. try:
  56. return [h[key] for h in self]
  57. except KeyError:
  58. raise AssertionError(msg % (self.step.sentence, key))
  59. @property
  60. def first(self):
  61. if len(self) > 0:
  62. return self[0]
  63. raise AssertionError(self.__base_msg % (self.step.sentence, 'first'))
  64. @property
  65. def last(self):
  66. if len(self) > 0:
  67. return self[-1]
  68. raise AssertionError(self.__base_msg % (self.step.sentence, 'last'))
  69. class Language(object):
  70. code = 'en'
  71. name = 'English'
  72. native = 'English'
  73. feature = 'Feature'
  74. scenario = 'Scenario'
  75. examples = 'Examples|Scenarios'
  76. scenario_outline = 'Scenario Outline'
  77. scenario_separator = 'Scenario( Outline)?'
  78. background = "Background"
  79. def __init__(self, code=u'en'):
  80. self.code = code
  81. for attr, value in languages.LANGUAGES[code].items():
  82. setattr(self, attr, unicode(value))
  83. def __repr__(self):
  84. return '<Language "%s">' % self.code
  85. def __getattr__(self, attr):
  86. for pattern in [REP.first_of, REP.last_of]:
  87. if pattern.match(attr):
  88. name = pattern.sub(u'', attr)
  89. return unicode(getattr(self, name, u'').split(u"|")[0])
  90. return super(Language, self).__getattribute__(attr)
  91. @property
  92. def non_capturable_scenario_separator(self):
  93. return re.sub(r'^[(]', '(?:', self.scenario_separator)
  94. @classmethod
  95. def guess_from_string(cls, string):
  96. match = re.search(REP.language, string)
  97. if match:
  98. instance = cls(match.group(1))
  99. else:
  100. instance = cls()
  101. return instance
  102. class StepDefinition(object):
  103. """A step definition is a wrapper for user-defined callbacks. It
  104. gets a few metadata from file, such as filename and line number"""
  105. def __init__(self, step, function):
  106. self.function = function
  107. self.file = fs.relpath(function.func_code.co_filename)
  108. self.line = function.func_code.co_firstlineno + 1
  109. self.step = step
  110. def __call__(self, *args, **kw):
  111. """Method that actually wrapps the call to step definition
  112. callback. Sends step object as first argument
  113. """
  114. try:
  115. ret = self.function(self.step, *args, **kw)
  116. self.step.passed = True
  117. except Exception, e:
  118. self.step.failed = True
  119. self.step.why = ReasonToFail(self.step, e)
  120. raise
  121. return ret
  122. class StepDescription(object):
  123. """A simple object that holds filename and line number of a step
  124. description (step within feature file)"""
  125. def __init__(self, line, filename):
  126. self.file = filename
  127. if self.file:
  128. self.file = fs.relpath(self.file)
  129. else:
  130. self.file = "unknown file"
  131. self.line = line or 0
  132. class ScenarioDescription(object):
  133. """A simple object that holds filename and line number of a scenario
  134. description (scenario within feature file)"""
  135. def __init__(self, scenario, filename, string, language):
  136. self.file = fs.relpath(filename)
  137. self.line = None
  138. for pline, part in enumerate(string.splitlines()):
  139. part = part.strip()
  140. if re.match(u"%s:[ ]+" % language.scenario_separator + re.escape(scenario.name), part):
  141. self.line = pline + 1
  142. break
  143. class FeatureDescription(object):
  144. """A simple object that holds filename and line number of a feature
  145. description"""
  146. def __init__(self, feature, filename, string, language):
  147. lines = [l.strip() for l in string.splitlines()]
  148. self.file = fs.relpath(filename)
  149. self.line = None
  150. described_at = []
  151. description_lines = strings.get_stripped_lines(feature.description)
  152. for pline, part in enumerate(lines):
  153. part = part.strip()
  154. line = pline + 1
  155. if re.match(u"(?:%s): " % language.feature, part):
  156. self.line = line
  157. else:
  158. for description in description_lines:
  159. if part == description:
  160. described_at.append(line)
  161. self.description_at = tuple(described_at)
  162. class Step(object):
  163. """ Object that represents each step on feature files."""
  164. has_definition = False
  165. indentation = 4
  166. table_indentation = indentation + 2
  167. defined_at = None
  168. why = None
  169. ran = False
  170. passed = None
  171. failed = None
  172. related_outline = None
  173. scenario = None
  174. background = None
  175. def __init__(self, sentence, remaining_lines, line=None, filename=None):
  176. self.sentence = sentence
  177. self.original_sentence = sentence
  178. self._remaining_lines = remaining_lines
  179. keys, hashes, self.multiline = self._parse_remaining_lines(remaining_lines)
  180. self.keys = tuple(keys)
  181. self.hashes = HashList(self, hashes)
  182. self.described_at = StepDescription(line, filename)
  183. self.proposed_method_name, self.proposed_sentence = self.propose_definition()
  184. def propose_definition(self):
  185. sentence = unicode(self.original_sentence)
  186. method_name = sentence
  187. groups = [
  188. ('"', REP.within_double_quotes, r'"([^"]*)"'),
  189. ("'", REP.within_single_quotes, r"\'([^\']*)\'"),
  190. ]
  191. attribute_names = []
  192. for char, group, template in groups:
  193. match_groups = group.search(self.original_sentence)
  194. if match_groups:
  195. for index, match in enumerate(group.findall(sentence)):
  196. sentence = sentence.replace(match, template)
  197. group_name = u"group%d" % (index + 1)
  198. method_name = method_name.replace(match, group_name)
  199. attribute_names.append(group_name)
  200. method_name = unicodedata.normalize('NFKD', method_name) \
  201. .encode('ascii', 'ignore')
  202. method_name = '%s(step%s)' % (
  203. "_".join(re.findall("\w+", method_name)).lower(),
  204. attribute_names and (", %s" % ", ".join(attribute_names)) or "")
  205. return method_name, sentence
  206. def solve_and_clone(self, data):
  207. sentence = self.sentence
  208. hashes = self.hashes[:] # deep copy
  209. for k, v in data.items():
  210. def evaluate(stuff):
  211. return stuff.replace(u'<%s>' % unicode(k), unicode(v))
  212. def evaluate_hash_value(hash_row):
  213. new_row = {}
  214. for rkey, rvalue in hash_row.items():
  215. new_row[rkey] = evaluate(rvalue)
  216. return new_row
  217. sentence = evaluate(sentence)
  218. hashes = map(evaluate_hash_value, hashes)
  219. new = deepcopy(self)
  220. new.sentence = sentence
  221. new.hashes = hashes
  222. return new
  223. def _calc_list_length(self, lst):
  224. length = self.table_indentation + 2
  225. for item in lst:
  226. length += strings.column_width(item) + 2
  227. if len(lst) > 1:
  228. length += 1
  229. return length
  230. def _calc_key_length(self, data):
  231. return self._calc_list_length(data.keys())
  232. def _calc_value_length(self, data):
  233. return self._calc_list_length(data.values())
  234. @property
  235. def max_length(self):
  236. max_length_sentence = strings.column_width(self.sentence) + \
  237. self.indentation
  238. max_length_original = strings.column_width(self.original_sentence) + \
  239. self.indentation
  240. max_length = max([max_length_original, max_length_sentence])
  241. for data in self.hashes:
  242. key_size = self._calc_key_length(data)
  243. if key_size > max_length:
  244. max_length = key_size
  245. value_size = self._calc_value_length(data)
  246. if value_size > max_length:
  247. max_length = value_size
  248. return max_length
  249. @property
  250. def parent(self):
  251. return self.scenario or self.background
  252. def represent_string(self, string):
  253. head = ' ' * self.indentation + string
  254. where = self.described_at
  255. if self.defined_at:
  256. where = self.defined_at
  257. return strings.rfill(head, self.parent.feature.max_length + 1, append=u'# %s:%d\n' % (where.file, where.line))
  258. def represent_hashes(self):
  259. lines = strings.dicts_to_string(self.hashes, self.keys).splitlines()
  260. return u"\n".join([(u" " * self.table_indentation) + line for line in lines]) + "\n"
  261. def __repr__(self):
  262. return u'<Step: "%s">' % self.sentence
  263. def _parse_remaining_lines(self, lines):
  264. multiline = strings.parse_multiline(lines)
  265. keys, hashes = strings.parse_hashes(lines)
  266. return keys, hashes, multiline
  267. def _get_match(self, ignore_case):
  268. matched, func = None, lambda: None
  269. for regex, func in STEP_REGISTRY.items():
  270. matched = re.search(regex, self.sentence, ignore_case and re.I or 0)
  271. if matched:
  272. break
  273. return matched, StepDefinition(self, func)
  274. def pre_run(self, ignore_case, with_outline=None):
  275. matched, step_definition = self._get_match(ignore_case)
  276. self.related_outline = with_outline
  277. if not self.defined_at:
  278. if not matched:
  279. raise NoDefinitionFound(self)
  280. self.has_definition = True
  281. self.defined_at = step_definition
  282. return matched, step_definition
  283. def given(self, string):
  284. return self.behave_as(string)
  285. def when(self, string):
  286. return self.behave_as(string)
  287. def then(self, string):
  288. return self.behave_as(string)
  289. def behave_as(self, string):
  290. """ Parses and runs steps given in string form.
  291. In your step definitions, you can use this to run one step from another.
  292. e.g.
  293. @step('something ordinary')
  294. def something(step):
  295. step.behave_as('Given something defined elsewhere')
  296. @step('something defined elsewhere')
  297. def elsewhere(step):
  298. # actual step behavior, maybe.
  299. This will raise the error of the first failing step (thus halting
  300. execution of the step) if a subordinate step fails.
  301. """
  302. lines = string.split('\n')
  303. steps = self.many_from_lines(lines)
  304. if hasattr(self, 'scenario'):
  305. for step in steps:
  306. step.scenario = self.scenario
  307. (_, _, steps_failed, steps_undefined, _) = self.run_all(steps)
  308. if not steps_failed and not steps_undefined:
  309. self.passed = True
  310. self.failed = False
  311. return self.passed
  312. self.passed = False
  313. self.failed = True
  314. assert not steps_failed, steps_failed[0].why.exception
  315. assert not steps_undefined, "Undefined step: %s" % steps_undefined[0].sentence
  316. def run(self, ignore_case):
  317. """Runs a step, trying to resolve it on available step
  318. definitions"""
  319. matched, step_definition = self.pre_run(ignore_case)
  320. self.ran = True
  321. kw = matched.groupdict()
  322. if kw:
  323. step_definition(**kw)
  324. else:
  325. groups = matched.groups()
  326. step_definition(*groups)
  327. self.passed = True
  328. return True
  329. @classmethod
  330. def _handle_inline_comments(klass, line):
  331. line = REP.comment_strip1.sub(r'\g<1>\g<2>', line)
  332. line = REP.comment_strip2.sub(r'\g<1>', line)
  333. return line
  334. @staticmethod
  335. def run_all(steps, outline=None, run_callbacks=False, ignore_case=True, failfast=False):
  336. """Runs each step in the given list of steps.
  337. Returns a tuple of five lists:
  338. - The full set of steps executed
  339. - The steps that passed
  340. - The steps that failed
  341. - The steps that were undefined
  342. - The reason for each failing step (indices matching per above)
  343. """
  344. all_steps = []
  345. steps_passed = []
  346. steps_failed = []
  347. steps_undefined = []
  348. reasons_to_fail = []
  349. for step in steps:
  350. if outline:
  351. step = step.solve_and_clone(outline)
  352. try:
  353. step.pre_run(ignore_case, with_outline=outline)
  354. if run_callbacks:
  355. call_hook('before_each', 'step', step)
  356. if not steps_failed and not steps_undefined:
  357. step.run(ignore_case)
  358. steps_passed.append(step)
  359. except NoDefinitionFound, e:
  360. steps_undefined.append(e.step)
  361. except Exception, e:
  362. if failfast:
  363. raise
  364. steps_failed.append(step)
  365. reasons_to_fail.append(step.why)
  366. finally:
  367. all_steps.append(step)
  368. if run_callbacks:
  369. call_hook('after_each', 'step', step)
  370. return (all_steps, steps_passed, steps_failed, steps_undefined, reasons_to_fail)
  371. @classmethod
  372. def many_from_lines(klass, lines, filename=None, original_string=None):
  373. """Parses a set of steps from lines of input.
  374. This will correctly parse and produce a list of steps from lines without
  375. any Scenario: heading at the top. Examples in table form are correctly
  376. parsed, but must be well-formed under a regular step sentence.
  377. """
  378. invalid_first_line_error = '\nFirst line of step "%s" is in %s form.'
  379. if lines and strings.wise_startswith(lines[0], u'|'):
  380. raise LettuceSyntaxError(
  381. None,
  382. invalid_first_line_error % (lines[0], 'table'))
  383. if lines and strings.wise_startswith(lines[0], u'"""'):
  384. raise LettuceSyntaxError(
  385. None,
  386. invalid_first_line_error % (lines[0], 'multiline'))
  387. # Select only lines that aren't end-to-end whitespace and aren't tags
  388. # Tags could be inclueed as steps if the first scenario following a background is tagged
  389. # This then causes the test to fail, because lettuce looks for the step's definition (which doesn't exist)
  390. lines = filter(lambda x: not (REP.only_whitespace.match(x) or re.match(r'^\s*@', x)), lines)
  391. step_strings = []
  392. in_multiline = False
  393. for line in lines:
  394. if strings.wise_startswith(line, u'"""'):
  395. in_multiline = not in_multiline
  396. step_strings[-1] += "\n%s" % line
  397. elif strings.wise_startswith(line, u"|") or in_multiline:
  398. step_strings[-1] += "\n%s" % line
  399. elif '#' in line:
  400. step_strings.append(klass._handle_inline_comments(line))
  401. else:
  402. step_strings.append(line)
  403. mkargs = lambda s: [s, filename, original_string]
  404. return [klass.from_string(*mkargs(s)) for s in step_strings]
  405. @classmethod
  406. def from_string(cls, string, with_file=None, original_string=None):
  407. """Creates a new step from string"""
  408. lines = strings.get_stripped_lines(string)
  409. sentence = lines.pop(0)
  410. line = None
  411. if with_file and original_string:
  412. for pline, line in enumerate(original_string.splitlines()):
  413. if sentence in line:
  414. line = pline + 1
  415. break
  416. return cls(sentence,
  417. remaining_lines=lines,
  418. line=line,
  419. filename=with_file)
  420. class Scenario(object):
  421. """ Object that represents each scenario on feature files."""
  422. described_at = None
  423. indentation = 2
  424. table_indentation = indentation + 2
  425. def __init__(self, name, remaining_lines, keys, outlines,
  426. with_file=None,
  427. original_string=None,
  428. language=None,
  429. previous_scenario=None):
  430. self.feature = None
  431. if not language:
  432. language = language()
  433. self.name = name
  434. self.language = language
  435. self.remaining_lines = remaining_lines
  436. self.original_steps = self._parse_remaining_lines(remaining_lines,
  437. with_file,
  438. original_string)
  439. self.steps = deepcopy(self.original_steps)
  440. self.keys = keys
  441. self.outlines = outlines
  442. self.with_file = with_file
  443. self.original_string = original_string
  444. self.previous_scenario = previous_scenario
  445. if with_file and original_string:
  446. scenario_definition = ScenarioDescription(self, with_file,
  447. original_string,
  448. language)
  449. self._set_definition(scenario_definition)
  450. self.solved_steps = list(self._resolve_steps(
  451. self.steps, self.outlines, with_file, original_string))
  452. self._add_myself_to_steps()
  453. if original_string and '@' in self.original_string:
  454. self.tags = self._find_tags_in(original_string)
  455. else:
  456. self.tags = []
  457. @property
  458. def max_length(self):
  459. if self.outlines:
  460. prefix = self.language.first_of_scenario_outline + ":"
  461. else:
  462. prefix = self.language.first_of_scenario + ":"
  463. max_length = strings.column_width(
  464. u"%s %s" % (prefix, self.name)) + self.indentation
  465. for step in self.steps:
  466. if step.max_length > max_length:
  467. max_length = step.max_length
  468. for outline in self.outlines:
  469. key_size = self._calc_key_length(outline)
  470. if key_size > max_length:
  471. max_length = key_size
  472. value_size = self._calc_value_length(outline)
  473. if value_size > max_length:
  474. max_length = value_size
  475. return max_length
  476. def _calc_list_length(self, lst):
  477. length = self.table_indentation + 2
  478. for item in lst:
  479. length += len(item) + 2
  480. if len(lst) > 1:
  481. length += 2
  482. return length
  483. def _calc_key_length(self, data):
  484. return self._calc_list_length(data.keys())
  485. def _calc_value_length(self, data):
  486. return self._calc_list_length(data.values())
  487. def __repr__(self):
  488. return u'<Scenario: "%s">' % self.name
  489. def matches_tags(self, tags):
  490. if tags is None:
  491. return True
  492. has_exclusionary_tags = any([t.startswith('-') for t in tags])
  493. if not self.tags and not has_exclusionary_tags:
  494. return False
  495. matched = []
  496. if isinstance(self.tags, list):
  497. for tag in self.tags:
  498. if tag in tags:
  499. return True
  500. else:
  501. self.tags = []
  502. for tag in tags:
  503. exclude = tag.startswith('-')
  504. if exclude:
  505. tag = tag[1:]
  506. fuzzable = tag.startswith('~')
  507. if fuzzable:
  508. tag = tag[1:]
  509. result = tag in self.tags
  510. if fuzzable:
  511. fuzzed = []
  512. for internal_tag in self.tags:
  513. ratio = fuzz.ratio(tag, internal_tag)
  514. if exclude:
  515. fuzzed.append(ratio <= 80)
  516. else:
  517. fuzzed.append(ratio > 80)
  518. result = any(fuzzed)
  519. elif exclude:
  520. result = tag not in self.tags
  521. matched.append(result)
  522. return all(matched)
  523. @property
  524. def evaluated(self):
  525. for outline in self.outlines:
  526. steps = []
  527. for step in self.steps:
  528. new_step = step.solve_and_clone(outline)
  529. new_step.original_sentence = step.sentence
  530. new_step.scenario = self
  531. steps.append(new_step)
  532. yield (outline, steps)
  533. @property
  534. def ran(self):
  535. return all([step.ran for step in self.steps])
  536. @property
  537. def passed(self):
  538. return self.ran and all([step.passed for step in self.steps])
  539. @property
  540. def failed(self):
  541. return any([step.failed for step in self.steps])
  542. def run(self, ignore_case, failfast=False):
  543. """Runs a scenario, running each of its steps. Also call
  544. before_each and after_each callbacks for steps and scenario"""
  545. results = []
  546. call_hook('before_each', 'scenario', self)
  547. def run_scenario(almost_self, order=-1, outline=None, run_callbacks=False):
  548. total_attempts = 3 if 'retry' in self.tags else 1
  549. attempts = 0
  550. while attempts < total_attempts:
  551. if attempts >= 1:
  552. call_hook('before_each', 'scenario_retry', self)
  553. try:
  554. if self.background:
  555. self.background.run(ignore_case)
  556. all_steps, steps_passed, steps_failed, steps_undefined, reasons_to_fail = Step.run_all(self.steps, outline, run_callbacks, ignore_case, failfast=failfast)
  557. except:
  558. if failfast:
  559. call_hook('after_each', 'scenario', self)
  560. raise
  561. if all_steps == steps_passed:
  562. break
  563. new_steps = []
  564. for step in self.original_steps:
  565. new_step = deepcopy(step)
  566. new_step.original_sentence = step.sentence
  567. new_step.scenario = self
  568. new_steps.append(new_step)
  569. self.steps = new_steps
  570. attempts += 1
  571. skip = lambda x: x not in steps_passed and x not in steps_undefined and x not in steps_failed
  572. steps_skipped = filter(skip, all_steps)
  573. if outline:
  574. call_hook('outline', 'scenario', self, order, outline,
  575. reasons_to_fail)
  576. return ScenarioResult(
  577. self,
  578. steps_passed,
  579. steps_failed,
  580. steps_skipped,
  581. steps_undefined
  582. )
  583. if self.outlines:
  584. first = True
  585. for index, outline in enumerate(self.outlines):
  586. results.append(run_scenario(self, index, outline, run_callbacks=first))
  587. first = False
  588. else:
  589. results.append(run_scenario(self, run_callbacks=True))
  590. call_hook('after_each', 'scenario', self)
  591. return results
  592. def _add_myself_to_steps(self):
  593. for step in self.steps:
  594. step.scenario = self
  595. for step in self.solved_steps:
  596. step.scenario = self
  597. def _find_tags_in(self, original_string):
  598. broad_regex = re.compile(ur"([@].*)%s: (%s)" % (
  599. self.language.scenario_separator,
  600. re.escape(self.name)), re.DOTALL)
  601. regexes = []
  602. if not self.previous_scenario:
  603. regexes.append(broad_regex)
  604. else:
  605. regexes.append(re.compile(ur"(?:%s: %s.*)([@]?.*)%s: (%s)\s*\n" % (
  606. self.language.non_capturable_scenario_separator,
  607. re.escape(self.previous_scenario.name),
  608. self.language.scenario_separator,
  609. re.escape(self.name)), re.DOTALL))
  610. def try_finding_with(regex):
  611. found = regex.search(original_string)
  612. if found:
  613. tag_lines = found.group().splitlines()
  614. tags = list(chain(*map(self._extract_tag, tag_lines)))
  615. return tags
  616. for regex in regexes:
  617. found = try_finding_with(regex)
  618. if found:
  619. return found
  620. return []
  621. def _extract_tag(self, item):
  622. return REP.tag_extraction_regex.findall(item)
  623. def _resolve_steps(self, steps, outlines, with_file, original_string):
  624. for outline in outlines:
  625. for step in steps:
  626. yield step.solve_and_clone(outline)
  627. def _parse_remaining_lines(self, lines, with_file, original_string):
  628. invalid_first_line_error = '\nInvalid step on scenario "%s".\n' \
  629. 'Maybe you killed the first step text of that scenario\n'
  630. if lines and strings.wise_startswith(lines[0], u'|'):
  631. raise LettuceSyntaxError(
  632. with_file,
  633. invalid_first_line_error % self.name)
  634. return Step.many_from_lines(lines, with_file, original_string)
  635. def _set_definition(self, definition):
  636. self.described_at = definition
  637. def represented(self):
  638. make_prefix = lambda x: u"%s%s: " % (u' ' * self.indentation, x)
  639. if self.outlines:
  640. prefix = make_prefix(self.language.first_of_scenario_outline)
  641. else:
  642. prefix = make_prefix(self.language.first_of_scenario)
  643. head_parts = []
  644. if self.tags:
  645. tags = ['@%s' % t for t in self.tags]
  646. head_parts.append(u' ' * self.indentation)
  647. head_parts.append(' '.join(tags) + '\n')
  648. head_parts.append(prefix + self.name)
  649. head = ''.join(head_parts)
  650. appendix = ''
  651. if self.described_at:
  652. fmt = (self.described_at.file, self.described_at.line)
  653. appendix = u'# %s:%d\n' % fmt
  654. max_length = self.max_length
  655. if self.feature:
  656. max_length = self.feature.max_length
  657. return strings.rfill(
  658. head, max_length + 1,
  659. append=appendix)
  660. def represent_examples(self):
  661. lines = strings.dicts_to_string(self.outlines, self.keys).splitlines()
  662. return "\n".join([(u" " * self.table_indentation) + line for line in lines]) + '\n'
  663. @classmethod
  664. def from_string(new_scenario, string,
  665. with_file=None,
  666. original_string=None,
  667. language=None,
  668. previous_scenario=None):
  669. """ Creates a new scenario from string"""
  670. # ignoring comments
  671. string = "\n".join(strings.get_stripped_lines(string, ignore_lines_starting_with='#'))
  672. if not language:
  673. language = Language()
  674. splitted = strings.split_wisely(string, u"(%s):" % language.examples, True)
  675. string = splitted[0]
  676. keys = []
  677. outlines = []
  678. if len(splitted) > 1:
  679. parts = [l for l in splitted[1:] if l not in language.examples]
  680. part = "".join(parts)
  681. keys, outlines = strings.parse_hashes(strings.get_stripped_lines(part))
  682. lines = strings.get_stripped_lines(string)
  683. scenario_line = lines.pop(0).strip()
  684. for repl in (language.scenario_outline, language.scenario):
  685. scenario_line = strings.remove_it(scenario_line, u"(%s): " % repl).strip()
  686. scenario = new_scenario(
  687. name=scenario_line,
  688. remaining_lines=lines,
  689. keys=keys,
  690. outlines=outlines,
  691. with_file=with_file,
  692. original_string=original_string,
  693. language=language,
  694. previous_scenario=previous_scenario,
  695. )
  696. return scenario
  697. class Background(object):
  698. indentation = 2
  699. def __init__(self, lines, feature,
  700. with_file=None,
  701. original_string=None,
  702. language=None):
  703. self.steps = map(self.add_self_to_step, Step.many_from_lines(
  704. lines, with_file, original_string))
  705. self.feature = feature
  706. self.original_string = original_string
  707. self.language = language
  708. def add_self_to_step(self, step):
  709. step.background = self
  710. return step
  711. def run(self, ignore_case):
  712. call_hook('before_each', 'background', self)
  713. results = []
  714. for step in self.steps:
  715. matched, step_definition = step.pre_run(ignore_case)
  716. call_hook('before_each', 'step', step)
  717. try:
  718. results.append(step.run(ignore_case))
  719. except Exception, e:
  720. print e
  721. pass
  722. call_hook('after_each', 'step', step)
  723. call_hook('after_each', 'background', self, results)
  724. return results
  725. def __repr__(self):
  726. return '<Background for feature: {0}>'.format(self.feature.name)
  727. @property
  728. def max_length(self):
  729. max_length = 0
  730. for step in self.steps:
  731. if step.max_length > max_length:
  732. max_length = step.max_length
  733. return max_length
  734. def represented(self):
  735. return ((' ' * self.indentation) + 'Background:')
  736. @classmethod
  737. def from_string(new_background,
  738. lines,
  739. feature,
  740. with_file=None,
  741. original_string=None,
  742. language=None):
  743. return new_background(
  744. lines,
  745. feature,
  746. with_file=with_file,
  747. original_string=original_string,
  748. language=language)
  749. class Feature(object):
  750. """ Object that represents a feature."""
  751. described_at = None
  752. def __init__(self, name, remaining_lines, with_file, original_string,
  753. language=None):
  754. if not language:
  755. language = language()
  756. self.name = name
  757. self.language = language
  758. self.original_string = original_string
  759. (self.background,
  760. self.scenarios,
  761. self.description) = self._parse_remaining_lines(
  762. remaining_lines,
  763. original_string,
  764. with_file)
  765. if with_file:
  766. feature_definition = FeatureDescription(self,
  767. with_file,
  768. original_string,
  769. language)
  770. self._set_definition(feature_definition)
  771. if original_string and '@' in self.original_string:
  772. self.tags = self._find_tags_in(original_string)
  773. else:
  774. self.tags = None
  775. self._add_myself_to_scenarios()
  776. @property
  777. def max_length(self):
  778. max_length = strings.column_width(u"%s: %s" % (
  779. self.language.first_of_feature, self.name))
  780. if max_length == 0:
  781. # in case feature has two keywords
  782. max_length = strings.column_width(u"%s: %s" % (
  783. self.language.last_of_feature, self.name))
  784. for line in self.description.splitlines():
  785. length = strings.column_width(line.strip()) + Scenario.indentation
  786. if length > max_length:
  787. max_length = length
  788. for scenario in self.scenarios:
  789. if scenario.max_length > max_length:
  790. max_length = scenario.max_length
  791. return max_length
  792. def _add_myself_to_scenarios(self):
  793. for scenario in self.scenarios:
  794. scenario.feature = self
  795. if scenario.tags and self.tags:
  796. scenario.tags.extend(self.tags)
  797. def _find_tags_in(self, original_string):
  798. broad_regex = re.compile(ur"([@].*)%s: (%s)" % (
  799. self.language.feature,
  800. re.escape(self.name)), re.DOTALL)
  801. regexes = [broad_regex]
  802. def try_finding_with(regex):
  803. found = regex.search(original_string)
  804. if found:
  805. tag_lines = found.group().splitlines()
  806. tags = set(chain(*map(self._extract_tag, tag_lines)))
  807. return tags
  808. for regex in regexes:
  809. found = try_finding_with(regex)
  810. if found:
  811. return found
  812. return []
  813. def _extract_tag(self, item):
  814. regex = re.compile(r'(?:(?:^|\s+)[@]([^@\s]+))')
  815. found = regex.findall(item)
  816. return found
  817. def __repr__(self):
  818. return u'<%s: "%s">' % (self.language.first_of_feature, self.name)
  819. def get_head(self):
  820. return u"%s: %s" % (self.language.first_of_feature, self.name)
  821. def represented(self):
  822. length = self.max_length + 1
  823. filename = self.described_at.file
  824. line = self.described_at.line
  825. head = strings.rfill(self.get_head(), length,
  826. append=u"# %s:%d\n" % (filename, line))
  827. for description, line in zip(self.description.splitlines(),
  828. self.described_at.description_at):
  829. head += strings.rfill(
  830. u" %s" % description, length, append=u"# %s:%d\n" % (filename, line))
  831. return head
  832. @classmethod
  833. def from_string(new_feature, string, with_file=None, language=None):
  834. """Creates a new feature from string"""
  835. lines = strings.get_stripped_lines(
  836. string,
  837. ignore_lines_starting_with='#',
  838. )
  839. if not language:
  840. language = Language()
  841. found = len(re.findall(r'(?:%s):[ ]*\w+' % language.feature, "\n".join(lines), re.U))
  842. if found > 1:
  843. raise LettuceSyntaxError(with_file,
  844. 'A feature file must contain ONLY ONE feature!')
  845. elif found == 0:
  846. raise LettuceSyntaxError(with_file,
  847. 'Features must have a name. e.g: "Feature: This is my name"')
  848. while lines:
  849. matched = re.search(r'(?:%s):(.*)' % language.feature, lines[0], re.I)
  850. if matched:
  851. name = matched.groups()[0].strip()
  852. break
  853. lines.pop(0)
  854. feature = new_feature(name=name,
  855. remaining_lines=lines,
  856. with_file=with_file,
  857. original_string=string,
  858. language=language)
  859. return feature
  860. @classmethod
  861. def from_file(new_feature, filename):
  862. """Creates a new feature from filename"""
  863. f = codecs.open(filename, "r", "utf-8")
  864. string = f.read()
  865. f.close()
  866. language = Language.guess_from_string(string)
  867. feature = new_feature.from_string(string, with_file=filename, language=language)
  868. return feature
  869. def _set_definition(self, definition):
  870. self.described_at = definition
  871. def _strip_next_scenario_tags(self, string):
  872. stripped = REP.tag_strip_regex.sub('', string)
  873. return stripped
  874. def _extract_desc_and_bg(self, joined):
  875. if not re.search(self.language.background, joined):
  876. return joined, None
  877. parts = strings.split_wisely(
  878. joined, "(%s):\s*" % self.language.background)
  879. description = parts.pop(0)
  880. if not re.search(self.language.background, description):
  881. if parts:
  882. parts = parts[1:]
  883. else:
  884. description = ""
  885. background_string = "".join(parts).splitlines()
  886. return description, background_string
  887. def _check_scenario_syntax(self, lines, filename):
  888. empty_scenario = ('%s:' % (self.language.first_of_scenario)).lower()
  889. for line in lines:
  890. if line.lower() == empty_scenario:
  891. raise LettuceSyntaxError(
  892. filename,
  893. ('In the feature "%s", scenarios '
  894. 'must have a name, make sure to declare a scenario like '
  895. 'this: `Scenario: name of your scenario`' % self.name),
  896. )
  897. def _parse_remaining_lines(self, lines, original_string, with_file=None):
  898. joined = u"\n".join(lines[1:])
  899. self._check_scenario_syntax(lines, filename=with_file)
  900. # replacing occurrences of Scenario Outline, with just "Scenario"
  901. scenario_prefix = u'%s:' % self.language.first_of_scenario
  902. regex = re.compile(
  903. ur"%s:[\t\r\f\v]*" % self.language.scenario_separator, re.U | re.I | re.DOTALL)
  904. joined = regex.sub(scenario_prefix, joined)
  905. parts = strings.split_wisely(joined, scenario_prefix)
  906. description = u""
  907. background = None
  908. if not re.search("^" + scenario_prefix, joined):
  909. if not parts:
  910. raise LettuceSyntaxError(
  911. with_file,
  912. (u"Features must have scenarios.\n"
  913. "Please refer to the documentation available at http://lettuce.it for more information.")
  914. )
  915. description, background_lines = self._extract_desc_and_bg(parts[0])
  916. background = background_lines and Background.from_string(
  917. background_lines,
  918. self,
  919. with_file=with_file,
  920. original_string=original_string,
  921. language=self.language,
  922. ) or None
  923. parts.pop(0)
  924. prefix = self.language.first_of_scenario
  925. upcoming_scenarios = [
  926. u"%s: %s" % (prefix, s) for s in parts if s.strip()]
  927. kw = dict(
  928. original_string=original_string,
  929. with_file=with_file,
  930. language=self.language,
  931. )
  932. scenarios = []
  933. while upcoming_scenarios:
  934. current = self._strip_next_scenario_tags(upcoming_scenarios.pop(0))
  935. previous_scenario = None
  936. has_previous = len(scenarios) > 0
  937. if has_previous:
  938. previous_scenario = scenarios[-1]
  939. params = dict(
  940. previous_scenario=previous_scenario,
  941. )
  942. params.update(kw)
  943. current_scenario = Scenario.from_string(current, **params)
  944. current_scenario.background = background
  945. scenarios.append(current_scenario)
  946. return background, scenarios, description
  947. def run(self, scenarios=None, ignore_case=True, tags=None, random=False, failfast=False):
  948. call_hook('before_each', 'feature', self)
  949. scenarios_ran = []
  950. if random:
  951. shuffle(self.scenarios)
  952. if isinstance(scenarios, (tuple, list)):
  953. if all(map(lambda x: isinstance(x, int), scenarios)):
  954. scenarios_to_run = scenarios
  955. else:
  956. scenarios_to_run = range(1, len(self.scenarios) + 1)
  957. try:
  958. for index, scenario in enumerate(self.scenarios):
  959. if scenarios_to_run and (index + 1) not in scenarios_to_run:
  960. continue
  961. if not scenario.matches_tags(tags):
  962. continue
  963. scenarios_ran.extend(scenario.run(ignore_case, failfast=failfast))
  964. except:
  965. if failfast:
  966. call_hook('after_each', 'feature', self)
  967. raise
  968. else:
  969. call_hook('after_each', 'feature', self)
  970. return FeatureResult(self, *scenarios_ran)
  971. class FeatureResult(object):
  972. """Object that holds results of each scenario ran from within a feature"""
  973. def __init__(self, feature, *scenario_results):
  974. self.feature = feature
  975. self.scenario_results = scenario_results
  976. @property
  977. def passed(self):
  978. return all([result.passed for result in self.scenario_results])
  979. class ScenarioResult(object):
  980. """Object that holds results of each step ran from within a scenario"""
  981. def __init__(self, scenario, steps_passed, steps_failed, steps_skipped,
  982. steps_undefined):
  983. self.scenario = scenario
  984. self.steps_passed = steps_passed
  985. self.steps_failed = steps_failed
  986. self.steps_skipped = steps_skipped
  987. self.steps_undefined = steps_undefined
  988. all_lists = [steps_passed + steps_skipped + steps_undefined + steps_failed]
  989. self.total_steps = sum(map(len, all_lists))
  990. @property
  991. def passed(self):
  992. return self.total_steps is len(self.steps_passed)
  993. class TotalResult(object):
  994. def __init__(self, feature_results):
  995. self.feature_results = feature_results
  996. self.scenario_results = []
  997. self.steps_passed = 0
  998. self.steps_failed = 0
  999. self.steps_skipped = 0
  1000. self.steps_undefined = 0
  1001. self._proposed_definitions = []
  1002. self.steps = 0
  1003. for feature_result in self.feature_results:
  1004. for scenario_result in feature_result.scenario_results:
  1005. self.scenario_results.append(scenario_result)
  1006. self.steps_passed += len(scenario_result.steps_passed)
  1007. self.steps_failed += len(scenario_result.steps_failed)
  1008. self.steps_skipped += len(scenario_result.steps_skipped)
  1009. self.steps_undefined += len(scenario_result.steps_undefined)
  1010. self.steps += scenario_result.total_steps
  1011. self._proposed_definitions.extend(scenario_result.steps_undefined)
  1012. def _filter_proposed_definitions(self):
  1013. sentences = []
  1014. for step in self._proposed_definitions:
  1015. if step.proposed_sentence not in sentences:
  1016. sentences.append(step.proposed_sentence)
  1017. yield step
  1018. @property
  1019. def proposed_definitions(self):
  1020. return list(self._filter_proposed_definitions())
  1021. @property
  1022. def features_ran(self):
  1023. return len(self.feature_results)
  1024. @property
  1025. def features_passed(self):
  1026. return len([result for result in self.feature_results if result.passed])
  1027. @property
  1028. def scenarios_ran(self):
  1029. return len(self.scenario_results)
  1030. @property
  1031. def scenarios_passed(self):
  1032. return len([result for result in self.scenario_results if result.passed])