# 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/. import datetime import os import subprocess import time from StringIO import StringIO from PIL import Image from marionette_driver import expected, By from marionette_driver.marionette import Actions from marionette_driver.gestures import pinch, smooth_scroll from mozlog.structured import get_default_logger from gaiatest import GaiaTestCase from gaiatest.apps.system.app import System class GaiaImageCompareTestCase(GaiaTestCase): def __init__(self, *args, **kwargs): GaiaTestCase.__init__(self, *args, **kwargs) self.store_reference_image = kwargs.pop('store_reference_image', False) self.fuzz_factor = kwargs.pop('fuzz_factor') self.reference_path = kwargs.pop('reference_path') self.screenshots_path = kwargs.pop('screenshots_path') self.mismatch_path = os.path.join(self.screenshots_path, 'mismatches') self.logger = get_default_logger() self.picture_index = 0 self.test_passed = True self.failcomment = "" # set up directories if not os.path.exists(self.reference_path): os.makedirs(self.reference_path) if not os.path.exists(self.screenshots_path): os.makedirs(self.screenshots_path) def setUp(self): GaiaTestCase.setUp(self) self.device_name = self.marionette.session_capabilities.get('device', 'unknown') def tearDown(self): GaiaTestCase.tearDown(self) """At the end of test execution, it checks for the errors""" self.assertTrue(self.test_passed, msg=self.failcomment) # if the status bar is visible, crop it off @property def crop_height(self): return int(System(self.marionette).status_bar.height * self.marionette.execute_script('return window.wrappedJSObject.devicePixelRatio;')) def take_screenshot(self, page_name=None, prewait=1, top_frame=False): """ invokes screen capture event, crops the status bar, and saves to the file page_name: a (optional) name that is given to the screenshot image file """ time.sleep(prewait) # take screenshot self.marionette.set_context(self.marionette.CONTEXT_CHROME) screenshot = self.marionette.screenshot(format="binary") self.marionette.set_context(self.marionette.CONTEXT_CONTENT) # Format: test_name.page_name(parameter).locale.device_name.index.png reference_filename = os.path.join(self.reference_path, '%s_%s_%s_%s_%s.png' % ( self.methodName, page_name, self.device_name, self.data_layer.get_setting('language.current'), self.picture_index)) # determine the image file name (path included) if self.store_reference_image: filename = reference_filename else: # obtain screenshot filename # Format: test_name.page_name(parameter).locale.device_name.index.timestamp.png timestamp = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S') filename = os.path.join(self.screenshots_path, '%s_%s_%s_%s_%s+%s.png' % ( self.methodName, page_name, self.device_name, self.data_layer.get_setting('language.current'), self.picture_index, timestamp)) # save the image after cropping it im = Image.open(StringIO(screenshot)) crop_box = (0, self.crop_height) + im.size new_image = im.crop(crop_box) new_image.save(filename) # when not collecting reference image, compare the image to the reference if self.store_reference_image is False: # get the contents for the reference folder for a match ref_file_list = os.listdir(self.reference_path) for i, p in enumerate(ref_file_list): ref_file_list[i] = os.path.join(self.reference_path, p) if reference_filename in ref_file_list: self.image_compare(filename, reference_filename, "{0}_diff.png".format(filename[0:filename.find(".png")]), self.fuzz_factor) else: self.failcomment += "\nRef file not found for: " + filename + '\n' self.test_passed = False # sometimes the frame should remain at the top level if top_frame is False: self.apps.switch_to_displayed_app() self.picture_index += 1 def image_compare(self, target, ref, diff, fuzz_value): """do single image compare using the convert console command of ImageMagick""" p = subprocess.Popen( ["compare", "-fuzz", str(fuzz_value) + "%", "-metric", "AE", target, ref, diff], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() p.wait() if not (err.rstrip('\n') == '0'): self.test_passed = False # msg should be saved at this point. Otherwise, it will get lost if any of the below line fails if len(err.split()) > 1: self.failcomment += '\n' + err else: self.failcomment += '\nWARNING: ' + err.rstrip( '\n') + ' pixels mismatched between ' + target + ' and ' + ref + '\n' # move the diff image and the target image to a separate folder if not os.path.exists(self.mismatch_path): os.makedirs(self.mismatch_path) target_image_name = target[target.rfind("/") + 1:] os.rename(target, os.path.join(self.mismatch_path, target_image_name)) # diff may not be generated if the image sizes do not match if os.path.exists(diff): diff_image_name = diff[diff.rfind("/") + 1:] os.rename(diff, os.path.join(self.mismatch_path, diff_image_name)) # Make UI action related methods static, so they can be used outside the GaiaImageCompareTestCase object as well. @staticmethod def edge_scroll(marionette, frame, direction, dist, release=True): """edge scroll - performs task switching action. direction = 'LtoR' or 'RtoL' (finger movement direction) dist = percentage of horizontal distance travel, max is 1.0 release = if set to False, the Action object will be returned so the user can complete the release action""" start_x = 0 dist_travelled = 0 time_increment = 0.01 if dist > 1: dist = 1 if direction == 'LtoR': start_x = 0 elif direction == 'RtoL': start_x = frame.rect['width'] dist *= -1 # travel opposite direction limit = dist * frame.rect['width'] dist_unit = limit * time_increment assert isinstance(marionette, object) action = Actions(marionette) action.press(frame, start_x, frame.rect['height'] / 2) # press either the left or right edge while abs(dist_travelled) < abs(limit): action.move_by_offset(dist_unit, 0) action.wait(time_increment) dist_travelled += dist_unit if release: action.release() action.perform() time.sleep(2) # compensate for the time taken for edge scroll to bring another app to active return action @staticmethod def pinch(marionette, locator, zoom, duration=200): """pinch method - works for gallery. Bug 1025167 is raised for the Browser pinch method. zoom = 'in' or 'out' level = level of zoom, 'low' or 'high'""" assert isinstance(marionette, object) screen = marionette.find_element(*locator) mid_x = screen.rect['width'] / 2 mid_y = screen.rect['height'] / 2 # default is zooming in init_index_x = mid_x init_index_y = mid_y init_thumb_x = mid_x init_thumb_y = mid_y disp_x = mid_x disp_y = mid_y if zoom == 'out': init_index_x = mid_x / 2 init_index_y = mid_y / 2 init_thumb_x = mid_x + mid_x / 2 init_thumb_y = mid_y + mid_y / 2 disp_x = - mid_x / 2 disp_y = - mid_y / 2 pinch(marionette, screen, init_index_x, init_index_y, init_thumb_x, init_thumb_y, -disp_x, -disp_y, disp_x, disp_y, duration) time.sleep(2) # compensate for the time taken for pinch to complete @staticmethod def scroll(marionette, direction, distance, locator=None, screen=None, increments=None): """scroll - uses smooth_scroll method in gestures.py. direction = 'up' or 'down' (page location) distance = total distance to travel increments = rate of scroll locator = locator of the screen that needs to be scrolled screen = screen element that needs to be scrolled locator is for backwards compatibility. screen should be used perform release afterwards """ if screen is None: assert isinstance(marionette, object) screen = marionette.find_element(*locator) vector = -1 axis = 'x' if direction == 'up' or direction == 'down': axis = 'y' # # define direction. if direction == 'up' or direction == 'left': vector = 0 smooth_scroll(marionette, screen, axis, vector, distance, increments) time.sleep(2) # compensate for the time taken for dynamic scroll