Bug 686019 - Add support for testing addon sync in TPS. r=mconnor

This commit is contained in:
Jonathan Griffin 2011-11-14 21:02:02 -08:00
Родитель cdebc6cee7
Коммит 30d1a0559e
8 изменённых файлов: 500 добавлений и 28 удалений

Просмотреть файл

@ -0,0 +1,50 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* The list of phases mapped to their corresponding profiles. The object
* here must be in strict JSON format, as it will get parsed by the Python
* testrunner (no single quotes, extra comma's, etc).
*/
var phases = { "phase1": "profile1",
"phase2": "profile1",
"phase3": "profile1",
"phase4": "profile1",
"phase5": "profile1" };
/*
* Test phases
*/
Phase('phase1', [
[Addons.install, ['unsigned-1.0.xml']],
[Addons.verify, ['unsigned-xpi@tests.mozilla.org'], STATE_DISABLED],
[Sync, SYNC_WIPE_SERVER],
]);
Phase('phase2', [
[Sync],
[Addons.verify, ['unsigned-xpi@tests.mozilla.org'], STATE_ENABLED],
[Addons.setState, ['unsigned-xpi@tests.mozilla.org'], STATE_DISABLED],
[Sync],
]);
Phase('phase3', [
[Sync],
[Addons.verify, ['unsigned-xpi@tests.mozilla.org'], STATE_DISABLED],
[Addons.setState, ['unsigned-xpi@tests.mozilla.org'], STATE_ENABLED],
[Sync],
]);
Phase('phase4', [
[Sync],
[Addons.verify, ['unsigned-xpi@tests.mozilla.org'], STATE_ENABLED],
[Addons.uninstall, ['unsigned-xpi@tests.mozilla.org']],
[Sync],
]);
Phase('phase5', [
[Sync],
[Addons.verifyNot, ['unsigned-xpi@tests.mozilla.org']],
]);

Просмотреть файл

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<searchresults total_results="1">
<addon id="5612">
<name>Unsigned Test XPI</name>
<type id="1">Extension</type>
<guid>unsigned-xpi@tests.mozilla.org</guid>
<slug>unsigned-xpi</slug>
<version>1.0</version>
<compatible_applications><application>
<name>Firefox</name>
<application_id>1</application_id>
<min_version>3.6</min_version>
<max_version>*</max_version>
<appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
</application></compatible_applications>
<all_compatible_os><os>ALL</os></all_compatible_os>
<install os="ALL" size="452">http://127.0.0.1:4567/unsigned-1.0.xpi</install>
<created epoch="1252903662">
2009-09-14T04:47:42Z
</created>
<last_updated epoch="1315255329">
2011-09-05T20:42:09Z
</last_updated>
</addon>
</searchresults>

Двоичные данные
services/sync/tests/tps/unsigned-1.0.xpi Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,252 @@
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is Crossweave.
*
* The Initial Developer of the Original Code is Mozilla.
* Portions created by the Initial Developer are Copyright (C) 2010
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Jonathan Griffin <jgriffin@mozilla.com>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
var EXPORTED_SYMBOLS = ["Addon", "STATE_ENABLED", "STATE_DISABLED"];
const CC = Components.classes;
const CI = Components.interfaces;
const CU = Components.utils;
CU.import("resource://gre/modules/AddonManager.jsm");
CU.import("resource://gre/modules/AddonRepository.jsm");
CU.import("resource://gre/modules/Services.jsm");
CU.import("resource://services-sync/async.js");
CU.import("resource://services-sync/util.js");
CU.import("resource://tps/logger.jsm");
var XPIProvider = CU.import("resource://gre/modules/XPIProvider.jsm")
.XPIProvider;
const ADDONSGETURL = 'http://127.0.0.1:4567/';
const STATE_ENABLED = 1;
const STATE_DISABLED = 2;
function GetFileAsText(file)
{
let channel = Services.io.newChannel(file, null, null);
let inputStream = channel.open();
if (channel instanceof CI.nsIHttpChannel &&
channel.responseStatus != 200) {
return "";
}
let streamBuf = "";
let sis = CC["@mozilla.org/scriptableinputstream;1"]
.createInstance(CI.nsIScriptableInputStream);
sis.init(inputStream);
let available;
while ((available = sis.available()) != 0) {
streamBuf += sis.read(available);
}
inputStream.close();
return streamBuf;
}
function Addon(TPS, id) {
this.TPS = TPS;
this.id = id;
}
Addon.prototype = {
_addons_requiring_restart: [],
_addons_pending_install: [],
Delete: function() {
// find our addon locally
let cb = Async.makeSyncCallback();
XPIProvider.getAddonsByTypes(null, cb);
let results = Async.waitForSyncCallback(cb);
var addon;
var id = this.id;
results.forEach(function(result) {
if (result.id == id) {
addon = result;
}
});
Logger.AssertTrue(!!addon, 'could not find addon ' + this.id + ' to uninstall');
addon.uninstall();
},
Find: function(state) {
let cb = Async.makeSyncCallback();
let addon_found = false;
var that = this;
var log_addon = function(addon) {
that.addon = addon;
Logger.logInfo('addon ' + addon.id + ' found, isActive: ' + addon.isActive);
if (state == STATE_ENABLED || state == STATE_DISABLED) {
Logger.AssertEqual(addon.isActive,
state == STATE_ENABLED ? true : false,
"addon " + that.id + " has an incorrect enabled state");
}
};
// first look in the list of all addons
XPIProvider.getAddonsByTypes(null, cb);
let addonlist = Async.waitForSyncCallback(cb);
addonlist.forEach(function(addon) {
if (addon.id == that.id) {
addon_found = true;
log_addon.call(that, addon);
}
});
if (!addon_found) {
// then look in the list of recent installs
cb = Async.makeSyncCallback();
XPIProvider.getInstallsByTypes(null, cb);
addonlist = Async.waitForSyncCallback(cb);
for (var i in addonlist) {
if (addonlist[i].addon && addonlist[i].addon.id == that.id &&
addonlist[i].state == AddonManager.STATE_INSTALLED) {
addon_found = true;
log_addon.call(that, addonlist[i].addon);
}
}
}
return addon_found;
},
Install: function() {
// For Install, the id parameter initially passed is really the filename
// for the addon's install .xml; we'll read the actual id from the .xml.
let url = this.id;
// set the url used by getAddonsByIDs
var prefs = CC["@mozilla.org/preferences-service;1"]
.getService(CI.nsIPrefBranch);
prefs.setCharPref('extensions.getAddons.get.url', ADDONSGETURL + url);
// read the XML and find the addon id
xml = GetFileAsText(ADDONSGETURL + url);
Logger.AssertTrue(xml.indexOf("<guid>") > -1, 'guid not found in ' + url);
this.id = xml.substring(xml.indexOf("<guid>") + 6, xml.indexOf("</guid"));
Logger.logInfo('addon XML = ' + this.id);
// find our addon on 'AMO'
let cb = Async.makeSyncCallback();
AddonRepository.getAddonsByIDs([this.id], {
searchSucceeded: cb,
searchFailed: cb
}, false);
// Result will be array of addons on searchSucceeded or undefined on
// searchFailed.
let install_addons = Async.waitForSyncCallback(cb);
Logger.AssertTrue(install_addons,
"no addons found for id " + this.id);
Logger.AssertEqual(install_addons.length,
1,
"multiple addons found for id " + this.id);
let addon = install_addons[0];
Logger.logInfo(JSON.stringify(addon), null, ' ');
if (XPIProvider.installRequiresRestart(addon)) {
this._addons_requiring_restart.push(addon.id);
}
// Start installing the addon asynchronously; finish up in
// onInstallEnded(), onInstallFailed(), or onDownloadFailed().
this._addons_pending_install.push(addon.id);
this.TPS.StartAsyncOperation();
Utils.nextTick(function() {
let callback = function(aInstall) {
addon.install = aInstall;
Logger.logInfo("addon install: " + addon.install);
Logger.AssertTrue(addon.install,
"could not get install object for id " + this.id);
addon.install.addListener(this);
addon.install.install();
};
AddonManager.getInstallForURL(addon.sourceURI.spec,
callback.bind(this),
"application/x-xpinstall");
}, this);
},
SetState: function(state) {
if (!this.Find())
return false;
this.addon.userDisabled = state == STATE_ENABLED ? false : true;
return true;
},
// addon installation callbacks
onInstallEnded: function(addon) {
try {
Logger.logInfo('--------- event observed: addon onInstallEnded');
Logger.AssertTrue(addon.addon,
"No addon object in addon instance passed to onInstallEnded");
Logger.AssertTrue(this._addons_pending_install.indexOf(addon.addon.id) > -1,
"onInstallEnded received for unexpected addon " + addon.addon.id);
this._addons_pending_install.splice(
this._addons_pending_install.indexOf(addon.addon.id),
1);
}
catch(e) {
// We can't throw during a callback, as it will just get eaten by
// the callback's caller.
Utils.nextTick(function() {
this.DumpError(e);
}, this);
return;
}
this.TPS.FinishAsyncOperation();
},
onInstallFailed: function(addon) {
Logger.logInfo('--------- event observed: addon onInstallFailed');
Utils.nextTick(function() {
this.DumpError('Installation failed for addon ' +
(addon.addon && addon.addon.id ? addon.addon.id : 'unknown'));
}, this);
},
onDownloadFailed: function(addon) {
Logger.logInfo('--------- event observed: addon onDownloadFailed');
Utils.nextTick(function() {
this.DumpError('Download failed for addon ' +
(addon.addon && addon.addon.id ? addon.addon.id : 'unknown'));
}, this);
},
};

Просмотреть файл

@ -48,9 +48,11 @@ const CU = Components.utils;
CU.import("resource://services-sync/service.js");
CU.import("resource://services-sync/constants.js");
CU.import("resource://services-sync/async.js");
CU.import("resource://services-sync/util.js");
CU.import("resource://gre/modules/XPCOMUtils.jsm");
CU.import("resource://gre/modules/Services.jsm");
CU.import("resource://tps/addons.jsm");
CU.import("resource://tps/bookmarks.jsm");
CU.import("resource://tps/logger.jsm");
CU.import("resource://tps/passwords.jsm");
@ -61,6 +63,8 @@ CU.import("resource://tps/tabs.jsm");
var hh = CC["@mozilla.org/network/protocol;1?name=http"]
.getService(CI.nsIHttpProtocolHandler);
var prefs = CC["@mozilla.org/preferences-service;1"]
.getService(CI.nsIPrefBranch);
var mozmillInit = {};
CU.import('resource://mozmill/modules/init.js', mozmillInit);
@ -73,37 +77,16 @@ const ACTION_SYNC = "sync";
const ACTION_DELETE = "delete";
const ACTION_PRIVATE_BROWSING = "private-browsing";
const ACTION_WIPE_SERVER = "wipe-server";
const ACTION_SETSTATE = "set-state";
const ACTIONS = [ACTION_ADD, ACTION_VERIFY, ACTION_VERIFY_NOT,
ACTION_MODIFY, ACTION_SYNC, ACTION_DELETE,
ACTION_PRIVATE_BROWSING, ACTION_WIPE_SERVER];
ACTION_PRIVATE_BROWSING, ACTION_WIPE_SERVER,
ACTION_SETSTATE];
const SYNC_WIPE_SERVER = "wipe-server";
const SYNC_RESET_CLIENT = "reset-client";
const SYNC_WIPE_CLIENT = "wipe-client";
function GetFileAsText(file)
{
let channel = Services.io.newChannel(file, null, null);
let inputStream = channel.open();
if (channel instanceof CI.nsIHttpChannel &&
channel.responseStatus != 200) {
return "";
}
let streamBuf = "";
let sis = CC["@mozilla.org/scriptableinputstream;1"]
.createInstance(CI.nsIScriptableInputStream);
sis.init(inputStream);
let available;
while ((available = sis.available()) != 0) {
streamBuf += sis.read(available);
}
inputStream.close();
return streamBuf;
}
var TPS =
{
_waitingForSync: false,
@ -351,6 +334,33 @@ var TPS =
}
},
HandleAddons: function (addons, action, state) {
for (var i in addons) {
Logger.logInfo("executing action " + action.toUpperCase() +
" on addon " + JSON.stringify(addons[i]));
var addon = new Addon(this, addons[i]);
switch(action) {
case ACTION_ADD:
addon.Install();
break;
case ACTION_DELETE:
addon.Delete();
break;
case ACTION_VERIFY:
Logger.AssertTrue(addon.Find(state), 'addon ' + addon.id + ' not found');
break;
case ACTION_VERIFY_NOT:
Logger.AssertTrue(!addon.Find(state), 'addon ' + addon.id + " is present, but it shouldn't be");
break;
case ACTION_SETSTATE:
Logger.AssertTrue(addon.SetState(state), 'addon ' + addon.id + ' not found');
break;
}
}
Logger.logPass("executing action " + action.toUpperCase() +
" on addons");
},
HandleBookmarks: function (bookmarks, action) {
try {
let items = [];
@ -460,7 +470,7 @@ var TPS =
let phase = this._phaselist["phase" + this._currentPhase];
let action = phase[this._currentAction];
Logger.logInfo("starting action: " + JSON.stringify(action));
action[0].call(this, action[1]);
action[0].apply(this, action.slice(1));
// if we're in an async operation, don't continue on to the next action
if (this._operations_pending)
@ -517,8 +527,6 @@ var TPS =
// Store account details as prefs so they're accessible to the mozmill
// framework.
let prefs = CC["@mozilla.org/preferences-service;1"]
.getService(CI.nsIPrefBranch);
prefs.setCharPref('tps.account.username', this.config.account.username);
prefs.setCharPref('tps.account.password', this.config.account.password);
prefs.setCharPref('tps.account.passphrase', this.config.account.passphrase);
@ -634,6 +642,24 @@ var TPS =
},
};
var Addons = {
install: function Addons__install(addons) {
TPS.HandleAddons(addons, ACTION_ADD);
},
setState: function Addons__setState(addons, state) {
TPS.HandleAddons(addons, ACTION_SETSTATE, state);
},
uninstall: function Addons__uninstall(addons) {
TPS.HandleAddons(addons, ACTION_DELETE);
},
verify: function Addons__verify(addons, state) {
TPS.HandleAddons(addons, ACTION_VERIFY, state);
},
verifyNot: function Addons__verifyNot(addons) {
TPS.HandleAddons(addons, ACTION_VERIFY_NOT);
},
};
var Bookmarks = {
add: function Bookmarks__add(bookmarks) {
TPS.HandleBookmarks(bookmarks, ACTION_ADD);

Просмотреть файл

@ -38,4 +38,5 @@
from firefoxrunner import TPSFirefoxRunner
from pulse import TPSPulseMonitor
from testrunner import TPSTestRunner
from mozhttpd import MozHttpd

111
testing/tps/tps/mozhttpd.py Normal file
Просмотреть файл

@ -0,0 +1,111 @@
#!/usr/bin/python
#
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is mozilla.org code.
#
# The Initial Developer of the Original Code is
# the Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Joel Maher <joel.maher@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
import BaseHTTPServer
import SimpleHTTPServer
import threading
import sys
import os
import urllib
import re
from urlparse import urlparse
from SocketServer import ThreadingMixIn
DOCROOT = '.'
class EasyServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
allow_reuse_address = True
class MozRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def translate_path(self, path):
# It appears that the default path is '/' and os.path.join makes the '/'
o = urlparse(path)
return "%s%s" % ('' if sys.platform == 'win32' else '/', '/'.join([i.strip('/') for i in (DOCROOT, o.path)]))
# I found on my local network that calls to this were timing out
# I believe all of these calls are from log_message
def address_string(self):
return "a.b.c.d"
# This produces a LOT of noise
def log_message(self, format, *args):
pass
class MozHttpd(object):
def __init__(self, host="127.0.0.1", port=8888, docroot='.'):
global DOCROOT
self.host = host
self.port = int(port)
DOCROOT = docroot
def start(self):
self.httpd = EasyServer((self.host, self.port), MozRequestHandler)
self.server = threading.Thread(target=self.httpd.serve_forever)
self.server.setDaemon(True) # don't hang on exit
self.server.start()
#self.testServer()
#TODO: figure this out
def testServer(self):
fileList = os.listdir(DOCROOT)
filehandle = urllib.urlopen('http://%s:%s' % (self.host, self.port))
data = filehandle.readlines();
filehandle.close()
for line in data:
found = False
# '@' denotes a symlink and we need to ignore it.
webline = re.sub('\<[a-zA-Z0-9\-\_\.\=\"\'\/\\\%\!\@\#\$\^\&\*\(\) ]*\>', '', line.strip('\n')).strip('/').strip().strip('@')
if webline != "":
if webline == "Directory listing for":
found = True
else:
for fileName in fileList:
if fileName == webline:
found = True
if (found == False):
print "NOT FOUND: " + webline.strip()
def stop(self):
if self.httpd:
self.httpd.shutdown()
__del__ = stop

Просмотреть файл

@ -53,7 +53,7 @@ from mozprofile import Profile
from tps.firefoxrunner import TPSFirefoxRunner
from tps.phase import TPSTestPhase
from tps.mozhttpd import MozHttpd
class TempFile(object):
"""Class for temporary files that delete themselves when garbage-collected.
@ -397,6 +397,9 @@ class TPSTestRunner(object):
testlist = [os.path.basename(self.testfile)]
testdir = os.path.dirname(self.testfile)
self.mozhttpd = MozHttpd(port=4567, docroot=testdir)
self.mozhttpd.start()
# run each test, and save the results
for test in testlist:
result = self.run_single_test(testdir, test)
@ -415,6 +418,8 @@ class TPSTestRunner(object):
else:
self.numfailed += 1
self.mozhttpd.stop()
# generate the postdata we'll use to post the results to the db
self.postdata = { 'tests': self.results,
'os':os_string,