OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 """Base classes for a test which uploads results (reference images, |
| 6 error images) to cloud storage.""" |
| 7 |
| 8 import logging |
| 9 import os |
| 10 import re |
| 11 import tempfile |
| 12 |
| 13 from py_utils import cloud_storage |
| 14 from telemetry.util import image_util |
| 15 from telemetry.util import rgba_color |
| 16 |
| 17 from gpu_tests import gpu_integration_test |
| 18 |
| 19 test_data_dir = os.path.abspath(os.path.join( |
| 20 os.path.dirname(__file__), '..', '..', 'data', 'gpu')) |
| 21 |
| 22 default_generated_data_dir = os.path.join(test_data_dir, 'generated') |
| 23 |
| 24 error_image_cloud_storage_bucket = 'chromium-browser-gpu-tests' |
| 25 |
| 26 |
| 27 class _ReferenceImageParameters(object): |
| 28 def __init__(self): |
| 29 # Parameters for cloud storage reference images. |
| 30 self.vendor_id = None |
| 31 self.device_id = None |
| 32 self.vendor_string = None |
| 33 self.device_string = None |
| 34 self.msaa = False |
| 35 self.model_name = None |
| 36 |
| 37 |
| 38 class CloudStorageIntegrationTestBase(gpu_integration_test.GpuIntegrationTest): |
| 39 # This class is abstract; don't warn about the superclass's abstract |
| 40 # methods that aren't overridden. |
| 41 # pylint: disable=abstract-method |
| 42 |
| 43 # This information is class-scoped, so that it can be shared across |
| 44 # invocations of tests; but it's zapped every time the browser is |
| 45 # restarted with different command line arguments. |
| 46 _reference_image_parameters = None |
| 47 |
| 48 # The command line options (which are passed to subclasses' |
| 49 # GenerateGpuTests) *must* be configured here, via a call to |
| 50 # SetParsedCommandLineOptions. If they are not, an error will be |
| 51 # raised when running the tests. |
| 52 _parsed_command_line_options = None |
| 53 |
| 54 @classmethod |
| 55 def SetParsedCommandLineOptions(cls, options): |
| 56 cls._parsed_command_line_options = options |
| 57 |
| 58 @classmethod |
| 59 def GetParsedCommandLineOptions(cls): |
| 60 return cls._parsed_command_line_options |
| 61 |
| 62 @classmethod |
| 63 def AddCommandlineArgs(cls, parser): |
| 64 parser.add_option( |
| 65 '--build-revision', |
| 66 help='Chrome revision being tested.', |
| 67 default="unknownrev") |
| 68 parser.add_option( |
| 69 '--upload-refimg-to-cloud-storage', |
| 70 dest='upload_refimg_to_cloud_storage', |
| 71 action='store_true', default=False, |
| 72 help='Upload resulting images to cloud storage as reference images') |
| 73 parser.add_option( |
| 74 '--download-refimg-from-cloud-storage', |
| 75 dest='download_refimg_from_cloud_storage', |
| 76 action='store_true', default=False, |
| 77 help='Download reference images from cloud storage') |
| 78 parser.add_option( |
| 79 '--refimg-cloud-storage-bucket', |
| 80 help='Name of the cloud storage bucket to use for reference images; ' |
| 81 'required with --upload-refimg-to-cloud-storage and ' |
| 82 '--download-refimg-from-cloud-storage. Example: ' |
| 83 '"chromium-gpu-archive/reference-images"') |
| 84 parser.add_option( |
| 85 '--os-type', |
| 86 help='Type of operating system on which the pixel test is being run, ' |
| 87 'used only to distinguish different operating systems with the same ' |
| 88 'graphics card. Any value is acceptable, but canonical values are ' |
| 89 '"win", "mac", and "linux", and probably, eventually, "chromeos" ' |
| 90 'and "android").', |
| 91 default='') |
| 92 parser.add_option( |
| 93 '--test-machine-name', |
| 94 help='Name of the test machine. Specifying this argument causes this ' |
| 95 'script to upload failure images and diffs to cloud storage directly, ' |
| 96 'instead of relying on the archive_gpu_pixel_test_results.py script.', |
| 97 default='') |
| 98 parser.add_option( |
| 99 '--generated-dir', |
| 100 help='Overrides the default on-disk location for generated test images ' |
| 101 '(only used for local testing without a cloud storage account)', |
| 102 default=default_generated_data_dir) |
| 103 |
| 104 def _CompareScreenshotSamples(self, tab, screenshot, expected_colors, |
| 105 device_pixel_ratio, test_machine_name): |
| 106 # First scan through the expected_colors and see if there are any scale |
| 107 # factor overrides that would preempt the device pixel ratio. This |
| 108 # is mainly a workaround for complex tests like the Maps test. |
| 109 for expectation in expected_colors: |
| 110 if 'scale_factor_overrides' in expectation: |
| 111 for override in expectation['scale_factor_overrides']: |
| 112 # Require exact matches to avoid confusion, because some |
| 113 # machine models and names might be subsets of others |
| 114 # (e.g. Nexus 5 vs Nexus 5X). |
| 115 if ('device_type' in override and |
| 116 (tab.browser.platform.GetDeviceTypeName() == |
| 117 override['device_type'])): |
| 118 logging.warning( |
| 119 'Overriding device_pixel_ratio ' + str(device_pixel_ratio) + |
| 120 ' with scale factor ' + str(override['scale_factor']) + |
| 121 ' for device type ' + override['device_type']) |
| 122 device_pixel_ratio = override['scale_factor'] |
| 123 break |
| 124 if (test_machine_name and 'machine_name' in override and |
| 125 override["machine_name"] == test_machine_name): |
| 126 logging.warning( |
| 127 'Overriding device_pixel_ratio ' + str(device_pixel_ratio) + |
| 128 ' with scale factor ' + str(override['scale_factor']) + |
| 129 ' for machine name ' + test_machine_name) |
| 130 device_pixel_ratio = override['scale_factor'] |
| 131 break |
| 132 # Only support one "scale_factor_overrides" in the expectation format. |
| 133 break |
| 134 for expectation in expected_colors: |
| 135 if "scale_factor_overrides" in expectation: |
| 136 continue |
| 137 location = expectation["location"] |
| 138 size = expectation["size"] |
| 139 x0 = int(location[0] * device_pixel_ratio) |
| 140 x1 = int((location[0] + size[0]) * device_pixel_ratio) |
| 141 y0 = int(location[1] * device_pixel_ratio) |
| 142 y1 = int((location[1] + size[1]) * device_pixel_ratio) |
| 143 for x in range(x0, x1): |
| 144 for y in range(y0, y1): |
| 145 if (x < 0 or y < 0 or x >= image_util.Width(screenshot) or |
| 146 y >= image_util.Height(screenshot)): |
| 147 self.fail( |
| 148 ('Expected pixel location [%d, %d] is out of range on ' + |
| 149 '[%d, %d] image') % |
| 150 (x, y, image_util.Width(screenshot), |
| 151 image_util.Height(screenshot))) |
| 152 |
| 153 actual_color = image_util.GetPixelColor(screenshot, x, y) |
| 154 expected_color = rgba_color.RgbaColor( |
| 155 expectation["color"][0], |
| 156 expectation["color"][1], |
| 157 expectation["color"][2]) |
| 158 if not actual_color.IsEqual(expected_color, expectation["tolerance"]): |
| 159 self.fail('Expected pixel at ' + str(location) + |
| 160 ' (actual pixel (' + str(x) + ', ' + str(y) + ')) ' + |
| 161 ' to be ' + |
| 162 str(expectation["color"]) + " but got [" + |
| 163 str(actual_color.r) + ", " + |
| 164 str(actual_color.g) + ", " + |
| 165 str(actual_color.b) + "]") |
| 166 |
| 167 ### |
| 168 ### Routines working with the local disk (only used for local |
| 169 ### testing without a cloud storage account -- the bots do not use |
| 170 ### this code path). |
| 171 ### |
| 172 |
| 173 def _UrlToImageName(self, url): |
| 174 image_name = re.sub(r'^(http|https|file)://(/*)', '', url) |
| 175 image_name = re.sub(r'\.\./', '', image_name) |
| 176 image_name = re.sub(r'(\.|/|-)', '_', image_name) |
| 177 return image_name |
| 178 |
| 179 def _WriteImage(self, image_path, png_image): |
| 180 output_dir = os.path.dirname(image_path) |
| 181 if not os.path.exists(output_dir): |
| 182 os.makedirs(output_dir) |
| 183 image_util.WritePngFile(png_image, image_path) |
| 184 |
| 185 def _WriteErrorImages(self, img_dir, img_name, screenshot, ref_png): |
| 186 full_image_name = img_name + '_' + str( |
| 187 self.GetParsedCommandLineOptions().build_revision) |
| 188 full_image_name = full_image_name + '.png' |
| 189 |
| 190 # Always write the failing image. |
| 191 self._WriteImage( |
| 192 os.path.join(img_dir, 'FAIL_' + full_image_name), screenshot) |
| 193 |
| 194 if ref_png is not None: |
| 195 # Save the reference image. |
| 196 # This ensures that we get the right revision number. |
| 197 self._WriteImage( |
| 198 os.path.join(img_dir, full_image_name), ref_png) |
| 199 |
| 200 # Save the difference image. |
| 201 diff_png = image_util.Diff(screenshot, ref_png) |
| 202 self._WriteImage( |
| 203 os.path.join(img_dir, 'DIFF_' + full_image_name), diff_png) |
| 204 |
| 205 ### |
| 206 ### Cloud storage code path -- the bots use this. |
| 207 ### |
| 208 |
| 209 @classmethod |
| 210 def ResetGpuInfo(cls): |
| 211 cls._reference_image_parameters = None |
| 212 |
| 213 @classmethod |
| 214 def _ComputeGpuInfo(cls, tab): |
| 215 if cls._reference_image_parameters: |
| 216 return |
| 217 browser = cls.browser |
| 218 if not browser.supports_system_info: |
| 219 raise Exception('System info must be supported by the browser') |
| 220 system_info = browser.GetSystemInfo() |
| 221 if not system_info.gpu: |
| 222 raise Exception('GPU information was absent') |
| 223 device = system_info.gpu.devices[0] |
| 224 cls._reference_image_parameters = _ReferenceImageParameters() |
| 225 params = cls._reference_image_parameters |
| 226 if device.vendor_id and device.device_id: |
| 227 params.vendor_id = device.vendor_id |
| 228 params.device_id = device.device_id |
| 229 elif device.vendor_string and device.device_string: |
| 230 params.vendor_string = device.vendor_string |
| 231 params.device_string = device.device_string |
| 232 else: |
| 233 raise Exception('GPU device information was incomplete') |
| 234 # TODO(senorblanco): This should probably be checking |
| 235 # for the presence of the extensions in system_info.gpu_aux_attributes |
| 236 # in order to check for MSAA, rather than sniffing the blacklist. |
| 237 params.msaa = not ( |
| 238 ('disable_chromium_framebuffer_multisample' in |
| 239 system_info.gpu.driver_bug_workarounds) or |
| 240 ('disable_multisample_render_to_texture' in |
| 241 system_info.gpu.driver_bug_workarounds)) |
| 242 params.model_name = system_info.model_name |
| 243 |
| 244 @classmethod |
| 245 def _FormatGpuInfo(cls, tab): |
| 246 cls._ComputeGpuInfo(tab) |
| 247 params = cls._reference_image_parameters |
| 248 msaa_string = '_msaa' if params.msaa else '_non_msaa' |
| 249 if params.vendor_id: |
| 250 return '%s_%04x_%04x%s' % ( |
| 251 cls.GetParsedCommandLineOptions().os_type, params.vendor_id, |
| 252 params.device_id, msaa_string) |
| 253 else: |
| 254 # This is the code path for Android devices. Include the model |
| 255 # name (e.g. "Nexus 9") in the GPU string to disambiguate |
| 256 # multiple devices on the waterfall which might have the same |
| 257 # device string ("NVIDIA Tegra") but different screen |
| 258 # resolutions and device pixel ratios. |
| 259 return '%s_%s_%s_%s%s' % ( |
| 260 cls.GetParsedCommandLineOptions().os_type, |
| 261 params.vendor_string, params.device_string, |
| 262 params.model_name, msaa_string) |
| 263 |
| 264 @classmethod |
| 265 def _FormatReferenceImageName(cls, img_name, page, tab): |
| 266 return '%s_v%s_%s.png' % ( |
| 267 img_name, |
| 268 page.revision, |
| 269 cls._FormatGpuInfo(tab)) |
| 270 |
| 271 @classmethod |
| 272 def _UploadBitmapToCloudStorage(cls, bucket, name, bitmap, public=False): |
| 273 # This sequence of steps works on all platforms to write a temporary |
| 274 # PNG to disk, following the pattern in bitmap_unittest.py. The key to |
| 275 # avoiding PermissionErrors seems to be to not actually try to write to |
| 276 # the temporary file object, but to re-open its name for all operations. |
| 277 temp_file = tempfile.NamedTemporaryFile(suffix='.png').name |
| 278 image_util.WritePngFile(bitmap, temp_file) |
| 279 cloud_storage.Insert(bucket, name, temp_file, publicly_readable=public) |
| 280 |
| 281 @classmethod |
| 282 def _ConditionallyUploadToCloudStorage(cls, img_name, page, tab, screenshot): |
| 283 """Uploads the screenshot to cloud storage as the reference image |
| 284 for this test, unless it already exists. Returns True if the |
| 285 upload was actually performed.""" |
| 286 if not cls.GetParsedCommandLineOptions().refimg_cloud_storage_bucket: |
| 287 raise Exception('--refimg-cloud-storage-bucket argument is required') |
| 288 cloud_name = cls._FormatReferenceImageName(img_name, page, tab) |
| 289 if not cloud_storage.Exists( |
| 290 cls.GetParsedCommandLineOptions().refimg_cloud_storage_bucket, |
| 291 cloud_name): |
| 292 cls._UploadBitmapToCloudStorage( |
| 293 cls.GetParsedCommandLineOptions().refimg_cloud_storage_bucket, |
| 294 cloud_name, |
| 295 screenshot) |
| 296 return True |
| 297 return False |
| 298 |
| 299 @classmethod |
| 300 def _DownloadFromCloudStorage(cls, img_name, page, tab): |
| 301 """Downloads the reference image for the given test from cloud |
| 302 storage, returning it as a Telemetry Bitmap object.""" |
| 303 # TODO(kbr): there's a race condition between the deletion of the |
| 304 # temporary file and gsutil's overwriting it. |
| 305 if not cls.GetParsedCommandLineOptions().refimg_cloud_storage_bucket: |
| 306 raise Exception('--refimg-cloud-storage-bucket argument is required') |
| 307 temp_file = tempfile.NamedTemporaryFile(suffix='.png').name |
| 308 cloud_storage.Get( |
| 309 cls.GetParsedCommandLineOptions().refimg_cloud_storage_bucket, |
| 310 cls._FormatReferenceImageName(img_name, page, tab), |
| 311 temp_file) |
| 312 return image_util.FromPngFile(temp_file) |
| 313 |
| 314 @classmethod |
| 315 def _UploadErrorImagesToCloudStorage(cls, image_name, screenshot, ref_img): |
| 316 """For a failing run, uploads the failing image, reference image (if |
| 317 supplied), and diff image (if reference image was supplied) to cloud |
| 318 storage. This subsumes the functionality of the |
| 319 archive_gpu_pixel_test_results.py script.""" |
| 320 machine_name = re.sub(r'\W+', '_', |
| 321 cls.GetParsedCommandLineOptions().test_machine_name) |
| 322 upload_dir = '%s_%s_telemetry' % ( |
| 323 cls.GetParsedCommandLineOptions().build_revision, machine_name) |
| 324 base_bucket = '%s/runs/%s' % (error_image_cloud_storage_bucket, upload_dir) |
| 325 image_name_with_revision = '%s_%s.png' % ( |
| 326 image_name, cls.GetParsedCommandLineOptions().build_revision) |
| 327 cls._UploadBitmapToCloudStorage( |
| 328 base_bucket + '/gen', image_name_with_revision, screenshot, |
| 329 public=True) |
| 330 if ref_img is not None: |
| 331 cls._UploadBitmapToCloudStorage( |
| 332 base_bucket + '/ref', image_name_with_revision, ref_img, public=True) |
| 333 diff_img = image_util.Diff(screenshot, ref_img) |
| 334 cls._UploadBitmapToCloudStorage( |
| 335 base_bucket + '/diff', image_name_with_revision, diff_img, |
| 336 public=True) |
| 337 print ('See http://%s.commondatastorage.googleapis.com/' |
| 338 'view_test_results.html?%s for this run\'s test results') % ( |
| 339 error_image_cloud_storage_bucket, upload_dir) |
| 340 |
| 341 def _ValidateScreenshotSamples(self, tab, url, |
| 342 screenshot, expectations, device_pixel_ratio): |
| 343 """Samples the given screenshot and verifies pixel color values. |
| 344 The sample locations and expected color values are given in expectations. |
| 345 In case any of the samples do not match the expected color, it raises |
| 346 a Failure and dumps the screenshot locally or cloud storage depending on |
| 347 what machine the test is being run.""" |
| 348 try: |
| 349 self._CompareScreenshotSamples( |
| 350 tab, screenshot, expectations, |
| 351 device_pixel_ratio, |
| 352 self.GetParsedCommandLineOptions().test_machine_name) |
| 353 except Exception: |
| 354 # An exception raised from self.fail() indicates a failure. |
| 355 image_name = self._UrlToImageName(url) |
| 356 if self.GetParsedCommandLineOptions().test_machine_name: |
| 357 self._UploadErrorImagesToCloudStorage(image_name, screenshot, None) |
| 358 else: |
| 359 self._WriteErrorImages( |
| 360 self.GetParsedCommandLineOptions().generated_dir, image_name, |
| 361 screenshot, None) |
| 362 raise |
OLD | NEW |