Bug 686019 - Add support for testing addon sync in TPS. r=mconnor
authorJonathan Griffin <jgriffin@mozilla.com>
Mon, 14 Nov 2011 21:02:02 -0800
changeset 81880 a0752c7b4bcf0dd1711cc839441418c3cd4b096b
parent 81879 276bfdbcb0068aef601e9a696277ca1cc801bc2f
child 81881 b9fe29b833deb812530e02db443835909cce2fa5
push idunknown
push userunknown
push dateunknown
reviewersmconnor
bugs686019
milestone11.0a1
Bug 686019 - Add support for testing addon sync in TPS. r=mconnor
services/sync/tests/tps/test_addon_sanity.js
services/sync/tests/tps/unsigned-1.0.xml
services/sync/tests/tps/unsigned-1.0.xpi
services/sync/tps/extensions/tps/modules/addons.jsm
services/sync/tps/extensions/tps/modules/tps.jsm
testing/tps/tps/__init__.py
testing/tps/tps/mozhttpd.py
testing/tps/tps/testrunner.py
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_addon_sanity.js
@@ -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']],
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/unsigned-1.0.xml
@@ -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>
\ No newline at end of file
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..51b00475a9641ea9d608874a3ab7679da3a4374b
GIT binary patch
literal 452
zc$^FHW@Zs#U}E54_?u^9#dA2DRf~~<A%uy6ftx{;Av3SIBrzvPuP7xgG=!6Z`R(;m
zPY@2RU}5;mD8f)0<mh+UKw!^vQMJ8_JN=layLm<}3F>4Pa^m=!CAB@`#_JRJCLMTP
zI9DyV<<;!;^s=<;=fu~BJ&BG`=N0*wGwVpVQP-#IS7-g;+M)E%WsUK%S<7acl?Nw(
z&oP^SK626~lcn2k1%78f5zUn+<o=|h$w0Vj$<=4!T1TCmO+4Styx{&-<y%?H6YYoF
z!mhnY7WkLQ7jcB0`=;kq&w>+E7tLzE!`U8JT5_uOTuP|ifs?i!UE8+CEYjcYy?ajS
z8plGZ?O!gpMb-1SG`8M($7*+F>!j#D=h``2kGE-L^!%E%W6!hu!CPCtPtFy8#3UhH
zdHeBp*^fM58gJ}(X{=0b>ML;8jErXXkXxv9p6O?ovig*H-&tb1i#~sHx%bD*`FKOi
zcd5uS!$os`<_Ejg2Y53wi8JF0X<h~p0CE|YG=f+t;m!&P_s|Lkh5&C?Hi$|_1~(v`
I0n)+%08F#C6#xJL
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/extensions/tps/modules/addons.jsm
@@ -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);
+  },
+
+};
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ b/services/sync/tps/extensions/tps/modules/tps.jsm
@@ -43,72 +43,55 @@
 var EXPORTED_SYMBOLS = ["TPS"];
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 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");
 CU.import("resource://tps/history.jsm");
 CU.import("resource://tps/forms.jsm");
 CU.import("resource://tps/prefs.jsm");
 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);
 
 const ACTION_ADD = "add";
 const ACTION_VERIFY = "verify";
 const ACTION_VERIFY_NOT = "verify-not";
 const ACTION_MODIFY = "modify";
 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,
   _test: null,
   _currentAction: -1,
   _currentPhase: -1,
   _errors: 0,
   _syncErrors: 0,
@@ -346,16 +329,43 @@ var TPS =
                      " on passwords");
     }
     catch(e) {
       DumpPasswords();
       throw(e);
     }
   },
 
+  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 = [];
       for (folder in bookmarks) {
         let last_item_pos = -1;
         for each (bookmark in bookmarks[folder]) {
           Logger.clearPotentialError();
           let placesItem;
@@ -455,17 +465,17 @@ var TPS =
       else {
         this.DumpError("seconds-since-epoch not set");
         return;
       }
       
       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)
         return;
 
       this._currentAction++;
     }
     catch(e) {
@@ -512,18 +522,16 @@ var TPS =
       Weave.Svc.Prefs.set("client.name", this.phases["phase" + this._currentPhase]);
 
       // wipe the server at the end of the final test phase
       if (this.phases["phase" + (parseInt(this._currentPhase) + 1)] == undefined)
         this_phase.push([this.WipeServer]);
 
       // 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);
       if (this.config.account['serverURL']) {
         prefs.setCharPref('tps.account.serverURL', this.config.account.serverURL);
       }
 
       // start processing the test actions
@@ -629,16 +637,34 @@ var TPS =
     Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status not OK");
     this._waitingForSync = true;
     this.StartAsyncOperation();
     Weave.Service.sync();
     return;
   },
 };
 
+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);
   },
   modify: function Bookmarks__modify(bookmarks) {
     TPS.HandleBookmarks(bookmarks, ACTION_MODIFY);
   },
   delete: function Bookmarks__delete(bookmarks) {
--- a/testing/tps/tps/__init__.py
+++ b/testing/tps/tps/__init__.py
@@ -33,9 +33,10 @@
 # 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 *****
 
 from firefoxrunner import TPSFirefoxRunner
 from pulse import TPSPulseMonitor
 from testrunner import TPSTestRunner
+from mozhttpd import MozHttpd
 
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/mozhttpd.py
@@ -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
+
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -48,17 +48,17 @@ import traceback
 import urllib
 
 from threading import RLock
 
 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.
   """
 
   def __init__(self, prefix=None):
     self.fd, self.filename = self.tmpfile = tempfile.mkstemp(prefix=prefix)
 
@@ -392,16 +392,19 @@ class TPSTestRunner(object):
       jsondata = f.read()
       f.close()
       testfiles = json.loads(jsondata)
       testlist = testfiles['tests']
     except ValueError:
       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)
 
       if not self.productversion:
         self.productversion = result['productversion']
       if not self.addonversion:
         self.addonversion = result['addonversion']
@@ -410,16 +413,18 @@ class TPSTestRunner(object):
                            'name': result['name'], 
                            'message': result['message'],
                            'logdata': result['logdata']})
       if result['state'] == 'TEST-PASS':
         self.numpassed += 1
       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,
                       'testtype': 'crossweave',
                       'productversion': self.productversion,
                       'addonversion': self.addonversion,
                       'synctype': self.synctype,
                     }