# Copyright 2014 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 json
import logging
import os
from distutils.version import LooseVersion
from PIL import Image
from common import cloud_bucket
from common import ispy_utils
class ISpyApi(object):
"""The public API for interacting with ISpy."""
def __init__(self, cloud_bucket):
"""Initializes the utility class.
Args:
cloud_bucket: a BaseCloudBucket in which to the version file,
expectations and results are to be stored.
"""
self._cloud_bucket = cloud_bucket
self._ispy = ispy_utils.ISpyUtils(self._cloud_bucket)
self._rebaselineable_cache = {}
def UpdateExpectationVersion(self, chrome_version, version_file):
"""Updates the most recent expectation version to the Chrome version.
Should be called after generating a new set of expectations.
Args:
chrome_version: the chrome version as a string of the form "31.0.123.4".
version_file: path to the version file in the cloud bucket. The version
file contains a json list of ordered Chrome versions for which
expectations exist.
"""
insert_pos = 0
expectation_versions = []
try:
expectation_versions = self._GetExpectationVersionList(version_file)
if expectation_versions:
try:
version = self._GetExpectationVersion(
chrome_version, expectation_versions)
if version == chrome_version:
return
insert_pos = expectation_versions.index(version)
except:
insert_pos = len(expectation_versions)
except cloud_bucket.FileNotFoundError:
pass
expectation_versions.insert(insert_pos, chrome_version)
logging.info('Updating expectation version...')
self._cloud_bucket.UploadFile(
version_file, json.dumps(expectation_versions),
'application/json')
def _GetExpectationVersion(self, chrome_version, expectation_versions):
"""Returns the expectation version for the given Chrome version.
Args:
chrome_version: the chrome version as a string of the form "31.0.123.4".
expectation_versions: Ordered list of Chrome versions for which
expectations exist, as stored in the version file.
Returns:
Expectation version string.
"""
# Find the closest version that is not greater than the chrome version.
for version in expectation_versions:
if LooseVersion(version) <= LooseVersion(chrome_version):
return version
raise Exception('No expectation exists for Chrome %s' % chrome_version)
def _GetExpectationVersionList(self, version_file):
"""Gets the list of expectation versions from google storage.
Args:
version_file: path to the version file in the cloud bucket. The version
file contains a json list of ordered Chrome versions for which
expectations exist.
Returns:
Ordered list of Chrome versions.
"""
try:
return json.loads(self._cloud_bucket.DownloadFile(version_file))
except:
return []
def _GetExpectationNameWithVersion(self, device_type, expectation,
chrome_version, version_file):
"""Get the expectation to be used with the current Chrome version.
Args:
device_type: string identifier for the device type.
expectation: name for the expectation to generate.
chrome_version: the chrome version as a string of the form "31.0.123.4".
Returns:
Version as an integer.
"""
version = self._GetExpectationVersion(
chrome_version, self._GetExpectationVersionList(version_file))
return self._CreateExpectationName(device_type, expectation, version)
def _CreateExpectationName(self, device_type, expectation, version):
"""Create the full expectation name from the expectation and version.
Args:
device_type: string identifier for the device type, example: mako
expectation: base name for the expectation, example: google.com
version: expectation version, example: 31.0.23.1
Returns:
Full expectation name as a string, example: mako:google.com(31.0.23.1)
"""
return '%s:%s(%s)' % (device_type, expectation, version)
def GenerateExpectation(self, device_type, expectation, chrome_version,
version_file, screenshots):
"""Create an expectation for I-Spy.
Args:
device_type: string identifier for the device type.
expectation: name for the expectation to generate.
chrome_version: the chrome version as a string of the form "31.0.123.4".
screenshots: a list of similar PIL.Images.
"""
# https://code.google.com/p/chromedriver/issues/detail?id=463
expectation_with_version = self._CreateExpectationName(
device_type, expectation, chrome_version)
if self._ispy.ExpectationExists(expectation_with_version):
logging.warning(
'I-Spy expectation \'%s\' already exists, overwriting.',
expectation_with_version)
logging.info('Generating I-Spy expectation...')
self._ispy.GenerateExpectation(expectation_with_version, screenshots)
def PerformComparison(self, test_run, device_type, expectation,
chrome_version, version_file, screenshot):
"""Compare a screenshot with the given expectation in I-Spy.
Args:
test_run: name for the test run.
device_type: string identifier for the device type.
expectation: name for the expectation to compare against.
chrome_version: the chrome version as a string of the form "31.0.123.4".
screenshot: a PIL.Image to compare.
"""
# https://code.google.com/p/chromedriver/issues/detail?id=463
logging.info('Performing I-Spy comparison...')
self._ispy.PerformComparison(
test_run,
self._GetExpectationNameWithVersion(
device_type, expectation, chrome_version, version_file),
screenshot)
def CanRebaselineToTestRun(self, test_run):
"""Returns whether the test run has associated expectations.
Returns:
True if RebaselineToTestRun() can be called for this test run.
"""
if test_run in self._rebaselineable_cache:
return True
return self._cloud_bucket.FileExists(
ispy_utils.GetTestRunPath(test_run, 'rebaseline.txt'))
def RebaselineToTestRun(self, test_run):
"""Update the version file to use expectations associated with |test_run|.
Args:
test_run: The name of the test run to rebaseline.
"""
rebaseline_path = ispy_utils.GetTestRunPath(test_run, 'rebaseline.txt')
rebaseline_attrib = json.loads(
self._cloud_bucket.DownloadFile(rebaseline_path))
self.UpdateExpectationVersion(
rebaseline_attrib['version'], rebaseline_attrib['version_file'])
self._cloud_bucket.RemoveFile(rebaseline_path)
def _SetTestRunRebaselineable(self, test_run, chrome_version, version_file):
"""Writes a JSON file containing the data needed to rebaseline.
Args:
test_run: The name of the test run to add the rebaseline file to.
chrome_version: the chrome version that can be rebaselined to (must have
associated Expectations).
version_file: the path of the version file associated with the test run.
"""
self._rebaselineable_cache[test_run] = True
self._cloud_bucket.UploadFile(
ispy_utils.GetTestRunPath(test_run, 'rebaseline.txt'),
json.dumps({
'version': chrome_version,
'version_file': version_file}),
'application/json')
def PerformComparisonAndPrepareExpectation(self, test_run, device_type,
expectation, chrome_version,
version_file, screenshots):
"""Perform comparison and generate an expectation that can used later.
The test run web UI will have a button to set the Expectations generated for
this version as the expectation for comparison with later versions.
Args:
test_run: The name of the test run to add the rebaseline file to.
device_type: string identifier for the device type.
chrome_version: the chrome version that can be rebaselined to (must have
associated Expectations).
version_file: the path of the version file associated with the test run.
screenshot: a list of similar PIL.Images.
"""
if not self.CanRebaselineToTestRun(test_run):
self._SetTestRunRebaselineable(test_run, chrome_version, version_file)
expectation_with_version = self._CreateExpectationName(
device_type, expectation, chrome_version)
self._ispy.GenerateExpectation(expectation_with_version, screenshots)
self._ispy.PerformComparison(
test_run,
self._GetExpectationNameWithVersion(
device_type, expectation, chrome_version, version_file),
screenshots[-1])