You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
245 lines
8.8 KiB
245 lines
8.8 KiB
# coding=utf-8
|
|
"""
|
|
Tools for running BDD frameworks in python.
|
|
You probably need to extend BddRunner (see its doc).
|
|
|
|
You may also need "get_what_to_run_by_env" that gets folder (current or passed as first argument)
|
|
"""
|
|
import os
|
|
import time
|
|
import abc
|
|
import tcmessages
|
|
from _jb_utils import VersionAgnosticUtils
|
|
|
|
__author__ = 'Ilya.Kazakevich'
|
|
|
|
|
|
def fix_win_drive(feature_path):
|
|
"""
|
|
Workaround to fix issues like http://bugs.python.org/issue7195 on windows.
|
|
Pass feature dir or file path as argument.
|
|
This function does nothing on non-windows platforms, so it could be run safely.
|
|
|
|
:param feature_path: path to feature (c:/fe.feature or /my/features)
|
|
"""
|
|
current_disk = (os.path.splitdrive(os.getcwd()))[0]
|
|
feature_disk = (os.path.splitdrive(feature_path))[0]
|
|
if current_disk and feature_disk and current_disk != feature_disk:
|
|
os.chdir(feature_disk)
|
|
|
|
|
|
def get_what_to_run_by_env(environment):
|
|
"""
|
|
:type environment dict
|
|
:param environment: os.environment (files and folders should be separated with | and passed to PY_STUFF_TO_RUN).
|
|
Scenarios optionally could be passed as SCENARIOS (names or order numbers, depends on runner)
|
|
:return: tuple (base_dir, scenarios[], what_to_run(list of feature files or folders))) where dir is current or first argument from env, checking it exists
|
|
:rtype tuple of (str, iterable)
|
|
"""
|
|
if "PY_STUFF_TO_RUN" not in environment:
|
|
what_to_run = ["."]
|
|
else:
|
|
what_to_run = str(environment["PY_STUFF_TO_RUN"]).split("|")
|
|
|
|
scenarios = []
|
|
if "SCENARIOS" in environment:
|
|
scenarios = str(environment["SCENARIOS"]).split("|")
|
|
|
|
if not what_to_run:
|
|
what_to_run = ["."]
|
|
|
|
for path in what_to_run:
|
|
assert os.path.exists(path), "{0} does not exist".format(path)
|
|
|
|
base_dir = what_to_run[0]
|
|
if os.path.isfile(what_to_run[0]):
|
|
base_dir = os.path.dirname(what_to_run[0]) # User may point to the file directly
|
|
return base_dir, scenarios, what_to_run
|
|
|
|
|
|
def get_location(base_dir, location_file, location_line):
|
|
"""
|
|
Generates location that PyCharm resolves to file
|
|
:param base_dir: base directory to resolve relative path against
|
|
:param location_file: path to file
|
|
:param location_line: line number
|
|
"""
|
|
my_file = str(location_file).lstrip("/\\")
|
|
return "file:///{0}:{1}".format(os.path.normpath(os.path.join(base_dir, my_file)), location_line)
|
|
|
|
|
|
class BddRunner(object):
|
|
"""
|
|
Extends this class, implement abstract methods and use its API to implement new BDD frameworks.
|
|
Call "run()" to launch it.
|
|
This class does the following:
|
|
* Gets features to run (using "_get_features_to_run()") and calculates steps in it
|
|
* Reports steps to Intellij or TC
|
|
* Calls "_run_tests()" where *you* should install all hooks you need into your BDD and use "self._" functions
|
|
to report tests and features. It actually wraps tcmessages but adds some stuff like duration count etc
|
|
:param base_dir:
|
|
"""
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
def __init__(self, base_dir):
|
|
"""
|
|
:type base_dir str
|
|
:param base_dir base directory of your project
|
|
"""
|
|
super(BddRunner, self).__init__()
|
|
self.tc_messages = tcmessages.TeamcityServiceMessages()
|
|
"""
|
|
tcmessages TeamCity/Intellij test API. See TeamcityServiceMessages
|
|
"""
|
|
self.__base_dir = base_dir
|
|
self.__last_test_start_time = None # TODO: Doc when use
|
|
self.__last_test_name = None
|
|
|
|
def run(self):
|
|
""""
|
|
Runs runner. To be called right after constructor.
|
|
"""
|
|
number_of_tests = self._get_number_of_tests()
|
|
self.tc_messages.testCount(number_of_tests)
|
|
self.tc_messages.testMatrixEntered()
|
|
if number_of_tests == 0: # Nothing to run, so no need to report even feature/scenario start. (See PY-13623)
|
|
return
|
|
self._run_tests()
|
|
|
|
def __gen_location(self, location):
|
|
"""
|
|
Generates location in format, supported by tcmessages
|
|
:param location object with "file" (relative to base_dir) and "line" fields.
|
|
:return: location in format file:line (as supported in tcmessages)
|
|
"""
|
|
return get_location(self.__base_dir, location.file, location.line)
|
|
|
|
def _test_undefined(self, test_name, location):
|
|
"""
|
|
Mark test as undefined
|
|
:param test_name: name of test
|
|
:type test_name str
|
|
:param location its location
|
|
|
|
"""
|
|
if test_name != self.__last_test_name:
|
|
self._test_started(test_name, location)
|
|
self._test_failed(test_name, message="Test undefined", details="Please define test")
|
|
|
|
def _test_skipped(self, test_name, reason, location):
|
|
"""
|
|
Mark test as skipped
|
|
:param test_name: name of test
|
|
:param reason: why test was skipped
|
|
:type reason str
|
|
:type test_name str
|
|
:param location its location
|
|
|
|
"""
|
|
if test_name != self.__last_test_name:
|
|
self._test_started(test_name, location)
|
|
self.tc_messages.testIgnored(test_name, "Skipped: {0}".format(reason))
|
|
self.__last_test_name = None
|
|
pass
|
|
|
|
|
|
def _test_failed(self, name, message, details, duration=None):
|
|
"""
|
|
Report test failure
|
|
:param name: test name
|
|
:type name str
|
|
:param message: failure message
|
|
:type message basestring
|
|
:param details: failure details (probably stacktrace)
|
|
:type details str
|
|
:param duration how long test took
|
|
:type duration int
|
|
"""
|
|
self.tc_messages.testFailed(name,
|
|
message=VersionAgnosticUtils().to_unicode(message),
|
|
details=details,
|
|
duration=duration)
|
|
self.__last_test_name = None
|
|
|
|
def _test_passed(self, name, duration=None):
|
|
"""
|
|
Reports test passed
|
|
:param name: test name
|
|
:type name str
|
|
:param duration: time (in seconds) test took. Pass None if you do not know (we'll try to calculate it)
|
|
:type duration int
|
|
:return:
|
|
"""
|
|
duration_to_report = duration
|
|
if self.__last_test_start_time and not duration: # And not provided
|
|
duration_to_report = int(time.time() - self.__last_test_start_time)
|
|
self.tc_messages.testFinished(name, duration=int(duration_to_report))
|
|
self.__last_test_start_time = None
|
|
self.__last_test_name = None
|
|
|
|
def _test_started(self, name, location):
|
|
"""
|
|
Reports test launched
|
|
:param name: test name
|
|
:param location object with "file" (relative to base_dir) and "line" fields.
|
|
:type name str
|
|
"""
|
|
self.__last_test_start_time = time.time()
|
|
self.__last_test_name = name
|
|
self.tc_messages.testStarted(name, self.__gen_location(location))
|
|
|
|
def _feature_or_scenario(self, is_started, name, location):
|
|
"""
|
|
Reports feature or scenario launched or stopped
|
|
:param is_started: started or finished?
|
|
:type is_started bool
|
|
:param name: scenario or feature name
|
|
:param location object with "file" (relative to base_dir) and "line" fields.
|
|
"""
|
|
if is_started:
|
|
self.tc_messages.testSuiteStarted(name, self.__gen_location(location))
|
|
else:
|
|
self.tc_messages.testSuiteFinished(name)
|
|
|
|
def _background(self, is_started, location):
|
|
"""
|
|
Reports background or stopped
|
|
:param is_started: started or finished?
|
|
:type is_started bool
|
|
:param location object with "file" (relative to base_dir) and "line" fields.
|
|
"""
|
|
self._feature_or_scenario(is_started, "Background", location)
|
|
|
|
def _get_number_of_tests(self):
|
|
""""
|
|
Gets number of tests using "_get_features_to_run()" to obtain number of features to calculate.
|
|
Supports backgrounds as well.
|
|
:return number of steps
|
|
:rtype int
|
|
"""
|
|
num_of_steps = 0
|
|
for feature in self._get_features_to_run():
|
|
if feature.background:
|
|
num_of_steps += len(list(feature.background.steps)) * len(list(feature.scenarios))
|
|
for scenario in feature.scenarios:
|
|
num_of_steps += len(list(scenario.steps))
|
|
return num_of_steps
|
|
|
|
@abc.abstractmethod
|
|
def _get_features_to_run(self):
|
|
"""
|
|
Implement it! Return list of features to run. Each "feature" should have "scenarios".
|
|
Each "scenario" should have "steps". Each "feature" may have "background" and each "background" should have
|
|
"steps". Duck typing.
|
|
:rtype list
|
|
:returns list of features
|
|
"""
|
|
return []
|
|
|
|
@abc.abstractmethod
|
|
def _run_tests(self):
|
|
"""
|
|
Implement it! It should launch tests using your BDD. Use "self._" functions to report results.
|
|
"""
|
|
pass
|