mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
merge fx-team to mozilla-central a=merge
This commit is contained in:
commit
f4dd0b592e
@ -3,6 +3,10 @@
|
||||
"plugins": [
|
||||
"mozilla"
|
||||
],
|
||||
"rules": {
|
||||
"mozilla/components-imports": 1,
|
||||
"mozilla/import-globals-from": 1,
|
||||
},
|
||||
"env": {
|
||||
"es6": true
|
||||
},
|
||||
|
@ -537,7 +537,7 @@ SocialShare = {
|
||||
populateProviderMenu: function() {
|
||||
if (!this.iframe)
|
||||
return;
|
||||
let providers = [p for (p of Social.providers) if (p.shareURL)];
|
||||
let providers = Social.providers.filter(p => p.shareURL);
|
||||
let hbox = document.getElementById("social-share-provider-buttons");
|
||||
// remove everything before the add-share-provider button (which should also
|
||||
// be lastChild if any share providers were added)
|
||||
@ -976,7 +976,7 @@ SocialSidebar = {
|
||||
// first, otherwise fallback to the first provider in the list
|
||||
let sbrowser = document.getElementById("social-sidebar-browser");
|
||||
let origin = sbrowser.getAttribute("origin");
|
||||
let providers = [p for (p of Social.providers) if (p.sidebarURL)];
|
||||
let providers = Social.providers.filter(p => p.sidebarURL);
|
||||
let provider;
|
||||
if (origin)
|
||||
provider = Social._getProviderFromOrigin(origin);
|
||||
@ -1093,7 +1093,7 @@ SocialSidebar = {
|
||||
menu.removeChild(providerMenuSep.previousSibling);
|
||||
}
|
||||
// only show a selection in the sidebar header menu if there is more than one
|
||||
let providers = [p for (p of Social.providers) if (p.sidebarURL)];
|
||||
let providers = Social.providers.filter(p => p.sidebarURL);
|
||||
if (providers.length < 2 && menu.id != "viewSidebarMenu") {
|
||||
providerMenuSep.hidden = true;
|
||||
return;
|
||||
@ -1153,7 +1153,9 @@ ToolbarHelper.prototype = {
|
||||
},
|
||||
|
||||
clearPalette: function() {
|
||||
[this.removeProviderButton(p.origin) for (p of Social.providers)];
|
||||
for (let p of Social.providers) {
|
||||
this.removeProviderButton(p.origin);
|
||||
}
|
||||
},
|
||||
|
||||
// should be called on enable of a provider
|
||||
@ -1320,8 +1322,7 @@ var SocialMarksWidgetListener = {
|
||||
*/
|
||||
SocialMarks = {
|
||||
get nodes() {
|
||||
let providers = [p for (p of Social.providers) if (p.markURL)];
|
||||
for (let p of providers) {
|
||||
for (let p of Social.providers.filter(p => p.markURL)) {
|
||||
let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin);
|
||||
let widget = CustomizableUI.getWidget(widgetId);
|
||||
if (!widget)
|
||||
@ -1348,8 +1349,8 @@ SocialMarks = {
|
||||
// also means that populateToolbarPalette must be called prior to using this
|
||||
// method, otherwise you get a big fat zero. For our use case with context
|
||||
// menu's, this is ok.
|
||||
return [p for (p of Social.providers) if (p.markURL &&
|
||||
document.getElementById(this._toolbarHelper.idFromOrigin(p.origin)))];
|
||||
return Social.providers.filter(p => p.markURL &&
|
||||
document.getElementById(this._toolbarHelper.idFromOrigin(p.origin)));
|
||||
},
|
||||
|
||||
populateContextMenu: function() {
|
||||
@ -1357,8 +1358,10 @@ SocialMarks = {
|
||||
let providers = this.getProviders();
|
||||
|
||||
// remove all previous entries by class
|
||||
let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))];
|
||||
[m.parentNode.removeChild(m) for (m of menus)];
|
||||
let menus = [...document.getElementsByClassName("context-socialmarks")];
|
||||
for (let m of menus) {
|
||||
m.parentNode.removeChild(m);
|
||||
}
|
||||
|
||||
let contextMenus = [
|
||||
{
|
||||
|
5
browser/base/content/test/alerts/.eslintrc
Normal file
5
browser/base/content/test/alerts/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/base/content/test/chat/.eslintrc
Normal file
5
browser/base/content/test/chat/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/base/content/test/chrome/.eslintrc
Normal file
5
browser/base/content/test/chrome/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/chrome.eslintrc"
|
||||
]
|
||||
}
|
6
browser/base/content/test/general/.eslintrc
Normal file
6
browser/base/content/test/general/.eslintrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc",
|
||||
"../../../../../testing/mochitest/mochitest.eslintrc",
|
||||
]
|
||||
}
|
5
browser/base/content/test/newtab/.eslintrc
Normal file
5
browser/base/content/test/newtab/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/base/content/test/plugins/.eslintrc
Normal file
5
browser/base/content/test/plugins/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/base/content/test/popupNotifications/.eslintrc
Normal file
5
browser/base/content/test/popupNotifications/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/base/content/test/referrer/.eslintrc
Normal file
5
browser/base/content/test/referrer/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/base/content/test/social/.eslintrc
Normal file
5
browser/base/content/test/social/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
@ -5,7 +5,7 @@
|
||||
browser.jar:
|
||||
content/browser/customizableui/aboutCustomizing.xul
|
||||
content/browser/customizableui/panelUI.css
|
||||
* content/browser/customizableui/panelUI.js
|
||||
content/browser/customizableui/panelUI.js
|
||||
content/browser/customizableui/panelUI.xml
|
||||
content/browser/customizableui/toolbar.xml
|
||||
|
||||
|
@ -10,6 +10,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
|
||||
"resource://gre/modules/ShortcutUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm");
|
||||
|
||||
/**
|
||||
* Maintains the state and dispatches events for the main menu panel.
|
||||
@ -488,12 +490,14 @@ const PanelUI = {
|
||||
},
|
||||
|
||||
_updateQuitTooltip: function() {
|
||||
#ifndef XP_WIN
|
||||
#ifdef XP_MACOSX
|
||||
let tooltipId = "quit-button.tooltiptext.mac";
|
||||
#else
|
||||
let tooltipId = "quit-button.tooltiptext.linux2";
|
||||
#endif
|
||||
if (AppConstants.platform == "win") {
|
||||
return;
|
||||
}
|
||||
|
||||
let tooltipId = AppConstants.platform == "macosx" ?
|
||||
"quit-button.tooltiptext.mac" :
|
||||
"quit-button.tooltiptext.linux2";
|
||||
|
||||
let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties");
|
||||
let stringArgs = [brands.GetStringFromName("brandShortName")];
|
||||
|
||||
@ -502,7 +506,6 @@ const PanelUI = {
|
||||
let tooltipString = CustomizableUI.getLocalizedProperty({x: tooltipId}, "x", stringArgs);
|
||||
let quitButton = document.getElementById("PanelUI-quit");
|
||||
quitButton.setAttribute("tooltiptext", tooltipString);
|
||||
#endif
|
||||
},
|
||||
|
||||
_overlayScrollListenerBoundFn: null,
|
||||
|
5
browser/components/customizableui/test/.eslintrc
Normal file
5
browser/components/customizableui/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/dirprovider/tests/unit/.eslintrc
Normal file
5
browser/components/dirprovider/tests/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
@ -471,11 +471,7 @@ const DownloadsPanel = {
|
||||
}
|
||||
|
||||
let pasting = aEvent.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_V &&
|
||||
#ifdef XP_MACOSX
|
||||
aEvent.metaKey;
|
||||
#else
|
||||
aEvent.ctrlKey;
|
||||
#endif
|
||||
aEvent.getModifierState("Accel");
|
||||
|
||||
if (!pasting) {
|
||||
return;
|
||||
|
@ -6,7 +6,7 @@ browser.jar:
|
||||
* content/browser/downloads/download.xml (content/download.xml)
|
||||
content/browser/downloads/download.css (content/download.css)
|
||||
content/browser/downloads/downloads.css (content/downloads.css)
|
||||
* content/browser/downloads/downloads.js (content/downloads.js)
|
||||
content/browser/downloads/downloads.js (content/downloads.js)
|
||||
* content/browser/downloads/downloadsOverlay.xul (content/downloadsOverlay.xul)
|
||||
content/browser/downloads/indicator.js (content/indicator.js)
|
||||
content/browser/downloads/indicatorOverlay.xul (content/indicatorOverlay.xul)
|
||||
|
5
browser/components/downloads/test/browser/.eslintrc
Normal file
5
browser/components/downloads/test/browser/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/downloads/test/unit/.eslintrc
Normal file
5
browser/components/downloads/test/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/feeds/test/.eslintrc
Normal file
5
browser/components/feeds/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/mochitest.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/feeds/test/chrome/.eslintrc
Normal file
5
browser/components/feeds/test/chrome/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/chrome.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/feeds/test/unit/.eslintrc
Normal file
5
browser/components/feeds/test/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/migration/tests/unit/.eslintrc
Normal file
5
browser/components/migration/tests/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/newtab/tests/browser/.eslintrc
Normal file
5
browser/components/newtab/tests/browser/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/newtab/tests/xpcshell/.eslintrc
Normal file
5
browser/components/newtab/tests/xpcshell/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/places/tests/browser/.eslintrc
Normal file
5
browser/components/places/tests/browser/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/places/tests/chrome/.eslintrc
Normal file
5
browser/components/places/tests/chrome/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/chrome.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/places/tests/unit/.eslintrc
Normal file
5
browser/components/places/tests/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
@ -379,19 +379,18 @@ var gMainPane = {
|
||||
// We should only include visible & non-pinned tabs
|
||||
|
||||
tabs = win.gBrowser.visibleTabs.slice(win.gBrowser._numPinnedTabs);
|
||||
|
||||
tabs = tabs.filter(this.isNotAboutPreferences);
|
||||
}
|
||||
|
||||
|
||||
return tabs;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Check to see if a tab is not about:preferences
|
||||
*/
|
||||
isNotAboutPreferences: function (aElement, aIndex, aArray)
|
||||
{
|
||||
return (aElement.linkedBrowser.currentURI.spec.startsWith != "about:preferences");
|
||||
return !aElement.linkedBrowser.currentURI.spec.startsWith("about:preferences");
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
@ -19,6 +19,7 @@ skip-if = os != "win" # This test tests the windows-specific app selection dialo
|
||||
[browser_cookies_exceptions.js]
|
||||
[browser_healthreport.js]
|
||||
skip-if = true || !healthreport || (os == 'linux' && debug) # Bug 1185403 for the "true"
|
||||
[browser_homepages_filter_aboutpreferences.js]
|
||||
[browser_notifications_do_not_disturb.js]
|
||||
[browser_permissions_urlFieldHidden.js]
|
||||
[browser_proxy_backup.js]
|
||||
|
@ -0,0 +1,20 @@
|
||||
add_task(function() {
|
||||
is(gBrowser.currentURI.spec, "about:blank", "Test starts with about:blank open");
|
||||
yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:home");
|
||||
yield openPreferencesViaOpenPreferencesAPI("paneGeneral", null, {leaveOpen: true});
|
||||
let doc = gBrowser.contentDocument;
|
||||
is(gBrowser.currentURI.spec, "about:preferences#general",
|
||||
"#general should be in the URI for about:preferences");
|
||||
let oldHomepagePref = Services.prefs.getCharPref("browser.startup.homepage");
|
||||
|
||||
let useCurrent = doc.getElementById("useCurrent");
|
||||
useCurrent.click();
|
||||
|
||||
is(gBrowser.tabs.length, 3, "Three tabs should be open");
|
||||
is(Services.prefs.getCharPref("browser.startup.homepage"), "about:blank|about:home",
|
||||
"about:blank and about:home should be the only homepages set");
|
||||
|
||||
Services.prefs.setCharPref("browser.startup.homepage", oldHomepagePref);
|
||||
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
|
||||
});
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/safebrowsing/content/test/.eslintrc
Normal file
5
browser/components/safebrowsing/content/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/search/test/.eslintrc
Normal file
5
browser/components/search/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/selfsupport/test/.eslintrc
Normal file
5
browser/components/selfsupport/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/sessionstore/test/.eslintrc
Normal file
5
browser/components/sessionstore/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/sessionstore/test/unit/.eslintrc
Normal file
5
browser/components/sessionstore/test/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/shell/test/.eslintrc
Normal file
5
browser/components/shell/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/shell/test/unit/.eslintrc
Normal file
5
browser/components/shell/test/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/test/.eslintrc
Normal file
5
browser/components/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/translation/test/.eslintrc
Normal file
5
browser/components/translation/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/translation/test/unit/.eslintrc
Normal file
5
browser/components/translation/test/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/components/uitour/test/.eslintrc
Normal file
5
browser/components/uitour/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/experiments/test/xpcshell/.eslintrc
Normal file
5
browser/experiments/test/xpcshell/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/extensions/pdfjs/test/.eslintrc
Normal file
5
browser/extensions/pdfjs/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/fuel/test/.eslintrc
Normal file
5
browser/fuel/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/modules/test/.eslintrc
Normal file
5
browser/modules/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
browser/modules/test/unit/social/.eslintrc
Normal file
5
browser/modules/test/unit/social/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
browser/modules/test/xpcshell/.eslintrc
Normal file
5
browser/modules/test/xpcshell/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
@ -24,9 +24,6 @@
|
||||
|
||||
// Rules from the mozilla plugin
|
||||
"mozilla/balanced-listeners": 2,
|
||||
"mozilla/components-imports": 1,
|
||||
"mozilla/import-globals-from": 1,
|
||||
"mozilla/import-headjs-globals": 1,
|
||||
"mozilla/mark-test-function-used": 1,
|
||||
"mozilla/no-aArgs": 1,
|
||||
"mozilla/no-cpows-in-tests": 1,
|
||||
|
@ -1,44 +1,10 @@
|
||||
// Parent config file for all devtools browser mochitest files.
|
||||
{
|
||||
"rules": {
|
||||
// Only disallow non-global unused vars, so that head.js does not produce
|
||||
// errors.
|
||||
"no-unused-vars": [2, {"vars": "local"}]
|
||||
},
|
||||
"extends": [
|
||||
"../testing/mochitest/browser.eslintrc"
|
||||
],
|
||||
// All globals made available in the test environment.
|
||||
"globals": {
|
||||
"add_task": true,
|
||||
"Assert": true,
|
||||
"BrowserTestUtils": true,
|
||||
"content": true,
|
||||
"ContentTask": true,
|
||||
"document": true,
|
||||
"EventUtils": true,
|
||||
"executeSoon": true,
|
||||
"export_assertions": true,
|
||||
"finish": true,
|
||||
"gBrowser": true,
|
||||
"gDevTools": true,
|
||||
"getRootDirectory": true,
|
||||
"getTestFilePath": true,
|
||||
"gTestPath": true,
|
||||
"info": true,
|
||||
"is": true,
|
||||
"isnot": true,
|
||||
"navigator": true,
|
||||
"ok": true,
|
||||
"promise": true,
|
||||
"registerCleanupFunction": true,
|
||||
"requestLongerTimeout": true,
|
||||
"setTimeout": true,
|
||||
"SimpleTest": true,
|
||||
"SpecialPowers": true,
|
||||
"todo": true,
|
||||
"todo_is": true,
|
||||
"todo_isnot": true,
|
||||
"waitForClipboard": true,
|
||||
"waitForExplicitFinish": true,
|
||||
"waitForFocus": true,
|
||||
"window": true,
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
// Parent config file for all devtools browser mochitest files.
|
||||
{
|
||||
"extends": [
|
||||
"../testing/xpcshell/xpcshell.eslintrc"
|
||||
],
|
||||
"rules": {
|
||||
// Allow non-camelcase so that run_test doesn't produce a warning.
|
||||
"camelcase": 0,
|
||||
// Only disallow non-global unused vars, so that things like the test
|
||||
// function do not produce errors.
|
||||
"no-unused-vars": [2, {"vars": "local"}],
|
||||
// Allow using undefined variables so that tests can refer to functions
|
||||
// and variables defined in head.js files, without having to maintain a
|
||||
// list of globals in each .eslintrc file.
|
||||
@ -13,39 +13,5 @@
|
||||
// from head.js files.
|
||||
"no-undef": 0,
|
||||
"block-scoped-var": 0
|
||||
},
|
||||
// All globals made available in the test environment.
|
||||
"globals": {
|
||||
"add_task": true,
|
||||
"add_test": true,
|
||||
"Assert": true,
|
||||
"deepEqual": true,
|
||||
"do_check_eq": true,
|
||||
"do_check_false": true,
|
||||
"do_check_neq": true,
|
||||
"do_check_null": true,
|
||||
"do_check_true": true,
|
||||
"do_execute_soon": true,
|
||||
"do_get_cwd": true,
|
||||
"do_get_file": true,
|
||||
"do_get_idle": true,
|
||||
"do_get_profile": true,
|
||||
"do_load_module": true,
|
||||
"do_parse_document": true,
|
||||
"do_print": true,
|
||||
"do_register_cleanup": true,
|
||||
"do_test_finished": true,
|
||||
"do_test_pending": true,
|
||||
"do_throw": true,
|
||||
"do_timeout": true,
|
||||
"equal": true,
|
||||
"load": true,
|
||||
"notDeepEqual": true,
|
||||
"notEqual": true,
|
||||
"notStrictEqual": true,
|
||||
"ok": true,
|
||||
"run_next_test": true,
|
||||
"run_test": true,
|
||||
"strictEqual": true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ add_task(function* runTest() {
|
||||
},
|
||||
{
|
||||
asyncCause: "promise callback",
|
||||
columnNumber: 1,
|
||||
columnNumber: 3,
|
||||
filename: TEST_URI,
|
||||
functionName: "time1",
|
||||
language: 2,
|
||||
|
@ -3197,36 +3197,8 @@ FrameActor.prototype = {
|
||||
return this.frame.arguments.map(arg => createValueGrip(arg,
|
||||
this.threadActor._pausePool, this.threadActor.objectGrip));
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a protocol request to pop this frame from the stack.
|
||||
*
|
||||
* @param aRequest object
|
||||
* The protocol request object.
|
||||
*/
|
||||
onPop: function (aRequest) {
|
||||
// TODO: remove this when Debugger.Frame.prototype.pop is implemented
|
||||
if (typeof this.frame.pop != "function") {
|
||||
return { error: "notImplemented",
|
||||
message: "Popping frames is not yet implemented." };
|
||||
}
|
||||
|
||||
while (this.frame != this.threadActor.dbg.getNewestFrame()) {
|
||||
this.threadActor.dbg.getNewestFrame().pop();
|
||||
}
|
||||
this.frame.pop(aRequest.completionValue);
|
||||
|
||||
// TODO: return the watches property when frame pop watch actors are
|
||||
// implemented.
|
||||
return { from: this.actorID };
|
||||
}
|
||||
};
|
||||
|
||||
FrameActor.prototype.requestTypes = {
|
||||
"pop": FrameActor.prototype.onPop,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a BreakpointActor. BreakpointActors exist for the lifetime of their
|
||||
* containing thread and are responsible for deleting breakpoints, handling
|
||||
|
@ -20,7 +20,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592
|
||||
/* Async parent frames from pushPrefEnv don't show up in e10s. */
|
||||
var isE10S = !SpecialPowers.isMainProcess();
|
||||
if (!isE10S && SpecialPowers.getBoolPref("javascript.options.asyncstack")) {
|
||||
asyncFrame = `Async*@${file}:153:1
|
||||
asyncFrame = `Async*@${file}:153:3
|
||||
`;
|
||||
} else {
|
||||
asyncFrame = "";
|
||||
|
@ -41,7 +41,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1107592
|
||||
var isE10S = !SpecialPowers.isMainProcess();
|
||||
var asyncStack = SpecialPowers.getBoolPref("javascript.options.asyncstack");
|
||||
var ourFile = location.href;
|
||||
var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:121:1
|
||||
var parentFrame = (asyncStack && !isE10S) ? `Async*@${ourFile}:121:3
|
||||
` : "";
|
||||
|
||||
Promise.all([
|
||||
|
@ -3243,21 +3243,17 @@ js::PCToLineNumber(unsigned startLine, jssrcnote* notes, jsbytecode* code, jsbyt
|
||||
ptrdiff_t target = pc - code;
|
||||
for (jssrcnote* sn = notes; !SN_IS_TERMINATOR(sn); sn = SN_NEXT(sn)) {
|
||||
offset += SN_DELTA(sn);
|
||||
SrcNoteType type = (SrcNoteType) SN_TYPE(sn);
|
||||
if (type == SRC_SETLINE) {
|
||||
if (offset <= target)
|
||||
lineno = unsigned(GetSrcNoteOffset(sn, 0));
|
||||
column = 0;
|
||||
} else if (type == SRC_NEWLINE) {
|
||||
if (offset <= target)
|
||||
lineno++;
|
||||
column = 0;
|
||||
}
|
||||
|
||||
if (offset > target)
|
||||
break;
|
||||
|
||||
if (type == SRC_COLSPAN) {
|
||||
SrcNoteType type = (SrcNoteType) SN_TYPE(sn);
|
||||
if (type == SRC_SETLINE) {
|
||||
lineno = unsigned(GetSrcNoteOffset(sn, 0));
|
||||
column = 0;
|
||||
} else if (type == SRC_NEWLINE) {
|
||||
lineno++;
|
||||
column = 0;
|
||||
} else if (type == SRC_COLSPAN) {
|
||||
ptrdiff_t colspan = SN_OFFSET_TO_COLSPAN(GetSrcNoteOffset(sn, 0));
|
||||
MOZ_ASSERT(ptrdiff_t(column) + colspan >= 0);
|
||||
column += colspan;
|
||||
|
@ -123,6 +123,7 @@ dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.robolectric:robolectric:3.0'
|
||||
testCompile 'org.simpleframework:simple-http:4.1.13'
|
||||
testCompile 'org.mockito:mockito-core:1.10.19'
|
||||
}
|
||||
|
||||
apply plugin: 'idea'
|
||||
|
@ -267,7 +267,7 @@ public class ZoomedView extends FrameLayout implements LayerView.DynamicToolbarL
|
||||
changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
|
||||
zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
|
||||
|
||||
setTextInZoomFactorButton(zoomFactor);
|
||||
updateUI();
|
||||
|
||||
toolbarHeight = getResources().getDimensionPixelSize(R.dimen.zoomed_view_toolbar_height);
|
||||
containterSize = getResources().getDimensionPixelSize(R.dimen.drawable_dropshadow_size);
|
||||
@ -504,19 +504,30 @@ public class ZoomedView extends FrameLayout implements LayerView.DynamicToolbarL
|
||||
return (GeckoAppShell.getScreenDepth() == 24) ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565;
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
// onFinishInflate is not yet completed, the update of the UI will be done later
|
||||
if (changeZoomFactorButton == null) {
|
||||
return;
|
||||
}
|
||||
if (isSimplifiedUI) {
|
||||
changeZoomFactorButton.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
setTextInZoomFactorButton(zoomFactor);
|
||||
changeZoomFactorButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void getPrefs() {
|
||||
prefSimplifiedUIObserverId = PrefsHelper.getPref("ui.zoomedview.simplified", new PrefsHelper.PrefHandlerBase() {
|
||||
@Override
|
||||
public void prefValue(String pref, boolean simplified) {
|
||||
isSimplifiedUI = simplified;
|
||||
if (simplified) {
|
||||
changeZoomFactorButton.setVisibility(View.INVISIBLE);
|
||||
zoomFactor = (float) defaultZoomFactor;
|
||||
} else {
|
||||
zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
|
||||
setTextInZoomFactorButton(zoomFactor);
|
||||
changeZoomFactorButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -533,8 +544,8 @@ public class ZoomedView extends FrameLayout implements LayerView.DynamicToolbarL
|
||||
zoomFactor = (float) defaultZoomFactor;
|
||||
} else {
|
||||
zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
|
||||
setTextInZoomFactorButton(zoomFactor);
|
||||
}
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
132
mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java
Normal file
132
mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java
Normal file
@ -0,0 +1,132 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.IntDef;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.gecko.background.nativecode.NativeCrypto;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.util.IOUtils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public abstract class BaseAction {
|
||||
private static final String LOGTAG = "GeckoDLCBaseAction";
|
||||
|
||||
/**
|
||||
* Exception indicating a recoverable error has happened. Download of the content will be retried later.
|
||||
*/
|
||||
/* package-private */ static class RecoverableDownloadContentException extends Exception {
|
||||
private static final long serialVersionUID = -2246772819507370734L;
|
||||
|
||||
@IntDef({MEMORY, DISK_IO, SERVER, NETWORK})
|
||||
public @interface ErrorType {}
|
||||
public static final int MEMORY = 1;
|
||||
public static final int DISK_IO = 2;
|
||||
public static final int SERVER = 3;
|
||||
public static final int NETWORK = 4;
|
||||
|
||||
private int errorType;
|
||||
|
||||
public RecoverableDownloadContentException(@ErrorType int errorType, String message) {
|
||||
super(message);
|
||||
this.errorType = errorType;
|
||||
}
|
||||
|
||||
public RecoverableDownloadContentException(@ErrorType int errorType, Throwable cause) {
|
||||
super(cause);
|
||||
this.errorType = errorType;
|
||||
}
|
||||
|
||||
@ErrorType
|
||||
public int getErrorType() {
|
||||
return errorType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should this error be counted as failure? If this type of error will happen multiple times in a row then this
|
||||
* error will be treated as permanently and the operation will not be tried again until the content changes.
|
||||
*/
|
||||
public boolean shouldBeCountedAsFailure() {
|
||||
if (NETWORK == errorType) {
|
||||
return false; // Always retry after network errors
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this exception is thrown the content will be marked as unrecoverable, permanently failed and we will not try
|
||||
* downloading it again - until a newer version of the content is available.
|
||||
*/
|
||||
/* package-private */ static class UnrecoverableDownloadContentException extends Exception {
|
||||
private static final long serialVersionUID = 8956080754787367105L;
|
||||
|
||||
public UnrecoverableDownloadContentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnrecoverableDownloadContentException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void perform(Context context, DownloadContentCatalog catalog);
|
||||
|
||||
protected File getDestinationFile(Context context, DownloadContent content) throws UnrecoverableDownloadContentException {
|
||||
if (content.isFont()) {
|
||||
return new File(new File(context.getApplicationInfo().dataDir, "fonts"), content.getFilename());
|
||||
}
|
||||
|
||||
// Unrecoverable: We downloaded a file and we don't know what to do with it (Should not happen)
|
||||
throw new UnrecoverableDownloadContentException("Can't determine destination for kind: " + content.getKind());
|
||||
}
|
||||
|
||||
protected boolean verify(File file, String expectedChecksum)
|
||||
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
|
||||
InputStream inputStream = null;
|
||||
|
||||
try {
|
||||
inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
|
||||
byte[] ctx = NativeCrypto.sha256init();
|
||||
if (ctx == null) {
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.MEMORY,
|
||||
"Could not create SHA-256 context");
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
|
||||
while ((read = inputStream.read(buffer)) != -1) {
|
||||
NativeCrypto.sha256update(ctx, buffer, read);
|
||||
}
|
||||
|
||||
String actualChecksum = Utils.byte2Hex(NativeCrypto.sha256finalize(ctx));
|
||||
|
||||
if (!actualChecksum.equalsIgnoreCase(expectedChecksum)) {
|
||||
Log.w(LOGTAG, "Checksums do not match. Expected=" + expectedChecksum + ", Actual=" + actualChecksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
// Recoverable: Just I/O discontinuation
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
|
||||
} finally {
|
||||
IOUtils.safeStreamClose(inputStream);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,17 +5,28 @@
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.support.v4.net.ConnectivityManagerCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.background.nativecode.NativeCrypto;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
import org.mozilla.gecko.util.IOUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.support.v4.net.ConnectivityManagerCompat;
|
||||
import android.util.Log;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpEntity;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
@ -25,97 +36,138 @@ import ch.boye.httpclientandroidlib.client.methods.HttpGet;
|
||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpRequestRetryHandler;
|
||||
import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/* package-private */ class DownloadContentHelper {
|
||||
private static final String LOGTAG = "GeckoDLCHelper";
|
||||
|
||||
public static final String ACTION_STUDY_CATALOG = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.STUDY";
|
||||
public static final String ACTION_VERIFY_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.VERIFY";
|
||||
public static final String ACTION_DOWNLOAD_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.DOWNLOAD";
|
||||
|
||||
private static final String CDN_BASE_URL = "https://mobile.cdn.mozilla.net/";
|
||||
/**
|
||||
* Download content that has been scheduled during "study" or "verify".
|
||||
*/
|
||||
public class DownloadAction extends BaseAction {
|
||||
private static final String LOGTAG = "DLCDownloadAction";
|
||||
|
||||
private static final String CACHE_DIRECTORY = "downloadContent";
|
||||
private static final String CDN_BASE_URL = "https://mobile.cdn.mozilla.net/";
|
||||
|
||||
/**
|
||||
* Exception indicating a recoverable error has happened. Download of the content will be retried later.
|
||||
*/
|
||||
/* package-private */ static class RecoverableDownloadContentException extends Exception {
|
||||
private static final long serialVersionUID = -2246772819507370734L;
|
||||
|
||||
public RecoverableDownloadContentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RecoverableDownloadContentException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
public interface Callback {
|
||||
void onContentDownloaded(DownloadContent content);
|
||||
}
|
||||
|
||||
/**
|
||||
* If this exception is thrown the content will be marked as unrecoverable, permanently failed and we will not try
|
||||
* downloading it again - until a newer version of the content is available.
|
||||
*/
|
||||
/* package-private */ static class UnrecoverableDownloadContentException extends Exception {
|
||||
private static final long serialVersionUID = 8956080754787367105L;
|
||||
private Callback callback;
|
||||
|
||||
public UnrecoverableDownloadContentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnrecoverableDownloadContentException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
public DownloadAction(Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/* package-private */ static HttpClient buildHttpClient() {
|
||||
// TODO: Implement proxy support (Bug 1209496)
|
||||
return HttpClientBuilder.create()
|
||||
.setUserAgent(HardwareUtils.isTablet() ?
|
||||
AppConstants.USER_AGENT_FENNEC_TABLET :
|
||||
AppConstants.USER_AGENT_FENNEC_MOBILE)
|
||||
.setRetryHandler(new DefaultHttpRequestRetryHandler())
|
||||
.build();
|
||||
}
|
||||
public void perform(Context context, DownloadContentCatalog catalog) {
|
||||
Log.d(LOGTAG, "Downloading content..");
|
||||
|
||||
/* package-private */ static File createTemporaryFile(Context context, DownloadContent content)
|
||||
throws RecoverableDownloadContentException {
|
||||
File cacheDirectory = new File(context.getCacheDir(), CACHE_DIRECTORY);
|
||||
|
||||
if (!cacheDirectory.exists() && !cacheDirectory.mkdirs()) {
|
||||
// Recoverable: File system might not be mounted NOW and we didn't download anything yet anyways.
|
||||
throw new RecoverableDownloadContentException("Could not create cache directory: " + cacheDirectory);
|
||||
if (!isConnectedToNetwork(context)) {
|
||||
Log.d(LOGTAG, "No connected network available. Postponing download.");
|
||||
// TODO: Reschedule download (bug 1209498)
|
||||
return;
|
||||
}
|
||||
|
||||
return new File(cacheDirectory, content.getDownloadChecksum() + "-" + content.getId());
|
||||
if (isActiveNetworkMetered(context)) {
|
||||
Log.d(LOGTAG, "Network is metered. Postponing download.");
|
||||
// TODO: Reschedule download (bug 1209498)
|
||||
return;
|
||||
}
|
||||
|
||||
final HttpClient client = buildHttpClient();
|
||||
|
||||
for (DownloadContent content : catalog.getScheduledDownloads()) {
|
||||
Log.d(LOGTAG, "Downloading: " + content);
|
||||
|
||||
File temporaryFile = null;
|
||||
|
||||
try {
|
||||
File destinationFile = getDestinationFile(context, content);
|
||||
if (destinationFile.exists() && verify(destinationFile, content.getChecksum())) {
|
||||
Log.d(LOGTAG, "Content already exists and is up-to-date.");
|
||||
catalog.markAsDownloaded(content);
|
||||
continue;
|
||||
}
|
||||
|
||||
temporaryFile = createTemporaryFile(context, content);
|
||||
|
||||
if (!hasEnoughDiskSpace(content, destinationFile, temporaryFile)) {
|
||||
Log.d(LOGTAG, "Not enough disk space to save content. Skipping download.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: Check space on disk before downloading content (bug 1220145)
|
||||
final String url = createDownloadURL(content);
|
||||
|
||||
if (!temporaryFile.exists() || temporaryFile.length() < content.getSize()) {
|
||||
download(client, url, temporaryFile);
|
||||
}
|
||||
|
||||
if (!verify(temporaryFile, content.getDownloadChecksum())) {
|
||||
Log.w(LOGTAG, "Wrong checksum after download, content=" + content.getId());
|
||||
temporaryFile.delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!content.isAssetArchive()) {
|
||||
Log.e(LOGTAG, "Downloaded content is not of type 'asset-archive': " + content.getType());
|
||||
temporaryFile.delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
extract(temporaryFile, destinationFile, content.getChecksum());
|
||||
|
||||
catalog.markAsDownloaded(content);
|
||||
|
||||
Log.d(LOGTAG, "Successfully downloaded: " + content);
|
||||
|
||||
if (callback != null) {
|
||||
callback.onContentDownloaded(content);
|
||||
}
|
||||
|
||||
if (temporaryFile != null && temporaryFile.exists()) {
|
||||
temporaryFile.delete();
|
||||
}
|
||||
} catch (RecoverableDownloadContentException e) {
|
||||
Log.w(LOGTAG, "Downloading content failed (Recoverable): " + content , e);
|
||||
|
||||
if (e.shouldBeCountedAsFailure()) {
|
||||
catalog.rememberFailure(content, e.getErrorType());
|
||||
}
|
||||
|
||||
// TODO: Reschedule download (bug 1209498)
|
||||
} catch (UnrecoverableDownloadContentException e) {
|
||||
Log.w(LOGTAG, "Downloading content failed (Unrecoverable): " + content, e);
|
||||
|
||||
catalog.markAsPermanentlyFailed(content);
|
||||
|
||||
if (temporaryFile != null && temporaryFile.exists()) {
|
||||
temporaryFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Done");
|
||||
}
|
||||
|
||||
/* package-private */ static void download(HttpClient client, String source, File temporaryFile)
|
||||
protected void download(HttpClient client, String source, File temporaryFile)
|
||||
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
|
||||
final HttpGet request = new HttpGet(source);
|
||||
|
||||
final long offset = temporaryFile.exists() ? temporaryFile.length() : 0;
|
||||
if (offset > 0) {
|
||||
request.setHeader("Range", "bytes=" + offset + "-");
|
||||
}
|
||||
|
||||
try {
|
||||
final HttpResponse response = client.execute(request);
|
||||
final int status = response.getStatusLine().getStatusCode();
|
||||
if (status != HttpStatus.SC_OK) {
|
||||
if (status != HttpStatus.SC_OK && status != HttpStatus.SC_PARTIAL_CONTENT) {
|
||||
// We are trying to be smart and only retry if this is an error that might resolve in the future.
|
||||
// TODO: This is guesstimating at best. We want to implement failure counters (Bug 1215106).
|
||||
if (status >= 500) {
|
||||
// Recoverable: Server errors 5xx
|
||||
throw new RecoverableDownloadContentException("(Recoverable) Download failed. Status code: " + status);
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER,
|
||||
"(Recoverable) Download failed. Status code: " + status);
|
||||
} else if (status >= 400) {
|
||||
// Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed.
|
||||
throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
|
||||
@ -130,73 +182,30 @@ import java.util.zip.GZIPInputStream;
|
||||
final HttpEntity entity = response.getEntity();
|
||||
if (entity == null) {
|
||||
// Recoverable: Should not happen for a valid asset
|
||||
throw new RecoverableDownloadContentException("Null entity");
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Null entity");
|
||||
}
|
||||
|
||||
inputStream = new BufferedInputStream(entity.getContent());
|
||||
outputStream = new BufferedOutputStream(new FileOutputStream(temporaryFile));
|
||||
outputStream = openFile(temporaryFile, status == HttpStatus.SC_PARTIAL_CONTENT);
|
||||
|
||||
IOUtils.copy(inputStream, outputStream);
|
||||
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
// TODO: Support resuming downloads using 'Range' header (Bug 1209513)
|
||||
temporaryFile.delete();
|
||||
|
||||
// Recoverable: Just I/O discontinuation
|
||||
throw new RecoverableDownloadContentException(e);
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.NETWORK, e);
|
||||
} finally {
|
||||
IOUtils.safeStreamClose(inputStream);
|
||||
IOUtils.safeStreamClose(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/* package-private */ static boolean verify(File file, String expectedChecksum)
|
||||
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
|
||||
InputStream inputStream = null;
|
||||
|
||||
try {
|
||||
inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
|
||||
byte[] ctx = NativeCrypto.sha256init();
|
||||
if (ctx == null) {
|
||||
throw new RecoverableDownloadContentException("Could not create SHA-256 context");
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
|
||||
while ((read = inputStream.read(buffer)) != -1) {
|
||||
NativeCrypto.sha256update(ctx, buffer, read);
|
||||
}
|
||||
|
||||
String actualChecksum = Utils.byte2Hex(NativeCrypto.sha256finalize(ctx));
|
||||
|
||||
if (!actualChecksum.equalsIgnoreCase(expectedChecksum)) {
|
||||
Log.w(LOGTAG, "Checksums do not match. Expected=" + expectedChecksum + ", Actual=" + actualChecksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
// Recoverable: Just I/O discontinuation
|
||||
throw new RecoverableDownloadContentException(e);
|
||||
} finally {
|
||||
IOUtils.safeStreamClose(inputStream);
|
||||
}
|
||||
protected OutputStream openFile(File file, boolean append) throws FileNotFoundException {
|
||||
return new BufferedOutputStream(new FileOutputStream(file, append));
|
||||
}
|
||||
|
||||
/* package-private */ static void move(File temporaryFile, File destinationFile)
|
||||
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
|
||||
if (!temporaryFile.renameTo(destinationFile)) {
|
||||
Log.d(LOGTAG, "Could not move temporary file to destination. Trying to copy..");
|
||||
copy(temporaryFile, destinationFile);
|
||||
temporaryFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/* package-private */ static void extract(File sourceFile, File destinationFile, String checksum)
|
||||
protected void extract(File sourceFile, File destinationFile, String checksum)
|
||||
throws UnrecoverableDownloadContentException, RecoverableDownloadContentException {
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
@ -225,9 +234,8 @@ import java.util.zip.GZIPInputStream;
|
||||
|
||||
move(temporaryFile, destinationFile);
|
||||
} catch (IOException e) {
|
||||
// We do not support resume yet (Bug 1209513). Therefore we have to treat this as unrecoverable: The
|
||||
// temporarily file will be deleted and we want to avoid downloading and failing repeatedly.
|
||||
throw new UnrecoverableDownloadContentException(e);
|
||||
// We could not extract to the destination: Keep temporary file and try again next time we run.
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
|
||||
} finally {
|
||||
IOUtils.safeStreamClose(inputStream);
|
||||
IOUtils.safeStreamClose(outputStream);
|
||||
@ -238,7 +246,55 @@ import java.util.zip.GZIPInputStream;
|
||||
}
|
||||
}
|
||||
|
||||
private static void copy(File temporaryFile, File destinationFile)
|
||||
protected boolean isConnectedToNetwork(Context context) {
|
||||
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
|
||||
|
||||
return networkInfo != null && networkInfo.isConnected();
|
||||
}
|
||||
|
||||
protected boolean isActiveNetworkMetered(Context context) {
|
||||
return ConnectivityManagerCompat.isActiveNetworkMetered(
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
|
||||
}
|
||||
|
||||
protected HttpClient buildHttpClient() {
|
||||
// TODO: Implement proxy support (Bug 1209496)
|
||||
return HttpClientBuilder.create()
|
||||
.setUserAgent(HardwareUtils.isTablet() ?
|
||||
AppConstants.USER_AGENT_FENNEC_TABLET :
|
||||
AppConstants.USER_AGENT_FENNEC_MOBILE)
|
||||
.setRetryHandler(new DefaultHttpRequestRetryHandler())
|
||||
.build();
|
||||
}
|
||||
|
||||
protected String createDownloadURL(DownloadContent content) {
|
||||
return CDN_BASE_URL + content.getLocation();
|
||||
}
|
||||
|
||||
protected File createTemporaryFile(Context context, DownloadContent content)
|
||||
throws RecoverableDownloadContentException {
|
||||
File cacheDirectory = new File(context.getCacheDir(), CACHE_DIRECTORY);
|
||||
|
||||
if (!cacheDirectory.exists() && !cacheDirectory.mkdirs()) {
|
||||
// Recoverable: File system might not be mounted NOW and we didn't download anything yet anyways.
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO,
|
||||
"Could not create cache directory: " + cacheDirectory);
|
||||
}
|
||||
|
||||
return new File(cacheDirectory, content.getDownloadChecksum() + "-" + content.getId());
|
||||
}
|
||||
|
||||
protected void move(File temporaryFile, File destinationFile)
|
||||
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
|
||||
if (!temporaryFile.renameTo(destinationFile)) {
|
||||
Log.d(LOGTAG, "Could not move temporary file to destination. Trying to copy..");
|
||||
copy(temporaryFile, destinationFile);
|
||||
temporaryFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
protected void copy(File temporaryFile, File destinationFile)
|
||||
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
|
||||
InputStream inputStream = null;
|
||||
OutputStream outputStream = null;
|
||||
@ -257,32 +313,27 @@ import java.util.zip.GZIPInputStream;
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
} catch (IOException e) {
|
||||
// Meh. This is an awkward situation: We downloaded the content but we can't move it to its destination. We
|
||||
// are treating this as "unrecoverable" error because we want to avoid downloading this again and again and
|
||||
// then always failing to copy it to the destination. This will be fixed after we implement resuming
|
||||
// downloads (Bug 1209513): We will keep the downloaded temporary file and just retry copying.
|
||||
throw new UnrecoverableDownloadContentException(e);
|
||||
// We could not copy the temporary file to its destination: Keep the temporary file and
|
||||
// try again the next time we run.
|
||||
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e);
|
||||
} finally {
|
||||
IOUtils.safeStreamClose(inputStream);
|
||||
IOUtils.safeStreamClose(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/* package-private */ static File getDestinationFile(Context context, DownloadContent content) throws UnrecoverableDownloadContentException {
|
||||
if (content.isFont()) {
|
||||
return new File(new File(context.getApplicationInfo().dataDir, "fonts"), content.getFilename());
|
||||
protected boolean hasEnoughDiskSpace(DownloadContent content, File destinationFile, File temporaryFile) {
|
||||
final File temporaryDirectory = temporaryFile.getParentFile();
|
||||
if (temporaryDirectory.getUsableSpace() < content.getSize()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unrecoverable: We downloaded a file and we don't know what to do with it (Should not happen)
|
||||
throw new UnrecoverableDownloadContentException("Can't determine destination for kind: " + content.getKind());
|
||||
}
|
||||
final File destinationDirectory = destinationFile.getParentFile();
|
||||
// We need some more space to extract the file (getSize() returns the uncompressed size)
|
||||
if (destinationDirectory.getUsableSpace() < content.getSize() * 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* package-private */ static boolean isActiveNetworkMetered(Context context) {
|
||||
return ConnectivityManagerCompat.isActiveNetworkMetered(
|
||||
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
|
||||
}
|
||||
|
||||
/* package-private */ static String createDownloadURL(DownloadContent content) {
|
||||
return CDN_BASE_URL + content.getLocation();
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,14 +1,10 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoEvent;
|
||||
import org.mozilla.gecko.dlc.DownloadContentHelper;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
|
||||
@ -18,30 +14,30 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import ch.boye.httpclientandroidlib.client.HttpClient;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Service to handle downloadable content that did not ship with the APK.
|
||||
*/
|
||||
public class DownloadContentService extends IntentService {
|
||||
private static final String LOGTAG = "GeckoDLCService";
|
||||
|
||||
private static final String ACTION_STUDY_CATALOG = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.STUDY";
|
||||
private static final String ACTION_VERIFY_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.VERIFY";
|
||||
private static final String ACTION_DOWNLOAD_CONTENT = AppConstants.ANDROID_PACKAGE_NAME + ".DLC.DOWNLOAD";
|
||||
|
||||
public static void startStudy(Context context) {
|
||||
Intent intent = new Intent(DownloadContentHelper.ACTION_STUDY_CATALOG);
|
||||
Intent intent = new Intent(ACTION_STUDY_CATALOG);
|
||||
intent.setComponent(new ComponentName(context, DownloadContentService.class));
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public static void startVerification(Context context) {
|
||||
Intent intent = new Intent(DownloadContentHelper.ACTION_VERIFY_CONTENT);
|
||||
Intent intent = new Intent(ACTION_VERIFY_CONTENT);
|
||||
intent.setComponent(new ComponentName(context, DownloadContentService.class));
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
public static void startDownloads(Context context) {
|
||||
Intent intent = new Intent(DownloadContentHelper.ACTION_DOWNLOAD_CONTENT);
|
||||
Intent intent = new Intent(ACTION_DOWNLOAD_CONTENT);
|
||||
intent.setComponent(new ComponentName(context, DownloadContentService.class));
|
||||
context.startService(intent);
|
||||
}
|
||||
@ -69,156 +65,34 @@ public class DownloadContentService extends IntentService {
|
||||
return;
|
||||
}
|
||||
|
||||
final BaseAction action;
|
||||
|
||||
switch (intent.getAction()) {
|
||||
case DownloadContentHelper.ACTION_STUDY_CATALOG:
|
||||
studyCatalog();
|
||||
case ACTION_STUDY_CATALOG:
|
||||
action = new StudyAction();
|
||||
break;
|
||||
|
||||
case DownloadContentHelper.ACTION_DOWNLOAD_CONTENT:
|
||||
downloadContent();
|
||||
case ACTION_DOWNLOAD_CONTENT:
|
||||
action = new DownloadAction(new DownloadAction.Callback() {
|
||||
@Override
|
||||
public void onContentDownloaded(DownloadContent content) {
|
||||
if (content.isFont()) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Fonts:Reload", ""));
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case DownloadContentHelper.ACTION_VERIFY_CONTENT:
|
||||
verifyCatalog();
|
||||
case ACTION_VERIFY_CONTENT:
|
||||
action = new VerifyAction();
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.e(LOGTAG, "Unknown action: " + intent.getAction());
|
||||
return;
|
||||
}
|
||||
|
||||
action.perform(this, catalog);
|
||||
catalog.persistChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Study: Scan the catalog for "new" content available for download.
|
||||
*/
|
||||
private void studyCatalog() {
|
||||
Log.d(LOGTAG, "Studying catalog..");
|
||||
|
||||
for (DownloadContent content : catalog.getContentWithoutState()) {
|
||||
if (content.isAssetArchive() && content.isFont()) {
|
||||
catalog.scheduleDownload(content);
|
||||
|
||||
Log.d(LOGTAG, "Scheduled download: " + content);
|
||||
}
|
||||
}
|
||||
|
||||
if (catalog.hasScheduledDownloads()) {
|
||||
startDownloads(this);
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Done");
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify: Validate downloaded content. Does it still exist and does it have the correct checksum?
|
||||
*/
|
||||
private void verifyCatalog() {
|
||||
Log.d(LOGTAG, "Verifying catalog..");
|
||||
|
||||
for (DownloadContent content : catalog.getDownloadedContent()) {
|
||||
try {
|
||||
File destinationFile = DownloadContentHelper.getDestinationFile(this, content);
|
||||
|
||||
if (!destinationFile.exists()) {
|
||||
Log.d(LOGTAG, "Downloaded content does not exist anymore: " + content);
|
||||
|
||||
// This file does not exist even though it is marked as downloaded in the catalog. Scheduling a
|
||||
// download to fetch it again.
|
||||
catalog.scheduleDownload(content);
|
||||
}
|
||||
|
||||
if (!DownloadContentHelper.verify(destinationFile, content.getChecksum())) {
|
||||
catalog.scheduleDownload(content);
|
||||
Log.d(LOGTAG, "Wrong checksum. Scheduling download: " + content);
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Content okay: " + content);
|
||||
} catch (DownloadContentHelper.UnrecoverableDownloadContentException e) {
|
||||
Log.w(LOGTAG, "Unrecoverable exception while verifying downloaded file", e);
|
||||
} catch (DownloadContentHelper.RecoverableDownloadContentException e) {
|
||||
// That's okay, we are just verifying already existing content. No log.
|
||||
}
|
||||
}
|
||||
|
||||
if (catalog.hasScheduledDownloads()) {
|
||||
startDownloads(this);
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Done");
|
||||
}
|
||||
|
||||
/**
|
||||
* Download content that has been scheduled during "study" or "verify".
|
||||
*/
|
||||
private void downloadContent() {
|
||||
Log.d(LOGTAG, "Downloading content..");
|
||||
|
||||
if (DownloadContentHelper.isActiveNetworkMetered(this)) {
|
||||
Log.d(LOGTAG, "Network is metered. Postponing download.");
|
||||
// TODO: Reschedule download (bug 1209498)
|
||||
return;
|
||||
}
|
||||
|
||||
HttpClient client = DownloadContentHelper.buildHttpClient();
|
||||
|
||||
for (DownloadContent content : catalog.getScheduledDownloads()) {
|
||||
Log.d(LOGTAG, "Downloading: " + content);
|
||||
|
||||
File temporaryFile = null;
|
||||
|
||||
try {
|
||||
File destinationFile = DownloadContentHelper.getDestinationFile(this, content);
|
||||
if (destinationFile.exists() && DownloadContentHelper.verify(destinationFile, content.getChecksum())) {
|
||||
Log.d(LOGTAG, "Content already exists and is up-to-date.");
|
||||
continue;
|
||||
}
|
||||
|
||||
temporaryFile = DownloadContentHelper.createTemporaryFile(this, content);
|
||||
|
||||
// TODO: Check space on disk before downloading content (bug 1220145)
|
||||
final String url = DownloadContentHelper.createDownloadURL(content);
|
||||
DownloadContentHelper.download(client, url, temporaryFile);
|
||||
|
||||
if (!DownloadContentHelper.verify(temporaryFile, content.getDownloadChecksum())) {
|
||||
Log.w(LOGTAG, "Wrong checksum after download, content=" + content.getId());
|
||||
temporaryFile.delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!content.isAssetArchive()) {
|
||||
Log.e(LOGTAG, "Downloaded content is not of type 'asset-archive': " + content.getType());
|
||||
continue;
|
||||
}
|
||||
|
||||
DownloadContentHelper.extract(temporaryFile, destinationFile, content.getChecksum());
|
||||
|
||||
catalog.markAsDownloaded(content);
|
||||
|
||||
Log.d(LOGTAG, "Successfully downloaded: " + content);
|
||||
|
||||
onContentDownloaded(content);
|
||||
} catch (DownloadContentHelper.RecoverableDownloadContentException e) {
|
||||
Log.w(LOGTAG, "Downloading content failed (Recoverable): " + content, e);
|
||||
// TODO: Reschedule download (bug 1209498)
|
||||
} catch (DownloadContentHelper.UnrecoverableDownloadContentException e) {
|
||||
Log.w(LOGTAG, "Downloading content failed (Unrecoverable): " + content, e);
|
||||
|
||||
catalog.markAsPermanentlyFailed(content);
|
||||
} finally {
|
||||
if (temporaryFile != null && temporaryFile.exists()) {
|
||||
temporaryFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Done");
|
||||
}
|
||||
|
||||
private void onContentDownloaded(DownloadContent content) {
|
||||
if (content.isFont()) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Fonts:Reload", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
|
||||
/**
|
||||
* Study: Scan the catalog for "new" content available for download.
|
||||
*/
|
||||
public class StudyAction extends BaseAction {
|
||||
private static final String LOGTAG = "DLCStudyAction";
|
||||
|
||||
public void perform(Context context, DownloadContentCatalog catalog) {
|
||||
Log.d(LOGTAG, "Studying catalog..");
|
||||
|
||||
for (DownloadContent content : catalog.getContentWithoutState()) {
|
||||
if (content.isAssetArchive() && content.isFont()) {
|
||||
catalog.scheduleDownload(content);
|
||||
|
||||
Log.d(LOGTAG, "Scheduled download: " + content);
|
||||
}
|
||||
}
|
||||
|
||||
if (catalog.hasScheduledDownloads()) {
|
||||
startDownloads(context);
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Done");
|
||||
}
|
||||
|
||||
protected void startDownloads(Context context) {
|
||||
DownloadContentService.startDownloads(context);
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Verify: Validate downloaded content. Does it still exist and does it have the correct checksum?
|
||||
*/
|
||||
public class VerifyAction extends BaseAction {
|
||||
private static final String LOGTAG = "DLCVerifyAction";
|
||||
|
||||
@Override
|
||||
public void perform(Context context, DownloadContentCatalog catalog) {
|
||||
Log.d(LOGTAG, "Verifying catalog..");
|
||||
|
||||
for (DownloadContent content : catalog.getDownloadedContent()) {
|
||||
try {
|
||||
File destinationFile = getDestinationFile(context, content);
|
||||
|
||||
if (!destinationFile.exists()) {
|
||||
Log.d(LOGTAG, "Downloaded content does not exist anymore: " + content);
|
||||
|
||||
// This file does not exist even though it is marked as downloaded in the catalog. Scheduling a
|
||||
// download to fetch it again.
|
||||
catalog.scheduleDownload(content);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!verify(destinationFile, content.getChecksum())) {
|
||||
catalog.scheduleDownload(content);
|
||||
Log.d(LOGTAG, "Wrong checksum. Scheduling download: " + content);
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Content okay: " + content);
|
||||
} catch (UnrecoverableDownloadContentException e) {
|
||||
Log.w(LOGTAG, "Unrecoverable exception while verifying downloaded file", e);
|
||||
} catch (RecoverableDownloadContentException e) {
|
||||
// That's okay, we are just verifying already existing content. No log.
|
||||
}
|
||||
}
|
||||
|
||||
if (catalog.hasScheduledDownloads()) {
|
||||
startDownloads(context);
|
||||
}
|
||||
|
||||
Log.v(LOGTAG, "Done");
|
||||
}
|
||||
|
||||
protected void startDownloads(Context context) {
|
||||
DownloadContentService.startDownloads(context);
|
||||
}
|
||||
}
|
@ -23,6 +23,8 @@ public class DownloadContent {
|
||||
private static final String KEY_KIND = "kind";
|
||||
private static final String KEY_SIZE = "size";
|
||||
private static final String KEY_STATE = "state";
|
||||
private static final String KEY_FAILURES = "failures";
|
||||
private static final String KEY_LAST_FAILURE_TYPE = "last_failure_type";
|
||||
|
||||
@IntDef({STATE_NONE, STATE_SCHEDULED, STATE_DOWNLOADED, STATE_FAILED, STATE_IGNORED})
|
||||
public @interface State {}
|
||||
@ -50,6 +52,8 @@ public class DownloadContent {
|
||||
private final String kind;
|
||||
private final long size;
|
||||
private int state;
|
||||
private int failures;
|
||||
private int lastFailureType;
|
||||
|
||||
private DownloadContent(@NonNull String id, @NonNull String location, @NonNull String filename,
|
||||
@NonNull String checksum, @NonNull String downloadChecksum, @NonNull long lastModified,
|
||||
@ -93,6 +97,10 @@ public class DownloadContent {
|
||||
return location;
|
||||
}
|
||||
|
||||
public long getLastModified() {
|
||||
return lastModified;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
@ -117,6 +125,24 @@ public class DownloadContent {
|
||||
return TYPE_ASSET_ARCHIVE.equals(type);
|
||||
}
|
||||
|
||||
/* package-private */ int getFailures() {
|
||||
return failures;
|
||||
}
|
||||
|
||||
/* package-private */ void rememberFailure(int failureType) {
|
||||
if (lastFailureType != failureType) {
|
||||
lastFailureType = failureType;
|
||||
failures = 1;
|
||||
} else {
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
/* package-private */ void resetFailures() {
|
||||
failures = 0;
|
||||
lastFailureType = 0;
|
||||
}
|
||||
|
||||
public static DownloadContent fromJSON(JSONObject object) throws JSONException {
|
||||
return new Builder()
|
||||
.setId(object.getString(KEY_ID))
|
||||
@ -129,6 +155,7 @@ public class DownloadContent {
|
||||
.setKind(object.getString(KEY_KIND))
|
||||
.setSize(object.getLong(KEY_SIZE))
|
||||
.setState(object.getInt(KEY_STATE))
|
||||
.setFailures(object.optInt(KEY_FAILURES), object.optInt(KEY_LAST_FAILURE_TYPE))
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -144,6 +171,12 @@ public class DownloadContent {
|
||||
object.put(KEY_KIND, kind);
|
||||
object.put(KEY_SIZE, size);
|
||||
object.put(KEY_STATE, state);
|
||||
|
||||
if (failures > 0) {
|
||||
object.put(KEY_FAILURES, failures);
|
||||
object.put(KEY_LAST_FAILURE_TYPE, lastFailureType);
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
@ -162,11 +195,16 @@ public class DownloadContent {
|
||||
private String kind;
|
||||
private long size;
|
||||
private int state;
|
||||
private int failures;
|
||||
private int lastFailureType;
|
||||
|
||||
public DownloadContent build() {
|
||||
DownloadContent content = new DownloadContent(id, location, filename, checksum, downloadChecksum,
|
||||
lastModified, type, kind, size);
|
||||
content.setState(state);
|
||||
content.failures = failures;
|
||||
content.lastFailureType = lastFailureType;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
@ -219,5 +257,12 @@ public class DownloadContent {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
/* package-private */ Builder setFailures(int failures, int lastFailureType) {
|
||||
this.failures = failures;
|
||||
this.lastFailureType = lastFailureType;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,6 @@
|
||||
|
||||
package org.mozilla.gecko.dlc.catalog;
|
||||
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentBootstrap;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.util.AtomicFile;
|
||||
import android.util.Log;
|
||||
@ -33,18 +31,25 @@ public class DownloadContentCatalog {
|
||||
private static final String LOGTAG = "GeckoDLCCatalog";
|
||||
private static final String FILE_NAME = "download_content_catalog";
|
||||
|
||||
private static final int MAX_FAILURES_UNTIL_PERMANENTLY_FAILED = 10;
|
||||
|
||||
private final AtomicFile file; // Guarded by 'file'
|
||||
private List<DownloadContent> content; // Guarded by 'this'
|
||||
private boolean hasLoadedCatalog; // Guarded by 'this
|
||||
private boolean hasCatalogChanged; // Guarded by 'this'
|
||||
|
||||
public DownloadContentCatalog(Context context) {
|
||||
content = Collections.emptyList();
|
||||
file = new AtomicFile(new File(context.getApplicationInfo().dataDir, FILE_NAME));
|
||||
this(new AtomicFile(new File(context.getApplicationInfo().dataDir, FILE_NAME)));
|
||||
|
||||
startLoadFromDisk();
|
||||
}
|
||||
|
||||
// For injecting mocked AtomicFile objects during test
|
||||
protected DownloadContentCatalog(AtomicFile file) {
|
||||
this.content = Collections.emptyList();
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public synchronized List<DownloadContent> getContentWithoutState() {
|
||||
awaitLoadingCatalogLocked();
|
||||
|
||||
@ -104,6 +109,7 @@ public class DownloadContentCatalog {
|
||||
|
||||
public synchronized void markAsDownloaded(DownloadContent content) {
|
||||
content.setState(DownloadContent.STATE_DOWNLOADED);
|
||||
content.resetFailures();
|
||||
hasCatalogChanged = true;
|
||||
}
|
||||
|
||||
@ -117,6 +123,17 @@ public class DownloadContentCatalog {
|
||||
hasCatalogChanged = true;
|
||||
}
|
||||
|
||||
public synchronized void rememberFailure(DownloadContent content, int failureType) {
|
||||
if (content.getFailures() >= MAX_FAILURES_UNTIL_PERMANENTLY_FAILED) {
|
||||
Log.d(LOGTAG, "Maximum number of failures reached. Marking content has permanently failed.");
|
||||
|
||||
markAsPermanentlyFailed(content);
|
||||
} else {
|
||||
content.rememberFailure(failureType);
|
||||
hasCatalogChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void persistChanges() {
|
||||
new Thread(LOGTAG + "-Persist") {
|
||||
public void run() {
|
||||
@ -145,14 +162,18 @@ public class DownloadContentCatalog {
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void loadFromDisk() {
|
||||
protected synchronized boolean hasCatalogChanged() {
|
||||
return hasCatalogChanged;
|
||||
}
|
||||
|
||||
protected synchronized void loadFromDisk() {
|
||||
Log.d(LOGTAG, "Loading from disk");
|
||||
|
||||
if (hasLoadedCatalog) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<DownloadContent> content = new ArrayList<DownloadContent>();
|
||||
List<DownloadContent> content = new ArrayList<>();
|
||||
|
||||
try {
|
||||
JSONArray array;
|
||||
@ -180,15 +201,19 @@ public class DownloadContentCatalog {
|
||||
Log.d(LOGTAG, "Can't read catalog due to IOException", e);
|
||||
}
|
||||
|
||||
this.content = content;
|
||||
this.hasLoadedCatalog = true;
|
||||
onCatalogLoaded(content);
|
||||
|
||||
notifyAll();
|
||||
|
||||
Log.d(LOGTAG, "Loaded " + content.size() + " elements");
|
||||
}
|
||||
|
||||
private synchronized void writeToDisk() {
|
||||
protected void onCatalogLoaded(List<DownloadContent> content) {
|
||||
this.content = content;
|
||||
this.hasLoadedCatalog = true;
|
||||
}
|
||||
|
||||
protected synchronized void writeToDisk() {
|
||||
if (!hasCatalogChanged) {
|
||||
Log.v(LOGTAG, "Not persisting: Catalog has not changed");
|
||||
return;
|
||||
|
@ -301,6 +301,7 @@ class SearchEngineRow extends AnimatedHeightLayout {
|
||||
*/
|
||||
private void updateFromSavedSearches(List<String> savedSuggestions, boolean animate, int suggestionStartIndex, int recycledSuggestionCount) {
|
||||
if (savedSuggestions == null || savedSuggestions.isEmpty()) {
|
||||
hideRecycledSuggestions(suggestionStartIndex, recycledSuggestionCount);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -402,6 +403,11 @@ class SearchEngineRow extends AnimatedHeightLayout {
|
||||
updateFromSavedSearches(searchHistorySuggestions, animate, 0, recycledSuggestionCount);
|
||||
} else if (searchSuggestionsEnabled) {
|
||||
updateFromSearchEngine(animate, searchEngineSuggestions, recycledSuggestionCount, 0);
|
||||
} else {
|
||||
// The current search term is treated separately from the suggestions list, hence we can
|
||||
// recycle ALL suggestion items here. (We always show the current search term, i.e. 1 item,
|
||||
// in front of the search engine suggestions and/or the search history.)
|
||||
hideRecycledSuggestions(0, recycledSuggestionCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,11 +248,14 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
||||
'distribution/Distribution.java',
|
||||
'distribution/ReferrerDescriptor.java',
|
||||
'distribution/ReferrerReceiver.java',
|
||||
'dlc/BaseAction.java',
|
||||
'dlc/catalog/DownloadContent.java',
|
||||
'dlc/catalog/DownloadContentBootstrap.java',
|
||||
'dlc/catalog/DownloadContentCatalog.java',
|
||||
'dlc/DownloadContentHelper.java',
|
||||
'dlc/DownloadAction.java',
|
||||
'dlc/DownloadContentService.java',
|
||||
'dlc/StudyAction.java',
|
||||
'dlc/VerifyAction.java',
|
||||
'DoorHangerPopup.java',
|
||||
'DownloadsIntegration.java',
|
||||
'DynamicToolbar.java',
|
||||
|
@ -0,0 +1,560 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mozilla.gecko.background.testhelpers.TestRunner;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpEntity;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.HttpStatus;
|
||||
import ch.boye.httpclientandroidlib.StatusLine;
|
||||
import ch.boye.httpclientandroidlib.client.HttpClient;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpGet;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
|
||||
|
||||
import static org.mockito.Matchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* DownloadAction: Download content that has been scheduled during "study" or "verify".
|
||||
*/
|
||||
@RunWith(TestRunner.class)
|
||||
public class TestDownloadAction {
|
||||
private static final String TEST_URL = "http://example.org";
|
||||
|
||||
/**
|
||||
* Scenario: The current network is metered.
|
||||
*
|
||||
* Verify that:
|
||||
* * No download is performed on a metered network
|
||||
*/
|
||||
@Test
|
||||
public void testNothingIsDoneOnMeteredNetwork() throws Exception {
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(true).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
|
||||
action.perform(RuntimeEnvironment.application, null);
|
||||
|
||||
verify(action, never()).buildHttpClient();
|
||||
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: No (connected) network is available.
|
||||
*
|
||||
* Verify that:
|
||||
* * No download is performed
|
||||
*/
|
||||
@Test
|
||||
public void testNothingIsDoneIfNoNetworkIsAvailable() throws Exception {
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
|
||||
|
||||
action.perform(RuntimeEnvironment.application, null);
|
||||
|
||||
verify(action, never()).isActiveNetworkMetered(any(Context.class));
|
||||
verify(action, never()).buildHttpClient();
|
||||
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Content is scheduled for download but already exists locally (with correct checksum).
|
||||
*
|
||||
* Verify that:
|
||||
* * No download is performed for existing file
|
||||
* * Content is marked as downloaded in the catalog
|
||||
*/
|
||||
@Test
|
||||
public void testExistingAndVerifiedFilesAreNotDownloadedAgain() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
|
||||
File file = mock(File.class);
|
||||
doReturn(true).when(file).exists();
|
||||
doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
doReturn(true).when(action).verify(eq(file), anyString());
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
|
||||
verify(catalog).markAsDownloaded(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Server returns a server error (HTTP 500).
|
||||
*
|
||||
* Verify that:
|
||||
* * Situation is treated as recoverable (RecoverableDownloadContentException)
|
||||
*/
|
||||
@Test(expected=BaseAction.RecoverableDownloadContentException.class)
|
||||
public void testServerErrorsAreRecoverable() throws Exception {
|
||||
HttpClient client = mockHttpClient(500, "");
|
||||
|
||||
File temporaryFile = mock(File.class);
|
||||
doReturn(false).when(temporaryFile).exists();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
action.download(client, TEST_URL, temporaryFile);
|
||||
|
||||
verify(client).execute(any(HttpUriRequest.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Server returns a client error (HTTP 404).
|
||||
*
|
||||
* Verify that:
|
||||
* * Situation is treated as unrecoverable (UnrecoverableDownloadContentException)
|
||||
*/
|
||||
@Test(expected=BaseAction.UnrecoverableDownloadContentException.class)
|
||||
public void testClientErrorsAreUnrecoverable() throws Exception {
|
||||
HttpClient client = mockHttpClient(404, "");
|
||||
|
||||
File temporaryFile = mock(File.class);
|
||||
doReturn(false).when(temporaryFile).exists();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
action.download(client, TEST_URL, temporaryFile);
|
||||
|
||||
verify(client).execute(any(HttpUriRequest.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: A successful download has been performed.
|
||||
*
|
||||
* Verify that:
|
||||
* * The content will be extracted to the destination
|
||||
* * The content is marked as downloaded in the catalog
|
||||
*/
|
||||
@Test
|
||||
public void testSuccessfulDownloadsAreMarkedAsDownloaded() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder()
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
|
||||
File file = mockNotExistingFile();
|
||||
doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
doReturn(false).when(action).verify(eq(file), anyString());
|
||||
doNothing().when(action).download(any(HttpClient.class), anyString(), eq(file));
|
||||
doReturn(true).when(action).verify(eq(file), anyString());
|
||||
doNothing().when(action).extract(eq(file), eq(file), anyString());
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(action).buildHttpClient();
|
||||
verify(action).download(any(HttpClient.class), anyString(), eq(file));
|
||||
verify(action).extract(eq(file), eq(file), anyString());
|
||||
verify(catalog).markAsDownloaded(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Pretend a partially downloaded file already exists.
|
||||
*
|
||||
* Verify that:
|
||||
* * Range header is set in request
|
||||
* * Content will be appended to existing file
|
||||
* * Content will be marked as downloaded in catalog
|
||||
*/
|
||||
@Test
|
||||
public void testResumingDownloadFromExistingFile() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder()
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.setSize(4223)
|
||||
.build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
|
||||
File temporaryFile = mockFileWithSize(1337L);
|
||||
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
|
||||
|
||||
HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
|
||||
doReturn(client).when(action).buildHttpClient();
|
||||
|
||||
File destinationFile = mockNotExistingFile();
|
||||
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
doReturn(true).when(action).verify(eq(temporaryFile), anyString());
|
||||
doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
ArgumentCaptor<HttpGet> argument = ArgumentCaptor.forClass(HttpGet.class);
|
||||
verify(client).execute(argument.capture());
|
||||
|
||||
HttpGet request = argument.getValue();
|
||||
Assert.assertTrue(request.containsHeader("Range"));
|
||||
Assert.assertEquals("bytes=1337-", request.getFirstHeader("Range").getValue());
|
||||
Assert.assertEquals("HelloWorld", new String(outputStream.toByteArray(), "UTF-8"));
|
||||
|
||||
verify(action).openFile(eq(temporaryFile), eq(true));
|
||||
verify(catalog).markAsDownloaded(content);
|
||||
verify(temporaryFile).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Download fails with IOException.
|
||||
*
|
||||
* Verify that:
|
||||
* * Partially downloaded file will not be deleted
|
||||
* * Content will not be marked as downloaded in catalog
|
||||
*/
|
||||
@Test
|
||||
public void testTemporaryFileIsNotDeletedAfterDownloadAborted() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder()
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.setSize(4223)
|
||||
.build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
|
||||
File temporaryFile = mockFileWithSize(1337L);
|
||||
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
|
||||
ByteArrayOutputStream outputStream = spy(new ByteArrayOutputStream());
|
||||
doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
|
||||
doThrow(IOException.class).when(outputStream).write(any(byte[].class), anyInt(), anyInt());
|
||||
|
||||
HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
|
||||
doReturn(client).when(action).buildHttpClient();
|
||||
|
||||
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(catalog, never()).markAsDownloaded(content);
|
||||
verify(action, never()).verify(any(File.class), anyString());
|
||||
verify(temporaryFile, never()).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Partially downloaded file is already complete.
|
||||
*
|
||||
* Verify that:
|
||||
* * No download request is made
|
||||
* * File is treated as completed and will be verified and extracted
|
||||
* * Content is marked as downloaded in catalog
|
||||
*/
|
||||
@Test
|
||||
public void testNoRequestIsSentIfFileIsAlreadyComplete() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder()
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.setSize(1337L)
|
||||
.build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
|
||||
File temporaryFile = mockFileWithSize(1337L);
|
||||
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
|
||||
File destinationFile = mockNotExistingFile();
|
||||
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
doReturn(true).when(action).verify(eq(temporaryFile), anyString());
|
||||
doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(action, never()).download(any(HttpClient.class), anyString(), eq(temporaryFile));
|
||||
verify(action).verify(eq(temporaryFile), anyString());
|
||||
verify(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
|
||||
verify(catalog).markAsDownloaded(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Download is completed but verification (checksum) failed.
|
||||
*
|
||||
* Verify that:
|
||||
* * Downloaded file is deleted
|
||||
* * File will not be extracted
|
||||
* * Content is not marked as downloaded in the catalog
|
||||
*/
|
||||
@Test
|
||||
public void testTemporaryFileWillBeDeletedIfVerificationFails() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder()
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.setSize(1337L)
|
||||
.build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
|
||||
doReturn(false).when(action).verify(any(File.class), anyString());
|
||||
|
||||
File temporaryFile = mockNotExistingFile();
|
||||
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
|
||||
File destinationFile = mockNotExistingFile();
|
||||
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(temporaryFile).delete();
|
||||
verify(action, never()).extract(any(File.class), any(File.class), anyString());
|
||||
verify(catalog, never()).markAsDownloaded(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Not enough storage space for content is available.
|
||||
*
|
||||
* Verify that:
|
||||
* * No download will per performed
|
||||
*/
|
||||
@Test
|
||||
public void testNoDownloadIsPerformedIfNotEnoughStorageIsAvailable() throws Exception {
|
||||
DownloadContent content = createFontWithSize(1337L);
|
||||
DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
|
||||
|
||||
File temporaryFile = mockNotExistingFile();
|
||||
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
|
||||
File destinationFile = mockNotExistingFile();
|
||||
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
doReturn(true).when(action).hasEnoughDiskSpace(content, destinationFile, temporaryFile);
|
||||
|
||||
verify(action, never()).buildHttpClient();
|
||||
verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
|
||||
verify(action, never()).verify(any(File.class), anyString());
|
||||
verify(catalog, never()).markAsDownloaded(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Not enough storage space for temporary file available.
|
||||
*
|
||||
* Verify that:
|
||||
* * hasEnoughDiskSpace() returns false
|
||||
*/
|
||||
@Test
|
||||
public void testWithNotEnoughSpaceForTemporaryFile() {
|
||||
DownloadContent content = createFontWithSize(2048);
|
||||
File destinationFile = mockNotExistingFile();
|
||||
File temporaryFile = mockNotExistingFileWithUsableSpace(1024);
|
||||
|
||||
DownloadAction action = new DownloadAction(null);
|
||||
Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Not enough storage space for destination file available.
|
||||
*
|
||||
* Verify that:
|
||||
* * hasEnoughDiskSpace() returns false
|
||||
*/
|
||||
@Test
|
||||
public void testWithNotEnoughSpaceForDestinationFile() {
|
||||
DownloadContent content = createFontWithSize(2048);
|
||||
File destinationFile = mockNotExistingFileWithUsableSpace(1024);
|
||||
File temporaryFile = mockNotExistingFile();
|
||||
|
||||
DownloadAction action = new DownloadAction(null);
|
||||
Assert.assertFalse(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Enough storage space for temporary and destination file available.
|
||||
*
|
||||
* Verify that:
|
||||
* * hasEnoughDiskSpace() returns true
|
||||
*/
|
||||
@Test
|
||||
public void testWithEnoughSpaceForEverything() {
|
||||
DownloadContent content = createFontWithSize(2048);
|
||||
File destinationFile = mockNotExistingFileWithUsableSpace(4096);
|
||||
File temporaryFile = mockNotExistingFileWithUsableSpace(4096);
|
||||
|
||||
DownloadAction action = new DownloadAction(null);
|
||||
Assert.assertTrue(action.hasEnoughDiskSpace(content, destinationFile, temporaryFile));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Download failed with network I/O error.
|
||||
*
|
||||
* Verify that:
|
||||
* * Error is not counted as failure
|
||||
*/
|
||||
@Test
|
||||
public void testNetworkErrorIsNotCountedAsFailure() throws Exception {
|
||||
DownloadContent content = createFont();
|
||||
DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
|
||||
|
||||
HttpClient client = mock(HttpClient.class);
|
||||
doThrow(IOException.class).when(client).execute(any(HttpUriRequest.class));
|
||||
doReturn(client).when(action).buildHttpClient();
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(catalog, never()).rememberFailure(eq(content), anyInt());
|
||||
verify(catalog, never()).markAsDownloaded(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Disk IO Error when extracting file.
|
||||
*
|
||||
* Verify that:
|
||||
* * Error is counted as failure
|
||||
* * After multiple errors the content is marked as permanently failed
|
||||
*/
|
||||
@Test
|
||||
public void testDiskIOErrorIsCountedAsFailure() throws Exception {
|
||||
DownloadContent content = createFont();
|
||||
DownloadContentCatalog catalog = mockCatalogWithScheduledDownloads(content);
|
||||
doCallRealMethod().when(catalog).rememberFailure(eq(content), anyInt());
|
||||
doCallRealMethod().when(catalog).markAsPermanentlyFailed(content);
|
||||
|
||||
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||
|
||||
DownloadAction action = spy(new DownloadAction(null));
|
||||
doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
|
||||
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
|
||||
doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
|
||||
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
|
||||
doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
|
||||
doReturn(true).when(action).verify(any(File.class), anyString());
|
||||
|
||||
File destinationFile = mock(File.class);
|
||||
doReturn(false).when(destinationFile).exists();
|
||||
File parentFile = mock(File.class);
|
||||
doReturn(false).when(parentFile).mkdirs();
|
||||
doReturn(false).when(parentFile).exists();
|
||||
doReturn(parentFile).when(destinationFile).getParentFile();
|
||||
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||
}
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState());
|
||||
verify(catalog, times(11)).rememberFailure(eq(content), anyInt());
|
||||
}
|
||||
|
||||
private DownloadContent createFont() {
|
||||
return createFontWithSize(102400L);
|
||||
}
|
||||
|
||||
private DownloadContent createFontWithSize(long size) {
|
||||
return new DownloadContent.Builder()
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.setSize(size)
|
||||
.build();
|
||||
}
|
||||
|
||||
private DownloadContentCatalog mockCatalogWithScheduledDownloads(DownloadContent... content) {
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
doReturn(Arrays.asList(content)).when(catalog).getScheduledDownloads();
|
||||
return catalog;
|
||||
}
|
||||
|
||||
private static File mockNotExistingFile() {
|
||||
return mockFileWithUsableSpace(false, 0, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
private static File mockNotExistingFileWithUsableSpace(long usableSpace) {
|
||||
return mockFileWithUsableSpace(false, 0, usableSpace);
|
||||
}
|
||||
|
||||
private static File mockFileWithSize(long length) {
|
||||
return mockFileWithUsableSpace(true, length, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
private static File mockFileWithUsableSpace(boolean exists, long length, long usableSpace) {
|
||||
File file = mock(File.class);
|
||||
doReturn(exists).when(file).exists();
|
||||
doReturn(length).when(file).length();
|
||||
|
||||
File parentFile = mock(File.class);
|
||||
doReturn(usableSpace).when(parentFile).getUsableSpace();
|
||||
doReturn(parentFile).when(file).getParentFile();
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
private static HttpClient mockHttpClient(int statusCode, String content) throws Exception {
|
||||
StatusLine status = mock(StatusLine.class);
|
||||
doReturn(statusCode).when(status).getStatusCode();
|
||||
|
||||
HttpEntity entity = mock(HttpEntity.class);
|
||||
doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(entity).getContent();
|
||||
|
||||
HttpResponse response = mock(HttpResponse.class);
|
||||
doReturn(status).when(response).getStatusLine();
|
||||
doReturn(entity).when(response).getEntity();
|
||||
|
||||
HttpClient client = mock(HttpClient.class);
|
||||
doReturn(response).when(client).execute(any(HttpUriRequest.class));
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mozilla.gecko.background.testhelpers.TestRunner;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* StudyAction: Scan the catalog for "new" content available for download.
|
||||
*/
|
||||
@RunWith(TestRunner.class)
|
||||
public class TestStudyAction {
|
||||
/**
|
||||
* Scenario: Catalog is empty.
|
||||
*
|
||||
* Verify that:
|
||||
* * No download is scheduled
|
||||
* * Download action is not started
|
||||
*/
|
||||
@Test
|
||||
public void testPerformWithEmptyCatalog() {
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.getContentWithoutState()).thenReturn(new ArrayList<DownloadContent>());
|
||||
|
||||
StudyAction action = spy(new StudyAction());
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(catalog).getContentWithoutState();
|
||||
verify(catalog, never()).markAsDownloaded(any(DownloadContent.class));
|
||||
verify(action, never()).startDownloads(any(Context.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Catalog contains two items that have not been downloaded yet.
|
||||
*
|
||||
* Verify that:
|
||||
* * Both items are scheduled to be downloaded
|
||||
*/
|
||||
@Test
|
||||
public void testPerformWithNewContent() {
|
||||
DownloadContent content1 = new DownloadContent.Builder()
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.build();
|
||||
DownloadContent content2 = new DownloadContent.Builder()
|
||||
.setType(DownloadContent.TYPE_ASSET_ARCHIVE)
|
||||
.setKind(DownloadContent.KIND_FONT)
|
||||
.build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.getContentWithoutState()).thenReturn(Arrays.asList(content1, content2));
|
||||
|
||||
StudyAction action = spy(new StudyAction());
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(catalog).scheduleDownload(content1);
|
||||
verify(catalog).scheduleDownload(content2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Catalog contains item that are scheduled for download.
|
||||
*
|
||||
* Verify that:
|
||||
* * Download action is started
|
||||
*/
|
||||
@Test
|
||||
public void testStartingDownloadsAfterScheduling() {
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.hasScheduledDownloads()).thenReturn(true);
|
||||
|
||||
StudyAction action = spy(new StudyAction());
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(action).startDownloads(any(Context.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Catalog contains unknown content.
|
||||
*
|
||||
* Verify that:
|
||||
* * Unknown content is not scheduled for download.
|
||||
*/
|
||||
@Test
|
||||
public void testPerformWithUnknownContent() {
|
||||
DownloadContent content = new DownloadContent.Builder()
|
||||
.setType("Unknown-Type")
|
||||
.setKind("Unknown-Kind")
|
||||
.build();
|
||||
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.getContentWithoutState()).thenReturn(Collections.singletonList(content));
|
||||
|
||||
StudyAction action = spy(new StudyAction());
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(catalog, never()).scheduleDownload(content);
|
||||
}
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.dlc;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mozilla.gecko.background.testhelpers.TestRunner;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContent;
|
||||
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* VerifyAction: Validate downloaded content. Does it still exist and does it have the correct checksum?
|
||||
*/
|
||||
@RunWith(TestRunner.class)
|
||||
public class TestVerifyAction {
|
||||
/**
|
||||
* Scenario: Downloaded file does not exist anymore.
|
||||
*
|
||||
* Verify that:
|
||||
* * Content is re-scheduled for download.
|
||||
*/
|
||||
@Test
|
||||
public void testReschedulingIfFileDoesNotExist() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content));
|
||||
|
||||
File file = mock(File.class);
|
||||
when(file.exists()).thenReturn(false);
|
||||
|
||||
VerifyAction action = spy(new VerifyAction());
|
||||
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(catalog).scheduleDownload(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Content has been scheduled for download.
|
||||
*
|
||||
* Verify that:
|
||||
* * Download action is started
|
||||
*/
|
||||
@Test
|
||||
public void testStartingDownloadsAfterScheduling() {
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.hasScheduledDownloads()).thenReturn(true);
|
||||
|
||||
VerifyAction action = spy(new VerifyAction());
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(action).startDownloads(any(Context.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Checksum of existing file does not match expectation.
|
||||
*
|
||||
* Verify that:
|
||||
* * Content is re-scheduled for download.
|
||||
*/
|
||||
@Test
|
||||
public void testReschedulingIfVerificationFailed() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content));
|
||||
|
||||
File file = mock(File.class);
|
||||
when(file.exists()).thenReturn(true);
|
||||
|
||||
VerifyAction action = spy(new VerifyAction());
|
||||
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
doReturn(false).when(action).verify(eq(file), anyString());
|
||||
|
||||
action.perform(RuntimeEnvironment.application, catalog);
|
||||
|
||||
verify(catalog).scheduleDownload(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Downloaded file exists and has the correct checksum.
|
||||
*
|
||||
* Verify that:
|
||||
* * No download is scheduled
|
||||
* * Download action is not started
|
||||
*/
|
||||
@Test
|
||||
public void testSuccessfulVerification() throws Exception {
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
|
||||
when(catalog.getDownloadedContent()).thenReturn(Collections.singletonList(content));
|
||||
|
||||
File file = mock(File.class);
|
||||
when(file.exists()).thenReturn(true);
|
||||
|
||||
VerifyAction action = spy(new VerifyAction());
|
||||
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
|
||||
doReturn(true).when(action).verify(eq(file), anyString());
|
||||
|
||||
verify(catalog, never()).scheduleDownload(content);
|
||||
verify(action, never()).startDownloads(RuntimeEnvironment.application);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
package org.mozilla.gecko.dlc.catalog;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mozilla.gecko.background.testhelpers.TestRunner;
|
||||
|
||||
@RunWith(TestRunner.class)
|
||||
public class TestDownloadContent {
|
||||
/**
|
||||
* Verify that the values passed to the builder are all set on the DownloadContent object.
|
||||
*/
|
||||
@Test
|
||||
public void testBuilder() {
|
||||
DownloadContent content = createTestContent();
|
||||
|
||||
Assert.assertEquals("Some-ID", content.getId());
|
||||
Assert.assertEquals("/somewhere/something", content.getLocation());
|
||||
Assert.assertEquals("some.file", content.getFilename());
|
||||
Assert.assertEquals("Some-checksum", content.getChecksum());
|
||||
Assert.assertEquals("Some-download-checksum", content.getDownloadChecksum());
|
||||
Assert.assertEquals(4223, content.getLastModified());
|
||||
Assert.assertEquals("Some-type", content.getType());
|
||||
Assert.assertEquals("Some-kind", content.getKind());
|
||||
Assert.assertEquals(27, content.getSize());
|
||||
Assert.assertEquals(DownloadContent.STATE_SCHEDULED, content.getState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a DownloadContent object exported to JSON and re-imported from JSON does not change.
|
||||
*/
|
||||
public void testJSONSerializationAndDeserialization() throws JSONException {
|
||||
DownloadContent content = DownloadContent.fromJSON(createTestContent().toJSON());
|
||||
|
||||
Assert.assertEquals("Some-ID", content.getId());
|
||||
Assert.assertEquals("/somewhere/something", content.getLocation());
|
||||
Assert.assertEquals("some.file", content.getFilename());
|
||||
Assert.assertEquals("Some-checksum", content.getChecksum());
|
||||
Assert.assertEquals("Some-download-checksum", content.getDownloadChecksum());
|
||||
Assert.assertEquals(4223, content.getLastModified());
|
||||
Assert.assertEquals("Some-type", content.getType());
|
||||
Assert.assertEquals("Some-kind", content.getKind());
|
||||
Assert.assertEquals(27, content.getSize());
|
||||
Assert.assertEquals(DownloadContent.STATE_SCHEDULED, content.getState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a DownloadContent object with arbitrary data.
|
||||
*/
|
||||
private DownloadContent createTestContent() {
|
||||
return new DownloadContent.Builder()
|
||||
.setId("Some-ID")
|
||||
.setLocation("/somewhere/something")
|
||||
.setFilename("some.file")
|
||||
.setChecksum("Some-checksum")
|
||||
.setDownloadChecksum("Some-download-checksum")
|
||||
.setLastModified(4223)
|
||||
.setType("Some-type")
|
||||
.setKind("Some-kind")
|
||||
.setSize(27)
|
||||
.setState(DownloadContent.STATE_SCHEDULED)
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,261 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.gecko.dlc.catalog;
|
||||
|
||||
import android.support.v4.util.AtomicFile;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mozilla.gecko.background.testhelpers.TestRunner;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@RunWith(TestRunner.class)
|
||||
public class TestDownloadContentCatalog {
|
||||
/**
|
||||
* Scenario: Create a new, fresh catalog.
|
||||
*
|
||||
* Verify that:
|
||||
* * Catalog has not changed
|
||||
* * Unchanged catalog will not be saved to disk
|
||||
*/
|
||||
@Test
|
||||
public void testUntouchedCatalogHasNotChangedAndWillNotBePersisted() throws Exception {
|
||||
AtomicFile file = mock(AtomicFile.class);
|
||||
doReturn("[]".getBytes("UTF-8")).when(file).readFully();
|
||||
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file));
|
||||
catalog.loadFromDisk();
|
||||
|
||||
Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
|
||||
|
||||
catalog.writeToDisk();
|
||||
|
||||
Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
|
||||
|
||||
verify(file, never()).startWrite();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Create a new, fresh catalog.
|
||||
*
|
||||
* Verify that:
|
||||
* * Catalog is bootstrapped with items.
|
||||
*/
|
||||
@Test
|
||||
public void testCatalogIsBootstrapedIfFileDoesNotExist() throws Exception {
|
||||
AtomicFile file = mock(AtomicFile.class);
|
||||
doThrow(FileNotFoundException.class).when(file).readFully();
|
||||
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file));
|
||||
catalog.loadFromDisk();
|
||||
|
||||
Assert.assertTrue("Catalog is not empty", catalog.getContentWithoutState().size() > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Schedule downloading an item from the catalog.
|
||||
*
|
||||
* Verify that:
|
||||
* * Catalog has changed
|
||||
*/
|
||||
@Test
|
||||
public void testCatalogHasChangedWhenDownloadIsScheduled() throws Exception {
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
catalog.onCatalogLoaded(Collections.singletonList(content));
|
||||
|
||||
Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
|
||||
|
||||
catalog.scheduleDownload(content);
|
||||
|
||||
Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Mark an item in the catalog as downloaded.
|
||||
*
|
||||
* Verify that:
|
||||
* * Catalog has changed
|
||||
*/
|
||||
@Test
|
||||
public void testCatalogHasChangedWhenContentIsDownloaded() throws Exception {
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
catalog.onCatalogLoaded(Collections.singletonList(content));
|
||||
|
||||
Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
|
||||
|
||||
catalog.markAsDownloaded(content);
|
||||
|
||||
Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Mark an item in the catalog as permanently failed.
|
||||
*
|
||||
* Verify that:
|
||||
* * Catalog has changed
|
||||
*/
|
||||
@Test
|
||||
public void testCatalogHasChangedIfDownloadHasFailedPermanently() throws Exception {
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
catalog.onCatalogLoaded(Collections.singletonList(content));
|
||||
|
||||
Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
|
||||
|
||||
catalog.markAsPermanentlyFailed(content);
|
||||
|
||||
Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Mark an item in the catalog as ignored.
|
||||
*
|
||||
* Verify that:
|
||||
* * Catalog has changed
|
||||
*/
|
||||
@Test
|
||||
public void testCatalogHasChangedIfContentIsIgnored() throws Exception {
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
catalog.onCatalogLoaded(Collections.singletonList(content));
|
||||
|
||||
Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
|
||||
|
||||
catalog.markAsIgnored(content);
|
||||
|
||||
Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: A changed catalog is written to disk.
|
||||
*
|
||||
* Verify that:
|
||||
* * Before write: Catalog has changed
|
||||
* * After write: Catalog has not changed.
|
||||
*/
|
||||
@Test
|
||||
public void testCatalogHasNotChangedAfterWritingToDisk() throws Exception {
|
||||
AtomicFile file = mock(AtomicFile.class);
|
||||
doReturn(mock(FileOutputStream.class)).when(file).startWrite();
|
||||
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(file));
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
catalog.onCatalogLoaded(Collections.singletonList(content));
|
||||
|
||||
catalog.scheduleDownload(content);
|
||||
|
||||
Assert.assertTrue("Catalog has changed", catalog.hasCatalogChanged());
|
||||
|
||||
catalog.writeToDisk();
|
||||
|
||||
Assert.assertFalse("Catalog has not changed", catalog.hasCatalogChanged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: A catalog with multiple items in different states.
|
||||
*
|
||||
* Verify that:
|
||||
* * getContentWithoutState(), getDownloadedContent() and getScheduledDownloads() returns
|
||||
* the correct items depenending on their state.
|
||||
*/
|
||||
@Test
|
||||
public void testContentClassification() {
|
||||
DownloadContentCatalog catalog = spy(new DownloadContentCatalog(mock(AtomicFile.class)));
|
||||
|
||||
DownloadContent content1 = new DownloadContent.Builder().setState(DownloadContent.STATE_NONE).build();
|
||||
DownloadContent content2 = new DownloadContent.Builder().setState(DownloadContent.STATE_NONE).build();
|
||||
DownloadContent content3 = new DownloadContent.Builder().setState(DownloadContent.STATE_SCHEDULED).build();
|
||||
DownloadContent content4 = new DownloadContent.Builder().setState(DownloadContent.STATE_SCHEDULED).build();
|
||||
DownloadContent content5 = new DownloadContent.Builder().setState(DownloadContent.STATE_SCHEDULED).build();
|
||||
DownloadContent content6 = new DownloadContent.Builder().setState(DownloadContent.STATE_DOWNLOADED).build();
|
||||
DownloadContent content7 = new DownloadContent.Builder().setState(DownloadContent.STATE_FAILED).build();
|
||||
DownloadContent content8 = new DownloadContent.Builder().setState(DownloadContent.STATE_IGNORED).build();
|
||||
DownloadContent content9 = new DownloadContent.Builder().setState(DownloadContent.STATE_IGNORED).build();
|
||||
|
||||
|
||||
catalog.onCatalogLoaded(Arrays.asList(content1, content2, content3, content4, content5, content6,
|
||||
content7, content8, content9));
|
||||
|
||||
Assert.assertEquals(2, catalog.getContentWithoutState().size());
|
||||
Assert.assertEquals(1, catalog.getDownloadedContent().size());
|
||||
Assert.assertEquals(3, catalog.getScheduledDownloads().size());
|
||||
|
||||
Assert.assertTrue(catalog.getContentWithoutState().contains(content1));
|
||||
Assert.assertTrue(catalog.getContentWithoutState().contains(content2));
|
||||
|
||||
Assert.assertTrue(catalog.getDownloadedContent().contains(content6));
|
||||
|
||||
Assert.assertTrue(catalog.getScheduledDownloads().contains(content3));
|
||||
Assert.assertTrue(catalog.getScheduledDownloads().contains(content4));
|
||||
Assert.assertTrue(catalog.getScheduledDownloads().contains(content5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Calling rememberFailure() on a catalog with varying values
|
||||
*/
|
||||
@Test
|
||||
public void testRememberingFailures() {
|
||||
DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class));
|
||||
Assert.assertFalse(catalog.hasCatalogChanged());
|
||||
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
Assert.assertEquals(0, content.getFailures());
|
||||
|
||||
catalog.rememberFailure(content, 42);
|
||||
Assert.assertEquals(1, content.getFailures());
|
||||
Assert.assertTrue(catalog.hasCatalogChanged());
|
||||
|
||||
catalog.rememberFailure(content, 42);
|
||||
Assert.assertEquals(2, content.getFailures());
|
||||
|
||||
// Failure counter is reset if different failure has been reported
|
||||
catalog.rememberFailure(content, 23);
|
||||
Assert.assertEquals(1, content.getFailures());
|
||||
|
||||
// Failure counter is reset after successful download
|
||||
catalog.markAsDownloaded(content);
|
||||
Assert.assertEquals(0, content.getFailures());
|
||||
}
|
||||
|
||||
/**
|
||||
* Scenario: Content has failed multiple times with the same failure type.
|
||||
*
|
||||
* Verify that:
|
||||
* * Content is marked as permanently failed
|
||||
*/
|
||||
@Test
|
||||
public void testContentWillBeMarkedAsPermanentlyFailedAfterMultipleFailures() {
|
||||
DownloadContentCatalog catalog = new DownloadContentCatalog(mock(AtomicFile.class));
|
||||
|
||||
DownloadContent content = new DownloadContent.Builder().build();
|
||||
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
catalog.rememberFailure(content, 42);
|
||||
|
||||
Assert.assertEquals(i + 1, content.getFailures());
|
||||
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
|
||||
}
|
||||
|
||||
catalog.rememberFailure(content, 42);
|
||||
Assert.assertEquals(10, content.getFailures());
|
||||
Assert.assertEquals(DownloadContent.STATE_FAILED, content.getState());
|
||||
}
|
||||
}
|
@ -138,7 +138,10 @@ this.FxAccountsWebChannel.prototype = {
|
||||
*/
|
||||
let listener = (webChannelId, message, sendingContext) => {
|
||||
if (message) {
|
||||
log.debug("FxAccountsWebChannel message received", message);
|
||||
log.debug("FxAccountsWebChannel message received", message.command);
|
||||
if (logPII) {
|
||||
log.debug("FxAccountsWebChannel message details", message);
|
||||
}
|
||||
let command = message.command;
|
||||
let data = message.data;
|
||||
|
||||
|
@ -0,0 +1,12 @@
|
||||
.. _import-browserjs-globals:
|
||||
|
||||
========================
|
||||
import-browserjs-globals
|
||||
========================
|
||||
|
||||
Rule Details
|
||||
------------
|
||||
|
||||
When included files from the main browser UI scripts will be loaded and any
|
||||
declared globals will be defined for the current file. This is mostly useful for
|
||||
browser-chrome mochitests that call browser functions.
|
@ -8,19 +8,28 @@
|
||||
|
||||
var escope = require("escope");
|
||||
var espree = require("espree");
|
||||
var estraverse = require("estraverse");
|
||||
var path = require("path");
|
||||
var fs = require("fs");
|
||||
|
||||
var regexes = [
|
||||
/^(?:Cu|Components\.utils)\.import\(".*\/(.*?)\.jsm?"\);?$/,
|
||||
/^loader\.lazyImporter\(\w+, "(\w+)"/,
|
||||
/^loader\.lazyRequireGetter\(\w+, "(\w+)"/,
|
||||
/^loader\.lazyServiceGetter\(\w+, "(\w+)"/,
|
||||
/^XPCOMUtils\.defineLazyModuleGetter\(\w+, "(\w+)"/,
|
||||
/^DevToolsUtils\.defineLazyModuleGetter\(\w+, "(\w+)"/,
|
||||
/^loader\.lazyGetter\(\w+, "(\w+)"/,
|
||||
/^XPCOMUtils\.defineLazyGetter\(\w+, "(\w+)"/,
|
||||
/^XPCOMUtils\.defineLazyServiceGetter\(\w+, "(\w+)"/,
|
||||
/^DevToolsUtils\.defineLazyGetter\(\w+, "(\w+)"/,
|
||||
var definitions = [
|
||||
/^loader\.lazyGetter\(this, "(\w+)"/,
|
||||
/^loader\.lazyImporter\(this, "(\w+)"/,
|
||||
/^loader\.lazyServiceGetter\(this, "(\w+)"/,
|
||||
/^loader\.lazyRequireGetter\(this, "(\w+)"/,
|
||||
/^XPCOMUtils\.defineLazyGetter\(this, "(\w+)"/,
|
||||
/^XPCOMUtils\.defineLazyModuleGetter\(this, "(\w+)"/,
|
||||
/^XPCOMUtils\.defineLazyServiceGetter\(this, "(\w+)"/,
|
||||
/^XPCOMUtils\.defineConstant\(this, "(\w+)"/,
|
||||
/^DevToolsUtils\.defineLazyModuleGetter\(this, "(\w+)"/,
|
||||
/^DevToolsUtils\.defineLazyGetter\(this, "(\w+)"/,
|
||||
/^Object\.defineProperty\(this, "(\w+)"/,
|
||||
/^Reflect\.defineProperty\(this, "(\w+)"/,
|
||||
/^this\.__defineGetter__\("(\w+)"/,
|
||||
];
|
||||
|
||||
var imports = [
|
||||
/^(?:Cu|Components\.utils)\.import\(".*\/(.*?)\.jsm?"(?:, this)?\)/,
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
@ -43,55 +52,120 @@ module.exports = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the source text of an AST node.
|
||||
* A simplistic conversion of some AST nodes to a standard string form.
|
||||
*
|
||||
* @param {ASTNode} node
|
||||
* The AST node representing the source text.
|
||||
* @param {ASTContext} context
|
||||
* The current context.
|
||||
* @param {Object} node
|
||||
* The AST node to convert.
|
||||
*
|
||||
* @return {String}
|
||||
* The source text representing the AST node.
|
||||
* The JS source for the node.
|
||||
*/
|
||||
getSource: function(node, context) {
|
||||
return context.getSource(node).replace(/[\r\n]+\s*/g, " ")
|
||||
.replace(/\s*=\s*/g, " = ")
|
||||
.replace(/\s+\./g, ".")
|
||||
.replace(/,\s+/g, ", ")
|
||||
.replace(/;\n(\d+)/g, ";$1")
|
||||
.replace(/\s+/g, " ");
|
||||
getASTSource: function(node) {
|
||||
switch (node.type) {
|
||||
case "MemberExpression":
|
||||
if (node.computed)
|
||||
throw new Error("getASTSource unsupported computed MemberExpression");
|
||||
return this.getASTSource(node.object) + "." + this.getASTSource(node.property);
|
||||
case "ThisExpression":
|
||||
return "this";
|
||||
case "Identifier":
|
||||
return node.name;
|
||||
case "Literal":
|
||||
return JSON.stringify(node.value);
|
||||
case "CallExpression":
|
||||
var args = node.arguments.map(a => this.getASTSource(a)).join(", ");
|
||||
return this.getASTSource(node.callee) + "(" + args + ")";
|
||||
case "ObjectExpression":
|
||||
return "{}";
|
||||
case "ExpressionStatement":
|
||||
return this.getASTSource(node.expression) + ";";
|
||||
case "FunctionExpression":
|
||||
return "function() {}";
|
||||
default:
|
||||
throw new Error("getASTSource unsupported node type: " + node.type);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the variable name from an import source
|
||||
* e.g. Cu.import("path/to/someName") will return "someName."
|
||||
* Attempts to convert an ExpressionStatement to a likely global variable
|
||||
* definition.
|
||||
*
|
||||
* Some valid input strings:
|
||||
* - Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
|
||||
* - loader.lazyImporter(this, "name1");
|
||||
* - loader.lazyRequireGetter(this, "name2"
|
||||
* - loader.lazyServiceGetter(this, "name3"
|
||||
* - XPCOMUtils.defineLazyModuleGetter(this, "setNamedTimeout", ...)
|
||||
* - loader.lazyGetter(this, "toolboxStrings"
|
||||
* - XPCOMUtils.defineLazyGetter(this, "clipboardHelper"
|
||||
* @param {Object} node
|
||||
* The AST node to convert.
|
||||
*
|
||||
* @param {String} source
|
||||
* The source representing an import statement.
|
||||
*
|
||||
* @return {String}
|
||||
* The variable name imported.
|
||||
* @return {String or null}
|
||||
* The variable name defined.
|
||||
*/
|
||||
getVarNameFromImportSource: function(source) {
|
||||
for (var i = 0; i < regexes.length; i++) {
|
||||
var regex = regexes[i];
|
||||
var matches = source.match(regex);
|
||||
convertExpressionToGlobal: function(node, isGlobal) {
|
||||
try {
|
||||
var source = this.getASTSource(node);
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
var name = matches[1];
|
||||
for (var reg of definitions) {
|
||||
var match = source.match(reg);
|
||||
if (match) {
|
||||
// Must be in the global scope
|
||||
if (!isGlobal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return name;
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
for (reg of imports) {
|
||||
var match = source.match(reg);
|
||||
if (match) {
|
||||
// The two argument form is only acceptable in the global scope
|
||||
if (node.expression.arguments.length > 1 && !isGlobal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Walks over an AST and calls a callback for every ExpressionStatement found.
|
||||
*
|
||||
* @param {Object} ast
|
||||
* The AST to traverse.
|
||||
*
|
||||
* @return {Function}
|
||||
* The callback to call for each ExpressionStatement.
|
||||
*/
|
||||
expressionTraverse: function(ast, callback) {
|
||||
var helpers = this;
|
||||
var parents = new Map();
|
||||
|
||||
// Walk the parents of a node to see if any are functions
|
||||
function isGlobal(node) {
|
||||
var parent = parents.get(node);
|
||||
while (parent) {
|
||||
if (parent.type == "FunctionExpression" ||
|
||||
parent.type == "FunctionDeclaration") {
|
||||
return false;
|
||||
}
|
||||
parent = parents.get(parent);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
estraverse.traverse(ast, {
|
||||
enter: function(node, parent) {
|
||||
parents.set(node, parent);
|
||||
|
||||
if (node.type == "ExpressionStatement") {
|
||||
callback(node, isGlobal(node));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -113,6 +187,15 @@ module.exports = {
|
||||
result.push(name);
|
||||
}
|
||||
|
||||
var helpers = this;
|
||||
this.expressionTraverse(ast, function(node, isGlobal) {
|
||||
var name = helpers.convertExpressionToGlobal(node, isGlobal);
|
||||
|
||||
if (name) {
|
||||
result.push(name);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
@ -142,25 +225,45 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the single line text represented by a particular AST node.
|
||||
*
|
||||
* @param {ASTNode} node
|
||||
* The AST node representing the source text.
|
||||
* @param {String} text
|
||||
* The text representing the AST node.
|
||||
*
|
||||
* @return {String}
|
||||
* A single line version of the string represented by node.
|
||||
*/
|
||||
getTextForNode: function(node, text) {
|
||||
var source = text.substr(node.range[0], node.range[1] - node.range[0]);
|
||||
// Caches globals found in a file so we only have to parse a file once.
|
||||
globalCache: new Map(),
|
||||
|
||||
return source.replace(/[\r\n]+\s*/g, "")
|
||||
.replace(/\s*=\s*/g, " = ")
|
||||
.replace(/\s+\./g, ".")
|
||||
.replace(/,\s+/g, ", ")
|
||||
.replace(/;\n(\d+)/g, ";$1");
|
||||
/**
|
||||
* Finds all the globals defined in a given file.
|
||||
*
|
||||
* @param {String} fileName
|
||||
* The file to parse for globals.
|
||||
*/
|
||||
getGlobalsForFile: function(fileName) {
|
||||
// If the file can't be found, let the error go up to the caller so it can
|
||||
// be logged as an error in the current file.
|
||||
var content = fs.readFileSync(fileName, "utf8");
|
||||
|
||||
if (this.globalCache.has(fileName)) {
|
||||
return this.globalCache.get(fileName);
|
||||
}
|
||||
|
||||
// Parse the content and get the globals from the ast.
|
||||
var ast = this.getAST(content);
|
||||
var globalVars = this.getGlobals(ast);
|
||||
this.globalCache.set(fileName, globalVars);
|
||||
|
||||
return globalVars;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a set of globals to a context.
|
||||
*
|
||||
* @param {Array} globalVars
|
||||
* An array of global variable names.
|
||||
* @param {ASTContext} context
|
||||
* The current context.
|
||||
*/
|
||||
addGlobals: function(globalVars, context) {
|
||||
for (var i = 0; i < globalVars.length; i++) {
|
||||
var varName = globalVars[i];
|
||||
this.addVarToScope(varName, context);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -238,6 +341,49 @@ module.exports = {
|
||||
return /.*[\\/]browser_.+\.js$/.test(pathAndFilename);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether we are in a test of some kind.
|
||||
*
|
||||
* @param {RuleContext} scope
|
||||
* You should pass this from within a rule
|
||||
* e.g. helpers.getIsTest(this)
|
||||
*
|
||||
* @return {Boolean}
|
||||
* True or false
|
||||
*/
|
||||
getIsTest: function(scope) {
|
||||
var pathAndFilename = scope.getFilename();
|
||||
|
||||
if (/.*[\\/]test_.+\.js$/.test(pathAndFilename)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.getIsBrowserMochitest(scope);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the root directory of the repository by walking up directories until
|
||||
* a .eslintignore file is found.
|
||||
* @param {ASTContext} context
|
||||
* The current context.
|
||||
*
|
||||
* @return {String} The absolute path of the repository directory
|
||||
*/
|
||||
getRootDir: function(context) {
|
||||
var fileName = this.getAbsoluteFilePath(context);
|
||||
var dirName = path.dirname(fileName);
|
||||
|
||||
while (dirName && !fs.existsSync(path.join(dirName, ".eslintignore"))) {
|
||||
dirName = path.dirname(dirName);
|
||||
}
|
||||
|
||||
if (!dirName) {
|
||||
throw new Error("Unable to find root of repository");
|
||||
}
|
||||
|
||||
return dirName;
|
||||
},
|
||||
|
||||
/**
|
||||
* ESLint may be executed from various places: from mach, at the root of the
|
||||
* repository, or from a directory in the repository when, for instance,
|
||||
|
@ -21,6 +21,7 @@ module.exports = {
|
||||
"components-imports": require("../lib/rules/components-imports"),
|
||||
"import-globals-from": require("../lib/rules/import-globals-from"),
|
||||
"import-headjs-globals": require("../lib/rules/import-headjs-globals"),
|
||||
"import-browserjs-globals": require("../lib/rules/import-browserjs-globals"),
|
||||
"mark-test-function-used": require("../lib/rules/mark-test-function-used"),
|
||||
"no-aArgs": require("../lib/rules/no-aArgs"),
|
||||
"no-cpows-in-tests": require("../lib/rules/no-cpows-in-tests"),
|
||||
@ -31,6 +32,7 @@ module.exports = {
|
||||
"components-imports": 0,
|
||||
"import-globals-from": 0,
|
||||
"import-headjs-globals": 0,
|
||||
"import-browserjs-globals": 0,
|
||||
"mark-test-function-used": 0,
|
||||
"no-aArgs": 0,
|
||||
"no-cpows-in-tests": 0,
|
||||
|
@ -22,8 +22,8 @@ module.exports = function(context) {
|
||||
|
||||
return {
|
||||
ExpressionStatement: function(node) {
|
||||
var source = helpers.getSource(node, context);
|
||||
var name = helpers.getVarNameFromImportSource(source);
|
||||
var scope = context.getScope();
|
||||
var name = helpers.convertExpressionToGlobal(node, scope.type == "global");
|
||||
|
||||
if (name) {
|
||||
helpers.addVarToScope(name, context);
|
||||
|
@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @fileoverview Import globals from browser.js.
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rule Definition
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var helpers = require("../helpers");
|
||||
|
||||
const SCRIPTS = [
|
||||
"toolkit/components/printing/content/printUtils.js",
|
||||
"toolkit/content/viewZoomOverlay.js",
|
||||
"browser/components/places/content/browserPlacesViews.js",
|
||||
"browser/base/content/browser.js",
|
||||
"browser/components/downloads/content/downloads.js",
|
||||
"browser/components/downloads/content/indicator.js",
|
||||
"browser/components/customizableui/content/panelUI.js",
|
||||
"toolkit/obsolete/content/inlineSpellCheckUI.js",
|
||||
"toolkit/components/viewsource/content/viewSourceUtils.js",
|
||||
"browser/base/content/browser-addons.js",
|
||||
"browser/base/content/browser-ctrlTab.js",
|
||||
"browser/base/content/browser-customization.js",
|
||||
"browser/base/content/browser-devedition.js",
|
||||
"browser/base/content/browser-eme.js",
|
||||
"browser/base/content/browser-feeds.js",
|
||||
"browser/base/content/browser-fullScreen.js",
|
||||
"browser/base/content/browser-fullZoom.js",
|
||||
"browser/base/content/browser-gestureSupport.js",
|
||||
"browser/base/content/browser-places.js",
|
||||
"browser/base/content/browser-plugins.js",
|
||||
"browser/base/content/browser-safebrowsing.js",
|
||||
"browser/base/content/browser-sidebar.js",
|
||||
"browser/base/content/browser-social.js",
|
||||
"browser/base/content/browser-syncui.js",
|
||||
"browser/base/content/browser-tabsintitlebar.js",
|
||||
"browser/base/content/browser-thumbnails.js",
|
||||
"browser/base/content/browser-trackingprotection.js",
|
||||
"browser/base/content/browser-data-submission-info-bar.js",
|
||||
"browser/base/content/browser-fxaccounts.js",
|
||||
];
|
||||
|
||||
module.exports = function(context) {
|
||||
return {
|
||||
Program: function(node) {
|
||||
if (!helpers.getIsBrowserMochitest(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let root = helpers.getRootDir(context);
|
||||
for (let script of SCRIPTS) {
|
||||
let fileName = path.join(root, script);
|
||||
try {
|
||||
let globals = helpers.getGlobalsForFile(fileName);
|
||||
helpers.addGlobals(globals, context);
|
||||
}
|
||||
catch (e) {
|
||||
context.report(
|
||||
node,
|
||||
"Could not load globals from file {{filePath}}: {{error}}",
|
||||
{
|
||||
filePath: path.relative(root, fileName),
|
||||
error: e
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
@ -19,25 +19,6 @@ var helpers = require("../helpers");
|
||||
var path = require("path");
|
||||
|
||||
module.exports = function(context) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function importGlobalsFrom(filePath) {
|
||||
// If the file can't be found, let the error go up to the caller so it can
|
||||
// be logged as an error in the current file.
|
||||
var content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Parse the content and get the globals from the ast.
|
||||
var ast = helpers.getAST(content);
|
||||
var globalVars = helpers.getGlobals(ast);
|
||||
|
||||
for (var i = 0; i < globalVars.length; i++) {
|
||||
var varName = globalVars[i];
|
||||
helpers.addVarToScope(varName, context);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -60,7 +41,8 @@ module.exports = function(context) {
|
||||
}
|
||||
|
||||
try {
|
||||
importGlobalsFrom(filePath);
|
||||
let globals = helpers.getGlobalsForFile(filePath);
|
||||
helpers.addGlobals(globals, context);
|
||||
} catch (e) {
|
||||
context.report(
|
||||
node,
|
||||
|
@ -87,15 +87,17 @@ module.exports = function(context) {
|
||||
|
||||
return {
|
||||
Program: function() {
|
||||
if (!helpers.getIsBrowserMochitest(this)) {
|
||||
if (!helpers.getIsTest(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var currentFilePath = helpers.getAbsoluteFilePath(context);
|
||||
var dirName = path.dirname(currentFilePath);
|
||||
var fullHeadjsPath = path.resolve(dirName, "head.js");
|
||||
|
||||
checkFile([currentFilePath, fullHeadjsPath]);
|
||||
if (fs.existsSync(fullHeadjsPath)) {
|
||||
let globals = helpers.getGlobalsForFile(fullHeadjsPath);
|
||||
helpers.addGlobals(globals, context);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -18,6 +18,7 @@
|
||||
"dependencies": {
|
||||
"escope": "^3.2.0",
|
||||
"espree": "^2.2.4",
|
||||
"estraverse": "^4.1.1",
|
||||
"sax": "^1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
|
48
testing/mochitest/browser.eslintrc
Normal file
48
testing/mochitest/browser.eslintrc
Normal file
@ -0,0 +1,48 @@
|
||||
// Parent config file for all browser-chrome files.
|
||||
{
|
||||
"rules": {
|
||||
// Head files want to define globals so don't warn for unused globals
|
||||
"no-unused-vars": [2, {"vars": "local", "args": "none"}],
|
||||
"mozilla/import-headjs-globals": 1,
|
||||
"mozilla/import-browserjs-globals": 1,
|
||||
},
|
||||
|
||||
"env": {
|
||||
"browser": true,
|
||||
},
|
||||
|
||||
// All globals made available in the test environment.
|
||||
"globals": {
|
||||
"add_task": false,
|
||||
"Assert": false,
|
||||
"BrowserTestUtils": false,
|
||||
"ContentTask": false,
|
||||
"EventUtils": false,
|
||||
"executeSoon": false,
|
||||
"export_assertions": false,
|
||||
"finish": false,
|
||||
"getRootDirectory": false,
|
||||
"getTestFilePath": false,
|
||||
"gTestPath": false,
|
||||
"info": false,
|
||||
"is": false,
|
||||
"isnot": false,
|
||||
"ok": false,
|
||||
"promise": false,
|
||||
"registerCleanupFunction": false,
|
||||
"requestLongerTimeout": false,
|
||||
"SimpleTest": false,
|
||||
"SpecialPowers": false,
|
||||
"todo": false,
|
||||
"todo_is": false,
|
||||
"todo_isnot": false,
|
||||
"waitForClipboard": false,
|
||||
"waitForExplicitFinish": false,
|
||||
"waitForFocus": false,
|
||||
"gBrowser": false,
|
||||
"gNavToolbox": false,
|
||||
"gURLBar": false,
|
||||
"gNavigatorBundle": false,
|
||||
"content": false,
|
||||
}
|
||||
}
|
40
testing/mochitest/chrome.eslintrc
Normal file
40
testing/mochitest/chrome.eslintrc
Normal file
@ -0,0 +1,40 @@
|
||||
// Parent config file for all mochitest files.
|
||||
{
|
||||
rules: {
|
||||
// Head files want to define globals so don't warn for unused globals
|
||||
"no-unused-vars": [2, {"vars": "local", "args": "none"}],
|
||||
"mozilla/import-headjs-globals": 1,
|
||||
},
|
||||
|
||||
"env": {
|
||||
"browser": true,
|
||||
},
|
||||
|
||||
// All globals made available in the test environment.
|
||||
"globals": {
|
||||
"add_task": false,
|
||||
"Assert": false,
|
||||
"EventUtils": false,
|
||||
"executeSoon": false,
|
||||
"export_assertions": false,
|
||||
"finish": false,
|
||||
"getRootDirectory": false,
|
||||
"getTestFilePath": false,
|
||||
"gTestPath": false,
|
||||
"info": false,
|
||||
"is": false,
|
||||
"isnot": false,
|
||||
"ok": false,
|
||||
"promise": false,
|
||||
"registerCleanupFunction": false,
|
||||
"requestLongerTimeout": false,
|
||||
"SimpleTest": false,
|
||||
"SpecialPowers": false,
|
||||
"todo": false,
|
||||
"todo_is": false,
|
||||
"todo_isnot": false,
|
||||
"waitForClipboard": false,
|
||||
"waitForExplicitFinish": false,
|
||||
"waitForFocus": false,
|
||||
}
|
||||
}
|
40
testing/mochitest/mochitest.eslintrc
Normal file
40
testing/mochitest/mochitest.eslintrc
Normal file
@ -0,0 +1,40 @@
|
||||
// Parent config file for all mochitest files.
|
||||
{
|
||||
rules: {
|
||||
// Head files want to define globals so don't warn for unused globals
|
||||
"no-unused-vars": [2, {"vars": "local", "args": "none"}],
|
||||
"mozilla/import-headjs-globals": 1,
|
||||
},
|
||||
|
||||
"env": {
|
||||
"browser": true,
|
||||
},
|
||||
|
||||
// All globals made available in the test environment.
|
||||
"globals": {
|
||||
"add_task": false,
|
||||
"Assert": false,
|
||||
"EventUtils": false,
|
||||
"executeSoon": false,
|
||||
"export_assertions": false,
|
||||
"finish": false,
|
||||
"getRootDirectory": false,
|
||||
"getTestFilePath": false,
|
||||
"gTestPath": false,
|
||||
"info": false,
|
||||
"is": false,
|
||||
"isnot": false,
|
||||
"ok": false,
|
||||
"promise": false,
|
||||
"registerCleanupFunction": false,
|
||||
"requestLongerTimeout": false,
|
||||
"SimpleTest": false,
|
||||
"SpecialPowers": false,
|
||||
"todo": false,
|
||||
"todo_is": false,
|
||||
"todo_isnot": false,
|
||||
"waitForClipboard": false,
|
||||
"waitForExplicitFinish": false,
|
||||
"waitForFocus": false,
|
||||
}
|
||||
}
|
43
testing/xpcshell/xpcshell.eslintrc
Normal file
43
testing/xpcshell/xpcshell.eslintrc
Normal file
@ -0,0 +1,43 @@
|
||||
// Parent config file for all xpcshell files.
|
||||
{
|
||||
rules: {
|
||||
// Head files want to define globals so don't warn for unused globals
|
||||
"no-unused-vars": [2, {"vars": "local", "args": "none"}],
|
||||
"mozilla/import-headjs-globals": 1,
|
||||
},
|
||||
|
||||
// All globals made available in the test environment.
|
||||
"globals": {
|
||||
"add_task": false,
|
||||
"add_test": false,
|
||||
"Assert": false,
|
||||
"deepEqual": false,
|
||||
"do_check_eq": false,
|
||||
"do_check_false": false,
|
||||
"do_check_neq": false,
|
||||
"do_check_null": false,
|
||||
"do_check_true": false,
|
||||
"do_execute_soon": false,
|
||||
"do_get_cwd": false,
|
||||
"do_get_file": false,
|
||||
"do_get_idle": false,
|
||||
"do_get_profile": false,
|
||||
"do_load_module": false,
|
||||
"do_parse_document": false,
|
||||
"do_print": false,
|
||||
"do_register_cleanup": false,
|
||||
"do_test_finished": false,
|
||||
"do_test_pending": false,
|
||||
"do_throw": false,
|
||||
"do_timeout": false,
|
||||
"equal": false,
|
||||
"load": false,
|
||||
"notDeepEqual": false,
|
||||
"notEqual": false,
|
||||
"notStrictEqual": false,
|
||||
"ok": false,
|
||||
"run_next_test": false,
|
||||
"run_test": false,
|
||||
"strictEqual": false,
|
||||
}
|
||||
}
|
@ -101,7 +101,7 @@
|
||||
// "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
|
||||
|
||||
// No unnecessary spacing
|
||||
// "no-multi-spaces": [2, { exceptions: { "AssignmentExpression": true, "VariableDeclarator": true, "ArrayExpression": true } }],
|
||||
// "no-multi-spaces": [2, { exceptions: { "AssignmentExpression": true, "VariableDeclarator": true, "ArrayExpression": true, "ObjectExpression": true } }],
|
||||
|
||||
// No reassigning native JS objects
|
||||
// "no-native-reassign": 2,
|
||||
|
5
toolkit/components/aboutmemory/tests/.eslintrc
Normal file
5
toolkit/components/aboutmemory/tests/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/chrome.eslintrc"
|
||||
]
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/addoncompat/tests/browser/.eslintrc
Normal file
5
toolkit/components/addoncompat/tests/browser/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/mochitest/browser.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/alerts/test/.eslintrc
Normal file
5
toolkit/components/alerts/test/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/mochitest.eslintrc"
|
||||
]
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/autocomplete/tests/unit/.eslintrc
Normal file
5
toolkit/components/autocomplete/tests/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/captivedetect/test/unit/.eslintrc
Normal file
5
toolkit/components/captivedetect/test/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/commandlines/test/unit/.eslintrc
Normal file
5
toolkit/components/commandlines/test/unit/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/commandlines/test/unit_unix/.eslintrc
Normal file
5
toolkit/components/commandlines/test/unit_unix/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/commandlines/test/unit_win/.eslintrc
Normal file
5
toolkit/components/commandlines/test/unit_win/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../../testing/xpcshell/xpcshell.eslintrc"
|
||||
]
|
||||
}
|
5
toolkit/components/console/tests/.eslintrc
Normal file
5
toolkit/components/console/tests/.eslintrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../testing/mochitest/chrome.eslintrc"
|
||||
]
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user