author | Jonathan Griffin <jgriffin@mozilla.com> |
Wed, 27 Jul 2011 16:32:42 -0700 | |
changeset 73683 | 5a1e829db409e9a853470e27b9db387d92419155 |
parent 73682 | 9d927d1f2ac82facccd6165ca47a4fe11c7f1e42 |
child 73684 | 3773e471ce368ee13d891eafa24353ab026cd8d9 |
push id | 20905 |
push user | pweitershausen@mozilla.com |
push date | Tue, 02 Aug 2011 19:02:56 +0000 |
treeherder | mozilla-central@aca5afbed188 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | philikon, test-only, DONTBUILD |
bugs | 674097 |
milestone | 8.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
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()