Client now launches a new browser session for each test.
authorMark Cote <mcote@mozilla.com>
Wed, 31 Aug 2011 15:17:46 -0400
changeset 59 579075358b1bf24957bf5d336b5428c65e3cdca2
parent 58 cc34c0f3e48fbbe5cdf905301779bf703943dbe6
child 60 94d175c236153133c873e8c9454e76e9223cb19a
push id57
push usermcote@mozilla.com
push dateWed, 31 Aug 2011 19:17:47 +0000
Client now launches a new browser session for each test.
client/results.py
client/speedtests.py
html/js/speedtests.js
server/speedtests_server.py
server/templates/done.html
server/templates/start.html
server/templates/test.html
new file mode 100644
--- /dev/null
+++ b/client/results.py
@@ -0,0 +1,94 @@
+from collections import defaultdict
+
+class SpeedTestReport(object):
+
+    def __init__(self, results):
+        self.results = results
+        self.highest_scores = defaultdict(lambda: {'score': 0,
+                                                   'score_str': '',
+                                                   'browsers': []})
+
+    def record_highest_score(self, test, score, score_str, browser):
+        if self.highest_scores[test]['score'] < score:
+            self.highest_scores[test]['score'] = score
+            self.highest_scores[test]['score_str'] = score_str
+            self.highest_scores[test]['browsers'] = [browser]
+        elif self.highest_scores[test]['score'] == score:
+            self.highest_scores[test]['browsers'].append(browser)
+
+    def report(self):
+        s = 'Results by browser:\n\n'
+        for browser, tests in self.results.iteritems():
+            s += '%s\n%s\n\n' % (browser, '=' * len(browser))
+            for test, results_strs in tests.iteritems():
+                s += '  %s\n  %s\n\n' % (test, '-' * len(test))
+
+                if test == 'PsychedelicBrowsing':
+                    colorwheel = int(results_strs[0]['colorwheel'])
+                    checkerboard = int(results_strs[0]['checkerboard'])
+                    s += '  Psychedelic (colorwheel): %d rpm\n' % colorwheel
+                    s += '  Hallucinogenic (checkerboard): %d rpm\n\n' % \
+                        checkerboard
+                    total = colorwheel + checkerboard
+                    self.record_highest_score(test, total, '%d/%d rpm' %
+                                              (colorwheel, checkerboard),
+                                              browser)
+                    continue
+
+                score = 0
+                results = map(lambda x: int(x['fps']), results_strs)
+                if len(results) == 1:
+                    score = results[0]
+                    score_str = '%d fps' % score
+                    s += '  %s\n' % score_str
+                else:
+                    if len(results) > 0:
+                        s += '  Series:'
+                        for r in results:
+                            s += ' %3d' % r
+                        s += '\n  Mean: %.1d\n' % (sum(results) / len(results))
+                        sorted_results = results[:]
+                        sorted_results.sort()
+                        if len(sorted_results) % 2 == 0:
+                            median = (sorted_results[len(sorted_results)/2 - 1] + sorted_results[len(sorted_results)/2]) / 2
+                        else:
+                            median = sorted_results[len(sorted_results)/2]
+                        s += '  Median: %d\n' % median
+                        score = median
+                        score_str = '%d fps' % score
+                    else:
+                        s += '  No data.\n'
+                if score:
+                    self.record_highest_score(test, score, score_str, browser)
+                s += '\n'
+            s += '\n'
+        test_list = self.highest_scores.keys()
+        test_list.sort()
+        s += 'Results by test:\n\n'
+        for test in test_list:
+            s += '%s\n%s\n\n' % (test, '=' * len(test))
+            s += ' Highest median score: %s (%s)\n\n' % \
+                (self.highest_scores[test]['score_str'],
+                 ', '.join(self.highest_scores[test]['browsers']))
+        return s
+
+
+def main():
+    results = {
+        'firefox': {
+            'fishtank': [{'fps': 34}, {'fps': 36}, {'fps': 40}, {'fps': 44}, {'fps': 42}, {'fps': 43}, {'fps': 44}, {'fps': 43}, {'fps': 42}, {'fps': 44}, {'fps': 43}, {'fps': 42}],
+            'SantasWorkshop': [{'fps': 10}, {'fps': 7}, {'fps': 4}, {'fps': 3}, {'fps': 3}, {'fps': 3}, {'fps': 3}, {'fps': 3}],
+            'PsychedelicBrowsing': [{'colorwheel': 1944, 'checkerboard': 966}]
+         },
+         'safari': {
+            'fishtank': [{'fps': 10}, {'fps': 9}, {'fps': 8}, {'fps': 7}, {'fps': 6}, {'fps': 6}, {'fps': 6}, {'fps': 6}],
+            'SantasWorkshop': [{'fps': 3}, {'fps': 3}, {'fps': 3}, {'fps': 3}, {'fps': 3}, {'fps': 3}, {'fps': 3}],
+            'PsychedelicBrowsing': [{'colorwheel': 1820, 'checkerboard': 840}]
+         }
+    }
+    report = SpeedTestReport(results)
+    print report.report()
+
+if __name__ == '__main__':
+    main()
+
--- a/client/speedtests.py
+++ b/client/speedtests.py
@@ -9,65 +9,77 @@ import os
 import platform
 import shutil
 import socket
 import subprocess
 import sys
 import tempfile
 import threading
 import time
