blob: 5e87c0d17748e9e39b400dbe0beb047a5c544e1a [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.
import glob
import os
import re
from subprocess import CalledProcessError
import sys
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 py_utils import cloud_storage
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;
"""
class PixelIntegrationTest(
cloud_storage_integration_test_base.CloudStorageIntegrationTestBase):
test_base_name = 'Pixel'
@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)
@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 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(
'--use-skia-gold',
dest='use_skia_gold',
action='store_true', default=False,
help='Use the Skia team\'s Gold tool to handle image comparisons')
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))
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)
if self.GetParsedCommandLineOptions().use_skia_gold:
self.RunSkiaGoldBasedPixelTest(test_path, do_page_action, args)
else:
self.RunLegacyPixelTest(test_path, do_page_action, args)
def RunLegacyPixelTest(self, test_path, do_page_action, args):
page = args[0]
tab = self.tab
try:
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))
if page.expected_colors:
# Use expected colors instead of ref images for validation.
self._ValidateScreenshotSamples(
tab, page.name, screenshot, page.expected_colors, page.tolerance,
dpr)
return
image_name = self._UrlToImageName(page.name)
if self.GetParsedCommandLineOptions().upload_refimg_to_cloud_storage:
if self._ConditionallyUploadToCloudStorage(image_name, page, tab,
screenshot):
# This is the new reference image; there's nothing to compare against.
ref_png = screenshot
else:
# There was a preexisting reference image, so we might as well
# compare against it.
ref_png = self._DownloadFromCloudStorage(image_name, page, tab)
elif self.GetParsedCommandLineOptions().\
download_refimg_from_cloud_storage:
# This bot doesn't have the ability to properly generate a
# reference image, so download it from cloud storage.
try:
ref_png = self._DownloadFromCloudStorage(image_name, page, tab)
except cloud_storage.NotFoundError:
# There is no reference image yet in cloud storage. This
# happens when the revision of the test is incremented or when
# a new test is added, because the trybots are not allowed to
# produce reference images, only the bots on the main
# waterfalls. Report this as a failure so the developer has to
# take action by explicitly suppressing the failure and
# removing the suppression once the reference images have been
# generated. Otherwise silent failures could happen for long
# periods of time.
self.fail('Could not find image %s in cloud storage' % image_name)
else:
# Legacy path using on-disk results.
ref_png = self._GetReferenceImage(
self.GetParsedCommandLineOptions().reference_dir,
image_name, page.revision, screenshot)
# Test new snapshot against existing reference image
if not image_util.AreEqual(ref_png, screenshot, tolerance=page.tolerance):
if self.GetParsedCommandLineOptions().test_machine_name:
self._UploadErrorImagesToCloudStorage(image_name, screenshot, ref_png)
else:
self._WriteErrorImages(
self.GetParsedCommandLineOptions().generated_dir, image_name,
screenshot, ref_png)
self.fail('Reference image did not match captured screen')
finally:
if do_page_action:
# Assume that page actions might have killed the GPU process.
self._RestartBrowser('Must restart after page actions')
def RunSkiaGoldBasedPixelTest(self, test_path, do_page_action, args):
page = args[0]
tab = self.tab
try:
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))
# This is required by Gold whether this is a tryjob or not.
build_id_args = [
'--commit',
self.GetParsedCommandLineOptions().build_revision,
]
parsed_options = self.GetParsedCommandLineOptions()
# Trybots conceptually download their reference images from
# cloud storage; they don't produce them. (TODO(kbr): remove
# this once fully switched over to Gold.)
if parsed_options.download_refimg_from_cloud_storage:
# Tryjobs in Gold need:
# CL issue number (in Gerrit)
# Patchset number
# Buildbucket ID
build_id_args += [
'--issue',
self.GetParsedCommandLineOptions().review_patch_issue,
'--patchset',
self.GetParsedCommandLineOptions().review_patch_set,
'--jobid',
self.GetParsedCommandLineOptions().buildbucket_build_id
]
if page.expected_colors:
# Use expected colors instead of ref images for validation.
self._ValidateScreenshotSamplesWithSkiaGold(
tab, page, screenshot, page.expected_colors, page.tolerance,
dpr, build_id_args)
return
image_name = self._UrlToImageName(page.name)
is_local_run = not (parsed_options.upload_refimg_to_cloud_storage or
parsed_options.download_refimg_from_cloud_storage)
if is_local_run:
# Legacy path using on-disk results.
ref_png = self._GetReferenceImage(
self.GetParsedCommandLineOptions().reference_dir,
image_name, page.revision, screenshot)
# Test new snapshot against existing reference image
if not image_util.AreEqual(ref_png, screenshot,
tolerance=page.tolerance):
if parsed_options.test_machine_name:
self._UploadErrorImagesToCloudStorage(image_name, screenshot,
ref_png)
else:
self._WriteErrorImages(
parsed_options.generated_dir, image_name, screenshot, ref_png)
self.fail('Reference image did not match captured screen')
else:
try:
self._UploadTestResultToSkiaGold(
image_name, screenshot,
tab, page,
build_id_args=build_id_args)
except CalledProcessError:
self.fail('Gold said the test failed, so fail.')
finally:
if do_page_action:
# Assume that page actions might have killed the GPU process.
self._RestartBrowser('Must restart after page actions')
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)
def _DeleteOldReferenceImages(self, ref_image_path, cur_revision):
if not cur_revision:
return
old_revisions = glob.glob(ref_image_path + "_*.png")
for rev_path in old_revisions:
m = re.match(r'^.*_(\d+)\.png$', rev_path)
if m and int(m.group(1)) < cur_revision:
print 'Found deprecated reference image. Deleting rev ' + m.group(1)
os.remove(rev_path)
def _GetReferenceImage(self, img_dir, img_name, cur_revision, screenshot):
if not cur_revision:
cur_revision = 0
image_path = os.path.join(img_dir, img_name)
self._DeleteOldReferenceImages(image_path, cur_revision)
image_path = image_path + '_v' + str(cur_revision) + '.png'
try:
ref_png = image_util.FromPngFile(image_path)
# This can raise a couple of exceptions including IOError and ValueError.
except Exception:
ref_png = None
if ref_png is not None:
return ref_png
print ('Reference image not found. Writing tab contents as reference to: ' +
image_path)
self._WriteImage(image_path, screenshot)
return screenshot
#
# 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()')
@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__])