From 0ce68fb65db71c39686b7befdeb0aa88ae513f8b Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Wed, 10 Feb 2016 17:16:23 +0100 Subject: [PATCH 01/45] Bug 1243779 - Remove uriIsPrefix option from nsINavHistoryQuery.r=adw MozReview-Commit-ID: CM2Jm6iApct --- .../source/lib/sdk/places/host/host-query.js | 22 +++++- addon-sdk/source/lib/sdk/places/utils.js | 25 ++++-- .../places/nsINavHistoryService.idl | 10 +-- toolkit/components/places/nsNavHistory.cpp | 46 ++--------- .../components/places/nsNavHistoryQuery.cpp | 23 +----- toolkit/components/places/nsNavHistoryQuery.h | 2 - .../queries/test_abstime-annotation-uri.js | 76 ++++--------------- .../tests/queries/test_querySerialization.js | 7 +- .../tests/queries/test_searchterms-uri.js | 51 ++----------- 9 files changed, 72 insertions(+), 190 deletions(-) diff --git a/addon-sdk/source/lib/sdk/places/host/host-query.js b/addon-sdk/source/lib/sdk/places/host/host-query.js index 7984db95ec2..f2dbd6550cd 100644 --- a/addon-sdk/source/lib/sdk/places/host/host-query.js +++ b/addon-sdk/source/lib/sdk/places/host/host-query.js @@ -47,18 +47,32 @@ function execute (queries, options) { return new Promise(resolve => { let root = historyService .executeQueries(queries, queries.length, options).root; - resolve(collect([], root)); + // Let's extract an eventual uri wildcard, if both domain and uri are set. + // See utils.js::urlQueryParser() for more details. + // In case of multiple queries, we only retain the first found wildcard. + let uriWildcard = queries.reduce((prev, query) => { + if (query.uri && query.domain) { + if (!prev) + prev = query.uri.spec; + query.uri = null; + } + return prev; + }, ""); + resolve(collect([], root, uriWildcard)); }); } -function collect (acc, node) { +function collect (acc, node, uriWildcard) { node.containerOpen = true; for (let i = 0; i < node.childCount; i++) { let child = node.getChild(i); - acc.push(child); + + if (!uriWildcard || child.uri.startsWith(uriWildcard)) { + acc.push(child); + } if (child.type === child.RESULT_TYPE_FOLDER) { let container = child.QueryInterface(Ci.nsINavHistoryContainerResultNode); - collect(acc, container); + collect(acc, container, uriWildcard); } } node.containerOpen = false; diff --git a/addon-sdk/source/lib/sdk/places/utils.js b/addon-sdk/source/lib/sdk/places/utils.js index 034b4f946f9..44366d2aaba 100644 --- a/addon-sdk/source/lib/sdk/places/utils.js +++ b/addon-sdk/source/lib/sdk/places/utils.js @@ -12,7 +12,7 @@ module.metadata = { } }; -const { Cc, Ci } = require('chrome'); +const { Cc, Ci, Cu } = require('chrome'); const { Class } = require('../core/heritage'); const { method } = require('../lang/functional'); const { defer, promised, all } = require('../core/promise'); @@ -22,6 +22,8 @@ const { merge } = require('../util/object'); const bmsrv = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. getService(Ci.nsINavBookmarksService); +Cu.importGlobalProperties(["URL"]); + /* * TreeNodes are used to construct dependency trees * for BookmarkItems @@ -128,9 +130,9 @@ exports.isRootGroup = isRootGroup; * --> 'http://moz.com', 'http://moz.com/thunderbird' * '*.moz.com' // domain: moz.com, domainIsHost: false * --> 'http://moz.com', 'http://moz.com/index', 'http://ff.moz.com/test' - * 'http://moz.com' // url: http://moz.com/, urlIsPrefix: false + * 'http://moz.com' // uri: http://moz.com/ * --> 'http://moz.com/' - * 'http://moz.com/*' // url: http://moz.com/, urlIsPrefix: true + * 'http://moz.com/*' // uri: http://moz.com/, domain: moz.com, domainIsHost: true * --> 'http://moz.com/', 'http://moz.com/thunderbird' */ @@ -139,8 +141,21 @@ function urlQueryParser (query, url) { if (/^https?:\/\//.test(url)) { query.uri = url.charAt(url.length - 1) === '/' ? url : url + '/'; if (/\*$/.test(url)) { - query.uri = url.replace(/\*$/, ''); - query.uriIsPrefix = true; + // Wildcard searches on URIs are not supported, so try to extract a + // domain and filter the data later. + url = url.replace(/\*$/, ''); + try { + query.domain = new URL(url).hostname; + query.domainIsHost = true; + // Unfortunately here we cannot use an expando to store the wildcard, + // cause the query is a wrapped native XPCOM object, so we reuse uri. + // We clearly don't want to query for both uri and domain, thus we'll + // have to handle this in host-query.js::execute() + query.uri = url; + } catch (ex) { + // Cannot extract an host cause it's not a valid uri, the query will + // just return nothing. + } } } else { if (/^\*/.test(url)) { diff --git a/toolkit/components/places/nsINavHistoryService.idl b/toolkit/components/places/nsINavHistoryService.idl index 037b3ea1e47..9cfa8bde391 100644 --- a/toolkit/components/places/nsINavHistoryService.idl +++ b/toolkit/components/places/nsINavHistoryService.idl @@ -903,17 +903,9 @@ interface nsINavHistoryQuery : nsISupports attribute AUTF8String domain; readonly attribute boolean hasDomain; - /** - * Controls the interpretation of 'uri'. When unset (default), the URI will - * request an exact match of the specified URI. When set, any history entry - * beginning in 'uri' will match. For example "http://bar.com/foo" will match - * "http://bar.com/foo" as well as "http://bar.com/foo/baz.gif". - */ - attribute boolean uriIsPrefix; - /** * This is a URI to match, to, for example, find out every time you visited - * a given URI. Use uriIsPrefix to control whether this is an exact match. + * a given URI. This is an exact match. */ attribute nsIURI uri; readonly attribute boolean hasUri; diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp index ab1e07bc910..e86ad98f534 100644 --- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -895,27 +895,12 @@ nsNavHistory::EvaluateQueryForNode(const nsCOMArray& aQueries if (NS_FAILED(NS_NewURI(getter_AddRefs(nodeUri), aNode->mURI))) continue; } - if (! query->UriIsPrefix()) { - // easy case: the URI is an exact match - bool equals; - nsresult rv = query->Uri()->Equals(nodeUri, &equals); - NS_ENSURE_SUCCESS(rv, false); - if (! equals) - continue; - } else { - // harder case: match prefix, note that we need to get the ASCII string - // from the node's parsed URI instead of using the node's mUrl string, - // because that might not be normalized - nsAutoCString nodeUriString; - nodeUri->GetAsciiSpec(nodeUriString); - nsAutoCString queryUriString; - query->Uri()->GetAsciiSpec(queryUriString); - if (queryUriString.Length() > nodeUriString.Length()) - continue; // not long enough to match as prefix - nodeUriString.SetLength(queryUriString.Length()); - if (! nodeUriString.Equals(queryUriString)) - continue; // prefixes don't match - } + + bool equals; + nsresult rv = query->Uri()->Equals(nodeUri, &equals); + NS_ENSURE_SUCCESS(rv, false); + if (! equals) + continue; } // Transitions matching. @@ -1324,9 +1309,6 @@ bool IsOptimizableHistoryQuery(const nsCOMArray& aQueries, if (aQuery->AnnotationIsNot() || !aQuery->Annotation().IsEmpty()) return false; - if (aQuery->UriIsPrefix() || aQuery->Uri()) - return false; - if (aQuery->Folders().Length() > 0) return false; @@ -3369,12 +3351,7 @@ nsNavHistory::QueryToSelectClause(nsNavHistoryQuery* aQuery, // const // URI if (NS_SUCCEEDED(aQuery->GetHasUri(&hasIt)) && hasIt) { - if (aQuery->UriIsPrefix()) { - clause.Condition("h.url >= ").Param(":uri") - .Condition("h.url <= ").Param(":uri_upper"); - } - else - clause.Condition("h.url =").Param(":uri"); + clause.Condition("h.url =").Param(":uri"); } // annotation @@ -3568,15 +3545,6 @@ nsNavHistory::BindQueryClauseParameters(mozIStorageBaseStatement* statement, statement, NS_LITERAL_CSTRING("uri") + qIndex, aQuery->Uri() ); NS_ENSURE_SUCCESS(rv, rv); - if (aQuery->UriIsPrefix()) { - nsAutoCString uriString; - aQuery->Uri()->GetSpec(uriString); - uriString.Append(char(0x7F)); // MAX_UTF8 - rv = URIBinder::Bind( - statement, NS_LITERAL_CSTRING("uri_upper") + qIndex, uriString - ); - NS_ENSURE_SUCCESS(rv, rv); - } } // annotation diff --git a/toolkit/components/places/nsNavHistoryQuery.cpp b/toolkit/components/places/nsNavHistoryQuery.cpp index e0acd8bff8b..337da6c7e33 100644 --- a/toolkit/components/places/nsNavHistoryQuery.cpp +++ b/toolkit/components/places/nsNavHistoryQuery.cpp @@ -126,7 +126,6 @@ static void SetOptionsKeyUint32(const nsCString& aValue, #define QUERYKEY_NOTANNOTATION "!annotation" #define QUERYKEY_ANNOTATION "annotation" #define QUERYKEY_URI "uri" -#define QUERYKEY_URIISPREFIX "uriIsPrefix" #define QUERYKEY_SEPARATOR "OR" #define QUERYKEY_GROUP "group" #define QUERYKEY_SORT "sort" @@ -426,9 +425,6 @@ nsNavHistory::QueriesToQueryString(nsINavHistoryQuery **aQueries, // uri query->GetHasUri(&hasIt); if (hasIt) { - AppendBoolKeyValueIfTrue(aQueryString, - NS_LITERAL_CSTRING(QUERYKEY_URIISPREFIX), - query, &nsINavHistoryQuery::GetUriIsPrefix); nsCOMPtr uri; query->GetUri(getter_AddRefs(uri)); NS_ENSURE_TRUE(uri, NS_ERROR_FAILURE); // hasURI should tell is if invalid @@ -725,10 +721,6 @@ nsNavHistory::TokensToQueries(const nsTArray& aTokens, rv = query->SetUri(uri); NS_ENSURE_SUCCESS(rv, rv); - // URI is prefix - } else if (kvp.key.EqualsLiteral(QUERYKEY_URIISPREFIX)) { - SetQueryKeyBool(kvp.value, query, &nsINavHistoryQuery::SetUriIsPrefix); - // not annotation } else if (kvp.key.EqualsLiteral(QUERYKEY_NOTANNOTATION)) { nsAutoCString unescaped(kvp.value); @@ -901,7 +893,7 @@ nsNavHistoryQuery::nsNavHistoryQuery() mBeginTimeReference(TIME_RELATIVE_EPOCH), mEndTime(0), mEndTimeReference(TIME_RELATIVE_EPOCH), mOnlyBookmarked(false), - mDomainIsHost(false), mUriIsPrefix(false), + mDomainIsHost(false), mAnnotationIsNot(false), mTagsAreNot(false) { @@ -916,7 +908,7 @@ nsNavHistoryQuery::nsNavHistoryQuery(const nsNavHistoryQuery& aOther) mEndTime(aOther.mEndTime), mEndTimeReference(aOther.mEndTimeReference), mSearchTerms(aOther.mSearchTerms), mOnlyBookmarked(aOther.mOnlyBookmarked), mDomainIsHost(aOther.mDomainIsHost), mDomain(aOther.mDomain), - mUriIsPrefix(aOther.mUriIsPrefix), mUri(aOther.mUri), + mUri(aOther.mUri), mAnnotationIsNot(aOther.mAnnotationIsNot), mAnnotation(aOther.mAnnotation), mTags(aOther.mTags), mTagsAreNot(aOther.mTagsAreNot), mTransitions(aOther.mTransitions) @@ -1073,17 +1065,6 @@ NS_IMETHODIMP nsNavHistoryQuery::GetHasDomain(bool* _retval) return NS_OK; } -NS_IMETHODIMP nsNavHistoryQuery::GetUriIsPrefix(bool* aIsPrefix) -{ - *aIsPrefix = mUriIsPrefix; - return NS_OK; -} -NS_IMETHODIMP nsNavHistoryQuery::SetUriIsPrefix(bool aIsPrefix) -{ - mUriIsPrefix = aIsPrefix; - return NS_OK; -} - NS_IMETHODIMP nsNavHistoryQuery::GetUri(nsIURI** aUri) { NS_IF_ADDREF(*aUri = mUri); diff --git a/toolkit/components/places/nsNavHistoryQuery.h b/toolkit/components/places/nsNavHistoryQuery.h index 581ce242b1f..d1a8b759a14 100644 --- a/toolkit/components/places/nsNavHistoryQuery.h +++ b/toolkit/components/places/nsNavHistoryQuery.h @@ -42,7 +42,6 @@ public: bool OnlyBookmarked() { return mOnlyBookmarked; } bool DomainIsHost() { return mDomainIsHost; } const nsCString& Domain() { return mDomain; } - bool UriIsPrefix() { return mUriIsPrefix; } nsIURI* Uri() { return mUri; } // NOT AddRef-ed! bool AnnotationIsNot() { return mAnnotationIsNot; } const nsCString& Annotation() { return mAnnotation; } @@ -82,7 +81,6 @@ protected: bool mOnlyBookmarked; bool mDomainIsHost; nsCString mDomain; // Default is IsVoid, empty string is valid query - bool mUriIsPrefix; nsCOMPtr mUri; bool mAnnotationIsNot; nsCString mAnnotation; diff --git a/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js b/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js index ce71e5b584b..fcb1885e217 100644 --- a/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js +++ b/toolkit/components/places/tests/queries/test_abstime-annotation-uri.js @@ -53,24 +53,6 @@ var testData = [ uri: "http://foo.com/", annoName: goodAnnoName, annoVal: val, lastVisit: jan14_2130, title: "moz"}, - // Test begin edge of time - {isInQuery: true, isVisit: true, isDetails: true, title: "moz mozilla", - uri: "http://foo.com/begin.html", lastVisit: beginTime}, - - // Test end edge of time - {isInQuery: true, isVisit: true, isDetails: true, title: "moz mozilla", - uri: "http://foo.com/end.html", lastVisit: endTime}, - - // Test uri included with isRedirect=true, different transtype - {isInQuery: true, isVisit: true, isDetails: true, title: "moz", - isRedirect: true, uri: "http://foo.com/redirect", lastVisit: jan11_800, - transType: PlacesUtils.history.TRANSITION_LINK}, - - // Test leading time edge with tag string is included - {isInQuery: true, isVisit: true, isDetails: true, title: "taggariffic", - uri: "http://foo.com/tagging/test.html", lastVisit: beginTime, isTag: true, - tagArray: ["moz"] }, - // Begin the invalid queries: // Test www. style URI is not included, with an annotation {isInQuery: false, isVisit: true, isDetails: true, isPageAnnotation: true, @@ -136,7 +118,6 @@ add_task(function* test_abstime_annotation_uri() query.endTimeReference = PlacesUtils.history.TIME_RELATIVE_EPOCH; query.searchTerms = "moz"; query.uri = uri("http://foo.com"); - query.uriIsPrefix = true; query.annotation = "text/foo"; query.annotationIsNot = true; @@ -156,53 +137,26 @@ add_task(function* test_abstime_annotation_uri() // Ensure the result set is correct compareArrayToResult(testData, root); - // Make some changes to the result set - // Let's add something first - var addItem = [{isInQuery: true, isVisit: true, isDetails: true, title: "moz", - uri: "http://foo.com/i-am-added.html", lastVisit: jan11_800}]; - yield task_populateDB(addItem); - do_print("Adding item foo.com/i-am-added.html"); - do_check_eq(isInResult(addItem, root), true); - - // Let's update something by title - var change1 = [{isDetails: true, uri: "http://foo.com/changeme1", - lastVisit: jan12_1730, title: "moz moz mozzie"}]; + // live update. + do_print("change title"); + var change1 = [{isDetails: true, uri:"http://foo.com/", + title: "mo"},]; yield task_populateDB(change1); - do_print("LiveUpdate by changing title"); - do_check_eq(isInResult(change1, root), true); + do_check_false(isInResult({uri: "http://foo.com/"}, root)); - // Let's update something by annotation - // Updating a page by removing an annotation does not cause it to join this - // query set. I tend to think that it should cause that page to join this - // query set, because this visit fits all theother specified criteria once the - // annotation is removed. Uncommenting this will fail the test. - // This is bug 424050 - appears to happen for both domain and URI queries - /*var change2 = [{isPageAnnotation: true, uri: "http://foo.com/badannotaion.html", - annoName: "text/mozilla", annoVal: "test"}]; + var change2 = [{isDetails: true, uri:"http://foo.com/", + title: "moz", lastvisit: endTime},]; yield task_populateDB(change2); - do_print("LiveUpdate by removing annotation"); - do_check_eq(isInResult(change2, root), true);*/ + dump_table("moz_places"); + do_check_false(isInResult({uri: "http://foo.com/"}, root)); - // Let's update by adding a visit in the time range for an existing URI - var change3 = [{isDetails: true, uri: "http://foo.com/changeme3.htm", - title: "moz", lastVisit: jan15_2045}]; + // Let's delete something from the result set - using annotation + var change3 = [{isPageAnnotation: true, + uri: "http://foo.com/", + annoName: badAnnoName, annoVal: "test"}]; yield task_populateDB(change3); - do_print("LiveUpdate by adding visit within timerange"); - do_check_eq(isInResult(change3, root), true); - - // And delete something from the result set - using annotation - // Once more, bug 424050 - /*var change4 = [{isPageAnnotation: true, uri: "http://foo.com/", - annoVal: "test", annoName: badAnnoName}]; - yield task_populateDB(change4); - do_print("LiveUpdate by deleting item from set by adding annotation"); - do_check_eq(isInResult(change4, root), false);*/ - - // Delete something by changing the title - var change5 = [{isDetails: true, uri: "http://foo.com/end.html", title: "deleted"}]; - yield task_populateDB(change5); - do_print("LiveUpdate by deleting item by changing title"); - do_check_eq(isInResult(change5, root), false); + do_print("LiveUpdate by removing annotation"); + do_check_false(isInResult({uri: "http://foo.com/"}, root)); root.containerOpen = false; }); diff --git a/toolkit/components/places/tests/queries/test_querySerialization.js b/toolkit/components/places/tests/queries/test_querySerialization.js index a728a29bd75..40196c163c8 100644 --- a/toolkit/components/places/tests/queries/test_querySerialization.js +++ b/toolkit/components/places/tests/queries/test_querySerialization.js @@ -165,18 +165,13 @@ const querySwitches = [ // hasUri { flag: "hasUri", - subswitches: ["uri", "uriIsPrefix"], + subswitches: ["uri"], desc: "nsINavHistoryQuery.hasUri", matches: flagSwitchMatches, runs: [ function (aQuery, aQueryOptions) { aQuery.uri = uri("http://mozilla.com"); - aQuery.uriIsPrefix = false; }, - function (aQuery, aQueryOptions) { - aQuery.uri = uri("http://mozilla.com"); - aQuery.uriIsPrefix = true; - } ] }, // hasAnnotation diff --git a/toolkit/components/places/tests/queries/test_searchterms-uri.js b/toolkit/components/places/tests/queries/test_searchterms-uri.js index 11f9f4ae632..8740306b63a 100644 --- a/toolkit/components/places/tests/queries/test_searchterms-uri.js +++ b/toolkit/components/places/tests/queries/test_searchterms-uri.js @@ -13,16 +13,6 @@ uri: "http://foo.com/", annoName: "moz/test", annoVal: "val", lastVisit: lastweek, title: "you know, moz is cool"}, - // Test subdomain included with isRedirect=true, different transtype - {isInQuery: true, isVisit: true, isDetails: true, title: "amozzie", - isRedirect: true, uri: "http://foo.com/redirect", lastVisit: old, - referrer: "http://myreferrer.com", transType: PlacesUtils.history.TRANSITION_LINK}, - - // Test www. style URI is included, with a tag - {isInQuery: true, isVisit: true, isDetails: true, isTag: true, - uri: "http://foo.com/yiihah", tagArray: ["moz"], lastVisit: yesterday, - title: "foo"}, - // Test https protocol {isInQuery: false, isVisit: true, isDetails: true, title: "moz", uri: "https://foo.com/", lastVisit: today}, @@ -62,7 +52,6 @@ add_task(function* test_searchterms_uri() var query = PlacesUtils.history.getNewQuery(); query.searchTerms = "moz"; query.uri = uri("http://foo.com"); - query.uriIsPrefix = true; // Options var options = PlacesUtils.history.getNewQueryOptions(); @@ -82,41 +71,17 @@ add_task(function* test_searchterms_uri() // Check our inital result set compareArrayToResult(testData, root); - // If that passes, check liveupdate - // Add to the query set - do_print("Adding item to query"); - var change1 = [{isVisit: true, isDetails: true, uri: "http://foo.com/added.htm", - title: "moz", transType: PlacesUtils.history.TRANSITION_LINK}]; + // live update. + do_print("change title"); + var change1 = [{isDetails: true, uri:"http://foo.com/", + title: "mo"},]; yield task_populateDB(change1); - do_check_true(isInResult(change1, root)); - // Update an existing URI - do_print("Updating Item"); - var change2 = [{isDetails: true, uri: "http://foo.com/changeme1.htm", - title: "moz" }]; + do_check_false(isInResult({uri: "http://foo.com/"}, root)); + var change2 = [{isDetails: true, uri:"http://foo.com/", + title: "moz"},]; yield task_populateDB(change2); - do_check_true(isInResult(change2, root)); - - // Add one and take one out of query set, and simply change one so that it - // still applies to the query. - do_print("Updating More Items"); - var change3 = [{isDetails: true, uri:"http://foo.com/changeme2.htm", - title: "moz"}, - {isDetails: true, uri: "http://foo.com/yiihah", - title: "moz now updated"}, - {isDetails: true, uri: "http://foo.com/redirect", - title: "gone"}]; - yield task_populateDB(change3); - do_check_true(isInResult({uri: "http://foo.com/changeme2.htm"}, root)); - do_check_true(isInResult({uri: "http://foo.com/yiihah"}, root)); - do_check_false(isInResult({uri: "http://foo.com/redirect"}, root)); - - // And now, delete one - do_print("Deleting items"); - var change4 = [{isDetails: true, uri: "http://foo.com/", - title: "mo,z"}]; - yield task_populateDB(change4); - do_check_false(isInResult(change4, root)); + do_check_true(isInResult({uri: "http://foo.com/"}, root)); root.containerOpen = false; }); From bbbe0d375577b5fd56d881d01f5ecd01afa3cefd Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 16 Feb 2016 09:15:06 -0600 Subject: [PATCH 02/45] Backout f5bd25c789b6 "Bug 1245121 - Enable JSON Viewer on RELEASE_BUILD" for Talos regressions. --- devtools/client/preferences/devtools.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js index bd7cb53b671..269dbe9979a 100644 --- a/devtools/client/preferences/devtools.js +++ b/devtools/client/preferences/devtools.js @@ -332,12 +332,11 @@ pref("devtools.fontinspector.enabled", true); // version for each user. pref("devtools.telemetry.tools.opened.version", "{}"); -// Enable the JSON View tool (an inspector for application/json documents) on -// Nightly and Dev. Edition. -#ifdef RELEASE_BUILD -pref("devtools.jsonview.enabled", false); +// Enable the JSON View tool (an inspector for application/json documents) +#ifdef MOZ_DEV_EDITION + pref("devtools.jsonview.enabled", true); #else -pref("devtools.jsonview.enabled", true); + pref("devtools.jsonview.enabled", false); #endif // Disable the HTML responsive design tool by default. Currently disabled until From c6d6db4cf7b04e1cf56972f59dead920c23392e5 Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Tue, 16 Feb 2016 07:23:56 -0800 Subject: [PATCH 03/45] Bug 1246692 - Test that the browser toolbox has a working console. r=jryans --- devtools/client/framework/ToolboxProcess.jsm | 2 + devtools/client/framework/test/browser.ini | 1 + .../framework/test/browser_browser_toolbox.js | 63 +++++++++++++++++++ .../framework/toolbox-process-window.js | 23 ++++++- 4 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 devtools/client/framework/test/browser_browser_toolbox.js diff --git a/devtools/client/framework/ToolboxProcess.jsm b/devtools/client/framework/ToolboxProcess.jsm index 1cfed182e6b..c2f2eebc746 100644 --- a/devtools/client/framework/ToolboxProcess.jsm +++ b/devtools/client/framework/ToolboxProcess.jsm @@ -198,6 +198,8 @@ BrowserToolboxProcess.prototype = { if (this._options.addonID) { xulURI += "?addonID=" + this._options.addonID; + } else if (this._options.testScript) { + xulURI += "?testScript=" + encodeURIComponent(this._options.testScript); } dumpn("Running chrome debugging process."); diff --git a/devtools/client/framework/test/browser.ini b/devtools/client/framework/test/browser.ini index f5c99fd23ea..bdcb18dd3e4 100644 --- a/devtools/client/framework/test/browser.ini +++ b/devtools/client/framework/test/browser.ini @@ -17,6 +17,7 @@ support-files = browser_toolbox_options_enable_serviceworkers_testing.html serviceworker.js +[browser_browser_toolbox.js] [browser_devtools_api.js] [browser_devtools_api_destroy.js] [browser_dynamic_tool_enabling.js] diff --git a/devtools/client/framework/test/browser_browser_toolbox.js b/devtools/client/framework/test/browser_browser_toolbox.js new file mode 100644 index 00000000000..05b5865a51c --- /dev/null +++ b/devtools/client/framework/test/browser_browser_toolbox.js @@ -0,0 +1,63 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// On debug test slave, it takes about 50s to run the test. +requestLongerTimeout(4); + +add_task(function* runTest() { + yield new Promise(done => { + let options = {"set": [ + ["devtools.debugger.prompt-connection", false], + ["devtools.debugger.remote-enabled", true], + ["devtools.chrome.enabled", true], + // Test-only pref to allow passing `testScript` argument to the browser + // toolbox + ["devtools.browser-toolbox.allow-unsafe-script", true], + // On debug test slave, it takes more than the default time (20s) + // to get a initialized console + ["devtools.debugger.remote-timeout", 120000] + ]}; + SpecialPowers.pushPrefEnv(options, done); + }); + + // Wait for a notification sent by a script evaluated in the webconsole + // of the browser toolbox. + let onCustomMessage = new Promise(done => { + Services.obs.addObserver(function listener() { + Services.obs.removeObserver(listener, "browser-toolbox-console-works"); + done(); + }, "browser-toolbox-console-works", false); + }); + + let { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {}); + let closePromise; + yield new Promise(onRun => { + let options = { + // Pass a test script evaluated in the browser toolbox window + // living in a distinct process. It has access to `toolbox` object + // in its global scope. + testScript: "new " + function () { + toolbox.selectTool("webconsole") + .then(() => toolbox.getPanel("webconsole")) + .then(() => { + let { jsterm } = toolbox.getPanel("webconsole").hud; + let js = "Services.obs.notifyObservers(null, 'browser-toolbox-console-works', null);"; + return jsterm.execute(js); + }) + .then(() => toolbox.destroy()); + } + }; + closePromise = new Promise(onClose => { + info("Opening the browser toolbox\n"); + BrowserToolboxProcess.init(onClose, onRun, options); + }); + }); + ok(true, "Browser toolbox started\n"); + + yield onCustomMessage; + ok(true, "Received the custom message"); + + yield closePromise; + ok(true, "Browser toolbox process just closed"); +}); diff --git a/devtools/client/framework/toolbox-process-window.js b/devtools/client/framework/toolbox-process-window.js index a24e4265370..b0cecbc560d 100644 --- a/devtools/client/framework/toolbox-process-window.js +++ b/devtools/client/framework/toolbox-process-window.js @@ -111,9 +111,26 @@ function openToolbox({ form, chrome, isTabActor }) { } function onNewToolbox(toolbox) { - gToolbox = toolbox; - bindToolboxHandlers(); - raise(); + gToolbox = toolbox; + bindToolboxHandlers(); + raise(); + let testScript = getParameterByName("testScript"); + if (testScript) { + // Only allow executing random chrome scripts when a special + // test-only pref is set + let prefName = "devtools.browser-toolbox.allow-unsafe-script"; + if (Services.prefs.getPrefType(prefName) == Services.prefs.PREF_BOOL && + Services.prefs.getBoolPref(prefName) === true) { + evaluateTestScript(testScript, toolbox); + } + } +} + +function evaluateTestScript(script, toolbox) { + let sandbox = Cu.Sandbox(window); + sandbox.window = window; + sandbox.toolbox = toolbox; + Cu.evalInSandbox(script, sandbox); } function bindToolboxHandlers() { From f713d7edade169d05b19fd5e415e69d3c475da5e Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Tue, 16 Feb 2016 07:23:56 -0800 Subject: [PATCH 04/45] Bug 1247270 - Ensure reloading every devtools-related documents when hitting the reload shortcut. r=jryans --- devtools/bootstrap.js | 56 +++++++++++++++++++++++++++++++++++--- devtools/shared/Loader.jsm | 20 +------------- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/devtools/bootstrap.js b/devtools/bootstrap.js index d9b1f423355..b9ab944c58b 100644 --- a/devtools/bootstrap.js +++ b/devtools/bootstrap.js @@ -79,11 +79,59 @@ function reload(event) { // Ask the loader to update itself and reopen the toolbox if needed const {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {}); - devtools.reload(reloadToolbox); + devtools.reload(); - // Also tells gDevTools to reload its dependencies - const {gDevTools} = devtools.require("devtools/client/framework/devtools"); - gDevTools.reload(); + // Go over all top level windows to reload all devtools related things + let windowsEnum = Services.wm.getEnumerator(null); + while (windowsEnum.hasMoreElements()) { + let window = windowsEnum.getNext(); + let windowtype = window.document.documentElement.getAttribute("windowtype"); + if (windowtype == "navigator:browser" && window.gBrowser) { + // Enumerate tabs on firefox windows + for (let tab of window.gBrowser.tabs) { + let browser = tab.linkedBrowser; + let location = browser.documentURI.spec; + let mm = browser.messageManager; + // To reload JSON-View tabs and any devtools document + if (location.startsWith("about:debugging") || + location.startsWith("chrome://devtools/")) { + browser.reload(); + } + // We have to use a frame script to query "baseURI" + mm.loadFrameScript("data:text/javascript,new " + function () { + let isJSONView = content.document.baseURI.startsWith("resource://devtools/"); + if (isJSONView) { + content.location.reload(); + } + }, false); + } + } else if (windowtype === "devtools:webide") { + window.location.reload(); + } else if (windowtype === "devtools:webconsole") { + // Browser console document can't just be reloaded. + // HUDService is going to close it on unload. + // Instead we have to manually toggle it. + let HUDService = devtools.require("devtools/client/webconsole/hudservice"); + HUDService.toggleBrowserConsole() + .then(() => { + HUDService.toggleBrowserConsole(); + }); + } + } + + if (reloadToolbox) { + // Reopen the toolbox automatically if we are reloading from toolbox shortcut + // and are on a browser window. + // Wait for a second before opening the toolbox to avoid races + // between the old and the new one. + let {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); + setTimeout(() => { + let { TargetFactory } = devtools.require("devtools/client/framework/target"); + let { gDevTools } = devtools.require("devtools/client/framework/devtools"); + let target = TargetFactory.forTab(top.gBrowser.selectedTab); + gDevTools.showToolbox(target); + }, 1000); + } } let listener; diff --git a/devtools/shared/Loader.jsm b/devtools/shared/Loader.jsm index cc08f17d4cc..c36407041e9 100644 --- a/devtools/shared/Loader.jsm +++ b/devtools/shared/Loader.jsm @@ -422,7 +422,7 @@ DevToolsLoader.prototype = { /** * Reload the current provider. */ - reload: function(showToolbox) { + reload: function() { var events = this.require("sdk/system/events"); events.emit("startupcache-invalidate", {}); events.emit("devtools-unloaded", {}); @@ -432,24 +432,6 @@ DevToolsLoader.prototype = { delete this._mainid; this._chooseProvider(); this.main("devtools/client/main"); - - let window = Services.wm.getMostRecentWindow(null); - let location = window.location.href; - if (location.includes("/browser.xul") && showToolbox) { - // Reopen the toolbox automatically if we are reloading from toolbox shortcut - // and are on a browser window. - // Wait for a second before opening the toolbox to avoid races - // between the old and the new one. - let {setTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); - setTimeout(() => { - let { gBrowser } = window; - let target = this.TargetFactory.forTab(gBrowser.selectedTab); - const { gDevTools } = require("devtools/client/framework/devtools"); - gDevTools.showToolbox(target); - }, 1000); - } else if (location.includes("/webide.xul")) { - window.location.reload(); - } }, /** From f778c4e38cf5d13f51d0c8c2fa1af1e283f43d48 Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Tue, 16 Feb 2016 07:23:56 -0800 Subject: [PATCH 05/45] Bug 1241050 - Convert DeveloperToolbar.jsm to commonjs module. r=jwalker --- browser/base/content/browser.js | 6 +++--- devtools/client/framework/toolbox.js | 4 ++-- devtools/client/inspector/inspector-panel.js | 2 +- .../{DeveloperToolbar.jsm => developer-toolbar.js} | 10 ++++------ devtools/client/shared/moz.build | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) rename devtools/client/shared/{DeveloperToolbar.jsm => developer-toolbar.js} (99%) diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index d41f4d8be7d..e394a0f3c4f 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -157,9 +157,9 @@ XPCOMUtils.defineLazyGetter(this, "PopupNotifications", function () { }); XPCOMUtils.defineLazyGetter(this, "DeveloperToolbar", function() { - let tmp = {}; - Cu.import("resource://devtools/client/shared/DeveloperToolbar.jsm", tmp); - return new tmp.DeveloperToolbar(window, document.getElementById("developer-toolbar")); + let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); + let { DeveloperToolbar } = require("devtools/client/shared/developer-toolbar"); + return new DeveloperToolbar(window, document.getElementById("developer-toolbar")); }); XPCOMUtils.defineLazyGetter(this, "BrowserToolboxProcess", function() { diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js index 53cbb237233..537f3e484f2 100644 --- a/devtools/client/framework/toolbox.js +++ b/devtools/client/framework/toolbox.js @@ -28,8 +28,6 @@ Cu.import("resource://devtools/client/scratchpad/scratchpad-manager.jsm"); Cu.import("resource://devtools/client/shared/DOMHelpers.jsm"); Cu.import("resource://gre/modules/Task.jsm"); -loader.lazyImporter(this, "CommandUtils", - "resource://devtools/client/shared/DeveloperToolbar.jsm"); loader.lazyGetter(this, "toolboxStrings", () => { const properties = "chrome://devtools/locale/toolbox.properties"; const bundle = Services.strings.createBundle(properties); @@ -45,6 +43,8 @@ loader.lazyGetter(this, "toolboxStrings", () => { } }; }); +loader.lazyRequireGetter(this, "CommandUtils", + "devtools/client/shared/developer-toolbar", true); loader.lazyRequireGetter(this, "getHighlighterUtils", "devtools/client/framework/toolbox-highlighter-utils", true); loader.lazyRequireGetter(this, "Hosts", diff --git a/devtools/client/inspector/inspector-panel.js b/devtools/client/inspector/inspector-panel.js index 40e43c12416..d94edf7bda3 100644 --- a/devtools/client/inspector/inspector-panel.js +++ b/devtools/client/inspector/inspector-panel.js @@ -36,7 +36,7 @@ loader.lazyGetter(this, "clipboardHelper", () => { return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); }); -loader.lazyImporter(this, "CommandUtils", "resource://devtools/client/shared/DeveloperToolbar.jsm"); +loader.lazyRequireGetter(this, "CommandUtils", "devtools/client/shared/developer-toolbar", true); /** * Represents an open instance of the Inspector for a tab. diff --git a/devtools/client/shared/DeveloperToolbar.jsm b/devtools/client/shared/developer-toolbar.js similarity index 99% rename from devtools/client/shared/DeveloperToolbar.jsm rename to devtools/client/shared/developer-toolbar.js index 24f73ed40d6..486319a93bf 100644 --- a/devtools/client/shared/DeveloperToolbar.jsm +++ b/devtools/client/shared/developer-toolbar.js @@ -4,16 +4,13 @@ "use strict"; -this.EXPORTED_SYMBOLS = [ "DeveloperToolbar", "CommandUtils" ]; - const NS_XHTML = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -const { classes: Cc, interfaces: Ci, utils: Cu } = Components; +const { Cc, Ci, Cu } = require("chrome"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); const { TargetFactory } = require("devtools/client/framework/target"); const promise = require("promise"); @@ -233,7 +230,7 @@ var CommandUtils = { }, }; -this.CommandUtils = CommandUtils; +exports.CommandUtils = CommandUtils; /** * Due to a number of panel bugs we need a way to check if we are running on @@ -257,7 +254,7 @@ XPCOMUtils.defineLazyGetter(this, "OS", function() { * @param aChromeWindow The browser window to which this toolbar is attached * @param aToolbarElement See browser.xul: */ -this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement) +function DeveloperToolbar(aChromeWindow, aToolbarElement) { this._chromeWindow = aChromeWindow; @@ -278,6 +275,7 @@ this.DeveloperToolbar = function DeveloperToolbar(aChromeWindow, aToolbarElement EventEmitter.decorate(this); } +exports.DeveloperToolbar = DeveloperToolbar; /** * Inspector notifications dispatched through the nsIObserverService diff --git a/devtools/client/shared/moz.build b/devtools/client/shared/moz.build index 21f2dbbcf86..9679b644bb7 100644 --- a/devtools/client/shared/moz.build +++ b/devtools/client/shared/moz.build @@ -21,7 +21,7 @@ DevToolsModules( 'css-parsing-utils.js', 'Curl.jsm', 'demangle.js', - 'DeveloperToolbar.jsm', + 'developer-toolbar.js', 'devices.js', 'DOMHelpers.jsm', 'doorhanger.js', From a708d7cbf6f70b065c18a16ca75e93f31100fb6a Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Tue, 16 Feb 2016 07:23:57 -0800 Subject: [PATCH 06/45] Bug 1241050 - Cleanup developer-toolbar imports. r=jwalker --- devtools/client/shared/developer-toolbar.js | 63 ++++++--------------- 1 file changed, 17 insertions(+), 46 deletions(-) diff --git a/devtools/client/shared/developer-toolbar.js b/devtools/client/shared/developer-toolbar.js index 486319a93bf..ce5bf37ef26 100644 --- a/devtools/client/shared/developer-toolbar.js +++ b/devtools/client/shared/developer-toolbar.js @@ -4,60 +4,31 @@ "use strict"; +const { Cc, Ci, Cu } = require("chrome"); +const promise = require("promise"); +const Services = require("Services"); +const { TargetFactory } = require("devtools/client/framework/target"); +const Telemetry = require("devtools/client/shared/telemetry"); + const NS_XHTML = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -const { Cc, Ci, Cu } = require("chrome"); - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); - -const { TargetFactory } = require("devtools/client/framework/target"); -const promise = require("promise"); - const Node = Ci.nsIDOMNode; -XPCOMUtils.defineLazyModuleGetter(this, "console", - "resource://gre/modules/Console.jsm"); +loader.lazyImporter(this, "console", "resource://gre/modules/Console.jsm"); +loader.lazyImporter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); +loader.lazyImporter(this, "EventEmitter", "resource://devtools/shared/event-emitter.js"); -XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", - "resource://gre/modules/PluralForm.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", - "resource://devtools/shared/event-emitter.js"); - -XPCOMUtils.defineLazyGetter(this, "prefBranch", function() { - let prefService = Cc["@mozilla.org/preferences-service;1"] - .getService(Ci.nsIPrefService); - return prefService.getBranch(null) +loader.lazyGetter(this, "prefBranch", function() { + return Services.prefs.getBranch(null) .QueryInterface(Ci.nsIPrefBranch2); }); - -XPCOMUtils.defineLazyGetter(this, "toolboxStrings", function () { +loader.lazyGetter(this, "toolboxStrings", function () { return Services.strings.createBundle("chrome://devtools/locale/toolbox.properties"); }); -const Telemetry = require("devtools/client/shared/telemetry"); - -XPCOMUtils.defineLazyGetter(this, "gcliInit", function() { - try { - return require("devtools/shared/gcli/commands/index"); - } - catch (ex) { - console.log(ex); - } -}); - -XPCOMUtils.defineLazyGetter(this, "util", () => { - return require("gcli/util/util"); -}); - -Object.defineProperty(this, "ConsoleServiceListener", { - get: function() { - return require("devtools/shared/webconsole/utils").ConsoleServiceListener; - }, - configurable: true, - enumerable: true -}); +loader.lazyRequireGetter(this, "gcliInit", "devtools/shared/gcli/commands/index"); +loader.lazyRequireGetter(this, "util", "gcli/util/util"); +loader.lazyRequireGetter(this, "ConsoleServiceListener", "devtools/shared/webconsole/utils", true); /** * A collection of utilities to help working with commands @@ -239,11 +210,11 @@ exports.CommandUtils = CommandUtils; * When bug 780102 is fixed all isLinux checks can be removed and we can revert * to using panels. */ -XPCOMUtils.defineLazyGetter(this, "isLinux", function() { +loader.lazyGetter(this, "isLinux", function() { return OS == "Linux"; }); -XPCOMUtils.defineLazyGetter(this, "OS", function() { +loader.lazyGetter(this, "OS", function() { let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS; return os; }); From c79fb62fb4d4466bf2fa3cab3f34c8022e78273f Mon Sep 17 00:00:00 2001 From: Alexandre Poirot Date: Tue, 16 Feb 2016 07:23:57 -0800 Subject: [PATCH 07/45] Bug 1241050 - Ensure reloading the developer toolbar when using the reload addon. r=jwalker --- devtools/bootstrap.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/devtools/bootstrap.js b/devtools/bootstrap.js index b9ab944c58b..7f5e5bbc516 100644 --- a/devtools/bootstrap.js +++ b/devtools/bootstrap.js @@ -105,6 +105,24 @@ function reload(event) { } }, false); } + + // Manually reload gcli if it has been used + // Bug 1248348: Inject the developer toolbar dynamically within browser/ + // so that we can easily remove/reinject it + const desc = Object.getOwnPropertyDescriptor(window, "DeveloperToolbar"); + if (desc && !desc.get) { + let wasVisible = window.DeveloperToolbar.visible; + window.DeveloperToolbar.hide() + .then(() => { + window.DeveloperToolbar.destroy(); + + let { DeveloperToolbar } = devtools.require("devtools/client/shared/developer-toolbar"); + window.DeveloperToolbar = new DeveloperToolbar(window, window.document.getElementById("developer-toolbar")); + if (wasVisible) { + window.DeveloperToolbar.show(); + } + }); + } } else if (windowtype === "devtools:webide") { window.location.reload(); } else if (windowtype === "devtools:webconsole") { From 21c1fbaca8bdef5666cd870888f2e1daa23ce5ca Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Mon, 15 Feb 2016 00:42:56 +0100 Subject: [PATCH 08/45] Bug 1224877 - fix animation_timeline_pause_button intermittent;r=pbro --- .../test/browser_animation_timeline_pause_button.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button.js b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button.js index 51d91043f6c..49d440e0be0 100644 --- a/devtools/client/animationinspector/test/browser_animation_timeline_pause_button.js +++ b/devtools/client/animationinspector/test/browser_animation_timeline_pause_button.js @@ -65,8 +65,10 @@ add_task(function*() { info("Select a finite animation, reload the page and wait for the " + "animation to complete"); yield selectNode(".negative-delay", inspector); + + let onScrubberStopped = waitForScrubberStopped(timeline); yield reloadTab(inspector); - yield waitForScrubberStopped(timeline); + yield onScrubberStopped; ok(btn.classList.contains("paused"), "The button is in paused state once finite animations are done"); From e204838f11b8d226f39d19f473d8f0f53289c5f8 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Fri, 12 Feb 2016 11:03:34 +0100 Subject: [PATCH 09/45] Bug 1243131 - memory tool: select snapshot using ACCEL+{UP/DOWN};r=fitzgen Adds a keydown listener on the memory panel window. Select previous/next snapshot when user presses UP/DOWN with the accelKey modifier (metaKey on OSX, ctrlKey on windows). Keydown events with modifiers are no longer listened to by the tree node elements. Updated tree node test. Added new mochitest to test the new keyboard navigation on the census view. ) --- devtools/client/memory/app.js | 33 +++++++ .../client/memory/test/browser/browser.ini | 1 + .../browser_memory_keyboard-snapshot-list.js | 94 +++++++++++++++++++ devtools/client/memory/test/browser/head.js | 23 +++++ .../test/mochitest/test_tree_06.html | 29 ++++++ devtools/client/shared/components/tree.js | 5 + 6 files changed, 185 insertions(+) create mode 100644 devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js diff --git a/devtools/client/memory/app.js b/devtools/client/memory/app.js index 0825cb9a024..35eb21834b8 100644 --- a/devtools/client/memory/app.js +++ b/devtools/client/memory/app.js @@ -3,6 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const { assert } = require("devtools/shared/DevToolsUtils"); +const { appinfo } = require("Services"); const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react"); const { connect } = require("devtools/client/shared/vendor/react-redux"); const { breakdowns, diffingState, viewState } = require("./constants"); @@ -53,6 +54,17 @@ const MemoryApp = createClass({ return {}; }, + componentDidMount() { + // Attach the keydown listener directly to the window. When an element that + // has the focus (such as a tree node) is removed from the DOM, the focus + // falls back to the body. + window.addEventListener("keydown", this.onKeyDown); + }, + + componentWillUnmount() { + window.removeEventListener("keydown", this.onKeyDown); + }, + childContextTypes: { front: PropTypes.any, heapWorker: PropTypes.any, @@ -67,6 +79,27 @@ const MemoryApp = createClass({ }; }, + onKeyDown(e) { + let { snapshots, dispatch, heapWorker } = this.props; + const selectedSnapshot = snapshots.find(s => s.selected); + const selectedIndex = snapshots.indexOf(selectedSnapshot); + + let isOSX = appinfo.OS == "Darwin"; + let isAccelKey = (isOSX && e.metaKey) || (!isOSX && e.ctrlKey); + // On ACCEL+UP, select previous snapshot. + if (isAccelKey && e.key === "ArrowUp") { + let previousIndex = Math.max(0, selectedIndex - 1); + let previousSnapshotId = snapshots[previousIndex].id; + dispatch(selectSnapshotAndRefresh(heapWorker, previousSnapshotId)); + } + // On ACCEL+DOWN, select next snapshot. + if (isAccelKey && e.key === "ArrowDown") { + let nextIndex = Math.min(snapshots.length - 1, selectedIndex + 1); + let nextSnapshotId = snapshots[nextIndex].id; + dispatch(selectSnapshotAndRefresh(heapWorker, nextSnapshotId)); + } + }, + render() { let { dispatch, diff --git a/devtools/client/memory/test/browser/browser.ini b/devtools/client/memory/test/browser/browser.ini index 29fee425dc4..769c61b3ca9 100644 --- a/devtools/client/memory/test/browser/browser.ini +++ b/devtools/client/memory/test/browser/browser.ini @@ -15,6 +15,7 @@ support-files = [browser_memory_dominator_trees_02.js] [browser_memory_filter_01.js] [browser_memory_keyboard.js] +[browser_memory_keyboard-snapshot-list.js] [browser_memory_no_allocation_stacks.js] [browser_memory_no_auto_expand.js] skip-if = debug # bug 1219554 diff --git a/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js new file mode 100644 index 00000000000..72614c0838e --- /dev/null +++ b/devtools/client/memory/test/browser/browser_memory_keyboard-snapshot-list.js @@ -0,0 +1,94 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that using ACCEL+UP/DOWN, the user can navigate between snapshots. + +"use strict"; + +const { + snapshotState +} = require("devtools/client/memory/constants"); +const { + takeSnapshotAndCensus +} = require("devtools/client/memory/actions/snapshot"); + +const TEST_URL = "http://example.com/browser/devtools/client/memory/test/browser/doc_steady_allocation.html"; + +this.test = makeMemoryTest(TEST_URL, function* ({ panel }) { + // Creating snapshots already takes ~25 seconds on linux 32 debug machines + // which makes the test very likely to go over the allowed timeout + requestLongerTimeout(2); + + const heapWorker = panel.panelWin.gHeapAnalysesClient; + const front = panel.panelWin.gFront; + const store = panel.panelWin.gStore; + const { dispatch } = store; + const doc = panel.panelWin.document; + + info("Take 3 snapshots"); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + dispatch(takeSnapshotAndCensus(front, heapWorker)); + + yield waitUntilState(store, state => + state.snapshots.length == 3 && + state.snapshots.every(s => s.state === snapshotState.SAVED_CENSUS)); + ok(true, "All snapshots are in SAVED_CENSUS state"); + + yield waitUntilSnapshotSelected(store, 2); + ok(true, "Third snapshot selected after creating all snapshots."); + + info("Press ACCEL+UP key, expect second snapshot selected."); + EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin); + yield waitUntilSnapshotSelected(store, 1); + ok(true, "Second snapshot selected after alt+UP."); + + info("Press ACCEL+UP key, expect first snapshot selected."); + EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin); + yield waitUntilSnapshotSelected(store, 0); + ok(true, "First snapshot is selected after ACCEL+UP"); + + info("Check ACCEL+UP is a noop when the first snapshot is selected."); + EventUtils.synthesizeKey("VK_UP", { accelKey: true }, panel.panelWin); + // We assume the snapshot selection should be synchronous here. + is(getSelectedSnapshotIndex(store), 0, "First snapshot is still selected"); + + info("Press ACCEL+DOWN key, expect second snapshot selected."); + EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin); + yield waitUntilSnapshotSelected(store, 1); + ok(true, "Second snapshot is selected after ACCEL+DOWN"); + + info("Click on first node."); + let firstNode = doc.querySelector(".tree .heap-tree-item-name"); + EventUtils.synthesizeMouseAtCenter(firstNode, {}, panel.panelWin); + yield waitUntilState(store, state => state.snapshots[1].census.focused === + state.snapshots[1].census.report.children[0] + ); + ok(true, "First root is selected after click."); + + info("Press DOWN key, expect second root focused."); + EventUtils.synthesizeKey("VK_DOWN", {}, panel.panelWin); + yield waitUntilState(store, state => state.snapshots[1].census.focused === + state.snapshots[1].census.report.children[1] + ); + ok(true, "Second root is selected after pressing DOWN."); + is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected"); + + info("Press UP key, expect second root focused."); + EventUtils.synthesizeKey("VK_UP", {}, panel.panelWin); + yield waitUntilState(store, state => state.snapshots[1].census.focused === + state.snapshots[1].census.report.children[0] + ); + ok(true, "First root is selected after pressing UP."); + is(getSelectedSnapshotIndex(store), 1, "Second snapshot is still selected"); + + info("Press ACCEL+DOWN key, expect third snapshot selected."); + EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin); + yield waitUntilSnapshotSelected(store, 2); + ok(true, "ThirdĖ† snapshot is selected after ACCEL+DOWN"); + + info("Check ACCEL+DOWN is a noop when the last snapshot is selected."); + EventUtils.synthesizeKey("VK_DOWN", { accelKey: true }, panel.panelWin); + // We assume the snapshot selection should be synchronous here. + is(getSelectedSnapshotIndex(store), 2, "Third snapshot is still selected"); +}); diff --git a/devtools/client/memory/test/browser/head.js b/devtools/client/memory/test/browser/head.js index a6f51e8381b..a9d7f445e68 100644 --- a/devtools/client/memory/test/browser/head.js +++ b/devtools/client/memory/test/browser/head.js @@ -142,3 +142,26 @@ function getDisplayedSnapshotStatus(document) { const status = document.querySelector(".snapshot-status"); return status ? status.textContent.trim() : null; } + +/** + * Get the index of the currently selected snapshot. + * + * @return {Number} + */ +function getSelectedSnapshotIndex(store) { + let snapshots = store.getState().snapshots; + let selectedSnapshot = snapshots.find(s => s.selected); + return snapshots.indexOf(selectedSnapshot); +} + +/** + * Returns a promise that will resolve when the snapshot with provided index + * becomes selected. + * + * @return {Promise} + */ +function waitUntilSnapshotSelected(store, snapshotIndex) { + return waitUntilState(store, state => + state.snapshots[snapshotIndex] && + state.snapshots[snapshotIndex].selected === true); +} diff --git a/devtools/client/shared/components/test/mochitest/test_tree_06.html b/devtools/client/shared/components/test/mochitest/test_tree_06.html index 5e4259358fb..8b673e74d99 100644 --- a/devtools/client/shared/components/test/mochitest/test_tree_06.html +++ b/devtools/client/shared/components/test/mochitest/test_tree_06.html @@ -276,6 +276,35 @@ window.onload = Task.async(function* () { "-N:false", "--O:false", ], "After the RIGHT, K should be focused."); + + // Check that keys are ignored if any modifier is present. + let keysWithModifier = [ + { key: "ArrowDown", altKey: true }, + { key: "ArrowDown", ctrlKey: true }, + { key: "ArrowDown", metaKey: true }, + { key: "ArrowDown", shiftKey: true }, + ]; + for (let key of keysWithModifier) { + Simulate.keyDown(document.querySelector(".tree"), key); + yield forceRender(tree); + isRenderedTree(document.body.textContent, [ + "A:false", + "-B:false", + "--E:false", + "---K:true", + "---L:false", + "--F:false", + "--G:false", + "-C:false", + "--H:false", + "--I:false", + "-D:false", + "--J:false", + "M:false", + "-N:false", + "--O:false", + ], "After DOWN + (alt|ctrl|meta|shift), K should remain focused."); + } } catch(e) { ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); } finally { diff --git a/devtools/client/shared/components/tree.js b/devtools/client/shared/components/tree.js index bf441ad3d51..bb6cfb38912 100644 --- a/devtools/client/shared/components/tree.js +++ b/devtools/client/shared/components/tree.js @@ -405,6 +405,11 @@ const Tree = module.exports = createClass({ return; } + // Allow parent nodes to use navigation arrows with modifiers. + if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { + return; + } + // Prevent scrolling when pressing navigation keys. Guard against mocked // events received when testing. if (e.nativeEvent && e.nativeEvent.preventDefault) { From d8f6c7f4a3fcc5942a512e2fe0fd532d53263466 Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Tue, 16 Feb 2016 13:28:40 -0200 Subject: [PATCH 10/45] Bug 1094761 - Fix browser_contextSearchTabPosition.js to not hit the network. r=Florian MozReview-Commit-ID: 9VrFlhmWaXv --- browser/base/content/test/general/browser.ini | 2 -- browser/components/search/test/browser.ini | 2 ++ .../test}/browser_contextSearchTabPosition.js | 17 +++++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) rename browser/{base/content/test/general => components/search/test}/browser_contextSearchTabPosition.js (85%) diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index 392f5020596..d5621869a27 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -290,8 +290,6 @@ skip-if = os == 'win' || e10s # Bug 1159268 - Need a content-process safe versio skip-if = e10s # Bug 1094510 - test hits the network in e10s mode only [browser_clipboard.js] [browser_contentAreaClick.js] -[browser_contextSearchTabPosition.js] -skip-if = os == "mac" || e10s # bug 967013; e10s: bug 1094761 - test hits the network in e10s, causing next test to crash [browser_ctrlTab.js] [browser_datachoices_notification.js] skip-if = !datareporting diff --git a/browser/components/search/test/browser.ini b/browser/components/search/test/browser.ini index 5d5745a2bda..39d8d1953ba 100644 --- a/browser/components/search/test/browser.ini +++ b/browser/components/search/test/browser.ini @@ -21,6 +21,8 @@ support-files = [browser_bing.js] [browser_bing_behavior.js] [browser_contextmenu.js] +[browser_contextSearchTabPosition.js] +skip-if = os == "mac" # bug 967013 [browser_eBay.js] [browser_eBay_behavior.js] [browser_google.js] diff --git a/browser/base/content/test/general/browser_contextSearchTabPosition.js b/browser/components/search/test/browser_contextSearchTabPosition.js similarity index 85% rename from browser/base/content/test/general/browser_contextSearchTabPosition.js rename to browser/components/search/test/browser_contextSearchTabPosition.js index 76646789000..b928a36979b 100644 --- a/browser/base/content/test/general/browser_contextSearchTabPosition.js +++ b/browser/components/search/test/browser_contextSearchTabPosition.js @@ -3,12 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ add_task(function* test() { - - // Will need to be changed if Google isn't the default search engine. - // Note: geoSpecificDefaults are disabled for mochitests, so this is the - // non-US en-US default. - let histogramKey = "google.contextmenu"; + let engine = yield promiseNewEngine("testEngine.xml"); + let histogramKey = "other-" + engine.name + ".contextmenu"; let numSearchesBefore = 0; + try { let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot(); if (histogramKey in hs) { @@ -19,7 +17,7 @@ add_task(function* test() { } let tabs = []; - let tabsLoadedDeferred = Promise.defer(); + let tabsLoadedDeferred = new Deferred(); function tabAdded(event) { let tab = event.target; @@ -54,3 +52,10 @@ add_task(function* test() { Assert.equal(hs[histogramKey].sum, numSearchesBefore + 2, "The histogram must contain the correct search count"); }); + +function Deferred() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); +} From 06a5ff290aaa8f03c71afa01bcfd516f727c884d Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Tue, 16 Feb 2016 13:28:40 -0200 Subject: [PATCH 11/45] Bug 1248494 - Fix browser_bug538331.js for e10s. r=Enn MozReview-Commit-ID: Dw90bxM7cKm --- browser/components/test/browser.ini | 1 - browser/components/test/browser_bug538331.js | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/browser/components/test/browser.ini b/browser/components/test/browser.ini index a6c154438a8..5e7a7ccc0eb 100644 --- a/browser/components/test/browser.ini +++ b/browser/components/test/browser.ini @@ -1,4 +1,3 @@ [DEFAULT] [browser_bug538331.js] -skip-if = e10s # Bug ?????? - child process crash, but only when run as part of the suite (ie, probably not actually this tests fault!?) diff --git a/browser/components/test/browser_bug538331.js b/browser/components/test/browser_bug538331.js index d8abb25045b..fce3790a055 100644 --- a/browser/components/test/browser_bug538331.js +++ b/browser/components/test/browser_bug538331.js @@ -361,11 +361,8 @@ function testShowNotification() if (i == (BG_NOTIFY_TESTS.length - 1)) { // Wait for any windows caught by the windowcatcher to close gWindowCatcher.finish(function () { + BrowserTestUtils.waitForNewTab(gBrowser).then(testNotificationURL); button.click(); - gBrowser.selectedBrowser.addEventListener("load", function () { - gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); - testNotificationURL(); - }, true); }); } else { notifyBox.removeAllNotifications(true); @@ -389,7 +386,7 @@ function testNotificationURL() { ok(true, "Test testNotificationURL: clicking the notification button " + "opened the url specified by the update"); - let href = gBrowser.selectedBrowser.contentWindow.location.href; + let href = gBrowser.currentURI.spec; let expectedURL = BG_NOTIFY_TESTS[BG_NOTIFY_TESTS.length - 1].notificationURL; is(href, expectedURL, "The url opened from the notification should be the " + "url provided by the update"); From a6c4043332ff2ddad682e76cacfb9b3ca8e12aad Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Tue, 16 Feb 2016 13:28:40 -0200 Subject: [PATCH 12/45] Bug 1150147 - Re-enable browser_SocialProvider.js for e10s, as it appears to be working fine. rs=me MozReview-Commit-ID: DYK8RvIBJf6 --- toolkit/components/social/test/browser/browser.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/toolkit/components/social/test/browser/browser.ini b/toolkit/components/social/test/browser/browser.ini index 7b18221e7a9..2d36a995fd1 100644 --- a/toolkit/components/social/test/browser/browser.ini +++ b/toolkit/components/social/test/browser/browser.ini @@ -16,7 +16,6 @@ support-files = [browser_workerAPI.js] [browser_SocialProvider.js] -skip-if = e10s && debug # Leaking docshells (bug 1150147) [browser_notifications.js] # These tests are currently unreliable on ASAN builds with remote frameworkers. From 14a3cd3a674add880f10e4d6894d76d62cfec6c8 Mon Sep 17 00:00:00 2001 From: Felipe Gomes Date: Tue, 16 Feb 2016 13:28:40 -0200 Subject: [PATCH 13/45] Bug 1212647 - Re-enable browser_search_favicon.js for e10s, as it appears to be working fine. rs=me MozReview-Commit-ID: EI2CizDWK4m --- browser/base/content/test/general/browser.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index d5621869a27..3185c7d9fe3 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -136,7 +136,6 @@ skip-if = e10s # Bug 1093153 - no about:home support yet [browser_action_searchengine_alias.js] [browser_addKeywordSearch.js] [browser_search_favicon.js] -skip-if = e10s # Bug 1212647 [browser_alltabslistener.js] [browser_audioTabIcon.js] [browser_autocomplete_a11y_label.js] From 47ea363ca9e4deb2d98302d46b07c416948e3505 Mon Sep 17 00:00:00 2001 From: Julian Descottes Date: Fri, 12 Feb 2016 01:43:44 +0100 Subject: [PATCH 14/45] Bug 1243695 - ensure caret is visible in ruleview prop editor;r=miker --- devtools/client/shared/inplace-editor.js | 2 ++ devtools/client/themes/rules.css | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/devtools/client/shared/inplace-editor.js b/devtools/client/shared/inplace-editor.js index c2a9bfcd553..247c6ab99a0 100644 --- a/devtools/client/shared/inplace-editor.js +++ b/devtools/client/shared/inplace-editor.js @@ -386,6 +386,8 @@ InplaceEditor.prototype = { // If the editor is empty use a width corresponding to 1 character. this.input.style.width = "1ch"; } else { + // Add 2 pixels to ensure the caret will be visible + width = width + 2; this.input.style.width = width + "px"; } }, diff --git a/devtools/client/themes/rules.css b/devtools/client/themes/rules.css index c9bd4a77084..5ece1bc11ce 100644 --- a/devtools/client/themes/rules.css +++ b/devtools/client/themes/rules.css @@ -319,7 +319,7 @@ .styleinspector-propertyeditor { border: 1px solid #CCC; padding: 0; - margin: -1px; + margin: -1px -3px -1px -1px; } .ruleview-property { From 8620edbdc83fbac60249a8a9df348e24898bf3a1 Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 11 Feb 2016 14:32:00 +0100 Subject: [PATCH 15/45] Bug 1247658 - Expose a method to JS for find the shortest retaining paths of some nodes in a heap snapshot; r=bz r=jimb This commit adds the `computeShortestPaths` method to the `HeapSnapshot` webidl interface. It implements this new method on the `mozilla::devtools::HeapSnapshot` class. --- devtools/shared/heapsnapshot/HeapSnapshot.cpp | 140 +++++++++++++++++- devtools/shared/heapsnapshot/HeapSnapshot.h | 6 + ...st_HeapSnapshot_computeShortestPaths_01.js | 69 +++++++++ ...st_HeapSnapshot_computeShortestPaths_02.js | 47 ++++++ .../heapsnapshot/tests/unit/xpcshell.ini | 2 + dom/webidl/HeapSnapshot.webidl | 27 ++++ 6 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js create mode 100644 devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js diff --git a/devtools/shared/heapsnapshot/HeapSnapshot.cpp b/devtools/shared/heapsnapshot/HeapSnapshot.cpp index b7167dcfe0d..cdfc5cf66da 100644 --- a/devtools/shared/heapsnapshot/HeapSnapshot.cpp +++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp @@ -14,6 +14,7 @@ #include "js/UbiNodeBreadthFirst.h" #include "js/UbiNodeCensus.h" #include "js/UbiNodeDominatorTree.h" +#include "js/UbiNodeShortestPaths.h" #include "mozilla/Attributes.h" #include "mozilla/CycleCollectedJSRuntime.h" #include "mozilla/devtools/AutoMemMap.h" @@ -54,6 +55,7 @@ using ::google::protobuf::io::GzipInputStream; using ::google::protobuf::io::ZeroCopyInputStream; using JS::ubi::AtomOrTwoByteChars; +using JS::ubi::ShortestPaths; MallocSizeOf GetCurrentThreadDebuggerMallocSizeOf() @@ -572,7 +574,7 @@ HeapSnapshot::ComputeDominatorTree(ErrorResult& rv) maybeTree = JS::ubi::DominatorTree::Create(rt, nogc, getRoot()); } - if (maybeTree.isNothing()) { + if (NS_WARN_IF(maybeTree.isNothing())) { rv.Throw(NS_ERROR_OUT_OF_MEMORY); return nullptr; } @@ -580,6 +582,142 @@ HeapSnapshot::ComputeDominatorTree(ErrorResult& rv) return MakeAndAddRef(Move(*maybeTree), this, mParent); } +void +HeapSnapshot::ComputeShortestPaths(JSContext*cx, uint64_t start, + const Sequence& targets, + uint64_t maxNumPaths, + JS::MutableHandleObject results, + ErrorResult& rv) +{ + // First ensure that our inputs are valid. + + if (NS_WARN_IF(maxNumPaths == 0)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + Maybe startNode = getNodeById(start); + if (NS_WARN_IF(startNode.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (NS_WARN_IF(targets.Length() == 0)) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + // Aggregate the targets into a set and make sure that they exist in the heap + // snapshot. + + JS::ubi::NodeSet targetsSet; + if (NS_WARN_IF(!targetsSet.init())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (const auto& target : targets) { + Maybe targetNode = getNodeById(target); + if (NS_WARN_IF(targetNode.isNothing())) { + rv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (NS_WARN_IF(!targetsSet.put(*targetNode))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + // Walk the heap graph and find the shortest paths. + + Maybe maybeShortestPaths; + { + auto ccrt = CycleCollectedJSRuntime::Get(); + MOZ_ASSERT(ccrt); + auto rt = ccrt->Runtime(); + MOZ_ASSERT(rt); + JS::AutoCheckCannotGC nogc(rt); + maybeShortestPaths = ShortestPaths::Create(rt, nogc, maxNumPaths, *startNode, + Move(targetsSet)); + } + + if (NS_WARN_IF(maybeShortestPaths.isNothing())) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + auto& shortestPaths = *maybeShortestPaths; + + // Convert the results into a Map object mapping target node IDs to arrays of + // paths found. + + RootedObject resultsMap(cx, JS::NewMapObject(cx)); + if (NS_WARN_IF(!resultsMap)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (auto range = shortestPaths.eachTarget(); !range.empty(); range.popFront()) { + JS::RootedValue key(cx, JS::NumberValue(range.front().identifier())); + JS::AutoValueVector paths(cx); + + bool ok = shortestPaths.forEachPath(range.front(), [&](JS::ubi::Path& path) { + JS::AutoValueVector pathValues(cx); + + for (JS::ubi::BackEdge* edge : path) { + JS::RootedObject pathPart(cx, JS_NewPlainObject(cx)); + if (!pathPart) { + return false; + } + + JS::RootedValue predecessor(cx, NumberValue(edge->predecessor().identifier())); + if (!JS_DefineProperty(cx, pathPart, "predecessor", predecessor, JSPROP_ENUMERATE)) { + return false; + } + + RootedValue edgeNameVal(cx, NullValue()); + if (edge->name()) { + RootedString edgeName(cx, JS_AtomizeUCString(cx, edge->name().get())); + if (!edgeName) { + return false; + } + edgeNameVal = StringValue(edgeName); + } + + if (!JS_DefineProperty(cx, pathPart, "edge", edgeNameVal, JSPROP_ENUMERATE)) { + return false; + } + + if (!pathValues.append(ObjectValue(*pathPart))) { + return false; + } + } + + RootedObject pathObj(cx, JS_NewArrayObject(cx, pathValues)); + return pathObj && paths.append(ObjectValue(*pathObj)); + }); + + if (NS_WARN_IF(!ok)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::RootedObject pathsArray(cx, JS_NewArrayObject(cx, paths)); + if (NS_WARN_IF(!pathsArray)) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + JS::RootedValue pathsVal(cx, ObjectValue(*pathsArray)); + if (NS_WARN_IF(!JS::MapSet(cx, resultsMap, key, pathsVal))) { + rv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + results.set(resultsMap); +} /*** Saving Heap Snapshots ************************************************************************/ diff --git a/devtools/shared/heapsnapshot/HeapSnapshot.h b/devtools/shared/heapsnapshot/HeapSnapshot.h index efc80c25cc8..8e591d9c900 100644 --- a/devtools/shared/heapsnapshot/HeapSnapshot.h +++ b/devtools/shared/heapsnapshot/HeapSnapshot.h @@ -164,6 +164,12 @@ public: already_AddRefed ComputeDominatorTree(ErrorResult& rv); + void ComputeShortestPaths(JSContext*cx, uint64_t start, + const dom::Sequence& targets, + uint64_t maxNumPaths, + JS::MutableHandleObject results, + ErrorResult& rv); + dom::Nullable GetCreationTime() { static const uint64_t maxTime = uint64_t(1) << 53; if (timestamp.isSome() && timestamp.ref() <= maxTime) { diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js new file mode 100644 index 00000000000..2ec577bd00e --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_01.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Sanity test that we can compute shortest paths. +// +// Because the actual heap graph is too unpredictable and likely to drastically +// change as various implementation bits change, we don't test exact paths +// here. See js/src/jsapi-tests/testUbiNode.cpp for such tests, where we can +// control the specific graph shape and structure and so testing exact paths is +// reliable. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + const dominatedByRoot = dominatorTree.getImmediatelyDominated(dominatorTree.root) + .slice(0, 10); + ok(dominatedByRoot); + ok(dominatedByRoot.length); + + const targetSet = new Set(dominatedByRoot); + + const shortestPaths = snapshot.computeShortestPaths(dominatorTree.root, dominatedByRoot, 2); + ok(shortestPaths); + ok(shortestPaths instanceof Map); + ok(shortestPaths.size === targetSet.size); + + for (let [target, paths] of shortestPaths) { + ok(targetSet.has(target), + "We should only get paths for our targets"); + targetSet.delete(target); + + ok(paths.length > 0, + "We must have at least one path, since the target is dominated by the root"); + ok(paths.length <= 2, + "Should not have recorded more paths than the max requested"); + + dumpn("---------------------"); + dumpn("Shortest paths for 0x" + target.toString(16) + ":"); + for (let path of paths) { + dumpn(" path ="); + for (let part of path) { + dumpn(" predecessor: 0x" + part.predecessor.toString(16) + + "; edge: " + part.edge); + } + } + dumpn("---------------------"); + + for (let path of paths) { + ok(path.length > 0, "Cannot have zero length paths"); + ok(path[0].predecessor === dominatorTree.root, + "The first predecessor is always our start node"); + + for (let part of path) { + ok(part.predecessor, "Each part of a path has a predecessor"); + ok(!!snapshot.describeNode({ by: "count", count: true, bytes: true}, + part.predecessor), + "The predecessor is in the heap snapshot"); + ok("edge" in part, "Each part has an (potentially null) edge property"); + } + } + } + + ok(targetSet.size === 0, + "We found paths for all of our targets"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js new file mode 100644 index 00000000000..04fe58733b2 --- /dev/null +++ b/devtools/shared/heapsnapshot/tests/unit/test_HeapSnapshot_computeShortestPaths_02.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test computing shortest paths with invalid arguments. + +function run_test() { + const path = ChromeUtils.saveHeapSnapshot({ runtime: true }); + const snapshot = ChromeUtils.readHeapSnapshot(path); + + const dominatorTree = snapshot.computeDominatorTree(); + const target = dominatorTree.getImmediatelyDominated(dominatorTree.root).pop(); + ok(target); + + let threw = false; + try { + snapshot.computeShortestPaths(0, [target], 2); + } catch (_) { + threw = true; + } + ok(threw, "invalid start node should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [0], 2); + } catch (_) { + threw = true; + } + ok(threw, "invalid target nodes should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [], 2); + } catch (_) { + threw = true; + } + ok(threw, "empty target nodes should throw"); + + threw = false; + try { + snapshot.computeShortestPaths(dominatorTree.root, [target], 0); + } catch (_) { + threw = true; + } + ok(threw, "0 max paths should throw"); + + do_test_finished(); +} diff --git a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini index c79371e0f68..1969ce5ec38 100644 --- a/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini +++ b/devtools/shared/heapsnapshot/tests/unit/xpcshell.ini @@ -66,6 +66,8 @@ support-files = [test_HeapSnapshot_creationTime_01.js] [test_HeapSnapshot_deepStack_01.js] [test_HeapSnapshot_describeNode_01.js] +[test_HeapSnapshot_computeShortestPaths_01.js] +[test_HeapSnapshot_computeShortestPaths_02.js] [test_HeapSnapshot_takeCensus_01.js] [test_HeapSnapshot_takeCensus_02.js] [test_HeapSnapshot_takeCensus_03.js] diff --git a/dom/webidl/HeapSnapshot.webidl b/dom/webidl/HeapSnapshot.webidl index bec4503648b..f1418dc2821 100644 --- a/dom/webidl/HeapSnapshot.webidl +++ b/dom/webidl/HeapSnapshot.webidl @@ -75,4 +75,31 @@ interface HeapSnapshot { */ [Throws] DominatorTree computeDominatorTree(); + + /** + * Find the shortest retaining paths from the node associated with the ID + * `start` to each node associated with the IDs in `targets`. Find at most + * `maxNumPaths` retaining paths for each target node. + * + * The return value is a Map object mapping from each target node ID to an + * array of retaining paths. The array may be empty if we did not find any + * retaining paths. + * + * A path is an array of objects of the form: + * + * { + * predecessor: , + * edge: , + * } + * + * The first `predecessor` will always be `start`. The last edge in the path + * leads to the `target` node that is mapped to the path; the `target` does + * not appear as a `predecessor` in the path. + * + * Throws when `start` or any of the elements of `targets` are not an ID of a + * node in the snapshot, or if we encounter an out of memory exception. + */ + [Throws] + object computeShortestPaths(NodeId start, sequence targets, + unsigned long long maxNumPaths); }; From ec4be39d423acb54c2775b2dce1b864b34eaccd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A3o=20Gottwald?= Date: Tue, 16 Feb 2016 12:05:47 +0100 Subject: [PATCH 16/45] Bug 1248266 - Make recently added bookmarks keyboard-accessible by not using a vbox container. r=mak --- browser/base/content/browser-menubar.inc | 2 +- browser/base/content/browser-places.js | 9 +++++---- browser/base/content/browser.xul | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/browser/base/content/browser-menubar.inc b/browser/base/content/browser-menubar.inc index f85031c233f..f6c2c16ae69 100644 --- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -453,8 +453,8 @@ key="bookmarkAllTabsKb"/> - - Date: Tue, 16 Feb 2016 16:34:38 +0100 Subject: [PATCH 17/45] Bug 1241837 - Use proxy for browsers property in tabbrowser instead of explicit array. r=dao --- browser/base/content/tabbrowser.xml | 45 ++++++++++++------- .../components/sessionstore/SessionStore.jsm | 5 +-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index e6c6455c0db..bde9c75e6f9 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1863,8 +1863,7 @@ }, 0, this.tabContainer); } - // invalidate caches - this._browsers = null; + // invalidate cache this._visibleTabs = null; this.tabContainer.appendChild(t); @@ -2379,10 +2378,6 @@ var wasPinned = aTab.pinned; - // Invalidate browsers cache, as the tab is removed from the - // tab container. - this._browsers = null; - // Remove the tab ... this.tabContainer.removeChild(aTab); @@ -2860,15 +2855,32 @@ onget="return this.mCurrentBrowser;" readonly="true"/> - - - tab.linkedBrowser)); - ]]> - - - null + + { + if (typeof name == "string" && Number.isInteger(parseInt(name))) { + return (name in this.tabs); + } + return false; + }, + get: (target, name) => { + if (name == "length") { + return this.tabs.length; + } + if (typeof name == "string" && Number.isInteger(parseInt(name))) { + if (!(name in this.tabs)) { + return undefined; + } + return this.tabs[name].linkedBrowser; + } + return target[name]; + } + }); + ]]> + @@ -2929,8 +2941,7 @@ this.mCurrentTab._logicallySelected = false; this.mCurrentTab._visuallySelected = false; - // invalidate caches - this._browsers = null; + // invalidate cache this._visibleTabs = null; // use .item() instead of [] because dragging to the end of the strip goes out of diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm index 2d619afefe8..5abdfc9b0dc 100644 --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -1236,9 +1236,7 @@ var SessionStoreInternal = { var tabbrowser = aWindow.gBrowser; - // The tabbrowser binding will go away once the window is closed, - // so we'll hold a reference to the browsers in the closure here. - let browsers = tabbrowser.browsers; + let browsers = Array.from(tabbrowser.browsers); TAB_EVENTS.forEach(function(aEvent) { tabbrowser.tabContainer.removeEventListener(aEvent, this, true); @@ -1320,7 +1318,6 @@ var SessionStoreInternal = { // access any DOM elements from aWindow within this callback unless // you're holding on to them in the closure. - // We can still access tabbrowser.browsers, thankfully. for (let browser of browsers) { if (this._closedWindowTabs.has(browser.permanentKey)) { let tabData = this._closedWindowTabs.get(browser.permanentKey); From 7f69186f8e6c8d402a71ac5d88f8ebb263220240 Mon Sep 17 00:00:00 2001 From: Eric Hu Date: Fri, 12 Feb 2016 20:23:25 +0700 Subject: [PATCH 18/45] Bug 920169 - Remove references to C++ constants in Histograms.json. r=gfritzsche --- toolkit/components/telemetry/Histograms.json | 8 ++++---- toolkit/components/telemetry/Telemetry.cpp | 17 +++++++++++++++++ .../components/telemetry/bucket-whitelist.json | 5 ++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 01e74397265..5f7157069f4 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -307,7 +307,7 @@ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", "kind": "enumerated", - "n_values": "JS::gcreason::NUM_TELEMETRY_REASONS", + "n_values": 100, "description": "Reason (enum value) for initiating a GC" }, "GC_IS_COMPARTMENTAL": { @@ -449,14 +449,14 @@ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", "kind": "enumerated", - "n_values": "JS::gcreason::NUM_TELEMETRY_REASONS", + "n_values": 100, "description": "Reason (enum value) for initiating a minor GC" }, "GC_MINOR_REASON_LONG": { "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"], "expires_in_version": "never", "kind": "enumerated", - "n_values": "JS::gcreason::NUM_TELEMETRY_REASONS", + "n_values": 100, "description": "Reason (enum value) that caused a long (>1ms) minor GC" }, "GC_MINOR_US": { @@ -3083,7 +3083,7 @@ "STARTUP_MEASUREMENT_ERRORS": { "expires_in_version": "default", "kind": "enumerated", - "n_values": "mozilla::StartupTimeline::MAX_EVENT_ID", + "n_values": 16, "description": "Flags errors in startup calculation()" }, "NETWORK_DISK_CACHE_OPEN": { diff --git a/toolkit/components/telemetry/Telemetry.cpp b/toolkit/components/telemetry/Telemetry.cpp index b3e575ec172..ac699e22950 100644 --- a/toolkit/components/telemetry/Telemetry.cpp +++ b/toolkit/components/telemetry/Telemetry.cpp @@ -1960,6 +1960,23 @@ mFailedLockCount(0) // histograms) using InitHistogramRecordingEnabled() will happen after instantiating // sTelemetry since it depends on the static GetKeyedHistogramById(...) - which // uses the singleton instance at sTelemetry. + + // Some Telemetry histograms depend on the value of C++ constants and hardcode + // their values in Histograms.json. + // We add static asserts here for those values to match so that future changes + // don't go unnoticed. + // TODO: Compare explicitly with gHistograms[].bucketCount here + // once we can make gHistograms constexpr (requires VS2015). + static_assert((JS::gcreason::NUM_TELEMETRY_REASONS == 100), + "NUM_TELEMETRY_REASONS is assumed to be a fixed value in Histograms.json." + " If this was an intentional change, update this assert with its value " + "and update the n_values for the following in Histograms.json: " + "GC_MINOR_REASON, GC_MINOR_REASON_LONG, GC_REASON_2"); + static_assert((mozilla::StartupTimeline::MAX_EVENT_ID == 16), + "MAX_EVENT_ID is assumed to be a fixed value in Histograms.json. If this" + " was an intentional change, update this assert with its value and update" + " the n_values for the following in Histograms.json:" + " STARTUP_MEASUREMENT_ERRORS"); } TelemetryImpl::~TelemetryImpl() { diff --git a/toolkit/components/telemetry/bucket-whitelist.json b/toolkit/components/telemetry/bucket-whitelist.json index afed5f30b9b..7cc352ec07b 100644 --- a/toolkit/components/telemetry/bucket-whitelist.json +++ b/toolkit/components/telemetry/bucket-whitelist.json @@ -182,5 +182,8 @@ "LOOP_VIDEO_QUALITY_OUTBOUND_RTT", "LOOP_AUDIO_QUALITY_OUTBOUND_RTT", "LOOP_CALL_DURATION", - "GFX_CRASH" + "GFX_CRASH", + "GC_REASON_2", + "GC_MINOR_REASON", + "GC_MINOR_REASON_LONG" ] From 58500c66b3d5bd8d644fe225736a154ce7e6298b Mon Sep 17 00:00:00 2001 From: Giorgos Logiotatidis Date: Mon, 15 Feb 2016 15:28:49 +0200 Subject: [PATCH 19/45] Bug 1248388 - Use snippets cdn. r=margaret --- mobile/android/app/mobile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index df30af46af5..9740bce9a29 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -860,7 +860,7 @@ pref("snav.enabled", true); // This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream // repackager of this code using an alternate snippet url, please keep your users safe -pref("browser.snippets.updateUrl", "https://snippets.mozilla.com/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/"); +pref("browser.snippets.updateUrl", "https://snippets.cdn.mozilla.net/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/"); // How frequently we check for new snippets, in seconds (1 day) pref("browser.snippets.updateInterval", 86400); From a5a294d01cfdde0d4b636b36f0b523401fcb22fc Mon Sep 17 00:00:00 2001 From: Johann Hofmann Date: Tue, 24 Nov 2015 17:40:08 +0100 Subject: [PATCH 20/45] Bug 1225743 - Implement chrome.bookmarks.search. r=mak --- .../components/extensions/ext-bookmarks.js | 4 +- .../extensions/schemas/bookmarks.json | 2 +- .../test/mochitest/test_ext_bookmarks.html | 151 ++++++++++++ toolkit/components/places/Bookmarks.jsm | 104 ++++++++ .../tests/bookmarks/test_bookmarks_search.js | 223 ++++++++++++++++++ .../places/tests/bookmarks/xpcshell.ini | 1 + 6 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 toolkit/components/places/tests/bookmarks/test_bookmarks_search.js diff --git a/browser/components/extensions/ext-bookmarks.js b/browser/components/extensions/ext-bookmarks.js index c1d759dd0b2..04f97d43528 100644 --- a/browser/components/extensions/ext-bookmarks.js +++ b/browser/components/extensions/ext-bookmarks.js @@ -111,7 +111,9 @@ extensions.registerSchemaAPI("bookmarks", "bookmarks", (extension, context) => { return getTree(id, false); }, - // search + search: function(query) { + return Bookmarks.search(query).then(result => result.map(convert)); + }, create: function(bookmark) { let info = { diff --git a/browser/components/extensions/schemas/bookmarks.json b/browser/components/extensions/schemas/bookmarks.json index 09f67c26363..867bb76715c 100644 --- a/browser/components/extensions/schemas/bookmarks.json +++ b/browser/components/extensions/schemas/bookmarks.json @@ -234,7 +234,6 @@ }, { "name": "search", - "unsupported": true, "type": "function", "description": "Searches for BookmarkTreeNodes matching the given query. Queries specified with an object produce BookmarkTreeNodes matching all specified properties.", "async": "callback", @@ -258,6 +257,7 @@ }, "url": { "type": "string", + "format": "url", "optional": true, "description": "The URL of the bookmark; matches verbatim. Note that folders have no URL." }, diff --git a/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html b/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html index 4d4a294d365..15ec4840e5a 100644 --- a/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html @@ -87,6 +87,157 @@ function backgroundScript() { }).then(() => { browser.test.assertEq(5, failures, "Expected failures"); + // test bookmarks.search + }).then(() => { + return Promise.all([ + browser.bookmarks.create({title: "MƘzillƤ", url: "http://mĆøzĆ®llƤ.ƶrg"}), + browser.bookmarks.create({title: "Example", url: "http://example.org"}), + browser.bookmarks.create({title: "Mozilla Folder"}), + browser.bookmarks.create({title: "EFF", url: "http://eff.org"}), + browser.bookmarks.create({title: "Menu Item", url: "http://menu.org", parentId: "menu________"}), + browser.bookmarks.create({title: "Toolbar Item", url: "http://toolbar.org", parentId: "toolbar_____"}), + ]); + }).then(results => { + return Promise.all([ + browser.bookmarks.create({title: "Mozilla", url: "http://allizom.org", parentId: results[1].id}), + browser.bookmarks.create({title: "Mozilla Corporation", url: "http://allizom.com", parentId: results[1].id}), + browser.bookmarks.create({title: "Firefox", url: "http://allizom.org/firefox", parentId: results[1].id}), + ]); + }).then(() => { + // returns all items on empty object + + return browser.bookmarks.search({}); + }).then(results => { + browser.test.assertTrue(results.length >= 9); + + // throws an error for invalid query objects + return browser.bookmarks.search(); + }).catch(error => { + browser.test.assertTrue(error.message.includes("Incorrect argument types for bookmarks.search")); + + return browser.bookmarks.search(null); + }).catch(error => { + browser.test.assertTrue(error.message.includes("Incorrect argument types for bookmarks.search")); + + return browser.bookmarks.search(function() {}); + }).catch(error => { + browser.test.assertTrue(error.message.includes("Incorrect argument types for bookmarks.search")); + + return browser.bookmarks.search({banana: "banana"}); + }).catch(error => { + browser.test.assertTrue(error.message.includes("banana")); + browser.test.assertTrue(error.message.includes("bookmarks.search")); + + return browser.bookmarks.search({url: "spider-man vs. batman"}); + }).catch(error => { + browser.test.assertTrue(error.message.includes("spider-man vs. batman")); + browser.test.assertTrue(error.message.includes("not a valid URL")); + browser.test.assertTrue(error.message.includes("bookmarks.search")); + + // queries the url + return browser.bookmarks.search("example.org"); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("Example", results[0].title); + browser.test.assertEq("http://example.org/", results[0].url); + browser.test.assertEq(2, results[0].index); + + // queries the title + return browser.bookmarks.search("EFF"); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("EFF", results[0].title); + browser.test.assertEq("http://eff.org/", results[0].url); + browser.test.assertEq("unfiled_____", results[0].parentId); + browser.test.assertEq(0, results[0].index); + + // finds menu items + return browser.bookmarks.search("Menu Item"); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("Menu Item", results[0].title); + browser.test.assertEq("http://menu.org/", results[0].url); + browser.test.assertEq("menu________", results[0].parentId); + + // finds toolbar items + return browser.bookmarks.search("Toolbar Item"); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("Toolbar Item", results[0].title); + browser.test.assertEq("http://toolbar.org/", results[0].url); + browser.test.assertEq("toolbar_____", results[0].parentId); + + // finds folders + return browser.bookmarks.search("Mozilla Folder"); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("Mozilla Folder", results[0].title); + + // is case-insensitive + return browser.bookmarks.search("corporation"); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("Mozilla Corporation", results[0].title); + + // is case-insensitive for non-ascii + return browser.bookmarks.search("MĆøZILLƄ"); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("MƘzillƤ", results[0].title); + + // returns multiple results + return browser.bookmarks.search("allizom"); + }).then(results => { + browser.test.assertEq(3, results.length); + browser.test.assertEq("Mozilla", results[0].title); + browser.test.assertEq("Mozilla Corporation", results[1].title); + browser.test.assertEq("Firefox", results[2].title); + + // accepts a url field + return browser.bookmarks.search({url: "http://allizom.com/"}); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("Mozilla Corporation", results[0].title); + browser.test.assertEq("http://allizom.com/", results[0].url); + + // normalizes urls + return browser.bookmarks.search({url: "http://allizom.com"}); + }).then(results => { + browser.test.assertEq(results.length, 1); + browser.test.assertEq("Mozilla Corporation", results[0].title); + browser.test.assertEq("http://allizom.com/", results[0].url); + + // normalizes urls even more + return browser.bookmarks.search({url: "http:allizom.com"}); + }).then(results => { + browser.test.assertEq(results.length, 1); + browser.test.assertEq("Mozilla Corporation", results[0].title); + browser.test.assertEq("http://allizom.com/", results[0].url); + + // accepts a title field + return browser.bookmarks.search({title: "Mozilla"}); + }).then(results => { + browser.test.assertEq(results.length, 1); + browser.test.assertEq("Mozilla", results[0].title); + browser.test.assertEq("http://allizom.org/", results[0].url); + + // can combine title and query + return browser.bookmarks.search({title: "Mozilla", query: "allizom"}); + }).then(results => { + browser.test.assertEq(1, results.length); + browser.test.assertEq("Mozilla", results[0].title); + browser.test.assertEq("http://allizom.org/", results[0].url); + + // uses AND conditions + return browser.bookmarks.search({title: "EFF", query: "allizom"}); + }).then(results => { + browser.test.assertEq(0, results.length); + + // returns an empty array on item not found + return browser.bookmarks.search("microsoft"); + }).then(results => { + browser.test.assertEq(0, results.length); + browser.test.notifyPass("bookmarks"); }).catch(error => { browser.test.fail(`Error: ${String(error)} :: ${error.stack}`); diff --git a/toolkit/components/places/Bookmarks.jsm b/toolkit/components/places/Bookmarks.jsm index 145f463b7f2..93a910ec60b 100644 --- a/toolkit/components/places/Bookmarks.jsm +++ b/toolkit/components/places/Bookmarks.jsm @@ -82,6 +82,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", const DB_URL_LENGTH_MAX = 65536; const DB_TITLE_LENGTH_MAX = 4096; +const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY; +const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK; + var Bookmarks = Object.freeze({ /** * Item's type constants. @@ -446,6 +449,57 @@ var Bookmarks = Object.freeze({ ); }, + /** + * Searches a list of bookmark-items by a search term, url or title. + * + * @param query + * Either a string to use as search term, or an object + * containing any of these keys: query, title or url with the + * corresponding string to match as value. + * The url property can be either a string or an nsIURI. + * + * @return {Promise} resolved when the search is complete. + * @resolves to an array of found bookmark-items. + * @rejects if an error happens while searching. + * @throws if the arguments are invalid. + * + * @note Any unknown property in the query object is ignored. + * Known properties may be overwritten. + */ + search(query) { + if (!query) { + throw new Error("Query object is required"); + } + if (typeof query === "string") { + query = { query: query }; + } + if (typeof query !== "object") { + throw new Error("Query must be an object or a string"); + } + if (query.query && typeof query.query !== "string") { + throw new Error("Query option must be a string"); + } + if (query.title && typeof query.title !== "string") { + throw new Error("Title option must be a string"); + } + + if (query.url) { + if (typeof query.url === "string" || (query.url instanceof URL)) { + query.url = new URL(query.url).href; + } else if (query.url instanceof Ci.nsIURI) { + query.url = query.url.spec; + } else { + throw new Error("Url option must be a string or a URL object"); + } + } + + return Task.spawn(function* () { + let results = yield queryBookmarks(query); + + return results; + }); + }, + /** * Fetches information about a bookmark-item. * @@ -823,6 +877,56 @@ function insertBookmark(item, parent) { })); } +//////////////////////////////////////////////////////////////////////////////// +// Query implementation. + +function queryBookmarks(info) { + let queryParams = {tags_folder: PlacesUtils.tagsFolderId}; + // we're searching for bookmarks, so exclude tags + let queryString = "WHERE p.parent <> :tags_folder"; + + if (info.title) { + queryString += " AND b.title = :title"; + queryParams.title = info.title; + } + + if (info.url) { + queryString += " AND h.url = :url"; + queryParams.url = info.url; + } + + if (info.query) { + queryString += " AND AUTOCOMPLETE_MATCH(:query, h.url, b.title, NULL, NULL, 1, 1, NULL, :matchBehavior, :searchBehavior) "; + queryParams.query = info.query; + queryParams.matchBehavior = MATCH_BOUNDARY; + queryParams.searchBehavior = BEHAVIOR_BOOKMARK; + } + + return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: queryBookmarks", + Task.async(function*(db) { + + // _id, _childCount, _grandParentId and _parentId fields + // are required to be in the result by the converting function + // hence setting them to NULL + let rows = yield db.executeCached( + `SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index', + b.dateAdded, b.lastModified, b.type, b.title, + h.url AS url, b.parent, p.parent, + NULL AS _id, + NULL AS _childCount, + NULL AS _grandParentId, + NULL AS _parentId + FROM moz_bookmarks b + LEFT JOIN moz_bookmarks p ON p.id = b.parent + LEFT JOIN moz_places h ON h.id = b.fk + ${queryString} + `, queryParams); + + return rowsToItemsArray(rows); + })); +} + + //////////////////////////////////////////////////////////////////////////////// // Fetch implementation. diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js new file mode 100644 index 00000000000..59f6eac4a95 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js @@ -0,0 +1,223 @@ +add_task(function* invalid_input_throws() { + Assert.throws(() => PlacesUtils.bookmarks.search(), + /Query object is required/); + Assert.throws(() => PlacesUtils.bookmarks.search(null), + /Query object is required/); + Assert.throws(() => PlacesUtils.bookmarks.search({title: 50}), + /Title option must be a string/); + Assert.throws(() => PlacesUtils.bookmarks.search({url: {url: "wombat"}}), + /Url option must be a string or a URL object/); + Assert.throws(() => PlacesUtils.bookmarks.search(50), + /Query must be an object or a string/); + Assert.throws(() => PlacesUtils.bookmarks.search(true), + /Query must be an object or a string/); +}); + +add_task(function* search_bookmark() { + yield PlacesUtils.bookmarks.eraseEverything(); + + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://menu.org/", + title: "a menu bookmark" }); + let bm4 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://toolbar.org/", + title: "a toolbar bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + checkBookmarkObject(bm4); + + // finds a result by query + let results = yield PlacesUtils.bookmarks.search("example.com"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // finds multiple results + results = yield PlacesUtils.bookmarks.search("example"); + Assert.equal(results.length, 2); + checkBookmarkObject(results[0]); + checkBookmarkObject(results[1]); + + // finds menu bookmarks + results = yield PlacesUtils.bookmarks.search("a menu bookmark"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm3, results[0]); + + // finds toolbar bookmarks + results = yield PlacesUtils.bookmarks.search("a toolbar bookmark"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm4, results[0]); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_by_query_object() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/", + title: "another bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + + let results = yield PlacesUtils.bookmarks.search({query: "example.com"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + + Assert.deepEqual(bm1, results[0]); + + results = yield PlacesUtils.bookmarks.search({query: "example"}); + Assert.equal(results.length, 2); + checkBookmarkObject(results[0]); + checkBookmarkObject(results[1]); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_by_url() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "third bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result by url + let results = yield PlacesUtils.bookmarks.search({url: "http://example.com/"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // normalizes the url + results = yield PlacesUtils.bookmarks.search({url: "http:/example.com"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // returns multiple matches + results = yield PlacesUtils.bookmarks.search({url: "http://example.org/path"}); + Assert.equal(results.length, 2); + + // requires exact match + results = yield PlacesUtils.bookmarks.search({url: "http://example.org/"}); + Assert.equal(results.length, 0); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_by_title() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "another bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result by title + let results = yield PlacesUtils.bookmarks.search({title: "a bookmark"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // returns multiple matches + results = yield PlacesUtils.bookmarks.search({title: "another bookmark"}); + Assert.equal(results.length, 2); + + // requires exact match + results = yield PlacesUtils.bookmarks.search({title: "bookmark"}); + Assert.equal(results.length, 0); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_bookmark_combinations() { + let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark" }); + let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark" }); + let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "third bookmark" }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result if title and url match + let results = yield PlacesUtils.bookmarks.search({url: "http://example.com/", title: "a bookmark"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // does not match if query is not matching but url and title match + results = yield PlacesUtils.bookmarks.search({url: "http://example.com/", title: "a bookmark", query: "nonexistent"}); + Assert.equal(results.length, 0); + + // does not match if one parameter is not matching + results = yield PlacesUtils.bookmarks.search({url: "http://what.ever", title: "a bookmark"}); + Assert.equal(results.length, 0); + + // query only matches if other fields match as well + results = yield PlacesUtils.bookmarks.search({query: "bookmark", url: "http://example.net/"}); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm3, results[0]); + + // non-matching query will also return no results + results = yield PlacesUtils.bookmarks.search({query: "nonexistent", url: "http://example.net/"}); + Assert.equal(results.length, 0); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(function* search_folder() { + let folder = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a test folder" }); + let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: folder.guid, + url: "http://example.com/", + title: "a bookmark" }); + checkBookmarkObject(folder); + checkBookmarkObject(bm); + + // also finds folders + let results = yield PlacesUtils.bookmarks.search("a test folder"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.equal(folder.title, results[0].title); + Assert.equal(folder.type, results[0].type); + Assert.equal(folder.parentGuid, results[0].parentGuid); + + // finds elements in folders + results = yield PlacesUtils.bookmarks.search("example.com"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm, results[0]); + Assert.equal(folder.guid, results[0].parentGuid); + + yield PlacesUtils.bookmarks.eraseEverything(); +}); + diff --git a/toolkit/components/places/tests/bookmarks/xpcshell.ini b/toolkit/components/places/tests/bookmarks/xpcshell.ini index 3a38934ed42..5410686832d 100644 --- a/toolkit/components/places/tests/bookmarks/xpcshell.ini +++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini @@ -37,6 +37,7 @@ skip-if = toolkit == 'android' || toolkit == 'gonk' [test_bookmarks_notifications.js] [test_bookmarks_remove.js] [test_bookmarks_reorder.js] +[test_bookmarks_search.js] [test_bookmarks_update.js] [test_changeBookmarkURI.js] [test_getBookmarkedURIFor.js] From 4142d7a3ea95a29c4c1b580e0f565364ad445f2e Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Tue, 16 Feb 2016 16:43:27 +0000 Subject: [PATCH 21/45] Bug 1233799 - Enable DownloadContentService (and exclude fonts) in Nightly. r=rnewman MozReview-Commit-ID: G7zDhHVt67C --- mobile/android/confvars.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mobile/android/confvars.sh b/mobile/android/confvars.sh index 4156bff85d4..497aa26e92e 100644 --- a/mobile/android/confvars.sh +++ b/mobile/android/confvars.sh @@ -116,3 +116,9 @@ MOZ_ADDON_SIGNING=1 # Note: The framework is always included in the app. This flag controls # usage of the framework. MOZ_SWITCHBOARD=1 + +# Enable DLC background service and stop shipping fonts in Nightly +if test "$NIGHTLY_BUILD"; then + MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE=1 + MOZ_ANDROID_EXCLUDE_FONTS=1 +fi \ No newline at end of file From c3646973230d4e5b4f54d13214cacf1db952e62f Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Tue, 16 Feb 2016 11:11:34 -0800 Subject: [PATCH 22/45] Bug 1247170 - Part 1: Add "android-gradle-build" Docker image. r=dustin,sebastian We can't run Docker-in-Docker, so this is a clone of https://hub.docker.com/r/sonatype/nexus, modified to run additional commands. Sonatype Nexus officially supports the Oracle JDK but the OpenJDK appears to work as well, so we use it. This bakes a build.sh and Gradle-specific dependencies into the docker image. This makes sense in a future where the dependencies this fetches are baked into the image used to build Fennec. It makes less sense right now, when the dependencies are uploaded to tooltool and subsequently consumed. It's not critical that we get this right immediately. The actual build.sh script runs the Gradle wrapper for a special dependency project. The wrapper installs Gradle itself, downloads a large number of Gradle dependencies, and uses Jake Wharton's https://github.com/JakeWharton/sdk-manager-plugin/ to download and install the Android SDK. (We could use |mach bootstrap|, but that doesn't (yet) support non-interactive use, and I want to try to use the SDK Manager more generally.) These outputs are then packaged for further use: right now, they're exposed as artifacts, but eventually we'll upload them directly to tooltool. MozReview-Commit-ID: 7upsk1ANuxN --- .../docker/android-gradle-build/Dockerfile | 57 +++++++ testing/docker/android-gradle-build/README.md | 2 + testing/docker/android-gradle-build/REGISTRY | 1 + testing/docker/android-gradle-build/VERSION | 1 + testing/docker/android-gradle-build/build.sh | 62 +++++++ .../android-gradle-build/project/README.md | 6 + .../android-gradle-build/project/build.gradle | 73 ++++++++ .../project/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53637 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../android-gradle-build/project/gradlew | 160 ++++++++++++++++++ .../android-gradle-build/project/gradlew.bat | 90 ++++++++++ .../project/src/main/AndroidManifest.xml | 6 + 12 files changed, 465 insertions(+) create mode 100644 testing/docker/android-gradle-build/Dockerfile create mode 100644 testing/docker/android-gradle-build/README.md create mode 100644 testing/docker/android-gradle-build/REGISTRY create mode 100644 testing/docker/android-gradle-build/VERSION create mode 100644 testing/docker/android-gradle-build/build.sh create mode 100644 testing/docker/android-gradle-build/project/README.md create mode 100644 testing/docker/android-gradle-build/project/build.gradle create mode 100644 testing/docker/android-gradle-build/project/gradle/wrapper/gradle-wrapper.jar create mode 100644 testing/docker/android-gradle-build/project/gradle/wrapper/gradle-wrapper.properties create mode 100755 testing/docker/android-gradle-build/project/gradlew create mode 100644 testing/docker/android-gradle-build/project/gradlew.bat create mode 100644 testing/docker/android-gradle-build/project/src/main/AndroidManifest.xml diff --git a/testing/docker/android-gradle-build/Dockerfile b/testing/docker/android-gradle-build/Dockerfile new file mode 100644 index 00000000000..6a2a569f9f8 --- /dev/null +++ b/testing/docker/android-gradle-build/Dockerfile @@ -0,0 +1,57 @@ +FROM centos:centos7 +MAINTAINER Nick Alexander + +# Reset user/workdir from parent image so we can install software. +WORKDIR / +USER root + +# Update base. +RUN yum upgrade -y + +# Install JDK and Sonatype Nexus. Cribbed directly from +# https://github.com/sonatype/docker-nexus/blob/fffd2c61b2368292040910c055cf690c8e76a272/oss/Dockerfile. + +RUN yum install -y \ + createrepo \ + curl \ + java-1.7.0-openjdk-devel \ + java-1.7.0-openjdk \ + sudo \ + tar \ + unzip \ + wget \ + zip \ + && yum clean all + +ENV NEXUS_VERSION 2.12.0-01 + +RUN mkdir -p /opt/sonatype/nexus + +WORKDIR /tmp +RUN curl --fail --silent --location --retry 3 \ + https://download.sonatype.com/nexus/oss/nexus-${NEXUS_VERSION}-bundle.tar.gz \ + -o /tmp/nexus-${NEXUS_VERSION}-bundle.tar.gz +RUN curl --fail --silent --location --retry 3 \ + https://download.sonatype.com/nexus/oss/nexus-${NEXUS_VERSION}-bundle.tar.gz.sha1 \ + -o /tmp/nexus-${NEXUS_VERSION}-bundle.tar.gz.sha1 + +RUN echo $(cat nexus-${NEXUS_VERSION}-bundle.tar.gz.sha1) nexus-${NEXUS_VERSION}-bundle.tar.gz | sha1sum -c + +RUN tar zxf nexus-${NEXUS_VERSION}-bundle.tar.gz \ + && mv /tmp/nexus-${NEXUS_VERSION}/* /opt/sonatype/nexus/ \ + && rm -rf /tmp/nexus-${NEXUS_VERSION} \ + && rm -rf /tmp/nexus-${NEXUS_VERSION}-bundle.tar.gz + +# Install tooltool directly from github. +RUN mkdir /build +ADD https://raw.githubusercontent.com/mozilla/build-tooltool/master/tooltool.py /build/tooltool.py +RUN chmod +rx /build/tooltool.py + +# Add build scripts. +ADD build.sh /build/ +RUN chmod +x /build/* + +ADD project /project + +# Invoke our build scripts by default, but allow other commands. +CMD /build/build.sh diff --git a/testing/docker/android-gradle-build/README.md b/testing/docker/android-gradle-build/README.md new file mode 100644 index 00000000000..6096b083689 --- /dev/null +++ b/testing/docker/android-gradle-build/README.md @@ -0,0 +1,2 @@ +This is a docker script for fetching Android Gradle dependenices for +use in Mozilla's build clusters. diff --git a/testing/docker/android-gradle-build/REGISTRY b/testing/docker/android-gradle-build/REGISTRY new file mode 100644 index 00000000000..cb1e1bb482a --- /dev/null +++ b/testing/docker/android-gradle-build/REGISTRY @@ -0,0 +1 @@ +taskcluster diff --git a/testing/docker/android-gradle-build/VERSION b/testing/docker/android-gradle-build/VERSION new file mode 100644 index 00000000000..8acdd82b765 --- /dev/null +++ b/testing/docker/android-gradle-build/VERSION @@ -0,0 +1 @@ +0.0.1 diff --git a/testing/docker/android-gradle-build/build.sh b/testing/docker/android-gradle-build/build.sh new file mode 100644 index 00000000000..3db216e5042 --- /dev/null +++ b/testing/docker/android-gradle-build/build.sh @@ -0,0 +1,62 @@ +#!/bin/bash -vex + +set -x -e + +: WORKSPACE ${WORKSPACE:=/workspace} +: GRADLE_VERSION ${GRADLE_VERSION:=2.7} + +set -v + +# Frowned upon, but simplest. +RUN_AS_USER=root NEXUS_WORK=${WORKSPACE}/nexus /opt/sonatype/nexus/bin/nexus start + +# Wait "a while" for Nexus to actually start. Don't fail if this fails. +wget --quiet --retry-connrefused --waitretry=2 --tries=100 \ + http://localhost:8081/nexus/service/local/status || true +rm -rf status + +# Verify Nexus has actually started. Fail if this fails. +curl --fail --silent --location http://localhost:8081/nexus/service/local/status | grep 'STARTED' + +export JAVA_HOME=/usr/lib/jvm/jre-1.7.0-openjdk + +pushd /project +./gradlew tasks +popd + +# Package everything up. +pushd ${WORKSPACE} +# Not yet. See notes on tooltool below. +# cp -R /root/.android-sdk android-sdk-linux +# tar cJf android-sdk-linux.tar.xz android-sdk-linux + +cp -R /workspace/nexus/storage/central jcentral +tar cJf jcentral.tar.xz jcentral + +# The Gradle wrapper will have downloaded and verified the hash of exactly one +# Gradle distribution. It will be located at +# ~/.gradle/wrapper/dists/gradle-2.7-all/$PROJECT_HASH/gradle-2.7-all.zip. We +# want to remove the version from the internal directory for use via tooltool +# in a mozconfig. +cp ~/.gradle/wrapper/dists/gradle-${GRADLE_VERSION}-all/*/gradle-${GRADLE_VERSION}-all.zip gradle-${GRADLE_VERSION}-all.zip +unzip -q gradle-${GRADLE_VERSION}-all.zip +mv gradle-${GRADLE_VERSION} gradle +tar cJf gradle.tar.xz gradle + +mkdir -p /artifacts +# We can't redistribute the Android SDK publicly just yet. We'll +# upload to (internal) tooltool eventually. mv +# android-sdk-linux.tar.xz /artifacts +mv jcentral.tar.xz /artifacts +mv gradle.tar.xz /artifacts +popd + +# Bug 1245170: at some point in the future, we'll be able to upload +# things directly to tooltool. +# pushd /artifacts +# /build/tooltool.py add --visibility=public jcentral.tar.xz +# /build/tooltool.py add --visibility=public gradle.tar.xz +# /build/tooltool.py add --visibility=internal android-sdk-linux.tar.xz +# /build/tooltool.py upload -v --url=http://relengapi/tooltool/ \ +# --message="No message - Gradle and jcentral archives uploaded from taskcluster." +# popd diff --git a/testing/docker/android-gradle-build/project/README.md b/testing/docker/android-gradle-build/project/README.md new file mode 100644 index 00000000000..07411b3ea21 --- /dev/null +++ b/testing/docker/android-gradle-build/project/README.md @@ -0,0 +1,6 @@ +This Gradle project exists only to list dependencies used to build Firefox for +Android. Eventually, this definition will live in ``mobile/android``, or it +will just *be* ``mobile/android/app/build.gradle``. + +Until we determine the best Docker and Taskcluster patterns for dependency +gathering and toolchain producing like this, this can live here. \ No newline at end of file diff --git a/testing/docker/android-gradle-build/project/build.gradle b/testing/docker/android-gradle-build/project/build.gradle new file mode 100644 index 00000000000..0029a0f271d --- /dev/null +++ b/testing/docker/android-gradle-build/project/build.gradle @@ -0,0 +1,73 @@ +buildscript { + repositories { + maven { + url 'http://localhost:8081/nexus/content/repositories/central/' + } + + maven { + url 'https://jitpack.io' + } + } + + dependencies { + classpath 'com.android.tools.build:gradle:1.3.0' + classpath('com.stanfy.spoon:spoon-gradle-plugin:1.0.4') { + // Without these, we get errors linting. + exclude module: 'guava' + } + + // See + // https://github.com/JakeWharton/sdk-manager-plugin/issues/73#issuecomment-106747867 + // and + // https://github.com/JakeWharton/sdk-manager-plugin/issues/99#issuecomment-166438751. + classpath 'com.github.JakeWharton:sdk-manager-plugin:220bf7a88a7072df3ed16dc8466fb144f2817070' + } +} + +allprojects { + repositories { + maven { + url 'http://localhost:8081/nexus/content/repositories/central/' + } + } +} + +apply plugin: 'android-sdk-manager' + +// Optionally, require an emulator. +// sdkManager { +// emulatorVersion 'android-19' +// emulatorArchitecture 'armeabi-v7a' // Optional, defaults to ARM. +// } + +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + targetSdkVersion 23 + minSdkVersion 15 + } +} + +// These are extracted from mobile/android/**/*.gradle. +dependencies { + compile 'com.android.support:appcompat-v7:23.0.1' + compile 'com.android.support:design:23.0.1' + compile 'com.android.support:mediarouter-v7:23.0.1' + compile 'com.android.support:recyclerview-v7:23.0.1' + compile 'com.android.support:support-v4:23.0.1' + compile 'com.google.android.gms:play-services-base:8.1.0' + compile 'com.google.android.gms:play-services-basement:8.1.0' + compile 'com.google.android.gms:play-services-cast:8.1.0' + compile 'com.google.android.gms:play-services-gcm:8.1.0' + compile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1' + compile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1' + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile 'org.robolectric:robolectric:3.0' + testCompile 'org.simpleframework:simple-http:6.0.1' + androidTestCompile 'com.jayway.android.robotium:robotium-solo:4.3.1' +} diff --git a/testing/docker/android-gradle-build/project/gradle/wrapper/gradle-wrapper.jar b/testing/docker/android-gradle-build/project/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..05ef575b0cd0173fc735f2857ce4bd594ce4f6bd GIT binary patch literal 53637 zcmagFW0a=N(k5EAZR081>auOywr$(CZC96V8(p@my3nWR?C*Rt?>>8Ga;>=U{1Lel zDD75u}rp6Jr1cQuqg>^C$(Gz+VQH zzl8R`GRg|dNs5UotI*4eJ<3i`$w<@DFThLFQO{1#H7hYLv+N%~Ow)}^&dAQtNYVns zT!fjV{VLI->cAu~`&D8zKG=$Lu6gHl?*#n6O!!In&y|7wozULN{2z<@cOKaP;xTtJ zG_f)LKeD3!lhxhH(80mf>HjyxBFMz7_%G|qUn2d_LqzP|?QHA~O~{z&jcp8_oqc0u zVFnqILia4#v}oKIf?(Ie@_rIJ5YzJt+6db~OG;MtX2T-x7Y?I2Uh98n5LS3V1C}HS4FGX~v z$Nc@PV}OL57{$6`F?OZpC3tYw1_6FuD$Mp!j{*rU*hqXn<%A*gByd7vSP+Eau|x2# zbojpicFH5Wp{r|$!G;AH>zuv{!no&WYcJOy1{EKKcOER79a z?4AB~2&Kxl_9%i#ei(r8v4z7*gWA;1RWFs}DEkEi9O&3cXeQYzSs4LaLs0WNcN6=> zhx(^zTh@EXx8j)QAE`vZsJBD2SG2W63c^S1{zh~fgVeITo?~@0xwiXYeNvP zh@DSQerPfkZJ10ogioa8axbRq$V#3hB)2X4*Hvv$DQo-GDR8ToL`Y31j{uZmPfbMA zDO<_ir_inB9$^)ChAVKt@$BqJST(FPZJ}%BPCY=jaRw#?9IjmBccA|-JE9aGzDlEg zeo%=%7G>$qB1lx89YeshqzNP9V4Y2bdLDuN2?(_%6$Z0L368S~6Kz}SMGE)t@mmsN zc-{tuAZhnI$c}w0ld&HggTlOv_yo8fgAE`4L#E?jYFxlIvpGP*Zau2r$I6qH{1mrxV-_P((Xe*bOifCT2vO#(V)|9y!dZ2Gsh8;} zQ?sCNCg|@t{8YP0s#TOLou-F|(Kd(lAtMK;sg)c|G-j$*YY1YaLz?{q;T^eCN-_4h zpZI%MF30$%+~z2klD@+^+(~()lTnS1pGMpOoL$T$A0;lXrQuTRuP|s*x=rn$Gr+d4 z3I4F^6Pv$E6^GF?I^-}mmKpx1G5H^QdwQkeT=iGlw*C^yf0jDQ|4+64B~zlYKmRHg zT-cxK^Aj}W9vHo6qx+s}7*IilC%txNb}60<7yfKW!hvuUo>Xk8iS*C+N1q)+AdEBb zGcPD8zakoPHhHMzbBa^-*%ZKrA!exlB&)W$Qb;o?vBr*(VoIi(IU?Vbw=Yv;#cPOQ z%cthdrSPCec1md&rBcJ>T@g|k8_wXJF+-=+#!E_c2U*N_@riQy4+jOv&JYZpDO+jR z>-8s_+W~*jf9@2l(rZWOuYM{1)i1jLyi@W2*I=nSn>tC@+nUPQ+grOj{A<&(%G&Zc zf@t4jiMp%LN;QDiHY;r~?G3GK)urL7sz?&KdVU=acE_TLA$-5RJjAAjRnkkD`65Jjn`R{(1?A?_+?MiP!W=HvIoVjJ8mhHson^bb zCK-2PX-u2WWAbJ&rM5S#fQ)S~-jlS{qjGrN45@v`>rzi8rHJsFGAg7zK6s zJ)0yWejy8z^(ZyQphG;H!2|ot-rY1-cm$)Pzap7soaKFpEwxZ@n?mU>ReMCcFW09% z!B%_3Bf>qp<3YOK^-KJ|%Si8yQ@E))xW^eXNcF~EBgVOnA;#$UB}eJCoA6*D%5_XQ z>+qEdvzV!4q}`2d;sbL0k#`i1bu;F@JW9LsThR;uD(?DN40We`e!x;xjrb-w<#Y=`i$V$+fEU#tq#5&}ge#UU~733BA zBe4RaFC;iUfm?X+4MH2F630E>h|()3W;~9yEOt11oZnaGGO`7Vk+ukY~$)| z>1HZsX=5sAY;5Z6ENf_IXm0vnRzFou+5y!R?~iR3g=Lp5@eg7J8=%k@g&+XNQc&8u zk%d+Pd?`43`vkjg*G_DASv=S!l;^-55#~M$!59H(EWjqASvVqeVbqC3 z4oEn&>PBE)gvEYXeiKfyv)NsFtTrn+$}WOWtyW=XglP%{vJ|+#$vjZa z(xTX?W)!-ki-W6D)gW9|-&k0pcFQ%gI?^NbyfunbH6~k}8goibT-n&|sNQ?5Mm8Bt zo{R)>m3dfoZKq6@g$kvaQgW=2E94!aP&SL~@UpN`o#<|AEv&t0jd3!IOe@3ir2$>^ zylt%0(ZApJJ=u(xGV+PF-Lhw};*pc>%*4o+JCh*b&BM@#6rO{Q0u5s#WGWvIm{?#9 zBj!^;W|sdT5YYw9hNROXv(+XxgFr?J#X8ei#w1Fqk z!8f$#-f_zKEx0N?vxS2j;=53N3^zirwR~$OJC<(teCN9|;<`AXI=HE5YNQ~0W+up| zxvZj{PxR)!iWjCW-Ig8CDHCWk#0%vtVOdMULc?IV!z_lSQLov;T*|y!zwPQB+7ttL zU?v!p!|rZS4&oJ%!e$sqYH++a!KbqFQfoCqGnfJx#auV4&&7;mVTJ(c$1?_^{d&lb zOnXQSm!w3~_Zvq|b%v|`bdv6I^wJXtl>K^$k7Q+<^l#p8sBnyYPMe4enXluVhw-AI z@a!F*NYbiI!d7fdbQWxkV&O8?OzJvGZ*oL!SeQj#9jkh;h5W|i-A#MKU%%ddjE0YY z+$YAwCz|J_Q-y|$OY2%&@V~`C7$fcKE zX3DpH%e}R8wDG#uA_= zu81aAn^uMGZ$ZG8>9wq&M)6H!>(a0JHdm;7;hx1KruTKEIM=_Pqz)Mjq*YZ*1&XcG zXZk|?;zjt>5Pt)mL>hIw0@@SV<%J?4qsTo?z;Y88GP>k&u>EBlz-+p0jZ;p{X4eTL zZ@iQiqe(faxGN82c+HgcNa(>8coQ$K&FyFdcY; z1@v~{hAL%lfP)cUAU=>vB_v3vOo0o&vpaH|N+mb#P>)K_4}N8apNaqqvQHe6p|x+6 z;UH6m{|j!0r2^XmrZ#hQvxDO*R|ud-Ps=bT8MJ&~Fg`^t-(|oh!3H!mF-3;}zh%J|M%P)C3KgaUaZE`o>X9 z`0;Lkfee?(9W<68&ayWg+!3NCbBM&(x}XlCUyQ$30J?Vw@EcfqT8q@TIKc31pZEyw z5t#Uh?&10MC7f5`gb32&6P)+b90bWEtRJ5=DmAN?R}T6_%T;bR=@Ie9PC!{3!`x3C zhcViN*pISAoN~mN`itwG67YwNN>Aw`QtfF6xs9$LsuY87YUils%)P>@=kJB06UN~h zYQg|sU2)Q8MHdT7DS1ua8=u3v)w%~=lE%EUy@g$|RU(c}%|vwG!TUn^Pw+AguP2uH z7reYf{BOaF`oDZ9VS76>OLJEzLl;YXyZ-_&$+q&Sf=FY3woX@r`GW$Aib$@Ba|-rZ zpb=G>RN>Gie1z*9(nycvwsqO=l`Tn_?n4O&5KVJ>wF_#thB;W8SswGhu5~^>=H~Q) zPVNBV(isy5?9q5Ja5s(uV>7%QubrL)GeS7gmb@nOFSY`AS85y$y5WWmjuw8*@MADB zwKLDttjRTJkx1gtQM_$&idMmSh7C9p#ilWsp+D6r-RP4WVcj!#jkogPxA{%ag9s zU;N~9qag(;Cpy{u&`}5Vko+R<-p=>zDnTXYac6P~RrsVN!8FO{MaUAeA68NcEpSTeL1$Kf|4njPYra1w zK}@)px4&TjDcg#^_?E|iK{@tc#KZWX5zoK-yAp1yZdtlLuar%sfUt* zhqCn6nvs!IQfY`bL?zE!5XKU{ENTh{M7YefOB|h5ysI4TEpDq>=w}$y5(;YQRgA+d z4hy!^=IB*PVkR@5a^93oem46fjMtbACAu`%sEye02|j5$svK=&hP&uXi}B-r7K#62 z1HkPNhP^yQn?|*Ph1qSR!)#cFhuz3bq^H}3w!@5q-R_qKCTnfTB@}5jkxD6#)iI2n zqzGGRU@OCvIAu6y63J;+o2cd^dLzL3z65(nYQ(}!iz;fl=73^pP}A*Z=PDvaWB)5p zV$^`MQbB$bo8G<^$JD8dEK2&ZDv16h55u+K_hzA2!v&Z4xr6SYjAod&!g?qZbrF%X<1xM+z_%}&Gmutk#z~z^IkX{sN1kC2`b3A%XjhxN8 z1W<8`dV{T~iU&4nczQk=NsLiYyd-$#~1k`dM5hUB8bcxqyn`1D8ekPY^;DXuT& zc-;eB>jc=g8lkbRyoX81YLl|w@ElTEN$b6@0d6HqY>g1Kd<`y%%G$d_;RJHh;C$=M0F6MP|*X$A5Og{hmDTkL3! ziS+E~3#+e4+4(KDo*^%hyCiM=V&Or8`s1%yTWH%qp*vv{k8fe$qt9rKJ`9M^07aJw zFCid(Bzd?h!dA#UH$}aaB`;F7xhg&}4lJ{KAFqmYzO1N;zGvnjUmgqE!kmBO4GJWJ z8A3eg2xT3pxJaWE7vT}x^ir?LaReZXbI(X#mgu56Igh_|NUGM(?>RguMg_M= zq&wtiAUUrBxgp;Tm*uATcQM2@)T%oBy)(1ke%4|NV-R~37t{OeO;H5R>cyN&e{tAau?m{vqLf=6gO)qzMbao!*zz8u0GdmVaclVyl``xLJ6Lh?F8&(?bYyGeKG zu)chV-+i~zH(8FoyR9s1tjZXQhcl+Ld^DtRxfNe`0pHcY>A1K!PHbDTtF6wtd<2Qj zHn&jWItWTh95200}C(M$vaUP;{gsSd3{KTE|lg74u6XDqmhtD?5WG;^zM}T>FUFq8f zK|}@z8?P);NK1$%*1Ln@KoAE}QKC3PT!Yf3ch=xK&BB32vbfzaL89&=l!@L=UMoQ0x+Qq*4#eM(Y$($Xs&| zJ&|dUys`?Gx$8p227PcDn(sU$`H7!l7QSKY%pG9Rri=CT0nN@1X>x6R4#+&fZ>m7E z@B1l;asBE2w1qSweR9MfuxHzNxkKnuH^o!HTE+CnPqQCqF+bAX%{8<`)uHuBC3b?R z{MPaE5ch?)N_R=}+QhY%r9J3+(ihjsE-YPE~t1##KlDUR_1^Oy-PoUT+OHqKu{8z>ri1 zNTS}Yh}72qrk306u(l?(r@rm#t{x6^LIu3~f`O!bKwxT74YvUM{fY6?6Kj=`&5lDTaqGgc z|A6i4W+8m6^lHnyHy88X0i@W-y3D!v*RG-3OLqLSaqLD1cb!>wtsrVE;QF0G5gBuA zxr&)>Gi8L;)*m%Vr~|%;ZY=uKnNQF#d8Bk2T|8;{vMY_^upaRnf# zcne261NoM;gJGE^m+UP$Ad^0UEpv@FNU~2i0x#b^kR|U@ai?QLTy5z9j(4D|>_V$o z&AYR}M^-n}6TIc=+6V40(d}GSaUkxt>axcdZvF;08hT)YfF%_6-|6dV9$R~C=-sN` zQf>}T$_9|G(Pf7y-vx3f>fu)&JACoq&;PMB^E;aGj6WeU=I!+sbH5H_I%oD1hAZtV zB^Q&T@ti5`bhx+(5W$&%+$E{Z>30UCR>QLE-kMh2$S`cI(s^3>8t@vw1lfs?_oAf3O0(TGXet6fGa!H4Cc0s#(f9x|s4qp|pucb69f&W{y7k z+~uCM?-px0{PKXSp;m_Pi=IQ=4SEX1)RS_Oyox-^g z4c|8VNmbQ{0K++9fC>i&QdUrPIWi^8_QZu%rTT_|lUW{fz7#AqyR5Gv&__0p@E7m^QMN1FZE_Y7nu!ZN6Jm^H$uPK_~BC*L{YcQ{6g{KXaVmC zF!l$ZIUUUIf^<8ha69u-l7Ch(0fjtWtUXwj0H?duK4>8xWExTEY9zG8GfabA2v#*y z7wWzW-i5hlr+19k`6)f#hyl;*iYl*U^-D8Ze$!ZHhUi&5BZ%?(Y6MUU#rD1pKGE^h zUnnQOG_s*FMi?EBKpGFaKd{(2HnXx*;dYs?rEV?dhE>{aR5m{vE%{5}R#b`Rq> zzt6hx9+5sc@S^oHMp3H?3SzqBh0up?2+L*W=nJ#bN)K6&MV?Wtn1yFbC&B9{`(t`zcppF`I3T;#g^jbHDih*k;w(q;VO^=lfzo;gHu7oqr@Lfj!f z3cx!&{`j|#8e`$9tv+azfBr2m%(>gPgZnp6enkZYMD(98R!KW&7egDHe?@z8HDP_w zj#~vNyEisyhiH%nC#^+DJi|F~kl-Z~){zqK7>O=S+>>IiNN;A7L~6C7rB?bBv=`KB z;*IE36(#2Z>sG#PFNLkGtt)EQ_LtYay{|93TOZV~{$_3**(OMb4EKskf5xo=Hs84Fmn%&S3q-yvIk3`E;w`Wci6o0UQ#7o$_MYj zSwlylI+LcrRYy+mH3?-(SyhfYGi)#ncaK7$m=iH0z*%$BCH|H9=@ZVK5#DJrx%dS} zbqX`9>s%IpxWbmzg@DqnMDls$jB5`4zxe; z8_2TWIB!m9N+ba}aPx9@DWge|RH5!v+o%P0nYgEVn)8%Vdf5BbZ&vR;TD$yo{GD0{ z))_(YvDO#t9QIu;g_W*Lqh%}E9Bj4roi4&VWvw!yGwGMzPgxNJmo=8HC}uUz;7f16 zJ!mb@nXID;Bn2O=Gkp?0%*zuEvKH{zeC>icS%yWIE83m}S%MIX9BzjhXS!s>rL7u5JC_n~)6lI9rOR~Gm}U~M zJo_G}F|vasg=bd9ZL*|55$g)o%v-9DgOWrB74Ly*sA{995n4IQsl3JQJUWfuT2?fZ zLR{oIEJrZ3UfBI{+>WA^3Ip^u0-<=2QCiOG$+I}(2a+h5B_paPcDPKzW|Iv|_c3l6 zxJ`_mW}3Ku7%34FqX8kyO~Bc8>pJ2t^I!Mupdf{n+xD^&`sSeG%WELyUR627_-v!H1>3O7b%S%w09JfbFXxeaQ{1cUU< zy}>Yq1IKG!GEtHSPhL}#XtQQ*7*%nn=?Z!mN(tx8rJa=T6w6hZgnq)!buxxCrJ-;k zWdYS>7%S}Yd1GHY5j?QBhzcStQiUTXpND*(EU5J!a2Dgve{r->K_Hw`sevqCGv&1+ zW5;H^URKar-eQA`7DK7+qN$0*P7+qK6cSy^s3=)>bq)G(I7N67WCRU5pVzd*b~hvh z5J2x<3^{bxF{WBWeixgTdNTDj+`^W&PDsWv6-h$FOPm2l;lw7nbp9RMIDe6-)=7g-M>lqJw`(zxpd)NH@he;;;wxTseZo$yE3{Vi3L#KE7waR48B=kX zESjro$+lBC_xfEk*saIn)&4+R^_zDu>iT_HY6i4M^2}H8nBgJ4 zK(sCi>TI>uRkcDH?Yn8x`<)%k?ItA00UX&&@L)@|FSx(xLH%7W_4QtNoc_i%c+kE2 zlkK}}^7YOy_4e3a!a0BPH5vu6;*;nL4)^E$VQgiFsaUMdpjp?Ik2WP;yW0FoI@zi9 zK}X`Uk)yP*pw+pV%#yKhM%sWMZaSV?En69f{!ElLzQnJrg=k;y#d5mo*~@CNOr~Lf z-;d)nwfAhFA8;=TlY56>GCXnskt}x<+C#0UWXXbup-xyZ zArLX^SBq1vaU#4`=UJ%|H#H-|=MQzO zZfN5cu5PjHRzHr#!DHhqeIf|e-=I_T(Z&c*{H|7oGn?rX=Re4Nt9XA1D8EAqls+sy zutVi9WC#8F(Tyz)SvYWtZ8J|<}mH^+{GD@r35ZEx&N$!%M>a-=!qew0J%v9h7pRK_;4mZJB0UB2Khq9Al^@XZX$@wc;ZjAE;os&`=<29G3brICGCR>iWoNL^O z@Gry)9Y8f+4+*RF78d&c42!Y93@X523z)4e z3v))!8?NEap1^>c`%LRX%uXxptukN)eZ%U`o|sa0!et&N^(DmJLBUeA*V9`EiB;Y- z*h#(zBS4n*IcR~|TW0Dc$q?jaUU?5Ws`*^c`${TWCe!Tta5lPV>AK-TF*G*gF`B2W z#^>et8ddT(*4Zt6sqvDIg&d&sr!XhSF4)0}i|B{vrd>Nv11`42yT?@XNjN5cl`&iD zL8E%@Hz|&ecWs&L1fu2O36c-V$*s&9Zbp80y_oPOHNi!eA7q;lQiHxN1k;hc!We*- zU~|vPIi81cbsf`?s7s60TY9hGbM{>=s}rfSfLMH-6x%H4PI0nqBv7pr1rda?%yGV_ zVrs|)$vu0~5(raaI;Lc)T{uA-oJtq)8)`GJB?!9{CX2gHj+SI&wCR1AI7{74Y&U|* zdpM<%y6YI2h8xMjp`V&mAE?JH?aaLvt)vtdKFKCN{U*oDzP>C-H5NLlkS3o<-{8TW zAi!NLrC!P`H%UUr&fx+ktJJ2iWN$b7bDGG~FgOc5b5B4fhlV4}>vY=jpr9a#)qBY! zha@Na@~pAw*ndf<*uc65He_!ar2~nir0eCR%WKFg76V{r0b-#yd(t|eOT;x}H$%@@ z=sbTAb?0tx{7K9a*Hu$F(fYF?x&rmUvP$;uCrxm&PYnJ^VuksthAsw*m^ zZd9GXHw)(2BlcB@%X&*bC+V6pZrVfc=Qi#+MT_^HD?Y&EK1ZGZ2l#O?ngtCWN2VSD z(KBN#Lp`UAl;^SGL#jG{8FaV}LcXv!&inlAh*WIZB6fly!Au!SPp%|~amjX}Wcz%r z$V>M4@JqHts(F8;4#AUOUS9w~;t3SE#7}2cQ2|+ zsanLZqu@TltW7n7C-6ranktBjiu^J@@sar0gl0JIv|uN4liDI|75E9vb*DPl4%1^D zQT-AI!6F~->^>Q9LGmBcXYA{1!L7$GJUh@cW}`OiOjuOKSuX>eps5RGWO@2(LZ8%-g14X zPa5=q`gOf3hpg@So}2MCU`=B$JBQYk*lYJ!gyNJ zx$R}8uaME2mp8Y8r(R^UzqAt|V_?UO66SYBg`|)$C;kO=EWdMCa=@Wcc{AZEN zY7NKy7b6M@L^VMHB=LyIrs!S?D5Eto`8jdTU65EvpD5x`P4&R@mdE2kXB5Js`+k`Y zsDMy>8So>V7?>5^af7v=^op_z#Sq65q@|y>VdbkPwe_P)8v$`a_aT-TO`_CGd3d!L zf_Glg1+Nt7crs`K%{&E>GfIIhFn@PNo|kjLZqiE22n58Ief&=nPmRtrgoUGmSFj0F z)N=1U5&1f~@JfN&rRIhJ2iqF2#EU5!$cnO6ZSo3z2TVE$A`Ck^os#t;^_Dizg~pCn zy8f!x8O*0B>el!8C6u2_O1H>b>}bu-w&gnTVQcf8oJQ0nOc5HqutoXdST;Zp_HD)k z;ryu(M1K5cd9f8elWNUO)n=r8rl)wGsGp}B_VQbfN!80lc)tM8sJ!H>7Z8?Q4L)gL zuNxm0Oa!fTs^aOMd{Yn6Nbs+TYN{#y6|0y}&r4ChC2A19@(Yu^n_WDF5`OJY;~dSl zLG6OITL;-Z6)Al|4d2vYeZjM#8ks;0;G4JY!7kLQ16|^ce%uaz(_%YtZ%t>WYaO!Ak!jJa*!&ZT_IRLUvky(fW&$dEm+B<2}`V*~!rvlT?set%f`@`~5 z?H9Tv6lN=4fhEG0tq1;TkKQ)Odg?Lr9#c{$9EM&{y6}82)cq%tQv`4R4+O^nH)!b*;7C7Q6mvwx#hT%VXQUp)7$0l29x&S1ep-S0Ih#jkn%g4c zS@>O(N$T3U_!*B)|JQohOStBoKU783Y56?vlQQn6=$YqGm|LEXSt-Y??HkH^zM985 za@UpP;zwm~XA$GF{6P;SV9$HrnGx43ls&$9V2&vZqD27H6ph{(0}pTtZ*;0FHnPujOXOv=!n6QgXtQ3~{*ZN4B!Z-QJ`HDzFBk-*#B}qS z)*L_EY#MpHkEQNi(S0((2KNMRlm1JWgcb7hjg%*w!(*o~VmEGw_^V>0g%TzHqWRK% zqaWwE!Dx`f-CJR?@bl=PDL;Ubo}|>7&v1#P_w%@a9O3Vm2TeADj@e_Db(bvJ_k(|p zAqW=ZyKor@zG=R&1n796=5hR#;)q=**&96DVukjCEPUrZ(}1R%D|}60+Jh|J3tlAz z$o&D5^8aD?MQY(2!hK07cuuN<$l#l>%lQ&i zHDHHwQH&_K0*d_-Fhoe~P0`+F_$j}?|7%ryo)U>F^GZ~9K}j)GtH?I<)hIl#w!xVwTDcg8qrc#Xy~0a9!1NpSczciN!rwFys7Mo8x?mMpdl&`q(%0KQ)97x4 zXrLtX$K-UWCL;OsX|CWVVm*S3fH(C4#>V2iP-)m4HOG);Ifv?r!7>cy%X*UnMkHm1 zwYxpwP5*pviC8JPe0nl{_?MiPD+Omsps@`C&QQi<}|JWz9gGp2KIBqX#x#-xy8LX)w|%t#>`hkb945` z`R$Oq^BvdhuZvk;cXq0z8=o&`nylkfR+!yE=K~GxV$MtCL9}ji}J3mD$U>$0j zP8a_CTS55FfK24@-@233zprinHwEEB_VzB$E`JNFWDPCtlwAy+T>fX#iKh0J8WP`N z6L=NMfDIFv0|;97h@7$%ZUHNFXaiP~K^k{SbOVE!NLmFg>RB4S0BZgnQX91kmq?wOf9&a>0K#$WGq_6)#1frO@Sj_P6zW@J4KhH7FoCnnoN zJu!b142F_nkWAQ98V5sPUcCEB;m;bWNa>7Z#mLqutEM&v%7c*45)K^kZw({iW6y62 zqvCHGgOtw-?@rocm`Nx~AU?`jg&RvCyoGmRK#rp_Ou(^BGX^xB)9lTw%eJ{>-x--I z&+sdYZ+%2)*Sd5xM0hNB^cJm0=r^z;cksnvSchAC*%1bO=-6ApxEtZ^TDNoOzy_-esc-&n1Vz z*jmtBjO*fVvSET^ zGNHe*kaJa;x}b#AR`troEgU{Xbg}(#`{QUFYau%BdN+bBIb>>->+C>?la_i6tiAJjH5XBLc)Kzz_ zB~xndPLF5rr1%TDrUi6DGUEWuw_;Hf{eV)M8{l3q(K_b29+mTckTnacJ^l#@%!<|K3(kS zWlQuT?fex!ci3GJhU;1J!YLHbynOK?jsZ~pl1w}*anoV=9}1qxlbOOqJEiec1oV5ayrkRttwqs0)8{bzlO%h8Z>aM^p_EJ`2X{2wU( zgDf&1X)~AzS_tK1(5M9txh=PYjCDqEJ5Mw7!h}G*2-BXJQot1Yp-jJi?2&yS2VD&b z$1FyD;0cFxM6%Lq42+LiYu{uALU$P4)Zd7SSB^YmxZ` z-55W8I;sV_!N9_xmh1qKdju~XC;7^`WetPD+=IqF95XNeW>2`+WPa_D*M{>4)E)6@ zMdIyhN~Pt9+y(8q9d5rP{xg9uvD!|y^tS|$6blFl@SpPx|5ait>S1c^`rmKNQq?^T z@Kmw?$Tm&bu`h+#CACpe(URLP&WKL!q>)N0GkwVdu-|tXhQvYNGJFUVu7{YXAQ)-( zAWc000pZ6yltW`*9%KRHBT-`^U#NmPaq>~Q@l#jI%pWd5`N)KEZ}%a0c!{|mCNG)- z{FuWVoLB?N4_`h&`cV7Pz&=y~43KxJKz-Cx^6&SpL|q}*mk(cIaPq2$*>7nQ?`?#8 z&_$Sg=;V8_haYc&881Ubej$XA_o$z&0r^xFdyBaE*f-ZW_~-a|>wMhX?cNq14i)Ae zCNhE*x6HQntBK1>sQ8LgG9?u3R2qx6C5vfkO>PzwF?9x}c>#5^7V+Xj-zN&ESLv%J>sE-m^$A9Q<#yNgMKhxkHK_;|n%gOQUK!)(9J{7+kX*KG$&7Cn-fVDI0Zl7KxMQjm=2gF3f~3+z}0&X$>PTbgdgG1j(7? zpj3js^Z`FbZ*4_7H}+@{4iqwU&AZO~V)ES-9W$4u!0H_x;p(#4TrOu*-b<2T;TdBg zF#akdz)5`EJCE)yw|3AiVzDJpAMkob%a#5O z1Rn9QLDU5W$XceAW^khRS+C<}`E2x_P<&L0ZriP&nPWd&&yB^n`LY^uni&OMc7 z6wf|T2>AW1kUvYqL=5_w+C!@{zxXMnv|7KFfZ8pc&A``1j+VSkLr0QH+qGtjg>k)9 z_Q7^9!2(Y1IA5NLDpFDwfq;|fAVO`ynI{C^dL;UbuvjcQYcR%Py$xIWsWa)WGtr=D zjh)bTyUXaM$}XRau^=+VIVwlHrlg}!e2VP!@3XTToumQIszp>TD^FhgaR zhV1xmy@^D{8=Kz{x2}T+XL1vYvR7RLdP^63C}v3b>wJd8QkIJ{r(J>!wwlJ?+@huV z4DC1$Ui!`1n7t}*>|W&HUb7XZCLguikty|PgY-zLM`Kj_eknD=z7#qY7WH?4fRg66 za=osWmij#7jjGOtva7jm<@B zQv#&XT@bJgyF2IcteJf}{RR}X^Hz~bK`W^z2QG=eF; zl8L+m6mDKi3}tU1@SbY&ysq4reWH&=l{aaPJ9V!tv$s>#9}sA`a;ADc=AL(zF?gYq_6S!t5yVrIp#$q;{4!}2c|hKh?yxgp+%w2 z4YfxwHEssjXNLNZrs1Ay%(DDoafzGCQC>H`Ovtn_R5c)>~JY<~3qN%EfD#g{JEs9}r^IC1`teKotg!XjewNAR_0gfhZOfXc@ zbY&MP@kSRVE)7FS=)x6IEqP)#F>qWd?W`?*kz5lYJNTkaHEG++3(+4Yiu^EWnmHFV ztsPd?HmoVRtSNb{4UOESFsgG$lygVKvK?ca+g3HLo7S=r3k{3s!blGX7DybHKg<>$ z*1ueg;co`{G)_Sp|JI<}1;k&jaN@Ue1}h4nQXbIOE0G}$0 zQI_ficsmj|owWh;2G4ItA9ui|D-#F`p(wMbG_zMk@g>7iH=2XkQ=R%?JEc^Nddj`v zKx=jEObay#v$55#{35Anabcss2WweqEsA;Pi>0v$ zm7E;2&-zf4dv)`MM_LyyeAcw#3@UZz%+>7n!!VydoW|C2RWn3@S3GtrJBz4Qauw;I z?u}yR5}jk-IQ|7MwTCxr29k>kohuEmX#;0_hy-oxR{3ai@yUAulHQddjFF4BAd0;6 zRa;1BD))j~b(X=PsV!7or64}aJ=#i-8IlU7+$9LU zqNZpVv7s_%4|;$BI>f$Q?IhYeIV*5Z-s-_s*QDz{-IXQKcfI}H6sQkvI#5~rJt&uY zAHuWWRW+Y!z5R%P^Ulnr@9{=GchIzbVC|S2Etw=Hoetf~y$Q+wdsFKo^CkEd(`1ir z_(3b}&b1RH#VLcK8%a;}3EkU`k5tKMPA_=v!6w0MPeQ?m3yAFhVeFmaEAO^#?Nn@4 zY*cJJ729^jw(ZQ=wrx8VqhfQ$wkoRN%e&Uv=e%p}eZJqmn0NDHqL1-!y^S`W{{G6b z%U!ohHzZIbYH-C_JQI4xM}{$K0l$slS|vIsTT@h>q;e`@Nk@JnCZ89R@~x4>QO$6? zYc<&euAI43u})(Zo!$C=@lQ-%*CxljC%8#9OXa1AXz+8ljhN<4Yes`WXJC?stR`_+ zI>APNv-) zR}@DB${lS4{T)hfZQfFq6Q*b&2@Gx_ZpuHpz86^&l_(B5&oscMD+}Y~`b2HxLUA|6 zuyiGSUZOsclTU6JEsK+4HA40rjY7`N^J?;>o9Efg&4n9CC-kESY4W1WKjZh@&r#M2Sin5_l)gmV1pX3L(aXJJKM!#ZX%dYoO+Wl1e zxX=lQjHn4lMpV4Rp$Brv~y=D8Bi|O3P4sd-p=>2}4jI^qF<8CQl>wfQ{2>)5T3-y$*<6E>l@)RDC zyK4sPTT_7a6S-{7Bd@u;a?jq+ZX{r!)3bvI@$vlZ?0l65`Ix&TcV>Wzk01528Flt) z6eA#koh7H~zKtz!LPm; zlL+JEy&)0owze*4wp=Z~$NGz7_(uSlOX#g^OYvDa%5CK}Cx(LVROjztf$|^}wgH|3 zrl8W|J($E$wFL>OF#iNb*-AdCjeZBdc-E(SZtZCaS{z%Jk>UHNI#$=*Xkjr?6c*pW zsBe8H?cm*|i78Ai45ZYNg6pi<9+Zb|=q9hcB5RI-#^W%(oCyPIOs zu9xz2dZ#E?jNyrRl=5>?J;mb&BuVu{A#OSB_#_k5pTlr|_UtLnUL)mUOg3^M{JdFb zU;)W4jfG5J6kwIyhIrBH`+3Vp!;bNlvMo`!9lWf9dgJ)|8+H9}P~2YfBXn;nVg|cU zMl#yZ*^=0psvUFaEc)LP*u@T-qOvO8`vvVU!Bi!&Bw3Qfu&O0@v0l=8ccW~xZ*Gzf z{3R>!B}I(}prXQ1@LQS9+5cG6aV+R^%HB?F@iP>(I|^MiPugFOCv?HB(?VFbK`vWj z_0i$j4$I=i?2xM!!s&iP_>5tXji^&Gw$mQzT1e$R5p1#rg{SQ|%fT;pfm*n3GQ4 zwmY@uj2Z4nEKS+Y<5Lje`>s6fd({rZ6HTJ!q0q%#Vj=LQ4e)d43g?q7VkxnUh){ZC zjev2fa?OD7G3*DP;@MWKymX)ug*mlX2js<$O@Cpu@^^An8n|=Fyx(PM1hUK4%eRVY zCrTPcp|cU+ypM;_3sghhs#aM@M&e@U>PfdoqYKgMSD2JSO}bEKn*Ay;?o>eGmqiN` zlBJ9)yH;jX3|`j|t1)Q%$_6^L`b`LZC_&DsJxxAZT_l`bN;IA17hAmqIGSR9xKzCc ziZrVtS;a{c*CovxUm^pPk^>F5sWDc{?yCBA3k$)Jm3%kR)m*I%c=y-W%-4vQ% zd~}??(MQDKn|E=JX;|1}W*}HhtPYP~NJD9*FVX_kX2HaWi7UbARk3-PaBN|%-ol=j z8}%%?$3SQryUrTX;4oF4*J$to>u;eThO&*oYcj+OM|b;wwH5Q5F@%;SEmBwN<7jAo_IdjUlWL89w1T$>vB*S z)v7T85qag!RDHGm4Oi4=h(o&?hLwZoqj{&hIzs45*qfM;lL{gR;U0j_y#g$E?$oAr7%#NV*3%zENQx4k-eAHykzLpb7QcRXYsnKdki!A|-~|q+ zS^rjf6Y65Ycf5FId?qR!*!Y;c#<6#s@&vl3A0m`H4Ci0!zk#S3fVF(NCJy_|VT<%+ zbV5+>`chieI{GnM{pf$oukxXy3ie*I?~aLM+;2lbW0eu$)i1<5)G`NC-}bD@2m-+u zf6@+y284?mIskSfV7$Ch;W}_A>gzHi?XJ*Z0ptoRyKpaa3XnlPf#TbQT3D2)__q)X zo2(J@Gp4;{s5;brLCTb*CLYp)bpmtrurD}s&`oG^1qGro)WH~X`3aPf^BM_as&N#H zbnkgTEl>s9HP@7y=rvfwBefRt))+%fg!>ApXpe9-n8K64LdzN~D$INjSp3@N4$HRR zOdj3Ll5!>He}=>DNoP}CJaDQQ0!b@QNjA;I;y2RRtlOgO>>;OzG0 z>$XjhCg#$SHV1_@X?CE*56PWlznM)TX=PbB1D9haDYfPT1->3uP9Zo4cVS$&ru1Y9 zT__0W*@FH~%nPd2Q82V4-n#V!7Y*+6s6%+VMz zRx|tT#!m5*yYaSi&7t(6&` z@QbhROI+&dOE5YvODU>yTRNAP4S~%5di{{l7s6yO>D)mw1(hCtNTyxtV{yQUqqv?d z$vYk1So@#ebe$dilgJp?ZvGvRYjfsX^Vi@~);`>LWUh=ZZmw)fiMr7NQ>?CTwVA^! zq)bZ}2a4+Rs~8@k9f3VgUgwS7UB`S!qdsIUGktSoHV+JS*<)LiSHOo_qiM*Oudmbv zhh(&0RAq{iWrlD{oJf6eOHym~7g`x@+*k}A88wTe5t3#kr0q&C8l;+cA>4^~XkdI$ z5;c$;(+J$_@e99Q+Fxv%mD0bhAX7>iZ2`-i6OuFEEb!v^b49LX_Os8MD2YRgWj@m3 zH4J{>jsg3#=^rQQALpp<<1JvwWb(dq#M(~mDxEr_bXlUF760c6+3FOEd)_B;py~5Y z*Z&I+_0Q<}e^J-6)verc7tw*sIGPc>l6YUfD29SF649(k!NYu$6Z*>IFUUkJw>vDW zJv>Jg%aWrgPD+uFl-JcyIs;mq=0=EYE{&^I#aV<9>snp2=zA{i3*nb%LKtm4-mpvl zTZ{j3ljSI<@rvsY|NZobwQU+$k@yDfW4BzCs1Y?t6)uhviI1-vXwI>$cfWi#vM@ zC1L{bMg)pnf|7v5qhK|^4Qf|gg=2FJlNqWPfK4QjeZ2k^A2yaEm02e(*tBp>i@{Sd zQqc`xW#$El*Vw~s#C51(;W%;sfNP`_>Mr)napsy9TRl0WO6d#iOWq!1pbc6iIotB* zee$VjomMe3S{1K`%K9EAzXnG2HwC$f4MP`d9Re)oKdzoL9PO~nU+*Lbcnm!Qo*hS6 zorbfd;>{p2$oM!j@xXwfz{cuae58+Y0+<@N<&x>)zA;p5gRir0o|+gHZOu2k)@ zZ`2ebG0dv_P~tNfwe}}R2d}C&oM)Y!JaOsG-oSPJ^8DQT3{T?=t z;$5^S|KtQtc$S9p-Q@hpfKh*~gh5UMmwe%O%sdc#Ld;%mgn|>Z?}zg%`cZm2*p#qZ zK2giJUhb{pozf?nk)tP}k*&c4f7%WsDuP7WXf_p%Mq?BhN8ev~7HBm+_IQDlo+Ue( zVEZ}!DJ4*%^K?Dtb|DE3BdJHSeznAPpt~ZR1kB`yv(3^y?aS9A=~$$hY>~WX9M?sY zI=3)u#-FB}vPMK5m$x{b= z0>@f`P1ln+C@b8CD^MQ&_ps>0!w#!N1ohd#DA*cGN%4XUHxE*dYe8z=AfNFM0Fcq+ zCcnopA5dR?THKe&zq#OUL7$Pg1XB=v$gOy-xAhoDbas)Y(&9eoqPT@%iXB!}RD7Co=qr9Pt^-i|J>I-keB#k2@uim?oTGp`j=ttG?*r&lq*Lf>tL&M)k2)kZw*5)}{a^yN#EWt@mR z#&T@d%T=lBPu64FJ;?Ckk0nhtll;s~&@#G!LU(2?0M45lKC-F0?t5D=ZraakEwU!| zNHnJ|-*5TZHFZK2+!2dO-4Y4H+M@;V?M`XkP@`F2jVC2<4~5kpc&k4GvY$9ycWCY_ zIU!Y`wvenGQakX2EI}X3_D0JRR|@s|;ykl?zm}Zu)#iOY2TGOzIGy+|4H=>s#?m{P zpk>>X4iuGScL;n{IjdZE^b9Qwy8H}~0LTSLs%^19*gO%ju)I5SeIFGI6KGp(Yxz1AWu&5JUGceYyacUvL(?c zo8$`!h#D9O2@}Mh4a*7N3z23qzOx3)o3k(w4^kqytWw0vDYt9hzI# zw3|G_tj^YUwWS47!HJtfFbKUVWfF+xI#v-9Wg|bN`V_A7zxNWV^0ENt%8qEBvSAyIRmo-CI*!OCQPb?IMSb?&sGyO( zzBOViJ4a^6NxvM#r&|k;^0Sz|lE(K#dA`}yC-RyUu^jdwRH?X)4ema@zmc3Bv%ZVl zUTSFhM$4)~{T;zew)`gyBx=9d66#p~%&+~u0;?!g44c}ihh|Ger{v<`Z6ev?8nVD* z4`a8A=3jKEzS=AC&mUx+IZ7^fhnEq&Bid}(6h9jCZO6{OWg)M!w}FWALL=+*_2QX+ z9;p7V7j$>?i#;FKk`!4B|IX3bko*-^wei<2D|^*l?#|73WdU3c<0un8;U^tD5sSz#4b5L|t ziV7%uxcK^1gzKn#sH^oXf41YV=`F1#;`YPSi#b7q( zD{2Smzk7TMMpC%g&>$evNFX4@|8ph$I|VaDJ=_n?4BOYVv6F=do(lt2gEFoJ!TOQ} zHlb;?mlw#go)z3RS$ z%y0oL#E5EEFBmm{FjC|pso``GH9^0)iMPz~h$`#eSL%#wNpz$=Wy9xrSOUdQw@r;T zSNX=nTW|>ThHRD>r{H1)&0BLw{kkoxmij3pV)DroWOG`iGtjQg9dt|OhAvB`PFbdh zE-DK(K^Znjz|Qeg_)Zs(U79U87@4L-~C zn99t{Pk1FR0*Mq%rC7O)%DT3B2r|s%VKvQ*T!*Fjw_0h3| z{)RSQ!pxwD8s~(@VQ`PW1avInV(bZ+CQt@xP?yK3q@7Nu*=D#7-__Z{YIvf}>sypa z?cSc2)3Q{D>9;5GYBV56w3&<%$xlYB6{!2wD$Ka#g+`W+Y?Ql%nX4(Yv=Q0gcvsCB zlU2o~SdR#j<5}ZHcP;hIeVZ^i1^tZ))Kn5HsC1BKIG4TmDphEf!#G&u#s~~Dn)1cg z1Nm3OYt#3KaPMLa zkV>Obk0)NOeQo9Z&vCAg~!MIU@rB zWLfi!(J$Rar>7vj`k_Vv`yV;?)O6=qMxJ+7;=?ITnw*gHN@p3v^mA=vFvqt}8l z8k9HURMOgY5b(4xluq4gCwEksN5C6$&jGY|XJKHp3tgy)(^F4+$6y;Cq(ZDwl!xCuFm7S# z*H5>VK5&;t!BthoVa_U;RkYcc7f>28*7fj_M37>ghb$?b^n2QxxYJu9K*#Uaq_mUf zUQeUGR_aWho_6QXF2NK^$$W4z6{_)x!Ro&s9p%6yD<{(1m8%hCFJH7tRHd_8O7NXu zU=X^9HMS6Jz?;oZwe4q4Gz}V(_(S&CQp%gsjg)n3>cvGFPBmaU6BxK3u)_{pE5s(#Lv))2V%V z+Slh1wdgXZ@!I7vM^xBtOY?~eHtVJe*yjosXwBj9Xc}Ax5p6z#Bi4k7-ahGF)D>zsB1iH}3)=Bc>yEMzkFAB6a(c?d@n+ zyj*sqNOPLZE7b<|b%V}Y&Z%`}YeBoW0<`xiqJLL%Hj zKN)^z7JoMbbXP-C*Z8kjw+O=^`~LmHMTy@DEAVE`a>;<1(2Sf=)IuTcrpk8`my3|FPO z!r<;%ok%PZ$Ooa<{J&Jcs9_&gnxxgH=s)bx@e9YqA>zBk5E@tc=3K~5kc{e7Lt|s`OB747iePjJwVdUVhaj+F=t;Zsk@f4=?#*Z&iVPv`beRwLa%NcHxg zSR8u$|HE=uo|=@Wnv_(Pkdz&t7^fYZnBG%Dq>@#=mZw)_WL98gY-VO^WoA>hcSS(_ z0*jU5h>mt(R!p9XwqEiNkpC(9k+CCs@?o;^VaeLRvHY(-dEb_YLDbWq9|Y%9_I{pc zf*873SR2zhni!c_*gOC2Q?SK$+72+ni@Lo_p#*q7#S2QefQqJI=)&<~i3gBjCs^O# zow35SdX0`tudz+McZo@hmS#bp<9mllG^e+j2XyUGA{U>Ud;q)x#+d*Qm(9R*!WdHS z5Iw5W7u#!F5wvV9ZXRmVm~YPzHSI0NBo^|xX39*yXL>)$G1V4WQ#+>T}5)QnR|X}UK! z+T`-OYIi!^1b+APdxx|SBL#ywKVD%&?u+??Kb`z2^Na07?htpkb({;z4CR))7 zG{#w0Iv=oGO}GdF5|Lzha}6zFfi;qIR`iQ}w4>3FbWGcU23C5#6Mb7yOlaN5Ny*q% zR3T?v0WFjk#*BJC^&USudN^k4N9-$4xO2!t18dIpE!YcwK{*prSMSwDSYmYu$&|r~ z%@e|A{&ZC(Y*hbk^J7u6zt;vZ;j)}80`o^QjZ+) z0z$`ID8$l}`D~J%IGSSYYHc8Y1m)1&%%h?7acG*zN4{u?Mw|nsB{FCWr>Yfm3jT)h32Nx*2 z`-dh~PQ}A;vQr#kjeO4-{$BD#v2PX3JJcxP3CO8W9a7V8{X1pruTo_GVG>*NS%Sx( zum1??{#ChuD?tSV$4`#^fBCW@QG$O>!w~&2Z`OiyJ?IFt5}sB-0~hW4I_O$PX8|ht z+n%1+KNMA2r^BBA?mMCB=GmJ&=qPe1w6I9woP?f-Kgxkl7!gspyd+6!DvA~p>!u1_wjqD7AsTHHPINJbF|bJJ>^Om>dJCq9W6lGF{~E8Zy} zE&7mNDd!q8?_3vHlXqx#uh`@%`om8k)A{W=}kYJIe3xw28?w|(& zXrLZT``$6)fX-?|}q7+!|Ti@pd`@V{0YzPf`Z#gcNf@YZn1$|A*zb zV6r7T2Q2DY=B-7!b~mJX93qo&^2E*pp=L9uOhp|tkb%1%z$UPCpHA#}GO8;Xi#%qp zKhIXf>mkN>IxdpgbI?@lL3n^j>6X1#a0mtg4r{(H3>Rl=rwc$9B`#R?{QeMTP?3tk zGV!n}0FZffWt1T>;`A*v0ywn^S8!bGDyJHlHt;b-oi-cRmcXSF11GU9Ui^oM)h#sS zg1$iza}jf6lU(py5POo}o`d9j?@;vrDFTe*8559CyJ6{HP6qB z6VPAavfGb=P>>}TA&+4)68PIe!VHt8IYzYzf9E*BvJ=>g#+z?L%fsO16Httqes7ge zzC4FBJg*F$_ZB8h1(h`*@!udGuiL5vt9xrP*5goJ*{B=W+bed4NYoS6oMsVc1H%?E z=Oi;ndHzac0Dg<9)-O88axX&t@V7|*U#q>VN|yOA>T}TNgNN^bvjYBE`pTd7l&#t4 z`mi_n#6bVoESPMS=}!tY+Pi6oiGfZ2ZJ~a1pjN(uF%{8g#H1)3rXJ-heE4R`MG3s7 z>)2(=Q*G~9CY09=XgK+BqhHd^q-(X1l_jV1X69p$$JM&s=KaVt!xjkI%|tKqAp(}= zY<-^5tUrLPIgL9-HN#qQBqBx?5I}b_s-H=mlKWkM=9ewd5UX5b#B-6iMr#vSv6+fl z%fYIjA2~Qz z1lTf>K_}Z!09RU*(T$N~=h42IECugLx1l)S?tLJU1v`%+H(*UF4UB)*<=z7Ve-cU*sd0_d%}MD+DKxGnLRinyhmeu;@^#qQe+)XK2PEc=!pEfwk_4 z(`WDmFvl@{$?jw36ABXB#o*IK(1DTeG+0YFw$MWU(FXn@gE#_R4MshxED@h;4rY(L zr{E-dD-!yhSj<7c)c*70z?Y5(6fJA7n=4>P3SSUYem3cp_NvoC4slI$kC4|mJqiP| zXWpWPcka7zuQ=1hNZi3*+QHY+J4v)>G&K+MZ%s?KI4DY+-%5lMc-n*sC>$$Cx9Mlc zNkYB$Ez0ppa-ze27Rf|eJLX^GzmUAqGp?LI|7Nk#FV#$-lnb3qNXk@WWMfm@k!|2j zNc^3`0)%vi9WK|8xn<%-ylG5>vmr1tWv2a#pvM0JrgRuHSIU+FXJoaUy>Aqjf6t- z?qbzZ&V46;j*I*Yp z*T3=|)BI!Plj<4z2_XAl?LgADpL4kWxefhOf&A?u4Aii4M>|0G{b`)2Ne%`G0SQnm z&4@F0Li!Rp(?ncQ1Q5WLiE3IiaFc=LU|COJ1wS8>(!K!d&9JL^)kCj&21ua_buH-C z75rW*kpFn_c;WSV*~+cvGc$E<%mmhjfB$ood6#{)(c|=I>T>8K$M1^(&t`Hxgj-D> z8FArPBUBk|VvQ)t+glGkYdt(Yof3ITEF>eLeiZEG?J{@>H>Ud##vY9ThMjR4=T@2B zpZ)7z-@H|aJ-zv&yiBYIe3(CZIk#i2#-AxfgZ?YP4d3v_kASN^sIFIq{@AA{PQvd* zdsqZX*GAYbb^T8;eiR-alu^02j|SMW+h#I#+v2hhru z$Bc`IGjSayx*4^f*7%iT&Tg@X6WV%OTlST1*t;_1&JR-QsSTiHV$r>8RbA&UF4|6X zQ&q6z_=^`lg4ooO3{59CdJPAn{G-S)v2X(0TOUX#npqt{>74{po35t2xxR4>J#LTH zUq1RUhLrkXYQJJmIIyw~&u-1NIL%=n^3?kf+T!ymz?UXM8`fKz3pdQ3j+bFw^Tqqr ztkv!DT`5<>W2ugXS_1{)VOZ&HmAMmL3BykWpIX63CSkbM-_)v?7P(z4H|Fpcn{*Zz zFBeoNRpzm`gx(zZ_a5=Nt42l}wzehNuc#p8_pk%9fh85OWWYjfb{8S1g(911TnE0I zO@mcSYm`MgR5=>Xpe^b)2o4%|3}M(QLy7*R-j)LTEh|n$ljK}3=Yu>y74*Tz$@y>1 zTQ5Wa>a;#Cm`2zsBe^~&cd`CESiRmzSl^MpUPDrsA=rx+v14$S z6I%#Ka|ahqNj$-7CES(!v}s>$URC?Iz!waYE4EQLQQ98B9xMZ5$Xa6XN){pPC&y0( zL1o7+i0(@;8GHgdcDtF)Sr^tU=t`}z=F8^o7_P)*L+ta^0E{DWb}v5moInB33bE(k=Z4E#&X_t2yY3?YkWxq<;^3hW`b=JRMp=67iQv!^p?Y9f^| zG`Tn5Hbu^oOR!?fK3f9T8e*f%wbb*yPxw3Wq*ACxq1=QGFusc4*k5N{&$c zHWr57E^8%+#k*gMu+U*-7L3#1zn;Tm3h6Pmg}Zox+e)4)+iyTG=OH z1X7Bdw>Z!INh)Vzl*+8johtHs*3M5dn<96AJV`kWlk-u@1ryC_zBJk9V?RHG2zx zKE5gBAoaVTL59I;km{9GbxYLyp|?gZGZO2KINU&z4`sS*bcH1D+UTIBUgx+&eV|+^ z(Y{}DbwzIYWjVU0H58yd>VLHz5=?j_fY@Qt1AGKg4~@j%1@$`5Vm)bYKq|sih|@vW z%Qk#NG;FFbZ|7FgWe0OG6-*<%X}Y{QVb(0)MqX^a&eKpZfZY`gp_&PTRkjaRH-L}U zUpRvTl-OMNBPh0Bw5u)eqI61*LHbUksHfS`5Hn59@oyqp9mf$%Mb&T zF`f9v2z!$DL~G7-x1ez`(sy=Uybh@q(W~@ z6zie!{jECEXT)w4xt`JpW*k*dN+Ujg_Yaz$q{iO03ydfXE~*}jvkg|tjt%oS$7dhN zdSk*em2mN~51S5PVzb_CMQzL$&no6{6){Mu zg%(Jao^f^>tWmKdr(4almS0}UHm?A)K2s%3aF}@5*1_VDSU5_w_=*ql64x0*bWJ-< zdTX-VH&nfKfqwa<12;LGxH7zXCNruEBAUzRTb(O#Z-cKEW<|sfEYA(Ommx*>1^^ zozY`--7@MLoO`qY%Y3YU4XKUVf~|J7f-0D@o=Jmiv;C@!x=BsBgYR-MDa2$w1faF3 z(QDBGIwDMS&hi+=4iTY6ZSxJd>nw5FCgs~-wYRy}=Q+X)D;5`G#M;48>*_uR60w%O zwR>yhs<><>v~G~;8(`VS+GRMG_|ppp30h367M#x_s85JT4>ixi9@Qu(G8hH)*mbk= z`rNyq5nrbi0zocRv@B}kviL)hZD_;SKU$i&%;T$7G_M$p-I>?Z9IURcyb9j(tn4 z+J=$bxZ}z(jPfo$Hr)Fbo^HbpY`k_R924r2ke}8mFiXi{p)8G8$3yb3*0+#B=DI7E zObCX5!U`F*YJxSG(r}(?_>w1@_N^ap_3P-LCyR-vGg^WfZb1(jWvYgxRm>)mM3QK! z?+uDCg5?@R$3OnPv)MOXq}cgfA-117`medYe~r)mo7?=i&gNg9ovN+X|Bs69RvlOR z?Bn_P#=aRa3qT{^goII!Aw%!vlZ25J7ptOag*50de^cH&HU?zKB>lMlp(BAFOO5I4 z|FJ#1+#ik0(NWjMmkx^}MCPz_xOut$nAPKRIl2FK)p`Z8@1QLRzX!|BI4fA0#hBQ? zKh&2LXfYw;z!qTz@3^{`LokFV{EFf>-qA@83V#Z=z63OhOda=3H!vJ>h|b!%Ehs*M zO-a{wl_ImnRF~1N-4#3CzJn*e#DO16HhYDb*4$usw92tsgTx<#3)KMZ6i)EV*T>`% z#Y4=qcZ)*u`DE2|33?5gEn)YM%f&~WVNg{j&y`&AA7-Y|>+PepHBad(p9kr$cv&V$ zfXSa9wcO45wjHF$yrpK*CE25<ZA;!n)`98)) zv~`e$d7=~>apRXAcFYI^R-h#dAOqoxFa-m~m8}>3k0Z5^hqvhA<}Zu&G)y9d{fI9b zfH*XSd{w2U(Z>a{TNH@`AJ+P}CYo7#nVug;P;pK5e8ElU1pRAI1pD~had9M>fif)b zD9nGrLwv+I{si(rpqC!YRHEvGn1T3_(Hp-@=}D9VHtm^sk5aZBqNOYST;dy$az z_k7MX{LQ*;!Wr8Kk`5Qw&=NbENxFUIqTdeLBk)V5&uPCnvG=>TeMN?XSA10Ddt@5c zmA`4c;~+YWP3pp$s5zmc<1KL^iN=cj;A(A00;;OosRRQ(ln!nY(Me<)dkX${kaaGl zMJU4W%9G`)=mW_DM_6KD*+vq7xFc1EucCsPa_J)FZU@l9jW8@VUX7-9Syes4c~K3m zO&$2EUjL&5CGi~7O8E4@(h)%ZbFRdHINty4I{)SOs%bmTt0BK9VU5>|qQVdE5D@tr zeciwSO)64=ZWWO5FOn3_6RlSjSBclrJe>Q}{RY={Uwu%F)TG>BG~xU*C~WpZ@gltD zE3Rg|+8|w$7(SJ=m;z{gKgU7>2X2c!CF5{xlvw7SLZyIu6;yyuU z4|WH$F-UjgE}%@H|3 z;UT1WVQ3=Bl6?Y2MzDrlhr_num`*$X=1)fbKBYPM)i}q?O{_fL?2eY%i$BfTv64xZfyiZYs(MaR4rm14nI9 zXHkF)*@>u1Cm>Nw;*En&uBse;-_ zAO%x4)haHNSQ{$RGRnz00;q zy(bWtbYjm;T6h)<)?ptEeg?{4mj{9gy};*2USQrc{jd_+(kEnS)`p$K(%(6IA| zVW`rl{-o8%LE^d(=&z-_6G#2VTYSV{ftXD zl8)(ET}m#_t(Q>ebQ#LL?rCT-Y1qkzN$3YWKo~~yoCjyt)ehX zWME%aUs~|R$?Qi%440ZJ83_g~9xwM0>)l;v(AEoOLZFF$ zVVhN9k1X=!*5h4nmi+~Eb$38mBcsFgh{qJ+C$)@5*Xr!v<=>chfgqs!Pf{_44fDGy}yKSuEp;;AsKpK z7JZ;~%tR6#He_l5!Vh?hnY6k@BH`%(@!MDFZ@lS;ndjF`wAYJGNB<3Vq=|DhpC88(0 zpC6&SErRi8Iq3dYne?t|SWd@L%RhOn&v6{+nkt2Mio!9Nk6#TNw9IP}$P?zxfz!Xd z29@LlE{wgH${}_>WpHr?DNc{&>h-U&I5(W=?p5hMI#FuY(;E%YF7G=PHIA=5;qR_q z_Lx{_OpX12v;Ri!j&A9$8Dnl)0LdXD>r)$E8Kl4TTn*Kwo$+-wjKd}{ z$f-p+)O^<+=F*|?IJA%dDZ~KrtJVW%$Uf5bNCz})1cISixlhkEw1TBiPp;*-IE{Me zoa9-{#kHTtmBT5@QLZNx&m&mkPb`8+ChS7zdhKKJq3=p7q1IEn&FPWj-F`y;{$cvY zB*qy2b%OLC8Jt^zvGmceMM6`y^XWLfq<`FpeFz{*8CE%cv=UFiYFP1g+i&VN9i1sQ zyo~3Z3OvvyVJN!VT5c^-4NW1|DVJ)>>>p@keo>!DMhqQ6c^2c8Gyp!kH z)H~i8{#_GgS?f%fe!9IS|2=v8AG`X$G|~UVQcPCT{VRFP*QnX(Dl6NRvFjE^B}Qe7 z_Tw9gxd2)qY&`E1yCmRZ)Ktxsg6yO4XOVme{}b3tVT2p|7Zf-PSAwbR&ZC@hKDYPR zw>S8044y&|igv0#Iphp|x&phGq^ka=UKcB5HIh=U~OTOj4gq(-PE&bl z=_-F=$1k3E?g8&A%7sHQ_{nxez9j6!&HHlIM{?<(=)a9bwSsyS06PV1-uqh~$PVa` zbcMyRXUa5Fq5V2H`>M$k-V(Tq2g=`~uImOs0Kik@i-8VcFiRDa%6q76wAPJ)+fZ?n zG*!=cyq^W+du- z9T36BOr{Theb15sL90o|J|6){Xh&k;PfyToP3*KqZDI0M^afl*1(TSxPA0UzLdQ`< zt3QV#N&6*uqt)tDQmRW|5iF5@nH*aiO#P0hphfm27cqGF5366>-8L=hQw)!w{Ev_H zfBfUdf0M=k^7qwO{czRM-^JEP=S1pNM`D2Fs`H#FCR~7TGw$V)d*rfs>r@Vs_FAxC ztw`kK%#vnD!?mTP^JhYeiy<;nd{`m_idbRDzo&3K-Av)ybzQ3?_wcabNH4W9F|d3F zEFO7|yv^F@K4)8xd$`K#s!LS4?rB3MlKW8!RLlkjonamXp^9k4x(G zHMoCg-dq8;SPtHzT|Z*> z&~JQI&AZ6ueA&WlcN#Q&bwRv^htC|k;sua;(g!o$rH{R(d3)#x?8csAf-g*0mt+ea zjXjoHoC`;@%Og({xHX!8&uuqp5ya0hS7IV8)@Wq}Cr1Ae2bxH-MFi3JjwV^4Lq(=& zQCbAuk@;LZELNC@z&JT5vcW2Moo zgvq2q$huEon^r^~v7N!($O?J>%2Jm$Q<28BvTGbV$RZCGN|c2m_Nfhi;J(5$YO%P< zRC0ZC21||uQUjv~?x)UI-N_|*3>l7-L4f4mr@u_2A0CJR-<(U3%p9XJL2?k_LH zo1(x?jHJy(hj&{vX`UXee<+|PNvqB;4M+DEmBSSTB@#L_tKGzzsFy)sR=T!ZN*`Nt z+ZR=&!e&TRSE9d1t+`$W zC!^%@mo&$fqlV+lM4UEMb~QdzmgpX%TlhDT!0fZ>oEAvo%jqZ^1Y86wHL_^V`9Jn8 z*j*kJGeIj5^I9t5OlUJL^1h6tFOvl+;~9z?gx=9X)_4D3Xx)v|RRLfqZmmADgk zC&U%v?(Xg`#GMFncO~w`-Q7coCnWiYcex)Bc=z3^|5Qz#nX2iv+fH|%-MiN+BIU8f zsx1uNbp+`mfG~qk&VgyB*queUqo5d4*qGgLmZ4d5%A(hzlCzS;hySc>LhdOf8ij@n z59zDn|Cz9KZujAqU?z~Y_}dpkk{g~d!hudNW-ofZ>uwno~Nj+-6RM*J8$cAinVIWTSFel1zyFNozGc4XXiWeC2b z57jKMz@}UGX!e8AA`^fA(mM6ooYypGEN3%g`>S2ChK8V`ZQKHPzG zf&yO>!;f9SgWYahQ)ca1GnS8<8?)_;KFWy}ixTo4Xq@u{!7$&ojy+i{stN@Rc52+j%!C@rskk1&J$We*H-07c?5(wJuJq0m_ zoMLlG^1s71cFqUG6>PQpC>E&E}-imBKbcL}- zl6nU;>qLJ@qAj}&dMW;LYinP+74*3~$b$R~;ZhBpaYlay6JB$Ok)A!E5ju-Jpg6^{ zKjd4yt_UPK%q?psgOIX+*LFTT2MMCHo3G`@!+)pF4Kikj`` zA7LcO*~BKaqn3Z>**UVXn%09J72X%?&@)+}`Y`z*<+gmzMu9c4*9fzFh#oIK& z7rd0U#YQa%TW5(^iCA`t&$F||S!;y~N=dWvGO>ldWy3|5DDW;SKR_UeMC)H@tVFdl zO5VNJ1V&xq2Nmw+rw3XRWNrpIwpi5{iPKz8GID2TC_lCwfK-!8rOF?V$)F{=c5vXD z5VOgF?A<|8!&sW!Hj% zyOZ#SX306CuKg_aj_&&SXr01+mNE~-wM|J%uys%{;ysZdDY)&a=dX*pP<|FOH^8C} z8nCG2{N2&@%Er<}U)K(BvjW6M8tdEsG{rv&m`sb2lyuH>Q>^A`!OXfoYansLrsBs7Z1TwdqO- zoy`vIreh#PsJ(Ws%}+eAT{!h$Qu^Y}H7}MyO?#b5>FechQEe(8K&)$HFQsyEZD`~+ zF(VM*7j9B=(JnG{sk%FdTOzcZv^x^HOFAQUy+|5|JPj6sbQ<9wfkPGeCiufv3-85r z5GMsu;7jj$KOIkrsqjlkbllRC*$}%g1_xSHl2`RpxKJxKd9W&q%b&57T5!YOFB;S1 zF?jZw!ghT0gbTM~_f2yISF2cISD-gM=EcH%b*`N^l9FT|7dCRl?VCO%2n8x%g=~up zorjkH?0qP*8{{B^M&#PL+P*ayt-IjFn_UUuFRy7pSN zJ0za2Dfd=~AY4L6fW$;#;_4Y#s==JOLjpj*({r^uA^G~P+odSx2@SRsG#IjAqU+8` z!_Ek|&BlYHPiGx+Jt2fECSS|2&573k3pkmhvdPhwTb6U$4 z2ZOD-)#o@N{>G&@+ftrn#U8wa2Qhv8jsgRohbm)@U;Vmr<9hs5F>^$p?sFWIMN=%( zT5$UXfSGthtjrvGB_Zx}0xjdZHadYO^1vh)1)FV#HR!;V_5yzj~ISjjXhco zu2dub`p|}E!_mWAV!47G$Eukc`B`_Wz%&u?1yxyC;TS4APXw1Zj{IlLYdSgp|69i4wlZ){B?!ljZOwzS9wh#alq1r34@tP}}zVc_fO)EWP>3ss( zb8+vb5C>bblO3~@EfL@2N0m%_5Xj{}g2q(6L#G?@4n~1L+ zLgU&z#SshE5&G&w6B+lm=pDt-Gw2QwM4p^83 ztEKCLi>dlv+htPHkQ5x*<;KP#w`*C;^!&l;NsZ(3*XsskA?8ro?QytU&zrBpJox=P zWmxyL2@f*(2b)>)oJViR3xZWQaMJ9IH90X4r{_AglBSt2jZ;&4Id}FH+5=>6UJ7hP zbE2Mpcsa7;^YXuVdL&-6cF0vHcF=zEWL!#SnodMw)$L-NhIaiHd2bZ%Gz0BEdS%?V}@Pm`r+z z<-+S2q)VA}r$elUpn82yS7oSEf+$zC(poLJCh8?S7doRgwOws$FvC^Hdg?LjnBn-> zyYrI{-cng%z%ijtf$K5^)f$?pD zf1_-{byG1{zpet7eajqV@?y_h_1Q2-;fl_! zq^i)v3__+wC4DB9dPXGkB9qW$TEe124wPbvLvww4v$=s68o=qG1{5fBiujA>H6%mb zUD)N%S<=_&hEQr%(&UQf6k5GdDB!W@D}AG>SgLujy69Ch7^DR#3**z#!;;hm(P)k} zQDDF~Boj4Aa}N?1?W55oS)psN8aZp##%cs0cZPj z$dN1YBCG6N3ucPzfb?V-#vI3*0Mm!BcPg=hW&}Id@*WK#*-)lA$!zuVGe92hm=_bM z9YlfS_-Nc$ULB-x$3IOc1#4)5Y(10I!T?^!X|AOVjqI$&aX!t&#!bdl*vJ(d4Pbi= z%!!FpC@!4U&`1`2h;k@ikc! zQM7jR0TT=x^)APwy|EjdSG8gYh_xR`%-uCfP%4w(^`;5TKP!I8PS(}GCsu26z)Fv} zC?8u9M_sAkj>IFnBuo zyZtQ@caH=FEW_-CQ{*}!BO)=ovR`9h*r6|(kMcK8WYUeAgDvqpGKR~3(V9X%ISlE{ zi=WdD9c8x|g|8pX>}*EHcX`Eg1%v?3>Xe0P+Dm4=&b3Pc?P%P*uximdo*B5ukhh){ z;mdy*-GlW;|1;h)H4HCtMp05>;LA t9m@SZ!E*7&jsr?!t7TL-WYI4eM@gAug8 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/testing/docker/android-gradle-build/project/gradlew.bat b/testing/docker/android-gradle-build/project/gradlew.bat new file mode 100644 index 00000000000..aec99730b4e --- /dev/null +++ b/testing/docker/android-gradle-build/project/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/testing/docker/android-gradle-build/project/src/main/AndroidManifest.xml b/testing/docker/android-gradle-build/project/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..decb23df4fd --- /dev/null +++ b/testing/docker/android-gradle-build/project/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + From 1486d49a182cfa6893ebc425d4b46bc531b0fb2c Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Tue, 16 Feb 2016 11:11:42 -0800 Subject: [PATCH 23/45] Bug 1247170 - Part 2: Add "android-gradle-build-dependencies" TaskCluster job. r=dustin MozReview-Commit-ID: I6rgyqci67J --- .../android-gradle-build-dependencies.yml | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 testing/taskcluster/tasks/android-gradle-build-dependencies.yml diff --git a/testing/taskcluster/tasks/android-gradle-build-dependencies.yml b/testing/taskcluster/tasks/android-gradle-build-dependencies.yml new file mode 100644 index 00000000000..b69bc3d385c --- /dev/null +++ b/testing/taskcluster/tasks/android-gradle-build-dependencies.yml @@ -0,0 +1,66 @@ +# A task to fetch Android Gradle dependencies from jcentral and package them +# for consumption by tooltool users. Normally invoked manually. +--- +taskId: {{build_slugid}} + +task: + created: '{{now}}' + deadline: '{{#from_now}}24 hours{{/from_now}}' + metadata: + name: '[TC] - Android Gradle build dependencies' + description: 'Fetch and package Android Gradle build dependencies' + owner: nalexander@mozilla.com + source: http://todo.com/soon + + tags: + createdForUser: {{owner}} + + workerType: b2gtest + provisionerId: aws-provisioner-v1 + schedulerId: task-graph-scheduler + + routes: + - 'index.gecko.v1.{{project}}.revision.linux.{{head_rev}}.{{build_name}}.{{build_type}}' + - 'index.gecko.v1.{{project}}.latest.linux.{{build_name}}.{{build_type}}' + + scopes: + - 'docker-worker:cache:level-{{level}}-{{project}}-tc-vcs' + + payload: + image: + type: 'task-image' + path: 'public/image.tar' + taskId: '{{#task_id_for_image}}android-gradle-build{{/task_id_for_image}}' + + command: + - bash + - /build/build.sh + + maxRunTime: 1800 + + cache: + level-{{level}}-{{project}}-tc-vcs: '/home/worker/.tc-vcs' + + artifacts: + 'public/build': + type: directory + path: '/artifacts/' + expires: '{{#from_now}}1 year{{/from_now}}' + + extra: + build_product: '{{build_product}}' + build_name: '{{build_name}}' + build_type: '{{build_type}}' + index: + rank: {{pushlog_id}} + locations: + build: null + tests: null + treeherder: + machine: + platform: lint + groupSymbol: tc + symbol: AG + treeherderEnv: + - production + - staging From c3d8dfd0626b09617bdac74e83585977865b38bf Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Tue, 2 Feb 2016 18:09:44 -0800 Subject: [PATCH 24/45] Bug 1247375 - Part 1: Add TaskCluster job building Fennec with --disable-compile-environment. r=dustin MozReview-Commit-ID: 4FDevhlYnUC --- .../android-api-15-frontend/nightly | 36 +++++++++++ .../android-frontend/releng.manifest | 26 ++++++++ .../64_api_15_frontend.py | 8 +++ .../mozharness/mozilla/building/buildbase.py | 1 + .../tasks/builds/android_api_15_frontend.yml | 60 +++++++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 mobile/android/config/mozconfigs/android-api-15-frontend/nightly create mode 100644 mobile/android/config/tooltool-manifests/android-frontend/releng.manifest create mode 100644 testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_frontend.py create mode 100644 testing/taskcluster/tasks/builds/android_api_15_frontend.yml diff --git a/mobile/android/config/mozconfigs/android-api-15-frontend/nightly b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly new file mode 100644 index 00000000000..40d055dc1fb --- /dev/null +++ b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly @@ -0,0 +1,36 @@ +# Many things aren't appropriate for a frontend-only build. +MOZ_AUTOMATION_BUILD_SYMBOLS=0 +MOZ_AUTOMATION_INSTALLER=0 +MOZ_AUTOMATION_L10N_CHECK=0 +MOZ_AUTOMATION_PACKAGE=0 +MOZ_AUTOMATION_PACKAGE_TESTS=0 +MOZ_AUTOMATION_SDK=0 +MOZ_AUTOMATION_UPDATE_PACKAGING=0 +MOZ_AUTOMATION_UPLOAD=0 +MOZ_AUTOMATION_UPLOAD_SYMBOLS=0 + +. "$topsrcdir/mobile/android/config/mozconfigs/common" + +ac_add_options --disable-compile-environment +ac_add_options --disable-tests + +# From here on, like ../android-api-15/nightly. + +ac_add_options --enable-profiling + +# Android +ac_add_options --with-android-min-sdk=15 +ac_add_options --target=arm-linux-androideabi + +ac_add_options --with-branding=mobile/android/branding/nightly + +# This will overwrite the default of stripping everything and keep the symbol table. +# This is useful for profiling with eideticker. See bug 788680 +STRIP_FLAGS="--strip-debug" + +export MOZILLA_OFFICIAL=1 +export MOZ_TELEMETRY_REPORTING=1 + +MOZ_ANDROID_GECKOLIBS_AAR=1 + +. "$topsrcdir/mobile/android/config/mozconfigs/common.override" diff --git a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest new file mode 100644 index 00000000000..454bc56ccd7 --- /dev/null +++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest @@ -0,0 +1,26 @@ +[ +{ +"size": 535625068, +"visibility": "internal", +"digest": "0627515046a23c1d109e2782865b1b3b546c1d552955e4156317f76cbb195eb630aa25feea3f4edd1c685f129da0c2a5169d4d6349c1c31d8a95158a4569a478", +"algorithm": "sha512", +"filename": "android-sdk-linux.tar.xz", +"unpack": true +}, +{ +"size": 167175, +"visibility": "public", +"digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831", +"algorithm": "sha512", +"filename": "sccache.tar.bz2", +"unpack": true +}, +{ +"size": 31013068, +"visibility": "public", +"digest": "e30a26f98a3448064857491aee1a7a26f98494f86a89113de9be17c37c8181ed60250706fed881ec1f035002fcdaf8b9b4a7d9ae70ce40acff2f1acfbb40f8d9", +"algorithm": "sha512", +"filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz", +"unpack": true +} +] diff --git a/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_frontend.py b/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_frontend.py new file mode 100644 index 00000000000..fd131edd769 --- /dev/null +++ b/testing/mozharness/configs/builds/releng_sub_android_configs/64_api_15_frontend.py @@ -0,0 +1,8 @@ +config = { + 'base_name': 'Android armv7 API 15+ frontend %(branch)s', + 'stage_platform': 'android-api-15-frontend', + 'build_type': 'api-15-opt', + 'src_mozconfig': 'mobile/android/config/mozconfigs/android-api-15-frontend/nightly', + 'tooltool_manifest_src': 'mobile/android/config/tooltool-manifests/android-frontend/releng.manifest', + 'multi_locale_config_platform': 'android', +} diff --git a/testing/mozharness/mozharness/mozilla/building/buildbase.py b/testing/mozharness/mozharness/mozilla/building/buildbase.py index bee671ccbcd..58f89e03202 100755 --- a/testing/mozharness/mozharness/mozilla/building/buildbase.py +++ b/testing/mozharness/mozharness/mozilla/building/buildbase.py @@ -357,6 +357,7 @@ class BuildOptionParser(object): 'source': 'builds/releng_sub_%s_configs/%s_source.py', 'api-9': 'builds/releng_sub_%s_configs/%s_api_9.py', 'api-11': 'builds/releng_sub_%s_configs/%s_api_11.py', + 'api-15-frontend': 'builds/releng_sub_%s_configs/%s_api_15_frontend.py', 'api-15': 'builds/releng_sub_%s_configs/%s_api_15.py', 'api-9-debug': 'builds/releng_sub_%s_configs/%s_api_9_debug.py', 'api-11-debug': 'builds/releng_sub_%s_configs/%s_api_11_debug.py', diff --git a/testing/taskcluster/tasks/builds/android_api_15_frontend.yml b/testing/taskcluster/tasks/builds/android_api_15_frontend.yml new file mode 100644 index 00000000000..7cdc5f5d590 --- /dev/null +++ b/testing/taskcluster/tasks/builds/android_api_15_frontend.yml @@ -0,0 +1,60 @@ +$inherits: + from: 'tasks/builds/mobile_base.yml' + variables: + build_name: 'android-api-15-frontend' + build_type: 'opt' +task: + metadata: + name: '[TC] Android armv7 API 15+ frontend' + description: 'Android armv7 API 15+ frontend' + + workerType: android-api-15 + + routes: + - 'index.buildbot.branches.{{project}}.android-api-15-frontend' + - 'index.buildbot.revisions.{{head_rev}}.{{project}}.android-api-15-frontend' + + scopes: + - 'docker-worker:cache:level-{{level}}-{{project}}-build-android-api-15-frontend-workspace' + - 'docker-worker:cache:tooltool-cache' + - 'docker-worker:relengapi-proxy:tooltool.download.internal' + - 'docker-worker:relengapi-proxy:tooltool.download.public' + + payload: + cache: + level-{{level}}-{{project}}-build-android-api-15-frontend-workspace: '/home/worker/workspace' + tooltool-cache: '/home/worker/tooltool-cache' + + features: + relengAPIProxy: true + + env: + # inputs to mozharness + MOZHARNESS_SCRIPT: 'mozharness/scripts/fx_desktop_build.py' + # TODO: make these additional configuration files go away + MOZHARNESS_CONFIG: > + builds/releng_base_android_64_builds.py + disable_signing.py + platform_supports_post_upload_to_latest.py + MH_CUSTOM_BUILD_VARIANT_CFG: api-15-frontend + MH_BRANCH: {{project}} + MH_BUILD_POOL: taskcluster + + maxRunTime: 36000 + + command: ["/bin/bash", "bin/build.sh"] + + extra: + treeherderEnv: + - production + - staging + treeherder: + machine: + # see https://github.com/mozilla/treeherder/blob/master/ui/js/values.js + platform: android-4-0-armv7-api15-frontend + # Rather then enforcing particular conventions we require that all build + # tasks provide the "build" extra field to specify where the build and tests + # files are located. + locations: + build: 'public/build/target.linux-x86_64.tar.bz2' + tests: 'public/build/target.tests.zip' From 2c22b05675d510b89bb77bcc6decdea7d17d3825 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Wed, 10 Feb 2016 19:29:27 -0800 Subject: [PATCH 25/45] Bug 1247375 - Part 2: Add build system Gradle defines for Fennec. r=glandium Enable building with Gradle using --with-gradle. Configure the location of Gradle with --with-gradle=/path/to/gradle. For local developers, this is always the in tree Gradle wrapper, which downloads and installs the correct Gradle version automatically. In automation, this will be a version of Gradle fetched from tooltool. Configure the location to use to download Gradle Maven dependencies (including the Android-Gradle plugin) by setting GRADLE_MAVEN_REPOSITORY in your mozconfig. For local developers, this defaults to the jcenter repository. In automation, this will be an archived directory fetched from tooltool. Android-specific Maven dependencies are shipped as "extras" with the Android SDK, and should be found automatically by the Android-Gradle plugin. MozReview-Commit-ID: Hrkn88Vig5H --- build.gradle | 9 ++++-- configure.in | 54 ++++++++++++++++++++++++++++++--- mobile/android/mach_commands.py | 2 +- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 94d92b8f55b..68922a84930 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,9 @@ allprojects { } repositories { - jcenter() + maven { + url gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY + } } } @@ -17,8 +19,9 @@ buildDir "${topobjdir}/gradle/build" buildscript { repositories { - jcenter() - + maven { + url gradle.mozconfig.substs.GRADLE_MAVEN_REPOSITORY + } } dependencies { diff --git a/configure.in b/configure.in index 1b6b5fb6e13..00c1b910885 100644 --- a/configure.in +++ b/configure.in @@ -5374,12 +5374,56 @@ if test -n "$MOZ_OMX_PLUGIN"; then fi dnl ======================================================== -dnl = Enable building mobile/android with Gradle +dnl Gradle support +dnl +dnl If --with-gradle is specified, build mobile/android with Gradle. +dnl +dnl If no Gradle binary is specified, use the in tree Gradle wrapper. +dnl The wrapper downloads and installs Gradle, which is good for local +dnl developers but not good in automation. dnl ======================================================== -MOZ_ARG_ENABLE_BOOL(gradle-mobile-android-builds, -[ --enable-gradle-mobile-android-builds Enable building mobile/android with Gradle], - MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=1, - MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=) + +GRADLE= +MOZ_ARG_WITH_STRING(gradle, +[ --with-gradle=/path/to/bin/gradle + Enable building mobile/android with Gradle (argument: location of binary or wrapper (gradle/gradlew))], + if test "$withval" = "no" ; then + dnl --without-gradle => use the wrapper in |mach gradle|, don't build + dnl with Gradle by default. + GRADLE=$srcdir/gradlew + MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE= + elif test "$withval" = "yes" ; then + dnl --with-gradle => use the wrapper in |mach gradle|, build with + dnl Gradle by default. + GRADLE=$srcdir/gradlew + MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=1 + else + dnl --with-gradle=/path/to/gradle => use the given binary in |mach + dnl gradle|, build with Gradle by default. + GRADLE=$withval + MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE=1 + fi + , + dnl No --with{out}-gradle => use the wrapper in |mach gradle|, don't build + dnl with Gradle by default. + GRADLE=$srcdir/gradlew + MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE= + ) + +if test "$OS_TARGET" = Android -a x"$MOZ_WIDGET_TOOLKIT" != x"gonk" ; then + if test -z "$GRADLE" -o ! -x "$GRADLE" ; then + AC_MSG_ERROR([The program gradlew/gradle was not found. Use --with-gradle=/path/to/bin/gradle}]) + fi +fi +AC_SUBST(GRADLE) + +dnl Path to Maven repository containing Gradle dependencies. Automation will +dnl set this to file:///path/to/local via the mozconfig. Local developer +dnl default is jcenter. +if test -z "$GRADLE_MAVEN_REPOSITORY" ; then + GRADLE_MAVEN_REPOSITORY=https://jcenter.bintray.com/ +fi +AC_SUBST(GRADLE_MAVEN_REPOSITORY) if test -n "$MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE"; then if test "$OS_TARGET" = "Android" -a x"$MOZ_WIDGET_TOOLKIT" != x"gonk"; then diff --git a/mobile/android/mach_commands.py b/mobile/android/mach_commands.py index 11cafbdcbbb..108a03cb727 100644 --- a/mobile/android/mach_commands.py +++ b/mobile/android/mach_commands.py @@ -57,7 +57,7 @@ class MachCommands(MachCommandBase): # Avoid logging the command self.log_manager.terminal_handler.setLevel(logging.CRITICAL) - return self.run_process(['./gradlew'] + args, + return self.run_process([self.substs['GRADLE']] + args, pass_thru=True, # Allow user to run gradle interactively. ensure_exit_code=False, # Don't throw on non-zero exit code. cwd=mozpath.join(self.topsrcdir)) From 133c3b89c5aba08b2bd0122211a66b5cad479608 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Fri, 12 Feb 2016 10:07:11 -0800 Subject: [PATCH 26/45] Bug 1247375 - Part 3: Use Gradle to build Fennec with --disable-compile-environment. r=glandium MozReview-Commit-ID: 3oolQKHxkBs --- .../mozconfigs/android-api-15-frontend/nightly | 3 +++ .../android-frontend/releng.manifest | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/mobile/android/config/mozconfigs/android-api-15-frontend/nightly b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly index 40d055dc1fb..fdff461d200 100644 --- a/mobile/android/config/mozconfigs/android-api-15-frontend/nightly +++ b/mobile/android/config/mozconfigs/android-api-15-frontend/nightly @@ -11,6 +11,9 @@ MOZ_AUTOMATION_UPLOAD_SYMBOLS=0 . "$topsrcdir/mobile/android/config/mozconfigs/common" +ac_add_options --with-gradle="$topsrcdir/gradle/bin/gradle" +export GRADLE_MAVEN_REPOSITORY="file://$topsrcdir/jcentral" + ac_add_options --disable-compile-environment ac_add_options --disable-tests diff --git a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest index 454bc56ccd7..72f8bb970a6 100644 --- a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest +++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest @@ -22,5 +22,21 @@ "algorithm": "sha512", "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz", "unpack": true +}, +{ +"algorithm": "sha512", +"visibility": "public", +"filename": "jcentral.tar.xz", +"unpack": true, +"digest": "b5d85a917785e1c034318f7495fef27a6274b04d8640245726b0cf1331b7ac374f5757868901c3fadd930bf10603173a706be653d769dde8ddfdb8673b143363", +"size": 38596168 +}, +{ +"algorithm": "sha512", +"visibility": "public", +"filename": "gradle.tar.xz", +"unpack": true, +"digest": "ef1d0038da879cc6840fced87671f8f6a18c51375498804f64d21fa48d7089ded4da2be36bd06a1457083e9110e59c0884f1e074dc609d29617c131caea8f234", +"size": 50542140 } ] From 88693264ac7760ac52317d7e12c6be967fb16e23 Mon Sep 17 00:00:00 2001 From: Tim Taubert Date: Thu, 11 Feb 2016 17:14:29 +0100 Subject: [PATCH 27/45] Bug 529899 - Purge cookies on clean shutdown with "Keep cookies until I close Firefox" r=Yoric --- browser/components/sessionstore/SessionSaver.jsm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/browser/components/sessionstore/SessionSaver.jsm b/browser/components/sessionstore/SessionSaver.jsm index 98c2ca12ad2..02149c966ff 100644 --- a/browser/components/sessionstore/SessionSaver.jsm +++ b/browser/components/sessionstore/SessionSaver.jsm @@ -21,6 +21,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/Console.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter", "resource:///modules/sessionstore/PrivacyFilter.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "RunState", + "resource:///modules/sessionstore/RunState.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", "resource:///modules/sessionstore/SessionStore.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "SessionFile", @@ -209,6 +211,19 @@ var SessionSaverInternal = { } } + // If this is the final write on a clean shutdown, and the user changed + // their cookie preferences to "Keep until I close Firefox", then we + // should remove all cookies. Check "resume_session_once" so we keep + // cookies when restarting due to a Firefox update. + if (RunState.isClosing && + Services.prefs.getIntPref("network.cookie.lifetimePolicy") == + Services.cookies.ACCEPT_SESSION && + !Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) { + for (let window of state.windows) { + delete window.cookies; + } + } + stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS"); return this._writeState(state); }, From 46ff855561c9291a73fed053ca9e528ae612d1e2 Mon Sep 17 00:00:00 2001 From: Lin Clark Date: Tue, 16 Feb 2016 14:37:45 -0800 Subject: [PATCH 28/45] Bug 1241707 - [e10s] Web Console should restore focus to proper place when closed. r=jryans,r=bgrins MozReview-Commit-ID: JlcJPGygm5F --- devtools/client/webconsole/hudservice.js | 8 ++++---- devtools/server/actors/webbrowser.js | 11 +++++++++++ devtools/shared/client/main.js | 7 +++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/devtools/client/webconsole/hudservice.js b/devtools/client/webconsole/hudservice.js index 34828bc629a..3dd2f138535 100644 --- a/devtools/client/webconsole/hudservice.js +++ b/devtools/client/webconsole/hudservice.js @@ -12,6 +12,7 @@ var WebConsoleUtils = require("devtools/shared/webconsole/utils").Utils; var Heritage = require("sdk/core/heritage"); var {TargetFactory} = require("devtools/client/framework/target"); var {Tools} = require("devtools/client/definitions"); +const { Task } = require("resource://gre/modules/Task.jsm"); var promise = require("promise"); loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm"); @@ -587,10 +588,9 @@ WebConsole.prototype = { } } - let onDestroy = function WC_onDestroyUI() { + let onDestroy = Task.async(function*() { try { - let tabWindow = this.target.isLocalTab ? this.target.window : null; - tabWindow && tabWindow.focus(); + yield this.target.activeTab.focus() } catch (ex) { // Tab focus can fail if the tab or target is closed. @@ -599,7 +599,7 @@ WebConsole.prototype = { let id = WebConsoleUtils.supportsString(this.hudId); Services.obs.notifyObservers(id, "web-console-destroyed", null); this._destroyer.resolve(null); - }.bind(this); + }.bind(this)); if (this.ui) { this.ui.destroy().then(onDestroy); diff --git a/devtools/server/actors/webbrowser.js b/devtools/server/actors/webbrowser.js index 8d309eaed51..196ea8c6757 100644 --- a/devtools/server/actors/webbrowser.js +++ b/devtools/server/actors/webbrowser.js @@ -1399,6 +1399,16 @@ TabActor.prototype = { return { type: "detached" }; }, + /** + * Bring the tab's window to front. + */ + onFocus: function() { + if (this.window) { + this.window.focus(); + } + return {}; + }, + /** * Reload the page in this tab. */ @@ -1900,6 +1910,7 @@ TabActor.prototype = { TabActor.prototype.requestTypes = { "attach": TabActor.prototype.onAttach, "detach": TabActor.prototype.onDetach, + "focus": TabActor.prototype.onFocus, "reload": TabActor.prototype.onReload, "navigateTo": TabActor.prototype.onNavigateTo, "reconfigure": TabActor.prototype.onReconfigure, diff --git a/devtools/shared/client/main.js b/devtools/shared/client/main.js index bad301d3805..03fbda5d933 100644 --- a/devtools/shared/client/main.js +++ b/devtools/shared/client/main.js @@ -1315,6 +1315,13 @@ TabClient.prototype = { telemetry: "TABDETACH" }), + /** + * Bring the window to the front. + */ + focus: DebuggerClient.requester({ + type: "focus" + }, {}), + /** * Reload the page in this tab. * From 8f4413a55a786f91f6ffac2562a251203d5b9f75 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Thu, 7 Jan 2016 13:15:55 -0700 Subject: [PATCH 29/45] Bug 1224785, Part 1 - Implement alert favicons backend. r=wchen MozReview-Commit-ID: Day8Hj8lT1f --- .../components/alerts/AlertNotification.cpp | 10 ++ toolkit/components/alerts/nsAlertsService.cpp | 114 +++++++++++++++++- .../components/alerts/nsIAlertsService.idl | 36 +++++- 3 files changed, 156 insertions(+), 4 deletions(-) diff --git a/toolkit/components/alerts/AlertNotification.cpp b/toolkit/components/alerts/AlertNotification.cpp index ef443f714a8..b72e5746adb 100644 --- a/toolkit/components/alerts/AlertNotification.cpp +++ b/toolkit/components/alerts/AlertNotification.cpp @@ -117,6 +117,16 @@ AlertNotification::GetPrincipal(nsIPrincipal** aPrincipal) return NS_OK; } +NS_IMETHODIMP +AlertNotification::GetURI(nsIURI** aURI) +{ + if (!nsAlertsUtils::IsActionablePrincipal(mPrincipal)) { + *aURI = nullptr; + return NS_OK; + } + return mPrincipal->GetURI(aURI); +} + NS_IMETHODIMP AlertNotification::GetInPrivateBrowsing(bool* aInPrivateBrowsing) { diff --git a/toolkit/components/alerts/nsAlertsService.cpp b/toolkit/components/alerts/nsAlertsService.cpp index d6765770ecb..345f6075406 100644 --- a/toolkit/components/alerts/nsAlertsService.cpp +++ b/toolkit/components/alerts/nsAlertsService.cpp @@ -22,10 +22,118 @@ #endif // !MOZ_WIDGET_ANDROID +#ifdef MOZ_PLACES +#include "mozIAsyncFavicons.h" +#include "nsIFaviconService.h" +#endif // MOZ_PLACES + using namespace mozilla; using mozilla::dom::ContentChild; +namespace { + +#ifdef MOZ_PLACES + +class IconCallback final : public nsIFaviconDataCallback +{ +public: + NS_DECL_ISUPPORTS + + IconCallback(nsIAlertsService* aBackend, + nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) + : mBackend(aBackend) + , mAlert(aAlert) + , mAlertListener(aAlertListener) + {} + + NS_IMETHOD + OnComplete(nsIURI *aIconURI, uint32_t aIconSize, const uint8_t *aIconData, + const nsACString &aMimeType) override + { + nsresult rv = NS_ERROR_FAILURE; + if (aIconSize > 0) { + nsCOMPtr alertsIconData(do_QueryInterface(mBackend)); + if (alertsIconData) { + rv = alertsIconData->ShowAlertWithIconData(mAlert, mAlertListener, + aIconSize, aIconData); + } + } else if (aIconURI) { + nsCOMPtr alertsIconURI(do_QueryInterface(mBackend)); + if (alertsIconURI) { + rv = alertsIconURI->ShowAlertWithIconURI(mAlert, mAlertListener, + aIconURI); + } + } + if (NS_FAILED(rv)) { + rv = mBackend->ShowAlert(mAlert, mAlertListener); + } + return rv; + } + +private: + virtual ~IconCallback() {} + + nsCOMPtr mBackend; + nsCOMPtr mAlert; + nsCOMPtr mAlertListener; +}; + +NS_IMPL_ISUPPORTS(IconCallback, nsIFaviconDataCallback) + +#endif // MOZ_PLACES + +nsresult +ShowWithIconBackend(nsIAlertsService* aBackend, nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) +{ +#ifdef MOZ_PLACES + nsCOMPtr uri; + nsresult rv = aAlert->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv) || !uri) { + return NS_ERROR_FAILURE; + } + + // Ensure the backend supports favicons. + nsCOMPtr alertsIconData(do_QueryInterface(aBackend)); + nsCOMPtr alertsIconURI; + if (!alertsIconData) { + alertsIconURI = do_QueryInterface(aBackend); + } + if (!alertsIconData && !alertsIconURI) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + nsCOMPtr favicons(do_GetService( + "@mozilla.org/browser/favicon-service;1")); + NS_ENSURE_TRUE(favicons, NS_ERROR_FAILURE); + + nsCOMPtr callback = + new IconCallback(aBackend, aAlert, aAlertListener); + if (alertsIconData) { + return favicons->GetFaviconDataForPage(uri, callback); + } + return favicons->GetFaviconURLForPage(uri, callback); +#else + return NS_ERROR_NOT_IMPLEMENTED; +#endif // !MOZ_PLACES +} + +nsresult +ShowWithBackend(nsIAlertsService* aBackend, nsIAlertNotification* aAlert, + nsIObserver* aAlertListener) +{ + nsresult rv = ShowWithIconBackend(aBackend, aAlert, aAlertListener); + if (NS_SUCCEEDED(rv)) { + return rv; + } + // If the backend doesn't support favicons, show the alert without one. + return aBackend->ShowAlert(aAlert, aAlertListener); +} + +} // anonymous namespace + NS_IMPL_ISUPPORTS(nsAlertsService, nsIAlertsService, nsIAlertsDoNotDisturb, nsIAlertsProgressListener) nsAlertsService::nsAlertsService() : @@ -66,7 +174,7 @@ bool nsAlertsService::ShouldShowAlert() return result; } -NS_IMETHODIMP nsAlertsService::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle, +NS_IMETHODIMP nsAlertsService::ShowAlertNotification(const nsAString & aImageUrl, const nsAString & aAlertTitle, const nsAString & aAlertText, bool aAlertTextClickable, const nsAString & aAlertCookie, nsIObserver * aAlertListener, @@ -135,7 +243,7 @@ NS_IMETHODIMP nsAlertsService::ShowAlert(nsIAlertNotification * aAlert, #else // Check if there is an optional service that handles system-level notifications if (mBackend) { - rv = mBackend->ShowAlert(aAlert, aAlertListener); + rv = ShowWithBackend(mBackend, aAlert, aAlertListener); if (NS_SUCCEEDED(rv)) { return rv; } @@ -154,7 +262,7 @@ NS_IMETHODIMP nsAlertsService::ShowAlert(nsIAlertNotification * aAlert, // Use XUL notifications as a fallback if above methods have failed. nsCOMPtr xulBackend(nsXULAlerts::GetInstance()); NS_ENSURE_TRUE(xulBackend, NS_ERROR_FAILURE); - return xulBackend->ShowAlert(aAlert, aAlertListener); + return ShowWithBackend(xulBackend, aAlert, aAlertListener); #endif // !MOZ_WIDGET_ANDROID } diff --git a/toolkit/components/alerts/nsIAlertsService.idl b/toolkit/components/alerts/nsIAlertsService.idl index f11848f566e..d34061ff21b 100644 --- a/toolkit/components/alerts/nsIAlertsService.idl +++ b/toolkit/components/alerts/nsIAlertsService.idl @@ -7,12 +7,13 @@ #include "nsIObserver.idl" interface nsIPrincipal; +interface nsIURI; %{C++ #define ALERT_NOTIFICATION_CONTRACTID "@mozilla.org/alert-notification;1" %} -[scriptable, uuid(9e87fc34-8dbb-4b14-a724-b27be6822ec8)] +[scriptable, uuid(1650a064-79d5-4eb6-8c9e-57dd6522b6ac)] interface nsIAlertNotification : nsISupports { /** Initializes an alert notification. */ @@ -85,6 +86,12 @@ interface nsIAlertNotification : nsISupports */ readonly attribute nsIPrincipal principal; + /** + * The URI of the page that created the alert. |null| if the alert is not + * actionable. + */ + readonly attribute nsIURI URI; + /** * Controls the image loading behavior. If true, the image URL will be loaded * in private browsing mode. @@ -224,3 +231,30 @@ interface nsIAlertsProgressListener : nsISupports void onCancel(in AString name); }; +[scriptable, uuid(fc6d7f0a-0cf6-4268-8c71-ab640842b9b1)] +interface nsIAlertsIconData : nsISupports +{ + /** + * Shows an alert with an icon. Web notifications use the favicon of the + * page that created the alert. If the favicon is not in the Places database, + * |iconSize| will be zero. + */ + void showAlertWithIconData(in nsIAlertNotification alert, + [optional] in nsIObserver alertListener, + [optional] in uint32_t iconSize, + [const, array, size_is(iconSize)] in uint8_t + iconData); +}; + +[scriptable, uuid(f3c82915-bf60-41ea-91ce-6c46b22e381a)] +interface nsIAlertsIconURI : nsISupports +{ + /** + * Shows an alert with an icon URI. Web notifications use |moz-anno:| + * URIs to reference favicons from Places. If the page doesn't have a + * favicon, |iconURI| will be |null|. + */ + void showAlertWithIconURI(in nsIAlertNotification alert, + [optional] in nsIObserver alertListener, + [optional] in nsIURI iconURI); +}; From c3bb67a66fb438ee3bdfe3a2cbf2f0945961e25e Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Thu, 7 Jan 2016 13:16:54 -0700 Subject: [PATCH 30/45] Bug 1224785, Part 2 - Show the site favicon in OS X notifications. r=mstange MozReview-Commit-ID: L7d00xEY6Ka --- widget/cocoa/OSXNotificationCenter.h | 4 +++- widget/cocoa/OSXNotificationCenter.mm | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/widget/cocoa/OSXNotificationCenter.h b/widget/cocoa/OSXNotificationCenter.h index 3ae5df5b448..f56ddd2762e 100644 --- a/widget/cocoa/OSXNotificationCenter.h +++ b/widget/cocoa/OSXNotificationCenter.h @@ -25,13 +25,15 @@ class OSXNotificationInfo; class OSXNotificationCenter : public nsIAlertsService, public imgINotificationObserver, - public nsITimerCallback + public nsITimerCallback, + public nsIAlertsIconData { public: NS_DECL_ISUPPORTS NS_DECL_NSIALERTSSERVICE NS_DECL_IMGINOTIFICATIONOBSERVER NS_DECL_NSITIMERCALLBACK + NS_DECL_NSIALERTSICONDATA OSXNotificationCenter(); diff --git a/widget/cocoa/OSXNotificationCenter.mm b/widget/cocoa/OSXNotificationCenter.mm index 54e51886f62..2fee40b40d6 100644 --- a/widget/cocoa/OSXNotificationCenter.mm +++ b/widget/cocoa/OSXNotificationCenter.mm @@ -218,7 +218,8 @@ OSXNotificationCenter::~OSXNotificationCenter() NS_OBJC_END_TRY_ABORT_BLOCK; } -NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, imgINotificationObserver, nsITimerCallback) +NS_IMPL_ISUPPORTS(OSXNotificationCenter, nsIAlertsService, nsITimerCallback, + imgINotificationObserver, nsIAlertsIconData) nsresult OSXNotificationCenter::Init() { @@ -255,6 +256,15 @@ OSXNotificationCenter::ShowAlertNotification(const nsAString & aImageUrl, const NS_IMETHODIMP OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert, nsIObserver* aAlertListener) +{ + return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr); +} + +NS_IMETHODIMP +OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert, + nsIObserver* aAlertListener, + uint32_t aIconSize, + const uint8_t* aIconData) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; @@ -348,6 +358,18 @@ OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert, OSXNotificationInfo *osxni = new OSXNotificationInfo(alertName, aAlertListener, cookie); + // Show the favicon if supported on this version of OS X. + if (aIconSize > 0 && + [notification respondsToSelector:@selector(set_identityImage:)] && + [notification respondsToSelector:@selector(set_identityImageHasBorder:)]) { + + NSData *iconData = [NSData dataWithBytes:aIconData length:aIconSize]; + NSImage *icon = [[[NSImage alloc] initWithData:iconData] autorelease]; + + [(NSObject*)notification setValue:icon forKey:@"_identityImage"]; + [(NSObject*)notification setValue:@(NO) forKey:@"_identityImageHasBorder"]; + } + nsAutoString imageUrl; rv = aAlert->GetImageURL(imageUrl); NS_ENSURE_SUCCESS(rv, rv); From c2e5068f987d0fbb9b1e6daa5dc362f759ec9510 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Fri, 8 Jan 2016 12:53:34 -0700 Subject: [PATCH 31/45] Bug 1206560 - Show the site favicon in XUL notifications. r=jaws MozReview-Commit-ID: Fe3R91icOuc --- .../test/alerts/browser_notification_close.js | 2 ++ .../test/alerts/file_dom_notifications.html | 1 + toolkit/components/alerts/nsXULAlerts.cpp | 21 ++++++++++++++++++- toolkit/components/alerts/nsXULAlerts.h | 4 +++- .../alerts/resources/content/alert.css | 6 ++++++ .../alerts/resources/content/alert.js | 10 +++++++++ .../alerts/resources/content/alert.xul | 1 + toolkit/themes/shared/alert-common.css | 16 ++++++++++++++ 8 files changed, 59 insertions(+), 2 deletions(-) diff --git a/browser/base/content/test/alerts/browser_notification_close.js b/browser/base/content/test/alerts/browser_notification_close.js index 82e1f314fac..3d4660dbb4a 100644 --- a/browser/base/content/test/alerts/browser_notification_close.js +++ b/browser/base/content/test/alerts/browser_notification_close.js @@ -29,6 +29,8 @@ add_task(function* test_notificationClose() { is(alertTitleLabel.value, "Test title", "Title text of notification should be present"); let alertTextLabel = alertWindow.document.getElementById("alertTextLabel"); is(alertTextLabel.textContent, "Test body", "Body text of notification should be present"); + let alertIcon = alertWindow.document.getElementById("alertIcon"); + ok(alertIcon.src, "Icon of notification should be present"); let alertCloseButton = alertWindow.document.querySelector(".alertCloseButton"); is(alertCloseButton.localName, "toolbarbutton", "close button found"); diff --git a/browser/base/content/test/alerts/file_dom_notifications.html b/browser/base/content/test/alerts/file_dom_notifications.html index 8f5c6f2fd57..e5a2be2d3f4 100644 --- a/browser/base/content/test/alerts/file_dom_notifications.html +++ b/browser/base/content/test/alerts/file_dom_notifications.html @@ -1,6 +1,7 @@ + diff --git a/devtools/client/shared/widgets/graphs-frame.xhtml b/devtools/client/shared/widgets/graphs-frame.xhtml index 6e951982fb0..8c6f45e03ca 100644 --- a/devtools/client/shared/widgets/graphs-frame.xhtml +++ b/devtools/client/shared/widgets/graphs-frame.xhtml @@ -7,7 +7,6 @@ -