+import urllib2
 import zipfile
 
 if platform.system() == 'Windows':
     import _winreg
     import ie_reg
 
 import fxinstall
-#import results
+import results
 
 class Config(object):
     DEFAULT_CONF_FILE = 'speedtests.conf'
     
     def __init__(self):
         self.cfg = None
         self.local_port = 8111
-        self.test_url = 'http://brasstacks.mozilla.com/speedtestssvr/start/?auto=true'
+        self.server_html_url = 'http://brasstacks.mozilla.com/speedtests'
+        self.server_api_url = 'http://brasstacks.mozilla.com/speedtests/api'
         self.cfg = None
+        self.local_test_base_path = '/speedtests'
+
+    @property
+    def local_test_base_url(self):
+        # IE has issues loading pages from localhost, so we'll use the
+        # external IP.
+        return 'http://%s:%d%s' % (self.local_ip, self.local_port,
+                                   self.local_test_base_path)
 
     def read(self, testmode=False, noresults=False, conf_file=None):
+        self.testmode = testmode
+        self.noresults = noresults
         if not conf_file:
             conf_file = Config.DEFAULT_CONF_FILE
         self.cfg = ConfigParser.ConfigParser()
         self.cfg.read(conf_file)
         
         try:
             self.local_port = self.cfg.getint('speedtests', 'local_port')
         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
             pass
         
         try:
-            self.test_url = self.cfg.get('speedtests', 'server_url').rstrip('/') + \
-                                    '/start/?auto=true'
+            self.server_html_url = self.cfg.get('speedtests', 'test_base_url').rstrip('/')
         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
             pass
 
+        try:
+            self.server_api_url = self.cfg.get('speedtests', 'server_url').rstrip('/')
+        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+            pass            
+
+        try:
+            self.server_results_url = self.cfg.get('speedtests', 'server_results_url').rstrip('/')
+        except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+            self.server_results_url = self.server_api_url + '/testresults/'
+
         # We can also find out the address like this, supposedly more reliable:
         #s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         #s.connect((<TEST_HOST>, 80))
         #local_ip = s.getsockname()
-        local_ip = socket.gethostbyname(socket.gethostname())
-        if self.test_url.find('?') == -1:
-            self.test_url += '?'
-        else:
-            self.test_url += '&'
-        self.test_url += 'ip=%s&port=%d' % (local_ip, self.local_port)
-        if testmode:
-            self.test_url += '&test=true'
-        if noresults:
-            self.test_url += '&noresults=true'
+        self.local_ip = socket.gethostbyname(socket.gethostname())
 
 
 config = Config()
 
 
 class BrowserController(object):
     
     def __init__(self, os_name, browser_name, profiles, cmd, args_tuple=()):
@@ -85,16 +97,30 @@ class BrowserController(object):
         self.args_tuple = args_tuple
         self.proc = None
         self.launch_time = None
         try:
             self.cmd = config.cfg.get(os_name, browser_name)
         except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
             pass
 
+    def set_test_urls(self, tests):
+        self.test_url_iter = iter(tests)
+        self.current_test_url = None
+
+    def next_test(self):
+        try:
+            self.current_test_url = self.test_url_iter.next()
+        except StopIteration:
+            return False
+        if self.running():
+            self.terminate()
+        self.launch(self.current_test_url)
+        return True
+
     def init_browser(self):
         pass
             
     def cmd_line(self, url):
         return (self.cmd,) + self.args_tuple + (url,)
 
     def browser_exists(self):
         return os.path.exists(self.cmd)
