diff --git a/pyvows/cli.py b/pyvows/cli.py index 923caa7..0502dff 100755 --- a/pyvows/cli.py +++ b/pyvows/cli.py @@ -125,7 +125,7 @@ def __init__(self, description=Messages.summary, **kwargs): def run(path, pattern, verbosity, show_progress, exclusion_patterns=None): # FIXME: Add Docstring - # This calls Vows.run(), which then calls VowsParallelRunner.run() + # This calls Vows.run(), which then calls VowsRunner.run() # needs to be imported here, else the no-color option won't work from pyvows.core import Vows diff --git a/pyvows/core.py b/pyvows/core.py index 0914afa..b239460 100644 --- a/pyvows/core.py +++ b/pyvows/core.py @@ -18,7 +18,7 @@ from pyvows import utils from pyvows.async_topic import VowsAsyncTopic, VowsAsyncTopicValue from pyvows.decorators import _batch, async_topic -from pyvows.runner import VowsParallelRunner +from pyvows.runner import VowsRunner #------------------------------------------------------------------------------------------------- @@ -168,7 +168,7 @@ def run(cls, on_vow_success, on_vow_error): # # * Used by `run()` in `cli.py` # * Please add a useful description if you wrote this! :) - runner = VowsParallelRunner(cls.suites, + runner = VowsRunner(cls.suites, cls.Context, on_vow_success, on_vow_error, diff --git a/pyvows/runner.py b/pyvows/runner.py deleted file mode 100644 index 03e4d82..0000000 --- a/pyvows/runner.py +++ /dev/null @@ -1,330 +0,0 @@ -# -*- coding: utf-8 -*- -'''This module contains the magic that makes PyVows run its tests *fast*. - -Contains `VowsParallelRunner` class. -''' - - -# pyvows testing engine -# https://github.com/heynemann/pyvows - -# Licensed under the MIT license: -# http://www.opensource.org/licenses/mit-license -# Copyright (c) 2011 Bernardo Heynemann heynemann@gmail.com - -import inspect -import sys -import time -import re - -from gevent.pool import Pool - -from pyvows.async_topic import VowsAsyncTopic, VowsAsyncTopicValue -from pyvows.decorators import FunctionWrapper -from pyvows.result import VowsResult -from pyvows.utils import elapsed - - -#------------------------------------------------------------------------------------------------- -# HELPERS -#------------------------------------------------------------------------------------------------- -def _get_code_for(obj): - # FIXME: Add Comment description - code = None - if hasattr(obj, '__code__'): - code = obj.__code__ - elif hasattr(obj, '__func__'): - code = obj.__func__.__code__ - return code - - -def _get_file_info_for(member): - # FIXME: Add Docstring - code = _get_code_for(member) - - filename = code.co_filename - lineno = code.co_firstlineno - - return filename, lineno - - -def _get_topics_for(topic_function, ctx_instance): - # FIXME: Add Docstring - if not ctx_instance.parent: - return [] - - # check for async topic - if hasattr(topic_function, '_original'): - topic_function = topic_function._original - async = True - else: - async = False - - code = _get_code_for(topic_function) - - if not code: - raise RuntimeError('Function %s does not have a code property') - - expected_args = code.co_argcount - 1 - - # taking the callback argument into consideration - if async: - expected_args -= 1 - - # prepare to create `topics` list - topics = [] - child = ctx_instance - context = ctx_instance.parent - - # populate `topics` list - for i in range(expected_args): - topic = context.topic_value - - if context.generated_topic: - topic = topic[child.index] - - topics.append(topic) - - if not context.parent: - break - - context = context.parent - child = child.parent - - return topics - - -#------------------------------------------------------------------------------------------------- -# CLASSES -#------------------------------------------------------------------------------------------------- -class VowsParallelRunner(object): - # FIXME: Add Docstring - - # Class is called from `pyvows.core:Vows.run()`, - # which is called from `pyvows.cli.run()` - - pool = Pool(1000) - - def __init__(self, suites, context_class, on_vow_success, on_vow_error, exclusion_patterns): - self.suites = suites # a suite is a file with pyvows tests - self.context_class = context_class - self.on_vow_success = on_vow_success - self.on_vow_error = on_vow_error - self.exclusion_patterns = exclusion_patterns - if self.exclusion_patterns: - self.exclusion_patterns = set([re.compile(x) for x in self.exclusion_patterns]) - - def run(self): - # FIXME: Add Docstring - - # called from `pyvows.core:Vows.run()`, - # which is called from `pyvows.cli.run()` - - start_time = time.time() - result = VowsResult() - for suite, batches in self.suites.iteritems(): - for batch in batches: - self.run_context(result.contexts, batch.__name__, batch(None)) - self.pool.join() - result.elapsed_time = elapsed(start_time) - return result - - def run_context(self, ctx_collection, ctx_name, ctx_instance): - # FIXME: Add Docstring - self.pool.spawn(self.run_context_async, ctx_collection, ctx_name, ctx_instance) - - def run_context_async(self, ctx_collection, ctx_name, ctx_instance, index=-1): - # FIXME: Add Docstring - - for pattern in self.exclusion_patterns: - if pattern.search(ctx_name): - return - - #----------------------------------------------------------------------- - # Local variables and defs - #----------------------------------------------------------------------- - context_obj = { - 'name': ctx_name, - 'topic_elapsed': 0, - 'contexts': [], - 'tests': [], - 'filename': inspect.getsourcefile(ctx_instance.__class__) - } - - ctx_collection.append(context_obj) - ctx_instance.index = index - ctx_instance.pool = self.pool - - def _init_topic(): - topic = None - - if hasattr(ctx_instance, 'topic'): - start_time = time.time() - try: - topic_func = getattr(ctx_instance, 'topic') - topic_list = _get_topics_for(topic_func, ctx_instance) - topic = topic_func(*topic_list) - except Exception as e: - topic = e - topic.error = ctx_instance.topic_error = sys.exc_info() - context_obj['topic_elapsed'] = elapsed(start_time) - else: # ctx_instance has no topic - topic = ctx_instance._get_first_available_topic(index) - - return topic - - def _run_teardown(): - try: - teardown() - except Exception as e: - topic = e - topic.error = ctx_instance.topic_error = ('teardown', sys.exc_info()) - - def _run_with_topic(topic): - ctx_instance.topic_value = topic - - # setup generated topics if needed - is_generator = inspect.isgenerator(topic) - if is_generator: - try: - ctx_instance.topic_value = list(topic) - ctx_instance.generated_topic = True - except Exception as e: - is_generator = False - topic = e - topic.error = ctx_instance.topic_error = sys.exc_info() - ctx_instance.topic_value = topic - - topic = ctx_instance.topic_value - special_names = set(('setup', 'teardown', 'topic')) - - if hasattr(ctx_instance, 'ignored_members'): - special_names.update(ctx_instance.ignored_members) - - # remove any special methods from context_members - context_members = filter( - lambda member: not (member[0] in special_names or member[0].startswith('_')), - inspect.getmembers(type(ctx_instance)) - ) - - def _iterate_members(topic, index=-1, enumerated=False): - vows = set((vow_name,vow) for vow_name, vow in context_members if inspect.ismethod(vow)) - subcontexts = set((subctx_name,subctx) for subctx_name, subctx in context_members if inspect.isclass(subctx)) - - # methods - for vow_name, vow in vows: - self.run_vow( - context_obj['tests'], - topic, - ctx_instance, - teardown.wrap(vow), - vow_name, - enumerated=enumerated) - - # classes - for subctx_name, subctx in subcontexts: - # resolve user-defined Context classes - if not issubclass(subctx, self.context_class): - subctx = type(ctx_name, (subctx, self.context_class), {}) - - subctx_instance = subctx(ctx_instance) - subctx_instance.pool = self.pool - subctx_instance.teardown = teardown.wrap(subctx_instance.teardown) - - self.pool.spawn( - self.run_context_async, - context_obj['contexts'], - subctx_name, - subctx_instance, - index - ) - - if is_generator: - for index, topic_value in enumerate(topic): - _iterate_members(topic_value, index, enumerated=True) - else: - _iterate_members(topic) - - if hasattr(topic, 'error'): - ctx_instance.topic_error = topic.error - - #----------------------------------------------------------------------- - # Begin - #----------------------------------------------------------------------- - # execute ctx_instance.setup() - try: - ctx_instance.setup() - except Exception as e: - topic = e - error = ('setup', sys.exc_info()) - topic.error = ctx_instance.topic_error = error - else: # when no errors are raised - topic = _init_topic() - - # Wrap teardown so it gets called at the appropriate time - teardown = FunctionWrapper(ctx_instance.teardown) - - # run the topic/async topic - if isinstance(topic, VowsAsyncTopic): - def handle_callback(*args, **kw): - _run_with_topic(VowsAsyncTopicValue(args, kw)) - topic(handle_callback) - else: - _run_with_topic(topic) - - # execute teardown() - _run_teardown() - - def run_vow(self, tests_collection, topic, ctx_instance, vow, vow_name, enumerated=False): - # FIXME: Add Docstring - for pattern in self.exclusion_patterns: - if pattern.search(vow_name): - return - self.pool.spawn(self.run_vow_async, tests_collection, topic, ctx_instance, vow, vow_name, enumerated) - - def run_vow_async(self, tests_collection, topic, ctx_instance, vow, vow_name, enumerated): - # FIXME: Add Docstring - - start_time = time.time() - filename, lineno = _get_file_info_for(vow._original) - - result_obj = { - 'context_instance': ctx_instance, - 'name': vow_name, - 'enumerated': enumerated, - 'result': None, - 'topic': topic, - 'error': None, - 'succeeded': False, - 'file': filename, - 'lineno': lineno, - 'elapsed': 0 - } - - try: - result = vow(ctx_instance, topic) - result_obj['result'] = result - result_obj['succeeded'] = True - if self.on_vow_success: - self.on_vow_success(result_obj) - - except: - # FIXME: - # - # Either... - # * Describe why we're catching every exception, or - # * Fix to catch specific kinds of exceptions - err_type, err_value, err_traceback = sys.exc_info() - - result_obj['error'] = { - 'type': err_type, - 'value': err_value, - 'traceback': err_traceback - } - if self.on_vow_error: - self.on_vow_error(result_obj) - - result_obj['elapsed'] = elapsed(start_time) - tests_collection.append(result_obj) - - return result_obj diff --git a/pyvows/runner/__init__.py b/pyvows/runner/__init__.py new file mode 100644 index 0000000..e539d63 --- /dev/null +++ b/pyvows/runner/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +'''This package contains different runtime implementations for PyVows. PyVows will +select the fastest possible runner, using fallbacks if unavailable. + +''' + + +try: + ## GEvent + from pyvows.runner.gevent import VowsParallelRunner as VowsRunner +except ImportError as e: + ## Sequential + from pyvows.runner.sequential import VowsSequentialRunner as VowsRunner +finally: + __all__ = ('VowsRunner') \ No newline at end of file diff --git a/pyvows/runner/abc.py b/pyvows/runner/abc.py new file mode 100644 index 0000000..7a6d888 --- /dev/null +++ b/pyvows/runner/abc.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +'''Abstract base class for all PyVows Runner implementations.''' + + +# pyvows testing engine +# https://github.com/heynemann/pyvows + +# Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license +# Copyright (c) 2011 Bernardo Heynemann heynemann@gmail.com + +import re, sys, time + +from pyvows.runner.utils import get_code_for, get_file_info_for, get_topics_for +from pyvows.utils import elapsed + + +class VowsRunnerABC(object): + + def __init__(self, suites, context_class, on_vow_success, on_vow_error, exclusion_patterns): + self.suites = suites # a suite is a file with pyvows tests + self.context_class = context_class + self.on_vow_success = on_vow_success + self.on_vow_error = on_vow_error + self.exclusion_patterns = exclusion_patterns + if self.exclusion_patterns: + self.exclusion_patterns = set([re.compile(x) for x in self.exclusion_patterns]) + + def is_excluded(self, name): + '''Return whether `name` is in `self.exclusion_patterns`.''' + for pattern in self.exclusion_patterns: + if pattern.search(name): + return True + return False + + def run(self): + pass + + def run_context(self): + pass + + def run_vow(self, tests_collection, topic, ctx_obj, vow, vow_name, enumerated): + # FIXME: Add Docstring + + start_time = time.time() + filename, lineno = get_file_info_for(vow._original) + + vow_result = { + 'context_instance': ctx_obj, + 'name': vow_name, + 'enumerated': enumerated, + 'result': None, + 'topic': topic, + 'error': None, + 'succeeded': False, + 'file': filename, + 'lineno': lineno, + 'elapsed': 0 + } + + try: + result = vow(ctx_obj, topic) + vow_result['result'] = result + vow_result['succeeded'] = True + if self.on_vow_success: + self.on_vow_success(vow_result) + + except: + # FIXME: + # + # Either... + # * Describe why we're catching every exception, or + # * Fix to catch specific kinds of exceptions + err_type, err_value, err_traceback = sys.exc_info() + vow_result['error'] = { + 'type': err_type, + 'value': err_value, + 'traceback': err_traceback + } + if self.on_vow_error: + self.on_vow_error(vow_result) + + vow_result['elapsed'] = elapsed(start_time) + tests_collection.append(vow_result) + + return vow_result \ No newline at end of file diff --git a/pyvows/runner/gevent.py b/pyvows/runner/gevent.py new file mode 100644 index 0000000..54043f2 --- /dev/null +++ b/pyvows/runner/gevent.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +'''The GEvent implementation of PyVows runner.''' + + +# pyvows testing engine +# https://github.com/heynemann/pyvows + +# Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license +# Copyright (c) 2011 Bernardo Heynemann heynemann@gmail.com + +from __future__ import absolute_import + +import inspect +import sys +import time +import re + +from gevent.pool import Pool + +from pyvows.async_topic import VowsAsyncTopic, VowsAsyncTopicValue +from pyvows.decorators import FunctionWrapper +from pyvows.runner.utils import get_code_for, get_file_info_for, get_topics_for +from pyvows.result import VowsResult +from pyvows.utils import elapsed +from pyvows.runner.abc import VowsRunnerABC + +class VowsParallelRunner(VowsRunnerABC): + # FIXME: Add Docstring + + # Class is called from `pyvows.core:Vows.run()`, + # which is called from `pyvows.cli.run()` + + pool = Pool(1000) + + def run(self): + # FIXME: Add Docstring + + # called from `pyvows.core:Vows.run()`, + # which is called from `pyvows.cli.run()` + + start_time = time.time() + result = VowsResult() + for suite, batches in self.suites.items(): + for batch in batches: + self.pool.spawn( + self.run_context, + result.contexts, + ctx_name = batch.__name__, + ctx_obj = batch(None), + index = -1, + suite = suite + ) + + self.pool.join() + result.elapsed_time = elapsed(start_time) + return result + + + def run_context(self, ctx_collection, ctx_name=None, ctx_obj=None, index=-1, suite=None): + # FIXME: Add Docstring + + if self.is_excluded(ctx_name): + return + + #----------------------------------------------------------------------- + # Local variables and defs + #----------------------------------------------------------------------- + ctx_result = { + 'filename': suite or inspect.getsourcefile(ctx_obj.__class__), + 'name': ctx_name, + 'tests': [], + 'contexts': [], + 'topic_elapsed': 0, + } + + ctx_collection.append(ctx_result) + ctx_obj.index = index + ctx_obj.pool = self.pool + teardown = FunctionWrapper(ctx_obj.teardown) # Wrapped teardown so it's called at the appropriate time + + def _run_setup_and_topic(ctx_obj): + try: + ctx_obj.setup() + except Exception as e: + topic = e + topic.error = ctx_obj.topic_error = ('setup', sys.exc_info()) + else: # setup() had no errors + topic = None + if not hasattr(ctx_obj, 'topic'): # ctx_obj has no topic + topic = ctx_obj._get_first_available_topic(index) + else: + start_time = time.time() + try: + topic_func = getattr(ctx_obj, 'topic') + topic_list = get_topics_for(topic_func, ctx_obj) + topic = topic_func(*topic_list) + except Exception as e: + topic = e + topic.error = ctx_obj.topic_error = sys.exc_info() + ctx_result['topic_elapsed'] = elapsed(start_time) + finally: + return topic + def _run_tests(topic): + def _run_with_topic(topic): + def _run_vows_and_subcontexts(topic, index=-1, enumerated=False): + # methods + for vow_name, vow in vows: + self._run_vow( + ctx_result['tests'], + topic, + ctx_obj, + teardown.wrap(vow), + vow_name, + enumerated=enumerated) + + # classes + for subctx_name, subctx in subcontexts: + # resolve user-defined Context classes + if not issubclass(subctx, self.context_class): + subctx = type(ctx_name, (subctx, self.context_class), {}) + + subctx_obj = subctx(ctx_obj) + subctx_obj.pool = self.pool + subctx_obj.teardown = teardown.wrap(subctx_obj.teardown) + + self.pool.spawn( + self.run_context, + ctx_result['contexts'], + ctx_name=subctx_name, + ctx_obj=subctx_obj, + index=index, + suite=suite or ctx_result['filename'] + ) + + + ctx_obj.topic_value = topic + is_generator = inspect.isgenerator(topic) + + # setup generated topics if needed + if is_generator: + try: + ctx_obj.generated_topic = True + ctx_obj.topic_value = list(topic) + except Exception as e: + is_generator = False + topic = ctx_obj.topic_value = e + topic.error = ctx_obj.topic_error = sys.exc_info() + + topic = ctx_obj.topic_value + + if is_generator: + for index, topic_value in enumerate(topic): + _run_vows_and_subcontexts(topic_value, index=index, enumerated=True) + else: + _run_vows_and_subcontexts(topic) + + if hasattr(topic, 'error'): + ctx_obj.topic_error = topic.error + + special_names = set(['setup', 'teardown', 'topic']) + if hasattr(ctx_obj, 'ignored_members'): + special_names.update(ctx_obj.ignored_members) + + # remove any special methods from ctx_members + ctx_members = tuple(filter( + lambda member: not (member[0] in special_names or member[0].startswith('_')), + inspect.getmembers(type(ctx_obj)) + )) + vows = set((vow_name,vow) for vow_name, vow in ctx_members if inspect.ismethod(vow)) + subcontexts = set((subctx_name,subctx) for subctx_name, subctx in ctx_members if inspect.isclass(subctx)) + + if not isinstance(topic, VowsAsyncTopic): + _run_with_topic(topic) + else: + def handle_callback(*args, **kw): + _run_with_topic(VowsAsyncTopicValue(args, kw)) + topic(handle_callback) + def _run_teardown(topic): + try: + teardown() + except Exception as e: + topic = e + topic.error = ctx_obj.topic_error = ('teardown', sys.exc_info()) + + + #----------------------------------------------------------------------- + # Begin + #----------------------------------------------------------------------- + topic = _run_setup_and_topic(ctx_obj) + _run_tests(topic) + _run_teardown(topic) + + def _run_vow(self, tests_collection, topic, ctx_obj, vow, vow_name, enumerated=False): + # FIXME: Add Docstring + if self.is_excluded(vow_name): + return + self.pool.spawn(self.run_vow, tests_collection, topic, ctx_obj, vow, vow_name, enumerated) \ No newline at end of file diff --git a/pyvows/runner/sequential.py b/pyvows/runner/sequential.py new file mode 100644 index 0000000..fea901f --- /dev/null +++ b/pyvows/runner/sequential.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +'''This is the slowest of PyVows' runner implementations. But it's also dependency-free; thus, +it's a universal fallback. + +''' + +from pyvows.runner.abc import VowsRunnerABC +from pyvows.runner.utils import get_code_for, get_file_info_for, get_topics_for + + +class VowsSequentialRunner(object): + + def run(self): + pass + #for suite, batches in self.suites.items(): + # for batch in batches: + # self.run_context(batch.__name__, batch(None)) + + def run_context(self, ctx_name, ctx_instance): + pass + # setup + # teardown + # topic + # vows + # subcontexts + # teardown \ No newline at end of file diff --git a/pyvows/runner/utils.py b/pyvows/runner/utils.py new file mode 100644 index 0000000..3966ce0 --- /dev/null +++ b/pyvows/runner/utils.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +'''Utility functions for all implementations of pyvows.runner. + +''' +import os.path as path + +def get_code_for(obj): + # FIXME: Add Comment description + code = None + if hasattr(obj, '__code__'): + code = obj.__code__ + elif hasattr(obj, '__func__'): + code = obj.__func__.__code__ + return code + + +def get_file_info_for(member): + # FIXME: Add Docstring + code = get_code_for(member) + + filename = code.co_filename + lineno = code.co_firstlineno + + return filename, lineno + + +def get_topics_for(topic_function, ctx_obj): + # FIXME: Add Docstring + if not ctx_obj.parent: + return [] + + # check for async topic + if hasattr(topic_function, '_original'): + topic_function = topic_function._original + async = True + else: + async = False + + code = get_code_for(topic_function) + + if not code: + raise RuntimeError('Function %s does not have a code property') + + expected_args = code.co_argcount - 1 + + # taking the callback argument into consideration + if async: + expected_args -= 1 + + # prepare to create `topics` list + topics = [] + child = ctx_obj + context = ctx_obj.parent + + # populate `topics` list + for i in range(expected_args): + topic = context.topic_value + + if context.generated_topic: + topic = topic[child.index] + + topics.append(topic) + + if not context.parent: + break + + context = context.parent + child = child.parent + + return topics \ No newline at end of file diff --git a/tests/filter_vows_to_run_vows.py b/tests/filter_vows_to_run_vows.py index 2e07d89..bbb08ae 100755 --- a/tests/filter_vows_to_run_vows.py +++ b/tests/filter_vows_to_run_vows.py @@ -11,7 +11,7 @@ from pyvows import Vows, expect from pyvows import cli -from pyvows.runner import VowsParallelRunner +from pyvows.runner import VowsRunner @Vows.batch @@ -45,10 +45,10 @@ def topic(self): def should_have_exclude_method(self, topic): expect(topic.exclude).to_be_a_function() - class VowsParallelRunner(Vows.Context): + class VowsRunner(Vows.Context): def topic(self): - return VowsParallelRunner + return VowsRunner def can_be_initialized_with_6_arguments(self, topic): try: @@ -57,16 +57,16 @@ def can_be_initialized_with_6_arguments(self, topic): expect(e).Not.to_be_instance_of(TypeError) def removes_appropriate_contexts(self, topic): - r = topic(None, None, None, None, ['foo', 'bar']) + r = topic(None, None, None, None, set(['foo', 'bar'])) col = [] - r.run_context_async(col, 'footer', r) + r.run_context(col, 'footer', r) expect(len(col)).to_equal(0) def leaves_unmatched_contexts(self, topic): - VowsParallelRunner.teardown = None + VowsRunner.teardown = None r = topic(None, None, None, None, ['foo', 'bar']) col = [] - r.run_context_async(col, 'baz', r) + r.run_context(col, 'baz', r) expect(len(col)).to_equal(1) - r.run_context_async(col, 'bip', r) + r.run_context(col, 'bip', r) expect(len(col)).to_equal(2)