Bug 738825 - mirror Marionette Python client into gecko, a=testonly (npotb)
authorJonathan Griffin <jgriffin@mozilla.com>
Mon, 26 Mar 2012 12:40:25 -0700
changeset 90332 55597f876d5e1bdc9d29b64592705cc95563593e
parent 90331 940563684bef47e0f9556774214ba568de13ed79
child 90333 79f4d3ddee57875286b8fc3d627fd42238ed8052
push id7601
push userjgriffin@mozilla.com
push dateMon, 26 Mar 2012 19:42:54 +0000
treeherdermozilla-inbound@55597f876d5e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstestonly
bugs738825
milestone14.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 738825 - mirror Marionette Python client into gecko, a=testonly (npotb)
dom/battery/test/marionette/manifest.ini
dom/battery/test/marionette/test_battery.py
dom/telephony/test/marionette/manifest.ini
dom/telephony/test/marionette/test_dial_answer.py
dom/telephony/test/marionette/test_dial_between_emulators.py
dom/telephony/test/marionette/test_dial_listeners.py
testing/marionette/client/README.md
testing/marionette/client/marionette/__init__.py
testing/marionette/client/marionette/automation.conf
testing/marionette/client/marionette/automator.py
testing/marionette/client/marionette/client.py
testing/marionette/client/marionette/emulator.py
testing/marionette/client/marionette/emulator_battery.py
testing/marionette/client/marionette/errors.py
testing/marionette/client/marionette/gitutils.py
testing/marionette/client/marionette/marionette.py
testing/marionette/client/marionette/marionette_test.py
testing/marionette/client/marionette/runtests.py
testing/marionette/client/marionette/selenium_proxy.py
testing/marionette/client/marionette/test_debugger.py
testing/marionette/client/marionette/test_emulator.py
testing/marionette/client/marionette/test_protocol.py
testing/marionette/client/marionette/test_selenium.py
testing/marionette/client/marionette/tests/head.js
testing/marionette/client/marionette/tests/unit-tests.ini
testing/marionette/client/marionette/tests/unit/test_click.py
testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
testing/marionette/client/marionette/tests/unit/test_execute_isolate.py
testing/marionette/client/marionette/tests/unit/test_execute_script.py
testing/marionette/client/marionette/tests/unit/test_findelement.py
testing/marionette/client/marionette/tests/unit/test_log.py
testing/marionette/client/marionette/tests/unit/test_navigation.py
testing/marionette/client/marionette/tests/unit/test_simpletest_chrome.js
testing/marionette/client/marionette/tests/unit/test_simpletest_fail.js
testing/marionette/client/marionette/tests/unit/test_simpletest_pass.js
testing/marionette/client/marionette/tests/unit/test_simpletest_sanity.py
testing/marionette/client/marionette/tests/unit/test_simpletest_timeout.js
testing/marionette/client/marionette/tests/unit/test_switch_frame.py
testing/marionette/client/marionette/tests/unit/test_switch_frame_b2g.py
testing/marionette/client/marionette/tests/unit/test_window_management.py
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/marionette/client/marionette/testserver.py
testing/marionette/client/marionette/venv_automation.sh
testing/marionette/client/marionette/venv_test.sh
testing/marionette/client/marionette/www/test.html
testing/marionette/client/marionette/www/test_iframe.html
testing/marionette/client/setup.py
new file mode 100644
--- /dev/null
+++ b/dom/battery/test/marionette/manifest.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+b2g = true
+browser = false
+qemu = true
+
+[test_battery.py]
+
new file mode 100644
--- /dev/null
+++ b/dom/battery/test/marionette/test_battery.py
@@ -0,0 +1,99 @@
+import unittest
+from marionette_test import MarionetteTestCase
+
+
+class BatteryTest(MarionetteTestCase):
+
+    @unittest.expectedFailure
+    def test_chargingchange(self):
+        marionette = self.marionette
+        self.assertTrue(marionette.emulator.is_running)
+        marionette.set_script_timeout(10000)
+
+        moz_charging = marionette.execute_script("return navigator.mozBattery.charging;")
+        emulator_charging = marionette.emulator.battery.charging
+        self.assertEquals(moz_charging, emulator_charging)
+
+        # setup event listeners to be notified when the level or charging status 
+        # changes
+        self.assertTrue(marionette.execute_script("""
+        window.wrappedJSObject._chargingchanged = false;
+        navigator.mozBattery.addEventListener("chargingchange", function() {
+            window.wrappedJSObject._chargingchanged = true;
+        });
+        return true;
+    """))
+
+        # set the battery charging state, and verify
+        marionette.emulator.battery.charging = not emulator_charging
+        new_emulator_charging_state = marionette.emulator.battery.charging
+        self.assertEquals(new_emulator_charging_state, (not emulator_charging))
+
+        # verify that the 'chargingchange' listener was hit
+        charging_changed = marionette.execute_async_script("""
+        var callback = arguments[arguments.length - 1];
+        function check_charging_change() {
+            if (window.wrappedJSObject._chargingchanged) {
+                callback(window.wrappedJSObject._chargingchanged);
+            }
+            else {
+                setTimeout(check_charging_change, 500);
+            }
+        }
+        setTimeout(check_charging_change, 0);
+    """)
+        self.assertTrue(charging_changed)
+
+        # if we have set the charging state to 'off', set it back to 'on' to prevent
+        # the emulator from sleeping
+        if not new_emulator_charging_state:
+            marionette.emulator.battery.charging = True
+
+    def test_levelchange(self):
+        marionette = self.marionette
+        self.assertTrue(marionette.emulator.is_running)
+        marionette.set_script_timeout(10000)
+
+        # verify the emulator's battery status as reported by Gecko is the same as
+        # reported by the device
+        moz_level = marionette.execute_script("return navigator.mozBattery.level;")
+        self.assertEquals(moz_level, marionette.emulator.battery.level)
+
+        # setup event listeners to be notified when the level or charging status 
+        # changes
+        self.assertTrue(marionette.execute_script("""
+        window.wrappedJSObject._levelchanged = false;
+        navigator.mozBattery.addEventListener("levelchange", function() {
+            window.wrappedJSObject._levelchanged = true;
+        });
+        return true;
+    """))
+
+        # set the battery to a new level, and verify
+        if moz_level > 0.2:
+            new_level = moz_level - 0.1
+        else:
+            new_level = moz_level + 0.1
+        marionette.emulator.battery.level = new_level
+
+        # XXX: do we need to wait here a bit?  this WFM...
+        moz_level = marionette.emulator.battery.level
+        self.assertEquals(int(new_level * 100), int(moz_level * 100))
+
+        # verify that the 'levelchange' listener was hit
+        level_changed = marionette.execute_async_script("""
+        var callback = arguments[arguments.length - 1];
+        function check_level_change() {
+            if (window.wrappedJSObject._levelchanged) {
+                callback(window.wrappedJSObject._levelchanged);
+            }
+            else {
+                setTimeout(check_level_change, 500);
+            }
+        }
+        setTimeout(check_level_change, 0);
+    """)
+        self.assertTrue(level_changed)
+
+
+
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/marionette/manifest.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+b2g = true
+browser = false
+qemu = true
+
+[test_dial_answer.py]
+[test_dial_between_emulators.py]
+[test_dial_listeners.py]
+
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/marionette/test_dial_answer.py
@@ -0,0 +1,89 @@
+from marionette_test import *
+
+
+class MultiEmulatorDialTest(MarionetteTestCase):
+    """A simple test which verifies the ability of one emulator to dial
+       another and to detect an incoming call.
+    """
+
+    def test_dial_answer(self):
+        # Tests always have one emulator available as self.marionette; we'll
+        # use this for the receiving emulator.  We'll also launch a second
+        # emulator to use as the sender.
+        sender = self.get_new_emulator()
+        receiver = self.marionette
+
+        # Setup the event listsener on the receiver, which should store
+        # a global variable when an incoming call is received.
+        receiver.set_context("chrome")
+        self.assertTrue(receiver.execute_script("""
+return navigator.mozTelephony != undefined && navigator.mozTelephony != null;
+"""))
+        receiver.execute_script("""
+window.wrappedJSObject.incoming = null;
+navigator.mozTelephony.addEventListener("incoming", function test_incoming(e) {
+    navigator.mozTelephony.removeEventListener("incoming", test_incoming);
+    window.wrappedJSObject.incoming = e.call;
+});
+""")
+
+        # Dial the receiver from the sender.
+        toPhoneNumber = "1555521%d" % receiver.emulator.port
+        fromPhoneNumber = "1555521%d" % sender.emulator.port
+        sender.set_context("chrome")
+        sender.execute_script("""
+window.wrappedJSObject.call = navigator.mozTelephony.dial("%s");
+""" % toPhoneNumber)
+
+        # On the receiver, wait up to 30s for an incoming call to be 
+        # detected, by checking the value of the global var that the 
+        # listener will change.
+        receiver.set_script_timeout(30000)
+        received = receiver.execute_async_script("""
+window.wrappedJSObject.callstate = null;
+waitFor(function() {
+    let call = window.wrappedJSObject.incoming;
+    call.addEventListener("connected", function test_connected(e) {
+        call.removeEventListener("connected", test_connected);
+        window.wrappedJSObject.callstate = e.call.state;
+    });
+    marionetteScriptFinished(call.number);
+},
+function() {
+    return window.wrappedJSObject.incoming != null;
+});
+""")
+        # Verify the phone number of the incoming call.
+        self.assertEqual(received, fromPhoneNumber)
+
+        # On the sender, add a listener to verify that the call changes
+        # state to connected when it's answered.
+        sender.execute_script("""
+let call = window.wrappedJSObject.call;
+window.wrappedJSObject.callstate = null;
+call.addEventListener("connected", function test_connected(e) {
+    call.removeEventListener("connected", test_connected);
+    window.wrappedJSObject.callstate = e.call.state;
+});
+""")
+
+        # Answer the call and verify that the callstate changes to
+        # connected.
+        receiver.execute_async_script("""
+window.wrappedJSObject.incoming.answer();
+waitFor(function() {
+    marionetteScriptFinished(true);
+}, function() {
+    return window.wrappedJSObject.callstate == "connected";
+});
+""")
+
+        # Verify that the callstate changes to connected on the caller as well.
+        self.assertTrue(receiver.execute_async_script("""
+waitFor(function() {
+    marionetteScriptFinished(true);
+}, function() {
+    return window.wrappedJSObject.callstate == "connected";
+});
+"""))
+
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/marionette/test_dial_between_emulators.py
@@ -0,0 +1,55 @@
+from marionette_test import *
+
+
+class MultiEmulatorDialTest(MarionetteTestCase):
+    """A simple test which verifies the ability of one emulator to dial
+       another and to detect an incoming call.
+    """
+
+    def test_dial_between_emulators(self):
+        # Tests always have one emulator available as self.marionette; we'll
+        # use this for the receiving emulator.  We'll also launch a second
+        # emulator to use as the sender.
+        sender = self.get_new_emulator()
+        receiver = self.marionette
+
+        # Setup the event listsener on the receiver, which should store
+        # a global variable when an incoming call is received.
+        receiver.set_context("chrome")
+        self.assertTrue(receiver.execute_script("""
+return navigator.mozTelephony != undefined && navigator.mozTelephony != null;
+"""))
+        receiver.execute_script("""
+window.wrappedJSObject.incoming = "none";
+navigator.mozTelephony.addEventListener("incoming", function(e) {
+    window.wrappedJSObject.incoming = e.call.number;
+});
+""")
+
+        # Dial the receiver from the sender.
+        toPhoneNumber = "1555521%d" % receiver.emulator.port
+        fromPhoneNumber = "1555521%d" % sender.emulator.port
+        sender.set_context("chrome")
+        sender.execute_script("""
+navigator.mozTelephony.dial("%s");
+""" % toPhoneNumber)
+
+        # On the receiver, wait up to 30s for an incoming call to be 
+        # detected, by checking the value of the global var that the 
+        # listener will change.
+        receiver.set_script_timeout(30000)
+        received = receiver.execute_async_script("""
+        function check_incoming() {
+            if (window.wrappedJSObject.incoming != "none") {
+                marionetteScriptFinished(window.wrappedJSObject.incoming);
+            }
+            else {
+                setTimeout(check_incoming, 500);
+            }
+        }
+        setTimeout(check_incoming, 0);
+    """)
+        # Verify the phone number of the incoming call.
+        self.assertEqual(received, fromPhoneNumber)
+
+
new file mode 100644
--- /dev/null
+++ b/dom/telephony/test/marionette/test_dial_listeners.py
@@ -0,0 +1,149 @@
+from marionette_test import *
+
+
+class DialListenerTest(MarionetteTestCase):
+    """A test of some of the different listeners for nsIDOMTelephonyCall.
+    """
+
+    def test_dial_listeners(self):
+        # Tests always have one emulator available as self.marionette; we'll
+        # use this for the receiving emulator.  We'll also launch a second
+        # emulator to use as the sender.
+        sender = self.get_new_emulator()
+        receiver = self.marionette
+
+        receiver.set_script_timeout(30000)
+        sender.set_script_timeout(30000)
+        receiver.set_context("chrome")
+        sender.set_context("chrome")
+        toPhoneNumber = "1555521%d" % receiver.emulator.port
+        fromPhoneNumber = "1555521%d" % sender.emulator.port
+
+        # Setup the event listsener on the receiver, which should store
+        # a global variable when an incoming call is received.
+        self.assertTrue(receiver.execute_script("""
+return navigator.mozTelephony != undefined && navigator.mozTelephony != null;
+"""))
+        receiver.execute_script("""
+window.wrappedJSObject.incoming = null;
+navigator.mozTelephony.addEventListener("incoming", function test_incoming(e) {
+    navigator.mozTelephony.removeEventListener("incoming", test_incoming);
+    window.wrappedJSObject.incoming = e.call;
+});
+""")
+
+        # dial the receiver from the sender
+        sender.execute_script("""
+window.wrappedJSObject.sender_state = [];
+window.wrappedJSObject.sender_call = navigator.mozTelephony.dial("%s");
+window.wrappedJSObject.sender_call.addEventListener("statechange", function test_sender_statechange(e) {
+    if (e.call.state == 'disconnected')
+        window.wrappedJSObject.sender_call.removeEventListener("statechange", test_sender_statechange);
+    window.wrappedJSObject.sender_state.push(e.call.state);
+});
+window.wrappedJSObject.sender_alerting = false;
+window.wrappedJSObject.sender_call.addEventListener("alerting", function test_sender_alerting(e) {
+    window.wrappedJSObject.sender_call.removeEventListener("alerting", test_sender_alerting);
+    window.wrappedJSObject.sender_alerting = e.call.state == 'alerting';
+});
+""" % toPhoneNumber)
+
+        # On the receiver, wait up to 30s for an incoming call to be 
+        # detected, by checking the value of the global var that the 
+        # listener will change.
+        received = receiver.execute_async_script("""
+window.wrappedJSObject.receiver_state = [];
+waitFor(function() {
+    let call = window.wrappedJSObject.incoming;
+    call.addEventListener("statechange", function test_statechange(e) {
+        if (e.call.state == 'disconnected')
+            call.removeEventListener("statechange", test_statechange);
+        window.wrappedJSObject.receiver_state.push(e.call.state);
+    });
+    call.addEventListener("connected", function test_connected(e) {
+        call.removeEventListener("connected", test_connected);
+        window.wrappedJSObject.receiver_connected = e.call.state == 'connected';
+    });
+    marionetteScriptFinished(call.number);
+},
+function() {
+    return window.wrappedJSObject.incoming != null;
+});
+""")
+        # Verify the phone number of the incoming call.
+        self.assertEqual(received, fromPhoneNumber)
+
+        # At this point, the sender's call should be in a 'alerting' state,
+        # as reflected by both 'statechange' and 'alerting' listeners.
+        self.assertTrue('alerting' in sender.execute_script("return window.wrappedJSObject.sender_state;"))
+        self.assertTrue(sender.execute_script("return window.wrappedJSObject.sender_alerting;"))
+
+        # Answer the call and verify that the callstate changes to
+        # connected.
+        receiver.execute_async_script("""
+window.wrappedJSObject.incoming.answer();
+waitFor(function() {
+    marionetteScriptFinished(true);
+}, function() {
+    return window.wrappedJSObject.receiver_connected;
+});
+""")
+        state = receiver.execute_script("return window.wrappedJSObject.receiver_state;")
+        self.assertTrue('connecting' in state)
+        self.assertTrue('connected' in state)
+
+        # verify that the callstate changes to connected on the caller as well
+        self.assertTrue('connected' in sender.execute_async_script("""
+waitFor(function() {
+    marionetteScriptFinished(window.wrappedJSObject.sender_state);
+}, function() {
+    return window.wrappedJSObject.sender_call.state == "connected";
+});
+"""))
+
+        # setup listeners to detect the 'disconnected event'
+        sender.execute_script("""
+window.wrappedJSObject.sender_disconnected = null;
+window.wrappedJSObject.sender_call.addEventListener("disconnected", function test_disconnected(e) {
+    window.wrappedJSObject.sender_call.removeEventListener("disconnected", test_disconnected);
+    window.wrappedJSObject.sender_disconnected = e.call.state == 'disconnected';
+});
+""")
+        receiver.execute_script("""
+window.wrappedJSObject.receiver_disconnected = null;
+window.wrappedJSObject.incoming.addEventListener("disconnected", function test_disconnected(e) {
+    window.wrappedJSObject.incoming.removeEventListener("disconnected", test_disconnected);
+    window.wrappedJSObject.receiver_disconnected = e.call.state == 'disconnected';
+});
+""")
+
+        # hang up from the receiver's side
+        receiver.execute_script("""
+window.wrappedJSObject.incoming.hangUp();
+""")
+
+        # Verify that the call state on the sender is 'disconnected', as
+        # notified by both the 'statechange' and 'disconnected' listeners.
+        sender_state = sender.execute_async_script("""
+waitFor(function() {
+    marionetteScriptFinished(window.wrappedJSObject.sender_state);
+}, function () {
+    return window.wrappedJSObject.sender_call.state == 'disconnected';
+});
+""")
+        self.assertTrue('disconnected' in sender_state)
+        self.assertTrue(sender.execute_script("return window.wrappedJSObject.sender_disconnected;"))
+
+        # Verify that the call state on the receiver is 'disconnected', as
+        # notified by both the 'statechange' and 'disconnected' listeners.
+        state = receiver.execute_async_script("""
+waitFor(function() {
+    marionetteScriptFinished(window.wrappedJSObject.receiver_state);
+}, function () {
+    return window.wrappedJSObject.incoming.state == 'disconnected';
+});
+""")
+        self.assertTrue('disconnected' in state)
+        self.assertTrue('disconnecting' in state)
+        self.assertTrue(receiver.execute_script("return window.wrappedJSObject.receiver_disconnected;"))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/README.md
@@ -0,0 +1,47 @@
+# Marionette Client
+
+[Marionette](https://developer.mozilla.org/en/Marionette) is a 
+Mozilla project to enable remote automation in Gecko-based projects,
+including desktop Firefox, mobile Firefox, and Boot-to-Gecko.
+
+It utilizes the [remote-debugger](https://wiki.mozilla.org/Platform/JSDebugv2) 
+inside Gecko for the transport layer of the Marionette server.  The commands
+the Marionette server will eventually implement are based on
+Selenium's [JSON Wire Protocol](http://code.google.com/p/selenium/wiki/JsonWireProtocol),
+although not all commands are presently implemented, and additional commands
+will likely be added.
+
+## Package Files
+
+- client.py:  This is the Marionette socket client; it speaks the same
+  socket protocol as the Gecko remote debugger.
+- marionette.py:  The Marionette client.  This uses client.py to communicate
+  with a server that speaks the Gecko remote debugger protocol.
+- selenium_proxy.py:  Acts as a remote driver for Selenium test runners.
+  This code translates the Selenium 
+  [JSON Wire Protocol](http://code.google.com/p/selenium/wiki/JsonWireProtocol)
+  to the [Marionette JSON Protocol](https://wiki.mozilla.org/Auto-tools/Projects/Marionette/JSON_Protocol).
+  This allows Selenium tests to utilize Marionette.
+- testserver.py:  A socket server which mimics the remote debugger in
+  Gecko, and can be used to test pieces of the Marionette client.
+- test_protocol.py:  Tests the Marionette JSON Protocol by using testserver.py.
+- test_selenium.py:  Tests the Selenium proxy by using testserver.py.
+
+## Installation
+
+You'll need the ManifestDestiny and MozHttpd packages from Mozbase:
+
+    git clone git://github.com/mozilla/mozbase.git
+    cd mozbase
+    python setup_development.py
+
+Other than that, there are no special requirements, unless you're using the Selenium proxy, in which
+case you'll need to install the Selenium Python bindings using:
+
+    pip install selenium
+
+## Writing and Running Tests Using Marionette
+
+See [Writing Marionette tests](https://developer.mozilla.org/en/Marionette/Tests),
+and [Running Marionette tests](https://developer.mozilla.org/en/Marionette/Running_Tests).
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/__init__.py
@@ -0,0 +1,3 @@
+from marionette import Marionette, HTMLElement
+from marionette_test import MarionetteTestCase
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/automation.conf
@@ -0,0 +1,8 @@
+[marionette]
+es_server = buildbot-es.metrics.sjc1.mozilla.com:9200
+rest_server = http://brasstacks.mozilla.com/autologserver/
+
+[tests]
+marionette = tests/unit-tests.ini
+marionette-gaia = $homedir$/gaia/tests
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/automator.py
@@ -0,0 +1,218 @@
+# This function will run the pulse build watcher,
+# then on detecting a build, it will run the tests
+# using that build.
+
+import ConfigParser
+import json
+import logging
+import os
+import sys
+import traceback
+import urllib
+import mozlog
+import shutil
+from optparse import OptionParser
+from threading import Thread, RLock
+from manifestparser import TestManifest
+from runtests import MarionetteTestRunner
+from marionette import Marionette
+from mozinstall import install
+
+from mozillapulse.config import PulseConfiguration
+from mozillapulse.consumers import GenericConsumer
+
+
+class B2GPulseConsumer(GenericConsumer):
+    def __init__(self, **kwargs):
+        super(B2GPulseConsumer, self).__init__(PulseConfiguration(**kwargs),
+                                               'org.mozilla.exchange.b2g',
+                                               **kwargs)
+
+
+class B2GAutomation:
+    def __init__(self, tests, testfile=None,
+                 es_server=None, rest_server=None, testgroup='marionette'):
+        self.logger = mozlog.getLogger('B2G_AUTOMATION')
+        self.tests = tests
+        self.testfile = testfile
+        self.es_server = es_server
+        self.rest_server = rest_server
+        self.testgroup = testgroup
+        self.lock = RLock()
+
+        self.logger.info("Testlist: %s" % self.tests)
+
+        pulse = B2GPulseConsumer(applabel='b2g_build_listener')
+        pulse.configure(topic='#', callback=self.on_build)
+
+        if not self.testfile:
+            self.logger.info('waiting for pulse messages...')
+            pulse.listen()
+        else:
+            t = Thread(target=pulse.listen)
+            t.daemon = True
+            t.start()
+            f = open(self.testfile, 'r')
+            data = json.loads(f.read())
+            self.on_build(data, None)
+
+    def get_test_list(self, manifest):
+        self.logger.info("Reading test manifest: %s" % manifest)
+        mft = TestManifest()
+        mft.read(manifest)
+
+        # In the future if we want to add in more processing to the manifest
+        # here is where you'd do that. Right now, we just return a list of
+        # tests
+        testlist = []
+        for i in mft.get():
+            testlist.append(i["path"])
+
+        return testlist
+
+    def on_build(self, data, msg):
+        # Found marionette build! Install it
+        if msg is not None:
+            msg.ack()
+        self.lock.acquire()
+
+        try:
+            self.logger.info("got pulse message! %s" % repr(data))
+            if "buildurl" in data["payload"]:
+                directory = self.install_build(data['payload']['buildurl'])
+                rev = data["payload"]["commit"]
+                if directory == None:
+                    self.logger.info("Failed to return build directory")
+                else:
+                    self.run_marionette(directory, rev)
+                    self.cleanup(directory)
+            else:
+                self.logger.error("Failed to find buildurl in msg, not running test")
+
+        except:
+            self.logger.exception("error while processing build")
+
+        self.lock.release()
+
+    # Download the build and untar it, return the directory it untared to
+    def install_build(self, url):
+        try:
+            self.logger.info("Installing build from url: %s" % url)
+            buildfile = os.path.abspath("b2gtarball.tar.gz")
+            urllib.urlretrieve(url, buildfile)
+        except:
+            self.logger.exception("Failed to download build")
+
+        try:
+            self.logger.info("Untarring build")
+            # Extract to the same local directory where we downloaded the build
+            # to.  This defaults to the local directory where our script runs
+            dest = os.path.join(os.path.dirname(buildfile), 'downloadedbuild')
+            if (os.access(dest, os.F_OK)):
+                shutil.rmtree(dest)
+            install(buildfile, dest)
+            # This should extract into a qemu directory
+            qemu = os.path.join(dest, 'qemu')
+            if os.path.exists(qemu):
+                return qemu
+            else:
+                return None
+        except:
+            self.logger.exception("Failed to untar file")
+        return None
+
+    def run_marionette(self, dir, rev):
+        self.logger.info("Starting test run for revision: %s" % rev)
+        runner = MarionetteTestRunner(emulator=True,
+                                      homedir=dir,
+                                      autolog=True,
+                                      revision=rev,
+                                      logger=self.logger,
+                                      es_server=self.es_server,
+                                      rest_server=self.rest_server,
+                                      testgroup=self.testgroup)
+        for test in self.tests:
+            manifest = test[1].replace('$homedir$', os.path.dirname(dir))
+            testgroup = test[0]
+            runner.testgroup = testgroup
+            runner.run_tests([manifest], 'b2g')
+
+    def cleanup(self, dir):
+        self.logger.info("Cleaning up")
+        if os.path.exists("b2gtarball.tar.gz"):
+            os.remove("b2gtarball.tar.gz")
+        if os.path.exists(dir):
+            shutil.rmtree(dir)
+
+def main():
+    parser = OptionParser(usage="%prog <options>")
+    parser.add_option("--config", action="store", dest="config_file",
+                      default="automation.conf",
+                      help="Specify the configuration file")
+    parser.add_option("--testfile", action="store", dest="testfile",
+                      help = "Start in test mode without using pulse, "
+                      "utilizing the pulse message defined in the specified file")
+    parser.add_option("--test-manifest", action="store", dest="testmanifest",
+                      default = os.path.join("tests","unit-tests.ini"),
+                      help="Specify the test manifest, defaults to tests/all-tests.ini")
+    parser.add_option("--log-file", action="store", dest="logfile",
+                      default="b2gautomation.log",
+                      help="Log file to store results, defaults to b2gautomation.log")
+
+    LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR")
+    LEVEL_STRING = ", ".join(LOG_LEVELS)
+    parser.add_option("--log-level", action="store", type="choice",
+                      dest="loglevel", default="DEBUG", choices=LOG_LEVELS,
+                      help = "One of %s for logging level, defaults  to debug" % LEVEL_STRING)
+    options, args = parser.parse_args()
+
+    cfg = ConfigParser.ConfigParser()
+    cfg.read(options.config_file)
+    try:
+        es_server = cfg.get('marionette', 'es_server')
+    except:
+        # let mozautolog provide the default
+        es_server = None
+    try:
+        rest_server = cfg.get('marionette', 'rest_server')
+    except:
+        # let mozautolog provide the default
+        rest_server = None
+
+    try:
+        tests = cfg.items('tests')
+    except:
+        tests = [('marionette', options.testmanifest)]
+
+    if not options.testmanifest:
+        parser.print_usage()
+        parser.exit()
+
+    if not os.path.exists(options.testmanifest):
+        print "Could not find manifest file: %s" % options.testmanifest
+        parser.print_usage()
+        parser.exit()
+
+    # Set up the logger
+    if os.path.exists(options.logfile):
+        os.remove(options.logfile)
+
+    logger = mozlog.getLogger("B2G_AUTOMATION", options.logfile)
+    if options.loglevel:
+        logger.setLevel(getattr(mozlog, options.loglevel, "DEBUG"))
+    logger.addHandler(logging.StreamHandler())
+
+    try:
+        b2gauto = B2GAutomation(tests,
+                                testfile=options.testfile,
+                                es_server=es_server,
+                                rest_server=rest_server)
+    except:
+        s = traceback.format_exc()
+        logger.error(s)
+        return 1
+    return 0
+
+if __name__ == "__main__":
+    main()
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/client.py
@@ -0,0 +1,118 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+import json
+import socket
+
+class MarionetteClient(object):
+    """ The Marionette socket client.  This speaks the same protocol
+        as the remote debugger inside Gecko, in which messages are
+        always preceded by the message length and a colon, e.g.,
+        
+        20:{'command': 'test'}
+    """
+
+    def __init__(self, addr, port):
+        self.addr = addr
+        self.port = port
+        self.sock = None
+        self.traits = None
+        self.applicationType = None
+        self.actor = 'root'
+
+    def _recv_n_bytes(self, n):
+        """ Convenience method for receiving exactly n bytes from
+            self.sock (assuming it's open and connected).
+        """
+        data = ''
+        while len(data) < n:
+            chunk = self.sock.recv(n - len(data))
+            if chunk == '':
+                break
+            data += chunk
+        return data
+
+    def receive(self):
+        """ Receive the next complete response from the server, and return
+            it as a dict.  Each response from the server is prepended by
+            len(message) + ':'.
+        """
+        assert(self.sock)
+        response = self.sock.recv(10)
+        sep = response.find(':')
+        length = response[0:sep]
+        response = response[sep + 1:]
+        response += self._recv_n_bytes(int(length) + 1 + len(length) - 10)
+        return json.loads(response)
+
+    def connect(self):
+        """ Connect to the server and process the hello message we expect
+            to receive in response.
+        """
+        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.sock.settimeout(180.0)
+        try:
+            self.sock.connect((self.addr, self.port))
+        except:
+            # Unset self.sock so that the next attempt to send will cause
+            # another connection attempt.
+            self.sock = None
+            raise
+        hello = self.receive()
+        self.traits = hello.get('traits')
+        self.applicationType = hello.get('applicationType')
+
+        # get the marionette actor id
+        response = self.send({'to':'root', 'type': 'getMarionetteID'})
+        self.actor = response['id']
+
+    def send(self, msg):
+        """ Send a message on the socket, prepending it with len(msg) + ':'.
+        """
+        if not self.sock:
+            self.connect()
+        if 'to' not in msg:
+            msg['to'] = self.actor
+        data = json.dumps(msg)
+        self.sock.send('%s:%s' % (len(data), data))
+        response = self.receive()
+        return response
+
+    def close(self):
+        """ Close the socket.
+        """
+        self.sock.close()
+        self.sock = None
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/emulator.py
@@ -0,0 +1,262 @@
+import datetime
+import os
+import re
+import shutil
+import socket
+import subprocess
+from telnetlib import Telnet
+import tempfile
+import time
+
+from emulator_battery import EmulatorBattery
+
+
+class Emulator(object):
+
+    deviceRe = re.compile(r"^emulator-(\d+)(\s*)(.*)$")
+
+    def __init__(self, homedir=None, noWindow=False):
+        self.port = None
+        self._emulator_launched = False
+        self.proc = None
+        self.marionette_port = None
+        self.telnet = None
+        self._tmp_userdata = None
+        self._adb_started = False
+        self.battery = EmulatorBattery(self)
+        self.homedir = homedir
+        self.noWindow = noWindow
+        if self.homedir is not None:
+            self.homedir = os.path.expanduser(homedir)
+
+    def _check_for_b2g(self):
+        if self.homedir is None:
+            self.homedir = os.getenv('B2G_HOME')
+        if self.homedir is None:
+            raise Exception('Must define B2G_HOME or pass the homedir parameter')
+
+        self.adb = os.path.join(self.homedir,
+                                'glue/gonk/out/host/linux-x86/bin/adb')
+        if not os.access(self.adb, os.F_OK):
+            self.adb = os.path.join(self.homedir, 'bin/adb')
+
+        self.binary = os.path.join(self.homedir,
+                                   'glue/gonk/out/host/linux-x86/bin/emulator')
+        if not os.access(self.binary, os.F_OK):
+            self.binary = os.path.join(self.homedir, 'bin/emulator')
+        self._check_file(self.binary)
+
+        self.kernelImg = os.path.join(self.homedir,
+                                      'boot/kernel-android-qemu/arch/arm/boot/zImage')
+        if not os.access(self.kernelImg, os.F_OK):
+            self.kernelImg = os.path.join(self.homedir, 'zImage')
+        self._check_file(self.kernelImg)
+
+        self.sysDir = os.path.join(self.homedir, 
+                                   'glue/gonk/out/target/product/generic/')
+        if not os.access(self.sysDir, os.F_OK):
+            self.sysDir = os.path.join(self.homedir, 'generic/')
+        self._check_file(self.sysDir)
+
+        self.dataImg = os.path.join(self.sysDir, 'userdata.img')
+        self._check_file(self.dataImg)
+
+    def __del__(self):
+        if self.telnet:
+            self.telnet.write('exit\n')
+            self.telnet.read_all()
+
+    def _check_file(self, filePath):
+        if not os.access(filePath, os.F_OK):
+            raise Exception(('File not found: %s; did you pass the B2G home '
+                             'directory as the homedir parameter, or set '
+                             'B2G_HOME correctly?') % filePath)
+
+    @property
+    def args(self):
+        qemuArgs =  [ self.binary,
+                      '-kernel', self.kernelImg,
+                      '-sysdir', self.sysDir,
+                      '-data', self.dataImg ]
+        if self.noWindow:
+            qemuArgs.append('-no-window')
+        qemuArgs.extend(['-memory', '512',
+                         '-partition-size', '512',
+                         '-verbose',
+                         '-skin', '480x800',
+                         '-qemu', '-cpu', 'cortex-a8'])
+        return qemuArgs
+
+    @property
+    def is_running(self):
+        if self._emulator_launched:
+            return self.proc is not None and self.proc.poll() is None
+        else:
+            return self.port is not None
+
+    def _check_for_adb(self):
+        self.adb = os.path.join(self.homedir,
+                                'glue/gonk/out/host/linux-x86/bin/adb')
+        if not os.access(self.adb, os.F_OK):
+            self.adb = os.path.join(self.homedir, 'bin/adb')
+            if not os.access(self.adb, os.F_OK):
+                adb = subprocess.Popen(['which', 'adb'],
+                                       stdout=subprocess.PIPE,
+                                       stderr=subprocess.STDOUT)
+                retcode = adb.wait()
+                if retcode:
+                    raise Exception('adb not found!')
+                out = adb.stdout.read().strip()
+                if len(out) and out.find('/') > -1:
+                    self.adb = out
+
+    def _run_adb(self, args):
+        args.insert(0, self.adb)
+        adb = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+        retcode = adb.wait()
+        if retcode:
+            raise Exception('adb terminated with exit code %d: %s' 
+                            % (retcode, adb.stdout.read()))
+        return adb.stdout.read()
+
+    def _get_telnet_response(self, command=None):
+        output = []
+        assert(self.telnet)
+        if command is not None:
+            self.telnet.write('%s\n' % command)
+        while True:
+            line = self.telnet.read_until('\n')
+            output.append(line.rstrip())
+            if line.startswith('OK'):
+                return output
+            elif line.startswith('KO:'):
+                raise Exception ('bad telnet response: %s' % line)
+
+    def _run_telnet(self, command):
+        if not self.telnet:
+            self.telnet = Telnet('localhost', self.port)
+            self._get_telnet_response()
+        return self._get_telnet_response(command)
+
+    def close(self):
+        if self.is_running and self._emulator_launched:
+            self.proc.terminate()
+            self.proc.wait()
+        if self._adb_started:
+            self._run_adb(['kill-server'])
+            self._adb_started = False
+        if self.proc:
+            retcode = self.proc.poll()
+            self.proc = None
+            if self._tmp_userdata:
+                os.remove(self._tmp_userdata)
+                self._tmp_userdata = None
+            return retcode
+        return 0
+
+    def _get_adb_devices(self):
+        offline = set()
+        online = set()
+        output = self._run_adb(['devices'])
+        for line in output.split('\n'):
+            m = self.deviceRe.match(line)
+            if m:
+                if m.group(3) == 'offline':
+                    offline.add(m.group(1))
+                else:
+                    online.add(m.group(1))
+        return (online, offline)
+
+    def restart(self, port):
+        if not self._emulator_launched:
+            return
+        self.close()
+        self.start()
+        return self.setup_port_forwarding(port)
+
+    def start_adb(self):
+        result = self._run_adb(['start-server'])
+        # We keep track of whether we've started adb or not, so we know
+        # if we need to kill it.
+        if 'daemon started successfully' in result:
+            self._adb_started = True
+        else:
+            self._adb_started = False
+
+    def connect(self):
+        self._check_for_adb()
+        self.start_adb()
+
+        online, offline = self._get_adb_devices()
+        now = datetime.datetime.now()
+        while online == set([]):
+            time.sleep(1)
+            if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
+                raise Exception('timed out waiting for emulator to be available')
+            online, offline = self._get_adb_devices()
+        self.port = int(list(online)[0])
+
+    def start(self):
+        self._check_for_b2g()
+        self.start_adb()
+
+        # Make a copy of the userdata.img for this instance of the emulator
+        # to use.
+        self._tmp_userdata = tempfile.mktemp(prefix='marionette')
+        shutil.copyfile(self.dataImg, self._tmp_userdata)
+        qemu_args = self.args[:]
+        qemu_args[qemu_args.index('-data') + 1] = self._tmp_userdata
+
+        original_online, original_offline = self._get_adb_devices()
+
+        self.proc = subprocess.Popen(qemu_args,
+                                     stdout=subprocess.PIPE,
+                                     stderr=subprocess.PIPE)
+
+        online, offline = self._get_adb_devices()
+        now = datetime.datetime.now()
+        while online - original_online == set([]):
+            time.sleep(1)
+            if datetime.datetime.now() - now > datetime.timedelta(seconds=60):
+                raise Exception('timed out waiting for emulator to start')
+            online, offline = self._get_adb_devices()
+        self.port = int(list(online - original_online)[0])
+        self._emulator_launched = True
+
+    def setup_port_forwarding(self, remote_port):
+        """ Setup TCP port forwarding to the specified port on the device,
+            using any availble local port, and return the local port.
+        """
+
+        import socket
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s.bind(("",0))
+        local_port = s.getsockname()[1]
+        s.close()
+
+        output = self._run_adb(['-s', 'emulator-%d' % self.port, 
+                                'forward',
+                                'tcp:%d' % local_port,
+                                'tcp:%d' % remote_port])
+
+        self.marionette_port = local_port
+
+        return local_port
+
+    def wait_for_port(self, timeout=300):
+        assert(self.marionette_port)
+        starttime = datetime.datetime.now()
+        while datetime.datetime.now() - starttime < datetime.timedelta(seconds=timeout):
+            try:
+                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                sock.connect(('localhost', self.marionette_port))
+                data = sock.recv(16)
+                sock.close()
+                if '"from"' in data:
+                    return True
+            except:
+                import traceback
+                print traceback.format_exc()
+            time.sleep(1)
+        return False
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/emulator_battery.py
@@ -0,0 +1,49 @@
+class EmulatorBattery(object):
+
+    def __init__(self, emulator):
+        self.emulator = emulator
+
+    def get_state(self):
+        status = {}
+        state = {}
+
+        response = self.emulator._run_telnet('power display')
+        for line in response:
+            if ':' in line:
+                field, value = line.split(':')
+                value = value.strip()
+                if value == 'true':
+                    value = True
+                elif value == 'false':
+                    value = False
+                elif field == 'capacity':
+                    value = float(value)
+                status[field] = value
+
+        state['level'] = status.get('capacity', 0.0) / 100
+        if status.get('AC') == 'online':
+            state['charging'] = True
+        else:
+            state['charging'] = False
+
+        return state
+
+    def get_charging(self):
+        return self.get_state()['charging']
+
+    def get_level(self):
+        return self.get_state()['level']
+
+    def set_level(self, level):
+        self.emulator._run_telnet('power capacity %d' % (level * 100))
+
+    def set_charging(self, charging):
+        if charging:
+            cmd = 'power ac on'
+        else:
+            cmd = 'power ac off'
+        self.emulator._run_telnet(cmd)
+
+    charging = property(get_charging, set_charging)
+    level = property(get_level, set_level)
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/errors.py
@@ -0,0 +1,78 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+
+class MarionetteException(Exception):
+
+    def __init__(self, message=None, status=500, stacktrace=None):
+        self.message = message
+        self.status = status
+        self.stacktrace = stacktrace
+
+    def __str__(self):
+        if self.stacktrace:
+            return '%s\nstacktrace:\n%s' % (self.message,
+                ''.join(['\t%s\n' % x for x in self.stacktrace.split('\n')]))
+        else:
+            return self.message
+
+class TimeoutException(MarionetteException):
+    pass
+
+class JavascriptException(MarionetteException):
+    pass
+
+class NoSuchElementException(MarionetteException):
+    pass
+
+class XPathLookupException(MarionetteException):
+    pass
+
+class NoSuchWindowException(MarionetteException):
+    pass
+
+class StaleElementException(MarionetteException):
+    pass
+
+class ScriptTimeoutException(MarionetteException):
+    pass
+
+class ElementNotVisibleException(MarionetteException):
+    pass
+
+class NoSuchFrameException(MarionetteException):
+    pass
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/gitutils.py
@@ -0,0 +1,68 @@
+from git import *
+from optparse import OptionParser
+import os
+import sys
+
+def updategaia(repopath):
+    b2g = Repo(repopath)
+    gaia = Repo(os.path.join(repopath, 'gaia'))
+
+    gaia_submodule = None
+    for submodule in b2g.submodules:
+        if 'gaia' in submodule.name:
+            gaia_submodule = submodule
+    assert(gaia_submodule)
+    gaia_submodule_commit = gaia_submodule.hexsha
+    print 'gaia_submodule_commit', gaia_submodule_commit
+
+    gaia.heads.master.checkout()
+    print 'pulling from gaia origin/master'
+    gaia.remotes.origin.pull('master')
+    gaia_new_head = gaia.heads.master.commit.hexsha
+    print 'gaia_new_head', gaia_new_head
+
+    if gaia_submodule_commit == gaia_new_head:
+        print 'no change, exiting with code 10'
+        sys.exit(10)
+
+def commitgaia(repopath):
+    b2g = Repo(repopath)
+    gaia = Repo(os.path.join(repopath, 'gaia'))
+
+    gaia_submodule = None
+    for submodule in b2g.submodules:
+        if 'gaia' in submodule.name:
+            gaia_submodule = submodule
+    assert(gaia_submodule)
+
+    gaia_submodule.binsha = gaia_submodule.module().head.commit.binsha
+    b2g.index.add([gaia_submodule])
+    commit = b2g.index.commit('Update gaia')
+    print 'pushing to B2G origin/master'
+    b2g.remotes.origin.push(b2g.head.reference)
+    print 'done!'
+
+
+if __name__ == '__main__':
+    parser = OptionParser(usage='%prog [options]')
+    parser.add_option("--repo",
+                      action = "store", dest = "repo",
+                      help = "path to B2G repo")
+    parser.add_option("--updategaia",
+                      action = "store_true", dest = "updategaia",
+                      help = "update the Gaia submodule to HEAD")
+    parser.add_option("--commitgaia",
+                      action = "store_true", dest = "commitgaia",
+                      help = "commit current Gaia submodule HEAD")
+    options, tests = parser.parse_args()
+
+    if not options.repo:
+        raise 'must specify --repo /path/to/B2G'
+
+    if options.updategaia:
+        updategaia(options.repo)
+    elif options.commitgaia:
+        commitgaia(options.repo)
+    else:
+        raise 'No command specified'
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/marionette.py
@@ -0,0 +1,353 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+import socket
+
+from client import MarionetteClient
+from errors import *
+from emulator import Emulator
+
+class HTMLElement(object):
+
+    CLASS = "class name"
+    SELECTOR = "css selector"
+    ID = "id"
+    NAME = "name"
+    LINK_TEXT = "link text"
+    PARTIAL_LINK_TEXT = "partial link text"
+    TAG = "tag name"
+    XPATH = "xpath"
+
+    def __init__(self, marionette, id):
+        self.marionette = marionette
+        assert(id is not None)
+        self.id = id
+
+    def __str__(self):
+        return self.id
+
+    def equals(self, other_element):
+        return self.marionette._send_message('elementsEqual', 'value', elements=[self.id, other_element.id])
+
+    def find_element(self, method, target):
+        return self.marionette.find_element(method, target, self.id)
+
+    def find_elements(self, method, target):
+        return self.marionette.find_elements(method, target, self.id)
+
+    def get_attribute(self, attribute):
+        return self.marionette._send_message('getElementAttribute', 'value', element=self.id, name=attribute)
+
+    def click(self):
+        return self.marionette._send_message('clickElement', 'ok', element=self.id)
+
+    def text(self):
+        return self.marionette._send_message('getElementText', 'value', element=self.id)
+
+    def send_keys(self, string):
+        return self.marionette._send_message('sendKeysToElement', 'ok', element=self.id, value=string)
+
+    def value(self):
+        return self.marionette._send_message('getElementValue', 'value', element=self.id)
+
+    def clear(self):
+        return self.marionette._send_message('clearElement', 'ok', element=self.id)
+
+    def selected(self):
+        return self.marionette._send_message('isElementSelected', 'value', element=self.id)
+
+    def enabled(self):
+        return self.marionette._send_message('isElementEnabled', 'value', element=self.id)
+
+    def displayed(self):
+        return self.marionette._send_message('isElementDisplayed', 'value', element=self.id)
+
+
+class Marionette(object):
+
+    CONTEXT_CHROME = 'chrome'
+    CONTEXT_CONTENT = 'content'
+
+    def __init__(self, host='localhost', port=2828, emulator=False,
+                 connectToRunningEmulator=False, homedir=None,
+                 baseurl=None, noWindow=False):
+        self.host = host
+        self.port = self.local_port = port
+        self.session = None
+        self.window = None
+        self.emulator = None
+        self.homedir = homedir
+        self.baseurl = baseurl
+        self.noWindow = noWindow
+
+        if emulator:
+            self.emulator = Emulator(homedir=homedir, noWindow=self.noWindow)
+            self.emulator.start()
+            self.port = self.emulator.setup_port_forwarding(self.port)
+            assert(self.emulator.wait_for_port())
+
+        if connectToRunningEmulator:
+            self.emulator = Emulator(homedir=homedir)
+            self.emulator.connect()
+            self.port = self.emulator.setup_port_forwarding(self.port)
+            assert(self.emulator.wait_for_port())
+
+        self.client = MarionetteClient(self.host, self.port)
+
+    def __del__(self):
+        if self.emulator:
+            self.emulator.close()
+
+    def _send_message(self, command, response_key, **kwargs):
+        if not self.session and command not in ('newSession', 'getStatus'):
+            raise MarionetteException(message="Please start a session")
+
+        message = { 'type': command }
+        if self.session:
+            message['session'] = self.session
+        if kwargs:
+            message.update(kwargs)
+
+        try:
+            response = self.client.send(message)
+        except socket.timeout:
+            self.session = None
+            self.window = None
+            self.client.close()
+            if self.emulator:
+                port = self.emulator.restart(self.local_port)
+                if port is not None:
+                    self.port = self.client.port = port
+            raise TimeoutException(message='socket.timeout', status=21, stacktrace=None)
+
+        if (response_key == 'ok' and response.get('ok') ==  True) or response_key in response:
+            return response[response_key]
+        else:
+            self._handle_error(response)
+
+    def _handle_error(self, response):
+        if 'error' in response and isinstance(response['error'], dict):
+            status = response['error'].get('status', 500)
+            message = response['error'].get('message')
+            stacktrace = response['error'].get('stacktrace')
+            # status numbers come from 
+            # http://code.google.com/p/selenium/wiki/JsonWireProtocol#Response_Status_Codes
+            if status == 7:
+                raise NoSuchElementException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 8:
+                raise NoSuchFrameException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 10:
+                raise StaleElementException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 11:
+                raise ElementNotVisibleException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 17:
+                raise JavascriptException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 19:
+                raise XPathLookupException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 21:
+                raise TimeoutException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 23:
+                raise NoSuchWindowException(message=message, status=status, stacktrace=stacktrace)
+            elif status == 28:
+                raise ScriptTimeoutException(message=message, status=status, stacktrace=stacktrace)
+            else:
+                raise MarionetteException(message=message, status=status, stacktrace=stacktrace)
+        raise MarionetteException(message=response, status=500)
+
+    def absolute_url(self, relative_url):
+        return "%s%s" % (self.baseurl, relative_url)
+
+    def status(self):
+        return self._send_message('getStatus', 'value')
+
+    def start_session(self, desired_capabilities=None):
+        # We are ignoring desired_capabilities, at least for now.
+        self.session = self._send_message('newSession', 'value')
+        self.b2g = 'b2g' in self.session
+        return self.session
+
+    def delete_session(self):
+        response = self._send_message('deleteSession', 'ok')
+        self.session = None
+        self.window = None
+        self.client.close()
+        return response
+
+    def get_session_capabilities(self):
+        response = self._send_message('getSessionCapabilities', 'value')
+        return response
+
+    def set_script_timeout(self, timeout):
+        response = self._send_message('setScriptTimeout', 'ok', value=timeout)
+        return response
+
+    def set_search_timeout(self, timeout):
+        response = self._send_message('setSearchTimeout', 'ok', value=timeout)
+        return response
+
+    def get_window(self):
+        self.window = self._send_message('getWindow', 'value')
+        return self.window
+
+    def get_windows(self):
+        response = self._send_message('getWindows', 'value')
+        return response
+
+    def close_window(self, window_id=None):
+        if not window_id:
+            window_id = self.get_window()
+        response = self._send_message('closeWindow', 'ok', value=window_id)
+        return response
+
+    def set_context(self, context):
+        assert(context == self.CONTEXT_CHROME or context == self.CONTEXT_CONTENT)
+        return self._send_message('setContext', 'ok', value=context)
+
+    def switch_to_window(self, window_id):
+        response = self._send_message('switchToWindow', 'ok', value=window_id)
+        self.window = window_id
+        return response
+
+    def switch_to_frame(self, frame=None):
+        if isinstance(frame, HTMLElement):
+            response = self._send_message('switchToFrame', 'ok', element=frame.id)
+        else:
+            response = self._send_message('switchToFrame', 'ok', value=frame)
+        return response
+
+    def get_url(self):
+        response = self._send_message('getUrl', 'value')
+        return response
+
+    def navigate(self, url):
+        response = self._send_message('goUrl', 'ok', value=url)
+        return response
+
+    def go_back(self):
+        response = self._send_message('goBack', 'ok')
+        return response
+
+    def go_forward(self):
+        response = self._send_message('goForward', 'ok')
+        return response
+
+    def refresh(self):
+        response = self._send_message('refresh', 'ok')
+        return response
+
+    def wrapArguments(self, args):
+        if isinstance(args, list):
+            wrapped = []
+            for arg in args:
+                wrapped.append(self.wrapArguments(arg))
+        elif isinstance(args, dict):
+            wrapped = {}
+            for arg in args:
+                wrapped[arg] = self.wrapArguments(args[arg])
+        elif type(args) == HTMLElement:
+            wrapped = {'ELEMENT': args.id }
+        elif (isinstance(args, bool) or isinstance(args, basestring) or
+              isinstance(args, int) or args is None):
+            wrapped = args
+
+        return wrapped
+
+    def unwrapValue(self, value):
+        if isinstance(value, list):
+            unwrapped = []
+            for item in value:
+                unwrapped.append(self.unwrapValue(item))
+        elif isinstance(value, dict):
+            unwrapped = {}
+            for key in value:
+                if key == 'ELEMENT':
+                    unwrapped = HTMLElement(self, value[key])
+                else:
+                    unwrapped[key] = self.unwrapValue(value[key])
+        else:
+            unwrapped = value
+
+        return unwrapped
+
+    def execute_js_script(self, script, script_args=None, timeout=True):
+        if script_args is None:
+            script_args = []
+        args = self.wrapArguments(script_args)
+        response = self._send_message('executeJSScript',
+                                      'value',
+                                      value=script,
+                                      args=args,
+                                      timeout=timeout)
+        return self.unwrapValue(response)
+
+    def execute_script(self, script, script_args=None):
+        if script_args is None:
+            script_args = []
+        args = self.wrapArguments(script_args)
+        response = self._send_message('executeScript', 'value', value=script, args=args)
+        return self.unwrapValue(response)
+
+    def execute_async_script(self, script, script_args=None):
+        if script_args is None:
+            script_args = []
+        args = self.wrapArguments(script_args)
+        response = self._send_message('executeAsyncScript', 'value', value=script, args=args)
+        return self.unwrapValue(response)
+
+    def find_element(self, method, target, id=None):
+        kwargs = { 'value': target, 'using': method }
+        if id:
+            kwargs['element'] = id
+        response = self._send_message('findElement', 'value', **kwargs)
+        element = HTMLElement(self, response)
+        return element
+
+    def find_elements(self, method, target, id=None):
+        kwargs = { 'value': target, 'using': method }
+        if id:
+            kwargs['element'] = id
+        response = self._send_message('findElements', 'value', **kwargs)
+        assert(isinstance(response, list))
+        elements = []
+        for x in response:
+            elements.append(HTMLElement(self, x))
+        return elements
+
+    def log(self, msg, level=None):
+        return self._send_message('log', 'ok', value=msg, level=level)
+
+    def get_logs(self):
+        return self._send_message('getLogs', 'value')
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/marionette_test.py
@@ -0,0 +1,167 @@
+import os
+import re
+import sys
+import types
+import unittest
+
+from errors import *
+from marionette import HTMLElement, Marionette
+
+def skip_if_b2g(target):
+    def wrapper(self, *args, **kwargs):
+        if not hasattr(self.marionette, 'b2g') or not self.marionette.b2g:
+            return target(self, *args, **kwargs)
+        else:
+            sys.stderr.write('skipping ... ')
+    return wrapper
+
+class CommonTestCase(unittest.TestCase):
+
+    def __init__(self, methodName):
+        self._qemu = []
+        unittest.TestCase.__init__(self, methodName)
+
+    def kill_gaia_app(self, url):
+        self.marionette.execute_script("""
+window.wrappedJSObject.getApplicationManager().kill("%s");
+return(true);
+""" % url)
+
+    def kill_gaia_apps(self):
+        # shut down any running Gaia apps
+        # XXX there's no API to do this currently
+        pass
+
+    def launch_gaia_app(self, url):
+        # launch the app using Gaia's AppManager
+        self.marionette.set_script_timeout(30000)
+        frame = self.marionette.execute_async_script("""
+var frame = window.wrappedJSObject.getApplicationManager().launch("%s").element;
+window.addEventListener('message', function frameload(e) {
+    if (e.data == 'appready') {
+        window.removeEventListener('message', frameload);
+        marionetteScriptFinished(frame);
+    }
+});
+    """ % url)
+
+        self.assertTrue(isinstance(frame, HTMLElement))
+        return frame
+
+    def setUp(self):
+        if self.marionette.session is None:
+            self.marionette.start_session()
+        self.loglines = None
+
+    def tearDown(self):
+        if self.marionette.session is not None:
+            self.marionette.delete_session()
+        for _qemu in self._qemu:
+            _qemu.emulator.close()
+            _qemu = None
+        self._qemu = []
+
+
+class MarionetteTestCase(CommonTestCase):
+
+    def __init__(self, marionette, methodName='runTest'):
+        self.marionette = marionette
+        CommonTestCase.__init__(self, methodName)
+
+    def get_new_emulator(self):
+        _qemu  = Marionette(emulator=True,
+                            homedir=self.marionette.homedir,
+                            baseurl=self.marionette.baseurl,
+                            noWindow=self.marionette.noWindow)
+        _qemu.start_session()
+        self._qemu.append(_qemu)
+        return _qemu
+
+
+class MarionetteJSTestCase(CommonTestCase):
+
+    context_re = re.compile(r"MARIONETTE_CONTEXT(\s*)=(\s*)['|\"](.*?)['|\"];")
+    timeout_re = re.compile(r"MARIONETTE_TIMEOUT(\s*)=(\s*)(\d+);")
+    launch_re = re.compile(r"MARIONETTE_LAUNCH_APP(\s*)=(\s*)['|\"](.*?)['|\"];")
+
+    def __init__(self, marionette, methodName='runTest', jsFile=None):
+        assert(jsFile)
+        self.jsFile = jsFile
+        self.marionette = marionette
+        CommonTestCase.__init__(self, methodName)
+
+    def runTest(self):
+        if self.marionette.session is None:
+            self.marionette.start_session()
+        f = open(self.jsFile, 'r')
+        js = f.read()
+        args = []
+
+        # if this is a browser_ test, prepend head.js to it
+        if os.path.basename(self.jsFile).startswith('browser_'):
+            local_head = open(os.path.join(os.path.dirname(__file__), 'tests', 'head.js'), 'r')
+            js = local_head.read() + js
+            head = open(os.path.join(os.path.dirname(self.jsFile), 'head.js'), 'r')
+            for line in head:
+                # we need a bigger timeout than the default specified by the
+                # 'real' head.js
+                if 'const kDefaultWait' in line:
+                    js += 'const kDefaultWait = 45000;\n'
+                else:
+                    js += line
+
+        context = self.context_re.search(js)
+        if context:
+            context = context.group(3)
+            self.marionette.set_context(context)
+
+        timeout = self.timeout_re.search(js)
+        if timeout:
+            timeout = timeout.group(3)
+            self.marionette.set_script_timeout(timeout)
+
+        launch_app = self.launch_re.search(js)
+        if launch_app:
+            launch_app = launch_app.group(3)
+            frame = self.launch_gaia_app(launch_app)
+            args.append({'__marionetteArgs': {'appframe': frame}})
+
+        try:
+            results = self.marionette.execute_js_script(js, args)
+
+            self.loglines = self.marionette.get_logs()
+
+            if launch_app:
+                self.kill_gaia_app(launch_app)
+
+            self.assertTrue(not 'timeout' in self.jsFile,
+                            'expected timeout not triggered')
+
+            if 'fail' in self.jsFile:
+                self.assertTrue(results['failed'] > 0,
+                                "expected test failures didn't occur")
+            else:
+                fails = []
+                for failure in results['failures']:
+                    diag = "" if failure.get('diag') is None else "| %s " % failure['diag']
+                    name = "got false, expected true" if failure.get('name') is None else failure['name']
+                    fails.append('TEST-UNEXPECTED-FAIL %s| %s' % (diag, name))
+                self.assertEqual(0, results['failed'],
+                                 '%d tests failed:\n%s' % (results['failed'], '\n'.join(fails)))
+
+            self.assertTrue(results['passed'] + results['failed'] > 0,
+                            'no tests run')
+            if self.marionette.session is not None:
+                self.marionette.delete_session()
+
+        except ScriptTimeoutException:
+            if 'timeout' in self.jsFile:
+                # expected exception
+                pass
+            else:
+                self.loglines = self.marionette.get_logs()
+                raise
+
+
+
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/runtests.py
@@ -0,0 +1,391 @@
+from datetime import datetime
+import imp
+import inspect
+import logging
+from optparse import OptionParser
+import os
+import types
+import unittest
+import socket
+import sys
+import time
+
+try:
+    from manifestparser import TestManifest
+    from mozhttpd import iface, MozHttpd
+except ImportError:
+    print "manifestparser or mozhttpd not found!  Please install mozbase:\n"
+    print "\tgit clone git clone git://github.com/mozilla/mozbase.git"
+    print "\tpython setup_development.py\n"
+    import sys
+    sys.exit(1)
+
+
+from marionette import Marionette
+from marionette_test import MarionetteJSTestCase
+
+
+class MarionetteTestResult(unittest._TextTestResult):
+
+    def __init__(self, *args):
+        super(MarionetteTestResult, self).__init__(*args)
+        self.passed = 0
+
+    def addSuccess(self, test):
+        super(MarionetteTestResult, self).addSuccess(test)
+        self.passed += 1
+
+    def getInfo(self, test):
+        if hasattr(test, 'jsFile'):
+            return os.path.basename(test.jsFile)
+        else:
+            return '%s.py:%s.%s' % (test.__class__.__module__,
+                                    test.__class__.__name__,
+                                    test._testMethodName)
+
+    def getDescription(self, test):
+        doc_first_line = test.shortDescription()
+        if self.descriptions and doc_first_line:
+            return '\n'.join((str(test), doc_first_line))
+        else:
+            desc = str(test)
+            if hasattr(test, 'jsFile'):
+                desc = "%s, %s" % (test.jsFile, desc)
+            return desc
+
+    def printLogs(self, test):
+        for testcase in test._tests:
+            if hasattr(testcase, 'loglines') and testcase.loglines:
+                print 'START LOG:'
+                for line in testcase.loglines:
+                    print ' '.join(line)
+                print 'END LOG:'
+
+
+class MarionetteTextTestRunner(unittest.TextTestRunner):
+
+    resultclass = MarionetteTestResult
+
+    def _makeResult(self):
+        return self.resultclass(self.stream, self.descriptions, self.verbosity)
+
+    def run(self, test):
+        "Run the given test case or test suite."
+        result = self._makeResult()
+        result.failfast = self.failfast
+        result.buffer = self.buffer
+        startTime = time.time()
+        startTestRun = getattr(result, 'startTestRun', None)
+        if startTestRun is not None:
+            startTestRun()
+        try:
+            test(result)
+        finally:
+            stopTestRun = getattr(result, 'stopTestRun', None)
+            if stopTestRun is not None:
+                stopTestRun()
+        stopTime = time.time()
+        timeTaken = stopTime - startTime
+        result.printErrors()
+        result.printLogs(test)
+        if hasattr(result, 'separator2'):
+            self.stream.writeln(result.separator2)
+        run = result.testsRun
+        self.stream.writeln("Ran %d test%s in %.3fs" %
+                            (run, run != 1 and "s" or "", timeTaken))
+        self.stream.writeln()
+
+        expectedFails = unexpectedSuccesses = skipped = 0
+        try:
+            results = map(len, (result.expectedFailures,
+                                result.unexpectedSuccesses,
+                                result.skipped))
+        except AttributeError:
+            pass
+        else:
+            expectedFails, unexpectedSuccesses, skipped = results
+
+        infos = []
+        if not result.wasSuccessful():
+            self.stream.write("FAILED")
+            failed, errored = map(len, (result.failures, result.errors))
+            if failed:
+                infos.append("failures=%d" % failed)
+            if errored:
+                infos.append("errors=%d" % errored)
+        else:
+            self.stream.write("OK")
+        if skipped:
+            infos.append("skipped=%d" % skipped)
+        if expectedFails:
+            infos.append("expected failures=%d" % expectedFails)
+        if unexpectedSuccesses:
+            infos.append("unexpected successes=%d" % unexpectedSuccesses)
+        if infos:
+            self.stream.writeln(" (%s)" % (", ".join(infos),))
+        else:
+            self.stream.write("\n")
+        return result
+
+
+class MarionetteTestRunner(object):
+
+    def __init__(self, address=None, emulator=False, homedir=None,
+                 autolog=False, revision=None, es_server=None,
+                 rest_server=None, logger=None, testgroup="marionette",
+                 noWindow=False):
+        self.address = address
+        self.emulator = emulator
+        self.homedir = homedir
+        self.autolog = autolog
+        self.testgroup = testgroup
+        self.revision = revision
+        self.es_server = es_server
+        self.rest_server = rest_server
+        self.logger = logger
+        self.noWindow = noWindow
+        self.httpd = None
+        self.baseurl = None
+        self.marionette = None
+
+        self.reset_test_stats()
+
+        if self.logger is None:
+            self.logger = logging.getLogger('Marionette')
+            self.logger.setLevel(logging.INFO)
+            self.logger.addHandler(logging.StreamHandler())
+
+    def reset_test_stats(self):
+        self.passed = 0
+        self.failed = 0
+        self.todo = 0
+        self.failures = []
+
+    def start_httpd(self):
+        host = iface.get_lan_ip()
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        s.bind(("",0))
+        port = s.getsockname()[1]
+        s.close()
+        self.baseurl = 'http://%s:%d/' % (host, port)
+        self.logger.info('running webserver on %s' % self.baseurl)
+        self.httpd = MozHttpd(host=host,
+                              port=port,
+                              docroot=os.path.join(os.path.dirname(__file__), 'www'))
+        self.httpd.start()
+
+    def start_marionette(self):
+        assert(self.baseurl is not None)
+        if self.address:
+            host, port = self.address.split(':')
+            if self.emulator:
+                self.marionette = Marionette(host=host, port=int(port),
+                                            connectToRunningEmulator=True,
+                                            homedir=self.homedir,
+                                            baseurl=self.baseurl)
+            else:
+                self.marionette = Marionette(host=host, port=int(port), baseurl=self.baseurl)
+        elif self.emulator:
+            self.marionette = Marionette(emulator=True,
+                                         homedir=self.homedir,
+                                         baseurl=self.baseurl,
+                                         noWindow=self.noWindow)
+        else:
+            raise Exception("must specify address or emulator")
+
+    def post_to_autolog(self, elapsedtime):
+        self.logger.info('posting results to autolog')
+
+        # This is all autolog stuff.
+        # See: https://wiki.mozilla.org/Auto-tools/Projects/Autolog
+        from mozautolog import RESTfulAutologTestGroup
+        testgroup = RESTfulAutologTestGroup(
+            testgroup = self.testgroup,
+            os = 'android',
+            platform = 'emulator',
+            harness = 'marionette',
+            server = self.es_server,
+            restserver = self.rest_server,
+            machine = socket.gethostname())
+
+        testgroup.set_primary_product(
+            tree = 'b2g',
+            buildtype = 'opt',
+            revision = self.revision)
+
+        testgroup.add_test_suite(
+            testsuite = 'b2g emulator testsuite',
+            elapsedtime = elapsedtime.seconds,
+            cmdline = '',
+            passed = self.passed,
+            failed = self.failed,
+            todo = self.todo)
+
+        # Add in the test failures.
+        for f in self.failures:
+            testgroup.add_test_failure(test=f[0], text=f[1], status=f[2])
+
+        testgroup.submit()
+
+    def run_tests(self, tests, testtype=None):
+        self.reset_test_stats()
+        starttime = datetime.utcnow()
+        for test in tests:
+            self.run_test(test, testtype)
+        self.logger.info('\nSUMMARY\n-------')
+        self.logger.info('passed: %d' % self.passed)
+        self.logger.info('failed: %d' % self.failed)
+        self.logger.info('todo: %d' % self.todo)
+        elapsedtime = datetime.utcnow() - starttime
+        if self.autolog:
+            self.post_to_autolog(elapsedtime)
+        if self.marionette.emulator:
+            self.marionette.emulator.close()
+            self.marionette.emulator = None
+        self.marionette = None
+
+    def run_test(self, test, testtype):
+        if not self.httpd:
+            self.start_httpd()
+        if not self.marionette:
+            self.start_marionette()
+
+        if not os.path.isabs(test):
+            filepath = os.path.join(os.path.dirname(__file__), test)
+        else:
+            filepath = test
+
+        if os.path.isdir(filepath):
+            for root, dirs, files in os.walk(filepath):
+                for filename in files:
+                    if ((filename.startswith('test_') or filename.startswith('browser_')) and 
+                        (filename.endswith('.py') or filename.endswith('.js'))):
+                        filepath = os.path.join(root, filename)
+                        self.run_test(filepath, testtype)
+            return
+
+        mod_name,file_ext = os.path.splitext(os.path.split(filepath)[-1])
+
+        testloader = unittest.TestLoader()
+        suite = unittest.TestSuite()
+
+        if file_ext == '.ini':
+            if testtype is not None:
+                testargs = {}
+                testtypes = testtype.replace('+', ' +').replace('-', ' -').split()
+                for atype in testtypes:
+                    if atype.startswith('+'):
+                        testargs.update({ atype[1:]: 'true' })
+                    elif atype.startswith('-'):
+                        testargs.update({ atype[1:]: 'false' })
+                    else:
+                        testargs.update({ atype: 'true' })
+            manifest = TestManifest()
+            manifest.read(filepath)
+
+            if testtype is None:
+                manifest_tests = manifest.get()
+            else:
+                manifest_tests = manifest.get(**testargs)
+
+            for i in manifest_tests:
+                self.run_test(i["path"], testtype)
+            return
+
+        self.logger.info('TEST-START %s' % os.path.basename(test))
+
+        if file_ext == '.py':
+            test_mod = imp.load_source(mod_name, filepath)
+
+            for name in dir(test_mod):
+                obj = getattr(test_mod, name)
+                if (isinstance(obj, (type, types.ClassType)) and
+                    issubclass(obj, unittest.TestCase)):
+                    testnames = testloader.getTestCaseNames(obj)
+                    for testname in testnames:
+                        suite.addTest(obj(self.marionette, methodName=testname))
+
+        elif file_ext == '.js':
+            suite.addTest(MarionetteJSTestCase(self.marionette, jsFile=filepath))
+
+        if suite.countTestCases():
+            results = MarionetteTextTestRunner(verbosity=3).run(suite)
+            self.failed += len(results.failures) + len(results.errors)
+            self.todo = 0
+            if hasattr(results, 'skipped'):
+                self.todo += len(results.skipped) + len(results.expectedFailures)
+            self.passed += results.passed
+            for failure in results.failures + results.errors:
+                self.failures.append((results.getInfo(failure[0]), failure[1], 'TEST-UNEXPECTED-FAIL'))
+            if hasattr(results, 'unexpectedSuccess'):
+                self.failed += len(results.unexpectedSuccesses)
+                for failure in results.unexpectedSuccesses:
+                    self.failures.append((results.getInfo(failure[0]), failure[1], 'TEST-UNEXPECTED-PASS'))
+
+    def cleanup(self):
+        if self.httpd:
+            self.httpd.stop()
+
+    __del__ = cleanup
+
+
+if __name__ == "__main__":
+    parser = OptionParser(usage='%prog [options] test_file_or_dir <test_file_or_dir> ...')
+    parser.add_option("--autolog",
+                      action = "store_true", dest = "autolog",
+                      default = False,
+                      help = "send test results to autolog")
+    parser.add_option("--revision",
+                      action = "store", dest = "revision",
+                      help = "git revision for autolog submissions")
+    parser.add_option("--testgroup",
+                      action = "store", dest = "testgroup",
+                      help = "testgroup names for autolog submissions")
+    parser.add_option("--emulator",
+                      action = "store_true", dest = "emulator",
+                      default = False,
+                      help = "launch a B2G emulator on which to run tests")
+    parser.add_option("--no-window",
+                      action = "store_true", dest = "noWindow",
+                      default = False,
+                      help = "when Marionette launches an emulator, start it "
+                      "with the -no-window argument")
+    parser.add_option('--address', dest='address', action='store',
+                      help='host:port of running Gecko instance to connect to')
+    parser.add_option('--type', dest='type', action='store',
+                      default='browser+b2g',
+                      help = "The type of test to run, can be a combination "
+                      "of values defined in unit-tests.ini; individual values "
+                      "are combined with '+' or '-' chars.  Ex:  'browser+b2g' "
+                      "means the set of tests which are compatible with both "
+                      "browser and b2g; 'b2g-qemu' means the set of tests "
+                      "which are compatible with b2g but do not require an "
+                      "emulator.  This argument is only used when loading "
+                      "tests from .ini files.")
+    parser.add_option('--homedir', dest='homedir', action='store',
+                      help='home directory of emulator files')
+
+    options, tests = parser.parse_args()
+
+    if not tests:
+        parser.print_usage()
+        parser.exit()
+
+    if not options.emulator and not options.address:
+        parser.print_usage()
+        print "must specify --emulator or --address"
+        parser.exit()
+
+    runner = MarionetteTestRunner(address=options.address,
+                                  emulator=options.emulator,
+                                  homedir=options.homedir,
+                                  noWindow=options.noWindow,
+                                  revision=options.revision,
+                                  testgroup=options.testgroup,
+                                  autolog=options.autolog)
+    runner.run_tests(tests, testtype=options.type)
+    if runner.failed > 0:
+        sys.exit(10)
+
+
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/selenium_proxy.py
@@ -0,0 +1,297 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+import BaseHTTPServer
+import json
+import re
+import traceback
+
+from errors import *
+from marionette import Marionette, HTMLElement
+
+class SeleniumRequestServer(BaseHTTPServer.HTTPServer):
+
+    def __init__(self, marionette, *args, **kwargs):
+        self.marionette = marionette
+        BaseHTTPServer.HTTPServer.__init__(self, *args, **kwargs)
+
+    def __del__(self):
+        if self.marionette.server:
+            self.marionette.delete_session()
+
+class SeleniumRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+    pathRe = re.compile(r'/session/(.*?)/((element/(.*?)/)?(.*))')
+
+    def server_error(self, error):
+        self.send_response(500)
+        self.send_header("Content-type", "application/json")
+        self.end_headers()
+        self.wfile.write(json.dumps({'status': 500, 'value': {'message': error}}))
+
+    def file_not_found(self):
+        self.send_response(404)
+        self.send_header("Content-type", "application/json")
+        self.end_headers()
+        self.wfile.write(json.dumps({'status': 404, 'value': {'message': '%s not found' % self.path}}))
+
+    def send_JSON(self, data=None, session=None, value=None):
+        self.send_response(200)
+        self.send_header("Content-type", "application/json")
+        self.end_headers()
+
+        if data is None:
+            data = {}
+        if not 'status' in data:
+            data['status'] = 0
+        if session is not None:
+            data['sessionId'] = session
+        if value is None:
+            data['value'] = {}
+        else:
+            data['value'] = value
+
+        self.wfile.write(json.dumps(data))
+
+    def process_request(self):
+        session = body = None
+        path = self.path
+        element = None
+        m = self.pathRe.match(self.path)
+        if m:
+            session = m.group(1)
+            element = m.group(4)
+            path = '/%s' % m.group(5)
+        content_len = self.headers.getheader('content-length')
+        if content_len:
+            body = json.loads(self.rfile.read(int(content_len)))
+        return path, body, session, element
+
+    def do_DELETE(self):
+        try:
+
+            path, body, session, element = self.process_request()
+
+            if path == '/session':
+                assert(session)
+                assert(self.server.marionette.delete_session())
+                self.send_JSON(session=session)
+            elif path == '/window':
+                assert(session)
+                assert(self.server.marionette.close_window())
+                self.send_JSON(session=session)
+            else:
+                self.file_not_found()
+
+        except MarionetteException, e:
+            self.send_JSON(data={'status': e.status}, value={'message': e.message})
+        except:
+            self.server_error(traceback.format_exc())
+
+    def do_GET(self):
+        try:
+
+            path, body, session, element = self.process_request()
+
+            if path.startswith('/attribute/'):
+                assert(session)
+                name = path[len('/attribute/'):]
+                marionette_element = HTMLElement(self.server.marionette, element)
+                self.send_JSON(session=session,
+                               value=marionette_element.get_attribute(name))
+            elif path == '/displayed':
+                assert(session)
+                marionette_element = HTMLElement(self.server.marionette, element)
+                self.send_JSON(session=session,
+                               value=marionette_element.displayed())
+            elif path == '/enabled':
+                assert(session)
+                marionette_element = HTMLElement(self.server.marionette, element)
+                self.send_JSON(session=session,
+                               value=marionette_element.enabled())
+            elif path.startswith('/equals/'):
+                assert(session)
+                other = path[len('/equals'):]
+                marionette_element = HTMLElement(self.server.marionette, element)
+                other_element = HTMLElement(self.server.marionette, other)
+                self.send_JSON(session=session,
+                               value=marionette_element.equals(other))
+            elif path == '/selected':
+                assert(session)
+                marionette_element = HTMLElement(self.server.marionette, element)
+                self.send_JSON(session=session,
+                               value=marionette_element.selected())
+            elif path == '/status':
+                self.send_JSON(data=self.server.marionette.status())
+            elif path == '/text':
+                assert(session)
+                marionette_element = HTMLElement(self.server.marionette, element)
+                self.send_JSON(session=session,
+                               value=marionette_element.text())
+            elif path == '/url':
+                assert(session)
+                self.send_JSON(value=self.server.marionette.get_url(),
+                               session=session)
+            elif path == '/value':
+                assert(session)
+                marionette_element = HTMLElement(self.server.marionette, element)
+                send.send_JSON(session=session,
+                               value=marionette_element.value())
+            elif path == '/window_handle':
+                assert(session)
+                self.send_JSON(session=session,
+                               value=self.server.marionette.get_window())
+            elif path == '/window_handles':
+                assert(session)
+                self.send_JSON(session=session,
+                               value=self.server.marionette.get_windows())
+            else:
+                self.file_not_found()
+
+        except MarionetteException, e:
+            self.send_JSON(data={'status': e.status}, value={'message': e.message})
+        except:
+            self.server_error(traceback.format_exc())
+
+    def do_POST(self):
+        try:
+
+            path, body, session, element = self.process_request()
+
+            if path == '/back':
+                assert(session)
+                assert(self.server.marionette.go_back())
+                self.send_JSON(session=session)
+            elif path == '/clear':
+                assert(session)
+                marionette_element = HTMLElement(self.server.marionette, element)
+                marionette_element.clear()
+                self.send_JSON(session=session)
+            elif path == '/click':
+                assert(session)
+                marionette_element = HTMLElement(self.server.marionette, element)
+                marionette_element.click()
+                self.send_JSON(session=session)
+            elif path == '/element':
+                # find element variants
+                assert(session)
+                self.send_JSON(session=session,
+                               value={'ELEMENT': str(self.server.marionette.find_element(body['using'], body['value'], id=element))})
+            elif path == '/elements':
+                # find elements variants
+                assert(session)
+                self.send_JSON(session=session,
+                               value=[{'ELEMENT': str(x)} for x in self.server.marionette.find_elements(body['using'], body['value'])])
+            elif path == '/execute':
+                assert(session)
+                if body['args']:
+                    result = self.server.marionette.execute_script(body['script'], script_args=body['args'])
+                else:
+                    result = self.server.marionette.execute_script(body['script'])
+                self.send_JSON(session=session, value=result)
+            elif path == '/execute_async':
+                assert(session)
+                if body['args']:
+                    result = self.server.marionette.execute_async_script(body['script'], script_args=body['args'])
+                else:
+                    result = self.server.marionette.execute_async_script(body['script'])
+                self.send_JSON(session=session, value=result)
+            elif path == '/forward':
+                assert(session)
+                assert(self.server.marionette.go_forward())
+                self.send_JSON(session=session)
+            elif path == '/frame':
+                assert(session)
+                frame = body['id']
+                if isinstance(frame, dict) and 'ELEMENT' in frame:
+                    frame = HTMLElement(self.server.marionette, frame['ELEMENT'])
+                assert(self.server.marionette.switch_to_frame(frame))
+                self.send_JSON(session=session)
+            elif path == '/refresh':
+                assert(session)
+                assert(self.server.marionette.refresh())
+                self.send_JSON(session=session)
+            elif path == '/session':
+                session = self.server.marionette.start_session()
+                # 'value' is the browser capabilities, which we're ignoring for now
+                self.send_JSON(session=session, value={})
+            elif path == '/timeouts/async_script':
+                assert(session)
+                assert(self.server.marionette.set_script_timeout(body['ms']))
+                self.send_JSON(session=session)
+            elif path == '/timeouts/implicit_wait':
+                assert(session)
+                assert(self.server.marionette.set_search_timeout(body['ms']))
+                self.send_JSON(session=session)
+            elif path == '/url':
+                assert(session)
+                assert(self.server.marionette.navigate(body['url']))
+                self.send_JSON(session=session)
+            elif path == '/value':
+                assert(session)
+                keys = ''.join(body['value'])
+                marionette_element = HTMLElement(self.server.marionette, element)
+                assert(marionette_element.send_keys(keys))
+                self.send_JSON(session=session)
+            elif path == '/window':
+                assert(session)
+                assert(self.server.marionette.switch_to_window(body['name']))
+                self.send_JSON(session=session)
+            else:
+                self.file_not_found()
+
+        except MarionetteException, e:
+            self.send_JSON(data={'status': e.status}, value={'message': e.message})
+        except:
+            self.server_error(traceback.format_exc())
+
+class SeleniumProxy(object):
+
+    def __init__(self, remote_host, remote_port, proxy_port=4444):
+        self.remote_host = remote_host
+        self.remote_port = remote_port
+        self.proxy_port = proxy_port
+
+    def start(self):
+        marionette = Marionette(self.remote_host, self.remote_port)
+        httpd = SeleniumRequestServer(marionette,
+                                      ('127.0.0.1', self.proxy_port),
+                                      SeleniumRequestHandler)
+        httpd.serve_forever()
+
+if __name__ == "__main__":
+    proxy = SeleniumProxy('localhost', 2626)
+    proxy.start()
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/test_debugger.py
@@ -0,0 +1,45 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+from marionette import Marionette, HTMLElement
+
+if __name__ == '__main__':
+
+    # launch Fennec with Marionette before starting this test!
+    m = Marionette(host='localhost', port=2828)
+    assert(m.start_session())
+    assert(10 == m.execute_script('return 10;'))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/test_emulator.py
@@ -0,0 +1,40 @@
+import time
+from marionette import Marionette, HTMLElement
+
+#
+# Test that Marionette can manage multiple emulators.
+# Before running this code, you should have built B2G with the config-qemu
+# configuration, see 
+# https://wiki.mozilla.org/Auto-tools/Projects/Marionette/DevNotes#Running_B2G_on_an_emulator
+#
+# You should also set your B2G_HOME environment variable to point to the
+# directory where the B2G code lives.
+#
+
+if __name__ == '__main__':
+    # launch two instance of Marionette, each with their own emulator
+    driver1 = Marionette(emulator=True, port=2828)
+    assert(driver1.emulator.is_running)
+    assert(driver1.emulator.port)
+    print 'emulator1 is running on port', driver1.emulator.port
+    assert(driver1.port != 2828)
+    print 'emulator1 port forwarding configured from port', driver1.port
+    print 'on localhost to port 2828 on the device'
+    assert(driver1.start_session())
+
+    driver2 = Marionette(emulator=True, port=2828)
+    assert(driver2.emulator.is_running)
+    assert(driver2.emulator.port)
+    print 'emulator2 is running on port', driver2.emulator.port
+    assert(driver2.port != 2828)
+    print 'emulator1 port forwarding configured from port', driver2.port
+    print 'on localhost to port 2828 on the device'
+    assert(driver2.start_session())
+
+    # shutdown both emulators
+    assert(driver2.emulator.close() == 0)
+    assert(not driver2.emulator.is_running)
+    assert(driver1.emulator.close() == 0)
+    assert(not driver1.emulator.is_running)
+
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/test_protocol.py
@@ -0,0 +1,97 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+import threading
+from testserver import TestServer
+from marionette import Marionette, HTMLElement
+
+if __name__ == '__main__':
+
+    # start the test server
+    server = TestServer(2626)
+    thread = threading.Thread(target=server.run)
+    thread.daemon = True
+    thread.start()
+
+    # run some trivial unit tests which just verify the protocol
+    m = Marionette(host='localhost', port=2626)
+    assert(m.status()['os']['arch'] == 'x86')
+    assert(m.start_session())
+    assert(m.get_session_capabilities()['javascriptEnabled'] == True)
+    assert(m.get_window() == server.TEST_CURRENT_WINDOW)
+    assert(m.window == server.TEST_CURRENT_WINDOW)
+    assert(m.get_windows() == server.TEST_WINDOW_LIST)
+    assert(m.switch_to_window('window2'))
+    assert(m.window == 'window2')
+    assert(m.close_window('window2'))
+    assert(m.set_script_timeout(1000))
+    assert(m.set_search_timeout(500))
+    assert(m.get_url() == server.TEST_URL)
+    assert(m.navigate(server.TEST_URL))
+    assert(m.go_back())
+    assert(m.go_forward())
+    assert(m.refresh())
+    assert(m.execute_script(server.TEST_EXECUTE_SCRIPT))
+    assert(m.execute_js_script(server.TEST_EXECUTE_SCRIPT))
+    assert(m.execute_js_script(server.TEST_EXECUTE_SCRIPT, server.TEST_EXECUTE_SCRIPT_ARGS))
+    assert(m.execute_script(server.TEST_EXECUTE_SCRIPT, server.TEST_EXECUTE_SCRIPT_ARGS))
+    assert(m.execute_async_script(server.TEST_EXECUTE_SCRIPT))
+    assert(m.execute_async_script(server.TEST_EXECUTE_SCRIPT, server.TEST_EXECUTE_SCRIPT_ARGS))
+    assert(str(m.find_element(HTMLElement.CLASS, 'heading')) == server.TEST_FIND_ELEMENT)
+    assert([str(x) for x in m.find_elements(HTMLElement.TAG, 'p')] == server.TEST_FIND_ELEMENTS)
+    assert(str(m.find_element(HTMLElement.CLASS, 'heading').find_element(HTMLElement.TAG, 'h1')) == server.TEST_FIND_ELEMENT)
+    assert([str(x) for x in m.find_element(HTMLElement.ID, 'div1').find_elements(HTMLElement.SELECTOR, '.main')] == \
+        server.TEST_FIND_ELEMENTS)
+    assert(m.find_element(HTMLElement.ID, 'id1').click())
+    assert(m.find_element(HTMLElement.ID, 'id2').text() == server.TEST_GET_TEXT)
+    assert(m.find_element(HTMLElement.ID, 'id3').send_keys('Mozilla Firefox'))
+    assert(m.find_element(HTMLElement.ID, 'id3').value() == server.TEST_GET_VALUE)
+    assert(m.find_element(HTMLElement.ID, 'id3').clear())
+    assert(m.find_element(HTMLElement.ID, 'id3').selected())
+    assert(m.find_element(HTMLElement.ID, 'id1').equals(m.find_element(HTMLElement.TAG, 'p')))
+    assert(m.find_element(HTMLElement.ID, 'id3').enabled())
+    assert(m.find_element(HTMLElement.ID, 'id3').displayed())
+    assert(m.find_element(HTMLElement.ID, 'id3').get_attribute('value') == server.TEST_GET_VALUE)
+    assert(m.delete_session())
+
+    # verify a session is started automatically for us if needed
+    assert(m.switch_to_frame('frame1'))
+    assert(m.switch_to_frame(1))
+    assert(m.switch_to_frame(m.find_element(HTMLElement.ID, 'frameid')))
+    assert(m.switch_to_frame())
+    assert(m.get_window() == server.TEST_CURRENT_WINDOW)
+    assert(m.set_context(m.CONTEXT_CHROME))
+    assert(m.delete_session())
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/test_selenium.py
@@ -0,0 +1,222 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+import threading
+
+try:
+    from selenium import webdriver
+    from selenium.common.exceptions import *
+    from selenium.webdriver.remote.webelement import WebElement
+except:
+    print 'requires selenium Python bindings; pip install selenium'
+    raise
+from selenium_proxy import SeleniumProxy
+from testserver import TestServer
+
+def test_find_element(driver, fn):
+    """ Test a find_element_by_FOO method of both the webdriver
+        and the WebElement class.  The former will return a WebElement which
+        should have a method of the same name, which should also return
+        a WebElement.
+    """
+    element = getattr(driver, fn)('foo')
+    assert(isinstance(element, WebElement))
+    assert(element.id == TestServer.TEST_FIND_ELEMENT)
+    child = getattr(element, fn)('foo')
+    assert(isinstance(child, WebElement))
+    assert(child.id == TestServer.TEST_FIND_ELEMENT)
+
+def test_find_elements(driver, fn):
+    """ Test a find_elements_by_FOO method of both the webdriver
+        and the WebElement class.  The former will return a list of 
+        WebElements, each of which should have a method of the same name,
+        and which in should turn should also return a list of WebElements.
+    """
+    elements = getattr(driver, fn)('foo')
+    # elements should be a list
+    assert(isinstance(elements, list))
+    # elements should match the TEST_FIND_ELEMENTS list
+    assert(map(lambda x: x.id, elements) == TestServer.TEST_FIND_ELEMENTS)
+    # Each member of elements should be a WebElement that has the same
+    # method, which should in turn return a list of WebElements when called.
+    for element in elements:
+        assert(isinstance(element, WebElement))
+        children = getattr(element, fn)('foo')
+        assert(isinstance(children, list))
+        assert(map(lambda x: x.id, children) == TestServer.TEST_FIND_ELEMENTS)
+        assert(len(filter(lambda x: not isinstance(x, WebElement), children)) == 0)
+
+if __name__ == '__main__':
+    # start the test server on port 2626
+    server = TestServer(2626)
+    thread = threading.Thread(target=server.run)
+    thread.daemon = True
+    thread.start()
+
+    # Start the selenium proxy on port 4444, connecting to the test server
+    # on port 2626.
+    proxy = SeleniumProxy('127.0.0.1', 2626, proxy_port=4444)
+    proxy_thread = threading.Thread(target=proxy.start)
+    proxy_thread.daemon = True
+    proxy_thread.start()
+
+    # invoke selenium commands as tests
+    driver = webdriver.Remote(command_executor='http://127.0.0.1:4444',
+                              desired_capabilities=webdriver.DesiredCapabilities.FIREFOX)
+    assert(driver)
+
+    # test navigation methods
+    driver.get(TestServer.TEST_URL)
+    assert(driver.current_url == TestServer.TEST_URL)
+    driver.back()
+    driver.forward()
+    driver.refresh()
+
+    # test script methods
+    driver.set_script_timeout(10) # in selenium the number is in seconds
+    driver.implicitly_wait(10)    # ditto
+
+    assert(TestServer.TEST_EXECUTE_RETURN_VALUE == driver.execute_script(TestServer.TEST_EXECUTE_SCRIPT))
+    assert(TestServer.TEST_EXECUTE_RETURN_VALUE == driver.execute_script(TestServer.TEST_EXECUTE_SCRIPT,
+                                                                         TestServer.TEST_EXECUTE_SCRIPT_ARGS))
+    assert(TestServer.TEST_EXECUTE_RETURN_VALUE == driver.execute_async_script(TestServer.TEST_EXECUTE_SCRIPT))
+    assert(TestServer.TEST_EXECUTE_RETURN_VALUE == driver.execute_async_script(TestServer.TEST_EXECUTE_SCRIPT,
+                                                                               TestServer.TEST_EXECUTE_SCRIPT_ARGS))
+
+    # test all the find_element_by_FOO methods
+    test_find_element(driver, 'find_element_by_name')
+    test_find_element(driver, 'find_element_by_id')
+    test_find_element(driver, 'find_element_by_xpath')
+    test_find_element(driver, 'find_element_by_link_text')
+    test_find_element(driver, 'find_element_by_partial_link_text')
+    test_find_element(driver, 'find_element_by_tag_name')
+    test_find_element(driver, 'find_element_by_class_name')
+    test_find_element(driver, 'find_element_by_css_selector')
+
+    # test all the find_elements_by_FOO methods
+    test_find_elements(driver, 'find_elements_by_name')
+    test_find_elements(driver, 'find_elements_by_id')
+    test_find_elements(driver, 'find_elements_by_xpath')
+    test_find_elements(driver, 'find_elements_by_link_text')
+    test_find_elements(driver, 'find_elements_by_partial_link_text')
+    test_find_elements(driver, 'find_elements_by_tag_name')
+    test_find_elements(driver, 'find_elements_by_class_name')
+    test_find_elements(driver, 'find_elements_by_css_selector')
+
+    # test WebElement methods
+    element = driver.find_element_by_name('foo')
+    element.click()
+    assert(element.text == TestServer.TEST_GET_TEXT)
+    element.send_keys('Mozilla Firefox')
+    element.clear()
+    assert(element.is_selected())
+    assert(element.is_enabled())
+    assert(element.is_displayed())
+    assert(element.get_attribute('id') == TestServer.TEST_GET_VALUE)
+
+    # make the server return error responses so we can test them
+    server.responses = server.error_responses
+
+    # test exception handling
+    try:
+        driver.execute_async_script(TestServer.TEST_EXECUTE_SCRIPT)
+        assert(False)
+    except TimeoutException:
+        # the Selenium Python driver maps SCRIPT_TIMEOUT to TIMEOUT
+        pass
+
+    try:
+        driver.execute_script(TestServer.TEST_EXECUTE_SCRIPT)
+        assert(False)
+    except WebDriverException:
+        # the Selenium Python driver doesn't specifically support JAVASCRIPT_ERROR
+        pass
+
+    try:
+        driver.find_element_by_name('foo')
+        assert(False)
+    except NoSuchElementException:
+        pass
+
+    try:
+        driver.find_elements_by_name('foo')
+        assert(False)
+    except WebDriverException:
+        # the Selenium Python driver doesn't specifically support XPATH_LOOKUP_ERROR
+        pass
+
+    try:
+        driver.close()
+        assert(False)
+    except NoSuchWindowException:
+        pass
+
+    try:
+        element.click()
+        assert(False)
+    except StaleElementReferenceException:
+        pass
+
+    try:
+        element.send_keys('Mozilla Firefox')
+        assert(False)
+    except ElementNotVisibleException:
+        pass
+
+    try:
+        driver.switch_to_frame('aframe')
+        assert(False)
+    except NoSuchFrameException:
+        pass
+
+    # restore normal test responses
+    server.responses = server.test_responses
+
+    # test window methods
+    assert(driver.current_window_handle == TestServer.TEST_CURRENT_WINDOW)
+    assert(driver.window_handles == TestServer.TEST_WINDOW_LIST)
+    driver.switch_to_window(TestServer.TEST_CURRENT_WINDOW)
+
+    # test frame methods
+    driver.switch_to_frame('aframe') # by name or id
+    driver.switch_to_frame(1)        # by index
+    driver.switch_to_frame(element)  # by element reference
+    driver.switch_to_frame(None)     # null; switch to default frame
+
+    driver.close() # this is close_window
+
+    print 'Tests complete!'
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/head.js
@@ -0,0 +1,287 @@
+MARIONETTE_CONTEXT="chrome";
+MARIONETTE_TIMEOUT=120000;
+
+// Must be synchronized with nsIDOMWindowUtils.
+const COMPOSITION_ATTR_RAWINPUT              = 0x02;
+const COMPOSITION_ATTR_SELECTEDRAWTEXT       = 0x03;
+const COMPOSITION_ATTR_CONVERTEDTEXT         = 0x04;
+const COMPOSITION_ATTR_SELECTEDCONVERTEDTEXT = 0x05;
+
+var EventUtils = {
+  /**
+   * see http://mxr.mozilla.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/EventUtils.js
+   */
+  sendMouseEvent: function EventUtils__sendMouseEvent(aEvent, aTarget, aWindow) {
+    if (['click', 'dblclick', 'mousedown', 'mouseup', 'mouseover', 'mouseout'].indexOf(aEvent.type) == -1) {
+      throw new Error("sendMouseEvent doesn't know about event type '" + aEvent.type + "'");
+    }
+
+    if (!aWindow) {
+      aWindow = window;
+    }
+
+    if (typeof(aTarget) == "string") {
+      aTarget = aWindow.document.getElementById(aTarget);
+    }
+
+    var event = aWindow.document.createEvent('MouseEvent');
+
+    var typeArg          = aEvent.type;
+    var canBubbleArg     = true;
+    var cancelableArg    = true;
+    var viewArg          = aWindow;
+    var detailArg        = aEvent.detail        || (aEvent.type == 'click'     ||
+                                                    aEvent.type == 'mousedown' ||
+                                                    aEvent.type == 'mouseup' ? 1 :
+                                                    aEvent.type == 'dblclick'? 2 : 0);
+    var screenXArg       = aEvent.screenX       || 0;
+    var screenYArg       = aEvent.screenY       || 0;
+    var clientXArg       = aEvent.clientX       || 0;
+    var clientYArg       = aEvent.clientY       || 0;
+    var ctrlKeyArg       = aEvent.ctrlKey       || false;
+    var altKeyArg        = aEvent.altKey        || false;
+    var shiftKeyArg      = aEvent.shiftKey      || false;
+    var metaKeyArg       = aEvent.metaKey       || false;
+    var buttonArg        = aEvent.button        || 0;
+    var relatedTargetArg = aEvent.relatedTarget || null;
+
+    event.initMouseEvent(typeArg, canBubbleArg, cancelableArg, viewArg, detailArg,
+                         screenXArg, screenYArg, clientXArg, clientYArg,
+                         ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
+                         buttonArg, relatedTargetArg);
+
+    aTarget.dispatchEvent(event);
+  },
+
+  sendChar: function EventUtils_sendChar(aChar, aWindow) {
+    // DOM event charcodes match ASCII (JS charcodes) for a-zA-Z0-9.
+    var hasShift = (aChar == aChar.toUpperCase());
+    this.synthesizeKey(aChar, { shiftKey: hasShift }, aWindow);
+  },
+
+  sendString: function EventUtils_sendString(aStr, aWindow) {
+    for (var i = 0; i < aStr.length; ++i) {
+      this.sendChar(aStr.charAt(i), aWindow);
+    }
+  },
+
+  sendKey: function EventUtils_sendKey(aKey, aWindow) {
+    var keyName = "VK_" + aKey.toUpperCase();
+    this.synthesizeKey(keyName, { shiftKey: false }, aWindow);
+  },
+
+  _getDOMWindowUtils: function EventUtils__getDOMWindowUtils(aWindow) {
+    if (!aWindow) {
+      aWindow = window;
+    }
+
+    // we need parent.SpecialPowers for:
+    //  layout/base/tests/test_reftests_with_caret.html
+    //  chrome: toolkit/content/tests/chrome/test_findbar.xul
+    //  chrome: toolkit/content/tests/chrome/test_popup_anchor.xul
+    /*if ("SpecialPowers" in window && window.SpecialPowers != undefined) {
+      return SpecialPowers.getDOMWindowUtils(aWindow);
+    }
+    if ("SpecialPowers" in parent && parent.SpecialPowers != undefined) {
+      return parent.SpecialPowers.getDOMWindowUtils(aWindow);
+    }*/
+
+    //TODO: this is assuming we are in chrome space
+    return aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
+                                 getInterface(Components.interfaces.nsIDOMWindowUtils);
+  },
+
+  _computeKeyCodeFromChar: function EventUtils__computeKeyCodeFromChar(aChar) {
+    if (aChar.length != 1) {
+      return 0;
+    }
+    const nsIDOMKeyEvent = Components.interfaces.nsIDOMKeyEvent;
+    if (aChar >= 'a' && aChar <= 'z') {
+      return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'a'.charCodeAt(0);
+    }
+    if (aChar >= 'A' && aChar <= 'Z') {
+      return nsIDOMKeyEvent.DOM_VK_A + aChar.charCodeAt(0) - 'A'.charCodeAt(0);
+    }
+    if (aChar >= '0' && aChar <= '9') {
+      return nsIDOMKeyEvent.DOM_VK_0 + aChar.charCodeAt(0) - '0'.charCodeAt(0);
+    }
+    // returns US keyboard layout's keycode
+    switch (aChar) {
+      case '~':
+      case '`':
+        return nsIDOMKeyEvent.DOM_VK_BACK_QUOTE;
+      case '!':
+        return nsIDOMKeyEvent.DOM_VK_1;
+      case '@':
+        return nsIDOMKeyEvent.DOM_VK_2;
+      case '#':
+        return nsIDOMKeyEvent.DOM_VK_3;
+      case '$':
+        return nsIDOMKeyEvent.DOM_VK_4;
+      case '%':
+        return nsIDOMKeyEvent.DOM_VK_5;
+      case '^':
+        return nsIDOMKeyEvent.DOM_VK_6;
+      case '&':
+        return nsIDOMKeyEvent.DOM_VK_7;
+      case '*':
+        return nsIDOMKeyEvent.DOM_VK_8;
+      case '(':
+        return nsIDOMKeyEvent.DOM_VK_9;
+      case ')':
+        return nsIDOMKeyEvent.DOM_VK_0;
+      case '-':
+      case '_':
+        return nsIDOMKeyEvent.DOM_VK_SUBTRACT;
+      case '+':
+      case '=':
+        return nsIDOMKeyEvent.DOM_VK_EQUALS;
+      case '{':
+      case '[':
+        return nsIDOMKeyEvent.DOM_VK_OPEN_BRACKET;
+      case '}':
+      case ']':
+        return nsIDOMKeyEvent.DOM_VK_CLOSE_BRACKET;
+      case '|':
+      case '\\':
+        return nsIDOMKeyEvent.DOM_VK_BACK_SLASH;
+      case ':':
+      case ';':
+        return nsIDOMKeyEvent.DOM_VK_SEMICOLON;
+      case '\'':
+      case '"':
+        return nsIDOMKeyEvent.DOM_VK_QUOTE;
+      case '<':
+      case ',':
+        return nsIDOMKeyEvent.DOM_VK_COMMA;
+      case '>':
+      case '.':
+        return nsIDOMKeyEvent.DOM_VK_PERIOD;
+      case '?':
+      case '/':
+        return nsIDOMKeyEvent.DOM_VK_SLASH;
+      default:
+        return 0;
+    }
+  },
+
+  _parseModifiers: function EventUtils__parseModifiers(aEvent) {
+    const masks = Components.interfaces.nsIDOMNSEvent;
+    var mval = 0;
+    if (aEvent.shiftKey)
+      mval |= masks.SHIFT_MASK;
+    if (aEvent.ctrlKey)
+      mval |= masks.CONTROL_MASK;
+    if (aEvent.altKey)
+      mval |= masks.ALT_MASK;
+    if (aEvent.metaKey)
+      mval |= masks.META_MASK;
+    if (aEvent.accelKey)
+      mval |= (navigator.platform.indexOf("Mac") >= 0) ? masks.META_MASK :
+                                                         masks.CONTROL_MASK;
+
+    return mval;
+  },
+
+  isKeypressFiredKey: function EventUtils_isKeypressFiredKey(aDOMKeyCode) {
+    if (typeof(aDOMKeyCode) == "string") {
+      if (aDOMKeyCode.indexOf("VK_") == 0) {
+        aDOMKeyCode = KeyEvent["DOM_" + aDOMKeyCode];
+        if (!aDOMKeyCode) {
+          throw "Unknown key: " + aDOMKeyCode;
+        }
+      } else {
+        // If the key generates a character, it must cause a keypress event.
+        return true;
+      }
+    }
+    switch (aDOMKeyCode) {
+      case KeyEvent.DOM_VK_SHIFT:
+      case KeyEvent.DOM_VK_CONTROL:
+      case KeyEvent.DOM_VK_ALT:
+      case KeyEvent.DOM_VK_CAPS_LOCK:
+      case KeyEvent.DOM_VK_NUM_LOCK:
+      case KeyEvent.DOM_VK_SCROLL_LOCK:
+      case KeyEvent.DOM_VK_META:
+        return false;
+      default:
+        return true;
+    }
+  },
+
+  synthesizeKey: function EventUtils_synthesizeKey(aKey, aEvent, aWindow) {
+    var utils = this._getDOMWindowUtils(aWindow);
+    if (utils) {
+      var keyCode = 0, charCode = 0;
+      if (aKey.indexOf("VK_") == 0) {
+        keyCode = KeyEvent["DOM_" + aKey];
+        if (!keyCode) {
+          throw "Unknown key: " + aKey;
+        }
+      } else {
+        charCode = aKey.charCodeAt(0);
+        keyCode = this._computeKeyCodeFromChar(aKey.charAt(0));
+      }
+
+      var modifiers = this._parseModifiers(aEvent);
+
+      if (!("type" in aEvent) || !aEvent.type) {
+        // Send keydown + (optional) keypress + keyup events.
+        var keyDownDefaultHappened =
+            utils.sendKeyEvent("keydown", keyCode, 0, modifiers);
+        if (this.isKeypressFiredKey(keyCode)) {
+          utils.sendKeyEvent("keypress", charCode ? 0 : keyCode, charCode,
+                             modifiers, !keyDownDefaultHappened);
+        }
+        utils.sendKeyEvent("keyup", keyCode, 0, modifiers);
+      } else if (aEvent.type == "keypress") {
+        // Send standalone keypress event.
+        utils.sendKeyEvent(aEvent.type, charCode ? 0 : keyCode,
+                           charCode, modifiers);
+      } else {
+        // Send other standalone event than keypress.
+        utils.sendKeyEvent(aEvent.type, keyCode, 0, modifiers);
+      }
+    }
+  },
+};
+
+function waitForExplicitFinish() {}
+
+var SpecialPowers = {
+  _prefService: Components.classes["@mozilla.org/preferences-service;1"]
+                .getService(Components.interfaces.nsIPrefBranch),
+
+  setBoolPref: function SpecialPowers__setBoolPref(pref, value) {
+    this._prefService.setBoolPref(pref, value);
+  },
+};
+
+var readyAndUnlocked;
+
+// see http://mxr.mozilla.org/mozilla-central/source/testing/mochitest/browser-test.js#478
+function nextStep(arg) {
+  try {
+    __generator.send(arg);
+  } catch(ex if ex instanceof StopIteration) {
+    finish();
+  } catch(ex) {
+    ok(false, "Unhandled exception: " + ex);
+    finish();
+  }
+}
+
+// see http://mxr.mozilla.org/mozilla-central/source/testing/mochitest/browser-test.js#523
+function requestLongerTimeout() {
+  /* no-op! */
+}
+
+// The browser-chrome tests either start with test() or generatorTest().
+var __generator = null;
+if (typeof(test) != 'undefined')
+  test();
+else if (typeof(generatorTest) != 'undefined') {
+  __generator = generatorTest();
+  __generator.next();
+}
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit-tests.ini
@@ -0,0 +1,19 @@
+[DEFAULT]
+; true if the test requires an emulator, otherwise false
+qemu = false
+
+; true if the test is compatible with the browser, otherwise false
+browser = true
+
+; true if the test is compatible with b2g, otherwise false
+b2g = true
+
+; webapi tests
+[include:../../../../../dom/telephony/test/marionette/manifest.ini]
+[include:../../../../../dom/battery/test/marionette/manifest.ini]
+
+; marionette unit tests
+[include:unit/unit-tests.ini]
+
+
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_click.py
@@ -0,0 +1,46 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 os
+from marionette_test import MarionetteTestCase
+
+class TestClick(MarionetteTestCase):
+    def test_click(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        link = self.marionette.find_element("id", "mozLink")
+        link.click()
+        self.assertEqual("Clicked", self.marionette.execute_script("return document.getElementById('mozLink').innerHTML;"))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_async_script.py
@@ -0,0 +1,121 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 ***** 
+
+from marionette_test import MarionetteTestCase, skip_if_b2g
+from errors import JavascriptException, MarionetteException, ScriptTimeoutException
+
+class TestExecuteAsyncContent(MarionetteTestCase):
+    def setUp(self):
+        super(TestExecuteAsyncContent, self).setUp()
+        self.marionette.set_script_timeout(1000)
+
+    def test_execute_async_simple(self):
+        self.assertEqual(1, self.marionette.execute_async_script("arguments[arguments.length-1](1);"))
+
+    def test_execute_async_ours(self):
+        self.assertEqual(1, self.marionette.execute_async_script("marionetteScriptFinished(1);"))
+
+    def test_execute_async_timeout(self):
+        self.assertRaises(ScriptTimeoutException, self.marionette.execute_async_script, "var x = 1;")
+
+    def test_no_timeout(self):
+        self.marionette.set_script_timeout(2000)
+        self.assertTrue(self.marionette.execute_async_script("""
+            var callback = arguments[arguments.length - 1];
+            setTimeout(function() { callback(true); }, 500);
+            """))
+
+    @skip_if_b2g
+    def test_execute_async_unload(self):
+        self.marionette.set_script_timeout(5000)
+        unload = """
+                window.location.href = "about:blank";
+                 """
+        self.assertRaises(JavascriptException, self.marionette.execute_async_script, unload)
+
+    def test_check_window(self):
+        self.assertTrue(self.marionette.execute_async_script("marionetteScriptFinished(window !=null && window != undefined);"))
+
+    def test_same_context(self):
+        var1 = 'testing'
+        self.assertEqual(self.marionette.execute_script("""
+            window.wrappedJSObject._testvar = '%s';
+            return window.wrappedJSObject._testvar;
+            """ % var1), var1)
+        self.assertEqual(self.marionette.execute_async_script(
+            "marionetteScriptFinished(window.wrappedJSObject._testvar);"), var1)
+
+    def test_execute_no_return(self):
+        self.assertEqual(self.marionette.execute_async_script("marionetteScriptFinished()"), None)
+
+    def test_execute_js_exception(self):
+        self.assertRaises(JavascriptException,
+            self.marionette.execute_async_script, "foo(bar);")
+
+    def test_execute_async_js_exception(self):
+        self.assertRaises(JavascriptException,
+            self.marionette.execute_async_script, """
+            var callback = arguments[arguments.length - 1];
+            callback(foo());
+            """)
+
+    def test_script_finished(self):
+        self.assertTrue(self.marionette.execute_async_script("""
+            marionetteScriptFinished(true);
+            """))
+
+    def test_execute_permission(self):
+        self.assertRaises(JavascriptException, self.marionette.execute_async_script, """
+var c = Components.classes;
+marionetteScriptFinished(1);
+""")
+
+class TestExecuteAsyncChrome(TestExecuteAsyncContent):
+    def setUp(self):
+        super(TestExecuteAsyncChrome, self).setUp()
+        self.marionette.set_context("chrome")
+
+    def test_execute_async_unload(self):
+        pass
+
+    def test_same_context(self):
+        pass
+
+    def test_execute_permission(self):
+        self.assertEqual(1, self.marionette.execute_async_script("""
+var c = Components.classes;
+marionetteScriptFinished(1);
+"""))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_isolate.py
@@ -0,0 +1,66 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 ***** 
+
+from marionette_test import MarionetteTestCase, skip_if_b2g
+from errors import JavascriptException, MarionetteException, ScriptTimeoutException
+
+class TestExecuteIsolationContent(MarionetteTestCase):
+    def setUp(self):
+        super(TestExecuteIsolationContent, self).setUp()
+        self.content = True
+
+    def test_execute_async_isolate(self):
+        # Results from one execute call that has timed out should not
+        # contaminate a future call.
+        multiplier = "*3" if self.content else "*1"
+        self.marionette.set_script_timeout(500)
+        self.assertRaises(ScriptTimeoutException,
+                          self.marionette.execute_async_script,
+                          ("setTimeout(function() { marionetteScriptFinished(5%s); }, 3000);"
+                               % multiplier))
+
+        self.marionette.set_script_timeout(6000)
+        result = self.marionette.execute_async_script("""
+setTimeout(function() { marionetteScriptFinished(10%s); }, 5000);
+""" % multiplier)
+        self.assertEqual(result, 30 if self.content else 10)
+
+class TestExecuteIsolationChrome(TestExecuteIsolationContent):
+    def setUp(self):
+        super(TestExecuteIsolationChrome, self).setUp()
+        self.marionette.set_context("chrome")
+        self.content = False
+
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_execute_script.py
@@ -0,0 +1,75 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 ***** 
+
+from marionette_test import MarionetteTestCase
+from errors import JavascriptException, MarionetteException
+
+class TestExecuteContent(MarionetteTestCase):
+    def test_execute_simple(self):
+        self.assertEqual(1, self.marionette.execute_script("return 1;"))
+
+    def test_check_window(self):
+        self.assertTrue(self.marionette.execute_script("return (window !=null && window != undefined);"))
+
+    def test_execute_no_return(self):
+        self.assertEqual(self.marionette.execute_script("1;"), None)
+
+    def test_execute_js_exception(self):
+        self.assertRaises(JavascriptException,
+            self.marionette.execute_script, "return foo(bar);")
+
+    def test_execute_permission(self):
+        self.assertRaises(JavascriptException,
+                          self.marionette.execute_script,
+                          "return Components.classes;")
+
+    def test_complex_return_values(self):
+        self.assertEqual(self.marionette.execute_script("return [1, 2];"), [1, 2])
+        self.assertEqual(self.marionette.execute_script("return {'foo': 'bar', 'fizz': 'fazz'};"),
+                         {'foo': 'bar', 'fizz': 'fazz'})
+        self.assertEqual(self.marionette.execute_script("return [1, {'foo': 'bar'}, 2];"),
+                         [1, {'foo': 'bar'}, 2])
+        self.assertEqual(self.marionette.execute_script("return {'foo': [1, 'a', 2]};"),
+                         {'foo': [1, 'a', 2]})
+
+
+class TestExecuteChrome(TestExecuteContent):
+    def setUp(self):
+        super(TestExecuteChrome, self).setUp()
+        self.marionette.set_context("chrome")
+
+    def test_execute_permission(self):
+        self.assertEqual(1, self.marionette.execute_script("var c = Components.classes;return 1;"))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_findelement.py
@@ -0,0 +1,157 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 os
+from marionette_test import MarionetteTestCase
+from marionette import HTMLElement
+from errors import NoSuchElementException
+
+class TestElements(MarionetteTestCase):
+    def test_id(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementById('mozLink');")
+        found_el = self.marionette.find_element("id", "mozLink")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_tag_name(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementsByTagName('body')[0];")
+        found_el = self.marionette.find_element("tag name", "body")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_class_name(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementsByClassName('linkClass')[0];")
+        found_el = self.marionette.find_element("class name", "linkClass")
+        self.assertEqual(HTMLElement, type(found_el));
+        self.assertTrue(el.id, found_el.id)
+
+    def test_name(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementsByName('myInput')[0];")
+        found_el = self.marionette.find_element("name", "myInput")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+    
+    def test_selector(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementById('testh1');")
+        found_el = self.marionette.find_element("css selector", "h1")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_link_text(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementById('mozLink');")
+        found_el = self.marionette.find_element("link text", "Click me!")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_partial_link_text(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementById('mozLink');")
+        found_el = self.marionette.find_element("partial link text", "Click m")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_xpath(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        el = self.marionette.execute_script("return window.document.getElementById('mozLink');")
+        found_el = self.marionette.find_element("xpath", "id('mozLink')")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_not_found(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        self.assertRaises(NoSuchElementException, self.marionette.find_element, "id", "I'm not on the page")
+
+    def test_timeout(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        self.assertRaises(NoSuchElementException, self.marionette.find_element, "id", "newDiv")
+        self.assertTrue(True, self.marionette.set_search_timeout(4000))
+        self.marionette.navigate(test_html)
+        self.assertEqual(HTMLElement, type(self.marionette.find_element("id", "newDiv")))
+
+class TestElementsChrome(MarionetteTestCase):
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        self.marionette.set_context("chrome")
+
+    def test_id(self):
+        el = self.marionette.execute_script("return window.document.getElementById('main-window');")
+        found_el = self.marionette.find_element("id", "main-window")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_tag_name(self):
+        el = self.marionette.execute_script("return window.document.getElementsByTagName('window')[0];")
+        found_el = self.marionette.find_element("tag name", "window")
+        self.assertEqual(HTMLElement, type(found_el))
+        self.assertTrue(el.id, found_el.id)
+
+    def test_class_name(self):
+        el = self.marionette.execute_script("return window.document.getElementsByClassName('editBookmarkPanelHeaderButton')[0];")
+        found_el = self.marionette.find_element("class name", "editBookmarkPanelHeaderButton")
+        self.assertEqual(HTMLElement, type(found_el));
+        self.assertTrue(el.id, found_el.id)
+
+    def test_xpath(self):
+        el = self.marionette.execute_script("return window.document.getElementById('main-window');")
+        found_el = self.marionette.find_element("xpath", "id('main-window')")
+        self.assertEqual(HTMLElement, type(found_el));
+        self.assertTrue(el.id, found_el.id)
+
+    def test_not_found(self):
+        self.assertRaises(NoSuchElementException, self.marionette.find_element, "id", "I'm not on the page")
+
+
+    def test_timeout(self):
+        self.assertRaises(NoSuchElementException, self.marionette.find_element, "id", "myid")
+        self.assertTrue(True, self.marionette.set_search_timeout(4000))
+        self.marionette.execute_script("window.setTimeout(function() {var b = window.document.createElement('button'); b.id = 'myid'; document.getElementById('main-window').appendChild(b);}, 1000)")
+        self.assertEqual(HTMLElement, type(self.marionette.find_element("id", "myid")))
+        self.marionette.execute_script("window.document.getElementById('main-window').removeChild(window.document.getElementById('myid'));")
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_log.py
@@ -0,0 +1,60 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 os
+from marionette_test import MarionetteTestCase
+
+class TestLog(MarionetteTestCase):
+    def test_log_basic(self):
+        self.marionette.log("I am info")
+        self.assertTrue("I am info" in self.marionette.get_logs()[0])
+        self.marionette.log("I AM ERROR", "ERROR")
+        self.assertTrue("I AM ERROR" in self.marionette.get_logs()[1])
+
+    def test_log_script(self):
+        self.marionette.execute_script("log('some log');")
+        self.assertTrue("some log" in self.marionette.get_logs()[0])
+        self.marionette.execute_script("log('some error', 'ERROR');")
+        self.assertTrue("some error" in self.marionette.get_logs()[1])
+        self.marionette.set_script_timeout(2000)
+        self.marionette.execute_async_script("log('some more logs'); finish();")
+        self.assertTrue("some more logs" in self.marionette.get_logs()[2])
+        self.marionette.execute_async_script("log('some more errors', 'ERROR'); finish();")
+        self.assertTrue("some more errors" in self.marionette.get_logs()[3])
+
+class TestLogChrome(TestLog):
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        self.marionette.set_context("chrome")
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_navigation.py
@@ -0,0 +1,93 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 os
+from marionette_test import MarionetteTestCase
+
+class TestNavigate(MarionetteTestCase):
+    def test_navigate(self):
+        self.assertTrue(self.marionette.execute_script("window.location.href = 'about:blank'; return true;"))
+        self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.assertEqual("Marionette Test", self.marionette.execute_script("return window.document.title;"))
+
+    def test_getUrl(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        self.assertTrue(test_html in self.marionette.get_url())
+        self.marionette.navigate("about:blank")
+        self.assertEqual("about:blank", self.marionette.get_url())
+
+    def test_goBack(self):
+        self.assertTrue(self.marionette.execute_script("window.location.href = 'about:blank'; return true;"))
+        self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.assertEqual("Marionette Test", self.marionette.execute_script("return window.document.title;"))
+        self.marionette.navigate("about:blank")
+        self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.marionette.go_back()
+        self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.assertEqual("Marionette Test", self.marionette.execute_script("return window.document.title;"))
+
+    def test_goForward(self):
+        self.assertTrue(self.marionette.execute_script("window.location.href = 'about:blank'; return true;"))
+        self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.assertEqual("Marionette Test", self.marionette.execute_script("return window.document.title;"))
+        self.marionette.navigate("about:blank")
+        self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.marionette.go_back()
+        self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.assertEqual("Marionette Test", self.marionette.execute_script("return window.document.title;"))
+        self.marionette.go_forward()
+        self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+
+    def test_refresh(self):
+        test_html = self.marionette.absolute_url("test.html")
+        self.marionette.navigate(test_html)
+        self.assertEqual("Marionette Test", self.marionette.execute_script("return window.document.title;"))
+        self.assertTrue(self.marionette.execute_script("var elem = window.document.createElement('div'); elem.id = 'someDiv';" +
+                                        "window.document.body.appendChild(elem); return true;"))
+        self.assertFalse(self.marionette.execute_script("return window.document.getElementById('someDiv') == undefined;"))
+        self.marionette.refresh()
+        self.assertEqual("Marionette Test", self.marionette.execute_script("return window.document.title;"))
+        self.assertTrue(self.marionette.execute_script("return window.document.getElementById('someDiv') == undefined;"))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_simpletest_chrome.js
@@ -0,0 +1,12 @@
+/* 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/. */
+
+MARIONETTE_TIMEOUT = 1000;
+MARIONETTE_CONTEXT = 'chrome';
+
+is(2, 2, "test for is()");
+isnot(2, 3, "test for isnot()");
+ok(2 == 2, "test for ok()");
+finish();
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_simpletest_fail.js
@@ -0,0 +1,16 @@
+/* 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/. */
+
+MARIONETTE_TIMEOUT = 1000;
+
+/* this test will fail */
+
+setTimeout(function() { 
+    is(1, 2); 
+    finish();
+}, 100);
+isnot(1, 1);
+ok(1 == 2);
+
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_simpletest_pass.js
@@ -0,0 +1,11 @@
+/* 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/. */
+
+MARIONETTE_TIMEOUT = 1000;
+
+is(2, 2, "test for is()");
+isnot(2, 3, "test for isnot()");
+ok(2 == 2, "test for ok()");
+setTimeout(finish, 100);
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_simpletest_sanity.py
@@ -0,0 +1,80 @@
+# 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/.
+
+from marionette_test import MarionetteTestCase
+from errors import JavascriptException, MarionetteException, ScriptTimeoutException
+
+class SimpletestSanityTest(MarionetteTestCase):
+
+    callFinish = "return finish();"
+
+    def test_is(self):
+        def runtests():
+            sentFail1 = "is(true, false, 'isTest1');" + self.callFinish
+            sentFail2 = "is(true, false, 'isTest2');" + self.callFinish
+            sentPass1 = "is(true, true, 'isTest3');" + self.callFinish
+            sentPass2 = "is(true, true, 'isTest4');" + self.callFinish
+
+            self.assertEqual(1, self.marionette.execute_script(sentFail1)["failed"])
+            self.assertEqual(0, self.marionette.execute_script(sentFail2)["passed"])
+            self.assertEqual(1, self.marionette.execute_script(sentPass1)["passed"])
+            self.assertEqual(0, self.marionette.execute_script(sentPass2)["failed"])
+
+            self.marionette.set_script_timeout(1000)
+            self.assertEqual(1, self.marionette.execute_async_script(sentFail1)["failed"])
+            self.assertEqual(0, self.marionette.execute_async_script(sentFail2)["passed"])
+            self.assertEqual(1, self.marionette.execute_async_script(sentPass1)["passed"])
+            self.assertEqual(0, self.marionette.execute_async_script(sentPass2)["failed"])
+
+        self.marionette.set_context("content")
+        runtests()
+        self.marionette.set_context("chrome")
+        runtests()
+
+    def test_isnot(self):
+        def runtests():
+           sentFail1 = "isnot(true, true, 'isTest3');" + self.callFinish
+           sentFail2 = "isnot(true, true, 'isTest4');" + self.callFinish
+           sentPass1 = "isnot(true, false, 'isTest1');" + self.callFinish
+           sentPass2 = "isnot(true, false, 'isTest2');" + self.callFinish
+
+           self.assertEqual(1, self.marionette.execute_script(sentFail1)["failed"]);
+           self.assertEqual(0, self.marionette.execute_script(sentFail2)["passed"]);
+           self.assertEqual(0, self.marionette.execute_script(sentPass1)["failed"]);
+           self.assertEqual(1, self.marionette.execute_script(sentPass2)["passed"]);
+
+           self.marionette.set_script_timeout(1000)
+           self.assertEqual(1, self.marionette.execute_async_script(sentFail1)["failed"]);
+           self.assertEqual(0, self.marionette.execute_async_script(sentFail2)["passed"]);
+           self.assertEqual(0, self.marionette.execute_async_script(sentPass1)["failed"]);
+           self.assertEqual(1, self.marionette.execute_async_script(sentPass2)["passed"]);
+
+        self.marionette.set_context("content")
+        runtests()
+        self.marionette.set_context("chrome")
+        runtests()
+
+    def test_ok(self):
+        def runtests():
+            sentFail1 = "ok(1==2, 'testOk');" + self.callFinish
+            sentFail2 = "ok(1==2, 'testOk');" + self.callFinish
+            sentPass1 = "ok(1==1, 'testOk');" + self.callFinish
+            sentPass2 = "ok(1==1, 'testOk');" + self.callFinish
+
+            self.assertEqual(1, self.marionette.execute_script(sentFail1)["failed"]);
+            self.assertEqual(0, self.marionette.execute_script(sentFail2)["passed"]);
+            self.assertEqual(0, self.marionette.execute_script(sentPass1)["failed"]);
+            self.assertEqual(1, self.marionette.execute_script(sentPass2)["passed"]);
+
+            self.marionette.set_script_timeout(1000)
+            self.assertEqual(1, self.marionette.execute_async_script(sentFail1)["failed"]);
+            self.assertEqual(0, self.marionette.execute_async_script(sentFail2)["passed"]);
+            self.assertEqual(0, self.marionette.execute_async_script(sentPass1)["failed"]);
+            self.assertEqual(1, self.marionette.execute_async_script(sentPass2)["passed"]);
+
+        self.marionette.set_context("content")
+        runtests()
+        self.marionette.set_context("chrome")
+        runtests()
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_simpletest_timeout.js
@@ -0,0 +1,16 @@
+/* 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/. */
+
+MARIONETTE_TIMEOUT = 100;
+
+/* this test will timeout */
+
+function do_test() {
+  is(1, 1);
+  isnot(1, 2);
+  ok(1 == 1);
+  finish();
+}
+
+setTimeout(do_test, 1000);
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_switch_frame.py
@@ -0,0 +1,48 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 os
+from marionette_test import MarionetteTestCase
+
+class TestSwitchFrame(MarionetteTestCase):
+    def test_switch_simple(self):
+        self.assertTrue(self.marionette.execute_script("window.location.href = 'about:blank'; return true;"))
+        self.assertEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        test_html = self.marionette.absolute_url("test_iframe.html")
+        self.marionette.navigate(test_html)
+        self.assertNotEqual("about:blank", self.marionette.execute_script("return window.location.href;"))
+        self.assertEqual("Marionette IFrame Test", self.marionette.execute_script("return window.document.title;"))
+        self.marionette.switch_to_frame("test_iframe")
+        self.assertTrue("test.html" in self.marionette.get_url())
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_switch_frame_b2g.py
@@ -0,0 +1,31 @@
+# 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/.
+
+from marionette_test import MarionetteTestCase
+from errors import *
+
+class TestGaiaLaunch(MarionetteTestCase):
+    """Trivial example of launching a Gaia app, entering its context and performing some test on it.
+    """
+
+    def test_launch_app(self):
+        # Launch a Gaia app; see CommonTestCase.launch_gaia_app in 
+        # marionette.py for implementation.  This returns an HTMLElement
+        # object representing the iframe the app was loaded in.
+        app_frame = self.launch_gaia_app('../sms/sms.html')
+
+        # Verify that the <title> element of the content loaded in the 
+        # iframe contains the text 'Messages'.
+        page_title = self.marionette.execute_script("""
+var frame = arguments[0];
+return frame.contentWindow.document.getElementsByTagName('title')[0].innerHTML;
+""", [app_frame])
+        self.assertEqual(page_title, 'Messages')
+
+        self.marionette.switch_to_frame(0)
+        self.assertEqual(self.marionette.execute_script("return window.document.getElementsByTagName('title')[0].innerHTML;"), 'Messages')
+        self.assertTrue("sms" in self.marionette.execute_script("return document.location.href;"))
+        self.marionette.switch_to_frame()
+        self.assertTrue("homescreen" in self.marionette.execute_script("return document.location.href;"))
+
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/test_window_management.py
@@ -0,0 +1,92 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# the Initial Developer. All Rights Reserved. 
+# 
+# Contributor(s): 
+# 
+# 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 os
+from marionette_test import MarionetteTestCase
+
+class TestSwitchWindow(MarionetteTestCase):
+    def open_new_window(self):
+        self.marionette.set_context("chrome")
+        self.marionette.set_script_timeout(5000)
+        self.marionette.execute_async_script("""
+                                        var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+                                                 .getService(Components.interfaces.nsIWindowWatcher); 
+                                        var win = ww.openWindow(null, "chrome://browser/content/browser.xul", "testWin", null, null);
+                                        win.addEventListener("load", function() { 
+                                                                        win.removeEventListener("load", arguments.callee, true); 
+                                                                        marionetteScriptFinished();
+                                                                        }, null);
+                                        """)
+        self.marionette.set_context("content")
+
+    def close_new_window(self):
+        self.marionette.set_context("chrome")
+        self.marionette.execute_script("""
+                                        var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+                                                 .getService(Components.interfaces.nsIWindowWatcher); 
+                                        var win = ww.getWindowByName("testWin", null);
+                                        if (win != null)
+                                          win.close();
+                                        """)
+        self.marionette.set_context("content")
+
+    def test_windows(self):
+        orig_win = self.marionette.get_window()
+        orig_available = self.marionette.get_windows()
+        self.open_new_window()
+        #assert we're still in the original window
+        self.assertEqual(self.marionette.get_window(), orig_win)
+        now_available = self.marionette.get_windows()
+        #assert we can find the new window
+        self.assertEqual(len(now_available), len(orig_available) + 1) 
+        #assert that our window is there
+        self.assertTrue(orig_win in now_available)
+        new_win = None
+        for win in now_available:
+            if win != orig_win:
+                new_win = orig_win
+        #switch to another window
+        self.marionette.switch_to_window(new_win)
+        self.assertEqual(self.marionette.get_window(), new_win)
+        #switch back
+        self.marionette.switch_to_window(orig_win)
+        self.close_new_window()
+        self.assertEqual(self.marionette.get_window(), orig_win)
+        self.assertEqual(len(self.marionette.get_windows()), len(orig_available))
+
+    def tearDown(self):
+        #ensure that we close the window, regardless of pass/failure
+        self.close_new_window()
+        MarionetteTestCase.tearDown(self)
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -0,0 +1,22 @@
+[test_click.py]
+b2g = false
+
+[test_log.py]
+[test_execute_async_script.py]
+[test_execute_script.py]
+[test_simpletest_fail.js]
+[test_findelement.py]
+b2g = false
+
+[test_navigation.py]
+b2g = false
+
+[test_simpletest_pass.js]
+[test_simpletest_sanity.py]
+[test_simpletest_chrome.js]
+[test_simpletest_timeout.js]
+[test_switch_frame.py]
+b2g = false
+
+[test_window_management.py]
+b2g = false
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/testserver.py
@@ -0,0 +1,236 @@
+# ***** 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 Marionette Client. 
+# 
+# The Initial Developer of the Original Code is 
+#   Mozilla Foundation. 
+# Portions created by the Initial Developer are Copyright (C) 2011
+# 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 ***** 
+
+import json
+import select
+import socket
+
+class TestServer(object):
+    """ A test Marionette server which can be used to test the Marionette
+        protocol.  Each request will trigger a canned response; see
+        process_command().
+    """
+
+    TEST_URL = 'http://www.mozilla.org'
+    TEST_CURRENT_WINDOW = 'window1'
+    TEST_WINDOW_LIST = ['window1', 'window2', 'window3']
+    TEST_EXECUTE_RETURN_VALUE = 10
+    TEST_EXECUTE_SCRIPT = 'return 2 * 5;'
+    TEST_EXECUTE_SCRIPT_ARGS = 'testing'
+    TEST_FIND_ELEMENT = 'element1'
+    TEST_FIND_ELEMENTS = ['element1', 'element2', 'element3']
+    TEST_GET_TEXT = 'first name'
+    TEST_GET_VALUE = 'Mozilla Firefox'
+
+    # canned responses for test messages
+    test_responses = {
+        'newSession': { 'value': 'a65bef90b145' },
+        'getMarionetteID': { 'id': 'conn0.marionette' },
+        'deleteSession': { 'ok': True },
+        'setScriptTimeout': { 'ok': True },
+        'setSearchTimeout': { 'ok': True },
+        'getWindow': { 'value': TEST_CURRENT_WINDOW },
+        'getWindows': { 'values': TEST_WINDOW_LIST },
+        'closeWindow': { 'ok': True },
+        'switchToWindow': { 'ok': True },
+        'switchToFrame': { 'ok': True },
+        'setContext': { 'ok': True },
+        'getUrl' : { 'value': TEST_URL },
+        'goUrl': { 'ok': True },
+        'goBack': { 'ok': True },
+        'goForward': { 'ok': True },
+        'refresh': { 'ok': True },
+        'executeScript': { 'value': TEST_EXECUTE_RETURN_VALUE },
+        'executeAsyncScript': { 'value': TEST_EXECUTE_RETURN_VALUE },
+        'executeJSScript': { 'value': TEST_EXECUTE_RETURN_VALUE },
+        'findElement': { 'value': TEST_FIND_ELEMENT },
+        'findElements': { 'values': TEST_FIND_ELEMENTS },
+        'clickElement': { 'ok': True },
+        'getElementText': { 'value': TEST_GET_TEXT },
+        'sendKeysToElement': { 'ok': True },
+        'getElementValue': { 'value': TEST_GET_VALUE },
+        'clearElement': { 'ok': True },
+        'isElementSelected': { 'value': True },
+        'elementsEqual': { 'value': True },
+        'isElementEnabled': { 'value': True },
+        'isElementDisplayed': { 'value': True },
+        'getElementAttribute': { 'value': TEST_GET_VALUE },
+        'getSessionCapabilities': { 'value': {
+            "cssSelectorsEnabled": True,
+            "browserName": "firefox",
+            "handlesAlerts": True,
+            "javascriptEnabled": True,
+            "nativeEvents": True,
+            "platform": 'linux',
+            "takeScreenshot": False,
+            "version": "10.1"
+            }
+        },
+        'getStatus': { 'value': {
+            "os": {
+                "arch": "x86",
+                "name": "linux",
+                "version": "unknown"
+                },
+            "build": {
+                "revision": "unknown",
+                "time": "unknown",
+                "version": "unknown"
+                }
+            }
+        }
+    }
+
+    # canned error responses for test messages
+    error_responses = {
+        'executeScript': { 'error': { 'message': 'JavaScript error', 'status': 17 } },
+        'executeAsyncScript': { 'error': { 'message': 'Script timed out', 'status': 28 } },
+        'findElement': { 'error': { 'message': 'Element not found', 'status': 7 } },
+        'findElements': { 'error': { 'message': 'XPath is invalid', 'status': 19 } },
+        'closeWindow': { 'error': { 'message': 'No such window', 'status': 23 } },
+        'getWindow': { 'error': { 'message': 'No such window', 'status': 23 } },
+        'clickElement': { 'error': { 'message': 'Element no longer exists', 'status': 10 } },
+        'sendKeysToElement': { 'error': { 'message': 'Element is not visible on the page', 'status': 11 } },
+        'switchToFrame': { 'error': { 'message': 'No such frame', 'status': 8 } }
+    }
+
+    def __init__(self, port):
+        self.port = port
+
+        self.srvsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.srvsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self.srvsock.bind(("", port))
+        self.srvsock.listen(5)
+        self.descriptors = [self.srvsock]
+        self.responses = self.test_responses
+        print 'TestServer started on port %s' % port
+
+    def _recv_n_bytes(self, sock, n):
+        """ Convenience method for receiving exactly n bytes from
+            self.sock (assuming it's open and connected).
+        """
+        data = ''
+        while len(data) < n:
+            chunk = sock.recv(n - len(data))
+            if chunk == '':
+                break
+            data += chunk
+        return data
+
+    def receive(self, sock):
+        """ Receive the next complete response from the server, and return
+            it as a dict.  Each response from the server is prepended by
+            len(message) + ':'.
+        """
+        assert(sock)
+        response = sock.recv(10)
+        sep = response.find(':')
+        if sep == -1:
+            return None
+        length = response[0:sep]
+        response = response[sep + 1:]
+        response += self._recv_n_bytes(sock, int(length) + 1 + len(length) - 10)
+        print 'received', response
+        return json.loads(response)
+
+    def send(self, sock, msg):
+        print 'msg', msg
+        data = json.dumps(msg)
+        print 'sending %s' % data
+        sock.send('%s:%s' % (len(data), data))
+
+    def accept_new_connection(self):
+        newsock, (remhost, remport) = self.srvsock.accept()
+        self.descriptors.append( newsock )
+        str = 'Client connected %s:%s\r\n' % (remhost, remport)
+        print str
+        self.send(newsock, {'from': 'root',
+                            'applicationType': 'gecko',
+                            'traits': []})
+
+    def process_command(self, data):
+        command = data['type']
+
+        if command == 'use_test_responses':
+            self.responses = self.test_responses
+            return { 'ok': True }
+        elif command == 'use_error_responses':
+            self.responses = self.error_responses
+            return { 'ok': True }
+
+        if command in self.responses:
+            response = self.responses[command]
+        else:
+            response = { 'error': { 'message': 'unknown command: %s' % command, 'status': 500} }
+
+        if command not in ('newSession', 'getStatus', 'getMarionetteID') and 'session' not in data:
+            response = { 'error': { 'message': 'no session specified', 'status': 500 } }
+
+        return response
+
+    def run(self):
+        while 1:
+            # Await an event on a readable socket descriptor
+            (sread, swrite, sexc) = select.select( self.descriptors, [], [] )
+            # Iterate through the tagged read descriptors
+            for sock in sread:
+                # Received a connect to the server (listening) socket
+                if sock == self.srvsock:
+                    self.accept_new_connection()
+                else:
+                    # Received something on a client socket
+                    try:
+                        data = self.receive(sock)
+                    except:
+                        data = None
+                    # Check to see if the peer socket closed
+                    if data is None:
+                        host,port = sock.getpeername()
+                        str = 'Client disconnected %s:%s\r\n' % (host, port)
+                        print str
+                        sock.close
+                        self.descriptors.remove(sock)
+                    else:
+                        if 'type' in data:
+                            msg = self.process_command(data)
+                        else:
+                            msg = 'command: %s' % json.dumps(data)
+                        self.send(sock, msg)
+
+
+if __name__ == "__main__":
+    server = TestServer(2626)
+    server.run()
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/venv_automation.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+# ***** 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) 2012
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#    Malini Das (mdas@mozilla.com)
+#    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 *****
+
+PYTHON=$1
+
+if [ -z "${PYTHON}" ]
+then
+    echo "No python found"
+    exit 1
+fi
+
+# Check if environment exists, if not, create a virtualenv:
+if [ -d "marionette_auto_venv" ]
+then
+  cd marionette_auto_venv
+  . bin/activate
+else
+  curl https://raw.github.com/pypa/virtualenv/develop/virtualenv.py | ${PYTHON} - marionette_auto_venv 
+  cd marionette_auto_venv
+  . bin/activate
+
+  # set up mozbase
+  git clone git://github.com/mozilla/mozbase.git
+  cd mozbase
+  python setup_development.py
+  cd ..
+
+  # set up mozautolog
+  hg clone http://hg.mozilla.org/users/jgriffin_mozilla.com/mozautolog/
+  cd mozautolog
+  python setup.py develop
+  cd ..
+
+  # set up gitpython
+  easy_install http://pypi.python.org/packages/source/G/GitPython/GitPython-0.3.2.RC1.tar.gz
+fi
+
+cd ../..
+# update the marionette_client
+python setup.py develop
+cd marionette
+
+#pop off the python parameter
+shift
+python runtests.py $@
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/venv_test.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+# ***** 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) 2012
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#    Malini Das (mdas@mozilla.com)
+#    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 *****
+
+PYTHON=$1
+
+if [ -z "${PYTHON}" ]
+then
+    echo "No python found"
+    exit 1
+fi
+
+# Check if environment exists, if not, create a virtualenv:
+if [ -d "marionette_venv" ]
+then
+  cd marionette_venv
+  . bin/activate
+else
+  curl https://raw.github.com/pypa/virtualenv/develop/virtualenv.py | ${PYTHON} - marionette_venv 
+  cd marionette_venv
+  . bin/activate
+  # set up mozbase
+  git clone git://github.com/mozilla/mozbase.git
+  cd mozbase
+  python setup_development.py
+  cd ..
+fi
+
+cd ../..
+# update the marionette_client
+python setup.py develop
+cd marionette
+
+#pop off the python parameter
+shift
+python runtests.py $@
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/www/test.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<html>
+<head>
+<title>Marionette Test</title>
+</head>
+<body>
+  <h1 id="testh1">Test Page</h1>
+  <script type="text/javascript">
+    window.ready = true;
+    setTimeout(addDelayedElement, 1000);
+    function addDelayedElement() {
+      var newDiv = document.createElement("div");
+      newDiv.id = "newDiv";
+      var newContent = document.createTextNode("I am a newly created div!");
+      newDiv.appendChild(newContent);
+      document.body.appendChild(newDiv);
+    }
+    function clicked() {
+      var link = document.getElementById("mozLink");
+      link.innerHTML = "Clicked";
+    }
+  </script>
+  <a href="#" id="mozLink" class="linkClass" onclick="clicked()">Click me!</a>
+  <a href="#" id="mozLink" class="linkClass" onclick="clicked()">Click me!</a>
+  <input name="myInput" type="text" />
+</body>
+</html> 
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/www/test_iframe.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<html>
+<head>
+<title>Marionette IFrame Test</title>
+</head>
+<body>
+  <iframe src="test.html" id="test_iframe"></iframe> 
+</body>
+</html> 
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/setup.py
@@ -0,0 +1,31 @@
+import os
+from setuptools import setup, find_packages
+
+version = '0.2'
+
+# get documentation from the README
+try:
+    here = os.path.dirname(os.path.abspath(__file__))
+    description = file(os.path.join(here, 'README.md')).read()
+except (OSError, IOError):
+    description = ''
+
+# dependencies
+deps = []
+
+setup(name='marionette',
+      version=version,
+      description="Marionette test automation client",
+      long_description=description,
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='mozilla',
+      author='Jonathan Griffin',
+      author_email='jgriffin@mozilla.com',
+      url='https://wiki.mozilla.org/Auto-tools/Projects/Marionette',
+      license='MPL',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=deps,
+      )
+