@@ -112,30 +138,46 @@ class BrowserController(object):
             profile_archive = self.get_profile_archive_path(p)
             profile_zip = zipfile.ZipFile(profile_archive, 'w')
             for (dirpath, dirnames, filenames) in os.walk(p['path']):
                 for f in filenames:
                     filepath = os.path.join(dirpath, f)
                     arcpath = filepath[len(p['path']):]
                     profile_zip.write(filepath, arcpath)
             profile_zip.close()
-    
+
+    def retry_file_op(self, func, args):
+        success = False
+        attempts = 0
+        while attempts < 3:
+            attempts += 1
+            try:
+                func(*args)
+            except (IOError, OSError):
+                pass
+            else:
+                success = True
+                break
+            time.sleep(2)
+        return success
+
     def copy_profiles(self):
         if not self.browser_exists():
             return False
         for p in self.profiles:
             profile_archive = self.get_profile_archive_path(p)
             if not os.path.exists(profile_archive):
                 return
             if os.path.exists(p['path']):
                 t = tempfile.mkdtemp()
-                try:
-                    shutil.move(p['path'], t)
-                except (IOError, OSError):
-                    print 'Failed to copy profile: %s' % sys.exc_info()[1]
+                def f(path, tmp):
+                    shutil.rmtree(tmp)
+                    shutil.move(path, tmp)
+                if not self.retry_file_op(f, [p['path'], t]):
+                    print 'Failed to copy profile 3 times; giving up.'
                     return False
                 p['previous_profile'] = os.path.join(t, os.path.basename(p['path']))
             else:
                 p['previous_profile'] = ''
             try:
                 os.mkdir(p['path'])
             except OSError:
                 pass
@@ -144,68 +186,71 @@ class BrowserController(object):
         return True
     
     def clean_up(self):
         if not self.profiles:
             return
         for p in self.profiles:
             if not p['previous_profile']:
                 continue
-            try:
+            def f(p):
                 shutil.rmtree(p['path'])
                 shutil.move(p['previous_profile'], p['path'])
                 os.rmdir(os.path.dirname(p['previous_profile']))
-            except (IOError, OSError):
-                print 'Failed to restore profile: %s' % sys.exc_info()[1]
-                pass
+
+            if not self.retry_file_op(f, [p]):
+                print 'Failed to restore profile 3 times; giving up.'
 
     def launch(self, url=None):
         if not self.copy_profiles():
-            print 'failed to copy profiles'
+            print 'Failed to copy profiles'
             return False
         if not url:
             url = config.test_url
         cl = self.cmd_line(url)
         print 'Launching %s...' % ' '.join(cl)
         self.launch_time = datetime.datetime.now()
         self.proc = subprocess.Popen(cl)
         return True
 
     def running(self):
-        running = self.proc and self.proc.poll()
+        if not self.proc:
+            return False
+        running = self.proc.poll()
         if running != None:
             self.proc = None
         return running == None
 	
     def execution_time(self):
         return datetime.datetime.now() - self.launch_time
 
     def terminate(self):
         if self.proc:
-            print 'terminating process'
+            print 'Terminating process...'
             try:
                 self.proc.terminate()
             except:  #FIXME
                 pass
             for i in range(0, 5):
-                print 'polling'
+                print 'Polling...'
                 if self.proc.poll() != None:
                     self.proc = None
                     break
                 time.sleep(2)
             if self.proc:
-                print 'killing process'
+                print 'Killing process...'
                 try:
                     self.proc.kill()
                 except:
                     pass
-                print 'waiting for process to die'
+                print 'Waiting for process to die...'
                 self.proc.wait()  # or poll and error out if still running?
                 self.proc = None
-            print 'process is dead'
+            print 'Process is dead.'
+            time.sleep(5)
         self.clean_up()
 
 
 class LatestFxBrowserController(BrowserController):
     
     """ Specialization to download latest nightly before launching. """
     
     INSTALL_SUBDIR = 'speedtests_firefox_nightly'
@@ -274,20 +319,20 @@ class IEController(BrowserController):
             #print 'setting %s' % str(i)
             _winreg.SetValueEx(hdl, i[0], 0, i[2], i[1])
         _winreg.CloseKey(hdl)
 
     def terminate(self):
         super(IEController, self).terminate()
         self.restore_reg()
 
-    def launch(self):
+    def launch(self, url=None):
         self.backup_reg()
         self.setup_reg()
-        return super(IEController, self).launch()
+        return BrowserController.launch(self, url)
         
             
 class BrowserControllerRedirFile(BrowserController):
     
     def __init__(self, os_name, browser_name, profile_path, cmd, args_tuple=()):
         super(BrowserControllerRedirFile, self).__init__(os_name, browser_name, profile_path, cmd, args_tuple)
         self.redir_file = None
     
