1# Copyright (c) 2014 The Chromium OS 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 5import abc 6import datetime 7import os 8import urllib2 9 10from autotest_lib.client.bin import test, utils 11from autotest_lib.client.common_lib import error, file_utils, lsbrelease_utils 12from autotest_lib.client.cros import constants 13from autotest_lib.client.cros.image_comparison import image_comparison_factory 14from PIL import Image 15from PIL import ImageDraw 16 17class ui_TestBase(test.test): 18 """ Encapsulates steps needed to collect screenshots for ui pieces. 19 20 Each child class must implement: 21 1. Abstract method capture_screenshot() 22 Each child class will define its own custom way of capturing the screenshot 23 of the piece it cares about. 24 25 E.g Child class ui_SystemTray will capture system tray screenshot, 26 ui_SettingsPage for the Chrome Settings page, etc. 27 28 2. Abstract property test_area: 29 This will get appended to screenshot file names so we know what image it is. 30 31 Flow at runtime: 32 At run time, autotest will call run_once() method on a particular child 33 class object, call it Y. 34 35 Say X is a parent of Y. 36 37 Y.run_once() will save any values passed from control file so as to use them 38 later. 39 40 Y.run_once() will then call the parent's X.run_screenshot_comparison_test() 41 42 This is the template algorithm for collecting screenshots. 43 44 Y.run_screenshot_comparison_test will execute its steps. It will then call 45 X.test_area to get custom string to use for project name and filename. 46 47 It will execute more steps and then call capture_screenshot(). X doesn't 48 implement that, but Y does, so the method will get called on Y to produce 49 Y's custom behavior. 50 51 Control will be returned to Y run_screenshot_comparison_test() which will 52 execute remainder steps. 53 54 """ 55 56 __metaclass__ = abc.ABCMeta 57 58 WORKING_DIR = '/tmp/test' 59 REMOTE_DIR = 'http://storage.googleapis.com/chromiumos-test-assets-public' 60 AUTOTEST_CROS_UI_DIR = '/usr/local/autotest/cros/ui' 61 IMG_COMP_CONF_FILE = 'image_comparison.conf' 62 63 version = 2 64 65 66 def run_screenshot_comparison_test(self): 67 """ 68 Template method to run screenshot comparison tests for ui pieces. 69 70 1. Set up test dirs. 71 2. Create folder name 72 3. Download golden image. 73 4. Capture test image. 74 5. Compare images locally, if FAIL upload to remote for analysis later. 75 6. Clean up test dirs. 76 77 """ 78 79 img_comp_conf_path = os.path.join(ui_TestBase.AUTOTEST_CROS_UI_DIR, 80 ui_TestBase.IMG_COMP_CONF_FILE) 81 82 img_comp_factory = image_comparison_factory.ImageComparisonFactory( 83 img_comp_conf_path) 84 85 golden_image_local_dir = os.path.join(ui_TestBase.WORKING_DIR, 86 'golden_images') 87 88 file_utils.make_leaf_dir(golden_image_local_dir) 89 90 filename = '%s.png' % self.tagged_testname 91 92 golden_image_remote_path = os.path.join( 93 ui_TestBase.REMOTE_DIR, 94 'ui', 95 lsbrelease_utils.get_chrome_milestone(), 96 self.folder_name, 97 filename) 98 99 golden_image_local_path = os.path.join(golden_image_local_dir, filename) 100 101 test_image_filepath = os.path.join(ui_TestBase.WORKING_DIR, filename) 102 103 try: 104 file_utils.download_file(golden_image_remote_path, 105 golden_image_local_path) 106 except urllib2.HTTPError as e: 107 warn = "No screenshot found for {0} on milestone {1}. ".format( 108 self.tagged_testname, lsbrelease_utils.get_chrome_milestone()) 109 warn += e.msg 110 raise error.TestWarn(warn) 111 112 self.capture_screenshot(test_image_filepath) 113 114 115 116 comparer = img_comp_factory.make_pdiff_comparer() 117 comp_res = comparer.compare(golden_image_local_path, 118 test_image_filepath) 119 120 if comp_res.diff_pixel_count > img_comp_factory.pixel_thres: 121 publisher = img_comp_factory.make_imagediff_publisher( 122 self.resultsdir) 123 124 # get chrome version 125 version_string = utils.system_output( 126 constants.CHROME_VERSION_COMMAND, ignore_status=True) 127 version_string = utils.parse_chrome_version(version_string)[0] 128 129 # tags for publishing 130 tags = { 131 'testname': self.tagged_testname, 132 'chromeos_version': utils.get_chromeos_release_version(), 133 'chrome_version': version_string, 134 'board': utils.get_board(), 135 'date': datetime.date.today().strftime("%m/%d/%y"), 136 'diff_pixels': comp_res.diff_pixel_count 137 } 138 139 publisher.publish(golden_image_local_path, 140 test_image_filepath, 141 comp_res.pdiff_image_path, tags) 142 143 raise error.TestFail('Test Failed. Please see image comparison ' 144 'result by opening index.html from the ' 145 'results directory.') 146 147 file_utils.rm_dir_if_exists(ui_TestBase.WORKING_DIR) 148 149 150 @property 151 def folder_name(self): 152 """ 153 Computes the folder name to look for golden images in 154 based on the current test area. 155 156 If we have tagged our testcase, it removes the tag to 157 get the base testname. 158 159 E.g if we add the tag 'guest' to the ui_SystemTray class, 160 the tagged test name will be ui_SystemTray.guest 161 162 This removes the tag if it was added 163 """ 164 165 return self.tagged_testname.split('.')[0] 166 167 168 @abc.abstractmethod 169 def capture_screenshot(self, filepath): 170 """ 171 Abstract method to capture a screenshot. 172 Child classes must implement a custom way to take screenshots. 173 This is because each will want to crop to different areas of the screen. 174 175 @param filepath: string, complete path to save the screenshot. 176 177 """ 178 pass 179 180 def draw_image_mask(self, filepath, rectangle, fill='white'): 181 """ 182 Used to draw a mask over selected portions of the captured screenshot. 183 This allows us to mask out things that change between runs while 184 letting us focus on the parts we do care about. 185 186 @param filepath: string, the complete path to the image 187 @param rectangle: tuple, the top left and bottom right coordinates 188 @param fill: string, the color to fill the mask with 189 190 """ 191 192 im = Image.open(filepath) 193 draw = ImageDraw.Draw(im) 194 draw.rectangle(rectangle, fill=fill) 195 im.save(filepath) 196