Merge autoland to central, a=merge

MozReview-Commit-ID: FTkalcNxO2k
This commit is contained in:
Wes Kocher 2016-12-28 16:00:35 -08:00
commit 8294014f54
36 changed files with 654 additions and 372 deletions

View File

@ -3,3 +3,4 @@
[browser_EventEmitter.js]
[browser_Storage.js]
[browser_Heartbeat.js]
skip-if = true # bug 1325409

View File

@ -140,19 +140,36 @@ normalize_path = normalize_path()
@imports(_from='which', _import='which')
@imports(_from='which', _import='WhichError')
@imports('itertools')
@imports('sys')
@imports(_from='os', _import='pathsep')
@imports(_from='os', _import='environ')
def find_program(file, paths=None):
# The following snippet comes from `which` itself, with a slight
# modification to use lowercase extensions, because it's confusing rustup
# (on top of making results not really appealing to the eye).
# Windows has the concept of a list of extensions (PATHEXT env var).
if sys.platform.startswith("win"):
exts = [e.lower()
for e in environ.get("PATHEXT", "").split(pathsep)]
# If '.exe' is not in exts then obviously this is Win9x and
# or a bogus PATHEXT, then use a reasonable default.
if '.exe' not in exts:
exts = ['.com', '.exe', '.bat']
else:
exts = None
try:
if is_absolute_or_relative(file):
return normalize_path(which(os.path.basename(file),
[os.path.dirname(file)]))
[os.path.dirname(file)], exts=exts))
if paths:
if not isinstance(paths, (list, tuple)):
die("Paths provided to find_program must be a list of strings, "
"not %r", paths)
paths = list(itertools.chain(
*(p.split(pathsep) for p in paths if p)))
return normalize_path(which(file, path=paths))
return normalize_path(which(file, path=paths, exts=exts))
except WhichError:
return None

View File

@ -188,7 +188,7 @@
</div>
</div>
<div id="propertyContainer" class="theme-separator" tabindex="0">
<div id="propertyContainer" class="theme-separator" tabindex="0" dir="ltr">
</div>
<div id="computedview-no-results" hidden="" data-localization="content=inspector.noProperties"></div>

View File

@ -490,7 +490,8 @@ ServoStyleSet::NoteStyleSheetsChanged()
void
ServoStyleSet::AssertTreeIsClean()
{
if (Element* root = mPresContext->Document()->GetRootElement()) {
DocumentStyleRootIterator iter(mPresContext->Document());
while (Element* root = iter.GetNextStyleRoot()) {
Servo_AssertTreeIsClean(root);
}
}

View File

@ -369,7 +369,17 @@ class RefTest(object):
# Enable leaks detection to its own log file.
self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log")
browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile
# Leak checking was broken in reftest unnoticed for a length of time. During
# this time, a leak slipped into the crashtest suite. The leak checking was
# fixed by bug 1325148, but it couldn't land until the regression in crashtest
# was also fixed or backed out. Rather than waiting and risking new regressions,
# temporarily disable leak checking in crashtest. Fix is tracked by bug 1325215.
if options.suite == 'crashtest' and mozinfo.info['os'] == 'linux':
self.log.warning('WARNING | leakcheck disabled due to bug 1325215')
else:
browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile
return browserEnv
def killNamedOrphans(self, pname):

View File

@ -146,11 +146,13 @@ class MachCommands(MachCommandBase):
try:
for future in as_completed(futures):
output, ret = future.result()
output, ret, test_path = future.result()
for line in output:
self.log(logging.INFO, 'python-test', {'line': line.rstrip()}, '{line}')
if ret and not return_code:
self.log(logging.ERROR, 'python-test', {'test_path': test_path, 'ret': ret}, 'Setting retcode to {ret} from {test_path}')
return_code = return_code or ret
except KeyboardInterrupt:
# Hack to force stop currently running threads.
@ -159,6 +161,7 @@ class MachCommands(MachCommandBase):
thread._threads_queues.clear()
raise
self.log(logging.INFO, 'python-test', {'return_code': return_code}, 'Return code from mach python-test: {return_code}')
return return_code
def _run_python_test(self, test_path):
@ -204,4 +207,4 @@ class MachCommands(MachCommandBase):
else:
_log('Test passed: {}'.format(test_path))
return output, return_code
return output, return_code, test_path

View File

@ -537,6 +537,16 @@ class BaseBootstrapper(object):
cargo_bin = os.path.join(cargo_home, 'bin')
return cargo_home, cargo_bin
def win_to_msys_path(self, path):
'''Convert a windows-style path to msys style.'''
drive, path = os.path.splitdrive(path)
path = '/'.join(path.split('\\'))
if drive:
if path[0] == '/':
path = path[1:]
path = '/%s/%s' % (drive[:-1], path)
return path
def print_rust_path_advice(self, template, cargo_home, cargo_bin):
# Suggest ~/.cargo/env if it exists.
if os.path.exists(os.path.join(cargo_home, 'env')):
@ -546,7 +556,7 @@ class BaseBootstrapper(object):
# so fall back to a manual PATH update. Bootstrap
# only runs under msys, so a unix-style shell command
# is appropriate there.
cmd = 'export PATH=%s:$PATH' % cargo_bin
cmd = 'export PATH=%s:$PATH' % self.win_to_msys_path(cargo_bin)
print(template % {
'cargo_bin': cargo_bin,
'cmd': cmd,

View File

@ -175,7 +175,7 @@ class ConfigureTestSandbox(ConfigureSandbox):
path_out.value = fake_short_path(path_in)
return length
def which(self, command, path=None):
def which(self, command, path=None, exts=None):
for parent in (path or self._search_path):
c = mozpath.abspath(mozpath.join(parent, command))
for candidate in (c, ensure_exe_extension(c)):

View File

@ -475,6 +475,8 @@ def build_macosx_engine_payload(config, task, task_def):
@payload_builder('buildbot-bridge')
def build_buildbot_bridge_payload(config, task, task_def):
del task['extra']['treeherder']
del task['extra']['treeherderEnv']
worker = task['worker']
task_def['payload'] = {
'buildername': worker['buildername'],

View File

@ -29,7 +29,7 @@ class TestDVCertificate(PuppeteerMixin, MarionetteTestCase):
with self.marionette.using_context('content'):
self.marionette.navigate(self.url)
self.assertEqual(self.locationbar.identity_box.get_attribute('className'),
self.assertEqual(self.locationbar.identity_box.get_property('className'),
'verifiedDomain')
# Open the identity popup
@ -41,7 +41,7 @@ class TestDVCertificate(PuppeteerMixin, MarionetteTestCase):
cert = self.browser.tabbar.selected_tab.certificate
# The shown host equals to the certificate
self.assertEqual(self.identity_popup.view.main.host.get_attribute('textContent'),
self.assertEqual(self.identity_popup.view.main.host.get_property('textContent'),
cert['commonName'])
# Only the secure label is visible in the main view
@ -52,7 +52,9 @@ class TestDVCertificate(PuppeteerMixin, MarionetteTestCase):
self.assertEqual(insecure_label.value_of_css_property('display'), 'none')
self.identity_popup.view.main.expander.click()
Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
Wait(self.marionette).until(
lambda _: self.identity_popup.view.security.selected,
message='Security view of identity popup has not been selected.')
# Only the secure label is visible in the security view
secure_label = self.identity_popup.view.security.secure_connection_label
@ -62,7 +64,7 @@ class TestDVCertificate(PuppeteerMixin, MarionetteTestCase):
self.assertEqual(insecure_label.value_of_css_property('display'), 'none')
verifier_label = self.browser.localize_property('identity.identified.verifier')
self.assertEqual(self.identity_popup.view.security.verifier.get_attribute('textContent'),
self.assertEqual(self.identity_popup.view.security.verifier.get_property('textContent'),
verifier_label.replace("%S", cert['issuerOrganization']))
def opener(mn):
@ -73,11 +75,11 @@ class TestDVCertificate(PuppeteerMixin, MarionetteTestCase):
self.assertEqual(deck.selected_panel, deck.security)
self.assertEqual(deck.security.domain.get_attribute('value'),
self.assertEqual(deck.security.domain.get_property('value'),
cert['commonName'])
self.assertEqual(deck.security.owner.get_attribute('value'),
self.assertEqual(deck.security.owner.get_property('value'),
page_info_window.localize_property('securityNoOwner'))
self.assertEqual(deck.security.verifier.get_attribute('value'),
self.assertEqual(deck.security.verifier.get_property('value'),
cert['issuerOrganization'])

View File

@ -30,7 +30,7 @@ class TestEVCertificate(PuppeteerMixin, MarionetteTestCase):
self.marionette.navigate(self.url)
# Check the identity box
self.assertEqual(self.locationbar.identity_box.get_attribute('className'),
self.assertEqual(self.locationbar.identity_box.get_property('className'),
'verifiedIdentity')
# Get the information from the certificate
@ -38,9 +38,9 @@ class TestEVCertificate(PuppeteerMixin, MarionetteTestCase):
address = self.puppeteer.security.get_address_from_certificate(cert)
# Check the identity popup label displays
self.assertEqual(self.locationbar.identity_organization_label.get_attribute('value'),
self.assertEqual(self.locationbar.identity_organization_label.get_property('value'),
cert['organization'])
self.assertEqual(self.locationbar.identity_country_label.get_attribute('value'),
self.assertEqual(self.locationbar.identity_country_label.get_property('value'),
'(' + address['country'] + ')')
# Open the identity popup
@ -50,7 +50,7 @@ class TestEVCertificate(PuppeteerMixin, MarionetteTestCase):
self.assertEqual(self.identity_popup.element.get_attribute('connection'), 'secure-ev')
# For EV certificates no hostname but the organization name is shown
self.assertEqual(self.identity_popup.view.main.host.get_attribute('textContent'),
self.assertEqual(self.identity_popup.view.main.host.get_property('textContent'),
cert['organization'])
# Only the secure label is visible in the main view
@ -73,8 +73,7 @@ class TestEVCertificate(PuppeteerMixin, MarionetteTestCase):
self.assertEqual(insecure_label.value_of_css_property('display'), 'none')
# Check the organization name
self.assertEqual(security_view.owner.get_attribute('textContent'),
cert['organization'])
self.assertEqual(security_view.owner.get_property('textContent'), cert['organization'])
# Check the owner location string
# More information:
@ -82,14 +81,12 @@ class TestEVCertificate(PuppeteerMixin, MarionetteTestCase):
location = self.browser.localize_property('identity.identified.state_and_country')
location = location.replace('%S', address['state'], 1).replace('%S', address['country'])
location = address['city'] + '\n' + location
self.assertEqual(security_view.owner_location.get_attribute('textContent'),
location)
self.assertEqual(security_view.owner_location.get_property('textContent'), location)
# Check the verifier
l10n_verifier = self.browser.localize_property('identity.identified.verifier')
l10n_verifier = l10n_verifier.replace('%S', cert['issuerOrganization'])
self.assertEqual(security_view.verifier.get_attribute('textContent'),
l10n_verifier)
self.assertEqual(security_view.verifier.get_property('textContent'), l10n_verifier)
# Open the Page Info window by clicking the More Information button
page_info = self.browser.open_page_info_window(
@ -101,14 +98,14 @@ class TestEVCertificate(PuppeteerMixin, MarionetteTestCase):
# Verify the domain listed on the security panel
self.assertIn(cert['commonName'],
page_info.deck.security.domain.get_attribute('value'))
page_info.deck.security.domain.get_property('value'))
# Verify the owner listed on the security panel
self.assertEqual(page_info.deck.security.owner.get_attribute('value'),
self.assertEqual(page_info.deck.security.owner.get_property('value'),
cert['organization'])
# Verify the verifier listed on the security panel
self.assertEqual(page_info.deck.security.verifier.get_attribute('value'),
self.assertEqual(page_info.deck.security.verifier.get_property('value'),
cert['issuerOrganization'])
finally:
page_info.close()

View File

@ -25,7 +25,7 @@ class TestMixedContentPage(PuppeteerMixin, MarionetteTestCase):
with self.marionette.using_context('content'):
self.marionette.navigate(self.url)
self.assertEqual(self.locationbar.identity_box.get_attribute('className'),
self.assertEqual(self.locationbar.identity_box.get_property('className'),
'unknownIdentity mixedDisplayContent')
# Open the identity popup

View File

@ -46,7 +46,7 @@ class TestMixedScriptContentBlocking(PuppeteerMixin, MarionetteTestCase):
# First call to Wait() needs a longer timeout due to the reload of the web page.
Wait(self.marionette, timeout=self.marionette.timeout.page_load).until(
lambda _: self.locationbar.identity_box.get_attribute('className') == identity,
lambda _: self.locationbar.identity_box.get_property('className') == identity,
message='Expected identity "{}" not found'.format(identity)
)

View File

@ -39,7 +39,7 @@ class TestNoCertificate(PuppeteerMixin, MarionetteTestCase):
self.assertFalse(favicon_hidden, 'The identity icon is visible')
# Check that the identity box organization label is blank
self.assertEqual(self.locationbar.identity_organization_label.get_attribute('value'), '',
self.assertEqual(self.locationbar.identity_organization_label.get_property('value'), '',
'The organization has no label')
# Open the identity popup
@ -70,12 +70,12 @@ class TestNoCertificate(PuppeteerMixin, MarionetteTestCase):
# Check the domain listed on the security panel contains the url's host name
self.assertIn(urlparse(self.url).hostname,
page_info.deck.security.domain.get_attribute('value'))
page_info.deck.security.domain.get_property('value'))
# Check the owner label equals localized 'securityNoOwner'
self.assertEqual(page_info.deck.security.owner.get_attribute('value'),
self.assertEqual(page_info.deck.security.owner.get_property('value'),
page_info.localize_property('securityNoOwner'))
# Check the verifier label equals localized 'notset'
self.assertEqual(page_info.deck.security.verifier.get_attribute('value'),
self.assertEqual(page_info.deck.security.verifier.get_property('value'),
page_info.localize_property('notset'))

View File

@ -50,7 +50,7 @@ class TestSecurityNotification(PuppeteerMixin, MarionetteTestCase):
self.marionette.navigate(self.urls[1])
Wait(self.marionette).until(lambda _: (
self.identity_box.get_attribute('className') == 'verifiedIdentity')
self.identity_box.get_property('className') == 'verifiedIdentity')
)
def test_insecure_website(self):
@ -58,5 +58,5 @@ class TestSecurityNotification(PuppeteerMixin, MarionetteTestCase):
self.marionette.navigate(self.urls[2])
Wait(self.marionette).until(lambda _: (
self.identity_box.get_attribute('className') == 'unknownIdentity')
self.identity_box.get_property('className') == 'unknownIdentity')
)

View File

@ -76,7 +76,7 @@ class TestSSLStatusAfterRestart(PuppeteerMixin, MarionetteTestCase):
self.locationbar.open_identity_popup()
# Check the type shown on the idenity popup doorhanger
# Check the type shown on the identity popup doorhanger
self.assertEqual(self.identity_popup.element.get_attribute('connection'),
cert_type)
@ -84,7 +84,7 @@ class TestSSLStatusAfterRestart(PuppeteerMixin, MarionetteTestCase):
Wait(self.marionette).until(lambda _: self.identity_popup.view.security.selected)
# Check the identity label
self.assertEqual(self.locationbar.identity_organization_label.get_attribute('value'),
self.assertEqual(self.locationbar.identity_organization_label.get_property('value'),
identity)
# Get the information from the certificate
@ -101,10 +101,10 @@ class TestSSLStatusAfterRestart(PuppeteerMixin, MarionetteTestCase):
# If this is a wildcard cert, check only the domain
if cert['commonName'].startswith('*'):
self.assertIn(self.puppeteer.security.get_domain_from_common_name(cert['commonName']),
page_info.deck.security.domain.get_attribute('value'),
page_info.deck.security.domain.get_property('value'),
'Expected domain found in certificate for ' + url)
else:
self.assertEqual(page_info.deck.security.domain.get_attribute('value'),
self.assertEqual(page_info.deck.security.domain.get_property('value'),
cert['commonName'],
'Domain value matches certificate common name.')
@ -114,11 +114,11 @@ class TestSSLStatusAfterRestart(PuppeteerMixin, MarionetteTestCase):
else:
owner = page_info.localize_property('securityNoOwner')
self.assertEqual(page_info.deck.security.owner.get_attribute('value'), owner,
self.assertEqual(page_info.deck.security.owner.get_property('value'), owner,
'Expected owner label found for ' + url)
# Verify the verifier listed on the security panel
self.assertEqual(page_info.deck.security.verifier.get_attribute('value'),
self.assertEqual(page_info.deck.security.verifier.get_property('value'),
cert['issuerOrganization'],
'Verifier matches issuer of certificate for ' + url)
page_info.close()

View File

@ -27,31 +27,31 @@ class TestAboutWindow(PuppeteerMixin, MarionetteTestCase):
"""Test correct retrieval of elements."""
self.assertNotEqual(self.about_window.dtds, [])
self.assertEqual(self.deck.element.get_attribute('localName'), 'deck')
self.assertEqual(self.deck.element.get_property('localName'), 'deck')
# apply panel
panel = self.deck.apply
self.assertEqual(panel.element.get_attribute('localName'), 'hbox')
self.assertEqual(panel.button.get_attribute('localName'), 'button')
self.assertEqual(panel.element.get_property('localName'), 'hbox')
self.assertEqual(panel.button.get_property('localName'), 'button')
# check_for_updates panel
panel = self.deck.check_for_updates
self.assertEqual(panel.element.get_attribute('localName'), 'hbox')
self.assertEqual(panel.button.get_attribute('localName'), 'button')
self.assertEqual(panel.element.get_property('localName'), 'hbox')
self.assertEqual(panel.button.get_property('localName'), 'button')
# checking_for_updates panel
self.assertEqual(self.deck.checking_for_updates.element.get_attribute('localName'), 'hbox')
self.assertEqual(self.deck.checking_for_updates.element.get_property('localName'), 'hbox')
# download_and_install panel
panel = self.deck.download_and_install
self.assertEqual(panel.element.get_attribute('localName'), 'hbox')
self.assertEqual(panel.button.get_attribute('localName'), 'button')
self.assertEqual(panel.element.get_property('localName'), 'hbox')
self.assertEqual(panel.button.get_property('localName'), 'button')
# download_failed panel
self.assertEqual(self.deck.download_failed.element.get_attribute('localName'), 'hbox')
self.assertEqual(self.deck.download_failed.element.get_property('localName'), 'hbox')
# downloading panel
self.assertEqual(self.deck.downloading.element.get_attribute('localName'), 'hbox')
self.assertEqual(self.deck.downloading.element.get_property('localName'), 'hbox')
def test_open_window(self):
"""Test various opening strategies."""

View File

@ -21,32 +21,32 @@ class TestPageInfoWindow(PuppeteerMixin, MarionetteTestCase):
self.assertNotEqual(page_info.dtds, [])
self.assertNotEqual(page_info.properties, [])
self.assertEqual(page_info.deck.element.get_attribute('localName'), 'deck')
self.assertEqual(page_info.deck.element.get_property('localName'), 'deck')
# feed panel
self.assertEqual(page_info.deck.feed.element.get_attribute('localName'), 'vbox')
self.assertEqual(page_info.deck.feed.element.get_property('localName'), 'vbox')
# general panel
self.assertEqual(page_info.deck.general.element.get_attribute('localName'), 'vbox')
self.assertEqual(page_info.deck.general.element.get_property('localName'), 'vbox')
# media panel
self.assertEqual(page_info.deck.media.element.get_attribute('localName'), 'vbox')
self.assertEqual(page_info.deck.media.element.get_property('localName'), 'vbox')
# permissions panel
self.assertEqual(page_info.deck.permissions.element.get_attribute('localName'), 'vbox')
self.assertEqual(page_info.deck.permissions.element.get_property('localName'), 'vbox')
# security panel
panel = page_info.deck.select(page_info.deck.security)
self.assertEqual(panel.element.get_attribute('localName'), 'vbox')
self.assertEqual(panel.element.get_property('localName'), 'vbox')
self.assertEqual(panel.domain.get_attribute('localName'), 'textbox')
self.assertEqual(panel.owner.get_attribute('localName'), 'textbox')
self.assertEqual(panel.verifier.get_attribute('localName'), 'textbox')
self.assertEqual(panel.domain.get_property('localName'), 'textbox')
self.assertEqual(panel.owner.get_property('localName'), 'textbox')
self.assertEqual(panel.verifier.get_property('localName'), 'textbox')
self.assertEqual(panel.view_certificate.get_attribute('localName'), 'button')
self.assertEqual(panel.view_cookies.get_attribute('localName'), 'button')
self.assertEqual(panel.view_passwords.get_attribute('localName'), 'button')
self.assertEqual(panel.view_certificate.get_property('localName'), 'button')
self.assertEqual(panel.view_cookies.get_property('localName'), 'button')
self.assertEqual(panel.view_passwords.get_property('localName'), 'button')
def test_select(self):
"""Test properties and methods for switching between panels."""

View File

@ -23,8 +23,8 @@ class TestTabBar(PuppeteerMixin, MarionetteTestCase):
self.assertEqual(len(tabbar.tabs), 1)
self.assertEqual(tabbar.tabs[0].handle, self.marionette.current_window_handle)
self.assertEqual(tabbar.newtab_button.get_attribute('localName'), 'toolbarbutton')
self.assertEqual(tabbar.toolbar.get_attribute('localName'), 'tabs')
self.assertEqual(tabbar.newtab_button.get_property('localName'), 'toolbarbutton')
self.assertEqual(tabbar.toolbar.get_property('localName'), 'tabs')
def test_open_close(self):
tabbar = self.browser.tabbar
@ -127,8 +127,8 @@ class TestTab(PuppeteerMixin, MarionetteTestCase):
self.assertEqual(tab.window, self.browser)
self.assertEqual(tab.tab_element.get_attribute('localName'), 'tab')
self.assertEqual(tab.close_button.get_attribute('localName'), 'toolbarbutton')
self.assertEqual(tab.tab_element.get_property('localName'), 'tab')
self.assertEqual(tab.close_button.get_property('localName'), 'toolbarbutton')
def test_certificate(self):
url = self.marionette.absolute_url('layout/mozilla.html')

View File

@ -26,11 +26,11 @@ class TestNavBar(PuppeteerMixin, MarionetteTestCase):
""")
def test_elements(self):
self.assertEqual(self.navbar.back_button.get_attribute('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.forward_button.get_attribute('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.home_button.get_attribute('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.menu_button.get_attribute('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.toolbar.get_attribute('localName'), 'toolbar')
self.assertEqual(self.navbar.back_button.get_property('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.forward_button.get_property('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.home_button.get_property('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.menu_button.get_property('localName'), 'toolbarbutton')
self.assertEqual(self.navbar.toolbar.get_property('localName'), 'toolbar')
def test_buttons(self):
self.marionette.set_context('content')
@ -85,24 +85,24 @@ class TestLocationBar(PuppeteerMixin, MarionetteTestCase):
self.locationbar = self.browser.navbar.locationbar
def test_elements(self):
self.assertEqual(self.locationbar.urlbar.get_attribute('localName'), 'textbox')
self.assertIn('urlbar-input', self.locationbar.urlbar_input.get_attribute('className'))
self.assertEqual(self.locationbar.urlbar.get_property('localName'), 'textbox')
self.assertIn('urlbar-input', self.locationbar.urlbar_input.get_property('className'))
self.assertEqual(self.locationbar.connection_icon.get_attribute('localName'), 'image')
self.assertEqual(self.locationbar.identity_box.get_attribute('localName'), 'box')
self.assertEqual(self.locationbar.identity_country_label.get_attribute('localName'),
self.assertEqual(self.locationbar.connection_icon.get_property('localName'), 'image')
self.assertEqual(self.locationbar.identity_box.get_property('localName'), 'box')
self.assertEqual(self.locationbar.identity_country_label.get_property('localName'),
'label')
self.assertEqual(self.locationbar.identity_organization_label.get_attribute('localName'),
self.assertEqual(self.locationbar.identity_organization_label.get_property('localName'),
'label')
self.assertEqual(self.locationbar.identity_icon.get_attribute('localName'), 'image')
self.assertEqual(self.locationbar.history_drop_marker.get_attribute('localName'),
self.assertEqual(self.locationbar.identity_icon.get_property('localName'), 'image')
self.assertEqual(self.locationbar.history_drop_marker.get_property('localName'),
'dropmarker')
self.assertEqual(self.locationbar.reload_button.get_attribute('localName'),
self.assertEqual(self.locationbar.reload_button.get_property('localName'),
'toolbarbutton')
self.assertEqual(self.locationbar.stop_button.get_attribute('localName'),
self.assertEqual(self.locationbar.stop_button.get_property('localName'),
'toolbarbutton')
self.assertEqual(self.locationbar.contextmenu.get_attribute('localName'), 'menupopup')
self.assertEqual(self.locationbar.contextmenu.get_property('localName'), 'menupopup')
self.assertEqual(self.locationbar.get_contextmenu_entry('paste').get_attribute('cmd'),
'cmd_paste')
@ -162,7 +162,7 @@ class TestAutoCompleteResults(PuppeteerMixin, MarionetteTestCase):
visible_result_count = len(self.autocomplete_results.visible_results)
self.assertTrue(visible_result_count > 0)
self.assertEqual(visible_result_count,
int(results.get_attribute('itemCount')))
int(results.get_property('itemCount')))
def test_close(self):
self.browser.navbar.locationbar.urlbar.send_keys('a')
@ -226,39 +226,39 @@ class TestIdentityPopup(PuppeteerMixin, MarionetteTestCase):
# Test main view elements
main = self.identity_popup.view.main
self.assertEqual(main.element.get_attribute('localName'), 'panelview')
self.assertEqual(main.element.get_property('localName'), 'panelview')
self.assertEqual(main.expander.get_attribute('localName'), 'button')
self.assertEqual(main.host.get_attribute('localName'), 'label')
self.assertEqual(main.insecure_connection_label.get_attribute('localName'),
self.assertEqual(main.expander.get_property('localName'), 'button')
self.assertEqual(main.host.get_property('localName'), 'label')
self.assertEqual(main.insecure_connection_label.get_property('localName'),
'description')
self.assertEqual(main.internal_connection_label.get_attribute('localName'),
self.assertEqual(main.internal_connection_label.get_property('localName'),
'description')
self.assertEqual(main.secure_connection_label.get_attribute('localName'),
self.assertEqual(main.secure_connection_label.get_property('localName'),
'description')
self.assertEqual(main.permissions.get_attribute('localName'), 'vbox')
self.assertEqual(main.permissions.get_property('localName'), 'vbox')
# Test security view elements
security = self.identity_popup.view.security
self.assertEqual(security.element.get_attribute('localName'), 'panelview')
self.assertEqual(security.element.get_property('localName'), 'panelview')
self.assertEqual(security.host.get_attribute('localName'), 'label')
self.assertEqual(security.insecure_connection_label.get_attribute('localName'),
self.assertEqual(security.host.get_property('localName'), 'label')
self.assertEqual(security.insecure_connection_label.get_property('localName'),
'description')
self.assertEqual(security.secure_connection_label.get_attribute('localName'),
self.assertEqual(security.secure_connection_label.get_property('localName'),
'description')
self.assertEqual(security.owner.get_attribute('localName'), 'description')
self.assertEqual(security.owner_location.get_attribute('localName'), 'description')
self.assertEqual(security.verifier.get_attribute('localName'), 'description')
self.assertEqual(security.owner.get_property('localName'), 'description')
self.assertEqual(security.owner_location.get_property('localName'), 'description')
self.assertEqual(security.verifier.get_property('localName'), 'description')
self.assertEqual(security.disable_mixed_content_blocking_button.get_attribute('localName'),
self.assertEqual(security.disable_mixed_content_blocking_button.get_property('localName'),
'button')
self.assertEqual(security.enable_mixed_content_blocking_button.get_attribute('localName'),
self.assertEqual(security.enable_mixed_content_blocking_button.get_property('localName'),
'button')
self.assertEqual(security.more_info_button.get_attribute('localName'), 'button')
self.assertEqual(security.more_info_button.get_property('localName'), 'button')
def test_open_close(self):
with self.marionette.using_context('content'):

View File

@ -36,13 +36,13 @@ class TestUpdateWizard(PuppeteerMixin, MarionetteTestCase):
def test_elements(self):
"""Test correct retrieval of elements."""
self.assertEqual(self.wizard.element.get_attribute('localName'), 'wizard')
self.assertEqual(self.wizard.element.get_property('localName'), 'wizard')
buttons = ('cancel_button', 'extra1_button', 'extra2_button',
'finish_button', 'next_button', 'previous_button',
)
for button in buttons:
self.assertEqual(getattr(self.wizard, button).get_attribute('localName'),
self.assertEqual(getattr(self.wizard, button).get_property('localName'),
'button')
panels = ('checking', 'downloading', 'dummy', 'error_patching', 'error',
@ -50,13 +50,13 @@ class TestUpdateWizard(PuppeteerMixin, MarionetteTestCase):
'manual_update', 'no_updates_found', 'updates_found_basic',
)
for panel in panels:
self.assertEqual(getattr(self.wizard, panel).element.get_attribute('localName'),
self.assertEqual(getattr(self.wizard, panel).element.get_property('localName'),
'wizardpage')
# elements of the checking panel
self.assertEqual(self.wizard.checking.progress.get_attribute('localName'),
self.assertEqual(self.wizard.checking.progress.get_property('localName'),
'progressmeter')
# elements of the downloading panel
self.assertEqual(self.wizard.downloading.progress.get_attribute('localName'),
self.assertEqual(self.wizard.downloading.progress.get_property('localName'),
'progressmeter')

View File

@ -30,12 +30,11 @@ this.capture = {};
* The canvas element where the element has been painted on.
*/
capture.element = function (node, highlights=[]) {
let doc = node.ownerDocument;
let win = doc.defaultView;
let win = node.ownerDocument.defaultView;
let rect = node.getBoundingClientRect();
return capture.canvas(
doc,
win,
rect.left,
rect.top,
rect.width,
@ -44,12 +43,12 @@ capture.element = function (node, highlights=[]) {
};
/**
* Take a screenshot of the document's viewport, taking into account
* the current window's offset.
* Take a screenshot of the window's viewport by taking into account
* the current offsets.
*
* @param {Document} document
* The DOM document providing the document element to capture,
* and a window for determining the offset of the viewport.
* @param {DOMWindow} win
* The DOM window providing the document element to capture,
* and the offsets for the viewport.
* @param {Array.<Node>=} highlights
* Optional array of nodes, around which a border will be marked to
* highlight them in the screenshot.
@ -57,25 +56,24 @@ capture.element = function (node, highlights=[]) {
* @return {HTMLCanvasElement}
* The canvas element where the viewport has been painted on.
*/
capture.viewport = function (document, highlights=[]) {
let win = document.defaultView;
let docEl = document.documentElement;
capture.viewport = function (win, highlights=[]) {
let rootNode = win.document.documentElement;
return capture.canvas(
document,
win,
win.pageXOffset,
win.pageYOffset,
docEl.clientWidth,
docEl.clientHeight,
rootNode.clientWidth,
rootNode.clientHeight,
highlights);
};
/**
* Low-level interface to draw a rectangle off the framebuffer.
*
* @param {Document} document
* A DOM document providing the window used to the framebuffer,
* and interfaces for creating an HTMLCanvasElement.
* @param {DOMWindow} win
* The DOM window used for the framebuffer, and providing the interfaces
* for creating an HTMLCanvasElement.
* @param {number} left
* The left, X axis offset of the rectangle.
* @param {number} top
@ -92,15 +90,23 @@ capture.viewport = function (document, highlights=[]) {
* The canvas on which the selection from the window's framebuffer
* has been painted on.
*/
capture.canvas = function (document, left, top, width, height, highlights=[]) {
let win = document.defaultView;
capture.canvas = function (win, left, top, width, height, highlights=[]) {
let scale = win.devicePixelRatio;
let canvas = document.createElementNS(XHTML_NS, "canvas");
canvas.width = width;
canvas.height = height;
let canvas = win.document.createElementNS(XHTML_NS, "canvas");
canvas.width = width * scale;
canvas.height = height * scale;
let ctx = canvas.getContext(CONTEXT_2D);
ctx.drawWindow(win, left, top, width, height, BG_COLOUR);
let flags = ctx.DRAWWINDOW_DRAW_CARET;
// Disabled in bug 1243415 for webplatform-test failures due to out of view elements.
// Needs https://github.com/w3c/web-platform-tests/issues/4383 fixed.
// ctx.DRAWWINDOW_DRAW_VIEW;
// Bug 1009762 - Crash in [@ mozilla::gl::ReadPixelsIntoDataSurface]
// ctx.DRAWWINDOW_USE_WIDGET_LAYERS;
ctx.scale(scale, scale);
ctx.drawWindow(win, left, top, width, height, BG_COLOUR, flags);
ctx = capture.highlight_(ctx, highlights, top, left);
return canvas;

View File

@ -1,19 +1,31 @@
<?xml version="1.0"?>
<!-- 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/. -->
<!DOCTYPE dialog [
]>
<dialog id="dialogTest"
buttons="accept, cancel"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<vbox id="things">
<checkbox id="testBox" label="box" />
<dialog id="testDialog"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="Test Dialog"
buttons="accept,cancel">
<vbox flex="1" style="min-width: 300px; min-height: 500px;">
<label>Settings</label>
<separator class="thin"/>
<richlistbox id="test-list" flex="1">
<richlistitem id="item-choose" orient="horizontal" selected="true">
<label id="choose-label" value="First Entry" flex="1"/>
<button id="choose-button" oncommand="" label="Choose..."/>
</richlistitem>
</richlistbox>
<separator class="thin"/>
<checkbox id="check-box" label="Test Mode 2" />
<hbox align="center">
<label id="text-box-label" control="text-box">Name:</label>
<textbox id="text-box" flex="1" />
</hbox>
</vbox>
</dialog>

View File

@ -69,8 +69,14 @@ class HTMLElement(object):
"""Returns the requested property, or None if the property is
not set.
"""
body = {"id": self.id, "name": name}
return self.marionette._send_message("getElementProperty", body, key="value")
try:
body = {"id": self.id, "name": name}
return self.marionette._send_message("getElementProperty", body, key="value")
except errors.UnknownCommandException:
# Keep backward compatibility for code which uses get_attribute() to
# also retrieve element properties.
# Remove when Firefox 55 is stable.
return self.get_attribute(name)
def click(self):
self.marionette._send_message("clickElement", {"id": self.id})

View File

@ -22,6 +22,7 @@ Cu.import("chrome://marionette/content/addon.js");
Cu.import("chrome://marionette/content/assert.js");
Cu.import("chrome://marionette/content/atom.js");
Cu.import("chrome://marionette/content/browser.js");
Cu.import("chrome://marionette/content/capture.js");
Cu.import("chrome://marionette/content/cert.js");
Cu.import("chrome://marionette/content/element.js");
Cu.import("chrome://marionette/content/error.js");
@ -1842,7 +1843,8 @@ GeckoDriver.prototype.getElementAttribute = function*(cmd, resp) {
case Context.CHROME:
let win = this.getCurrentWindow();
let el = this.curBrowser.seenEls.get(id, {frame: win});
resp.body.value = atom.getElementAttribute(el, name, this.getCurrentWindow());
resp.body.value = el.getAttribute(name);
break;
case Context.CONTENT:
@ -2393,6 +2395,9 @@ GeckoDriver.prototype.clearImportedScripts = function*(cmd, resp) {
* Reference to a web element.
* @param {string} highlights
* List of web elements to highlight.
* @param {boolean} full
* True to take a screenshot of the entire document element. Is not
* considered if {@code id} is not defined. Defaults to true.
* @param {boolean} hash
* True if the user requests a hash of the image data.
*
@ -2407,44 +2412,41 @@ GeckoDriver.prototype.takeScreenshot = function (cmd, resp) {
switch (this.context) {
case Context.CHROME:
let win = this.getCurrentWindow();
let canvas = win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
let doc;
if (this.appName == "B2G") {
doc = win.document.body;
} else {
doc = win.document.documentElement;
}
let docRect = doc.getBoundingClientRect();
let width = docRect.width;
let height = docRect.height;
let canvas;
let highlightEls = [];
// Convert width and height from CSS pixels (potentially fractional)
// to device pixels (integer).
let scale = win.devicePixelRatio;
canvas.setAttribute("width", Math.round(width * scale));
canvas.setAttribute("height", Math.round(height * scale));
let container = {frame: this.getCurrentWindow().document.defaultView};
let context = canvas.getContext("2d");
let flags;
if (this.appName == "B2G") {
flags =
context.DRAWWINDOW_DRAW_CARET |
context.DRAWWINDOW_DRAW_VIEW |
context.DRAWWINDOW_USE_WIDGET_LAYERS;
} else {
// Bug 1075168: CanvasRenderingContext2D image is distorted
// when using certain flags in chrome context.
flags =
context.DRAWWINDOW_DRAW_VIEW |
context.DRAWWINDOW_USE_WIDGET_LAYERS;
if (!container.frame) {
throw new NoSuchWindowError('Unable to locate window');
}
for (let h of highlights) {
let el = this.curBrowser.seenEls.get(h, container);
highlightEls.push(el);
}
// viewport
if (!id && !full) {
canvas = capture.viewport(container.frame, highlightEls);
// element or full document element
} else {
let node;
if (id) {
node = this.curBrowser.seenEls.get(id, container);
} else {
node = container.frame.document.documentElement;
}
canvas = capture.element(node, highlightEls);
}
if (hash) {
return capture.toHash(canvas);
} else {
return capture.toBase64(canvas);
}
context.scale(scale, scale);
context.drawWindow(win, 0, 0, width, height, "rgb(255,255,255)", flags);
let dataUrl = canvas.toDataURL("image/png", "");
let data = dataUrl.substring(dataUrl.indexOf(",") + 1);
resp.body.value = data;
break;
case Context.CONTENT:
if (hash) {

View File

@ -51,7 +51,7 @@ class TestMouseAction(MarionetteTestCase):
def context_menu_state():
with self.marionette.using_context("chrome"):
cm_el = self.marionette.find_element(By.ID, "contentAreaContextMenu")
return cm_el.get_attribute("state")
return cm_el.get_property("state")
self.assertEqual("closed", context_menu_state())
self.action.context_click(click_el).perform()
@ -88,20 +88,20 @@ class TestMouseAction(MarionetteTestCase):
with self.marionette.using_context("chrome"):
urlbar = self.marionette.find_element(By.ID, "urlbar")
self.assertEqual("", urlbar.get_attribute("value"))
self.assertEqual("", urlbar.get_property("value"))
urlbar.send_keys(test_word)
self.assertEqual(urlbar.get_attribute("value"), test_word)
self.assertEqual(urlbar.get_property("value"), test_word)
(self.action.double_click(urlbar).perform()
.key_down(self.mod_key)
.key_down("x").perform())
self.assertEqual(urlbar.get_attribute("value"), "")
self.assertEqual(urlbar.get_property("value"), "")
def test_chrome_context_click_action(self):
self.marionette.set_context("chrome")
def context_menu_state():
cm_el = self.marionette.find_element(By.ID, "tabContextMenu")
return cm_el.get_attribute("state")
return cm_el.get_property("state")
currtab = self.marionette.execute_script("return gBrowser.selectedTab")
self.assertEqual("closed", context_menu_state())

View File

@ -10,54 +10,104 @@ import urllib
from unittest import skip
from marionette_driver.by import By
from marionette_harness import MarionetteTestCase, WindowManagerMixin
from marionette_driver import By
from marionette_driver.errors import JavascriptException, NoSuchWindowException
from marionette_harness import MarionetteTestCase, skip_if_mobile, WindowManagerMixin
def inline(doc, mime="text/html;charset=utf-8"):
return "data:{0},{1}".format(mime, urllib.quote(doc))
ELEMENT = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAVklEQVRoge3PMQ0AMAzAsPJHVWYbjEWTj/zx7O75oXk9AAISD6QWSC2QWiC1QGqB1AKpBVILpBZILZBaILVAaoHUAqkFUgukFkgtkFogtUBqgdT6BnIBMKa1DtYxhPkAAAAASUVORK5CYII="
HIGHLIGHTED_ELEMENT = "iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAVklEQVRoge3PQRHAQAgAMfyrwhm1sb3JIwIyN3MvmJu53f01kRqRGpEakRqRGpEakRqRGpEakRqRGpEakRqRGpEakRqRGpEakRqRGpEakRqRmvciL/gAQgW/OxTpMPwAAAAASUVORK5CYII="
box = inline(
"<div id=green style='width: 50px; height: 50px; background: silver;'></div>")
long = inline("<body style='height: 300vh'><p style='margin-top: 100vh'>foo")
short = inline("<body style='height: 10vh'>")
svg = inline("""<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20">
<rect height="20" width="20"/>
</svg>""", mime="image/svg+xml")
box = inline("<body><div id='box'><p id='green' style='width: 50px; height: 50px; "
"background: silver;'></p></div></body>")
input = inline("<body><input id='text-input'></input></body>")
long = inline("<body style='height: 300vh'><p style='margin-top: 100vh'>foo</p></body>")
short = inline("<body style='height: 10vh'></body>")
svg = inline("""
<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20">
<rect height="20" width="20"/>
</svg>""", mime="image/svg+xml")
class ScreenCaptureTestCase(MarionetteTestCase):
def assert_png(self, string):
"""Test that string is a Base64 encoded PNG file."""
image = base64.decodestring(string)
def setUp(self):
super(ScreenCaptureTestCase, self).setUp()
self._device_pixel_ratio = None
@property
def device_pixel_ratio(self):
if self._device_pixel_ratio is None:
self._device_pixel_ratio = self.marionette.execute_script("""
return window.devicePixelRatio
""")
return self._device_pixel_ratio
@property
def document_element(self):
return self.marionette.find_element(By.CSS_SELECTOR, ":root")
@property
def page_y_offset(self):
return self.marionette.execute_script("return window.pageYOffset")
@property
def viewport_dimensions(self):
return self.marionette.execute_script("""
return [arguments[0].clientWidth,
arguments[0].clientHeight];
""", script_args=[self.document_element])
def assert_png(self, screenshot):
"""Test that screenshot is a Base64 encoded PNG file."""
image = base64.decodestring(screenshot)
self.assertEqual(imghdr.what("", image), "png")
def get_image_dimensions(self, string):
self.assert_png(string)
image = base64.decodestring(string)
def assert_formats(self, element=None):
if element is None:
element = self.document_element
screenshot_default = self.marionette.screenshot(element=element)
screenshot_image = self.marionette.screenshot(element=element, format="base64")
binary1 = self.marionette.screenshot(element=element, format="binary")
binary2 = self.marionette.screenshot(element=element, format="binary")
hash1 = self.marionette.screenshot(element=element, format="hash")
hash2 = self.marionette.screenshot(element=element, format="hash")
# Valid data should have been returned
self.assert_png(screenshot_image)
self.assertEqual(imghdr.what("", binary1), "png")
self.assertEqual(screenshot_image, base64.b64encode(binary1))
self.assertEqual(hash1, hashlib.sha256(screenshot_image).hexdigest())
# Different formats produce different data
self.assertNotEqual(screenshot_image, binary1)
self.assertNotEqual(screenshot_image, hash1)
self.assertNotEqual(binary1, hash1)
# A second capture should be identical
self.assertEqual(screenshot_image, screenshot_default)
self.assertEqual(binary1, binary2)
self.assertEqual(hash1, hash2)
def get_element_dimensions(self, element):
rect = element.rect
return rect["width"], rect["height"]
def get_image_dimensions(self, screenshot):
self.assert_png(screenshot)
image = base64.decodestring(screenshot)
width, height = struct.unpack(">LL", image[16:24])
return int(width), int(height)
def scale(self, rect):
return (int(rect[0] * self.device_pixel_ratio),
int(rect[1] * self.device_pixel_ratio))
class TestScreenCaptureChrome(WindowManagerMixin, ScreenCaptureTestCase):
@property
def primary_window_dimensions(self):
current_window = self.marionette.current_chrome_window_handle
self.marionette.switch_to_window(self.start_window)
with self.marionette.using_context("chrome"):
rv = tuple(self.marionette.execute_script("""
let el = document.documentElement;
let rect = el.getBoundingClientRect();
return [rect.width, rect.height];
"""))
self.marionette.switch_to_window(current_window)
return rv
def setUp(self):
super(TestScreenCaptureChrome, self).setUp()
@ -67,90 +117,245 @@ class TestScreenCaptureChrome(WindowManagerMixin, ScreenCaptureTestCase):
self.close_all_windows()
super(TestScreenCaptureChrome, self).tearDown()
# A full chrome window screenshot is not the outer dimensions of
# the window, but instead the bounding box of the <window> inside
# <browser>.
def test_window(self):
ss = self.marionette.screenshot()
self.assert_png(ss)
self.assertEqual(self.primary_window_dimensions,
self.get_image_dimensions(ss))
@property
def window_dimensions(self):
return tuple(self.marionette.execute_script("""
let el = document.documentElement;
let rect = el.getBoundingClientRect();
return [rect.width, rect.height];
"""))
def test_chrome_delegation(self):
with self.marionette.using_context("content"):
content = self.marionette.screenshot()
chrome = self.marionette.screenshot()
self.assertNotEqual(content, chrome)
def open_dialog(self, url=None, width=None, height=None):
if url is None:
url = "chrome://marionette/content/test_dialog.xul"
def opener():
features = "chrome"
if height is not None:
features += ",height={}".format(height)
if width is not None:
features += ",width={}".format(width)
# This tests that GeckoDriver#takeScreenshot uses
# currentContext.document.documentElement instead of looking for a
# <window> element, which does not exist for all windows.
def test_secondary_windows(self):
def open_window_with_js():
self.marionette.execute_script("""
window.open('chrome://marionette/content/test_dialog.xul', 'foo',
'dialog,height=200,width=300');
""")
window.open(arguments[0], "", arguments[1]);
""", script_args=[url, features])
new_window = self.open_window(open_window_with_js)
self.marionette.switch_to_window(new_window)
return self.open_window(opener)
ss = self.marionette.screenshot()
size = self.get_image_dimensions(ss)
self.assert_png(ss)
self.assertNotEqual(self.primary_window_dimensions, size)
def test_capture_different_context(self):
"""Check that screenshots in content and chrome are different."""
with self.marionette.using_context("content"):
screenshot_content = self.marionette.screenshot()
screenshot_chrome = self.marionette.screenshot()
self.assertNotEqual(screenshot_content, screenshot_chrome)
@skip_if_mobile
def test_capture_element(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
# Ensure we only capture the element
el = self.marionette.find_element(By.ID, "test-list")
screenshot_element = self.marionette.screenshot(element=el)
self.assertEqual(self.scale(self.get_element_dimensions(el)),
self.get_image_dimensions(screenshot_element))
# Ensure we do not capture the full window
screenshot_dialog = self.marionette.screenshot()
self.assertNotEqual(screenshot_dialog, screenshot_element)
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
def test_capture_flags(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
class Content(ScreenCaptureTestCase):
@property
def body_scroll_dimensions(self):
return tuple(self.marionette.execute_script(
"return [document.body.scrollWidth, document.body.scrollHeight]"))
textbox = self.marionette.find_element(By.ID, "text-box")
textbox.send_keys("")
screenshot_focus = self.marionette.screenshot()
@property
def viewport_dimensions(self):
return tuple(self.marionette.execute_script("""
let docEl = document.documentElement;
return [docEl.clientWidth, docEl.clientHeight];"""))
self.marionette.execute_script("arguments[0].blur();", script_args=[textbox])
screenshot_no_focus = self.marionette.screenshot()
@property
def document_element(self):
return self.marionette.execute_script("return document.documentElement")
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
@property
def page_y_offset(self):
return self.marionette.execute_script("return window.pageYOffset")
self.assertNotEqual(screenshot_focus, screenshot_no_focus)
def test_capture_full_area(self):
# A full capture is not the outer dimensions of the window,
# but instead the bounding box of the window's root node (documentElement).
screenshot_full = self.marionette.screenshot()
screenshot_root = self.marionette.screenshot(element=self.document_element)
self.assert_png(screenshot_full)
self.assert_png(screenshot_root)
self.assertEqual(screenshot_root, screenshot_full)
self.assertEqual(self.scale(self.get_element_dimensions(self.document_element)),
self.get_image_dimensions(screenshot_full))
def test_capture_viewport(self):
# Load a HTML test page into the chrome window to get scrollbars
test_page = self.marionette.absolute_url("test.html")
dialog = self.open_dialog(url=test_page, width=50, height=50)
self.marionette.switch_to_window(dialog)
# Size of screenshot has to match viewport size
screenshot = self.marionette.screenshot(full=False)
self.assert_png(screenshot)
self.assertEqual(self.scale(self.viewport_dimensions),
self.get_image_dimensions(screenshot))
self.assertNotEqual(self.scale(self.window_dimensions),
self.get_image_dimensions(screenshot))
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
@skip("https://bugzilla.mozilla.org/show_bug.cgi?id=1213875")
def test_capture_scroll_element_into_view(self):
pass
def test_capture_window_already_closed(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
self.marionette.close_chrome_window()
self.assertRaises(NoSuchWindowException, self.marionette.screenshot)
self.marionette.switch_to_window(self.start_window)
def test_formats(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
self.assert_formats()
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
def test_format_unknown(self):
with self.assertRaises(ValueError):
self.marionette.screenshot(format="cheese")
@skip_if_mobile
def test_highlight_elements(self):
dialog = self.open_dialog()
self.marionette.switch_to_window(dialog)
# Highlighting the element itself shouldn't make the image larger
element = self.marionette.find_element(By.ID, "test-list")
screenshot_element = self.marionette.screenshot(element=element)
screenshot_highlight = self.marionette.screenshot(element=element,
highlights=[element])
self.assertEqual(self.scale(self.get_element_dimensions(element)),
self.get_image_dimensions(screenshot_element))
self.assertNotEqual(screenshot_element, screenshot_highlight)
# Highlighting a sub element
button = self.marionette.find_element(By.ID, "choose-button")
screenshot_highlight_button = self.marionette.screenshot(element=element,
highlights=[button])
self.assertNotEqual(screenshot_element, screenshot_highlight_button)
self.assertNotEqual(screenshot_highlight, screenshot_highlight_button)
self.marionette.close_chrome_window()
self.marionette.switch_to_window(self.start_window)
def test_highlight_element_not_seen(self):
"""Check that for not found elements an exception is raised."""
with self.marionette.using_context('content'):
self.marionette.navigate(box)
content_element = self.marionette.find_element(By.ID, "green")
self.assertRaisesRegexp(JavascriptException, "Element reference not seen before",
self.marionette.screenshot, highlights=[content_element])
chrome_document_element = self.document_element
with self.marionette.using_context('content'):
self.assertRaisesRegexp(JavascriptException, "Element reference not seen before",
self.marionette.screenshot,
highlights=[chrome_document_element])
class TestScreenCaptureContent(WindowManagerMixin, ScreenCaptureTestCase):
def setUp(self):
ScreenCaptureTestCase.setUp(self)
super(TestScreenCaptureContent, self).setUp()
self.marionette.set_context("content")
def test_html_document_element(self):
def tearDown(self):
self.close_all_tabs()
super(TestScreenCaptureContent, self).tearDown()
@property
def scroll_dimensions(self):
return tuple(self.marionette.execute_script("""
return [document.body.scrollWidth, document.body.scrollHeight]
"""))
@skip_if_mobile # Needs application independent method to open a new tab
def test_capture_tab_already_closed(self):
tab = self.open_tab()
self.marionette.switch_to_window(tab)
self.marionette.close()
self.assertRaises(NoSuchWindowException, self.marionette.screenshot)
self.marionette.switch_to_window(self.start_tab)
def test_capture_element(self):
self.marionette.navigate(box)
el = self.marionette.find_element(By.TAG_NAME, "div")
screenshot = self.marionette.screenshot(element=el)
self.assert_png(screenshot)
self.assertEqual(self.scale(self.get_element_dimensions(el)),
self.get_image_dimensions(screenshot))
@skip("https://bugzilla.mozilla.org/show_bug.cgi?id=1213875")
def test_capture_element_scrolled_into_view(self):
self.marionette.navigate(long)
string = self.marionette.screenshot()
self.assert_png(string)
self.assertEqual(
self.body_scroll_dimensions, self.get_image_dimensions(string))
el = self.marionette.find_element(By.TAG_NAME, "p")
screenshot = self.marionette.screenshot(element=el)
self.assert_png(screenshot)
self.assertEqual(self.scale(self.get_element_dimensions(el)),
self.get_image_dimensions(screenshot))
self.assertGreater(self.page_y_offset, 0)
def test_svg_document_element(self):
def test_capture_flags(self):
self.marionette.navigate(input)
textbox = self.marionette.find_element(By.ID, "text-input")
textbox.send_keys("")
screenshot_focus = self.marionette.screenshot()
self.marionette.execute_script("arguments[0].blur();", script_args=[textbox])
screenshot_no_focus = self.marionette.screenshot()
self.assertNotEqual(screenshot_focus, screenshot_no_focus)
def test_capture_html_document_element(self):
self.marionette.navigate(long)
screenshot = self.marionette.screenshot()
self.assert_png(screenshot)
self.assertEqual(self.scale(self.scroll_dimensions),
self.get_image_dimensions(screenshot))
def test_capture_svg_document_element(self):
self.marionette.navigate(svg)
doc_el = self.document_element
string = self.marionette.screenshot()
self.assert_png(string)
self.assertEqual((doc_el.rect["width"], doc_el.rect["height"]),
self.get_image_dimensions(string))
screenshot = self.marionette.screenshot()
self.assert_png(screenshot)
self.assertEqual(self.scale(self.get_element_dimensions(self.document_element)),
self.get_image_dimensions(screenshot))
def test_viewport(self):
def test_capture_viewport(self):
url = self.marionette.absolute_url("clicks.html")
self.marionette.navigate(short)
string = self.marionette.screenshot(full=False)
self.assert_png(string)
self.assertEqual(
self.viewport_dimensions, self.get_image_dimensions(string))
self.marionette.navigate(url)
screenshot = self.marionette.screenshot(full=False)
self.assert_png(screenshot)
self.assertEqual(self.scale(self.viewport_dimensions),
self.get_image_dimensions(screenshot))
def test_viewport_after_scroll(self):
def test_capture_viewport_after_scroll(self):
self.marionette.navigate(long)
before = self.marionette.screenshot()
el = self.marionette.find_element(By.TAG_NAME, "p")
@ -160,48 +365,32 @@ class Content(ScreenCaptureTestCase):
self.assertNotEqual(before, after)
self.assertGreater(self.page_y_offset, 0)
def test_element(self):
def test_formats(self):
self.marionette.navigate(box)
el = self.marionette.find_element(By.TAG_NAME, "div")
string = self.marionette.screenshot(element=el)
self.assert_png(string)
self.assertEqual(
(el.rect["width"], el.rect["height"]), self.get_image_dimensions(string))
self.assertEqual(ELEMENT, string)
@skip("https://bugzilla.mozilla.org/show_bug.cgi?id=1213875")
def test_element_scrolled_into_view(self):
self.marionette.navigate(long)
el = self.marionette.find_element(By.TAG_NAME, "p")
string = self.marionette.screenshot(element=el)
self.assert_png(string)
self.assertEqual(
(el.rect["width"], el.rect["height"]), self.get_image_dimensions(string))
self.assertGreater(self.page_y_offset, 0)
# Use a smaller region to speed up the test
element = self.marionette.find_element(By.TAG_NAME, "div")
self.assert_formats(element=element)
def test_element_with_highlight(self):
self.marionette.navigate(box)
el = self.marionette.find_element(By.TAG_NAME, "div")
string = self.marionette.screenshot(element=el, highlights=[el])
self.assert_png(string)
self.assertEqual(
(el.rect["width"], el.rect["height"]), self.get_image_dimensions(string))
self.assertEqual(HIGHLIGHTED_ELEMENT, string)
def test_binary_element(self):
self.marionette.navigate(box)
el = self.marionette.find_element(By.TAG_NAME, "div")
bin = self.marionette.screenshot(element=el, format="binary")
enc = base64.b64encode(bin)
self.assertEqual(ELEMENT, enc)
def test_unknown_format(self):
def test_format_unknown(self):
with self.assertRaises(ValueError):
self.marionette.screenshot(format="cheese")
def test_hash_format(self):
def test_highlight_elements(self):
self.marionette.navigate(box)
el = self.marionette.find_element(By.TAG_NAME, "div")
content = self.marionette.screenshot(element=el, format="hash")
hash = hashlib.sha256(ELEMENT).hexdigest()
self.assertEqual(content, hash)
element = self.marionette.find_element(By.TAG_NAME, "div")
# Highlighting the element itself shouldn't make the image larger
screenshot_element = self.marionette.screenshot(element=element)
screenshot_highlight = self.marionette.screenshot(element=element,
highlights=[element])
self.assertEqual(self.scale(self.get_element_dimensions(element)),
self.get_image_dimensions(screenshot_highlight))
self.assertNotEqual(screenshot_element, screenshot_highlight)
# Highlighting a sub element
paragraph = self.marionette.find_element(By.ID, "green")
screenshot_highlight_paragraph = self.marionette.screenshot(element=element,
highlights=[paragraph])
self.assertNotEqual(screenshot_element, screenshot_highlight_paragraph)
self.assertNotEqual(screenshot_highlight, screenshot_highlight_paragraph)

View File

@ -1673,7 +1673,7 @@ function screenshot(id, full=true, highlights=[]) {
// viewport
if (!id && !full) {
canvas = capture.viewport(curContainer.frame.document, highlightEls);
canvas = capture.viewport(curContainer.frame, highlightEls);
// element or full document element
} else {

View File

@ -62,7 +62,7 @@ class TabBar(UIBaseLib):
:return: Index of the selected tab.
"""
return int(self.toolbar.get_attribute('selectedIndex'))
return int(self.toolbar.get_property('selectedIndex'))
@property
def selected_tab(self):

View File

@ -98,7 +98,7 @@ class LocationBar(UIBaseLib):
self.focus('shortcut')
self.urlbar.send_keys(keys.Keys.DELETE)
Wait(self.marionette).until(
lambda _: self.urlbar.get_attribute('value') == '',
lambda _: self.value == '',
message='Contents of location bar could not be cleared.')
def close_context_menu(self):
@ -298,7 +298,7 @@ class LocationBar(UIBaseLib):
:returns: The urlbar value.
"""
return self.urlbar.get_attribute('value')
return self.urlbar.get_property('value')
class AutocompleteResults(UIBaseLib):
@ -342,7 +342,7 @@ class AutocompleteResults(UIBaseLib):
{'class': 'ac-emphasize-text ac-emphasize-text-%s' % match_type}
)
return [node.get_attribute('textContent') for node in emphasized_nodes]
return [node.get_property('textContent') for node in emphasized_nodes]
@property
def visible_results(self):
@ -350,7 +350,7 @@ class AutocompleteResults(UIBaseLib):
:returns: The list of visible results.
"""
match_count = self.element.get_attribute('_matchCount')
match_count = self.element.get_property('_matchCount')
return self.marionette.execute_script("""
let rv = [];
@ -370,7 +370,7 @@ class AutocompleteResults(UIBaseLib):
:returns: True when the popup is open, otherwise false.
"""
return self.element.get_attribute('state') == 'open'
return self.element.get_property('state') == 'open'
@property
def is_complete(self):
@ -404,7 +404,7 @@ class AutocompleteResults(UIBaseLib):
:returns: The index.
"""
return self.results.get_attribute('selectedIndex')
return self.results.get_property('selectedIndex')
class IdentityPopup(UIBaseLib):
@ -421,7 +421,7 @@ class IdentityPopup(UIBaseLib):
:returns: True when the popup is open, otherwise false.
"""
return self.element.get_attribute('state') == 'open'
return self.element.get_property('state') == 'open'
def close(self, force=False):
"""Closes the identity popup by hitting the escape key.

View File

@ -98,7 +98,7 @@ class Deck(UIBaseLib):
:return: Index of the selected panel.
"""
return int(self.element.get_attribute('selectedIndex'))
return int(self.element.get_property('selectedIndex'))
@property
def selected_panel(self):
@ -133,7 +133,7 @@ class PageInfoPanel(Panel):
:returns: Reference to the tab element.
"""
name = self.element.get_attribute('id').split('Panel')[0]
name = self.element.get_property('id').split('Panel')[0]
return self.window.window_element.find_element(By.ID, name + 'Tab')

View File

@ -48,12 +48,12 @@ class ShutdownLeaks(object):
def process(self):
if not self.seenShutdown:
self.logger.warning(
self.logger.error(
"TEST-UNEXPECTED-FAIL | ShutdownLeaks | process() called before end of test suite")
for test in self._parseLeakingTests():
for url, count in self._zipLeakedWindows(test["leakedWindows"]):
self.logger.warning(
self.logger.error(
"TEST-UNEXPECTED-FAIL | %s | leaked %d window(s) until shutdown "
"[url = %s]" % (test["fileName"], count, url))
@ -62,9 +62,9 @@ class ShutdownLeaks(object):
(test["fileName"], test["leakedWindowsString"]))
if test["leakedDocShells"]:
self.logger.warning("TEST-UNEXPECTED-FAIL | %s | leaked %d docShell(s) until "
"shutdown" %
(test["fileName"], len(test["leakedDocShells"])))
self.logger.error("TEST-UNEXPECTED-FAIL | %s | leaked %d docShell(s) until "
"shutdown" %
(test["fileName"], len(test["leakedDocShells"])))
self.logger.info("TEST-INFO | %s | docShell(s) leaked: %s" %
(test["fileName"], ', '.join(["[pid = %s] [id = %s]" %
x for x in test["leakedDocShells"]]
@ -77,7 +77,7 @@ class ShutdownLeaks(object):
# log line has invalid format
if not pid or not serial:
self.logger.warning(
self.logger.error(
"TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>" % line)
return
@ -99,7 +99,7 @@ class ShutdownLeaks(object):
# log line has invalid format
if not pid or not id:
self.logger.warning(
self.logger.error(
"TEST-UNEXPECTED-FAIL | ShutdownLeaks | failed to parse line <%s>" % line)
return
@ -233,8 +233,8 @@ class LSANLeaks(object):
def process(self):
if self.fatalError:
self.logger.warning("TEST-UNEXPECTED-FAIL | LeakSanitizer | LeakSanitizer "
"has encountered a fatal error.")
self.logger.error("TEST-UNEXPECTED-FAIL | LeakSanitizer | LeakSanitizer "
"has encountered a fatal error.")
if self.foundFrames:
self.logger.info("TEST-INFO | LeakSanitizer | To show the "
@ -243,7 +243,7 @@ class LSANLeaks(object):
"in testing/mozbase/mozrunner/mozrunner/utils.py")
for f in self.foundFrames:
self.logger.warning(
self.logger.error(
"TEST-UNEXPECTED-FAIL | LeakSanitizer | leak at " + f)
def _finishStack(self):

View File

@ -275,6 +275,9 @@ def setup_argument_parser():
import imp
path = os.path.join(build_obj.topobjdir, mochitest_dir, 'runtests.py')
if not os.path.exists(path):
path = os.path.join(here, "runtests.py")
with open(path, 'r') as fh:
imp.load_module('mochitest', fh, path,
('.py', 'r', imp.PY_SOURCE))

View File

@ -824,6 +824,7 @@ class MochitestDesktop(object):
self.result = {}
self.start_script = os.path.join(here, 'start_desktop.js')
self.disable_leak_checking = False
def update_mozinfo(self):
"""walk up directories to find mozinfo.json update the info"""
@ -1495,7 +1496,8 @@ toolbar#nav-bar {
self.log.error(str(e))
return None
browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
if not self.disable_leak_checking:
browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leak_report_file
try:
gmp_path = self.getGMPPluginPath(options)
@ -1939,12 +1941,13 @@ toolbar#nav-bar {
args.append('-foreground')
self.start_script_args.append(testUrl or 'about:blank')
if detectShutdownLeaks:
if detectShutdownLeaks and not self.disable_leak_checking:
shutdownLeaks = ShutdownLeaks(self.log)
else:
shutdownLeaks = None
if mozinfo.info["asan"] and (mozinfo.isLinux or mozinfo.isMac):
if mozinfo.info["asan"] and (mozinfo.isLinux or mozinfo.isMac) \
and not self.disable_leak_checking:
lsanLeaks = LSANLeaks(self.log)
else:
lsanLeaks = None
@ -2201,6 +2204,36 @@ toolbar#nav-bar {
result = 1 # default value, if no tests are run.
for d in dirs:
print "dir: %s" % d
# BEGIN LEAKCHECK HACK
# Leak checking was broken in mochitest unnoticed for a length of time. During
# this time, several leaks slipped through. The leak checking was fixed by bug
# 1325148, but it couldn't land until all the regressions were also fixed or
# backed out. Rather than waiting and risking new regressions, in the meantime
# this code will selectively disable leak checking on flavors/directories where
# known regressions exist. At least this way we can prevent further damage while
# they get fixed.
info = mozinfo.info
skip_leak_conditions = [
(options.flavor in ('browser', 'chrome', 'plain') and d.startswith('toolkit/components/extensions/test/mochitest'), 'bug 1325158'), # noqa
(info['debug'] and options.flavor == 'browser' and d.startswith('browser/components/extensions/test/browser'), 'bug 1325141'), # noqa
(info['debug'] and options.flavor == 'plain' and d == 'dom/animation/test/css-animations', 'bug 1325277'), # noqa
(info['debug'] and options.flavor == 'plain' and d == 'dom/tests/mochitest/gamepad' and info['os'] == 'win', 'bug 1324592'), # noqa
(info['debug'] and options.flavor == 'plain' and d == 'toolkit/components/prompts/test' and info['os'] == 'mac', 'bug 1325275'), # noqa
(info['debug'] and options.flavor == 'plain' and d == 'tests/dom/xhr/tests', 'bug 1325438'), # noqa
]
for condition, reason in skip_leak_conditions:
if condition:
self.log.warning('WARNING | disabling leakcheck due to {}'.format(reason))
self.disable_leak_checking = True
break
else:
self.disable_leak_checking = False
# END LEAKCHECK HACK
tests_in_dir = [t for t in testsToRun if os.path.dirname(t) == d]
# If we are using --run-by-dir, we should not use the profile path (if) provided

View File

@ -38,8 +38,6 @@ def process_single_leak_file(leakLogFileName, processType, leakThreshold,
processString = "%s process:" % processType
crashedOnPurpose = False
totalBytesLeaked = None
logAsWarning = False
leakAnalysis = []
leakedObjectAnalysis = []
leakedObjectNames = []
recordLeakedObjects = False
@ -68,9 +66,9 @@ def process_single_leak_file(leakLogFileName, processType, leakThreshold,
# log, particularly on B2G. Eventually, these should be split into multiple
# logs (bug 1068869), but for now, we report the largest leak.
if totalBytesLeaked is not None:
leakAnalysis.append("WARNING | leakcheck | %s "
"multiple BloatView byte totals found"
% processString)
log.warning("leakcheck | %s "
"multiple BloatView byte totals found"
% processString)
else:
totalBytesLeaked = 0
if bytesLeaked > totalBytesLeaked:
@ -83,22 +81,15 @@ def process_single_leak_file(leakLogFileName, processType, leakThreshold,
else:
recordLeakedObjects = False
if size < 0 or bytesLeaked < 0 or numLeaked < 0:
leakAnalysis.append("TEST-UNEXPECTED-FAIL | leakcheck | %s negative leaks caught!"
% processString)
logAsWarning = True
log.error("TEST-UNEXPECTED-FAIL | leakcheck | %s negative leaks caught!"
% processString)
continue
if name != "TOTAL" and numLeaked != 0 and recordLeakedObjects:
leakedObjectNames.append(name)
leakedObjectAnalysis.append("TEST-INFO | leakcheck | %s leaked %d %s"
% (processString, numLeaked, name))
leakAnalysis.extend(leakedObjectAnalysis)
if logAsWarning:
log.warning('\n'.join(leakAnalysis))
else:
log.info('\n'.join(leakAnalysis))
logAsWarning = False
log.info('\n'.join(leakedObjectAnalysis))
if totalBytesLeaked is None:
# We didn't see a line with name 'TOTAL'
@ -109,8 +100,8 @@ def process_single_leak_file(leakLogFileName, processType, leakThreshold,
log.info("TEST-INFO | leakcheck | %s ignoring missing output line for total leaks"
% processString)
else:
log.info("TEST-UNEXPECTED-FAIL | leakcheck | %s missing output line for total leaks!"
% processString)
log.error("TEST-UNEXPECTED-FAIL | leakcheck | %s missing output line for total leaks!"
% processString)
log.info("TEST-INFO | leakcheck | missing output line from log file %s"
% leakLogFileName)
return
@ -120,12 +111,6 @@ def process_single_leak_file(leakLogFileName, processType, leakThreshold,
processString)
return
if totalBytesLeaked > leakThreshold:
logAsWarning = True
# Fail the run if we're over the threshold (which defaults to 0)
prefix = "TEST-UNEXPECTED-FAIL"
else:
prefix = "WARNING"
# Create a comma delimited string of the first N leaked objects found,
# to aid with bug summary matching in TBPL. Note: The order of the objects
# had no significance (they're sorted alphabetically).
@ -134,14 +119,15 @@ def process_single_leak_file(leakLogFileName, processType, leakThreshold,
if len(leakedObjectNames) > maxSummaryObjects:
leakedObjectSummary += ', ...'
message = "leakcheck | %s %d bytes leaked (%s)" % (
processString, totalBytesLeaked, leakedObjectSummary)
# totalBytesLeaked will include any expected leaks, so it can be off
# by a few thousand bytes.
if logAsWarning:
log.warning("%s | leakcheck | %s %d bytes leaked (%s)"
% (prefix, processString, totalBytesLeaked, leakedObjectSummary))
if totalBytesLeaked > leakThreshold:
log.error("TEST-UNEXPECTED-FAIL | %s" % message)
else:
log.info("%s | leakcheck | %s %d bytes leaked (%s)"
% (prefix, processString, totalBytesLeaked, leakedObjectSummary))
log.warning(message)
def process_leak_log(leak_log_file, leak_thresholds=None,
@ -175,8 +161,8 @@ def process_leak_log(leak_log_file, leak_thresholds=None,
leakLogFile = leak_log_file
if not os.path.exists(leakLogFile):
log.info(
"WARNING | leakcheck | refcount logging is off, so leaks can't be detected!")
log.warning(
"leakcheck | refcount logging is off, so leaks can't be detected!")
return
leakThresholds = leak_thresholds or {}
@ -192,8 +178,8 @@ def process_leak_log(leak_log_file, leak_thresholds=None,
for processType in leakThresholds:
if processType not in knownProcessTypes:
log.info("TEST-UNEXPECTED-FAIL | leakcheck | Unknown process type %s in leakThresholds"
% processType)
log.error("TEST-UNEXPECTED-FAIL | leakcheck | "
"Unknown process type %s in leakThresholds" % processType)
(leakLogFileDir, leakFileBase) = os.path.split(leakLogFile)
if leakFileBase[-4:] == ".log":
@ -211,8 +197,8 @@ def process_leak_log(leak_log_file, leak_thresholds=None,
else:
processType = "default"
if processType not in knownProcessTypes:
log.info("TEST-UNEXPECTED-FAIL | leakcheck | Leak log with unknown process type %s"
% processType)
log.error("TEST-UNEXPECTED-FAIL | leakcheck | "
"Leak log with unknown process type %s" % processType)
leakThreshold = leakThresholds.get(processType, 0)
process_single_leak_file(thisFile, processType, leakThreshold,
processType in ignoreMissingLeaks,

View File

@ -314,7 +314,9 @@ stage-extensions: make-stage-dir
check::
$(eval cores=$(shell $(PYTHON) -c 'import multiprocessing; print(multiprocessing.cpu_count())'))
@echo "Starting 'mach python-test' with -j$(cores)"
@$(topsrcdir)/mach --log-no-times python-test -j$(cores)
@echo "Finished 'mach python-test' successfully"
.PHONY: \