@@ -372,18 +417,19 @@ class BrowserRunner(object):
             while True:
                 try:
                     n = self.iter.next()
                 except StopIteration:
                     raise
                 if not self.browser_names or n.browser_name in self.browser_names:
                     return n
                    
-    def __init__(self, evt, browser_names=[]):
+    def __init__(self, evt, browser_names=[], test_urls=[]):
         self.evt = evt
+        self.test_urls = test_urls
         try:
             self.browsers = BrowserRunner.browsers_by_os(platform.system())
         except KeyError:
             sys.stderr.write('Unknown platform "%s".\n' % platform.system())
             sys.exit(errno.EOPNOTSUPP)
         self.browser_iter = BrowserRunner.BrowserControllerIter(self.browsers, browser_names)
         self.current_controller = None
         self.lock = threading.Lock()
@@ -418,97 +464,110 @@ class BrowserRunner(object):
         return t
 
     def browser_name(self):
         self.lock.acquire()
         browser_name = self.current_controller.browser_name
         self.lock.release()
         return browser_name
 
+    def next_test(self):
+        self.lock.acquire()
+        need_to_launch = not self.current_controller or not self.current_controller.next_test()
+        self.lock.release()
+        if need_to_launch:
+            self.launch_next_browser()
+
     def launch_next_browser(self):
         self.lock.acquire()
         if self.current_controller:
+            print 'Closing browser...'
             self.current_controller.terminate()
-            print 'Test running time: %s' % self.current_controller.execution_time()
-
+            print '%s test running time: %s' % (self.current_controller.browser_name, self.current_controller.execution_time())
         while True:
             try:
                 self.current_controller = self.browser_iter.next()
             except StopIteration:
                 self.evt.set()
                 self.lock.release()
                 return
+            self.current_controller.set_test_urls(self.test_urls)
             self.current_controller.init_browser()
             if self.current_controller.browser_exists():
-                print 'launching %s' % self.current_controller.browser_name
-                if self.current_controller.launch():
+                print 'Launching %s...' % self.current_controller.browser_name
+                if self.current_controller.next_test():
                     break
                 else:
-                    print 'failed to launch browser.'
+                    print 'Failed to launch browser.'
         self.lock.release()
 
 
 class TestRunnerHTTPServer(BaseHTTPServer.HTTPServer):
     
     def __init__(self, server_address, RequestHandlerClass, browser_runner):
         BaseHTTPServer.HTTPServer.__init__(self, server_address, RequestHandlerClass)
         self.browser_runner = browser_runner
         self.results = collections.defaultdict(lambda: collections.defaultdict(list))
-
+    
+    def standard_web_data(self):
+        return {'ip': config.local_ip}
+        
 
 class TestRunnerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
     
     def do_GET(self):
-        # Indicates that the tests on the current browser have finished.
-        print 'got pingback'
-        self.server.browser_runner.launch_next_browser()
-        text = '<html><body>Done tests; launching next browser...</body></html>'
-        try:
-            self.send_response(200)
-            self.send_header('Content-type', 'text/html')
-            self.send_header('Content-Length', str(len(text)))
-            self.end_headers()
-            self.wfile.write(text)
-        except socket.error:
-            # Browser was probably closed before we could send the response
-            pass
+        if self.path.startswith(config.local_test_base_path):
+            try:
+                url = config.server_html_url + self.path[len(config.local_test_base_path):]
+                u = urllib2.urlopen(url)
+            except urllib2.HTTPError, e:
+                self.send_response(e.getcode())
+            else:
+                data = u.read()
+                self.send_response(200)
+                for header, value in u.info().items():
+                    self.send_header(header, value)
+                self.end_headers()
+                self.wfile.write(data)
+        else:
+            self.send_response(404)
 
     def do_POST(self):
-        # Parse the form data posted
-        form = cgi.FieldStorage(
-            fp=self.rfile, 
-            headers=self.headers,
-            environ={'REQUEST_METHOD':'POST',
-                     'CONTENT_TYPE':self.headers['Content-Type'],
-                     })
-
-        # record results
-        web_data = json.loads(form['body'].value)
+        length = int(self.headers.getheader('content-length'))
+        web_data = json.loads(self.rfile.read(length))
         testname = web_data['testname']
