Commit b23a486c authored by dpranke@chromium.org's avatar dpranke@chromium.org

Switch webkitpy to use the typ test framework (delete webkitpy.test).

R=ojan@chromium.org
BUG=402172

Review URL: https://codereview.chromium.org/654063002

git-svn-id: svn://svn.chromium.org/blink/trunk@184383 bbb929c8-8fbe-4397-9dbb-9b2b20218538
parent effe7f52
......@@ -27,6 +27,39 @@
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from webkitpy.common import multiprocessing_bootstrap
multiprocessing_bootstrap.run('webkitpy', 'test', 'main.py')
import os
import sys
dirname = os.path.dirname
scripts_dir = dirname(os.path.realpath(__file__))
chromium_src_dir = dirname(dirname(dirname(dirname(scripts_dir))))
path_to_typ = os.path.join(chromium_src_dir, 'third_party', 'typ')
if path_to_typ not in sys.path:
sys.path.append(path_to_typ)
import typ
if sys.platform == 'win32':
# These test fail on win32. We could annotate some of these in
# class-level skips, but we don't support package/module-level skips.
# bugs.webkit.org/show_bug.cgi?id=54526 .
skip = [
'webkitpy.common.checkout.*',
'webkitpy.common.config.*',
'webkitpy.tool.*',
'webkitpy.w3c.*',
'webkitpy.layout_tests.layout_package.bot_test_expectations_unittest.*',
]
else:
# The scm tests are really slow, so we skip them by default.
# bugs.webkit.org/show_bug.cgi?id=31818 .
skip = [
'webkitpy.common.checkout.scm.scm_unittest.*',
]
sys.exit(typ.main(top_level_dir=scripts_dir,
skip=skip,
path=[os.path.join(scripts_dir, 'webkitpy', 'thirdparty')],
win_multiprocessing='spawn'))
......@@ -36,13 +36,10 @@ import time
import unittest
# Since we execute this script directly as part of the unit tests, we need to ensure
# that Tools/Scripts and Tools/Scripts/thirdparty are in sys.path for the next imports to work correctly.
# that Tools/Scripts is in sys.path for the next imports to work correctly.
script_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
if script_dir not in sys.path:
sys.path.append(script_dir)
third_party_py = os.path.join(script_dir, "webkitpy", "thirdparty")
if third_party_py not in sys.path:
sys.path.append(third_party_py)
from webkitpy.common.system.executive import Executive, ScriptError
......
......@@ -36,12 +36,6 @@ from StringIO import StringIO
class OutputCapture(object):
# By default we capture the output to a stream. Other modules may override
# this function in order to do things like pass through the output. See
# webkitpy.test.main for an example.
@staticmethod
def stream_wrapper(stream):
return StringIO()
def __init__(self):
self.saved_outputs = dict()
......@@ -54,7 +48,7 @@ class OutputCapture(object):
def _capture_output_with_name(self, output_name):
stream = getattr(sys, output_name)
captured_output = self.stream_wrapper(stream)
captured_output = StringIO()
self.saved_outputs[output_name] = stream
setattr(sys, output_name, captured_output)
return captured_output
......
......@@ -53,7 +53,6 @@ from webkitpy.layout_tests import run_webkit_tests
from webkitpy.layout_tests.models import test_run_results
from webkitpy.layout_tests.port import Port
from webkitpy.layout_tests.port import test
from webkitpy.test.skip import skip_if
from webkitpy.tool import grammar
from webkitpy.tool.mocktool import MockOptions
......
# Required for Python to search this directory for module files
# Copyright (C) 2012 Google, Inc.
# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""this module is responsible for finding python tests."""
import logging
import re
_log = logging.getLogger(__name__)
class _DirectoryTree(object):
def __init__(self, filesystem, top_directory, starting_subdirectory):
self.filesystem = filesystem
self.top_directory = filesystem.realpath(top_directory)
self.search_directory = self.top_directory
self.top_package = ''
if starting_subdirectory:
self.top_package = starting_subdirectory.replace(filesystem.sep, '.') + '.'
self.search_directory = filesystem.join(self.top_directory, starting_subdirectory)
def find_modules(self, suffixes, sub_directory=None):
if sub_directory:
search_directory = self.filesystem.join(self.top_directory, sub_directory)
else:
search_directory = self.search_directory
def file_filter(filesystem, dirname, basename):
return any(basename.endswith(suffix) for suffix in suffixes)
filenames = self.filesystem.files_under(search_directory, file_filter=file_filter)
return [self.to_module(filename) for filename in filenames]
def to_module(self, path):
return path.replace(self.top_directory + self.filesystem.sep, '').replace(self.filesystem.sep, '.')[:-3]
def subpath(self, path):
"""Returns the relative path from the top of the tree to the path, or None if the path is not under the top of the tree."""
realpath = self.filesystem.realpath(self.filesystem.join(self.top_directory, path))
if realpath.startswith(self.top_directory + self.filesystem.sep):
return realpath.replace(self.top_directory + self.filesystem.sep, '')
return None
def clean(self):
"""Delete all .pyc files in the tree that have no matching .py file."""
_log.debug("Cleaning orphaned *.pyc files from: %s" % self.search_directory)
filenames = self.filesystem.files_under(self.search_directory)
for filename in filenames:
if filename.endswith(".pyc") and filename[:-1] not in filenames:
_log.info("Deleting orphan *.pyc file: %s" % filename)
self.filesystem.remove(filename)
class Finder(object):
def __init__(self, filesystem):
self.filesystem = filesystem
self.trees = []
self._names_to_skip = []
def add_tree(self, top_directory, starting_subdirectory=None):
self.trees.append(_DirectoryTree(self.filesystem, top_directory, starting_subdirectory))
def skip(self, names, reason, bugid):
self._names_to_skip.append(tuple([names, reason, bugid]))
def additional_paths(self, paths):
return [tree.top_directory for tree in self.trees if tree.top_directory not in paths]
def clean_trees(self):
for tree in self.trees:
tree.clean()
def is_module(self, name):
relpath = name.replace('.', self.filesystem.sep) + '.py'
return any(self.filesystem.exists(self.filesystem.join(tree.top_directory, relpath)) for tree in self.trees)
def is_dotted_name(self, name):
return re.match(r'[a-zA-Z.][a-zA-Z0-9_.]*', name)
def to_module(self, path):
for tree in self.trees:
if path.startswith(tree.top_directory):
return tree.to_module(path)
return None
def find_names(self, args, find_all):
suffixes = ['_unittest.py', '_integrationtest.py']
if args:
names = []
for arg in args:
names.extend(self._find_names_for_arg(arg, suffixes))
return names
return self._default_names(suffixes, find_all)
def _find_names_for_arg(self, arg, suffixes):
realpath = self.filesystem.realpath(arg)
if self.filesystem.exists(realpath):
names = self._find_in_trees(realpath, suffixes)
if not names:
_log.error("%s is not in one of the test trees." % arg)
return names
# See if it's a python package in a tree (or a relative path from the top of a tree).
names = self._find_in_trees(arg.replace('.', self.filesystem.sep), suffixes)
if names:
return names
if self.is_dotted_name(arg):
# The name may not exist, but that's okay; we'll find out later.
return [arg]
_log.error("%s is not a python name or an existing file or directory." % arg)
return []
def _find_in_trees(self, path, suffixes):
for tree in self.trees:
relpath = tree.subpath(path)
if not relpath:
continue
if self.filesystem.isfile(path):
return [tree.to_module(path)]
else:
return tree.find_modules(suffixes, path)
return []
def _default_names(self, suffixes, find_all):
modules = []
for tree in self.trees:
modules.extend(tree.find_modules(suffixes))
modules.sort()
for module in modules:
_log.debug("Found: %s" % module)
if not find_all:
for (names, reason, bugid) in self._names_to_skip:
self._exclude(modules, names, reason, bugid)
return modules
def _exclude(self, modules, module_prefixes, reason, bugid):
_log.info('Skipping tests in the following modules or packages because they %s:' % reason)
for prefix in module_prefixes:
_log.info(' %s' % prefix)
modules_to_exclude = filter(lambda m: m.startswith(prefix), modules)
for m in modules_to_exclude:
if len(modules_to_exclude) > 1:
_log.debug(' %s' % m)
modules.remove(m)
_log.info(' (https://bugs.webkit.org/show_bug.cgi?id=%d; use --all to include)' % bugid)
_log.info('')
# Copyright (C) 2012 Google, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
import unittest
from webkitpy.common.system.filesystem_mock import MockFileSystem
from webkitpy.common.system.outputcapture import OutputCapture
from webkitpy.test.finder import Finder
class FinderTest(unittest.TestCase):
def setUp(self):
files = {
'/foo/bar/baz.py': '',
'/foo/bar/baz_unittest.py': '',
'/foo2/bar2/baz2.py': '',
'/foo2/bar2/baz2.pyc': '',
'/foo2/bar2/baz2_integrationtest.py': '',
'/foo2/bar2/missing.pyc': '',
'/tmp/another_unittest.py': '',
}
self.fs = MockFileSystem(files)
self.finder = Finder(self.fs)
self.finder.add_tree('/foo', 'bar')
self.finder.add_tree('/foo2')
# Here we have to jump through a hoop to make sure test-webkitpy doesn't log
# any messages from these tests :(.
self.root_logger = logging.getLogger()
self.log_levels = []
self.log_handlers = self.root_logger.handlers[:]
for handler in self.log_handlers:
self.log_levels.append(handler.level)
handler.level = logging.CRITICAL
def tearDown(self):
for handler in self.log_handlers:
handler.level = self.log_levels.pop(0)
def test_additional_system_paths(self):
self.assertEqual(self.finder.additional_paths(['/usr']),
['/foo', '/foo2'])
def test_is_module(self):
self.assertTrue(self.finder.is_module('bar.baz'))
self.assertTrue(self.finder.is_module('bar2.baz2'))
self.assertTrue(self.finder.is_module('bar2.baz2_integrationtest'))
# Missing the proper namespace.
self.assertFalse(self.finder.is_module('baz'))
def test_to_module(self):
self.assertEqual(self.finder.to_module('/foo/test.py'), 'test')
self.assertEqual(self.finder.to_module('/foo/bar/test.py'), 'bar.test')
self.assertEqual(self.finder.to_module('/foo/bar/pytest.py'), 'bar.pytest')
def test_clean(self):
self.assertTrue(self.fs.exists('/foo2/bar2/missing.pyc'))
self.finder.clean_trees()
self.assertFalse(self.fs.exists('/foo2/bar2/missing.pyc'))
def check_names(self, names, expected_names, find_all=True):
self.assertEqual(self.finder.find_names(names, find_all), expected_names)
def test_default_names(self):
self.check_names([], ['bar.baz_unittest', 'bar2.baz2_integrationtest'], find_all=True)
self.check_names([], ['bar.baz_unittest', 'bar2.baz2_integrationtest'], find_all=False)
# Should return the names given it, even if they don't exist.
self.check_names(['foobar'], ['foobar'], find_all=False)
def test_paths(self):
self.fs.chdir('/foo/bar')
self.check_names(['baz_unittest.py'], ['bar.baz_unittest'])
self.check_names(['./baz_unittest.py'], ['bar.baz_unittest'])
self.check_names(['/foo/bar/baz_unittest.py'], ['bar.baz_unittest'])
self.check_names(['.'], ['bar.baz_unittest'])
self.check_names(['../../foo2/bar2'], ['bar2.baz2_integrationtest'])
self.fs.chdir('/')
self.check_names(['bar'], ['bar.baz_unittest'])
self.check_names(['/foo/bar/'], ['bar.baz_unittest'])
# This works 'by accident' since it maps onto a package.
self.check_names(['bar/'], ['bar.baz_unittest'])
# This should log an error, since it's outside the trees.
oc = OutputCapture()
oc.set_log_level(logging.ERROR)
oc.capture_output()
try:
self.check_names(['/tmp/another_unittest.py'], [])
finally:
_, _, logs = oc.restore_output()
self.assertIn('another_unittest.py', logs)
# Paths that don't exist are errors.
oc.capture_output()
try:
self.check_names(['/foo/bar/notexist_unittest.py'], [])
finally:
_, _, logs = oc.restore_output()
self.assertIn('notexist_unittest.py', logs)
# Names that don't exist are caught later, at load time.
self.check_names(['bar.notexist_unittest'], ['bar.notexist_unittest'])
# Copyright (C) 2012 Google, Inc.
# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""unit testing code for webkitpy."""
import StringIO
import logging
import multiprocessing
import optparse
import os
import sys
import time
import traceback
import unittest
from webkitpy.common.webkit_finder import WebKitFinder
from webkitpy.common.system.filesystem import FileSystem
from webkitpy.common.system.executive import Executive
from webkitpy.test.finder import Finder
from webkitpy.test.printer import Printer
from webkitpy.test.runner import Runner, unit_test_name
_log = logging.getLogger(__name__)
up = os.path.dirname
webkit_root = up(up(up(up(up(os.path.abspath(__file__))))))
def main():
filesystem = FileSystem()
wkf = WebKitFinder(filesystem)
tester = Tester(filesystem, wkf)
tester.add_tree(wkf.path_from_webkit_base('Tools', 'Scripts'), 'webkitpy')
tester.skip(('webkitpy.common.checkout.scm.scm_unittest',), 'are really, really, slow', 31818)
if sys.platform == 'win32':
tester.skip(('webkitpy.common.checkout', 'webkitpy.common.config', 'webkitpy.tool', 'webkitpy.w3c', 'webkitpy.layout_tests.layout_package.bot_test_expectations'), 'fail horribly on win32', 54526)
# This only needs to run on Unix, so don't worry about win32 for now.
appengine_sdk_path = '/usr/local/google_appengine'
if os.path.exists(appengine_sdk_path):
if not appengine_sdk_path in sys.path:
sys.path.append(appengine_sdk_path)
import dev_appserver
from google.appengine.dist import use_library
use_library('django', '1.2')
dev_appserver.fix_sys_path()
tester.add_tree(wkf.path_from_webkit_base('Tools', 'TestResultServer'))
else:
_log.info('Skipping TestResultServer tests; the Google AppEngine Python SDK is not installed.')
return not tester.run()
class Tester(object):
def __init__(self, filesystem=None, webkit_finder=None):
self.filesystem = filesystem or FileSystem()
self.executive = Executive()
self.finder = Finder(self.filesystem)
self.printer = Printer(sys.stderr)
self.webkit_finder = webkit_finder or WebKitFinder(self.filesystem)
self._options = None
def add_tree(self, top_directory, starting_subdirectory=None):
self.finder.add_tree(top_directory, starting_subdirectory)
def skip(self, names, reason, bugid):
self.finder.skip(names, reason, bugid)
def _parse_args(self, argv):
parser = optparse.OptionParser(usage='usage: %prog [options] [args...]')
parser.add_option('-a', '--all', action='store_true', default=False,
help='run all the tests')
parser.add_option('-c', '--coverage', action='store_true', default=False,
help='generate code coverage info')
parser.add_option('-j', '--child-processes', action='store', type='int', default=(1 if sys.platform == 'win32' else multiprocessing.cpu_count()),
help='number of tests to run in parallel (default=%default)')
parser.add_option('-p', '--pass-through', action='store_true', default=False,
help='be debugger friendly by passing captured output through to the system')
parser.add_option('-q', '--quiet', action='store_true', default=False,
help='run quietly (errors, warnings, and progress only)')
parser.add_option('-t', '--timing', action='store_true', default=False,
help='display per-test execution time (implies --verbose)')
parser.add_option('-v', '--verbose', action='count', default=0,
help='verbose output (specify once for individual test results, twice for debug messages)')
parser.epilog = ('[args...] is an optional list of modules, test_classes, or individual tests. '
'If no args are given, all the tests will be run.')
return parser.parse_args(argv)
def run(self):
argv = sys.argv[1:]
self._options, args = self._parse_args(argv)
# Make sure PYTHONPATH is set up properly.
sys.path = self.finder.additional_paths(sys.path) + sys.path
# FIXME: coverage needs to be in sys.path for its internal imports to work.
thirdparty_path = self.webkit_finder.path_from_webkit_base('Tools', 'Scripts', 'webkitpy', 'thirdparty')
if not thirdparty_path in sys.path:
sys.path.append(thirdparty_path)
self.printer.configure(self._options)
# Do this after configuring the printer, so that logging works properly.
if self._options.coverage:
argv = ['-j', '1'] + [arg for arg in argv if arg not in ('-c', '--coverage', '-j', '--child-processes')]
_log.warning('Checking code coverage, so running things serially')
return self._run_under_coverage(argv)
self.finder.clean_trees()
names = self.finder.find_names(args, self._options.all)
if not names:
_log.error('No tests to run')
return False
return self._run_tests(names)
def _run_under_coverage(self, argv):
# coverage doesn't run properly unless its parent dir is in PYTHONPATH.
# This means we need to add that dir to the environment. Also, the
# report output is best when the paths are relative to the Scripts dir.
dirname = self.filesystem.dirname
script_dir = dirname(dirname(dirname(__file__)))
thirdparty_dir = self.filesystem.join(script_dir, 'webkitpy', 'thirdparty')
env = os.environ.copy()
python_path = env.get('PYTHONPATH', '')
python_path = python_path + os.pathsep + thirdparty_dir
env['PYTHONPATH'] = python_path
prefix_cmd = [sys.executable, 'webkitpy/thirdparty/coverage']
exit_code = self.executive.call(prefix_cmd + ['run', __file__] + argv, cwd=script_dir, env=env)
if not exit_code:
exit_code = self.executive.call(prefix_cmd + ['report', '--omit', 'webkitpy/thirdparty/*,/usr/*,/Library/*'], cwd=script_dir, env=env)
return (exit_code == 0)
def _run_tests(self, names):
self.printer.write_update("Checking imports ...")
if not self._check_imports(names):
return False
self.printer.write_update("Finding the individual test methods ...")
loader = unittest.TestLoader()
tests = self._test_names(loader, names)
self.printer.write_update("Running the tests ...")
self.printer.num_tests = len(tests)
start = time.time()
test_runner = Runner(self.printer, loader, self.webkit_finder)
test_runner.run(tests, self._options.child_processes)
self.printer.print_result(time.time() - start)
return not self.printer.num_errors and not self.printer.num_failures
def _check_imports(self, names):
for name in names:
if self.finder.is_module(name):
# if we failed to load a name and it looks like a module,
# try importing it directly, because loadTestsFromName()
# produces lousy error messages for bad modules.
try:
__import__(name)
except ImportError:
_log.fatal('Failed to import %s:' % name)
self._log_exception()
return False
return True
def _test_names(self, loader, names):
tests = []
for name in names:
tests.extend(self._all_test_names(loader.loadTestsFromName(name, None)))
return tests
def _all_test_names(self, suite):
names = []
if hasattr(suite, '_tests'):
for t in suite._tests:
names.extend(self._all_test_names(t))
else:
names.append(unit_test_name(suite))
return names
def _log_exception(self):
s = StringIO.StringIO()
traceback.print_exc(file=s)
for l in s.buflist:
_log.error(' ' + l.rstrip())
if __name__ == '__main__':
sys.exit(main())
# Copyright (C) 2012 Google, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import StringIO
import logging
import sys
import unittest
from webkitpy.common.system.filesystem import FileSystem
from webkitpy.common.system.executive import Executive
from webkitpy.common.system.outputcapture import OutputCapture
from webkitpy.test.main import Tester
STUBS_CLASS = __name__ + ".TestStubs"
class TestStubs(unittest.TestCase):
def test_empty(self):
pass
class TesterTest(unittest.TestCase):
def test_no_tests_found(self):
tester = Tester()
errors = StringIO.StringIO()
# Here we need to remove any existing log handlers so that they
# don't log the messages webkitpy.test while we're testing it.
root_logger = logging.getLogger()
root_handlers = root_logger.handlers
root_logger.handlers = []
tester.printer.stream = errors
tester.finder.find_names = lambda args, run_all: []
oc = OutputCapture()
orig_argv = sys.argv[:]
try:
sys.argv = sys.argv[0:1]
oc.capture_output()
self.assertFalse(tester.run())
finally:
_, _, logs = oc.restore_output()
root_logger.handlers = root_handlers
sys.argv = orig_argv
self.assertIn('No tests to run', errors.getvalue())
self.assertIn('No tests to run', logs)
def _find_test_names(self, args):
tester = Tester()
tester._options, args = tester._parse_args(args)
return tester._test_names(unittest.TestLoader(), args)
def test_individual_names_are_not_run_twice(self):
args = [STUBS_CLASS + '.test_empty']
tests = self._find_test_names(args)
self.assertEqual(tests, args)
def test_coverage_works(self):
# This is awkward; by design, running test-webkitpy -c will
# create a .coverage file in Tools/Scripts, so we need to be
# careful not to clobber an existing one, and to clean up.
# FIXME: This design needs to change since it means we can't actually
# run this method itself under coverage properly.
filesystem = FileSystem()
executive = Executive()
module_path = filesystem.path_to_module(self.__module__)
script_dir = module_path[0:module_path.find('webkitpy') - 1]
coverage_file = filesystem.join(script_dir, '.coverage')
coverage_file_orig = None
if filesystem.exists(coverage_file):
coverage_file_orig = coverage_file + '.orig'
filesystem.move(coverage_file, coverage_file_orig)
try:
proc = executive.popen([sys.executable, filesystem.join(script_dir, 'test-webkitpy'), '-c', STUBS_CLASS + '.test_empty'],
stdout=executive.PIPE, stderr=executive.PIPE)
out, _ = proc.communicate()
retcode = proc.returncode
self.assertEqual(retcode, 0)
self.assertIn('Cover', out)
finally:
if coverage_file_orig:
filesystem.move(coverage_file_orig, coverage_file)
elif filesystem.exists(coverage_file):
filesystem.remove(coverage_file)
# Copyright (C) 2012 Google, Inc.
# Copyright (C) 2010 Chris Jerdonek (cjerdonek@webkit.org)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import StringIO
import logging
from webkitpy.common.system import outputcapture
from webkitpy.common.system.systemhost import SystemHost
from webkitpy.layout_tests.views.metered_stream import MeteredStream
_log = logging.getLogger(__name__)
class Printer(object):
def __init__(self, stream, options=None):
self.stream = stream
self.meter = None
self.options = options
self.num_tests = 0
self.num_completed = 0
self.num_errors = 0
self.num_failures = 0
self.running_tests = []
self.completed_tests = []
if options:
self.configure(options)
def configure(self, options):
self.options = options
if options.timing:
# --timing implies --verbose
options.verbose = max(options.verbose, 1)
log_level = logging.INFO
if options.quiet:
log_level = logging.WARNING
elif options.verbose >= 2:
log_level = logging.DEBUG
self.meter = MeteredStream(self.stream, (options.verbose >= 2),
number_of_columns=SystemHost().platform.terminal_width())
handler = logging.StreamHandler(self.stream)
# We constrain the level on the handler rather than on the root
# logger itself. This is probably better because the handler is
# configured and known only to this module, whereas the root logger
# is an object shared (and potentially modified) by many modules.
# Modifying the handler, then, is less intrusive and less likely to
# interfere with modifications made by other modules (e.g. in unit
# tests).
handler.name = __name__
handler.setLevel(log_level)
formatter = logging.Formatter("%(message)s")
handler.setFormatter(formatter)
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.NOTSET)
# Filter out most webkitpy messages.
#
# Messages can be selectively re-enabled for this script by updating
# this method accordingly.
def filter_records(record):
"""Filter out non-third-party webkitpy messages."""
# FIXME: Figure out a way not to use strings here, for example by
# using syntax like webkitpy.test.__name__. We want to be
# sure not to import any non-Python 2.4 code, though, until
# after the version-checking code has executed.
if (record.name.startswith("webkitpy.test")):
return True
if record.name.startswith("webkitpy"):
return False
return True
testing_filter = logging.Filter()
testing_filter.filter = filter_records
# Display a message so developers are not mystified as to why
# logging does not work in the unit tests.
_log.info("Suppressing most webkitpy logging while running unit tests.")
handler.addFilter(testing_filter)
if self.options.pass_through:
outputcapture.OutputCapture.stream_wrapper = _CaptureAndPassThroughStream
def write_update(self, msg):
self.meter.write_update(msg)
def print_started_test(self, source, test_name):
self.running_tests.append(test_name)
if len(self.running_tests) > 1:
suffix = ' (+%d)' % (len(self.running_tests) - 1)
else:
suffix = ''
if self.options.verbose:
write = self.meter.write_update
else:
write = self.meter.write_throttled_update
write(self._test_line(self.running_tests[0], suffix))
def print_finished_test(self, source, test_name, test_time, failures, errors):
write = self.meter.writeln
if failures:
lines = failures[0].splitlines() + ['']
suffix = ' failed:'
self.num_failures += 1
elif errors:
lines = errors[0].splitlines() + ['']
suffix = ' erred:'
self.num_errors += 1
else:
suffix = ' passed'
lines = []
if self.options.verbose:
write = self.meter.writeln
else:
write = self.meter.write_throttled_update
if self.options.timing:
suffix += ' %.4fs' % test_time
self.num_completed += 1
if test_name == self.running_tests[0]:
self.completed_tests.insert(0, [test_name, suffix, lines])
else:
self.completed_tests.append([test_name, suffix, lines])
self.running_tests.remove(test_name)
for test_name, msg, lines in self.completed_tests:
if lines:
self.meter.writeln(self._test_line(test_name, msg))
for line in lines:
self.meter.writeln(' ' + line)
else:
write(self._test_line(test_name, msg))
self.completed_tests = []
def _test_line(self, test_name, suffix):
format_string = '[%d/%d] %s%s'
status_line = format_string % (self.num_completed, self.num_tests, test_name, suffix)
if len(status_line) > self.meter.number_of_columns():
overflow_columns = len(status_line) - self.meter.number_of_columns()
ellipsis = '...'
if len(test_name) < overflow_columns + len(ellipsis) + 3:
# We don't have enough space even if we elide, just show the test method name.
test_name = test_name.split('.')[-1]
else:
new_length = len(test_name) - overflow_columns - len(ellipsis)
prefix = int(new_length / 2)
test_name = test_name[:prefix] + ellipsis + test_name[-(new_length - prefix):]
return format_string % (self.num_completed, self.num_tests, test_name, suffix)
def print_result(self, run_time):
write = self.meter.writeln
write('Ran %d test%s in %.3fs' % (self.num_completed, self.num_completed != 1 and "s" or "", run_time))
if self.num_failures or self.num_errors:
write('FAILED (failures=%d, errors=%d)\n' % (self.num_failures, self.num_errors))
else:
write('\nOK\n')
class _CaptureAndPassThroughStream(object):
def __init__(self, stream):
self._buffer = StringIO.StringIO()
self._stream = stream
def write(self, msg):
self._stream.write(msg)
# Note that we don't want to capture any output generated by the debugger
# because that could cause the results of capture_output() to be invalid.
if not self._message_is_from_pdb():
self._buffer.write(msg)
def _message_is_from_pdb(self):
# We will assume that if the pdb module is in the stack then the output
# is being generated by the python debugger (or the user calling something
# from inside the debugger).
import inspect
import pdb
stack = inspect.stack()
return any(frame[1] == pdb.__file__.replace('.pyc', '.py') for frame in stack)
def flush(self):
self._stream.flush()
def getvalue(self):
return self._buffer.getvalue()
# Copyright (C) 2012 Google, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""code to actually run a list of python tests."""
import re
import sys
import time
import unittest
from webkitpy.common import message_pool
_test_description = re.compile("(\w+) \(([\w.]+)\)")
def unit_test_name(test):
m = _test_description.match(str(test))
return "%s.%s" % (m.group(2), m.group(1))
class Runner(object):
def __init__(self, printer, loader, webkit_finder):
self.printer = printer
self.loader = loader
self.webkit_finder = webkit_finder
self.tests_run = 0
self.errors = []
self.failures = []
self.worker_factory = lambda caller: _Worker(caller, self.loader, self.webkit_finder)
def run(self, test_names, num_workers):
if not test_names:
return
num_workers = min(num_workers, len(test_names))
with message_pool.get(self, self.worker_factory, num_workers) as pool:
pool.run(('test', test_name) for test_name in test_names)
def handle(self, message_name, source, test_name, delay=None, failures=None, errors=None):
if message_name == 'started_test':
self.printer.print_started_test(source, test_name)
return
self.tests_run += 1
if failures:
self.failures.append((test_name, failures))
if errors:
self.errors.append((test_name, errors))
self.printer.print_finished_test(source, test_name, delay, failures, errors)
class _Worker(object):
def __init__(self, caller, loader, webkit_finder):
self._caller = caller
self._loader = loader
# FIXME: coverage needs to be in sys.path for its internal imports to work.
thirdparty_path = webkit_finder.path_from_webkit_base('Tools', 'Scripts', 'webkitpy', 'thirdparty')
if not thirdparty_path in sys.path:
sys.path.append(thirdparty_path)
def handle(self, message_name, source, test_name):
assert message_name == 'test'
result = unittest.TestResult()
start = time.time()
self._caller.post('started_test', test_name)
# We will need to rework this if a test_name results in multiple tests.
self._loader.loadTestsFromName(test_name, None).run(result)
self._caller.post('finished_test', test_name, time.time() - start,
[failure[1] for failure in result.failures], [error[1] for error in result.errors])
# Copyright (C) 2012 Google, Inc.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import StringIO
import logging
import re
import unittest
from webkitpy.common.system.filesystem import FileSystem
from webkitpy.common.webkit_finder import WebKitFinder
from webkitpy.tool.mocktool import MockOptions
from webkitpy.test.printer import Printer
from webkitpy.test.runner import Runner
class FakeModuleSuite(object):
def __init__(self, name, result, msg):
self.name = name
self.result = result
self.msg = msg
def __str__(self):
return self.name
def run(self, result):
result.testsRun += 1
if self.result == 'F':
result.failures.append((self.name, self.msg))
elif self.result == 'E':
result.errors.append((self.name, self.msg))
class FakeTopSuite(object):
def __init__(self, tests):
self._tests = tests
class FakeLoader(object):
def __init__(self, *test_triples):
self.triples = test_triples
self._tests = []
self._results = {}
for test_name, result, msg in self.triples:
self._tests.append(test_name)
m = re.match("(\w+) \(([\w.]+)\)", test_name)
self._results['%s.%s' % (m.group(2), m.group(1))] = tuple([test_name, result, msg])
def top_suite(self):
return FakeTopSuite(self._tests)
def loadTestsFromName(self, name, _):
return FakeModuleSuite(*self._results[name])
class RunnerTest(unittest.TestCase):
def setUp(self):
# Here we have to jump through a hoop to make sure test-webkitpy doesn't log
# any messages from these tests :(.
self.root_logger = logging.getLogger()
self.log_levels = []
self.log_handlers = self.root_logger.handlers[:]
for handler in self.log_handlers:
self.log_levels.append(handler.level)
handler.level = logging.CRITICAL
def tearDown(self):
for handler in self.log_handlers:
handler.level = self.log_levels.pop(0)
def test_run(self, verbose=0, timing=False, child_processes=1, quiet=False):
options = MockOptions(verbose=verbose, timing=timing, child_processes=child_processes, quiet=quiet, pass_through=False)
stream = StringIO.StringIO()
loader = FakeLoader(('test1 (Foo)', '.', ''),
('test2 (Foo)', 'F', 'test2\nfailed'),
('test3 (Foo)', 'E', 'test3\nerred'))
runner = Runner(Printer(stream, options), loader, WebKitFinder(FileSystem()))
runner.run(['Foo.test1', 'Foo.test2', 'Foo.test3'], 1)
self.assertEqual(runner.tests_run, 3)
self.assertEqual(len(runner.failures), 1)
self.assertEqual(len(runner.errors), 1)
# Copyright (C) 2010 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
_log = logging.getLogger(__name__)
def skip_if(klass, condition, message=None, logger=None):
"""Makes all test_* methods in a given class no-ops if the given condition
is False. Backported from Python 3.1+'s unittest.skipIf decorator."""
if not logger:
logger = _log
if not condition:
return klass
for name in dir(klass):
attr = getattr(klass, name)
if not callable(attr):
continue
if not name.startswith('test_'):
continue
setattr(klass, name, _skipped_method(attr, message, logger))
klass._printed_skipped_message = False
return klass
def _skipped_method(method, message, logger):
def _skip(*args):
if method.im_class._printed_skipped_message:
return
method.im_class._printed_skipped_message = True
logger.info('Skipping %s.%s: %s' % (method.__module__, method.im_class.__name__, message))
return _skip
# Copyright (C) 2010 Apple Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import StringIO
import logging
import unittest
from webkitpy.test.skip import skip_if
class SkipTest(unittest.TestCase):
def setUp(self):
self.logger = logging.getLogger(__name__)
self.old_level = self.logger.level
self.logger.setLevel(logging.INFO)
self.old_propagate = self.logger.propagate
self.logger.propagate = False
self.log_stream = StringIO.StringIO()
self.handler = logging.StreamHandler(self.log_stream)
self.logger.addHandler(self.handler)
self.foo_was_called = False
def tearDown(self):
self.logger.removeHandler(self.handler)
self.propagate = self.old_propagate
self.logger.setLevel(self.old_level)
def create_fixture_class(self):
class TestSkipFixture(object):
def __init__(self, callback):
self.callback = callback
def test_foo(self):
self.callback()
return TestSkipFixture
def foo_callback(self):
self.foo_was_called = True
def test_skip_if_false(self):
klass = skip_if(self.create_fixture_class(), False, 'Should not see this message.', logger=self.logger)
klass(self.foo_callback).test_foo()
self.assertEqual(self.log_stream.getvalue(), '')
self.assertTrue(self.foo_was_called)
def test_skip_if_true(self):
klass = skip_if(self.create_fixture_class(), True, 'Should see this message.', logger=self.logger)
klass(self.foo_callback).test_foo()
self.assertEqual(self.log_stream.getvalue(), 'Skipping webkitpy.test.skip_unittest.TestSkipFixture: Should see this message.\n')
self.assertFalse(self.foo_was_called)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment