blob: ea1d873911f1a368a6d25e6ee71efceef9530be2 [file] [log] [blame]
# Copyright 2016 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from datetime import date
import json
import logging
import os
import re
import subprocess
from subprocess import CalledProcessError
import shutil
import sys
import tempfile
from gpu_tests import gpu_integration_test
from gpu_tests import cloud_storage_integration_test_base
from gpu_tests import path_util
from gpu_tests import pixel_test_pages
from gpu_tests import color_profile_manager
from telemetry.util import image_util
gpu_relative_path = "content/test/data/gpu/"
gpu_data_dir = os.path.join(path_util.GetChromiumSrcDir(), gpu_relative_path)
default_reference_image_dir = os.path.join(gpu_data_dir, 'gpu_reference')
test_data_dirs = [gpu_data_dir,
os.path.join(
path_util.GetChromiumSrcDir(), 'media/test/data')]
test_harness_script = r"""
var domAutomationController = {};
domAutomationController._proceed = false;
domAutomationController._readyForActions = false;
domAutomationController._succeeded = false;
domAutomationController._finished = false;
domAutomationController.send = function(msg) {
domAutomationController._proceed = true;
let lmsg = msg.toLowerCase();
if (lmsg == "ready") {
domAutomationController._readyForActions = true;
} else {
domAutomationController._finished = true;
if (lmsg == "success") {
domAutomationController._succeeded = true;
} else {
domAutomationController._succeeded = false;
}
}
}
window.domAutomationController = domAutomationController;
"""
goldctl_bin = os.path.join(
path_util.GetChromiumSrcDir(), 'tools', 'skia_goldctl')
if sys.platform == 'win32':
goldctl_bin = os.path.join(goldctl_bin, 'win', 'goldctl') + '.exe'
elif sys.platform == 'darwin':
goldctl_bin = os.path.join(goldctl_bin, 'mac', 'goldctl')
else:
goldctl_bin = os.path.join(goldctl_bin, 'linux', 'goldctl')
SKIA_GOLD_INSTANCE = 'chrome-gpu'
class _ImageParameters(object):
def __init__(self):
# Parameters for cloud storage reference images.
self.vendor_id = None
self.device_id = None
self.vendor_string = None
self.device_string = None
self.msaa = False
self.model_name = None
class PixelIntegrationTest(
cloud_storage_integration_test_base.CloudStorageIntegrationTestBase):
test_base_name = 'Pixel'
# This information is class-scoped, so that it can be shared across
# invocations of tests; but it's zapped every time the browser is
# restarted with different command line arguments.
_image_parameters = None
_skia_gold_temp_dir = None
@classmethod
def Name(cls):
"""The name by which this test is invoked on the command line."""
return 'pixel'
@classmethod
def SetUpProcess(cls):
options = cls.GetParsedCommandLineOptions()
color_profile_manager.ForceUntilExitSRGB(
options.dont_restore_color_profile_after_test)
super(PixelIntegrationTest, cls).SetUpProcess()
cls.CustomizeBrowserArgs(cls._AddDefaultArgs([]))
cls.StartBrowser()
cls.SetStaticServerDirs(test_data_dirs)
cls._skia_gold_temp_dir = tempfile.mkdtemp()
@staticmethod
def _AddDefaultArgs(browser_args):
if not browser_args:
browser_args = []
# All tests receive the following options.
return [
'--force-color-profile=srgb',
'--ensure-forced-color-profile',
'--enable-gpu-benchmarking',
'--test-type=gpu'] + browser_args
@classmethod
def StopBrowser(cls):
super(PixelIntegrationTest, cls).StopBrowser()
cls.ResetGpuInfo()
@classmethod
def TearDownProcess(cls):
super(PixelIntegrationTest, cls).TearDownProcess()
if not cls.GetParsedCommandLineOptions().local_run:
shutil.rmtree(cls._skia_gold_temp_dir)
@classmethod
def AddCommandlineArgs(cls, parser):
super(PixelIntegrationTest, cls).AddCommandlineArgs(parser)
parser.add_option(
'--reference-dir',
help='Overrides the default on-disk location for reference images '
'(only used for local testing without a cloud storage account)',
default=default_reference_image_dir)
parser.add_option(
'--review-patch-issue',
help='For Skia Gold integration. Gerrit issue ID.',
default='')
parser.add_option(
'--review-patch-set',
help='For Skia Gold integration. Gerrit patch set number.',
default='')
parser.add_option(
'--buildbucket-build-id',
help='For Skia Gold integration. Buildbucket build ID.',
default='')
parser.add_option(
'--no-skia-gold-failure',
action='store_true', default=False,
help='For Skia Gold integration. Always report that the test passed even '
'if the Skia Gold image comparison reported a failure, but '
'otherwise perform the same steps as usual.')
parser.add_option(
'--local-run',
action='store_true', default=False,
help='Runs the tests in a manner more suitable for local testing. '
'Specifically, runs goldctl in extra_imgtest_args mode (no upload) '
'and outputs local links to generated images. Implies '
'--no-luci-auth.')
parser.add_option(
'--no-luci-auth',
action='store_true', default=False,
help='Don\'t use the service account provided by LUCI for authentication '
'for Skia Gold, instead relying on gsutil to be pre-authenticated. '
'Meant for testing locally instead of on the bots.')
@classmethod
def GenerateGpuTests(cls, options):
cls.SetParsedCommandLineOptions(options)
namespace = pixel_test_pages.PixelTestPages
pages = namespace.DefaultPages(cls.test_base_name)
pages += namespace.GpuRasterizationPages(cls.test_base_name)
pages += namespace.ExperimentalCanvasFeaturesPages(cls.test_base_name)
# pages += namespace.NoGpuProcessPages(cls.test_base_name)
# The following pages should run only on platforms where SwiftShader is
# enabled. They are skipped on other platforms through test expectations.
# pages += namespace.SwiftShaderPages(cls.test_base_name)
if sys.platform.startswith('darwin'):
pages += namespace.MacSpecificPages(cls.test_base_name)
if sys.platform.startswith('win'):
pages += namespace.DirectCompositionPages(cls.test_base_name)
for p in pages:
yield(p.name, gpu_relative_path + p.url, (p))
@classmethod
def ResetGpuInfo(cls):
cls._image_parameters = None
@classmethod
def GetImageParameters(cls, tab, page):
if not cls._image_parameters:
cls._ComputeGpuInfo(tab, page)
return cls._image_parameters
@classmethod
def _ComputeGpuInfo(cls, tab, page):
if cls._image_parameters:
return
browser = cls.browser
system_info = browser.GetSystemInfo()
if not system_info:
raise Exception('System info must be supported by the browser')
if not system_info.gpu:
raise Exception('GPU information was absent')
device = system_info.gpu.devices[0]
cls._image_parameters = _ImageParameters()
params = cls._image_parameters
if device.vendor_id and device.device_id:
params.vendor_id = device.vendor_id
params.device_id = device.device_id
elif device.vendor_string and device.device_string:
params.vendor_string = device.vendor_string
params.device_string = device.device_string
elif page.gpu_process_disabled:
# Match the vendor and device IDs that the browser advertises
# when the software renderer is active.
params.vendor_id = 65535
params.device_id = 65535
else:
raise Exception('GPU device information was incomplete')
# TODO(senorblanco): This should probably be checking
# for the presence of the extensions in system_info.gpu_aux_attributes
# in order to check for MSAA, rather than sniffing the blacklist.
params.msaa = not (
('disable_chromium_framebuffer_multisample' in
system_info.gpu.driver_bug_workarounds) or
('disable_multisample_render_to_texture' in
system_info.gpu.driver_bug_workarounds))
params.model_name = system_info.model_name
# Not used consistently, but potentially useful for debugging issues on the
# bots, so kept around for future use.
@classmethod
def _UploadGoldErrorImageToCloudStorage(cls, image_name, screenshot):
machine_name = re.sub(r'\W+', '_',
cls.GetParsedCommandLineOptions().test_machine_name)
base_bucket = '%s/gold_failures' % (cls._error_image_cloud_storage_bucket)
image_name_with_revision_and_machine = '%s_%s_%s.png' % (
image_name, machine_name,
cls.GetParsedCommandLineOptions().build_revision)
cls._UploadBitmapToCloudStorage(
base_bucket, image_name_with_revision_and_machine, screenshot,
public=True)
def ToHex(self, num):
return hex(int(num))
def ToHexOrNone(self, num):
return 'None' if num == None else self.ToHex(num)
def ToNonEmptyStrOrNone(self, val):
return 'None' if val == '' else str(val)
def RunActualGpuTest(self, test_path, *args):
page = args[0]
# Some pixel tests require non-standard browser arguments. Need to
# check before running each page that it can run in the current
# browser instance.
self.RestartBrowserIfNecessaryWithArgs(self._AddDefaultArgs(
page.browser_args))
url = self.UrlOfStaticFilePath(test_path)
# This property actually comes off the class, not 'self'.
tab = self.tab
tab.Navigate(url, script_to_evaluate_on_commit=test_harness_script)
tab.action_runner.WaitForJavaScriptCondition(
'domAutomationController._proceed', timeout=300)
do_page_action = tab.EvaluateJavaScript(
'domAutomationController._readyForActions')
if do_page_action:
self._DoPageAction(tab, page)
self._RunSkiaGoldBasedPixelTest(do_page_action, page)
def _RunSkiaGoldBasedPixelTest(self, do_page_action, page):
"""Captures and compares a test image using Skia Gold.
Raises an Exception if the comparison fails.
Args:
do_page_action: a bool indicating if an action was run on the page.
page: the GPU PixelTestPage object for the test.
"""
tab = self.tab
try:
# Actually run the test and capture the screenshot.
if not tab.EvaluateJavaScript('domAutomationController._succeeded'):
self.fail('page indicated test failure')
if not tab.screenshot_supported:
self.fail('Browser does not support screenshot capture')
screenshot = tab.Screenshot(5)
if screenshot is None:
self.fail('Could not capture screenshot')
dpr = tab.EvaluateJavaScript('window.devicePixelRatio')
if page.test_rect:
screenshot = image_util.Crop(
screenshot, int(page.test_rect[0] * dpr),
int(page.test_rect[1] * dpr), int(page.test_rect[2] * dpr),
int(page.test_rect[3] * dpr))
build_id_args = self._GetBuildIdArgs()
# Compare images against approved images/colors.
if page.expected_colors:
# Use expected colors instead of hash comparison for validation.
self._ValidateScreenshotSamplesWithSkiaGold(
tab, page, screenshot, dpr, build_id_args)
return
image_name = self._UrlToImageName(page.name)
self._UploadTestResultToSkiaGold(
image_name, screenshot,
tab, page,
build_id_args=build_id_args)
finally:
if do_page_action:
# Assume that page actions might have killed the GPU process.
self._RestartBrowser('Must restart after page actions')
def _GetBuildIdArgs(self):
# Get all the information that goldctl requires.
parsed_options = self.GetParsedCommandLineOptions()
build_id_args = [
'--commit',
parsed_options.build_revision,
]
# If --review-patch-issue is passed, then we assume we're running on a
# trybot.
if parsed_options.review_patch_issue:
build_id_args += [
'--issue',
parsed_options.review_patch_issue,
'--patchset',
parsed_options.review_patch_set,
'--jobid',
parsed_options.buildbucket_build_id
]
return build_id_args
def _UploadTestResultToSkiaGold(self, image_name, screenshot,
tab, page, build_id_args=None):
"""Compares the given image using Skia Gold and uploads the result.
No uploading is done if the test is being run in local run mode. Compares
the given screenshot to baselines provided by Gold, raising an Exception if
a match is not found.
Args:
image_name: the name of the image being checked.
screenshot: the image being checked as a Telemetry Bitmap.
tab: the Telemetry Tab object that the test was run in.
page: the GPU PixelTestPage object for the test.
build_id_args: a list of build-identifying flags and values.
"""
if not isinstance(build_id_args, list) or '--commit' not in build_id_args:
raise Exception('Requires build args to be specified, including --commit')
# Write screenshot to PNG file on local disk.
png_temp_file = tempfile.NamedTemporaryFile(
suffix='.png', dir=self._skia_gold_temp_dir).name
image_util.WritePngFile(screenshot, png_temp_file)
# Get all information that goldctl will need.
img_params = self.GetImageParameters(tab, page)
# All values need to be strings, otherwise goldctl fails.
gpu_keys = {
'vendor_id': self.ToHexOrNone(img_params.vendor_id),
'device_id': self.ToHexOrNone(img_params.device_id),
'vendor_string': self.ToNonEmptyStrOrNone(img_params.vendor_string),
'device_string': self.ToNonEmptyStrOrNone(img_params.device_string),
'msaa': str(img_params.msaa),
'model_name': self.ToNonEmptyStrOrNone(img_params.model_name),
}
json_temp_file = tempfile.NamedTemporaryFile(
suffix='.json', dir=self._skia_gold_temp_dir).name
failure_file = tempfile.NamedTemporaryFile(
suffix='.txt', dir=self._skia_gold_temp_dir).name
with open(json_temp_file, 'w+') as f:
json.dump(gpu_keys, f)
# Figure out any extra args we need to pass to goldctl.
extra_imgtest_args = []
extra_auth_args = []
parsed_options = self.GetParsedCommandLineOptions()
if parsed_options.local_run:
extra_imgtest_args.append('--dryrun')
elif not parsed_options.no_luci_auth:
extra_auth_args = ['--luci']
# Run goldctl for a result.
try:
subprocess.check_output([goldctl_bin, 'auth',
'--work-dir', self._skia_gold_temp_dir]
+ extra_auth_args,
stderr=subprocess.STDOUT)
cmd = ([goldctl_bin, 'imgtest', 'add', '--passfail',
'--test-name', image_name,
'--instance', SKIA_GOLD_INSTANCE,
'--keys-file', json_temp_file,
'--png-file', png_temp_file,
'--work-dir', self._skia_gold_temp_dir,
'--failure-file', failure_file] +
build_id_args + extra_imgtest_args)
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except CalledProcessError as e:
# The triage link for the image is output to the failure file, so report
# that if it's available so it shows up in Milo. If for whatever reason
# the file is not present or malformed, the triage link will still be
# present in the stdout of the goldctl command.
# If we're running on a trybot, instead generate a link to all results
# for the CL so that the user can visit a single page instead of
# clicking on multiple links on potentially multiple bots.
if parsed_options.review_patch_issue:
cl_images = ('https://%s-gold.skia.org/search?'
'issue=%s&new_clstore=true' % (
SKIA_GOLD_INSTANCE, parsed_options.review_patch_issue))
self.artifacts.CreateLink('triage_link_for_entire_cl', cl_images)
else:
try:
with open(failure_file, 'r') as ff:
self.artifacts.CreateLink('gold_triage_link', ff.read())
except Exception:
logging.error('Failed to read contents of goldctl failure file')
logging.error('goldctl failed with output: %s', e.output)
if parsed_options.local_run:
logging.error(
'Image produced by %s: file://%s', image_name, png_temp_file)
gold_images = ('https://%s-gold.skia.org/search?'
'match=name&metric=combined&pos=true&'
'query=name%%3D%s&unt=false' % (
SKIA_GOLD_INSTANCE, image_name))
logging.error(
'Approved images for %s in Gold: %s', image_name, gold_images)
if self._ShouldReportGoldFailure(page):
raise Exception('goldctl command failed, see above for details')
def _ShouldReportGoldFailure(self, page):
"""Determines if a Gold failure should actually be surfaced.
Args:
page: The GPU PixelTestPage object for the test.
Returns:
True if the failure should be surfaced, i.e. the test should fail,
otherwise False.
"""
parsed_options = self.GetParsedCommandLineOptions()
# Don't surface if we're explicitly told not to.
if parsed_options.no_skia_gold_failure:
return False
# Don't surface if the test was recently added and we're still within its
# grace period. However, fail if we're on a trybot so that as many images
# can be triaged as possible before a new test is committed.
if (page.grace_period_end and date.today() <= page.grace_period_end and
not parsed_options.review_patch_issue):
return False
return True
def _ValidateScreenshotSamplesWithSkiaGold(self, tab, page, screenshot,
device_pixel_ratio,
build_id_args):
"""Samples the given screenshot and verifies pixel color values.
In case any of the samples do not match the expected color, it raises
a Failure and uploads the image to Gold.
Args:
tab: the Telemetry Tab object that the test was run in.
page: the GPU PixelTestPage object for the test.
screenshot: the screenshot of the test page as a Telemetry Bitmap.
device_pixel_ratio: the device pixel ratio for the test device as a float.
build_id_args: a list of build-identifying flags and values.
"""
try:
self._CompareScreenshotSamples(
tab, screenshot, page.expected_colors, page.tolerance,
device_pixel_ratio,
self.GetParsedCommandLineOptions().test_machine_name)
except Exception:
# An exception raised from self.fail() indicates a failure.
image_name = self._UrlToImageName(page.name)
# We want to report the screenshot comparison failure, not any failures
# related to Gold.
try:
self._UploadTestResultToSkiaGold(
image_name, screenshot,
tab, page,
build_id_args=build_id_args)
except Exception as e:
logging.error(str(e))
raise
def _DoPageAction(self, tab, page):
getattr(self, '_' + page.optional_action)(tab, page)
# Now that we've done the page's specific action, wait for it to
# report completion.
tab.action_runner.WaitForJavaScriptCondition(
'domAutomationController._finished', timeout=300)
#
# Optional actions pages can take.
# These are specified as methods taking the tab and the page as
# arguments.
#
def _CrashGpuProcess(self, tab, page):
# Crash the GPU process.
#
# This used to create a new tab and navigate it to
# chrome://gpucrash, but there was enough unreliability
# navigating between these tabs (one of which was created solely
# in order to navigate to chrome://gpucrash) that the simpler
# solution of provoking the GPU process crash from this renderer
# process was chosen.
tab.EvaluateJavaScript('chrome.gpuBenchmarking.crashGpuProcess()')
def _SwitchTabs(self, tab, page):
if not tab.browser.supports_tab_control:
self.fail('Browser must support tab control')
dummy_tab = tab.browser.tabs.New()
dummy_tab.Activate()
# Wait for 2 seconds so that new tab becomes visible.
dummy_tab.action_runner.Wait(2)
tab.Activate()
@classmethod
def ExpectationsFiles(cls):
return [
os.path.join(os.path.dirname(os.path.abspath(__file__)),
'test_expectations',
'pixel_expectations.txt')]
def load_tests(loader, tests, pattern):
del loader, tests, pattern # Unused.
return gpu_integration_test.LoadAllTestsInModule(sys.modules[__name__])