-        self.server.results[self.server.browser_runner.browser_name()][testname].append(web_data['results'])
+        self.server.results[self.server.browser_runner.browser_name()][testname].extend(web_data['results'])
+        if not config.testmode and not config.noresults:
+            web_data.update(self.server.standard_web_data())
+            raw_data = json.dumps(web_data)
+            req = urllib2.Request(config.server_results_url, raw_data)
+            req.add_header('Content-Type', 'application/json; charset=utf-8')
+            req.add_header('Content-Length', len(raw_data))
+            try:
+                urllib2.urlopen(req)
+            except urllib2.HTTPError, e:
+                print '**ERROR sending results to server: %s' % e
+                print ''
         self.send_response(200)
         self.end_headers()
         self.wfile.write('<html></html>')
+        self.server.browser_runner.next_test()
 
-    def read_data(self):
-        data = ''
-        while True:
-            buf = self.rfile.read()
-            if buf == '':
-                break
-            data += buf
-        return data
+    def log_message(self, format, *args):
+        """ Suppress log output. """
+        return
 
 
 MAX_TEST_TIME = datetime.timedelta(seconds=60*10)
         
 def main():
     from optparse import OptionParser
     parser = OptionParser()
-    parser.add_option('-t', '--test', dest='testmode', action='store_true')
+    parser.add_option('-t', '--test', dest='tests', action='append', default=[])
+    parser.add_option('--testmode', dest='testmode', action='store_true')
     parser.add_option('-n', '--noresults', dest='noresults', action='store_true')
     (options, args) = parser.parse_args()
     config.read(options.testmode, options.noresults)
     
     def get_browser_arg():
         try:
             browser = args[1]
         except IndexError:
@@ -522,33 +581,60 @@ def main():
         BrowserRunner(evt).archive_current_profiles(browser)
         sys.exit(0)
     elif len(args) >= 1 and args[0] == 'load':
         browser = get_browser_arg()
         BrowserRunner(evt).launch(browser, 'http://google.ca')
         sys.exit(0)
     
     # start tests in specified browsers.  if none given, run all.
-    br = BrowserRunner(evt, args)
+    url_prefix = config.local_test_base_url + '/start.html?ip=%s&port=%d' % (config.local_ip, config.local_port)
+    if config.testmode:
+        url_prefix += '&test=true'
+    url_prefix += '&testUrl='
+    if not options.tests:
+        print 'Getting test list from server...'
+        try:
+            tests_url = config.server_api_url + '/tests/'
+            print 'Getting test list from %s...' % tests_url
+            options.tests = json.loads(urllib2.urlopen(tests_url).read())
+        except urllib2.HTTPError, e:
+            sys.stderr.write('Could not get test list: %s\n' % e)
+            sys.exit(errno.EPERM)
+        except urllib2.URLError, e:
+            sys.stderr.write('Could not get test list: %s\n' % e.reason)
+            sys.exit(e.reason.errno)
+
+    test_urls = map(lambda x: url_prefix + x, options.tests)
+    br = BrowserRunner(evt, args, test_urls)
     trs = TestRunnerHTTPServer(('', config.local_port), TestRunnerRequestHandler, br)
     server_thread = threading.Thread(target=trs.serve_forever)
     server_thread.start()
+    start = datetime.datetime.now()
     br.launch_next_browser()
     while not evt.is_set():
         if br.browser_running():
+            if evt.is_set():
+                # evt may have been set while we were waiting for the lock in browser_running().
+                break
             if br.execution_time() > MAX_TEST_TIME:
-                print 'test has taken too long; starting next browser'
+                print 'Test has taken too long; starting next browser'
                 br.launch_next_browser()
         else:
-            print 'browser isn\'t running!'
+            print 'Browser isn\'t running!'
             br.launch_next_browser()
         evt.wait(5)
     trs.shutdown()
     server_thread.join()
+    end = datetime.datetime.now()
+    print ''
     print 'Done!'
-    #report = results.SpeedTestReport(trs.results)
-    #print 'Test results:'
-    #print ''
-    #print report.report()
+    report = results.SpeedTestReport(trs.results)
+    print
+    print 'Start: %s' % start
+    print 'Duration: %s' % (end - start)
+    print 'Client: %s' % config.local_ip
+    print
+    print report.report()
 
 
 if __name__ == '__main__':
     main()
