Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
240 lines
9.4 KiB
Python
Executable file
240 lines
9.4 KiB
Python
Executable file
# 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
|