Dactyloidae/testing/marionette/harness/marionette_harness/marionette_test/testcases.py

504 lines
20 KiB
Python

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import imp
import os
import re
import sys
import time
import types
import unittest
import warnings
import weakref
from unittest.case import (
_ExpectedFailure,
_UnexpectedSuccess,
SkipTest,
)
from marionette_driver.errors import (
MarionetteException,
ScriptTimeoutException,
TimeoutException,
)
from mozlog import get_default_logger
def _wraps_parameterized(func, func_suffix, args, kwargs):
"""Internal: Decorator used in class MetaParameterized."""
def wrapper(self):
return func(self, *args, **kwargs)
wrapper.__name__ = func.__name__ + '_' + str(func_suffix)
wrapper.__doc__ = '[{0}] {1}'.format(func_suffix, func.__doc__)
return wrapper
class MetaParameterized(type):
"""
A metaclass that allow a class to use decorators.
It can be used like :func:`parameterized`
or :func:`with_parameters` to generate new methods.
"""
RE_ESCAPE_BAD_CHARS = re.compile(r'[\.\(\) -/]')
def __new__(cls, name, bases, attrs):
for k, v in attrs.items():
if callable(v) and hasattr(v, 'metaparameters'):
for func_suffix, args, kwargs in v.metaparameters:
func_suffix = cls.RE_ESCAPE_BAD_CHARS.sub('_', func_suffix)
wrapper = _wraps_parameterized(v, func_suffix, args, kwargs)
if wrapper.__name__ in attrs:
raise KeyError("{0} is already a defined method on {1}"
.format(wrapper.__name__, name))
attrs[wrapper.__name__] = wrapper
del attrs[k]
return type.__new__(cls, name, bases, attrs)
class JSTest:
head_js_re = re.compile(r"MARIONETTE_HEAD_JS(\s*)=(\s*)['|\"](.*?)['|\"];")
context_re = re.compile(r"MARIONETTE_CONTEXT(\s*)=(\s*)['|\"](.*?)['|\"];")
timeout_re = re.compile(r"MARIONETTE_TIMEOUT(\s*)=(\s*)(\d+);")
inactivity_timeout_re = re.compile(r"MARIONETTE_INACTIVITY_TIMEOUT(\s*)=(\s*)(\d+);")
class CommonTestCase(unittest.TestCase):
__metaclass__ = MetaParameterized
match_re = None
failureException = AssertionError
pydebugger = None
def __init__(self, methodName, marionette_weakref, fixtures, **kwargs):
super(CommonTestCase, self).__init__(methodName)
self.methodName = methodName
self._marionette_weakref = marionette_weakref
self.fixtures = fixtures
self.loglines = []
self.duration = 0
self.start_time = 0
self.expected = kwargs.pop('expected', 'pass')
self.logger = get_default_logger()
def _enter_pm(self):
if self.pydebugger:
self.pydebugger.post_mortem(sys.exc_info()[2])
def _addSkip(self, result, reason):
addSkip = getattr(result, 'addSkip', None)
if addSkip is not None:
addSkip(self, reason)
else:
warnings.warn("TestResult has no addSkip method, skips not reported",
RuntimeWarning, 2)
result.addSuccess(self)
def run(self, result=None):
# Bug 967566 suggests refactoring run, which would hopefully
# mean getting rid of this inner function, which only sits
# here to reduce code duplication:
def expected_failure(result, exc_info):
addExpectedFailure = getattr(result, "addExpectedFailure", None)
if addExpectedFailure is not None:
addExpectedFailure(self, exc_info)
else:
warnings.warn("TestResult has no addExpectedFailure method, "
"reporting as passes", RuntimeWarning)
result.addSuccess(self)
self.start_time = time.time()
orig_result = result
if result is None:
result = self.defaultTestResult()
startTestRun = getattr(result, 'startTestRun', None)
if startTestRun is not None:
startTestRun()
result.startTest(self)
testMethod = getattr(self, self._testMethodName)
if (getattr(self.__class__, "__unittest_skip__", False) or
getattr(testMethod, "__unittest_skip__", False)):
# If the class or method was skipped.
try:
skip_why = (getattr(self.__class__, '__unittest_skip_why__', '') or
getattr(testMethod, '__unittest_skip_why__', ''))
self._addSkip(result, skip_why)
finally:
result.stopTest(self)
self.stop_time = time.time()
return
try:
success = False
try:
if self.expected == "fail":
try:
self.setUp()
except Exception:
raise _ExpectedFailure(sys.exc_info())
else:
self.setUp()
except SkipTest as e:
self._addSkip(result, str(e))
except KeyboardInterrupt:
raise
except _ExpectedFailure as e:
expected_failure(result, e.exc_info)
except:
self._enter_pm()
result.addError(self, sys.exc_info())
else:
try:
if self.expected == 'fail':
try:
testMethod()
except:
raise _ExpectedFailure(sys.exc_info())
raise _UnexpectedSuccess
else:
testMethod()
except self.failureException:
self._enter_pm()
result.addFailure(self, sys.exc_info())
except KeyboardInterrupt:
raise
except _ExpectedFailure as e:
expected_failure(result, e.exc_info)
except _UnexpectedSuccess:
addUnexpectedSuccess = getattr(result, 'addUnexpectedSuccess', None)
if addUnexpectedSuccess is not None:
addUnexpectedSuccess(self)
else:
warnings.warn("TestResult has no addUnexpectedSuccess method, "
"reporting as failures",
RuntimeWarning)
result.addFailure(self, sys.exc_info())
except SkipTest as e:
self._addSkip(result, str(e))
except:
self._enter_pm()
result.addError(self, sys.exc_info())
else:
success = True
try:
if self.expected == "fail":
try:
self.tearDown()
except:
raise _ExpectedFailure(sys.exc_info())
else:
self.tearDown()
except KeyboardInterrupt:
raise
except _ExpectedFailure as e:
expected_failure(result, e.exc_info)
except:
self._enter_pm()
result.addError(self, sys.exc_info())
success = False
# Here we could handle doCleanups() instead of calling cleanTest directly
self.cleanTest()
if success:
result.addSuccess(self)
finally:
result.stopTest(self)
if orig_result is None:
stopTestRun = getattr(result, 'stopTestRun', None)
if stopTestRun is not None:
stopTestRun()
@classmethod
def match(cls, filename):
"""Determine if the specified filename should be handled by this test class.
This is done by looking for a match for the filename using cls.match_re.
"""
if not cls.match_re:
return False
m = cls.match_re.match(filename)
return m is not None
@classmethod
def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette,
fixtures, testvars, **kwargs):
"""Add all the tests in the specified file to the specified suite."""
raise NotImplementedError
@property
def test_name(self):
if hasattr(self, 'jsFile'):
return os.path.basename(self.jsFile)
else:
return '{0}.py {1}.{2}'.format(self.__class__.__module__,
self.__class__.__name__,
self._testMethodName)
def id(self):
# TBPL starring requires that the "test name" field of a failure message
# not differ over time. The test name to be used is passed to
# mozlog via the test id, so this is overriden to maintain
# consistency.
return self.test_name
def setUp(self):
# Convert the marionette weakref to an object, just for the
# duration of the test; this is deleted in tearDown() to prevent
# a persistent circular reference which in turn would prevent
# proper garbage collection.
self.start_time = time.time()
self.marionette = self._marionette_weakref()
if self.marionette.session is None:
self.marionette.start_session()
self.marionette.timeout.reset()
super(CommonTestCase, self).setUp()
def cleanTest(self):
self._deleteSession()
def _deleteSession(self):
if hasattr(self, 'start_time'):
self.duration = time.time() - self.start_time
if hasattr(self.marionette, 'session'):
if self.marionette.session is not None:
try:
self.loglines.extend(self.marionette.get_logs())
except Exception, inst:
self.loglines = [['Error getting log: {}'.format(inst)]]
try:
self.marionette.delete_session()
except IOError:
# Gecko has crashed?
pass
self.marionette = None
def setup_SpecialPowers_observer(self):
self.marionette.set_context("chrome")
self.marionette.execute_script("""
let SECURITY_PREF = "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer";
Components.utils.import("resource://gre/modules/Preferences.jsm");
Preferences.set(SECURITY_PREF, true);
if (!testUtils.hasOwnProperty("specialPowersObserver")) {
let loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserver.jsm",
testUtils);
testUtils.specialPowersObserver = new testUtils.SpecialPowersObserver();
testUtils.specialPowersObserver.init();
}
""")
def run_js_test(self, filename, marionette=None):
"""Run a JavaScript test file.
It collects its set of assertions into the current test's results.
:param filename: The path to the JavaScript test file to execute.
May be relative to the current script.
:param marionette: The Marionette object in which to execute the test.
Defaults to self.marionette.
"""
marionette = marionette or self.marionette
if not os.path.isabs(filename):
# Find the caller's filename and make the path relative to that.
caller_file = sys._getframe(1).f_globals.get('__file__', '')
caller_file = os.path.abspath(caller_file)
filename = os.path.join(os.path.dirname(caller_file), filename)
self.assert_(os.path.exists(filename),
'Script "{}" must exist' .format(filename))
original_test_name = self.marionette.test_name
self.marionette.test_name = os.path.basename(filename)
f = open(filename, 'r')
js = f.read()
args = []
head_js = JSTest.head_js_re.search(js)
if head_js:
head_js = head_js.group(3)
head = open(os.path.join(os.path.dirname(filename), head_js), 'r')
js = head.read() + js
context = JSTest.context_re.search(js)
if context:
context = context.group(3)
else:
context = 'content'
if 'SpecialPowers' in js:
self.setup_SpecialPowers_observer()
if context == 'content':
js = "var SpecialPowers = window.wrappedJSObject.SpecialPowers;\n" + js
else:
marionette.execute_script("""
if (typeof(SpecialPowers) == 'undefined') {
let loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://specialpowers/content/specialpowersAPI.js");
loader.loadSubScript("chrome://specialpowers/content/SpecialPowersObserverAPI.js");
loader.loadSubScript("chrome://specialpowers/content/ChromePowers.js");
}
""")
marionette.set_context(context)
if context != 'chrome':
marionette.navigate('data:text/html,<html>test page</html>')
timeout = JSTest.timeout_re.search(js)
if timeout:
ms = timeout.group(3)
marionette.timeout.script = int(ms) / 1000.0
inactivity_timeout = JSTest.inactivity_timeout_re.search(js)
if inactivity_timeout:
inactivity_timeout = int(inactivity_timeout.group(3))
try:
results = marionette.execute_js_script(
js,
args,
inactivity_timeout=inactivity_timeout,
filename=os.path.basename(filename)
)
self.assertTrue('timeout' not in filename,
'expected timeout not triggered')
if 'fail' in filename:
self.assertTrue(len(results['failures']) > 0,
"expected test failures didn't occur")
else:
for failure in results['failures']:
diag = "" if failure.get('diag') is None else failure['diag']
name = ("got false, expected true" if failure.get('name') is None else
failure['name'])
self.logger.test_status(self.test_name, name, 'FAIL',
message=diag)
for failure in results['expectedFailures']:
diag = "" if failure.get('diag') is None else failure['diag']
name = ("got false, expected false" if failure.get('name') is None else
failure['name'])
self.logger.test_status(self.test_name, name, 'FAIL',
expected='FAIL', message=diag)
for failure in results['unexpectedSuccesses']:
diag = "" if failure.get('diag') is None else failure['diag']
name = ("got true, expected false" if failure.get('name') is None else
failure['name'])
self.logger.test_status(self.test_name, name, 'PASS',
expected='FAIL', message=diag)
self.assertEqual(0, len(results['failures']),
'{} tests failed' .format(len(results['failures'])))
if len(results['unexpectedSuccesses']) > 0:
raise _UnexpectedSuccess('')
if len(results['expectedFailures']) > 0:
raise _ExpectedFailure((AssertionError, AssertionError(''), None))
self.assertTrue(results['passed'] +
len(results['failures']) +
len(results['expectedFailures']) +
len(results['unexpectedSuccesses']) > 0,
'no tests run')
except ScriptTimeoutException:
if 'timeout' in filename:
# expected exception
pass
else:
self.loglines = marionette.get_logs()
raise
self.marionette.test_name = original_test_name
class MarionetteTestCase(CommonTestCase):
match_re = re.compile(r"test_(.*)\.py$")
def __init__(self, marionette_weakref, fixtures, methodName='runTest',
filepath='', **kwargs):
self.filepath = filepath
self.testvars = kwargs.pop('testvars', None)
super(MarionetteTestCase, self).__init__(
methodName, marionette_weakref=marionette_weakref, fixtures=fixtures, **kwargs)
@classmethod
def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette,
fixtures, testvars, **kwargs):
# since we use imp.load_source to load test modules, if a module
# is loaded with the same name as another one the module would just be
# reloaded.
#
# We may end up by finding too many test in a module then since
# reload() only update the module dict (so old keys are still there!)
# see https://docs.python.org/2/library/functions.html#reload
#
# we get rid of that by removing the module from sys.modules,
# so we ensure that it will be fully loaded by the
# imp.load_source call.
if mod_name in sys.modules:
del sys.modules[mod_name]
test_mod = imp.load_source(mod_name, filepath)
for name in dir(test_mod):
obj = getattr(test_mod, name)
if (isinstance(obj, (type, types.ClassType)) and
issubclass(obj, unittest.TestCase)):
testnames = testloader.getTestCaseNames(obj)
for testname in testnames:
suite.addTest(obj(weakref.ref(marionette),
fixtures,
methodName=testname,
filepath=filepath,
testvars=testvars,
**kwargs))
def setUp(self):
super(MarionetteTestCase, self).setUp()
self.marionette.test_name = self.test_name
self.marionette.execute_script("log('TEST-START: {0}:{1}')"
.format(self.filepath.replace('\\', '\\\\'),
self.methodName),
sandbox="simpletest")
def tearDown(self):
# In the case no session is active (eg. the application was quit), start
# a new session for clean-up steps.
if not self.marionette.session:
self.marionette.start_session()
if not self.marionette.crashed:
try:
self.marionette.clear_imported_scripts()
self.marionette.execute_script("log('TEST-END: {0}:{1}')"
.format(self.filepath.replace('\\', '\\\\'),
self.methodName),
sandbox="simpletest")
self.marionette.test_name = None
except (MarionetteException, IOError):
# We have tried to log the test end when there is no listener
# object that we can access
pass
super(MarionetteTestCase, self).tearDown()
def wait_for_condition(self, method, timeout=30):
timeout = float(timeout) + time.time()
while time.time() < timeout:
value = method(self.marionette)
if value:
return value
time.sleep(0.5)
else:
raise TimeoutException("wait_for_condition timed out")