--- a/html/js/speedtests.js
+++ b/html/js/speedtests.js
@@ -1,10 +1,8 @@
-DYNAMIC_SERVER_URL = "http://" + window.location.hostname + '/speedtestssvr';
-
 var SpeedTests = function() {
 
   var loadingNextTest = false;
   var all_results = [];
   var startTime = null;
   var lastReportTime = null;
   
   var isoDateTime = function (d) {
@@ -19,51 +17,36 @@ var SpeedTests = function() {
 
   var recordResults = function (testname, results) {
     results.browser_width = window.innerWidth;
     results.browser_height = window.innerHeight;
     results.teststart = isoDateTime(startTime);
     all_results.push(results);
   };
 
-//  var crossDomainPost = function NetUtils_crossDomainPost(url, values, callback) {
-//    if (!arguments.callee.c)
-//      arguments.callee.c = 1;
-//    var iframeName = "iframe" + arguments.callee.c++;
-//    var iframe = $("<iframe></iframe>").hide().attr("name", iframeName).appendTo("body");
-//    var form = $("<form></form>").hide().attr({ action: url, method: "post", target: iframeName }).appendTo("body");
-//    for (var i in values) {
-//      $("<input type='hidden'>").attr({ name: i, value: values[i]}).appendTo(form);
-//    }
-//    form.get(0).submit();
-//    form.remove();
-//    iframe.get(0).onload = function crossDomainIframeLoaded() {
-//      callback();
-//      setTimeout(function () { iframe.remove(); }, 0);
-//    }
-//  };
-
   var getSearchParams = function() {
     var params = document.location.search.slice(1).split("&");
     var args = new Object();
-    var l;
-    for (var i = 0; i < params.length; i++) {
-      l = params[i].split("=");
-      if (l.length != 2) {
+    for (p in params) {
+      var l = params[p].split("=");
+      for (var i = 0; i < l.length; i++) {
+        l[i] = decodeURIComponent(l[i]);
+      }
+      if (l.length != 2)
         continue;
-      }
-      args[decodeURIComponent(l[0])] = decodeURIComponent(l[1]);
+      args[l[0]] = l[1];
     }
     return args;
   };
   
   return {
     init: function() {
       startTime = new Date();
     },
+    getSearchParams: getSearchParams,
     isoDateTime: isoDateTime,
     recordResults: recordResults,
     periodicRecordResults: function (testname, resultFunc) {
       var now = new Date();
       var etms = now - startTime;
       if (lastReportTime == null || now - lastReportTime > 5000) {
         lastReportTime = now;
         var results = resultFunc();
@@ -73,39 +56,28 @@ var SpeedTests = function() {
       return etms;
     },
     nextTest: function (testname) {
       if (loadingNextTest) {
         return;
       }
       loadingNextTest = true;
       var searchParams = getSearchParams();
-      if (searchParams.noresults === undefined) {
-        var results = {
-          testname: testname,
-          ip: searchParams.ip,
-          results: all_results,
-          ua: navigator.userAgent
-        };
-        if (searchParams.test !== undefined) {
-          results.test = true;
-        }
-        var body = JSON.stringify(results);
-        var req = new XMLHttpRequest();
-        req.open("POST", DYNAMIC_SERVER_URL + "/testresults/", false);
-        req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
-        req.setRequestHeader("Content-length", body.length);
-        req.setRequestHeader("Connection", "close");
-        req.send(body);
+      if (!searchParams.ip) {
+        alert("Can't submit test results: no local IP provided.");
+        return;
+      }
+      if (!searchParams.port) {
+        alert("Can't submit test results: no local port provided.");
+        return;
       }
-
-      var url = DYNAMIC_SERVER_URL + "/nexttest/" + testname + "/" +
-                document.location.search;
-      window.location.assign(url);
-//      var local_url = 'http://' + searchParams.ip + ':' + searchParams.port + '/';
-//      crossDomainPost(local_url, {body: body}, function () {
-//        var url = DYNAMIC_SERVER_URL + "/nexttest/" + testname + "/" +
-//                  document.location.search;
-//        window.location.assign(url);
-//      });
+      var body = JSON.stringify({ testname: testname, ip: searchParams.ip,
+                                  results: all_results,
+                                  ua: navigator.userAgent });
+      var req = new XMLHttpRequest();
+      req.open("POST", "http://" + searchParams.ip + ":" + searchParams.port + "/", false);
+      req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+      req.setRequestHeader("Content-length", body.length);
+      req.setRequestHeader("Connection", "close");
+      req.send(body);
     }
   };
 }();
--- a/server/speedtests_server.py
+++ b/server/speedtests_server.py
@@ -1,184 +1,13 @@
 #!/usr/bin/env python2.6
-import ConfigParser
-import json
-import os
-import re
+import templeton.handlers
+import templeton.middleware
+import handlers
 import web
-import speedtests
-import urllib2
-
-TESTS_DIR = 'speedtests'
-
-DEFAULT_CONF_FILE = 'speedtests_server.conf'
-cfg = ConfigParser.ConfigParser()
-cfg.read(DEFAULT_CONF_FILE)
-try:
-    HTML_URL = cfg.get('speedtests', 'html_url')
-except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
-    HTML_URL = 'http://192.168.1.101/speedtests'
-try:
-    HTML_DIR = cfg.get('speedtests', 'html_dir')
-except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
-    HTML_DIR = os.path.join('..', 'html')
-try:
-    SERVER_URL = cfg.get('speedtests', 'server_url')
-except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
-    SERVER_URL = 'http://192.168.1.101/speedtestssvr'
-try:
-    PROXY_TO = cfg.get('speedtests', 'proxy')
-except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
-    PROXY_TO = None
-try:
-    RESULTS_ONLY = cfg.get('speedtests', 'results only')
-except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
-    RESULTS_ONLY = False
-
-urls = ['/testresults/', 'TestResults']
-if not RESULTS_ONLY:
-    urls.extend([
-        '/nexttest/(.*)', 'NextTest',
-        '/start/', 'StartTests',
-        '/done/', 'DoneTests'
-        ])
-
-
-def query_params():
-    params = {}
-    if web.ctx.query:
-        for q in web.ctx.query[1:].split('&'):
-            name, equals, value = q.partition('=')
-            if equals:
-                params[name] = value
-    return params
-                    
-
-class NextTest(object):
-    
-    def GET(self, current_testname):
-        params = query_params()
-        # The start page is new and launches the tests in a new window with a
-        # set size.  For anyone going straight to /nexttest/, we'll redirect
-        # them to the start page.  The start page will include the search
-        # string 'runtests=true' to actually load the next test. 
-        if not params.get('runtests', False):
-            raise web.seeother('%s/start/' % SERVER_URL)
-
-        tests = filter(lambda x: os.path.exists(os.path.join(HTML_DIR, x, 'Default.html')), os.listdir(HTML_DIR))
-        tests.sort()
-        for t in tests:
-            if t > current_testname:
-                if params.get('test', False):
-                    testpage = web.template.frender('templates/test.html')
-                    return testpage(t, HTML_URL)
-                else:
-                    raise web.seeother('%s/%s/Default.html%s' % (HTML_URL, t, web.ctx.query))
-        if params.get('auto', False):
-            # Redirect to the local server to start the next browser.
-            # We can't use localhost here because IE has issues connecting to the server via
-            # localhost.
-            raise web.seeother('http://%s:%s/' % (params['ip'], params['port']))
-        raise web.seeother('%s/done/' % SERVER_URL) 
-
 
-def get_browser_id(ua):
-    ua = ua.lower()
-    platform = 'unknown'
-    geckover = 'n/a'
-    buildid = 'unknown'
-    browserid = 0
-    
-    if 'firefox' in ua:
-        bname = 'Firefox'
-        m = re.match('[^\(]*\((.*) rv:([^\)]*)\) gecko/([^ ]+) firefox/(.*)',
-                     ua)
-        platform = m.group(1).replace(';', '').strip()
-        geckover = m.group(2)
-        buildid = m.group(3)
-        bver = m.group(4)
-    elif 'msie' in ua:
-        bname = 'Internet Explorer'
-        m = re.search('msie ([^;]*);([^\)]*)\)', ua)
-        bver = m.group(1)
-        platform = m.group(2).replace(';', '').strip()
-    elif 'chrome' in ua:
-        bname = 'Chrome'
-        m = re.match('mozilla/[^ ]* \(([^\)]*)\).*chrome/([^ ]*)', ua)
-        platform = m.group(1).strip()
-        bver = m.group(2)
-    elif 'safari' in ua:
-        bname = 'Safari'
-        m = re.match('[^\(]*\(([^\)]*)\).*safari/(.*)', ua)
-        platform = m.group(1)
-        # 64-bit builds have an extra part separated by a semicolon.
-        # Strip it off here rather than making the re much more complicated.
-        delim = platform.find(';')
-        if delim != -1:
-            platform = platform[:delim]
-        bver = m.group(2)
-    elif 'opera' in ua:
-        bname = 'Opera'
-        m = re.match('[^\(]*\(([^;]*);[^\)]*\).*version/(.*)', ua)
-        platform = m.group(1).strip()
-        if platform == 'x11':
-            platform = 'linux'
-        bver = m.group(2).strip()
-    
-    wheredict = {
-        'browsername': bname,
-        'browserversion': bver,
-        'platform': platform,
-        'geckoversion': geckover,
-        'buildid': buildid
-        }
-    browser = db.select('browser', where=web.db.sqlwhere(wheredict))
-    if not browser:
-        db.insert('browser', **wheredict)
-        browser = db.select('browser', where=web.db.sqlwhere(wheredict))
-    return browser[0].id
-        
+urls = templeton.handlers.load_urls(handlers.urls)
 
-class TestResults(object):
-    
-    def POST(self):
-        if PROXY_TO:
-            headers = {
-                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
-                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
-                'Accept-Encoding': 'gzip, deflate'
-            }
-            request = urllib2.Request(PROXY_TO, web.data(), headers)
-            response = urllib2.urlopen(request, timeout=120).read()
-            return
-        web_data = json.loads(web.data())
-        machine_ip = web_data['ip']
-        testname = web_data['testname']
-        browser_id = get_browser_id(web_data['ua'])
-        for results in web_data['results']:
-            results['browser_id'] = browser_id
-    	    results['ip'] = machine_ip
-            cols = {}
-            for k, v in results.iteritems():
-                cols[k.encode('ascii')] = v
-            db.insert(testname, **cols)
+app = web.application(urls, handlers.__dict__)
 
 
-class StartTests(object):
-    
-    def GET(self):
-        start = web.template.frender('templates/start.html')
-        return start(SERVER_URL, HTML_URL)
-
-
-class DoneTests(object):
-    
-    def GET(self):
-        done = web.template.frender('templates/done.html')
-        return done(SERVER_URL, HTML_URL)
-
-
-db = web.database(dbn='mysql', db='speedtests', user='speedtests',
-                  pw='speedtests')
-app = web.application(urls, globals())
-
 if __name__ == '__main__':
     app.run()
deleted file mode 100644
--- a/server/templates/done.html
+++ /dev/null
@@ -1,13 +0,0 @@
-$def with (svrurl, htmldir)
-<html>
-<head>
-  <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
-  <title>All tests complete!</title>
-</head>
-<body>
-<p>
-All tests have been completed and the results recorded.
-</p>
-<script>window.close();</script>
-</body>
-</html>
deleted file mode 100644
--- a/server/templates/start.html
+++ /dev/null
@@ -1,35 +0,0 @@
-$def with (svrurl, htmldir)
-<html>
-<head>
-  <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
-  <title>Mozilla SpeedTest Runner</title>
-  <script type="text/javascript" src="$htmldir/js/jquery.min.js"></script>
-</head>
-<body>
-<div id="main">
-<p>
-SpeedTests go!
-</p>
-</div>
-<script>
-$$(document).ready(function() {
-  
-  var search = '';
-  if (document.location.search) {
-    search = '&' + document.location.search.slice(1);
-  }
-  
-  var w = window.open('$svrurl/nexttest/?runtests=true' + search, 'testWindow',
-      'resizable=no,status=no,top=0,left=0,height=768,width=1024,menubar=no,toolbar=no,location=no,personalbar=no');
-  
-  var iId = setInterval(function() {
-    if (w.closed) {
-      clearInterval(iId);
-      $$("#main").html('<p>SpeedTests are done!</p><p><a href="$svrurl/start/">Run them again</a>.</p>');
-    }
-  }, 1000);
-  
-});
-</script>
-</body>
-</html>
--- a/server/templates/test.html
+++ b/server/templates/test.html
@@ -1,22 +1,22 @@
-$def with (testname, htmlurl)
+$def with (testname)
 <html>
 <head>
   <meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
   <title>TEST $testname</title>
 </head>
 <body>
 <div id="main">
 <p>TEST $testname</p>
 </div>
 
-<script type="text/javascript" src="$htmlurl/js/jquery.min.js"></script>
-<script type="text/javascript" src="$htmlurl/js/json2.js"></script>
-<script type="text/javascript" src="$htmlurl/js/speedtests.js"></script>
+<script type="text/javascript" src="../../js/jquery.min.js"></script>
+<script type="text/javascript" src="../../js/json2.js"></script>
+<script type="text/javascript" src="../../js/speedtests.js"></script>
 <script>
 $$(document).ready(function() {
     window.setTimeout(function() {
         $$("#main").append("<p>TEST Submitting results and loading next test...</p>");
         SpeedTests.nextTest("$testname");
     }, 2000);
 });
 </script>