new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/all_tests.json
@@ -0,0 +1,28 @@
+{ "tests": [
+ "test_sync.js",
+ "test_prefs.js",
+ "test_tabs.js",
+ "test_passwords.js",
+ "test_history.js",
+ "test_formdata.js",
+ "test_bug530717.js",
+ "test_bug531489.js",
+ "test_bug538298.js",
+ "test_bug556509.js",
+ "test_bug562515.js",
+ "test_bug563989.js",
+ "test_bug535326.js",
+ "test_bug501528.js",
+ "test_bug575423.js",
+ "test_bug546807.js",
+ "test_history_collision.js",
+ "test_privbrw_formdata.js",
+ "test_privbrw_passwords.js",
+ "test_privbrw_tabs.js",
+ "test_bookmarks_in_same_named_folder.js",
+ "test_client_wipe.js",
+ "test_special_tabs.js"
+ ]
+}
+
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bookmarks_in_same_named_folder.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// bug 558077
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1"};
+
+var bookmarks_initial_1 = {
+ "menu": [
+ { folder: "aaa",
+ description: "foo"
+ },
+ { uri: "http://www.mozilla.com"
+ }
+ ],
+ "menu/aaa": [
+ { uri: "http://www.yahoo.com",
+ title: "testing Yahoo"
+ },
+ { uri: "http://www.google.com",
+ title: "testing Google"
+ }
+ ]
+};
+
+var bookmarks_initial_2 = {
+ "menu": [
+ { folder: "aaa",
+ description: "bar"
+ },
+ { uri: "http://www.mozilla.com"
+ }
+ ],
+ "menu/aaa": [
+ { uri: "http://bugzilla.mozilla.org/show_bug.cgi?id=%s",
+ title: "Bugzilla"
+ },
+ { uri: "http://www.apple.com",
+ tags: [ "apple" ]
+ }
+ ]
+};
+
+Phase('phase1', [
+ [Bookmarks.add, bookmarks_initial_1],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial_1],
+ [Bookmarks.add, bookmarks_initial_2],
+ [Sync]
+]);
+
+Phase('phase3', [
+ [Sync],
+ // XXX [Bookmarks.verify, bookmarks_initial_1],
+ [Bookmarks.verify, bookmarks_initial_2]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug501528.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Password lists
+ */
+
+var passwords_initial = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "secret",
+ usernameField: "uname",
+ passwordField: "pword",
+ changes: {
+ password: "SeCrEt$$$"
+ }
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "jack",
+ password: "secretlogin"
+ }
+];
+
+var passwords_after_first_update = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "SeCrEt$$$",
+ usernameField: "uname",
+ passwordField: "pword"
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "jack",
+ password: "secretlogin"
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Passwords.add, passwords_initial],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Passwords.add, passwords_initial],
+ [Sync]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [Passwords.verify, passwords_initial],
+ [Passwords.modify, passwords_initial],
+ [Passwords.verify, passwords_after_first_update],
+ [Sync]
+]);
+
+Phase('phase4', [
+ [Sync],
+ [Passwords.verify, passwords_after_first_update],
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug530717.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1"};
+
+/*
+ * Preference lists
+ */
+
+var prefs1 = [
+ { name: "browser.startup.homepage",
+ value: "http://www.getfirefox.com"
+ },
+ { name: "browser.urlbar.maxRichResults",
+ value: 20
+ },
+ { name: "browser.tabs.autoHide",
+ value: true
+ }
+];
+
+var prefs2 = [
+ { name: "browser.startup.homepage",
+ value: "http://www.mozilla.com"
+ },
+ { name: "browser.urlbar.maxRichResults",
+ value: 18
+ },
+ { name: "browser.tabs.autoHide",
+ value: false
+ }
+];
+
+/*
+ * Test phases
+ */
+
+// Add prefs to profile1 and sync.
+Phase('phase1', [
+ [Prefs.modify, prefs1],
+ [Prefs.verify, prefs1],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+// Sync profile2 and verify same prefs are present.
+Phase('phase2', [
+ [Sync],
+ [Prefs.verify, prefs1]
+]);
+
+// Using profile1, change some prefs, then do another sync with wipe-client.
+// Verify that the cloud's prefs are restored, and the recent local changes
+// discarded.
+Phase('phase3', [
+ [Prefs.modify, prefs2],
+ [Prefs.verify, prefs2],
+ [Sync, SYNC_WIPE_CLIENT],
+ [Prefs.verify, prefs1]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug531489.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1"};
+
+/*
+ * Bookmark asset lists: these define bookmarks that are used during the test
+ */
+
+// the initial list of bookmarks to add to the browser
+var bookmarks_initial = {
+ "menu": [
+ { folder: "foldera" },
+ { uri: "http://www.google.com",
+ title: "Google"
+ }
+ ],
+ "menu/foldera": [
+ { uri: "http://www.google.com",
+ title: "Google"
+ }
+ ],
+ "toolbar": [
+ { uri: "http://www.google.com",
+ title: "Google"
+ }
+ ]
+};
+
+/*
+ * Test phases
+ */
+
+// Add three bookmarks with the same url to different locations and sync.
+Phase('phase1', [
+ [Bookmarks.add, bookmarks_initial],
+ [Bookmarks.verify, bookmarks_initial],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+// Sync to profile2 and verify that all three bookmarks are present
+Phase('phase2', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
+// Sync again to profile1 and verify that all three bookmarks are still
+// present.
+Phase('phase3', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug535326.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2"};
+
+var tabs1 = [
+ { uri: "data:text/html,<html><head><title>Howdy</title></head><body>Howdy</body></html>",
+ title: "Howdy",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>America</title></head><body>America</body></html>",
+ title: "America",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Apple</title></head><body>Apple</body></html>",
+ title: "Apple",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>This</title></head><body>This</body></html>",
+ title: "This",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Bug</title></head><body>Bug</body></html>",
+ title: "Bug",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>IRC</title></head><body>IRC</body></html>",
+ title: "IRC",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Tinderbox</title></head><body>Tinderbox</body></html>",
+ title: "Tinderbox",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Fox</title></head><body>Fox</body></html>",
+ title: "Fox",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Hello</title></head><body>Hello</body></html>",
+ title: "Hello",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Eagle</title></head><body>Eagle</body></html>",
+ title: "Eagle",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Train</title></head><body>Train</body></html>",
+ title: "Train",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Macbook</title></head><body>Macbook</body></html>",
+ title: "Macbook",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Clock</title></head><body>Clock</body></html>",
+ title: "Clock",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Google</title></head><body>Google</body></html>",
+ title: "Google",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Human</title></head><body>Human</body></html>",
+ title: "Human",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Jetpack</title></head><body>Jetpack</body></html>",
+ title: "Jetpack",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Selenium</title></head><body>Selenium</body></html>",
+ title: "Selenium",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Mozilla</title></head><body>Mozilla</body></html>",
+ title: "Mozilla",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Firefox</title></head><body>Firefox</body></html>",
+ title: "Firefox",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Weave</title></head><body>Weave</body></html>",
+ title: "Weave",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Android</title></head><body>Android</body></html>",
+ title: "Android",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Bye</title></head><body>Bye</body></html>",
+ title: "Bye",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Hi</title></head><body>Hi</body></html>",
+ title: "Hi",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Final</title></head><body>Final</body></html>",
+ title: "Final",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Fennec</title></head><body>Fennec</body></html>",
+ title: "Fennec",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Mobile</title></head><body>Mobile</body></html>",
+ title: "Mobile",
+ profile: "profile1"
+ }
+];
+
+Phase('phase1', [
+ [Tabs.add, tabs1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Tabs.verify, tabs1]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug538298.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Bookmark asset lists: these define bookmarks that are used during the test
+ */
+
+// the initial list of bookmarks to add to the browser
+var bookmarks_initial = {
+ "toolbar": [
+ { uri: "http://www.google.com",
+ title: "Google"
+ },
+ { uri: "http://www.cnn.com",
+ title: "CNN",
+ changes: {
+ position: "Google"
+ }
+ },
+ { uri: "http://www.mozilla.com",
+ title: "Mozilla"
+ },
+ { uri: "http://www.firefox.com",
+ title: "Firefox",
+ changes: {
+ position: "Mozilla"
+ }
+ }
+ ]
+};
+
+var bookmarks_after_move = {
+ "toolbar": [
+ { uri: "http://www.cnn.com",
+ title: "CNN"
+ },
+ { uri: "http://www.google.com",
+ title: "Google"
+ },
+ { uri: "http://www.firefox.com",
+ title: "Firefox"
+ },
+ { uri: "http://www.mozilla.com",
+ title: "Mozilla"
+ }
+ ]
+};
+
+/*
+ * Test phases
+ */
+
+// Add four bookmarks to the toolbar and sync.
+Phase('phase1', [
+ [Bookmarks.add, bookmarks_initial],
+ [Bookmarks.verify, bookmarks_initial],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+// Sync to profile2 and verify that all four bookmarks are present.
+Phase('phase2', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
+// Change the order of the toolbar bookmarks, and sync.
+Phase('phase3', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial],
+ [Bookmarks.modify, bookmarks_initial],
+ [Bookmarks.verify, bookmarks_after_move],
+ [Sync],
+]);
+
+// Go back to profile2, sync, and verify that the bookmarks are reordered
+// as expected.
+Phase('phase4', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_after_move]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug546807.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2"};
+
+/*
+ * Tabs data
+ */
+
+var tabs1 = [
+ { uri: "about:config",
+ profile: "profile1"
+ },
+ { uri: "about:credits",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Apple</title></head><body>Apple</body></html>",
+ title: "Apple",
+ profile: "profile1"
+ }
+];
+
+var tabs_absent = [
+ { uri: "about:config",
+ profile: "profile1"
+ },
+ { uri: "about:credits",
+ profile: "profile1"
+ },
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Tabs.add, tabs1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Tabs.verifyNot, tabs_absent]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug556509.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2"};
+
+
+// the initial list of bookmarks to add to the browser
+var bookmarks_initial = {
+ "menu": [
+ { folder: "testfolder",
+ description: "it's just me, a test folder"
+ }
+ ],
+ "menu/testfolder": [
+ { uri: "http://www.mozilla.com",
+ title: "Mozilla"
+ }
+ ]
+};
+
+/*
+ * Test phases
+ */
+
+// Add a bookmark folder which has a description, and sync.
+Phase('phase1', [
+ [Bookmarks.add, bookmarks_initial],
+ [Bookmarks.verify, bookmarks_initial],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+// Sync to profile2 and verify that the bookmark folder is created, along
+// with its description.
+Phase('phase2', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug562515.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Bookmark lists
+ */
+
+// the initial list of bookmarks to add to the browser
+var bookmarks_initial = {
+ "menu": [
+ { uri: "http://www.google.com",
+ loadInSidebar: true,
+ tags: [ "google", "computers", "internet", "www"]
+ },
+ { uri: "http://bugzilla.mozilla.org/show_bug.cgi?id=%s",
+ title: "Bugzilla",
+ keyword: "bz"
+ },
+ { folder: "foldera" },
+ { uri: "http://www.mozilla.com" },
+ { separator: true },
+ { folder: "folderb" }
+ ],
+ "menu/foldera": [
+ { uri: "http://www.yahoo.com",
+ title: "testing Yahoo"
+ },
+ { uri: "http://www.cnn.com",
+ description: "This is a description of the site a at www.cnn.com"
+ },
+ { livemark: "Livemark1",
+ feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
+ siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html"
+ }
+ ],
+ "menu/folderb": [
+ { uri: "http://www.apple.com",
+ tags: [ "apple", "mac" ]
+ }
+ ],
+ "toolbar": [
+ { uri: "place:queryType=0&sort=8&maxResults=10&beginTimeRef=1&beginTime=0",
+ title: "Visited Today"
+ }
+ ]
+};
+
+// a list of bookmarks to delete during a 'delete' action
+var bookmarks_to_delete = {
+ "menu": [
+ { uri: "http://www.google.com",
+ loadInSidebar: true,
+ tags: [ "google", "computers", "internet", "www"]
+ }
+ ],
+ "menu/foldera": [
+ { uri: "http://www.yahoo.com",
+ title: "testing Yahoo"
+ }
+ ]
+};
+
+/*
+ * Test phases
+ */
+
+// add bookmarks to profile1 and sync
+Phase('phase1', [
+ [Bookmarks.add, bookmarks_initial],
+ [Bookmarks.verify, bookmarks_initial],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+// sync to profile2 and verify that the bookmarks are present
+Phase('phase2', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
+// delete some bookmarks from profile1, then sync with "wipe-client"
+// set; finally, verify that the deleted bookmarks were restored.
+Phase('phase3', [
+ [Bookmarks.delete, bookmarks_to_delete],
+ [Bookmarks.verifyNot, bookmarks_to_delete],
+ [Sync, SYNC_WIPE_CLIENT],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
+// sync profile2 again, verify no bookmarks have been deleted
+Phase('phase4', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug563989.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Bookmark asset lists: these define bookmarks that are used during the test
+ */
+
+// the initial list of bookmarks to add to the browser
+var bookmarks_initial = {
+ "menu": [
+ { uri: "http://www.google.com",
+ loadInSidebar: true,
+ tags: [ "google", "computers", "internet", "www" ]
+ },
+ { uri: "http://bugzilla.mozilla.org/show_bug.cgi?id=%s",
+ title: "Bugzilla",
+ keyword: "bz"
+ },
+ { folder: "foldera" },
+ { uri: "http://www.mozilla.com" },
+ { separator: true },
+ { folder: "folderb" }
+ ],
+ "menu/foldera": [
+ { uri: "http://www.yahoo.com",
+ title: "testing Yahoo"
+ },
+ { uri: "http://www.cnn.com",
+ description: "This is a description of the site a at www.cnn.com"
+ },
+ { livemark: "Livemark1",
+ feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
+ siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html"
+ }
+ ],
+ "menu/folderb": [
+ { uri: "http://www.apple.com",
+ tags: [ "apple", "mac" ]
+ }
+ ],
+ "toolbar": [
+ { uri: "place:queryType=0&sort=8&maxResults=10&beginTimeRef=1&beginTime=0",
+ title: "Visited Today"
+ }
+ ]
+};
+
+// a list of bookmarks to delete during a 'delete' action
+var bookmarks_to_delete = {
+ "menu/folderb": [
+ { uri: "http://www.apple.com",
+ tags: [ "apple", "mac" ]
+ }
+ ],
+ "toolbar": [
+ { uri: "place:queryType=0&sort=8&maxResults=10&beginTimeRef=1&beginTime=0",
+ title: "Visited Today"
+ }
+ ]
+};
+
+/*
+ * Test phases
+ */
+
+// Add bookmarks to profile1 and sync.
+Phase('phase1', [
+ [Bookmarks.add, bookmarks_initial],
+ [Bookmarks.verify, bookmarks_initial],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+// Sync to profile2 and verify that the bookmarks are present. Delete
+// some bookmarks, and verify that they're not present, but don't sync again.
+Phase('phase2', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial],
+ [Bookmarks.delete, bookmarks_to_delete],
+ [Bookmarks.verifyNot, bookmarks_to_delete]
+]);
+
+// Using profile1, sync again with wipe-server set to true. Verify our
+// initial bookmarks are still all present.
+Phase('phase3', [
+ [Sync, SYNC_WIPE_SERVER],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
+// Back in profile2, do a sync and verify that the bookmarks we had
+// deleted earlier are now restored.
+Phase('phase4', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_bug575423.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2"};
+
+/*
+ * History data
+ */
+
+// the history data to add to the browser
+var history1 = [
+ { uri: "http://www.google.com/",
+ title: "Google",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -1
+ }
+ ]
+ },
+ { uri: "http://www.cnn.com/",
+ title: "CNN",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ }
+];
+
+// Another history data to add to the browser
+var history2 = [
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ },
+ { uri: "http://www.google.com/language_tools?hl=en",
+ title: "Language Tools",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -40
+ }
+ ]
+ }
+];
+
+/*
+ * Test phases
+ */
+Phase('phase1', [
+ [History.add, history1],
+ [Sync],
+ [History.add, history2],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [Sync],
+ [History.verify, history2]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_client_wipe.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1"};
+
+/*
+ * Bookmark lists
+ */
+
+// the initial list of bookmarks to add to the browser
+var bookmarks_initial = {
+ toolbar: [
+ { uri: "http://www.google.com",
+ title: "Google"
+ },
+ { uri: "http://www.cnn.com",
+ title: "CNN",
+ changes: {
+ position: "Google"
+ }
+ },
+ { uri: "http://www.mozilla.com",
+ title: "Mozilla"
+ },
+ { uri: "http://www.firefox.com",
+ title: "Firefox",
+ changes: {
+ position: "Mozilla"
+ }
+ }
+ ]
+};
+
+var bookmarks_after_move = {
+ toolbar: [
+ { uri: "http://www.cnn.com",
+ title: "CNN"
+ },
+ { uri: "http://www.google.com",
+ title: "Google"
+ },
+ { uri: "http://www.firefox.com",
+ title: "Firefox"
+ },
+ { uri: "http://www.mozilla.com",
+ title: "Mozilla"
+ }
+ ]
+};
+
+/*
+ * Password data
+ */
+
+// Initial password data
+var passwords_initial = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "secret",
+ usernameField: "uname",
+ passwordField: "pword",
+ changes: {
+ password: "SeCrEt$$$"
+ }
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "jack",
+ password: "secretlogin"
+ }
+];
+
+// Password after first modify action has been performed
+var passwords_after_change = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "SeCrEt$$$",
+ usernameField: "uname",
+ passwordField: "pword",
+ changes: {
+ username: "james"
+ }
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "jack",
+ password: "secretlogin"
+ }
+];
+
+/*
+ * Prefs to use in the test
+ */
+var prefs1 = [
+ { name: "browser.startup.homepage",
+ value: "http://www.getfirefox.com"
+ },
+ { name: "browser.urlbar.maxRichResults",
+ value: 20
+ },
+ { name: "browser.tabs.autoHide",
+ value: true
+ }
+];
+
+var prefs2 = [
+ { name: "browser.startup.homepage",
+ value: "http://www.mozilla.com"
+ },
+ { name: "browser.urlbar.maxRichResults",
+ value: 18
+ },
+ { name: "browser.tabs.autoHide",
+ value: false
+ }
+];
+
+/*
+ * Test phases
+ */
+
+// Add prefs,passwords and bookmarks to profile1 and sync.
+Phase('phase1', [
+ [Passwords.add, passwords_initial],
+ [Bookmarks.add, bookmarks_initial],
+ [Prefs.modify, prefs1],
+ [Prefs.verify, prefs1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+// Sync profile2 and verify same prefs,passwords and bookmarks are present.
+Phase('phase2', [
+ [Sync],
+ [Prefs.verify, prefs1],
+ [Passwords.verify, passwords_initial],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
+// Using profile1, change some prefs,bookmarks and pwds, then do another sync with wipe-client.
+// Verify that the cloud's settings are restored, and the recent local changes
+// discarded.
+Phase('phase3', [
+ [Prefs.modify, prefs2],
+ [Passwords.modify, passwords_initial],
+ [Bookmarks.modify, bookmarks_initial],
+ [Prefs.verify, prefs2],
+ [Passwords.verify, passwords_after_change],
+ [Bookmarks.verify, bookmarks_after_move],
+ [Sync, SYNC_WIPE_CLIENT],
+ [Prefs.verify, prefs1],
+ [Passwords.verify, passwords_initial],
+ [Bookmarks.verify, bookmarks_initial]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_formdata.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Form data asset lists: these define form values that are used in the tests.
+ */
+
+var formdata1 = [
+ { fieldname: "testing",
+ value: "success",
+ date: -1
+ },
+ { fieldname: "testing",
+ value: "failure",
+ date: -2
+ },
+ { fieldname: "username",
+ value: "joe"
+ }
+];
+
+var formdata2 = [
+ { fieldname: "testing",
+ value: "success",
+ date: -1
+ },
+ { fieldname: "username",
+ value: "joe"
+ }
+];
+
+var formdata_delete = [
+ { fieldname: "testing",
+ value: "failure"
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Formdata.add, formdata1],
+ [Formdata.verify, formdata1],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Formdata.verify, formdata1],
+]);
+
+/*
+ * Note: Weave does not support syncing deleted form data, so those
+ * tests are disabled below. See bug 568363.
+ */
+
+Phase('phase3', [
+ [Sync],
+ [Formdata.delete, formdata_delete],
+//[Formdata.verifyNot, formdata_delete],
+ [Formdata.verify, formdata2],
+ [Sync],
+]);
+
+Phase('phase4', [
+ [Sync],
+ [Formdata.verify, formdata2],
+//[Formdata.verifyNot, formdata_delete]
+]);
+
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_history.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2" };
+
+/*
+ * History asset lists: these define history entries that are used during
+ * the test
+ */
+
+// the initial list of history items to add to the browser
+var history1 = [
+ { uri: "http://www.google.com/",
+ title: "Google",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -1
+ }
+ ]
+ },
+ { uri: "http://www.cnn.com/",
+ title: "CNN",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ },
+ { uri: "http://www.google.com/language_tools?hl=en",
+ title: "Language Tools",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -40
+ }
+ ]
+ },
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 1,
+ date: -1
+ },
+ { type: 1,
+ date: -20
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ }
+];
+
+// a list of items to delete from the history
+var history_to_delete = [
+ { uri: "http://www.cnn.com/" },
+ { begin: -24,
+ end: -1
+ },
+ { host: "www.google.com" }
+];
+
+// a list which reflects items that should be in the history after
+// the above items are deleted
+var history2 = [
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ }
+];
+
+// a list which includes history entries that should not be present
+// after deletion of the history_to_delete entries
+var history_not = [
+ { uri: "http://www.google.com/",
+ title: "Google",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -1
+ }
+ ]
+ },
+ { uri: "http://www.cnn.com/",
+ title: "CNN",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ },
+ { uri: "http://www.google.com/language_tools?hl=en",
+ title: "Language Tools",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -40
+ }
+ ]
+ },
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 1,
+ date: -20
+ }
+ ]
+ }
+];
+
+/*
+ * Test phases
+ * Note: there is no test phase in which deleted history entries are
+ * synced to other clients. This functionality is not supported by
+ * Sync, see bug 446517.
+ */
+
+Phase('phase1', [
+ [History.add, history1],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Sync],
+ [History.verify, history1],
+ [History.delete, history_to_delete],
+ [History.verify, history2],
+ [History.verifyNot, history_not],
+ [Sync]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_history_collision.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * History lists
+ */
+
+// the initial list of history to add to the browser
+var history1 = [
+ { uri: "http://www.google.com/",
+ title: "Google",
+ visits: [
+ { type: 1,
+ date: 0
+ }
+ ]
+ },
+ { uri: "http://www.cnn.com/",
+ title: "CNN",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ },
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ }
+];
+
+// the history to delete
+var history_to_delete = [
+ { uri: "http://www.cnn.com/",
+ title: "CNN"
+ },
+ { begin: -36,
+ end: -1
+ }
+];
+
+var history_not = [
+ { uri: "http://www.cnn.com/",
+ title: "CNN",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ }
+];
+
+var history_after_delete = [
+ { uri: "http://www.google.com/",
+ title: "Google",
+ visits: [
+ { type: 1,
+ date: 0
+ }
+ ]
+ },
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: 0
+ }
+ ]
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [History.add, history1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [History.add, history1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [History.verify, history1],
+ [History.delete, history_to_delete],
+ [History.verify, history_after_delete],
+ [History.verifyNot, history_not],
+ [Sync]
+]);
+
+Phase('phase4', [
+ [Sync],
+ [History.verify, history_after_delete],
+ [History.verifyNot, history_not]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_passwords.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Password asset lists: these define password entries that are used during
+ * the test
+ */
+
+// initial password list to be loaded into the browser
+var passwords_initial = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "SeCrEt123",
+ usernameField: "uname",
+ passwordField: "pword",
+ changes: {
+ password: "zippity-do-dah"
+ }
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+// expected state of passwords after the changes in the above list are applied
+var passwords_after_first_update = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "zippity-do-dah",
+ usernameField: "uname",
+ passwordField: "pword"
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+var passwords_to_delete = [
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+var passwords_absent = [
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+// expected state of passwords after the delete operation
+var passwords_after_second_update = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "zippity-do-dah",
+ usernameField: "uname",
+ passwordField: "pword"
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Passwords.add, passwords_initial],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Passwords.verify, passwords_initial],
+ [Passwords.modify, passwords_initial],
+ [Passwords.verify, passwords_after_first_update],
+ [Sync]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [Passwords.verify, passwords_after_first_update],
+ [Passwords.delete, passwords_to_delete],
+ [Passwords.verify, passwords_after_second_update],
+ [Passwords.verifyNot, passwords_absent],
+ [Sync]
+]);
+
+Phase('phase4', [
+ [Sync],
+ [Passwords.verify, passwords_after_second_update],
+ [Passwords.verifyNot, passwords_absent]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_prefs.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1"};
+
+var prefs1 = [
+ { name: "browser.startup.homepage",
+ value: "http://www.getfirefox.com"
+ },
+ { name: "browser.urlbar.maxRichResults",
+ value: 20
+ },
+ { name: "browser.tabs.autoHide",
+ value: true
+ }
+];
+
+var prefs2 = [
+ { name: "browser.startup.homepage",
+ value: "http://www.mozilla.com"
+ },
+ { name: "browser.urlbar.maxRichResults",
+ value: 18
+ },
+ { name: "browser.tabs.autoHide",
+ value: false
+ }
+];
+
+Phase('phase1', [
+ [Prefs.modify, prefs1],
+ [Prefs.verify, prefs1],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Prefs.verify, prefs1],
+ [Prefs.modify, prefs2],
+ [Prefs.verify, prefs2],
+ [Sync]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [Prefs.verify, prefs2]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_privbrw_formdata.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Form data
+ */
+
+// the form data to add to the browser
+var formdata1 = [
+ { fieldname: "name",
+ value: "xyz",
+ date: -1
+ },
+ { fieldname: "email",
+ value: "abc@gmail.com",
+ date: -2
+ },
+ { fieldname: "username",
+ value: "joe"
+ }
+];
+
+// the form data to add in private browsing mode
+var formdata2 = [
+ { fieldname: "password",
+ value: "secret",
+ date: -1
+ },
+ { fieldname: "city",
+ value: "mtview"
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Formdata.add, formdata1],
+ [Formdata.verify, formdata1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Formdata.verify, formdata1]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [SetPrivateBrowsing, true],
+ [Formdata.add, formdata2],
+ [Formdata.verify, formdata2],
+ [Sync],
+]);
+
+Phase('phase4', [
+ [Sync],
+ [Formdata.verify, formdata1],
+ [Formdata.verifyNot, formdata2]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_privbrw_passwords.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Password data
+ */
+
+// Initial password data
+var passwords_initial = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "secret",
+ usernameField: "uname",
+ passwordField: "pword",
+ changes: {
+ password: "SeCrEt$$$"
+ }
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "jack",
+ password: "secretlogin"
+ }
+];
+
+// Password after first modify action has been performed
+var passwords_after_first_change = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "SeCrEt$$$",
+ usernameField: "uname",
+ passwordField: "pword",
+ changes: {
+ username: "james"
+ }
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "jack",
+ password: "secretlogin"
+ }
+];
+
+// Password after second modify action has been performed
+var passwords_after_second_change = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "james",
+ password: "SeCrEt$$$",
+ usernameField: "uname",
+ passwordField: "pword"
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "jack",
+ password: "secretlogin"
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Passwords.add, passwords_initial],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Passwords.verify, passwords_initial],
+ [Passwords.modify, passwords_initial],
+ [Passwords.verify, passwords_after_first_change],
+ [Sync]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [SetPrivateBrowsing, true],
+ [Passwords.verify, passwords_after_first_change],
+ [Passwords.modify, passwords_after_first_change],
+ [Passwords.verify, passwords_after_second_change],
+ [Sync]
+]);
+
+Phase('phase4', [
+ [Sync],
+ [Passwords.verify, passwords_after_second_change]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_privbrw_tabs.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Tabs data
+ */
+
+var tabs1 = [
+ { uri: "data:text/html,<html><head><title>Firefox</title></head><body>Firefox</body></html>",
+ title: "Firefox",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Weave</title></head><body>Weave</body></html>",
+ title: "Weave",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Apple</title></head><body>Apple</body></html>",
+ title: "Apple",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>IRC</title></head><body>IRC</body></html>",
+ title: "IRC",
+ profile: "profile1"
+ }
+];
+
+var tabs2 = [
+ { uri: "data:text/html,<html><head><title>Tinderbox</title></head><body>Tinderbox</body></html>",
+ title: "Tinderbox",
+ profile: "profile2"
+ },
+ { uri: "data:text/html,<html><head><title>Fox</title></head><body>Fox</body></html>",
+ title: "Fox",
+ profile: "profile2"
+ }
+];
+
+var tabs3 = [
+ { uri: "data:text/html,<html><head><title>Jetpack</title></head><body>Jetpack</body></html>",
+ title: "Jetpack",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Selenium</title></head><body>Selenium</body></html>",
+ title: "Selenium",
+ profile: "profile1"
+ }
+];
+
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Tabs.add, tabs1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Tabs.verify, tabs1],
+ [Tabs.add, tabs2],
+ [Sync]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [SetPrivateBrowsing, true],
+ [Tabs.add, tabs3],
+ [Sync]
+]);
+
+Phase('phase4', [
+ [Sync],
+ [Tabs.verifyNot, tabs3]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_special_tabs.js
@@ -0,0 +1,77 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Bug 532173 - Dont sync tabs like about:* , weave firstrun etc
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2" };
+
+var tabs1 = [
+ { uri: "data:text/html,<html><head><title>Firefox</title></head><body>Firefox</body></html>",
+ title: "Firefox",
+ profile: "profile1"
+ },
+ { uri: "about:plugins",
+ title: "About",
+ profile: "profile1"
+ },
+ { uri: "about:credits",
+ title: "Credits",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Mozilla</title></head><body>Mozilla</body></html>",
+ title: "Mozilla",
+ profile: "profile1"
+ },
+ { uri: "http://www.mozilla.com/en-US/firefox/sync/firstrun.html",
+ title: "Firstrun",
+ profile: "profile1"
+ }
+];
+
+var tabs2 = [
+ { uri: "data:text/html,<html><head><title>Firefox</title></head><body>Firefox</body></html>",
+ title: "Firefox",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Mozilla</title></head><body>Mozilla</body></html>",
+ title: "Mozilla",
+ profile: "profile1"
+ }
+];
+
+var tabs3 = [
+ { uri: "http://www.mozilla.com/en-US/firefox/sync/firstrun.html",
+ title: "Firstrun",
+ profile: "profile1"
+ },
+ { uri: "about:plugins",
+ title: "About",
+ profile: "profile1"
+ },
+ { uri: "about:credits",
+ title: "Credits",
+ profile: "profile1"
+ }
+];
+
+/*
+ * Test phases
+ */
+Phase('phase1', [
+ [Tabs.add, tabs1],
+ [Sync, SYNC_WIPE_SERVER]
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Tabs.verify, tabs2],
+ [Tabs.verifyNot, tabs3]
+]);
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_sync.js
@@ -0,0 +1,424 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1",
+ "phase4": "profile2" };
+
+/*
+ * Bookmark asset lists: these define bookmarks that are used during the test
+ */
+
+// the initial list of bookmarks to be added to the browser
+var bookmarks_initial = {
+ "menu": [
+ { uri: "http://www.google.com",
+ loadInSidebar: true,
+ tags: ["google", "computers", "internet", "www"],
+ changes: {
+ title: "Google",
+ loadInSidebar: false,
+ tags: ["google", "computers", "misc"],
+ }
+ },
+ { uri: "http://bugzilla.mozilla.org/show_bug.cgi?id=%s",
+ title: "Bugzilla",
+ keyword: "bz",
+ changes: {
+ keyword: "bugzilla"
+ }
+ },
+ { folder: "foldera" },
+ { uri: "http://www.mozilla.com" },
+ { separator: true },
+ { folder: "folderb" },
+ ],
+ "menu/foldera": [
+ { uri: "http://www.yahoo.com",
+ title: "testing Yahoo",
+ changes: {
+ location: "menu/folderb"
+ }
+ },
+ { uri: "http://www.cnn.com",
+ description: "This is a description of the site a at www.cnn.com",
+ changes: {
+ uri: "http://money.cnn.com",
+ description: "new description",
+ }
+ },
+ { livemark: "Livemark1",
+ feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
+ siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html",
+ changes: {
+ livemark: "LivemarkOne"
+ }
+ },
+ ],
+ "menu/folderb": [
+ { uri: "http://www.apple.com",
+ tags: ["apple", "mac"],
+ changes: {
+ uri: "http://www.apple.com/iphone/",
+ title: "iPhone",
+ location: "menu",
+ position: "Google",
+ tags: []
+ }
+ }
+ ],
+ toolbar: [
+ { uri: "place:queryType=0&sort=8&maxResults=10&beginTimeRef=1&beginTime=0",
+ title: "Visited Today"
+ }
+ ]
+};
+
+// the state of bookmarks after the first 'modify' action has been performed
+// on them
+var bookmarks_after_first_modify = {
+ "menu": [
+ { uri: "http://www.apple.com/iphone/",
+ title: "iPhone",
+ before: "Google",
+ tags: []
+ },
+ { uri: "http://www.google.com",
+ title: "Google",
+ loadInSidebar: false,
+ tags: [ "google", "computers", "misc"]
+ },
+ { uri: "http://bugzilla.mozilla.org/show_bug.cgi?id=%s",
+ title: "Bugzilla",
+ keyword: "bugzilla"
+ },
+ { folder: "foldera" },
+ { uri: "http://www.mozilla.com" },
+ { separator: true },
+ { folder: "folderb",
+ changes: {
+ location: "menu/foldera",
+ folder: "Folder B",
+ description: "folder description"
+ }
+ }
+ ],
+ "menu/foldera": [
+ { uri: "http://money.cnn.com",
+ title: "http://www.cnn.com",
+ description: "new description"
+ },
+ { livemark: "LivemarkOne",
+ feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
+ siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html"
+ }
+ ],
+ "menu/folderb": [
+ { uri: "http://www.yahoo.com",
+ title: "testing Yahoo"
+ }
+ ],
+ "toolbar": [
+ { uri: "place:queryType=0&sort=8&maxResults=10&beginTimeRef=1&beginTime=0",
+ title: "Visited Today"
+ }
+ ]
+};
+
+// a list of bookmarks to delete during a 'delete' action
+var bookmarks_to_delete = {
+ "menu": [
+ { uri: "http://www.google.com",
+ title: "Google",
+ loadInSidebar: false,
+ tags: [ "google", "computers", "misc" ]
+ }
+ ]
+};
+
+// the state of bookmarks after the second 'modify' action has been performed
+// on them
+var bookmarks_after_second_modify = {
+ "menu": [
+ { uri: "http://www.apple.com/iphone/",
+ title: "iPhone"
+ },
+ { uri: "http://bugzilla.mozilla.org/show_bug.cgi?id=%s",
+ title: "Bugzilla",
+ keyword: "bugzilla"
+ },
+ { folder: "foldera" },
+ { uri: "http://www.mozilla.com" },
+ { separator: true },
+ ],
+ "menu/foldera": [
+ { uri: "http://money.cnn.com",
+ title: "http://www.cnn.com",
+ description: "new description"
+ },
+ { livemark: "LivemarkOne",
+ feedUri: "http://rss.wunderground.com/blog/JeffMasters/rss.xml",
+ siteUri: "http://www.wunderground.com/blog/JeffMasters/show.html"
+ },
+ { folder: "Folder B",
+ description: "folder description"
+ }
+ ],
+ "menu/foldera/Folder B": [
+ { uri: "http://www.yahoo.com",
+ title: "testing Yahoo"
+ }
+ ]
+};
+
+// a list of bookmarks which should not be present after the last
+// 'delete' and 'modify' actions
+var bookmarks_absent = {
+ "menu": [
+ { uri: "http://www.google.com",
+ title: "Google"
+ },
+ { folder: "folderb" },
+ { folder: "Folder B" }
+ ]
+};
+
+/*
+ * History asset lists: these define history entries that are used during
+ * the test
+ */
+
+// the initial list of history items to add to the browser
+var history_initial = [
+ { uri: "http://www.google.com/",
+ title: "Google",
+ visits: [
+ { type: 1, date: 0 },
+ { type: 2, date: -1 }
+ ]
+ },
+ { uri: "http://www.cnn.com/",
+ title: "CNN",
+ visits: [
+ { type: 1, date: -1 },
+ { type: 2, date: -36 }
+ ]
+ },
+ { uri: "http://www.google.com/language_tools?hl=en",
+ title: "Language Tools",
+ visits: [
+ { type: 1, date: 0 },
+ { type: 2, date: -40 }
+ ]
+ },
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1, date: 0 },
+ { type: 1, date: -1 },
+ { type: 1, date: -20 },
+ { type: 2, date: -36 }
+ ]
+ }
+];
+
+// a list of history entries to delete during a 'delete' action
+var history_to_delete = [
+ { uri: "http://www.cnn.com/" },
+ { begin: -24,
+ end: -1 },
+ { host: "www.google.com" }
+];
+
+// the expected history entries after the first 'delete' action
+var history_after_delete = [
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ }
+];
+
+// history entries expected to not exist after a 'delete' action
+var history_absent = [
+ { uri: "http://www.google.com/",
+ title: "Google",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -1
+ }
+ ]
+ },
+ { uri: "http://www.cnn.com/",
+ title: "CNN",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 2,
+ date: -36
+ }
+ ]
+ },
+ { uri: "http://www.google.com/language_tools?hl=en",
+ title: "Language Tools",
+ visits: [
+ { type: 1,
+ date: 0
+ },
+ { type: 2,
+ date: -40
+ }
+ ]
+ },
+ { uri: "http://www.mozilla.com/",
+ title: "Mozilla",
+ visits: [
+ { type: 1,
+ date: -1
+ },
+ { type: 1,
+ date: -20
+ }
+ ]
+ }
+];
+
+/*
+ * Password asset lists: these define password entries that are used during
+ * the test
+ */
+
+// the initial list of passwords to add to the browser
+var passwords_initial = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "SeCrEt123",
+ usernameField: "uname",
+ passwordField: "pword",
+ changes: {
+ password: "zippity-do-dah"
+ }
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+// the expected state of passwords after the first 'modify' action
+var passwords_after_first_modify = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "zippity-do-dah",
+ usernameField: "uname",
+ passwordField: "pword"
+ },
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+// a list of passwords to delete during a 'delete' action
+var passwords_to_delete = [
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+// a list of passwords expected to be absent after 'delete' and 'modify'
+// actions
+var passwords_absent = [
+ { hostname: "http://www.example.com",
+ realm: "login",
+ username: "joe",
+ password: "secretlogin"
+ }
+];
+
+// the expected state of passwords after the seconds 'modify' action
+var passwords_after_second_modify = [
+ { hostname: "http://www.example.com",
+ submitURL: "http://login.example.com",
+ username: "joe",
+ password: "zippity-do-dah",
+ usernameField: "uname",
+ passwordField: "pword"
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Bookmarks.add, bookmarks_initial],
+ [Passwords.add, passwords_initial],
+ [History.add, history_initial],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_initial],
+ [Passwords.verify, passwords_initial],
+ [History.verify, history_initial],
+ [Bookmarks.modify, bookmarks_initial],
+ [Passwords.modify, passwords_initial],
+ [History.delete, history_to_delete],
+ [Bookmarks.verify, bookmarks_after_first_modify],
+ [Passwords.verify, passwords_after_first_modify],
+ [History.verify, history_after_delete],
+ [History.verifyNot, history_absent],
+ [Sync],
+]);
+
+Phase('phase3', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_after_first_modify],
+ [Passwords.verify, passwords_after_first_modify],
+ [History.verify, history_after_delete],
+ [Bookmarks.modify, bookmarks_after_first_modify],
+ [Passwords.modify, passwords_after_first_modify],
+ [Bookmarks.delete, bookmarks_to_delete],
+ [Passwords.delete, passwords_to_delete],
+ [Bookmarks.verify, bookmarks_after_second_modify],
+ [Passwords.verify, passwords_after_second_modify],
+ [Bookmarks.verifyNot, bookmarks_absent],
+ [Passwords.verifyNot, passwords_absent],
+ [Sync],
+]);
+
+Phase('phase4', [
+ [Sync],
+ [Bookmarks.verify, bookmarks_after_second_modify],
+ [Passwords.verify, passwords_after_second_modify],
+ [Bookmarks.verifyNot, bookmarks_absent],
+ [Passwords.verifyNot, passwords_absent],
+ [History.verifyNot, history_absent],
+]);
+
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/tps/test_tabs.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * The list of phases mapped to their corresponding profiles. The object
+ * here must be in strict JSON format, as it will get parsed by the Python
+ * testrunner (no single quotes, extra comma's, etc).
+ */
+
+var phases = { "phase1": "profile1",
+ "phase2": "profile2",
+ "phase3": "profile1"};
+
+/*
+ * Tab lists.
+ */
+
+var tabs1 = [
+ { uri: "http://hg.mozilla.org/automation/crossweave/raw-file/2d9aca9585b6/pages/page1.html",
+ title: "Crossweave Test Page 1",
+ profile: "profile1"
+ },
+ { uri: "data:text/html,<html><head><title>Hello</title></head><body>Hello</body></html>",
+ title: "Hello",
+ profile: "profile1"
+ }
+];
+
+var tabs2 = [
+ { uri: "http://hg.mozilla.org/automation/crossweave/raw-file/2d9aca9585b6/pages/page3.html",
+ title: "Crossweave Test Page 3",
+ profile: "profile2"
+ },
+ { uri: "data:text/html,<html><head><title>Bye</title></head><body>Bye</body></html>",
+ profile: "profile2"
+ }
+];
+
+/*
+ * Test phases
+ */
+
+Phase('phase1', [
+ [Tabs.add, tabs1],
+ [Sync, SYNC_WIPE_SERVER],
+]);
+
+Phase('phase2', [
+ [Sync],
+ [Tabs.verify, tabs1],
+ [Tabs.add, tabs2],
+ [Sync]
+]);
+
+Phase('phase3', [
+ [Sync],
+ [Tabs.verify, tabs2]
+]);
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/chrome.manifest
@@ -0,0 +1,4 @@
+resource tps modules/
+component {4e5bd3f0-41d3-11df-9879-0800200c9a66} components/tps-cmdline.js
+contract @mozilla.org/commandlinehandler/general-startup;1?type=tps {4e5bd3f0-41d3-11df-9879-0800200c9a66}
+category command-line-handler m-tps @mozilla.org/commandlinehandler/general-startup;1?type=tps
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/components/tps-cmdline.js
@@ -0,0 +1,186 @@
+/* ***** 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 TPS.
+ *
+ * The Initial Developer of the Original Code is
+ * Christopher A. Aillon <christopher@aillon.com>.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Christopher A. Aillon <christopher@aillon.com>
+ * L. David Baron, Mozilla Corporation <dbaron@dbaron.org> (modified for reftest)
+ * Jonathan Griffin <jgriffin@mozilla.com> (modified for TPS)
+ *
+ * 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 ***** */
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+
+const TPS_ID = "tps@mozilla.org";
+const TPS_CMDLINE_CONTRACTID = "@mozilla.org/commandlinehandler/general-startup;1?type=tps";
+const TPS_CMDLINE_CLSID = Components.ID('{4e5bd3f0-41d3-11df-9879-0800200c9a66}');
+const CATMAN_CONTRACTID = "@mozilla.org/categorymanager;1";
+const nsISupports = Components.interfaces.nsISupports;
+
+const nsICategoryManager = Components.interfaces.nsICategoryManager;
+const nsICmdLineHandler = Components.interfaces.nsICmdLineHandler;
+const nsICommandLine = Components.interfaces.nsICommandLine;
+const nsICommandLineHandler = Components.interfaces.nsICommandLineHandler;
+const nsIComponentRegistrar = Components.interfaces.nsIComponentRegistrar;
+const nsISupportsString = Components.interfaces.nsISupportsString;
+const nsIWindowWatcher = Components.interfaces.nsIWindowWatcher;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+function TPSCmdLineHandler() {}
+TPSCmdLineHandler.prototype =
+{
+ classDescription: "TPSCmdLineHandler",
+ classID : TPS_CMDLINE_CLSID,
+ contractID : TPS_CMDLINE_CONTRACTID,
+
+ QueryInterface: XPCOMUtils.generateQI([nsISupports,
+ nsICommandLineHandler,
+ nsICmdLineHandler]), /* nsISupports */
+
+ /* nsICmdLineHandler */
+ commandLineArgument : "-tps",
+ prefNameForStartup : "general.startup.tps",
+ helpText : "Run TPS tests with the given test file.",
+ handlesArgs : true,
+ defaultArgs : "",
+ openWindowWithArgs : true,
+
+ /* nsICommandLineHandler */
+ handle : function handler_handle(cmdLine) {
+ var uristr = cmdLine.handleFlagWithParam("tps", false);
+ if (uristr == null)
+ return;
+ var phase = cmdLine.handleFlagWithParam("tpsphase", false);
+ if (phase == null)
+ throw("must specify --tpsphase with --tps");
+ var logfile = cmdLine.handleFlagWithParam("tpslogfile", false);
+ if (logfile == null)
+ logfile = "";
+
+ let uri = cmdLine.resolveURI(uristr).spec;
+
+ /* Ignore the platform's online/offline status while running tests. */
+ var ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService2);
+ ios.manageOfflineStatus = false;
+ ios.offline = false;
+
+ Components.utils.import("resource://tps/tps.jsm");
+ Components.utils.import("resource://tps/quit.js", TPS);
+ TPS.RunTestPhase(uri, phase, logfile);
+
+ //cmdLine.preventDefault = true;
+ },
+
+ helpInfo : " -tps <file> Run TPS tests with the given test file.\n" +
+ " -tpsphase <phase> Run the specified phase in the TPS test.\n" +
+ " -tpslogfile <file> Logfile for TPS output.\n",
+};
+
+
+var TPSCmdLineFactory =
+{
+ createInstance : function(outer, iid)
+ {
+ if (outer != null) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+
+ return new TPSCmdLineHandler().QueryInterface(iid);
+ }
+};
+
+
+var TPSCmdLineModule =
+{
+ registerSelf : function(compMgr, fileSpec, location, type)
+ {
+ compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
+
+ compMgr.registerFactoryLocation(TPS_CMDLINE_CLSID,
+ "TPS CommandLine Service",
+ TPS_CMDLINE_CONTRACTID,
+ fileSpec,
+ location,
+ type);
+
+ var catman = Components.classes[CATMAN_CONTRACTID].getService(nsICategoryManager);
+ catman.addCategoryEntry("command-line-argument-handlers",
+ "TPS command line handler",
+ TPS_CMDLINE_CONTRACTID, true, true);
+ catman.addCategoryEntry("command-line-handler",
+ "m-tps",
+ TPS_CMDLINE_CONTRACTID, true, true);
+ },
+
+ unregisterSelf : function(compMgr, fileSpec, location)
+ {
+ compMgr = compMgr.QueryInterface(nsIComponentRegistrar);
+
+ compMgr.unregisterFactoryLocation(TPS_CMDLINE_CLSID, fileSpec);
+ catman = Components.classes[CATMAN_CONTRACTID].getService(nsICategoryManager);
+ catman.deleteCategoryEntry("command-line-argument-handlers",
+ "TPS command line handler", true);
+ catman.deleteCategoryEntry("command-line-handler",
+ "m-tps", true);
+ },
+
+ getClassObject : function(compMgr, cid, iid)
+ {
+ if (cid.equals(TPS_CMDLINE_CLSID)) {
+ return TPSCmdLineFactory;
+ }
+
+ if (!iid.equals(Components.interfaces.nsIFactory)) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ canUnload : function(compMgr)
+ {
+ return true;
+ }
+};
+
+/**
+* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4).
+* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6).
+*/
+if (XPCOMUtils.generateNSGetFactory)
+ var NSGetFactory = XPCOMUtils.generateNSGetFactory([TPSCmdLineHandler]);
+
+function NSGetModule(compMgr, fileSpec) {
+ return TPSCmdLineModule;
+}
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>tps@mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <em:targetApplication>
+ <!-- Firefox -->
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>3.5.0</em:minVersion>
+ <em:maxVersion>8.0.*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- front-end metadata -->
+ <em:name>TPS</em:name>
+ <em:description>Sync test extension</em:description>
+ <em:creator>Jonathan Griffin</em:creator>
+ <em:homepageURL>http://www.mozilla.org/</em:homepageURL>
+ </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/bookmarks.jsm
@@ -0,0 +1,1017 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Crossweave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ * Philipp von Weitershausen <philipp@weitershausen.de>
+ *
+ * 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 ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ * Components.utils.import() and acts as a singleton. Only the following
+ * listed symbols will exposed on import, and only when and where imported.
+ */
+
+var EXPORTED_SYMBOLS = ["PlacesItem", "Bookmark", "Separator", "Livemark",
+ "BookmarkFolder"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+CU.import("resource://tps/logger.jsm");
+CU.import("resource://gre/modules/Services.jsm");
+CU.import("resource://gre/modules/PlacesUtils.jsm");
+
+/**
+ * extend, causes a child object to inherit from a parent
+ */
+function extend(child, supertype)
+{
+ child.prototype.__proto__ = supertype.prototype;
+}
+
+/**
+ * PlacesItemProps object, holds properties for places items
+ */
+function PlacesItemProps(props) {
+ this.location = null;
+ this.uri = null;
+ this.loadInSidebar = null;
+ this.keyword = null;
+ this.title = null;
+ this.description = null;
+ this.after = null;
+ this.before = null;
+ this.folder = null;
+ this.position = null;
+ this.delete = false;
+ this.siteUri = null;
+ this.feedUri = null;
+ this.livemark = null;
+ this.tags = null;
+ this.last_item_pos = null;
+ this.type = null;
+
+ for (var prop in props) {
+ if (prop in this)
+ this[prop] = props[prop];
+ }
+}
+
+/**
+ * PlacesItem object. Base class for places items.
+ */
+function PlacesItem(props) {
+ this.props = new PlacesItemProps(props);
+ if (this.props.location == null)
+ this.props.location = "menu";
+ if ("changes" in props)
+ this.updateProps = new PlacesItemProps(props.changes);
+ else
+ this.updateProps = null;
+}
+
+/**
+ * Instance methods for generic places items.
+ */
+PlacesItem.prototype = {
+ // an array of possible root folders for places items
+ _bookmarkFolders: {
+ "places": "placesRoot",
+ "menu": "bookmarksMenuFolder",
+ "tags": "tagFolder",
+ "unfiled": "unfiledBookmarksFolder",
+ "toolbar": "toolbarFolder",
+ },
+
+ toString: function() {
+ var that = this;
+ var props = ['uri', 'title', 'location', 'folder', 'feedUri', 'siteUri', 'livemark'];
+ var string = (this.props.type ? this.props.type + " " : "") +
+ "(" +
+ (function() {
+ var ret = [];
+ for (var i in props) {
+ if (that.props[props[i]]) {
+ ret.push(props[i] + ": " + that.props[props[i]])
+ }
+ }
+ return ret;
+ })().join(", ") + ")";
+ return string;
+ },
+
+ /**
+ * GetPlacesNodeId
+ *
+ * Finds the id of the an item with the specified properties in the places
+ * database.
+ *
+ * @param folder The id of the folder to search
+ * @param type The type of the item to find, or null to match any item;
+ * this is one of the values listed at
+ * https://developer.mozilla.org/en/nsINavHistoryResultNode#Constants
+ * @param title The title of the item to find, or null to match any title
+ * @param uri The uri of the item to find, or null to match any uri
+ *
+ * @return the node id if the item was found, otherwise -1
+ */
+ GetPlacesNodeId: function (folder, type, title, uri) {
+ let node_id = -1;
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([folder], 1);
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+
+ for (let j = 0; j < rootNode.childCount; j ++) {
+ let node = rootNode.getChild(j);
+ if (node.title == title) {
+ if (type == null || type == undefined || node.type == type)
+ if (uri == undefined || uri == null || node.uri.spec == uri.spec)
+ node_id = node.itemId;
+ }
+ }
+ rootNode.containerOpen = false;
+
+ return node_id;
+ },
+
+ /**
+ * IsAdjacentTo
+ *
+ * Determines if this object is immediately adjacent to another.
+ *
+ * @param itemName The name of the other object; this may be any kind of
+ * places item
+ * @param relativePos The relative position of the other object. If -1,
+ * it means the other object should precede this one, if +1,
+ * the other object should come after this one
+ * @return true if this object is immediately adjacent to the other object,
+ * otherwise false
+ */
+ IsAdjacentTo: function(itemName, relativePos) {
+ Logger.AssertTrue(this.props.folder_id != -1 && this.props.item_id != -1,
+ "Either folder_id or item_id was invalid");
+ let other_id = this.GetPlacesNodeId(this.props.folder_id, null, itemName);
+ Logger.AssertTrue(other_id != -1, "item " + itemName + " not found");
+ let other_pos = PlacesUtils.bookmarks.getItemIndex(other_id);
+ let this_pos = PlacesUtils.bookmarks.getItemIndex(this.props.item_id);
+ if (other_pos + relativePos != this_pos) {
+ Logger.logPotentialError("Invalid position - " +
+ (this.props.title ? this.props.title : this.props.folder) +
+ " not " + (relativePos == 1 ? "after " : "before ") + itemName +
+ " for " + this.toString());
+ return false;
+ }
+ return true;
+ },
+
+ /**
+ * GetItemIndex
+ *
+ * Gets the item index for this places item.
+ *
+ * @return the item index, or -1 if there's an error
+ */
+ GetItemIndex: function() {
+ if (this.props.item_id == -1)
+ return -1;
+ return PlacesUtils.bookmarks.getItemIndex(this.props.item_id);
+ },
+
+ /**
+ * GetFolder
+ *
+ * Gets the folder id for the specified bookmark folder
+ *
+ * @param location The full path of the folder, which must begin
+ * with one of the bookmark root folders
+ * @return the folder id if the folder is found, otherwise -1
+ */
+ GetFolder: function(location) {
+ let folder_parts = location.split("/");
+ if (!(folder_parts[0] in this._bookmarkFolders)) {
+ return -1;
+ }
+ let folder_id = PlacesUtils.bookmarks[this._bookmarkFolders[folder_parts[0]]];
+ for (let i = 1; i < folder_parts.length; i++) {
+ let subfolder_id = this.GetPlacesNodeId(
+ folder_id,
+ CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ folder_parts[i]);
+ if (subfolder_id == -1) {
+ return -1;
+ }
+ else {
+ folder_id = subfolder_id;
+ }
+ }
+ return folder_id;
+ },
+
+ /**
+ * CreateFolder
+ *
+ * Creates a bookmark folder.
+ *
+ * @param location The full path of the folder, which must begin
+ * with one of the bookmark root folders
+ * @return the folder id if the folder was created, otherwise -1
+ */
+ CreateFolder: function(location) {
+ let folder_parts = location.split("/");
+ if (!(folder_parts[0] in this._bookmarkFolders)) {
+ return -1;
+ }
+ let folder_id = PlacesUtils.bookmarks[this._bookmarkFolders[folder_parts[0]]];
+ for (let i = 1; i < folder_parts.length; i++) {
+ let subfolder_id = this.GetPlacesNodeId(
+ folder_id,
+ CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ folder_parts[i]);
+ if (subfolder_id == -1) {
+ folder_id = PlacesUtils.bookmarks.createFolder(folder_id,
+ folder_parts[i], -1);
+ }
+ else {
+ folder_id = subfolder_id;
+ }
+ }
+ return folder_id;
+ },
+
+ /**
+ * GetOrCreateFolder
+ *
+ * Locates the specified folder; if not found it is created.
+ *
+ * @param location The full path of the folder, which must begin
+ * with one of the bookmark root folders
+ * @return the folder id if the folder was found or created, otherwise -1
+ */
+ GetOrCreateFolder: function(location) {
+ folder_id = this.GetFolder(location);
+ if (folder_id == -1)
+ folder_id = this.CreateFolder(location);
+ return folder_id;
+ },
+
+ /**
+ * CheckDescription
+ *
+ * Compares the description of this places item with an expected
+ * description.
+ *
+ * @param expectedDescription The description this places item is
+ * expected to have
+ * @return true if the actual and expected descriptions match, or if
+ * there is no expected description; otherwise false
+ */
+ CheckDescription: function(expectedDescription) {
+ if (expectedDescription != null) {
+ let description = "";
+ if (PlacesUtils.annotations.itemHasAnnotation(this.props.item_id,
+ "bookmarkProperties/description")) {
+ description = PlacesUtils.annotations.getItemAnnotation(
+ this.props.item_id, "bookmarkProperties/description");
+ }
+ if (description != expectedDescription) {
+ Logger.logPotentialError("Invalid description, expected: " +
+ expectedDescription + ", actual: " + description + " for " +
+ this.toString());
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * CheckPosition
+ *
+ * Verifies the position of this places item.
+ *
+ * @param before The name of the places item that this item should be
+ before, or null if this check should be skipped
+ * @param after The name of the places item that this item should be
+ after, or null if this check should be skipped
+ * @param last_item_pos The index of the places item above this one,
+ * or null if this check should be skipped
+ * @return true if this item is in the correct position, otherwise false
+ */
+ CheckPosition: function(before, after, last_item_pos) {
+ if (after)
+ if (!this.IsAdjacentTo(after, 1)) return false;
+ if (before)
+ if (!this.IsAdjacentTo(before, -1)) return false;
+ if (last_item_pos != null && last_item_pos > -1) {
+ if (this.GetItemIndex() != last_item_pos + 1) {
+ Logger.logPotentialError("Item not found at the expected index, got " +
+ this.GetItemIndex() + ", expected " + (last_item_pos + 1) + " for " +
+ this.toString());
+ return false;
+ }
+ }
+ return true;
+ },
+
+ /**
+ * SetLocation
+ *
+ * Moves this places item to a different folder.
+ *
+ * @param location The full path of the folder to which to move this
+ * places item, which must begin with one of the bookmark root
+ * folders; if null, no changes are made
+ * @return nothing if successful, otherwise an exception is thrown
+ */
+ SetLocation: function(location) {
+ if (location != null) {
+ let newfolder_id = this.GetOrCreateFolder(location);
+ Logger.AssertTrue(newfolder_id != -1, "Location " + location +
+ " doesn't exist; can't change item's location");
+ PlacesUtils.bookmarks.moveItem(this.props.item_id, newfolder_id, -1);
+ this.props.folder_id = newfolder_id;
+ }
+ },
+
+ /**
+ * SetDescription
+ *
+ * Updates the description for this places item.
+ *
+ * @param description The new description to set; if null, no changes are
+ * made
+ * @return nothing
+ */
+ SetDescription: function(description) {
+ if (description != null) {
+ if (description != "")
+ PlacesUtils.annotations.setItemAnnotation(this.props.item_id,
+ "bookmarkProperties/description",
+ description,
+ 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ else
+ PlacesUtils.annotations.removeItemAnnotation(this.props.item_id,
+ "bookmarkProperties/description");
+ }
+ },
+
+ /**
+ * SetPosition
+ *
+ * Updates the position of this places item within this item's current
+ * folder. Use SetLocation to change folders.
+ *
+ * @param position The new index this item should be moved to; if null,
+ * no changes are made; if -1, this item is moved to the bottom of
+ * the current folder
+ * @return nothing if successful, otherwise an exception is thrown
+ */
+ SetPosition: function(position) {
+ if (position != null) {
+ let newposition = -1;
+ if (position != -1) {
+ newposition = this.GetPlacesNodeId(this.props.folder_id,
+ null, position);
+ Logger.AssertTrue(newposition != -1, "position " + position +
+ " is invalid; unable to change position");
+ newposition = PlacesUtils.bookmarks.getItemIndex(newposition);
+ }
+ PlacesUtils.bookmarks.moveItem(this.props.item_id,
+ this.props.folder_id, newposition);
+ }
+ },
+
+ /**
+ * Update the title of this places item
+ *
+ * @param title The new title to set for this item; if null, no changes
+ * are made
+ * @return nothing
+ */
+ SetTitle: function(title) {
+ if (title != null) {
+ PlacesUtils.bookmarks.setItemTitle(this.props.item_id, title);
+ }
+ },
+};
+
+/**
+ * Bookmark class constructor. Initializes instance properties.
+ */
+function Bookmark(props) {
+ PlacesItem.call(this, props);
+ if (this.props.title == null)
+ this.props.title = this.props.uri;
+ this.props.type = "bookmark";
+}
+
+/**
+ * Bookmark instance methods.
+ */
+Bookmark.prototype = {
+ /**
+ * SetKeyword
+ *
+ * Update this bookmark's keyword.
+ *
+ * @param keyword The keyword to set for this bookmark; if null, no
+ * changes are made
+ * @return nothing
+ */
+ SetKeyword: function(keyword) {
+ if (keyword != null)
+ PlacesUtils.bookmarks.setKeywordForBookmark(this.props.item_id, keyword);
+ },
+
+ /**
+ * SetLoadInSidebar
+ *
+ * Updates this bookmark's loadInSidebar property.
+ *
+ * @param loadInSidebar if true, the loadInSidebar property will be set,
+ * if false, it will be cleared, and any other value will result
+ * in no change
+ * @return nothing
+ */
+ SetLoadInSidebar: function(loadInSidebar) {
+ if (loadInSidebar == true)
+ PlacesUtils.annotations.setItemAnnotation(this.props.item_id,
+ "bookmarkProperties/loadInSidebar",
+ true,
+ 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ else if (loadInSidebar == false)
+ PlacesUtils.annotations.removeItemAnnotation(this.props.item_id,
+ "bookmarkProperties/loadInSidebar");
+ },
+
+ /**
+ * SetTitle
+ *
+ * Updates this bookmark's title.
+ *
+ * @param title The new title to set for this boomark; if null, no changes
+ * are made
+ * @return nothing
+ */
+ SetTitle: function(title) {
+ if (title)
+ PlacesUtils.bookmarks.setItemTitle(this.props.item_id, title);
+ },
+
+ /**
+ * SetUri
+ *
+ * Updates this bookmark's URI.
+ *
+ * @param uri The new URI to set for this boomark; if null, no changes
+ * are made
+ * @return nothing
+ */
+ SetUri: function(uri) {
+ if (uri) {
+ let newURI = Services.io.newURI(uri, null, null);
+ PlacesUtils.bookmarks.changeBookmarkURI(this.props.item_id, newURI);
+ }
+ },
+
+ /**
+ * SetTags
+ *
+ * Updates this bookmark's tags.
+ *
+ * @param tags An array of tags which should be associated with this
+ * bookmark; any previous tags are removed; if this param is null,
+ * no changes are made. If this param is an empty array, all
+ * tags are removed from this bookmark.
+ * @return nothing
+ */
+ SetTags: function(tags) {
+ if (tags != null) {
+ let URI = Services.io.newURI(this.props.uri, null, null);
+ PlacesUtils.tagging.untagURI(URI, null);
+ if (tags.length > 0)
+ PlacesUtils.tagging.tagURI(URI, tags);
+ }
+ },
+
+ /**
+ * Create
+ *
+ * Creates the bookmark described by this object's properties.
+ *
+ * @return the id of the created bookmark
+ */
+ Create: function() {
+ this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+ Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+ "bookmark, error creating folder " + this.props.location);
+ let bookmarkURI = Services.io.newURI(this.props.uri, null, null);
+ this.props.item_id = PlacesUtils.bookmarks.insertBookmark(this.props.folder_id,
+ bookmarkURI,
+ -1,
+ this.props.title);
+ this.SetKeyword(this.props.keyword);
+ this.SetDescription(this.props.description);
+ this.SetLoadInSidebar(this.props.loadInSidebar);
+ this.SetTags(this.props.tags);
+ return this.props.item_id;
+ },
+
+ /**
+ * Update
+ *
+ * Updates this bookmark's properties according the properties on this
+ * object's 'updateProps' property.
+ *
+ * @return nothing
+ */
+ Update: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Remove");
+ this.SetKeyword(this.updateProps.keyword);
+ this.SetDescription(this.updateProps.description);
+ this.SetLoadInSidebar(this.updateProps.loadInSidebar);
+ this.SetTitle(this.updateProps.title);
+ this.SetUri(this.updateProps.uri);
+ this.SetTags(this.updateProps.tags);
+ this.SetLocation(this.updateProps.location);
+ this.SetPosition(this.updateProps.position);
+ },
+
+ /**
+ * Find
+ *
+ * Locates the bookmark which corresponds to this object's properties.
+ *
+ * @return the bookmark id if the bookmark was found, otherwise -1
+ */
+ Find: function() {
+ this.props.folder_id = this.GetFolder(this.props.location);
+ if (this.props.folder_id == -1) {
+ Logger.logError("Unable to find folder " + this.props.location);
+ return -1;
+ }
+ let bookmarkTitle = this.props.title;
+ this.props.item_id = this.GetPlacesNodeId(this.props.folder_id,
+ null,
+ bookmarkTitle,
+ this.props.uri);
+
+ if (this.props.item_id == -1) {
+ Logger.logPotentialError(this.toString() + " not found");
+ return -1;
+ }
+ if (!this.CheckDescription(this.props.description))
+ return -1;
+ if (this.props.keyword != null) {
+ let keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.props.item_id);
+ if (keyword != this.props.keyword) {
+ Logger.logPotentialError("Incorrect keyword - expected: " +
+ this.props.keyword + ", actual: " + keyword +
+ " for " + this.toString());
+ return -1;
+ }
+ }
+ let loadInSidebar = PlacesUtils.annotations.itemHasAnnotation(
+ this.props.item_id,
+ "bookmarkProperties/loadInSidebar");
+ if (loadInSidebar)
+ loadInSidebar = PlacesUtils.annotations.getItemAnnotation(
+ this.props.item_id,
+ "bookmarkProperties/loadInSidebar");
+ if (this.props.loadInSidebar != null &&
+ loadInSidebar != this.props.loadInSidebar) {
+ Logger.logPotentialError("Incorrect loadInSidebar setting - expected: " +
+ this.props.loadInSidebar + ", actual: " + loadInSidebar +
+ " for " + this.toString());
+ return -1;
+ }
+ if (this.props.tags != null) {
+ try {
+ let URI = Services.io.newURI(this.props.uri, null, null);
+ let tags = PlacesUtils.tagging.getTagsForURI(URI, {});
+ tags.sort();
+ this.props.tags.sort();
+ if (JSON.stringify(tags) != JSON.stringify(this.props.tags)) {
+ Logger.logPotentialError("Wrong tags - expected: " +
+ JSON.stringify(this.props.tags) + ", actual: " +
+ JSON.stringify(tags) + " for " + this.toString());
+ return -1;
+ }
+ }
+ catch (e) {
+ Logger.logPotentialError("error processing tags " + e);
+ return -1;
+ }
+ }
+ if (!this.CheckPosition(this.props.before,
+ this.props.after,
+ this.props.last_item_pos))
+ return -1;
+ return this.props.item_id;
+ },
+
+ /**
+ * Remove
+ *
+ * Removes this bookmark. The bookmark should have been located previously
+ * by a call to Find.
+ *
+ * @return nothing
+ */
+ Remove: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Remove");
+ PlacesUtils.bookmarks.removeItem(this.props.item_id);
+ },
+};
+
+extend(Bookmark, PlacesItem);
+
+/**
+ * BookmarkFolder class constructor. Initializes instance properties.
+ */
+function BookmarkFolder(props) {
+ PlacesItem.call(this, props);
+ this.props.type = "folder";
+}
+
+/**
+ * BookmarkFolder instance methods
+ */
+BookmarkFolder.prototype = {
+ /**
+ * Create
+ *
+ * Creates the bookmark folder described by this object's properties.
+ *
+ * @return the id of the created bookmark folder
+ */
+ Create: function() {
+ this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+ Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+ "folder, error creating parent folder " + this.props.location);
+ this.props.item_id = PlacesUtils.bookmarks.createFolder(this.props.folder_id,
+ this.props.folder,
+ -1);
+ this.SetDescription(this.props.description);
+ return this.props.folder_id;
+ },
+
+ /**
+ * Find
+ *
+ * Locates the bookmark folder which corresponds to this object's
+ * properties.
+ *
+ * @return the folder id if the folder was found, otherwise -1
+ */
+ Find: function() {
+ this.props.folder_id = this.GetFolder(this.props.location);
+ if (this.props.folder_id == -1) {
+ Logger.logError("Unable to find folder " + this.props.location);
+ return -1;
+ }
+ this.props.item_id = this.GetPlacesNodeId(
+ this.props.folder_id,
+ CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ this.props.folder);
+ if (!this.CheckDescription(this.props.description))
+ return -1;
+ if (!this.CheckPosition(this.props.before,
+ this.props.after,
+ this.props.last_item_pos))
+ return -1;
+ return this.props.item_id;
+ },
+
+ /**
+ * Remove
+ *
+ * Removes this folder. The folder should have been located previously
+ * by a call to Find.
+ *
+ * @return nothing
+ */
+ Remove: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Remove");
+ PlacesUtils.bookmarks.removeFolderChildren(this.props.item_id);
+ PlacesUtils.bookmarks.removeItem(this.props.item_id);
+ },
+
+ /**
+ * Update
+ *
+ * Updates this bookmark's properties according the properties on this
+ * object's 'updateProps' property.
+ *
+ * @return nothing
+ */
+ Update: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Update");
+ this.SetLocation(this.updateProps.location);
+ this.SetPosition(this.updateProps.position);
+ this.SetTitle(this.updateProps.folder);
+ this.SetDescription(this.updateProps.description);
+ },
+};
+
+extend(BookmarkFolder, PlacesItem);
+
+/**
+ * Livemark class constructor. Initialzes instance properties.
+ */
+function Livemark(props) {
+ PlacesItem.call(this, props);
+ this.props.type = "livemark";
+}
+
+/**
+ * Livemark instance methods
+ */
+Livemark.prototype = {
+ /**
+ * Create
+ *
+ * Creates the livemark described by this object's properties.
+ *
+ * @return the id of the created livemark
+ */
+ Create: function() {
+ this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+ Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+ "folder, error creating parent folder " + this.props.location);
+ let siteURI = null;
+ if (this.props.siteUri != null)
+ siteURI = Services.io.newURI(this.props.siteUri, null, null);
+ this.props.item_id = PlacesUtils.livemarks.createLivemark(
+ this.props.folder_id,
+ this.props.livemark,
+ siteURI,
+ Services.io.newURI(this.props.feedUri, null, null),
+ -1);
+ return this.props.item_id;
+ },
+
+ /**
+ * Find
+ *
+ * Locates the livemark which corresponds to this object's
+ * properties.
+ *
+ * @return the item id if the livemark was found, otherwise -1
+ */
+ Find: function() {
+ this.props.folder_id = this.GetFolder(this.props.location);
+ if (this.props.folder_id == -1) {
+ Logger.logError("Unable to find folder " + this.props.location);
+ return -1;
+ }
+ this.props.item_id = this.GetPlacesNodeId(
+ this.props.folder_id,
+ CI.nsINavHistoryResultNode.RESULT_TYPE_FOLDER,
+ this.props.livemark);
+ if (!PlacesUtils.livemarks.isLivemark(this.props.item_id)) {
+ Logger.logPotentialError("livemark folder found, but it's just a regular folder, for " +
+ this.toString());
+ this.props.item_id = -1;
+ return -1;
+ }
+ let feedURI = Services.io.newURI(this.props.feedUri, null, null);
+ let lmFeedURI = PlacesUtils.livemarks.getFeedURI(this.props.item_id);
+ if (feedURI.spec != lmFeedURI.spec) {
+ Logger.logPotentialError("livemark feed uri not correct, expected: " +
+ this.props.feedUri + ", actual: " + lmFeedURI.spec +
+ " for " + this.toString());
+ return -1;
+ }
+ if (this.props.siteUri != null) {
+ let siteURI = Services.io.newURI(this.props.siteUri, null, null);
+ let lmSiteURI = PlacesUtils.livemarks.getSiteURI(this.props.item_id);
+ if (siteURI.spec != lmSiteURI.spec) {
+ Logger.logPotentialError("livemark site uri not correct, expected: " +
+ this.props.siteUri + ", actual: " + lmSiteURI.spec + " for " +
+ this.toString());
+ return -1;
+ }
+ }
+ if (!this.CheckPosition(this.props.before,
+ this.props.after,
+ this.props.last_item_pos))
+ return -1;
+ return this.props.item_id;
+ },
+
+ /**
+ * SetSiteUri
+ *
+ * Sets the siteURI property for this livemark.
+ *
+ * @param siteUri the URI to set; if null, no changes are made
+ * @return nothing
+ */
+ SetSiteUri: function(siteUri) {
+ if (siteUri) {
+ let siteURI = Services.io.newURI(siteUri, null, null);
+ PlacesUtils.livemarks.setSiteURI(this.props.item_id, siteURI);
+ }
+ },
+
+ /**
+ * SetFeedUri
+ *
+ * Sets the feedURI property for this livemark.
+ *
+ * @param feedUri the URI to set; if null, no changes are made
+ * @return nothing
+ */
+ SetFeedUri: function(feedUri) {
+ if (feedUri) {
+ let feedURI = Services.io.newURI(feedUri, null, null);
+ PlacesUtils.livemarks.setFeedURI(this.props.item_id, feedURI);
+ }
+ },
+
+ /**
+ * Update
+ *
+ * Updates this livemark's properties according the properties on this
+ * object's 'updateProps' property.
+ *
+ * @return nothing
+ */
+ Update: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Update");
+ this.SetLocation(this.updateProps.location);
+ this.SetPosition(this.updateProps.position);
+ this.SetSiteUri(this.updateProps.siteUri);
+ this.SetFeedUri(this.updateProps.feedUri);
+ this.SetTitle(this.updateProps.livemark);
+ return true;
+ },
+
+ /**
+ * Remove
+ *
+ * Removes this livemark. The livemark should have been located previously
+ * by a call to Find.
+ *
+ * @return nothing
+ */
+ Remove: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Remove");
+ PlacesUtils.bookmarks.removeItem(this.props.item_id);
+ },
+};
+
+extend(Livemark, PlacesItem);
+
+/**
+ * Separator class constructor. Initializes instance properties.
+ */
+function Separator(props) {
+ PlacesItem.call(this, props);
+ this.props.type = "separator";
+}
+
+/**
+ * Separator instance methods.
+ */
+Separator.prototype = {
+ /**
+ * Create
+ *
+ * Creates the bookmark separator described by this object's properties.
+ *
+ * @return the id of the created separator
+ */
+ Create: function () {
+ this.props.folder_id = this.GetOrCreateFolder(this.props.location);
+ Logger.AssertTrue(this.props.folder_id != -1, "Unable to create " +
+ "folder, error creating parent folder " + this.props.location);
+ this.props.item_id = PlacesUtils.bookmarks.insertSeparator(this.props.folder_id,
+ -1);
+ return this.props.item_id;
+ },
+
+ /**
+ * Find
+ *
+ * Locates the bookmark separator which corresponds to this object's
+ * properties.
+ *
+ * @return the item id if the separator was found, otherwise -1
+ */
+ Find: function () {
+ this.props.folder_id = this.GetFolder(this.props.location);
+ if (this.props.folder_id == -1) {
+ Logger.logError("Unable to find folder " + this.props.location);
+ return -1;
+ }
+ if (this.props.before == null && this.props.last_item_pos == null) {
+ Logger.logPotentialError("Separator requires 'before' attribute if it's the" +
+ "first item in the list");
+ return -1;
+ }
+ let expected_pos = -1;
+ if (this.props.before) {
+ other_id = this.GetPlacesNodeId(this.props.folder_id,
+ null,
+ this.props.before);
+ if (other_id == -1) {
+ Logger.logPotentialError("Can't find places item " + this.props.before +
+ " for locating separator");
+ return -1;
+ }
+ expected_pos = PlacesUtils.bookmarks.getItemIndex(other_id) - 1;
+ }
+ else {
+ expected_pos = this.props.last_item_pos + 1;
+ }
+ this.props.item_id = PlacesUtils.bookmarks.getIdForItemAt(this.props.folder_id,
+ expected_pos);
+ if (this.props.item_id == -1) {
+ Logger.logPotentialError("No separator found at position " + expected_pos);
+ }
+ else {
+ if (PlacesUtils.bookmarks.getItemType(this.props.item_id) !=
+ PlacesUtils.bookmarks.TYPE_SEPARATOR) {
+ Logger.logPotentialError("Places item at position " + expected_pos +
+ " is not a separator");
+ return -1;
+ }
+ }
+ return this.props.item_id;
+ },
+
+ /**
+ * Update
+ *
+ * Updates this separator's properties according the properties on this
+ * object's 'updateProps' property.
+ *
+ * @return nothing
+ */
+ Update: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Update");
+ this.SetLocation(this.updateProps.location);
+ this.SetPosition(this.updateProps.position);
+ return true;
+ },
+
+ /**
+ * Remove
+ *
+ * Removes this separator. The separator should have been located
+ * previously by a call to Find.
+ *
+ * @return nothing
+ */
+ Remove: function() {
+ Logger.AssertTrue(this.props.item_id != -1 && this.props.item_id != null,
+ "Invalid item_id during Update");
+ PlacesUtils.bookmarks.removeItem(this.props.item_id);
+ },
+};
+
+extend(Separator, PlacesItem);
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/forms.jsm
@@ -0,0 +1,295 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Crossweave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ Components.utils.import() and acts as a singleton. Only the following
+ listed symbols will exposed on import, and only when and where imported.
+ */
+
+var EXPORTED_SYMBOLS = ["FormData"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+CU.import("resource://tps/logger.jsm");
+
+let formService = CC["@mozilla.org/satchel/form-history;1"]
+ .getService(CI.nsIFormHistory2);
+
+/**
+ * FormDB
+ *
+ * Helper object containing methods to interact with the moz_formhistory
+ * SQLite table.
+ */
+let FormDB = {
+ /**
+ * makeGUID
+ *
+ * Generates a brand-new globally unique identifier (GUID). Borrowed
+ * from Weave's utils.js.
+ *
+ * @return the new guid
+ */
+ makeGUID: function makeGUID() {
+ // 70 characters that are not-escaped URL-friendly
+ const code =
+ "!()*-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~";
+
+ let guid = "";
+ let num = 0;
+ let val;
+
+ // Generate ten 70-value characters for a 70^10 (~61.29-bit) GUID
+ for (let i = 0; i < 10; i++) {
+ // Refresh the number source after using it a few times
+ if (i == 0 || i == 5)
+ num = Math.random();
+
+ // Figure out which code to use for the next GUID character
+ num *= 70;
+ val = Math.floor(num);
+ guid += code[val];
+ num -= val;
+ }
+
+ return guid;
+ },
+
+ /**
+ * insertValue
+ *
+ * Inserts the specified value for the specified fieldname into the
+ * moz_formhistory table.
+ *
+ * @param fieldname The form fieldname to insert
+ * @param value The form value to insert
+ * @param us The time, in microseconds, to use for the lastUsed
+ * and firstUsed columns
+ * @return nothing
+ */
+ insertValue: function (fieldname, value, us) {
+ let query = this.createStatement(
+ "INSERT INTO moz_formhistory " +
+ "(fieldname, value, timesUsed, firstUsed, lastUsed, guid) VALUES " +
+ "(:fieldname, :value, :timesUsed, :firstUsed, :lastUsed, :guid)");
+ query.params.fieldname = fieldname;
+ query.params.value = value;
+ query.params.timesUsed = 1;
+ query.params.firstUsed = us;
+ query.params.lastUsed = us;
+ query.params.guid = this.makeGUID();
+ query.execute();
+ query.reset();
+ },
+
+ /**
+ * updateValue
+ *
+ * Updates a row in the moz_formhistory table with a new value.
+ *
+ * @param id The id of the row to update
+ * @param newvalue The new value to set
+ * @return nothing
+ */
+ updateValue: function (id, newvalue) {
+ let query = this.createStatement(
+ "UPDATE moz_formhistory SET value = :value WHERE id = :id");
+ query.params.id = id;
+ query.params.value = newvalue;
+ query.execute();
+ query.reset();
+ },
+
+ /**
+ * getDataForValue
+ *
+ * Retrieves a set of values for a row in the database that
+ * corresponds to the given fieldname and value.
+ *
+ * @param fieldname The fieldname of the row to query
+ * @param value The value of the row to query
+ * @return null if no row is found with the specified fieldname and value,
+ * or an object containing the row's id, lastUsed, and firstUsed
+ * values
+ */
+ getDataForValue: function (fieldname, value) {
+ let query = this.createStatement(
+ "SELECT id, lastUsed, firstUsed FROM moz_formhistory WHERE " +
+ "fieldname = :fieldname AND value = :value");
+ query.params.fieldname = fieldname;
+ query.params.value = value;
+ if (!query.executeStep())
+ return null;
+
+ return {
+ id: query.row.id,
+ lastUsed: query.row.lastUsed,
+ firstUsed: query.row.firstUsed
+ };
+ },
+
+ /**
+ * createStatement
+ *
+ * Creates a statement from a SQL string. This function is borrowed
+ * from Weave's forms.js.
+ *
+ * @param query The SQL query string
+ * @return the mozIStorageStatement created from the specified SQL
+ */
+ createStatement: function createStatement(query) {
+ try {
+ // Just return the statement right away if it's okay
+ return formService.DBConnection.createStatement(query);
+ }
+ catch(ex) {
+ // Assume guid column must not exist yet, so add it with an index
+ formService.DBConnection.executeSimpleSQL(
+ "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT");
+ formService.DBConnection.executeSimpleSQL(
+ "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index " +
+ "ON moz_formhistory (guid)");
+ }
+
+ // Try creating the query now that the column exists
+ return formService.DBConnection.createStatement(query);
+ }
+};
+
+/**
+ * FormData class constructor
+ *
+ * Initializes instance properties.
+ */
+function FormData(props, usSinceEpoch) {
+ this.fieldname = null;
+ this.value = null;
+ this.date = 0;
+ this.newvalue = null;
+ this.usSinceEpoch = usSinceEpoch;
+
+ for (var prop in props) {
+ if (prop in this)
+ this[prop] = props[prop];
+ }
+}
+
+/**
+ * FormData instance methods
+ */
+FormData.prototype = {
+ /**
+ * hours_to_us
+ *
+ * Converts hours since present to microseconds since epoch.
+ *
+ * @param hours The number of hours since the present time (e.g., 0 is
+ * 'now', and -1 is 1 hour ago)
+ * @return the corresponding number of microseconds since the epoch
+ */
+ hours_to_us: function(hours) {
+ return this.usSinceEpoch + (hours * 60 * 60 * 1000 * 1000);
+ },
+
+ /**
+ * Create
+ *
+ * If this FormData object doesn't exist in the moz_formhistory database,
+ * add it. Throws on error.
+ *
+ * @return nothing
+ */
+ Create: function() {
+ Logger.AssertTrue(this.fieldname != null && this.value != null,
+ "Must specify both fieldname and value");
+
+ let formdata = FormDB.getDataForValue(this.fieldname, this.value);
+ if (!formdata) {
+ // this item doesn't exist yet in the db, so we need to insert it
+ FormDB.insertValue(this.fieldname, this.value,
+ this.hours_to_us(this.date));
+ }
+ else {
+ /* Right now, we ignore this case. If bug 552531 is ever fixed,
+ we might need to add code here to update the firstUsed or
+ lastUsed fields, as appropriate.
+ */
+ }
+ },
+
+ /**
+ * Find
+ *
+ * Attempts to locate an entry in the moz_formhistory database that
+ * matches the fieldname and value for this FormData object.
+ *
+ * @return true if this entry exists in the database, otherwise false
+ */
+ Find: function() {
+ let formdata = FormDB.getDataForValue(this.fieldname, this.value);
+ let status = formdata != null;
+ if (status) {
+ /*
+ //form history dates currently not synced! bug 552531
+ let us = this.hours_to_us(this.date);
+ status = Logger.AssertTrue(
+ us >= formdata.firstUsed && us <= formdata.lastUsed,
+ "No match for with that date value");
+
+ if (status)
+ */
+ this.id = formdata.id;
+ }
+ return status;
+ },
+
+ /**
+ * Remove
+ *
+ * Removes the row represented by this FormData instance from the
+ * moz_formhistory database.
+ *
+ * @return nothing
+ */
+ Remove: function() {
+ /* Right now Weave doesn't handle this correctly, see bug 568363.
+ */
+ formService.removeEntry(this.fieldname, this.value);
+ return true;
+ },
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/history.jsm
@@ -0,0 +1,194 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Crossweave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ * Philipp von Weitershausen <philipp@weitershausen.de>
+ *
+ * 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 ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ * Components.utils.import() and acts as a singleton. Only the following
+ * listed symbols will exposed on import, and only when and where imported.
+ */
+
+var EXPORTED_SYMBOLS = ["HistoryEntry"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+CU.import("resource://gre/modules/Services.jsm");
+CU.import("resource://gre/modules/PlacesUtils.jsm");
+CU.import("resource://tps/logger.jsm");
+CU.import("resource://services-sync/async.js");
+
+/**
+ * HistoryEntry object
+ *
+ * Contains methods for manipulating browser history entries.
+ */
+var HistoryEntry = {
+ /**
+ * _db
+ *
+ * Returns the DBConnection object for the history service.
+ */
+ get _db() {
+ return PlacesUtils.history.QueryInterface(CI.nsPIPlacesDatabase).DBConnection;
+ },
+
+ /**
+ * _visitStm
+ *
+ * Return the SQL statement for getting history visit information
+ * from the moz_historyvisits table. Borrowed from Weave's
+ * history.js.
+ */
+ get _visitStm() {
+ let stm = this._db.createStatement(
+ "SELECT visit_type type, visit_date date " +
+ "FROM moz_historyvisits " +
+ "WHERE place_id = (" +
+ "SELECT id " +
+ "FROM moz_places " +
+ "WHERE url = :url) " +
+ "ORDER BY date DESC LIMIT 10");
+ this.__defineGetter__("_visitStm", function() stm);
+ return stm;
+ },
+
+ /**
+ * _getVisits
+ *
+ * Gets history information about visits to a given uri.
+ *
+ * @param uri The uri to get visits for
+ * @return an array of objects with 'date' and 'type' properties,
+ * corresponding to the visits in the history database for the
+ * given uri
+ */
+ _getVisits: function HistStore__getVisits(uri) {
+ this._visitStm.params.url = uri;
+ return Async.querySpinningly(this._visitStm, ["date", "type"]);
+ },
+
+ /**
+ * Add
+ *
+ * Adds visits for a uri to the history database. Throws on error.
+ *
+ * @param item An object representing one or more visits to a specific uri
+ * @param usSinceEpoch The number of microseconds from Epoch to
+ * the time the current Crossweave run was started
+ * @return nothing
+ */
+ Add: function(item, usSinceEpoch) {
+ Logger.AssertTrue("visits" in item && "uri" in item,
+ "History entry in test file must have both 'visits' " +
+ "and 'uri' properties");
+ let uri = Services.io.newURI(item.uri, null, null);
+ for each (visit in item.visits) {
+ let visitId = PlacesUtils.history.addVisit(
+ uri,
+ usSinceEpoch + (visit.date * 60 * 60 * 1000 * 1000),
+ null, visit.type,
+ visit.type == 5 || visit.type == 6, 0);
+ Logger.AssertTrue(visitId, "Error adding history entry");
+ if ("title" in item)
+ PlacesUtils.history.setPageTitle(uri, item.title);
+ }
+ },
+
+ /**
+ * Find
+ *
+ * Finds visits for a uri to the history database. Throws on error.
+ *
+ * @param item An object representing one or more visits to a specific uri
+ * @param usSinceEpoch The number of microseconds from Epoch to
+ * the time the current Crossweave run was started
+ * @return true if all the visits for the uri are found, otherwise false
+ */
+ Find: function(item, usSinceEpoch) {
+ Logger.AssertTrue("visits" in item && "uri" in item,
+ "History entry in test file must have both 'visits' " +
+ "and 'uri' properties");
+ let curvisits = curvisits = this._getVisits(item.uri);
+ for each (visit in curvisits) {
+ for each (itemvisit in item.visits) {
+ let expectedDate = itemvisit.date * 60 * 60 * 1000 * 1000
+ + usSinceEpoch;
+ if (visit.type == itemvisit.type && visit.date == expectedDate) {
+ itemvisit.found = true;
+ }
+ }
+ }
+
+ let all_items_found = true;
+ for each (itemvisit in item.visits) {
+ all_items_found = all_items_found && "found" in itemvisit;
+ Logger.logInfo("History entry for " + item.uri + ", type:" +
+ itemvisit.type + ", date:" + itemvisit.date +
+ ("found" in itemvisit ? " is present" : " is not present"));
+ }
+ return all_items_found;
+ },
+
+ /**
+ * Delete
+ *
+ * Removes visits from the history database. Throws on error.
+ *
+ * @param item An object representing items to delete
+ * @param usSinceEpoch The number of microseconds from Epoch to
+ * the time the current Crossweave run was started
+ * @return nothing
+ */
+ Delete: function(item, usSinceEpoch) {
+ if ("uri" in item) {
+ let uri = Services.io.newURI(item.uri, null, null);
+ PlacesUtils.history.removePage(uri);
+ }
+ else if ("host" in item) {
+ PlacesUtils.history.removePagesFromHost(item.host, false);
+ }
+ else if ("begin" in item && "end" in item) {
+ PlacesUtils.history.removeVisitsByTimeframe(
+ usSinceEpoch + (item.begin * 60 * 60 * 1000 * 1000),
+ usSinceEpoch + (item.end * 60 * 60 * 1000 * 1000));
+ }
+ else {
+ Logger.AssertTrue(false, "invalid entry in delete history");
+ }
+ },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/logger.jsm
@@ -0,0 +1,152 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Crossweave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ Components.utils.import() and acts as a singleton.
+ Only the following listed symbols will exposed on import, and only when
+ and where imported. */
+
+var EXPORTED_SYMBOLS = ["Logger"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+var Logger =
+{
+ _foStream: null,
+ _converter: null,
+ _potentialError: null,
+
+ init: function (path) {
+ if (this._converter != null) {
+ // we're already open!
+ return;
+ }
+
+ this._file = CC["@mozilla.org/file/local;1"]
+ .createInstance(CI.nsILocalFile);
+ this._file.initWithPath(path);
+ var exists = this._file.exists();
+
+ // Make a file output stream and converter to handle it.
+ this._foStream = CC["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(CI.nsIFileOutputStream);
+ // If the file already exists, append it, otherwise create it.
+ var fileflags = exists ? 0x02 | 0x08 | 0x10 : 0x02 | 0x08 | 0x20;
+
+ this._foStream.init(this._file, fileflags, 0666, 0);
+ this._converter = CC["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(CI.nsIConverterOutputStream);
+ this._converter.init(this._foStream, "UTF-8", 0, 0);
+ },
+
+ write: function (data) {
+ if (this._converter == null) {
+ CU.reportError(
+ "TPS Logger.write called with _converter == null!");
+ return;
+ }
+ this._converter.writeString(data);
+ },
+
+ close: function () {
+ if (this._converter != null) {
+ this._converter.close();
+ this._converter = null;
+ this._foStream = null;
+ }
+ },
+
+ AssertTrue: function(bool, msg, showPotentialError) {
+ if (!bool) {
+ let message = msg;
+ if (showPotentialError && this._potentialError) {
+ message += "; " + this._potentialError;
+ this._potentialError = null;
+ }
+ throw("ASSERTION FAILED! " + message);
+ }
+ },
+
+ AssertEqual: function(val1, val2, msg) {
+ if (val1 != val2)
+ throw("ASSERTION FAILED! " + msg + "; expected " +
+ JSON.stringify(val2) + ", got " + JSON.stringify(val1));
+ },
+
+ log: function (msg) {
+ dump(msg + "\n");
+ var now = new Date()
+ this.write(now.getFullYear() + "-" + (now.getMonth() < 9 ? '0' : '') +
+ (now.getMonth() + 1) + "-" +
+ (now.getDay() < 9 ? '0' : '') + (now.getDay() + 1) + " " +
+ (now.getHours() < 10 ? '0' : '') + now.getHours() + ":" +
+ (now.getMinutes() < 10 ? '0' : '') + now.getMinutes() + ":" +
+ (now.getSeconds() < 10 ? '0' : '') + now.getSeconds() + " " +
+ msg + "\n");
+ },
+
+ clearPotentialError: function() {
+ this._potentialError = null;
+ },
+
+ logPotentialError: function(msg) {
+ this._potentialError = msg;
+ },
+
+ logLastPotentialError: function(msg) {
+ var message = msg;
+ if (this._potentialError) {
+ message = this._poentialError;
+ this._potentialError = null;
+ }
+ this.log("CROSSWEAVE ERROR: " + message);
+ },
+
+ logError: function (msg) {
+ this.log("CROSSWEAVE ERROR: " + msg);
+ },
+
+ logInfo: function (msg) {
+ this.log("CROSSWEAVE INFO: " + msg);
+ },
+
+ logPass: function (msg) {
+ this.log("CROSSWEAVE TEST PASS: " + msg);
+ },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/passwords.jsm
@@ -0,0 +1,185 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Crossweave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ * Philipp von Weitershausen <philipp@weitershausen.de>
+ *
+ * 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 ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ * Components.utils.import() and acts as a singleton. Only the following
+ * listed symbols will exposed on import, and only when and where imported.
+ */
+
+var EXPORTED_SYMBOLS = ["Password"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+CU.import("resource://gre/modules/Services.jsm");
+CU.import("resource://tps/logger.jsm");
+
+let nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ CI.nsILoginInfo,
+ "init");
+
+/**
+ * PasswordProps object; holds password properties.
+ */
+function PasswordProps(props) {
+ this.hostname = null;
+ this.submitURL = null;
+ this.realm = null;
+ this.username = "";
+ this.password = "";
+ this.usernameField = "";
+ this.passwordField = "";
+ this.delete = false;
+
+ for (var prop in props) {
+ if (prop in this)
+ this[prop] = props[prop];
+ }
+}
+
+/**
+ * Password class constructor. Initializes instance properties.
+ */
+function Password(props) {
+ this.props = new PasswordProps(props);
+ if ("changes" in props) {
+ this.updateProps = new PasswordProps(props);
+ for (var prop in props.changes)
+ if (prop in this.updateProps)
+ this.updateProps[prop] = props.changes[prop];
+ }
+ else {
+ this.updateProps = null;
+ }
+}
+
+/**
+ * Password instance methods.
+ */
+Password.prototype = {
+ /**
+ * Create
+ *
+ * Adds a password entry to the login manager for the password
+ * represented by this object's properties. Throws on error.
+ *
+ * @return the new login guid
+ */
+ Create: function() {
+ let login = new nsLoginInfo(this.props.hostname, this.props.submitURL,
+ this.props.realm, this.props.username,
+ this.props.password,
+ this.props.usernameField,
+ this.props.passwordField);
+ Services.logins.addLogin(login);
+ login.QueryInterface(CI.nsILoginMetaInfo);
+ return login.guid;
+ },
+
+ /**
+ * Find
+ *
+ * Finds a password entry in the login manager, for the password
+ * represented by this object's properties.
+ *
+ * @return the guid of the password if found, otherwise -1
+ */
+ Find: function() {
+ let logins = Services.logins.findLogins({}, this.props.hostname,
+ this.props.submitURL,
+ this.props.realm);
+ for (var i = 0; i < logins.length; i++) {
+ if (logins[i].username == this.props.username &&
+ logins[i].password == this.props.password &&
+ logins[i].usernameField == this.props.usernameField &&
+ logins[i].passwordField == this.props.passwordField) {
+ logins[i].QueryInterface(CI.nsILoginMetaInfo);
+ return logins[i].guid;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Update
+ *
+ * Updates an existing password entry in the login manager with
+ * new properties. Throws on error. The 'old' properties are this
+ * object's properties, the 'new' properties are the properties in
+ * this object's 'updateProps' object.
+ *
+ * @return nothing
+ */
+ Update: function() {
+ let oldlogin = new nsLoginInfo(this.props.hostname,
+ this.props.submitURL,
+ this.props.realm,
+ this.props.username,
+ this.props.password,
+ this.props.usernameField,
+ this.props.passwordField);
+ let newlogin = new nsLoginInfo(this.updateProps.hostname,
+ this.updateProps.submitURL,
+ this.updateProps.realm,
+ this.updateProps.username,
+ this.updateProps.password,
+ this.updateProps.usernameField,
+ this.updateProps.passwordField);
+ Services.logins.modifyLogin(oldlogin, newlogin);
+ },
+
+ /**
+ * Remove
+ *
+ * Removes an entry from the login manager for a password which
+ * matches this object's properties. Throws on error.
+ *
+ * @return nothing
+ */
+ Remove: function() {
+ let login = new nsLoginInfo(this.props.hostname,
+ this.props.submitURL,
+ this.props.realm,
+ this.props.username,
+ this.props.password,
+ this.props.usernameField,
+ this.props.passwordField);
+ Services.logins.removeLogin(login);
+ },
+};
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/prefs.jsm
@@ -0,0 +1,150 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Crossweave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ Components.utils.import() and acts as a singleton.
+ Only the following listed symbols will exposed on import, and only when
+ and where imported. */
+
+var EXPORTED_SYMBOLS = ["Preference"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+const WEAVE_PREF_PREFIX = "services.sync.prefs.sync.";
+
+let prefs = CC["@mozilla.org/preferences-service;1"]
+ .getService(CI.nsIPrefBranch);
+
+CU.import("resource://tps/logger.jsm");
+
+/**
+ * Preference class constructor
+ *
+ * Initializes instance properties.
+ */
+function Preference (props) {
+ Logger.AssertTrue("name" in props && "value" in props,
+ "Preference must have both name and value");
+
+ this.name = props.name;
+ this.value = props.value;
+}
+
+/**
+ * Preference instance methods
+ */
+Preference.prototype = {
+ /**
+ * Modify
+ *
+ * Sets the value of the preference this.name to this.value.
+ * Throws on error.
+ *
+ * @return nothing
+ */
+ Modify: function() {
+ // Determine if this pref is actually something Weave even looks at.
+ let weavepref = WEAVE_PREF_PREFIX + this.name;
+ try {
+ let syncPref = prefs.getBoolPref(weavepref);
+ if (!syncPref)
+ prefs.setBoolPref(weavepref, true);
+ }
+ catch(e) {
+ Logger.AssertTrue(false, "Weave doesn't sync pref " + this.name);
+ }
+
+ // Modify the pref; throw an exception if the pref type is different
+ // than the value type specified in the test.
+ let prefType = prefs.getPrefType(this.name);
+ switch (prefType) {
+ case CI.nsIPrefBranch.PREF_INT:
+ Logger.AssertEqual(typeof(this.value), "number",
+ "Wrong type used for preference value");
+ prefs.setIntPref(this.name, this.value);
+ break;
+ case CI.nsIPrefBranch.PREF_STRING:
+ Logger.AssertEqual(typeof(this.value), "string",
+ "Wrong type used for preference value");
+ prefs.setCharPref(this.name, this.value);
+ break;
+ case CI.nsIPrefBranch.PREF_BOOL:
+ Logger.AssertEqual(typeof(this.value), "boolean",
+ "Wrong type used for preference value");
+ prefs.setBoolPref(this.name, this.value);
+ break;
+ }
+ },
+
+ /**
+ * Find
+ *
+ * Verifies that the preference this.name has the value
+ * this.value. Throws on error, or if the pref's type or value
+ * doesn't match.
+ *
+ * @return nothing
+ */
+ Find: function() {
+ // Read the pref value.
+ let value;
+ try {
+ let prefType = prefs.getPrefType(this.name);
+ switch(prefType) {
+ case CI.nsIPrefBranch.PREF_INT:
+ value = prefs.getIntPref(this.name);
+ break;
+ case CI.nsIPrefBranch.PREF_STRING:
+ value = prefs.getCharPref(this.name);
+ break;
+ case CI.nsIPrefBranch.PREF_BOOL:
+ value = prefs.getBoolPref(this.name);
+ break;
+ }
+ }
+ catch (e) {
+ Logger.AssertTrue(false, "Error accessing pref " + this.name);
+ }
+
+ // Throw an exception if the current and expected values aren't of
+ // the same type, or don't have the same values.
+ Logger.AssertEqual(typeof(value), typeof(this.value),
+ "Value types don't match");
+ Logger.AssertEqual(value, this.value, "Preference values don't match");
+ },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/quit.js
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; -*- */
+/* ***** 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 The Original Code is Mozilla Automated Testing Code
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2005
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s): Bob Clary <bob@bclary.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 mozilla/toolkit/content
+ These files did not have a license
+*/
+var EXPORTED_SYMBOLS = ["goQuitApplication"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function canQuitApplication()
+{
+ try
+ {
+ var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Components.interfaces.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // Something aborted the quit process.
+ if (cancelQuit.data)
+ {
+ return false;
+ }
+ }
+ catch (ex)
+ {
+ }
+ return true;
+}
+
+function goQuitApplication()
+{
+ if (!canQuitApplication())
+ {
+ return false;
+ }
+
+ const kAppStartup = '@mozilla.org/toolkit/app-startup;1';
+ const kAppShell = '@mozilla.org/appshell/appShellService;1';
+ var appService;
+ var forceQuit;
+
+ if (kAppStartup in Components.classes)
+ {
+ appService = Components.classes[kAppStartup].
+ getService(Components.interfaces.nsIAppStartup);
+ forceQuit = Components.interfaces.nsIAppStartup.eForceQuit;
+ }
+ else if (kAppShell in Components.classes)
+ {
+ appService = Components.classes[kAppShell].
+ getService(Components.interfaces.nsIAppShellService);
+ forceQuit = Components.interfaces.nsIAppShellService.eForceQuit;
+ }
+ else
+ {
+ throw 'goQuitApplication: no AppStartup/appShell';
+ }
+
+ try
+ {
+ appService.quit(forceQuit);
+ }
+ catch(ex)
+ {
+ throw('goQuitApplication: ' + ex);
+ }
+
+ return true;
+}
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/tabs.jsm
@@ -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 Crossweave.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ Components.utils.import() and acts as a singleton.
+ Only the following listed symbols will exposed on import, and only when
+ and where imported. */
+
+var EXPORTED_SYMBOLS = ["BrowserTabs"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+CU.import("resource://services-sync/engines.js");
+
+var BrowserTabs = {
+ /**
+ * Add
+ *
+ * Opens a new tab in the current browser window for the
+ * given uri. Throws on error.
+ *
+ * @param uri The uri to load in the new tab
+ * @return nothing
+ */
+ Add: function(uri, fn) {
+ // Open the uri in a new tab in the current browser window, and calls
+ // the callback fn from the tab's onload handler.
+ let wm = CC["@mozilla.org/appshell/window-mediator;1"]
+ .getService(CI.nsIWindowMediator);
+ let mainWindow = wm.getMostRecentWindow("navigator:browser");
+ let newtab = mainWindow.getBrowser().addTab(uri);
+ mainWindow.getBrowser().selectedTab = newtab;
+ let win = mainWindow.getBrowser().getBrowserForTab(newtab);
+ win.addEventListener("load", function() { fn.call(); }, true);
+ },
+
+ /**
+ * Find
+ *
+ * Finds the specified uri and title in Weave's list of remote tabs
+ * for the specified profile.
+ *
+ * @param uri The uri of the tab to find
+ * @param title The page title of the tab to find
+ * @param profile The profile to search for tabs
+ * @return true if the specified tab could be found, otherwise false
+ */
+ Find: function(uri, title, profile) {
+ // Find the uri in Weave's list of tabs for the given profile.
+ let engine = Engines.get("tabs");
+ for (let [guid, client] in Iterator(engine.getAllClients())) {
+ for each (tab in client.tabs) {
+ let weaveTabUrl = tab.urlHistory[0];
+ if (uri == weaveTabUrl && profile == client.clientName)
+ if (title == undefined || title == tab.title)
+ return true;
+ }
+ }
+ return false;
+ },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tps/modules/tps.jsm
@@ -0,0 +1,651 @@
+/* ***** 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 TPS.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Jonathan Griffin <jgriffin@mozilla.com>
+ * Philipp von Weitershausen <philipp@weitershausen.de>
+ *
+ * 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 ***** */
+
+ /* This is a JavaScript module (JSM) to be imported via
+ * Components.utils.import() and acts as a singleton. Only the following
+ * listed symbols will exposed on import, and only when and where imported.
+ */
+
+var EXPORTED_SYMBOLS = ["TPS"];
+
+const CC = Components.classes;
+const CI = Components.interfaces;
+const CU = Components.utils;
+
+CU.import("resource://services-sync/service.js");
+CU.import("resource://services-sync/constants.js");
+CU.import("resource://services-sync/util.js");
+CU.import("resource://gre/modules/XPCOMUtils.jsm");
+CU.import("resource://gre/modules/Services.jsm");
+CU.import("resource://tps/bookmarks.jsm");
+CU.import("resource://tps/logger.jsm");
+CU.import("resource://tps/passwords.jsm");
+CU.import("resource://tps/history.jsm");
+CU.import("resource://tps/forms.jsm");
+CU.import("resource://tps/prefs.jsm");
+CU.import("resource://tps/tabs.jsm");
+
+const ACTION_ADD = "add";
+const ACTION_VERIFY = "verify";
+const ACTION_VERIFY_NOT = "verify-not";
+const ACTION_MODIFY = "modify";
+const ACTION_SYNC = "sync";
+const ACTION_DELETE = "delete";
+const ACTION_PRIVATE_BROWSING = "private-browsing";
+const ACTION_WIPE_SERVER = "wipe-server";
+const ACTIONS = [ACTION_ADD, ACTION_VERIFY, ACTION_VERIFY_NOT,
+ ACTION_MODIFY, ACTION_SYNC, ACTION_DELETE,
+ ACTION_PRIVATE_BROWSING, ACTION_WIPE_SERVER];
+
+const SYNC_WIPE_SERVER = "wipe-server";
+const SYNC_RESET_CLIENT = "reset-client";
+const SYNC_WIPE_CLIENT = "wipe-client";
+
+function GetFileAsText(file)
+{
+ let channel = Services.io.newChannel(file, null, null);
+ let inputStream = channel.open();
+ if (channel instanceof CI.nsIHttpChannel &&
+ channel.responseStatus != 200) {
+ return "";
+ }
+
+ let streamBuf = "";
+ let sis = CC["@mozilla.org/scriptableinputstream;1"]
+ .createInstance(CI.nsIScriptableInputStream);
+ sis.init(inputStream);
+
+ let available;
+ while ((available = sis.available()) != 0) {
+ streamBuf += sis.read(available);
+ }
+
+ inputStream.close();
+ return streamBuf;
+}
+
+var TPS =
+{
+ _waitingForSync: false,
+ _test: null,
+ _currentAction: -1,
+ _currentPhase: -1,
+ _errors: 0,
+ _syncErrors: 0,
+ _usSinceEpoch: 0,
+ _tabsAdded: 0,
+ _tabsFinished: 0,
+ _phaselist: {},
+ _operations_pending: 0,
+
+ DumpError: function (msg) {
+ this._errors++;
+ Logger.logError("[phase" + this._currentPhase + "] " + msg);
+ this.quit();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver,
+ CI.nsISupportsWeakReference]),
+
+ observe: function TPS__observe(subject, topic, data) {
+ try {
+ Logger.logInfo("----------event observed: " + topic);
+ switch(topic) {
+ case "private-browsing":
+ Logger.logInfo("private browsing " + data);
+ break;
+ case "weave:service:sync:error":
+ if (this._waitingForSync && this._syncErrors == 0) {
+ // if this is the first sync error, retry...
+ Logger.logInfo("sync error; retrying...");
+ this._syncErrors++;
+ this._waitingForSync = false;
+ Weave.Service.logout();
+ Utils.nextTick(this.RunNextTestAction, this);
+ }
+ else {
+ // ...otherwise abort the test
+ this.DumpError("sync error; aborting test");
+ return;
+ }
+ break;
+ case "weave:service:sync:finish":
+ if (this._waitingForSync) {
+ this._syncErrors = 0;
+ this._waitingForSync = false;
+ // Wait a second before continuing, otherwise we can get
+ // 'sync not complete' errors.
+ Utils.namedTimer(function() {
+ Weave.Service.logout();
+ this.FinishAsyncOperation();
+ }, 1000, this, "postsync");
+ }
+ break;
+ case "sessionstore-windows-restored":
+ Utils.nextTick(this.RunNextTestAction, this);
+ break;
+ }
+ }
+ catch(e) {
+ this.DumpError("Exception caught: " + e);
+ return;
+ }
+ },
+
+ StartAsyncOperation: function TPS__StartAsyncOperation() {
+ this._operations_pending++;
+ },
+
+ FinishAsyncOperation: function TPS__FinishAsyncOperation() {
+ this._operations_pending--;
+ if (!this.operations_pending) {
+ this._currentAction++;
+ Utils.nextTick(function() {
+ this.RunNextTestAction();
+ }, this);
+ }
+ },
+
+ quit: function () {
+ Logger.close();
+ this.goQuitApplication();
+ },
+
+ HandleTabs: function (tabs, action) {
+ this._tabsAdded = tabs.length;
+ this._tabsFinished = 0;
+ for each (let tab in tabs) {
+ Logger.logInfo("executing action " + action.toUpperCase() +
+ " on tab " + JSON.stringify(tab));
+ switch(action) {
+ case ACTION_ADD:
+ // When adding tabs, we keep track of how many tabs we're adding,
+ // and wait until we've received that many onload events from our
+ // new tabs before continuing
+ let that = this;
+ let taburi = tab.uri;
+ BrowserTabs.Add(tab.uri, function() {
+ that._tabsFinished++;
+ Logger.logInfo("tab for " + taburi + " finished loading");
+ if (that._tabsFinished == that._tabsAdded) {
+ Logger.logInfo("all tabs loaded, continuing...");
+ that.FinishAsyncOperation();
+ }
+ });
+ break;
+ case ACTION_VERIFY:
+ Logger.AssertTrue(typeof(tab.profile) != "undefined",
+ "profile must be defined when verifying tabs");
+ Logger.AssertTrue(
+ BrowserTabs.Find(tab.uri, tab.title, tab.profile), "error locating tab");
+ break;
+ case ACTION_VERIFY_NOT:
+ Logger.AssertTrue(typeof(tab.profile) != "undefined",
+ "profile must be defined when verifying tabs");
+ Logger.AssertTrue(
+ !BrowserTabs.Find(tab.uri, tab.title, tab.profile),
+ "tab found which was expected to be absent");
+ break;
+ default:
+ Logger.AssertTrue(false, "invalid action: " + action);
+ }
+ }
+ Logger.logPass("executing action " + action.toUpperCase() + " on tabs");
+ },
+
+ HandlePrefs: function (prefs, action) {
+ for each (pref in prefs) {
+ Logger.logInfo("executing action " + action.toUpperCase() +
+ " on pref " + JSON.stringify(pref));
+ let preference = new Preference(pref);
+ switch(action) {
+ case ACTION_MODIFY:
+ preference.Modify();
+ break;
+ case ACTION_VERIFY:
+ preference.Find();
+ break;
+ default:
+ Logger.AssertTrue(false, "invalid action: " + action);
+ }
+ }
+ Logger.logPass("executing action " + action.toUpperCase() + " on pref");
+ },
+
+ HandleForms: function (data, action) {
+ for each (datum in data) {
+ Logger.logInfo("executing action " + action.toUpperCase() +
+ " on form entry " + JSON.stringify(datum));
+ let formdata = new FormData(datum, this._usSinceEpoch);
+ switch(action) {
+ case ACTION_ADD:
+ formdata.Create();
+ break;
+ case ACTION_DELETE:
+ formdata.Remove();
+ break;
+ case ACTION_VERIFY:
+ Logger.AssertTrue(formdata.Find(), "form data not found");
+ break;
+ case ACTION_VERIFY_NOT:
+ Logger.AssertTrue(!formdata.Find(),
+ "form data found, but it shouldn't be present");
+ break;
+ default:
+ Logger.AssertTrue(false, "invalid action: " + action);
+ }
+ }
+ Logger.logPass("executing action " + action.toUpperCase() +
+ " on formdata");
+ },
+
+ HandleHistory: function (entries, action) {
+ for each (entry in entries) {
+ Logger.logInfo("executing action " + action.toUpperCase() +
+ " on history entry " + JSON.stringify(entry));
+ switch(action) {
+ case ACTION_ADD:
+ HistoryEntry.Add(entry, this._usSinceEpoch);
+ break;
+ case ACTION_DELETE:
+ HistoryEntry.Delete(entry, this._usSinceEpoch);
+ break;
+ case ACTION_VERIFY:
+ Logger.AssertTrue(HistoryEntry.Find(entry, this._usSinceEpoch),
+ "Uri visits not found in history database");
+ break;
+ case ACTION_VERIFY_NOT:
+ Logger.AssertTrue(!HistoryEntry.Find(entry, this._usSinceEpoch),
+ "Uri visits found in history database, but they shouldn't be");
+ break;
+ default:
+ Logger.AssertTrue(false, "invalid action: " + action);
+ }
+ }
+ Logger.logPass("executing action " + action.toUpperCase() +
+ " on history");
+ },
+
+ HandlePasswords: function (passwords, action) {
+ for each (password in passwords) {
+ let password_id = -1;
+ Logger.logInfo("executing action " + action.toUpperCase() +
+ " on password " + JSON.stringify(password));
+ var password = new Password(password);
+ switch (action) {
+ case ACTION_ADD:
+ Logger.AssertTrue(password.Create() > -1, "error adding password");
+ break;
+ case ACTION_VERIFY:
+ Logger.AssertTrue(password.Find() != -1, "password not found");
+ break;
+ case ACTION_VERIFY_NOT:
+ Logger.AssertTrue(password.Find() == -1,
+ "password found, but it shouldn't exist");
+ break;
+ case ACTION_DELETE:
+ Logger.AssertTrue(password.Find() != -1, "password not found");
+ password.Remove();
+ break;
+ case ACTION_MODIFY:
+ if (password.updateProps != null) {
+ Logger.AssertTrue(password.Find() != -1, "password not found");
+ password.Update();
+ }
+ break;
+ default:
+ Logger.AssertTrue(false, "invalid action: " + action);
+ }
+ }
+ Logger.logPass("executing action " + action.toUpperCase() +
+ " on passwords");
+ },
+
+ HandleBookmarks: function (bookmarks, action) {
+ let items = [];
+ for (folder in bookmarks) {
+ let last_item_pos = -1;
+ for each (bookmark in bookmarks[folder]) {
+ Logger.clearPotentialError();
+ let placesItem;
+ bookmark['location'] = folder;
+ if (last_item_pos != -1)
+ bookmark['last_item_pos'] = last_item_pos;
+ let item_id = -1;
+ if (action != ACTION_MODIFY && action != ACTION_DELETE)
+ Logger.logInfo("executing action " + action.toUpperCase() +
+ " on bookmark " + JSON.stringify(bookmark));
+ if ("uri" in bookmark)
+ placesItem = new Bookmark(bookmark);
+ else if ("folder" in bookmark)
+ placesItem = new BookmarkFolder(bookmark);
+ else if ("livemark" in bookmark)
+ placesItem = new Livemark(bookmark);
+ else if ("separator" in bookmark)
+ placesItem = new Separator(bookmark);
+ if (action == ACTION_ADD) {
+ item_id = placesItem.Create();
+ }
+ else {
+ item_id = placesItem.Find();
+ if (action == ACTION_VERIFY_NOT) {
+ Logger.AssertTrue(item_id == -1,
+ "places item exists but it shouldn't: " +
+ JSON.stringify(bookmark));
+ }
+ else
+ Logger.AssertTrue(item_id != -1, "places item not found", true);
+ }
+
+ last_item_pos = placesItem.GetItemIndex();
+ items.push(placesItem);
+ }
+ }
+
+ if (action == ACTION_DELETE || action == ACTION_MODIFY) {
+ for each (item in items) {
+ Logger.logInfo("executing action " + action.toUpperCase() +
+ " on bookmark " + JSON.stringify(item));
+ switch(action) {
+ case ACTION_DELETE:
+ item.Remove();
+ break;
+ case ACTION_MODIFY:
+ if (item.updateProps != null)
+ item.Update();
+ break;
+ }
+ }
+ }
+
+ Logger.logPass("executing action " + action.toUpperCase() +
+ " on bookmarks");
+ },
+
+ RunNextTestAction: function() {
+ try {
+ if (this._currentAction >=
+ this._phaselist["phase" + this._currentPhase].length) {
+ // we're all done
+ Logger.logInfo("test phase " + this._currentPhase + ": " +
+ (this._errors ? "FAIL" : "PASS"));
+ this.quit();
+ return;
+ }
+
+ if (this.seconds_since_epoch)
+ this._usSinceEpoch = this.seconds_since_epoch * 1000 * 1000;
+ else {
+ this.DumpError("seconds-since-epoch not set");
+ return;
+ }
+
+ let phase = this._phaselist["phase" + this._currentPhase];
+ let action = phase[this._currentAction];
+ Logger.logInfo("starting action: " + JSON.stringify(action));
+ action[0].call(this, action[1]);
+
+ // if we're in an async operation, don't continue on to the next action
+ if (this._operations_pending)
+ return;
+
+ this._currentAction++;
+ }
+ catch(e) {
+ this.DumpError("Exception caught: " + e);
+ return;
+ }
+ this.RunNextTestAction();
+ },
+
+ RunTestPhase: function (file, phase, logpath) {
+ try {
+ Logger.init(logpath);
+ Logger.logInfo("Weave version: " + WEAVE_VERSION);
+ Logger.logInfo("Firefox builddate: " + Services.appinfo.appBuildID);
+ Logger.logInfo("Firefox version: " + Services.appinfo.version);
+
+ // do some weave housekeeping
+ if (Weave.Service.isLoggedIn) {
+ this.DumpError("Weave logged in on startup...profile may be dirty");
+ return;
+ }
+
+ // setup observers
+ Services.obs.addObserver(this, "weave:service:sync:finish", true);
+ Services.obs.addObserver(this, "weave:service:sync:error", true);
+ Services.obs.addObserver(this, "sessionstore-windows-restored", true);
+ Services.obs.addObserver(this, "private-browsing", true);
+
+ // parse the test file
+ Services.scriptloader.loadSubScript(file, this);
+ this._currentPhase = phase;
+ let this_phase = this._phaselist["phase" + this._currentPhase];
+
+ if (this_phase == undefined) {
+ this.DumpError("invalid phase " + this._currentPhase);
+ return;
+ }
+
+ if (this.phases["phase" + this._currentPhase] == undefined) {
+ this.DumpError("no profile defined for phase " + this._currentPhase);
+ return;
+ }
+ Logger.logInfo("setting client.name to " + this.phases["phase" + this._currentPhase]);
+ Weave.Svc.Prefs.set("client.name", this.phases["phase" + this._currentPhase]);
+
+ // wipe the server at the end of the final test phase
+ if (this.phases["phase" + (parseInt(this._currentPhase) + 1)] == undefined)
+ this_phase.push([this.WipeServer]);
+
+ // start processing the test actions
+ this._currentAction = 0;
+ }
+ catch(e) {
+ this.DumpError("Exception caught: " + e);
+ return;
+ }
+ },
+
+ Phase: function Test__Phase(phasename, fnlist) {
+ this._phaselist[phasename] = fnlist;
+ },
+
+ SetPrivateBrowsing: function TPS__SetPrivateBrowsing(options) {
+ let PBSvc = CC["@mozilla.org/privatebrowsing;1"].
+ getService(CI.nsIPrivateBrowsingService);
+ PBSvc.privateBrowsingEnabled = options;
+ Logger.logInfo("set privateBrowsingEnabled: " + options);
+ },
+
+ Sync: function TPS__Sync(options) {
+ Logger.logInfo("executing Sync " + (options ? options : ""));
+ if (options == SYNC_WIPE_SERVER) {
+ Weave.Svc.Prefs.set("firstSync", "wipeRemote");
+ }
+ else if (options == SYNC_WIPE_CLIENT) {
+ Weave.Svc.Prefs.set("firstSync", "wipeClient");
+ }
+ else if (options == SYNC_RESET_CLIENT) {
+ Weave.Svc.Prefs.set("firstSync", "resetClient");
+ }
+ else {
+ Weave.Svc.Prefs.reset("firstSync");
+ }
+ if (this.config.account) {
+ let account = this.config.account;
+ if (account["serverURL"]) {
+ Weave.Service.serverURL = account["serverURL"];
+ }
+ if (account["admin-secret"]) {
+ // if admin-secret is specified, we'll dynamically create
+ // a new sync account
+ Weave.Svc.Prefs.set("admin-secret", account["admin-secret"]);
+ let suffix = account["account-suffix"];
+ Weave.Service.account = "tps" + suffix + "@mozilla.com";
+ Weave.Service.password = "tps" + suffix + "tps" + suffix;
+ Weave.Service.passphrase = Weave.Utils.generatePassphrase();
+ Weave.Service.createAccount(Weave.Service.account,
+ Weave.Service.password,
+ "dummy1", "dummy2");
+ Weave.Service.login();
+ }
+ else if (account["username"] && account["password"] &&
+ account["passphrase"]) {
+ Weave.Service.account = account["username"];
+ Weave.Service.password = account["password"];
+ Weave.Service.passphrase = account["passphrase"];
+ Weave.Service.login();
+ }
+ else {
+ this.DumpError("Must specify admin-secret, or " +
+ "username/password/passphrase in the config file");
+ return;
+ }
+ }
+ else {
+ this.DumpError("No account information found; did you use " +
+ "a valid config file?");
+ return;
+ }
+ Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status not OK");
+ Weave.Svc.Obs.notify("weave:service:setup-complete");
+ this._waitingForSync = true;
+ this.StartAsyncOperation();
+ Weave.Service.sync();
+ },
+
+ WipeServer: function TPS__WipeServer() {
+ Logger.logInfo("WipeServer()");
+ Weave.Service.login();
+ Weave.Service.wipeServer();
+ Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status not OK");
+ this._waitingForSync = true;
+ this.StartAsyncOperation();
+ Weave.Service.sync();
+ return;
+ },
+};
+
+var Bookmarks = {
+ add: function Bookmarks__add(bookmarks) {
+ TPS.HandleBookmarks(bookmarks, ACTION_ADD);
+ },
+ modify: function Bookmarks__modify(bookmarks) {
+ TPS.HandleBookmarks(bookmarks, ACTION_MODIFY);
+ },
+ delete: function Bookmarks__delete(bookmarks) {
+ TPS.HandleBookmarks(bookmarks, ACTION_DELETE);
+ },
+ verify: function Bookmarks__verify(bookmarks) {
+ TPS.HandleBookmarks(bookmarks, ACTION_VERIFY);
+ },
+ verifyNot: function Bookmarks__verifyNot(bookmarks) {
+ TPS.HandleBookmarks(bookmarks, ACTION_VERIFY_NOT);
+ }
+};
+
+var Formdata = {
+ add: function Formdata__add(formdata) {
+ this.HandleForms(formdata, ACTION_ADD);
+ },
+ delete: function Formdata__delete(formdata) {
+ this.HandleForms(formdata, ACTION_DELETE);
+ },
+ verify: function Formdata__verify(formdata) {
+ this.HandleForms(formdata, ACTION_VERIFY);
+ },
+ verifyNot: function Formdata__verifyNot(formdata) {
+ this.HandleForms(formdata, ACTION_VERIFY_NOT);
+ }
+};
+
+var History = {
+ add: function History__add(history) {
+ this.HandleHistory(history, ACTION_ADD);
+ },
+ delete: function History__delete(history) {
+ this.HandleHistory(history, ACTION_DELETE);
+ },
+ verify: function History__verify(history) {
+ this.HandleHistory(history, ACTION_VERIFY);
+ },
+ verifyNot: function History__verifyNot(history) {
+ this.HandleHistory(history, ACTION_VERIFY_NOT);
+ }
+};
+
+var Passwords = {
+ add: function Passwords__add(passwords) {
+ this.HandlePasswords(passwords, ACTION_ADD);
+ },
+ modify: function Passwords__modify(passwords) {
+ this.HandlePasswords(passwords, ACTION_MODIFY);
+ },
+ delete: function Passwords__delete(passwords) {
+ this.HandlePasswords(passwords, ACTION_DELETE);
+ },
+ verify: function Passwords__verify(passwords) {
+ this.HandlePasswords(passwords, ACTION_VERIFY);
+ },
+ verifyNot: function Passwords__verifyNot(passwords) {
+ this.HandlePasswords(passwords, ACTION_VERIFY_NOT);
+ }
+};
+
+var Prefs = {
+ modify: function Prefs__modify(prefs) {
+ TPS.HandlePrefs(prefs, ACTION_MODIFY);
+ },
+ verify: function Prefs__verify(prefs) {
+ TPS.HandlePrefs(prefs, ACTION_VERIFY);
+ }
+};
+
+var Tabs = {
+ add: function Tabs__add(tabs) {
+ TPS.StartAsyncOperation();
+ TPS.HandleTabs(tabs, ACTION_ADD);
+ },
+ verify: function Tabs__verify(tabs) {
+ TPS.HandleTabs(tabs, ACTION_VERIFY);
+ },
+ verifyNot: function Tabs__verifyNot(tabs) {
+ TPS.HandleTabs(tabs, ACTION_VERIFY_NOT);
+ }
+};
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/INSTALL.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+
+# This scripts sets up a virutalenv and installs TPS into it.
+# It's probably best to specify a path NOT inside the repo, otherwise
+# all the virtualenv files will show up in e.g. hg status.
+
+# get target directory
+if [ ! -z "$1" ]
+then
+ TARGET=$1
+else
+ echo "Usage: INSTALL.sh /path/to/create/virtualenv [/path/to/python2.6]"
+ exit 1
+fi
+
+# decide which python to use
+if [ ! -z "$2" ]
+then
+ PYTHON=$2
+else
+ PYTHON=`which python`
+fi
+if [ -z "${PYTHON}" ]
+then
+ echo "No python found"
+ exit 1
+fi
+
+CWD="`pwd`"
+
+# create the destination directory
+mkdir ${TARGET}
+
+if [ "$?" -gt 0 ]
+then
+ exit 1
+fi
+
+if [ "${OS}" = "Windows_NT" ]
+then
+ BIN_NAME=Scripts/activate
+else
+ BIN_NAME=bin/activate
+fi
+
+# Create a virtualenv:
+curl https://raw.github.com/jonallengriffin/virtualenv/msys/virtualenv.py | ${PYTHON} - ${TARGET}
+cd ${TARGET}
+. $BIN_NAME
+if [ -z "${VIRTUAL_ENV}" ]
+then
+ echo "virtualenv wasn't installed correctly, aborting"
+ exit 1
+fi
+
+# install TPS
+cd ${CWD}
+python setup.py develop
+
+if [ "$?" -gt 0 ]
+then
+ exit 1
+fi
+
+echo
+echo "To run TPS, activate the virtualenv using:"
+echo " source ${TARGET}/${BIN_NAME}"
+echo "then execute tps using:"
+echo " runtps --binary=/path/to/firefox"
+echo
+echo "See runtps --help for all options"
new file mode 100644
--- /dev/null
+++ b/testing/tps/README
@@ -0,0 +1,17 @@
+TPS is a test automation framework for Firefox Sync. See
+https://developer.mozilla.org/en/TPS for documentation.
+
+INSTALLATION:
+
+TPS requires several packages to operate properly. To install TPS and
+required packages, use the INSTALL.sh script, provided:
+
+ ./INSTALL.sh /path/to/create/virtualenv
+
+This script will create a virtalenv and install TPS into it. TPS can then
+be run by activating the virtualenv and executing:
+
+ runtps --binary=/path/to/firefox
+
+
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/config.json
@@ -0,0 +1,23 @@
+{
+ "account": {
+ "serverURL": "",
+ "admin-secret": "",
+ "username": "crossweaveservices@mozilla.com",
+ "password": "crossweaveservicescrossweaveservices",
+ "passphrase": "r-jwcbc-zgf42-fjn72-p5vpp-iypmi"
+ },
+ "resultstore": {
+ "host": "brasstacks.mozilla.com",
+ "path": "/resultserv/post/"
+ },
+ "email": {
+ "username": "crossweave@mozilla.com",
+ "password": "",
+ "passednotificationlist": ["crossweave@mozilla.com"],
+ "notificationlist": ["crossweave@mozilla.com"]
+ },
+ "platform": "win32",
+ "os": "win7",
+ "es": "localhost:9200"
+}
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/microsummary1.txt
@@ -0,0 +1,1 @@
+Static microsummary #1
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/microsummary2.txt
@@ -0,0 +1,1 @@
+Static microsummary #2
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/microsummary3.txt
@@ -0,0 +1,1 @@
+Static microsummary #3
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/page1.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Crossweave Test Page 1</title>
+</head>
+<body>
+<p>
+Crossweave Test Page 1
+</p>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/page2.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Crossweave Test Page 2</title>
+</head>
+<body>
+<p>
+Crossweave Test Page 2
+</p>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/page3.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Crossweave Test Page 3</title>
+</head>
+<body>
+<p>
+Crossweave Test Page 3
+</p>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/page4.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Crossweave Test Page 4</title>
+</head>
+<body>
+<p>
+Crossweave Test Page 4
+</p>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/pages/page5.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+<title>Crossweave Test Page 5</title>
+</head>
+<body>
+<p>
+Crossweave Test Page 5
+</p>
+</body>
+</html>
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/setup.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 TPS.
+#
+# 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 sys
+from setuptools import setup, find_packages
+
+version = '0.2.40'
+
+deps = ['pulsebuildmonitor >= 0.2', 'MozillaPulse == .4',
+ 'mozinfo == 0.3.1', 'mozprofile == 0.1a',
+ 'mozprocess == 0.1a', 'mozrunner == 3.0a', 'mozregression == 0.3',
+ 'mozautolog >= 0.2.0']
+
+# we only support python 2.6+ right now
+assert sys.version_info[0] == 2
+assert sys.version_info[1] >= 6
+
+setup(name='tps',
+ version=version,
+ description='run automated multi-profile sync tests',
+ long_description="""\
+""",
+ classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ keywords='',
+ author='Jonathan Griffin',
+ author_email='jgriffin@mozilla.com',
+ url='http://hg.mozilla.org/services/tps',
+ license='MPL',
+ dependency_links = [
+ "http://people.mozilla.org/~jgriffin/packages/"
+ ],
+ packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=deps,
+ entry_points="""
+ # -*- Entry points: -*-
+ [console_scripts]
+ runtps = tps.cli:main
+ """,
+ )
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/__init__.py
@@ -0,0 +1,41 @@
+# ***** 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 TPS.
+#
+# 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 firefoxrunner import TPSFirefoxRunner
+from pulse import TPSPulseMonitor
+from testrunner import TPSTestRunner
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/cli.py
@@ -0,0 +1,132 @@
+# ***** 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 TPS.
+#
+# 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 optparse
+import os
+import logging
+
+from threading import RLock
+
+from tps import TPSFirefoxRunner, TPSPulseMonitor, TPSTestRunner
+
+def main():
+ parser = optparse.OptionParser()
+ parser.add_option("--email-results",
+ action = "store_true", dest = "emailresults",
+ default = False,
+ help = "email the test results to the recipients defined "
+ "in the config file")
+ parser.add_option("--mobile",
+ action = "store_true", dest = "mobile",
+ default = False,
+ help = "run with mobile settings")
+ parser.add_option("--autolog",
+ action = "store_true", dest = "autolog",
+ default = False,
+ help = "post results to Autolog")
+ parser.add_option("--testfile",
+ action = "store", type = "string", dest = "testfile",
+ default = '../../services/sync/tests/tps/test_sync.js',
+ help = "path to the test file to run "
+ "[default: %default]")
+ parser.add_option("--logfile",
+ action = "store", type = "string", dest = "logfile",
+ default = 'tps.log',
+ help = "path to the log file [default: %default]")
+ parser.add_option("--binary",
+ action = "store", type = "string", dest = "binary",
+ default = None,
+ help = "path to the Firefox binary, specified either as "
+ "a local file or a url; if omitted, the PATH "
+ "will be searched;")
+ parser.add_option("--configfile",
+ action = "store", type = "string", dest = "configfile",
+ default = "config.json",
+ help = "path to the config file to use "
+ "[default: %default]")
+ parser.add_option("--pulsefile",
+ action = "store", type = "string", dest = "pulsefile",
+ default = None,
+ help = "path to file containing a pulse message in "
+ "json format that you want to inject into the monitor")
+ (options, args) = parser.parse_args()
+
+ # load the config file
+ f = open(options.configfile, 'r')
+ configcontent = f.read()
+ f.close()
+ config = json.loads(configcontent)
+
+ rlock = RLock()
+
+ extensionDir = os.path.join(os.getcwd(), "..", "..", "services", "sync", "tps")
+
+ if options.binary is None:
+ # If no binary is specified, start the pulse build monitor, and wait
+ # until we receive build notifications before running tests.
+ monitor = TPSPulseMonitor(extensionDir,
+ config=config,
+ autolog=options.autolog,
+ emailresults=options.emailresults,
+ testfile=options.testfile,
+ logfile=options.logfile,
+ rlock=rlock)
+ print "waiting for pulse build notifications"
+
+ if options.pulsefile:
+ # For testing purposes, inject a pulse message directly into
+ # the monitor.
+ builddata = json.loads(open(options.pulsefile, 'r').read())
+ monitor.onBuildComplete(builddata)
+
+ monitor.listen()
+ return
+
+ TPS = TPSTestRunner(extensionDir,
+ emailresults=options.emailresults,
+ testfile=options.testfile,
+ logfile=options.logfile,
+ binary=options.binary,
+ config=config,
+ rlock=rlock,
+ mobile=options.mobile,
+ autolog=options.autolog)
+ TPS.run_tests()
+
+if __name__ == "__main__":
+ main()
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/emailtemplate.py
@@ -0,0 +1,188 @@
+# ***** 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 TPS.
+#
+# 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 datetime
+
+def GenerateEmailBody(data, numpassed, numfailed, serverUrl):
+
+ now = datetime.datetime.now()
+ builddate = datetime.datetime.strptime(data['productversion']['buildid'],
+ '%Y%m%d%H%M%S')
+ tree = data['productversion']['repository']
+
+ row = """
+<tr>
+ <td><a href="http://hg.mozilla.org/services/services-central/file/tip/services/sync/tests/tps/{name}">{name}</a></td>
+ <td>{state}</td>
+ <td>{message}</td>
+</tr>
+"""
+
+ rowWithLog = """
+<tr>
+ <td><a href="http://hg.mozilla.org/services/services-central/services/sync/tests/tps/file/tip/{name}">{name}</a></td>
+ <td>{state}</td>
+ <td>{message} [<a href="{logurl}">view log</a>]</td>
+</tr>
+"""
+
+ rows = ""
+ for test in data['tests']:
+ if test.get('logurl'):
+ rows += rowWithLog.format(name=test['name'],
+ state=test['state'],
+ message=test['message'] if test['message'] else 'None',
+ logurl=test['logurl'])
+ else:
+ rows += row.format(name=test['name'],
+ state=test['state'],
+ message=test['message'] if test['message'] else 'None')
+
+ body = """
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+ <title>TPS</title>
+ <style type="text/css">
+#headertable {{ border: solid 1px black; margin-bottom: 2em; border-collapse: collapse; font-size: 0.8em; }}
+#headertable th {{ border: solid 1px black; background-color: lightgray; padding: 4px; }}
+#headertable td {{ border: solid 1px black; padding: 4px; }}
+.light {{ color: gray; }}
+.pass, a.pass:link, a.pass:visited {{ color: green; font-weight: bold; }}
+.fail, a.fail:link, a.fail:visited {{ color: red; font-weight: bold; }}
+.rightgray {{ text-align: right; background-color: lightgray; }}
+#summarytable {{ border: solid 1px black; margin-bottom: 2em; border-collapse: collapse; font-size: 0.8em; }}
+#summarytable th {{ border: solid 1px black; background-color: lightgray; padding: 4px; }}
+#summarytable td {{ border: solid 1px black; padding: 4px; }}
+</style>
+</head>
+
+<body>
+ <div id="content">
+
+<h2>TPS Testrun Details</h2>
+
+<table id="headertable">
+
+<tr>
+ <td class="rightgray">Testrun Date</td>
+ <td>{date}</td>
+
+</tr>
+<tr>
+ <td class="rightgray">Firefox Version</td>
+ <td>{firefox_version}</td>
+</tr>
+<tr>
+ <td class="rightgray">Firefox Build Date</td>
+ <td>{firefox_date}</td>
+</tr>
+
+<tr>
+ <td class="rightgray">Firefox Sync Version / Type</td>
+ <td>{sync_version} / {sync_type}
+ </td>
+</tr>
+<tr>
+ <td class="rightgray">Firefox Sync Changeset</td>
+ <td>
+
+ <a href="{repository}/rev/{changeset}">
+
+ {changeset}</a> / {sync_tree}
+
+ </td>
+</tr>
+<tr>
+ <td class="rightgray">Sync Server</td>
+ <td>{server}</td>
+</tr>
+<tr>
+ <td class="rightgray">OS</td>
+ <td>{os}</td>
+</tr>
+<tr>
+ <td class="rightgray">Passed Tests</td>
+
+ <td>
+ <span class="{passclass}">{numpassed}</span>
+ </td>
+</tr>
+<tr>
+ <td class="rightgray">Failed Tests</td>
+ <td>
+
+ <span class="{failclass}">{numfailed}</span>
+ </td>
+</tr>
+</table>
+
+
+<table id="summarytable">
+<thead>
+<tr>
+ <th>Testcase</th>
+ <th>Result</th>
+ <th>Message</th>
+</tr>
+</thead>
+
+{rows}
+
+</table>
+
+ </div>
+</body>
+</html>
+
+""".format(date=now.ctime(),
+ firefox_version=data['productversion']['version'],
+ firefox_date=builddate.ctime(),
+ sync_version=data['addonversion']['version'],
+ sync_type=data['synctype'],
+ sync_tree=tree[tree.rfind("/") + 1:],
+ repository=data['productversion']['repository'],
+ changeset=data['productversion']['changeset'],
+ os=data['os'],
+ rows=rows,
+ numpassed=numpassed,
+ numfailed=numfailed,
+ passclass="pass" if numpassed > 0 else "light",
+ failclass="fail" if numfailed > 0 else "light",
+ server=serverUrl if serverUrl != "" else "default"
+ )
+
+ return body
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/firefoxrunner.py
@@ -0,0 +1,161 @@
+# ***** 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 TPS.
+#
+# 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 copy
+import os
+import shutil
+import sys
+
+from mozprocess.pid import get_pids
+from mozprofile import Profile
+from mozregression.mozInstall import MozInstaller
+from mozregression.utils import download_url, get_platform
+from mozrunner import FirefoxRunner
+
+class TPSFirefoxRunner(object):
+
+ PROCESS_TIMEOUT = 240
+
+ def __init__(self, binary):
+ if binary is not None and ('http://' in binary or 'ftp://' in binary):
+ self.url = binary
+ self.binary = None
+ else:
+ self.url = None
+ self.binary = binary
+ self.runner = None
+ self.installdir = None
+
+ def __del__(self):
+ if self.installdir:
+ shutil.rmtree(self.installdir, True)
+
+ @property
+ def names(self):
+ if sys.platform == 'darwin':
+ return ['firefox', 'minefield']
+ if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
+ return ['firefox', 'mozilla-firefox', 'minefield']
+ if os.name == 'nt' or sys.platform == 'cygwin':
+ return ['firefox']
+
+ def download_build(self, installdir='downloadedbuild',
+ appname='firefox', macAppName='Minefield.app'):
+ self.installdir = os.path.abspath(installdir)
+ buildName = os.path.basename(self.url)
+ pathToBuild = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ buildName)
+
+ # delete the build if it already exists
+ if os.access(pathToBuild, os.F_OK):
+ os.remove(pathToBuild)
+
+ # download the build
+ print "downloading build"
+ download_url(self.url, pathToBuild)
+
+ # install the build
+ print "installing %s" % pathToBuild
+ shutil.rmtree(self.installdir, True)
+ MozInstaller(src=pathToBuild, dest=self.installdir, dest_app=macAppName)
+
+ # remove the downloaded archive
+ os.remove(pathToBuild)
+
+ # calculate path to binary
+ platform = get_platform()
+ if platform['name'] == 'Mac':
+ binary = '%s/%s/Contents/MacOS/%s-bin' % (installdir,
+ macAppName,
+ appname)
+ else:
+ binary = '%s/%s/%s%s' % (installdir,
+ appname,
+ appname,
+ '.exe' if platform['name'] == 'Windows' else '')
+
+ return binary
+
+ def get_respository_info(self):
+ """Read repository information from application.ini and platform.ini."""
+ import ConfigParser
+
+ config = ConfigParser.RawConfigParser()
+ dirname = os.path.dirname(self.runner.binary)
+ repository = { }
+
+ for entry in [['application', 'App'], ['platform', 'Build']]:
+ (file, section) = entry
+ config.read(os.path.join(dirname, '%s.ini' % file))
+
+ for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]:
+ (key, id) = entry
+
+ try:
+ repository['%s_%s' % (file, id)] = config.get(section, key);
+ except:
+ repository['%s_%s' % (file, id)] = None
+
+ return repository
+
+ def run(self, profile=None, timeout=PROCESS_TIMEOUT, env=None, args=None):
+ """Runs the given FirefoxRunner with the given Profile, waits
+ for completion, then returns the process exit code
+ """
+ if profile is None:
+ profile = Profile()
+ self.profile = profile
+
+ if self.binary is None and self.url:
+ self.binary = self.download_build()
+
+ if self.runner is None:
+ self.runner = FirefoxRunner(self.profile, binary=self.binary)
+
+ self.runner.profile = self.profile
+
+ if env is not None:
+ self.runner.env.update(env)
+
+ if args is not None:
+ self.runner.cmdargs = copy.copy(args)
+
+ self.runner.start()
+
+ status = self.runner.process_handler.waitForFinish(timeout=timeout)
+
+ return status
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/phase.py
@@ -0,0 +1,106 @@
+# ***** 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 TPS.
+#
+# 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 os
+import re
+
+class TPSTestPhase(object):
+
+ lineRe = re.compile(
+ r"^(.*?)test phase (?P<matchphase>\d+): (?P<matchstatus>.*)$")
+
+ def __init__(self, phase, profile, testname, testpath, logfile, env,
+ firefoxRunner, logfn):
+ self.phase = phase
+ self.profile = profile
+ self.testname = str(testname) # this might be passed in as unicode
+ self.testpath = testpath
+ self.logfile = logfile
+ self.env = env
+ self.firefoxRunner = firefoxRunner
+ self.log = logfn
+ self._status = None
+ self.errline = ''
+
+ @property
+ def phasenum(self):
+ match = re.match('.*?(\d+)', self.phase)
+ if match:
+ return match.group(1)
+
+ @property
+ def status(self):
+ return self._status if self._status else 'unknown'
+
+ def run(self):
+ # launch Firefox
+ args = [ '-tps', self.testpath,
+ '-tpsphase', self.phasenum,
+ '-tpslogfile', self.logfile ]
+
+ self.log("\nlaunching firefox for phase %s with args %s\n" %
+ (self.phase, str(args)))
+ returncode = self.firefoxRunner.run(env=self.env,
+ args=args,
+ profile=self.profile)
+
+ # parse the logfile and look for results from the current test phase
+ found_test = False
+ f = open(self.logfile, 'r')
+ for line in f:
+
+ # skip to the part of the log file that deals with the test we're running
+ if not found_test:
+ if line.find("Running test %s" % self.testname) > -1:
+ found_test = True
+ else:
+ continue
+
+ # look for the status of the current phase
+ match = self.lineRe.match(line)
+ if match:
+ if match.group("matchphase") == self.phasenum:
+ self._status = match.group("matchstatus")
+ break
+
+ # set the status to FAIL if there is TPS error
+ if line.find("CROSSWEAVE ERROR: ") > -1 and not self._status:
+ self._status = "FAIL"
+ self.errline = line[line.find("CROSSWEAVE ERROR: ") + len("CROSSWEAVE ERROR: "):]
+
+ f.close()
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/pulse.py
@@ -0,0 +1,102 @@
+# ***** 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 TPS.
+#
+# 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 logging
+import socket
+
+from pulsebuildmonitor import PulseBuildMonitor
+
+from tps.thread import TPSTestThread
+
+class TPSPulseMonitor(PulseBuildMonitor):
+ """Listens to pulse messages, and initiates a TPS test run when
+ a relevant 'build complete' message is received.
+ """
+
+ def __init__(self, extensionDir, platform='linux', config=None,
+ autolog=False, emailresults=False, testfile=None,
+ logfile=None, rlock=None, **kwargs):
+ self.buildtype = 'opt'
+ self.autolog = autolog
+ self.emailresults = emailresults
+ self.testfile = testfile
+ self.logfile = logfile
+ self.rlock = rlock
+ self.extensionDir = extensionDir
+ self.config = config
+ self.tree = self.config.get('tree', ['services-central', 'places'])
+ self.platform = self.config.get('platform', 'linux')
+ self.label=('crossweave@mozilla.com|tps_build_monitor_' +
+ socket.gethostname())
+
+ self.logger = logging.getLogger('tps_pulse')
+ self.logger.setLevel(logging.DEBUG)
+ handler = logging.FileHandler('tps_pulse.log')
+ self.logger.addHandler(handler)
+
+ PulseBuildMonitor.__init__(self,
+ tree=self.tree,
+ label=self.label,
+ mobile=False,
+ logger=self.logger,
+ **kwargs)
+
+ def onPulseMessage(self, data):
+ key = data['_meta']['routing_key']
+ #print key
+
+ def onBuildComplete(self, builddata):
+ print "================================================================="
+ print json.dumps(builddata)
+ print "================================================================="
+ try:
+ if not (builddata['platform'] == self.platform and
+ builddata['buildtype'] == self.buildtype):
+ return
+ except KeyError:
+ return
+ thread = TPSTestThread(self.extensionDir,
+ builddata=builddata,
+ emailresults=self.emailresults,
+ autolog=self.autolog,
+ testfile=self.testfile,
+ logfile=self.logfile,
+ rlock=self.rlock,
+ config=self.config)
+ thread.daemon = True
+ thread.start()
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/sendemail.py
@@ -0,0 +1,83 @@
+# ***** 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 TPS.
+#
+# 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 smtplib
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+
+def SendEmail(From=None, To=None, Subject='No Subject',
+ TextData=None, HtmlData=None,
+ Server='mail.mozilla.com', Port=465,
+ Username=None, Password=None):
+ """Sends an e-mail.
+
+ From is an e-mail address, To is a list of e-mail adresses.
+
+ TextData and HtmlData are both strings. You can specify one or both.
+ If you specify both, the e-mail will be sent as a MIME multipart
+ alternative; i.e., the recipient will see the HTML content if his
+ viewer supports it, otherwise he'll see the text content.
+ """
+
+ if From is None or To is None:
+ raise Exception("Both From and To must be specified")
+ if TextData is None and HtmlData is None:
+ raise Exception("Must specify either TextData or HtmlData")
+
+ server = smtplib.SMTP_SSL(Server, Port)
+
+ if Username is not None and Password is not None:
+ server.login(Username, Password)
+
+ if HtmlData is None:
+ msg = MIMEText(TextData)
+ elif TextData is None:
+ msg = MIMEMultipart()
+ msg.preamble = Subject
+ msg.attach(MIMEText(HtmlData, 'html'))
+ else:
+ msg = MIMEMultipart('alternative')
+ msg.attach(MIMEText(TextData, 'plain'))
+ msg.attach(MIMEText(HtmlData, 'html'))
+
+ msg['Subject'] = Subject
+ msg['From'] = From
+ msg['To'] = ', '.join(To)
+
+ server.sendmail(From, To, msg.as_string())
+
+ server.quit()
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/testrunner.py
@@ -0,0 +1,526 @@
+# ***** 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 TPS.
+#
+# 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 httplib
+import json
+import os
+import platform
+import random
+import re
+import socket
+import tempfile
+import time
+import traceback
+import urllib
+
+from threading import RLock
+
+from mozprofile import Profile
+
+from tps.firefoxrunner import TPSFirefoxRunner
+from tps.phase import TPSTestPhase
+
+
+class TempFile(object):
+ """Class for temporary files that delete themselves when garbage-collected.
+ """
+
+ def __init__(self, prefix=None):
+ self.fd, self.filename = self.tmpfile = tempfile.mkstemp(prefix=prefix)
+
+ def write(self, data):
+ if self.fd:
+ os.write(self.fd, data)
+
+ def close(self):
+ if self.fd:
+ os.close(self.fd)
+ self.fd = None
+
+ def cleanup(self):
+ if self.fd:
+ self.close()
+ if os.access(self.filename, os.F_OK):
+ os.remove(self.filename)
+
+ __del__ = cleanup
+
+
+class TPSTestRunner(object):
+
+ default_env = { 'MOZ_CRASHREPORTER_DISABLE': '1',
+ 'GNOME_DISABLE_CRASH_DIALOG': '1',
+ 'XRE_NO_WINDOWS_CRASH_DIALOG': '1',
+ 'MOZ_NO_REMOTE': '1',
+ 'XPCOM_DEBUG_BREAK': 'warn',
+ }
+ default_preferences = { 'app.update.enabled' : False,
+ 'extensions.update.enabled' : False,
+ 'extensions.update.notifyUser' : False,
+ 'browser.shell.checkDefaultBrowser' : False,
+ 'browser.tabs.warnOnClose' : False,
+ 'browser.warnOnQuit': False,
+ 'browser.sessionstore.resume_from_crash': False,
+ 'services.sync.firstSync': 'notReady',
+ 'services.sync.lastversion': '1.0',
+ 'services.sync.log.rootLogger': 'Trace',
+ 'services.sync.log.logger.service.main': 'Trace',
+ 'services.sync.log.logger.engine.bookmarks': 'Trace',
+ 'services.sync.log.appender.console': 'Trace',
+ 'services.sync.log.appender.debugLog.enabled': True,
+ 'browser.dom.window.dump.enabled': True,
+ 'extensions.checkCompatibility.4.0': False,
+ }
+ syncVerRe = re.compile(
+ r"Weave version: (?P<syncversion>.*)\n")
+ ffVerRe = re.compile(
+ r"Firefox version: (?P<ffver>.*)\n")
+ ffDateRe = re.compile(
+ r"Firefox builddate: (?P<ffdate>.*)\n")
+
+ def __init__(self, extensionDir, emailresults=False, testfile="sync.test",
+ binary=None, config=None, rlock=None, mobile=False,
+ autolog=False, logfile="tps.log"):
+ self.extensions = []
+ self.emailresults = emailresults
+ self.testfile = testfile
+ self.logfile = os.path.abspath(logfile)
+ self.binary = binary
+ self.config = config if config else {}
+ self.repo = None
+ self.changeset = None
+ self.branch = None
+ self.numfailed = 0
+ self.numpassed = 0
+ self.nightly = False
+ self.rlock = rlock
+ self.mobile = mobile
+ self.autolog = autolog
+ self.tpsxpi = None
+ self.firefoxRunner = None
+ self.extensionDir = extensionDir
+ self.productversion = None
+ self.addonversion = None
+ self.postdata = {}
+ self.errorlogs = {}
+
+ @property
+ def mobile(self):
+ return self._mobile
+
+ @mobile.setter
+ def mobile(self, value):
+ self._mobile = value
+ self.synctype = 'desktop' if not self._mobile else 'mobile'
+
+ def log(self, msg, printToConsole=False):
+ """Appends a string to the logfile"""
+
+ f = open(self.logfile, 'a')
+ f.write(msg)
+ f.close()
+ if printToConsole:
+ print msg
+
+ def _zip_add_file(self, zip, file, rootDir):
+ zip.write(os.path.join(rootDir, file), file)
+
+ def _zip_add_dir(self, zip, dir, rootDir):
+ try:
+ zip.write(os.path.join(rootDir, dir), dir)
+ except:
+ # on some OS's, adding directory entries doesn't seem to work
+ pass
+ for root, dirs, files in os.walk(os.path.join(rootDir, dir)):
+ for f in files:
+ zip.write(os.path.join(root, f), os.path.join(dir, f))
+
+ def make_xpi(self):
+ """Build the test extension."""
+
+ if self.tpsxpi is None:
+ tpsxpi = os.path.join(self.extensionDir, "tps.xpi")
+
+ if os.access(tpsxpi, os.F_OK):
+ os.remove(tpsxpi)
+ if not os.access(os.path.join(self.extensionDir, "install.rdf"), os.F_OK):
+ raise Exception("extension code not found in %s" % self.extensionDir)
+
+ from zipfile import ZipFile
+ z = ZipFile(tpsxpi, 'w')
+ self._zip_add_file(z, 'chrome.manifest', self.extensionDir)
+ self._zip_add_file(z, 'install.rdf', self.extensionDir)
+ self._zip_add_dir(z, 'components', self.extensionDir)
+ self._zip_add_dir(z, 'modules', self.extensionDir)
+ z.close()
+
+ self.tpsxpi = tpsxpi
+
+ return self.tpsxpi
+
+ def run_single_test(self, testdir, testname):
+ testpath = os.path.join(testdir, testname)
+ self.log("Running test %s\n" % testname)
+
+ # Create a random account suffix that is used when creating test
+ # accounts on a staging server.
+ account_suffix = {"account-suffix": ''.join([str(random.randint(0,9))
+ for i in range(1,6)])}
+ self.config['account'].update(account_suffix)
+
+ # Read and parse the test file, merge it with the contents of the config
+ # file, and write the combined output to a temporary file.
+ f = open(testpath, 'r')
+ testcontent = f.read()
+ f.close()
+ try:
+ test = json.loads(testcontent)
+ except:
+ test = json.loads(testcontent[testcontent.find("{"):testcontent.find("}") + 1])
+
+ testcontent += 'var config = %s;\n' % json.dumps(self.config, indent=2)
+ testcontent += 'var seconds_since_epoch = %d;\n' % int(time.time())
+
+ tmpfile = TempFile(prefix='tps_test_')
+ tmpfile.write(testcontent)
+ tmpfile.close()
+
+ # generate the profiles defined in the test, and a list of test phases
+ profiles = {}
+ phaselist = []
+ for phase in test:
+ profilename = test[phase]
+
+ # create the profile if necessary
+ if not profilename in profiles:
+ profiles[profilename] = Profile(preferences = self.preferences,
+ addons = self.extensions)
+
+ # create the test phase
+ phaselist.append(TPSTestPhase(phase,
+ profiles[profilename],
+ testname,
+ tmpfile.filename,
+ self.logfile,
+ self.env,
+ self.firefoxRunner,
+ self.log))
+
+ # sort the phase list by name
+ phaselist = sorted(phaselist, key=lambda phase: phase.phase)
+
+ # run each phase in sequence, aborting at the first failure
+ for phase in phaselist:
+ phase.run()
+
+ # if a failure occurred, dump the entire sync log into the test log
+ if phase.status != "PASS":
+ for profile in profiles:
+ self.log("\nDumping sync log for profile %s\n" % profiles[profile].profile)
+ for root, dirs, files in os.walk(os.path.join(profiles[profile].profile, 'weave', 'logs')):
+ for f in files:
+ weavelog = os.path.join(profiles[profile].profile, 'weave', 'logs', f)
+ if os.access(weavelog, os.F_OK):
+ f = open(weavelog, 'r')
+ msg = f.read()
+ self.log(msg)
+ f.close()
+ self.log("\n")
+ break;
+
+ # grep the log for FF and sync versions
+ f = open(self.logfile)
+ logdata = f.read()
+ match = self.syncVerRe.search(logdata)
+ sync_version = match.group("syncversion") if match else 'unknown'
+ match = self.ffVerRe.search(logdata)
+ firefox_version = match.group("ffver") if match else 'unknown'
+ match = self.ffDateRe.search(logdata)
+ firefox_builddate = match.group("ffdate") if match else 'unknown'
+ f.close()
+ if phase.status == 'PASS':
+ logdata = ''
+ else:
+ # we only care about the log data for this specific test
+ logdata = logdata[logdata.find('Running test %s' % (str(testname))):]
+
+ result = {
+ 'PASS': lambda x: ('TEST-PASS', ''),
+ 'FAIL': lambda x: ('TEST-UNEXPECTED-FAIL', x.rstrip()),
+ 'unknown': lambda x: ('TEST-UNEXPECTED-FAIL', 'test did not complete')
+ } [phase.status](phase.errline)
+ logstr = "\n%s | %s%s\n" % (result[0], testname, (' | %s' % result[1] if result[1] else ''))
+
+ repoinfo = self.firefoxRunner.get_respository_info()
+ apprepo = repoinfo.get('application_repository', '')
+ appchangeset = repoinfo.get('application_changeset', '')
+
+ # save logdata to a temporary file for posting to the db
+ tmplogfile = None
+ if logdata:
+ tmplogfile = TempFile(prefix='tps_log_')
+ tmplogfile.write(logdata)
+ tmplogfile.close()
+ self.errorlogs[testname] = tmplogfile
+
+ resultdata = ({ "productversion": { "version": firefox_version,
+ "buildid": firefox_builddate,
+ "builddate": firefox_builddate[0:8],
+ "product": "Firefox",
+ "repository": apprepo,
+ "changeset": appchangeset,
+ },
+ "addonversion": { "version": sync_version,
+ "product": "Firefox Sync" },
+ "name": testname,
+ "message": result[1],
+ "state": result[0],
+ "logdata": logdata
+ })
+
+ self.log(logstr, True)
+ for phase in phaselist:
+ print "\t%s: %s" % (phase.phase, phase.status)
+ if phase.status == 'FAIL':
+ break
+
+ return resultdata
+
+ def run_tests(self):
+ # delete the logfile if it already exists
+ if os.access(self.logfile, os.F_OK):
+ os.remove(self.logfile)
+
+ # Make a copy of the default env variables and preferences, and update
+ # them for mobile settings if needed.
+ self.env = self.default_env.copy()
+ self.preferences = self.default_preferences.copy()
+ if self.mobile:
+ self.preferences.update({'services.sync.client.type' : 'mobile'})
+
+ # Acquire a lock to make sure no other threads are running tests
+ # at the same time.
+ if self.rlock:
+ self.rlock.acquire()
+
+ try:
+ # Create the Firefox runner, which will download and install the
+ # build, as needed.
+ if not self.firefoxRunner:
+ self.firefoxRunner = TPSFirefoxRunner(self.binary)
+
+ # now, run the test group
+ self.run_test_group()
+
+ except:
+ traceback.print_exc()
+ self.numpassed = 0
+ self.numfailed = 1
+ if self.emailresults:
+ try:
+ self.sendEmail('<pre>%s</pre>' % traceback.format_exc(),
+ sendTo='crossweave@mozilla.com')
+ except:
+ traceback.print_exc()
+ else:
+ raise
+
+ else:
+ try:
+ if self.autolog:
+ self.postToAutolog()
+ if self.emailresults:
+ self.sendEmail()
+ except:
+ traceback.print_exc()
+ try:
+ self.sendEmail('<pre>%s</pre>' % traceback.format_exc(),
+ sendTo='crossweave@mozilla.com')
+ except:
+ traceback.print_exc()
+
+ # release our lock
+ if self.rlock:
+ self.rlock.release()
+
+ # dump out a summary of test results
+ print 'Test Summary\n'
+ for test in self.postdata.get('tests', {}):
+ print '%s | %s | %s' % (test['state'], test['name'], test['message'])
+
+ def run_test_group(self):
+ self.results = []
+ self.extensions = []
+
+ # set the OS we're running on
+ os_string = platform.uname()[2] + " " + platform.uname()[3]
+ if os_string.find("Darwin") > -1:
+ os_string = "Mac OS X " + platform.mac_ver()[0]
+ if platform.uname()[0].find("Linux") > -1:
+ os_string = "Linux " + platform.uname()[5]
+ if platform.uname()[0].find("Win") > -1:
+ os_string = "Windows " + platform.uname()[3]
+
+ # reset number of passed/failed tests
+ self.numpassed = 0
+ self.numfailed = 0
+
+ # build our tps.xpi extension
+ self.extensions.append(self.make_xpi())
+
+ # build the test list
+ try:
+ f = open(self.testfile)
+ jsondata = f.read()
+ f.close()
+ testfiles = json.loads(jsondata)
+ testlist = testfiles['tests']
+ except ValueError:
+ testlist = [os.path.basename(self.testfile)]
+ testdir = os.path.dirname(self.testfile)
+
+ # run each test, and save the results
+ for test in testlist:
+ result = self.run_single_test(testdir, test)
+
+ if not self.productversion:
+ self.productversion = result['productversion']
+ if not self.addonversion:
+ self.addonversion = result['addonversion']
+
+ self.results.append({'state': result['state'],
+ 'name': result['name'],
+ 'message': result['message'],
+ 'logdata': result['logdata']})
+ if result['state'] == 'TEST-PASS':
+ self.numpassed += 1
+ else:
+ self.numfailed += 1
+
+ # generate the postdata we'll use to post the results to the db
+ self.postdata = { 'tests': self.results,
+ 'os':os_string,
+ 'testtype': 'crossweave',
+ 'productversion': self.productversion,
+ 'addonversion': self.addonversion,
+ 'synctype': self.synctype,
+ }
+
+ def sendEmail(self, body=None, sendTo=None):
+ # send the result e-mail
+ if self.config.get('email') and self.config['email'].get('username') \
+ and self.config['email'].get('password'):
+
+ from tps.sendemail import SendEmail
+ from tps.emailtemplate import GenerateEmailBody
+
+ if body is None:
+ body = GenerateEmailBody(self.postdata, self.numpassed, self.numfailed, self.config['account']['serverURL'])
+
+ subj = "TPS Report: "
+ if self.numfailed == 0 and self.numpassed > 0:
+ subj += "YEEEAAAHHH"
+ else:
+ subj += "PC LOAD LETTER"
+
+ changeset = self.postdata['productversion']['changeset'] if \
+ self.postdata and self.postdata.get('productversion') and \
+ self.postdata['productversion'].get('changeset') \
+ else 'unknown'
+ subj +=", changeset " + changeset + "; " + str(self.numfailed) + \
+ " failed, " + str(self.numpassed) + " passed"
+
+ To = [sendTo] if sendTo else None
+ if not To:
+ if self.numfailed > 0 or self.numpassed == 0:
+ To = self.config['email'].get('notificationlist')
+ else:
+ To = self.config['email'].get('passednotificationlist')
+
+ if To:
+ SendEmail(From=self.config['email']['username'],
+ To=To,
+ Subject=subj,
+ HtmlData=body,
+ Username=self.config['email']['username'],
+ Password=self.config['email']['password'])
+
+ def postToAutolog(self):
+ from mozautolog import RESTfulAutologTestGroup as AutologTestGroup
+
+ group = AutologTestGroup(
+ harness='crossweave',
+ testgroup='crossweave-%s' % self.synctype,
+ server=self.config.get('es'),
+ restserver=self.config.get('restserver'),
+ machine=socket.gethostname(),
+ platform=self.config.get('platform', None),
+ os=self.config.get('os', None),
+ )
+ tree = self.postdata['productversion']['repository']
+ group.set_primary_product(
+ tree=tree[tree.rfind("/")+1:],
+ version=self.postdata['productversion']['version'],
+ buildid=self.postdata['productversion']['buildid'],
+ buildtype='opt',
+ revision=self.postdata['productversion']['changeset'],
+ )
+ group.add_test_suite(
+ passed=self.numpassed,
+ failed=self.numfailed,
+ todo=0,
+ )
+ for test in self.results:
+ if test['state'] != "TEST-PASS":
+ errorlog = self.errorlogs.get(test['name'])
+ errorlog_filename = errorlog.filename if errorlog else None
+ group.add_test_failure(
+ test = test['name'],
+ status = test['state'],
+ text = test['message'],
+ logfile = errorlog_filename
+ )
+ group.submit()
+
+ # Iterate through all testfailure objects, and update the postdata
+ # dict with the testfailure logurl's, if any.
+ for tf in group.testsuites[-1].testfailures:
+ result = [x for x in self.results if x.get('name') == tf.test]
+ if not result:
+ continue
+ result[0]['logurl'] = tf.logurl
+
new file mode 100644
--- /dev/null
+++ b/testing/tps/tps/thread.py
@@ -0,0 +1,102 @@
+# ***** 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 TPS.
+#
+# 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 threading import Thread
+
+from testrunner import TPSTestRunner
+
+class TPSTestThread(Thread):
+
+ def __init__(self, extensionDir, builddata=None, emailresults=False,
+ testfile=None, logfile=None, rlock=None, config=None,
+ autolog=False):
+ assert(builddata)
+ assert(config)
+ self.extensionDir = extensionDir
+ self.builddata = builddata
+ self.emailresults = emailresults
+ self.testfile = testfile
+ self.logfile = logfile
+ self.rlock = rlock
+ self.config = config
+ self.autolog = autolog
+ Thread.__init__(self)
+
+ def run(self):
+ # run the tests in normal mode ...
+ TPS = TPSTestRunner(self.extensionDir,
+ emailresults=self.emailresults,
+ testfile=self.testfile,
+ logfile=self.logfile,
+ binary=self.builddata['buildurl'],
+ config=self.config,
+ rlock=self.rlock,
+ mobile=False,
+ autolog=self.autolog)
+ TPS.run_tests()
+
+ # ... and then again in mobile mode
+ TPS = TPSTestRunner(self.extensionDir,
+ emailresults=self.emailresults,
+ testfile=self.testfile,
+ logfile=self.logfile,
+ binary=self.builddata['buildurl'],
+ config=self.config,
+ rlock=self.rlock,
+ mobile=True,
+ autolog=self.autolog)
+ TPS.run_tests()
+
+ # ... and again via the staging server, if credentials are present
+ stageaccount = self.config.get('stageaccount')
+ if stageaccount:
+ username = stageaccount.get('username')
+ password = stageaccount.get('password')
+ passphrase = stageaccount.get('passphrase')
+ if username and password and passphrase:
+ stageconfig = self.config.copy()
+ stageconfig['account'] = stageaccount.copy()
+ TPS = TPSTestRunner(self.extensionDir,
+ emailresults=self.emailresults,
+ testfile=self.testfile,
+ logfile=self.logfile,
+ binary=self.builddata['buildurl'],
+ config=stageconfig,
+ rlock=self.rlock,
+ mobile=False,
+ autolog=self.autolog)
+ TPS.run_tests()