Merge mozilla-central to inbound. a=merge CLOSED TREE

This commit is contained in:
Bogdan Tara 2018-08-03 13:28:32 +03:00
commit 4759ec60f7
110 changed files with 1881 additions and 398 deletions

View File

@ -256,9 +256,7 @@ nsContextMenu.prototype = {
InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
} else {
var targetWin = this.ownerDoc.defaultView;
var editingSession = targetWin.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIEditingSession);
var {editingSession} = targetWin.docShell;
InlineSpellCheckerUI.init(editingSession.getEditorForWindow(targetWin));
InlineSpellCheckerUI.initFromEvent(document.popupRangeParent,

View File

@ -2532,10 +2532,20 @@ window._gBrowser = {
},
getTabsToTheEndFrom(aTab) {
let tab;
if (aTab.multiselected) {
// In a multi-select context, pick the rightmost
// selected tab as reference.
let selectedTabs = this.selectedTabs;
tab = selectedTabs[selectedTabs.length - 1];
} else {
tab = aTab;
}
let tabsToEnd = [];
let tabs = this.visibleTabs;
for (let i = tabs.length - 1; i >= 0; --i) {
if (tabs[i] == aTab || tabs[i].pinned) {
if (tabs[i] == tab || tabs[i].pinned) {
break;
}
tabsToEnd.push(tabs[i]);
@ -2543,6 +2553,10 @@ window._gBrowser = {
return tabsToEnd;
},
/**
* In a multi-select context, the tabs (except pinned tabs) that are located to the
* right of the rightmost selected tab will be removed.
*/
removeTabsToTheEndFrom(aTab) {
let tabs = this.getTabsToTheEndFrom(aTab);
if (!this.warnAboutClosingTabs(tabs.length, this.closingTabsEnum.TO_END)) {
@ -2562,7 +2576,6 @@ window._gBrowser = {
tabsToRemove = this.visibleTabs.filter(tab => !tab.multiselected && !tab.pinned);
} else {
tabsToRemove = this.visibleTabs.filter(tab => tab != aTab && !tab.pinned);
this.selectedTab = aTab;
}
if (!this.warnAboutClosingTabs(tabsToRemove.length, this.closingTabsEnum.OTHER)) {
@ -2582,27 +2595,36 @@ window._gBrowser = {
},
removeTabs(tabs) {
let tabsWithBeforeUnload = [];
let lastToClose;
let params = { animate: true };
for (let tab of tabs) {
if (tab.selected) {
lastToClose = tab;
} else if (this._hasBeforeUnload(tab)) {
tabsWithBeforeUnload.push(tab);
} else {
this.removeTab(tab, params);
this._clearMultiSelectionLocked = true;
// Guarantee that _clearMultiSelectionLocked lock gets released.
try {
let tabsWithBeforeUnload = [];
let lastToClose;
let aParams = { animate: true };
for (let tab of tabs) {
if (tab.selected)
lastToClose = tab;
else if (this._hasBeforeUnload(tab))
tabsWithBeforeUnload.push(tab);
else
this.removeTab(tab, aParams);
}
}
for (let tab of tabsWithBeforeUnload) {
this.removeTab(tab, params);
for (let tab of tabsWithBeforeUnload) {
this.removeTab(tab, aParams);
}
// Avoid changing the selected browser several times by removing it,
// if appropriate, lastly.
if (lastToClose) {
this.removeTab(lastToClose, aParams);
}
} catch (e) {
Cu.reportError(e);
}
// Avoid changing the selected browser several times by removing it,
// if appropriate, lastly.
if (lastToClose) {
this.removeTab(lastToClose, params);
}
this._clearMultiSelectionLocked = false;
this.avoidSingleSelectedTab();
},
removeCurrentTab(aParams) {
@ -3693,12 +3715,11 @@ window._gBrowser = {
return;
}
let selectedTabs = this.selectedTabs;
if (selectedTabs.length < 2) {
if (this.multiSelectedTabsCount < 1) {
return;
}
for (let tab of selectedTabs) {
for (let tab of this.selectedTabs) {
tab.removeAttribute("multiselected");
}
this._multiSelectedTabsSet = new WeakSet();
@ -3714,16 +3735,41 @@ window._gBrowser = {
},
/**
* Remove the active tab from the multiselection if it's the only one left there.
* Remove a tab from the multiselection if it's the only one left there.
*
* In fact, some scenario may lead to only one single tab multi-selected,
* this is something to avoid (Chrome does the same)
* Consider 4 tabs A,B,C,D with A having the focus
* 1. select C with Ctrl
* 2. Right-click on B and "Close Tabs to The Right"
*
* Expected result
* C and D closing
* A being the only multi-selected tab, selection should be cleared
*
*
* Single selected tab could even happen with a none-focused tab.
* For exemple with the menu "Close other tabs", it could happen
* with a multi-selected pinned tab.
* For illustration, consider 4 tabs A,B,C,D with B active
* 1. pin A and Ctrl-select it
* 2. Ctrl-select C
* 3. right-click on D and click "Close Other Tabs"
*
* Expected result
* B and C closing
* A[pinned] being the only multi-selected tab, selection should be cleared.
*/
updateActiveTabMultiSelectState() {
if (this.selectedTabs.length == 1) {
avoidSingleSelectedTab() {
if (this.multiSelectedTabsCount == 1 ) {
this.clearMultiSelectedTabs();
}
},
switchToNextMultiSelectedTab() {
this._clearMultiSelectionLocked = true;
// Guarantee that _clearMultiSelectionLocked lock gets released.
try {
let lastMultiSelectedTab = gBrowser.lastMultiSelectedTab;
if (lastMultiSelectedTab != gBrowser.selectedTab) {
@ -3737,6 +3783,7 @@ window._gBrowser = {
} catch (e) {
Cu.reportError(e);
}
this._clearMultiSelectionLocked = false;
},
@ -5098,10 +5145,9 @@ var TabContextMenu = {
gBrowser.getTabsToTheEndFrom(this.contextTab).length == 0;
// Disable "Close other Tabs" if there are no unpinned tabs.
let unpinnedTabsToClose = gBrowser.visibleTabs.length - gBrowser._numPinnedTabs;
if (!this.contextTab.pinned) {
unpinnedTabsToClose--;
}
let unpinnedTabsToClose = multiselectionContext ?
gBrowser.visibleTabs.filter(t => !t.multiselected && !t.pinned).length :
gBrowser.visibleTabs.filter(t => t != this.contextTab && !t.pinned).length;
document.getElementById("context_closeOtherTabs").disabled = unpinnedTabsToClose < 1;
// Only one of close_tab/close_selected_tabs should be visible

View File

@ -2086,7 +2086,7 @@
if (this == gBrowser.selectedTab) {
gBrowser.switchToNextMultiSelectedTab();
}
gBrowser.updateActiveTabMultiSelectState();
gBrowser.avoidSingleSelectedTab();
} else if (this != gBrowser.selectedTab) {
for (let tab of [this, gBrowser.selectedTab]) {
gBrowser.addToMultiSelectedTabs(tab, true);

View File

@ -23,6 +23,7 @@ support-files =
[browser_multiselect_tabs_bookmark.js]
[browser_multiselect_tabs_clear_selection_when_tab_switch.js]
[browser_multiselect_tabs_close_other_tabs.js]
[browser_multiselect_tabs_close_tabs_to_the_right.js]
[browser_multiselect_tabs_close_using_shortcuts.js]
[browser_multiselect_tabs_close.js]
[browser_multiselect_tabs_move_to_new_window_contextmenu.js]

View File

@ -65,23 +65,31 @@ add_task(async function withNotAMultiSelectedTab() {
let tab2 = await addTab();
let tab3 = await addTab();
let tab4 = await addTab();
let tab5 = await addTab();
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
await BrowserTestUtils.switchTab(gBrowser, tab1);
await triggerClickOn(tab2, { ctrlKey: true });
await triggerClickOn(tab5, { ctrlKey: true });
let tab4Pinned = BrowserTestUtils.waitForEvent(tab4, "TabPinned");
gBrowser.pinTab(tab4);
await tab4Pinned;
let tab5Pinned = BrowserTestUtils.waitForEvent(tab5, "TabPinned");
gBrowser.pinTab(tab5);
await tab5Pinned;
ok(!initialTab.multiselected, "InitialTab is not multiselected");
ok(tab1.multiselected, "Tab1 is multiselected");
ok(tab2.multiselected, "Tab2 is multiselected");
ok(!tab3.multiselected, "Tab3 is not multiselected");
ok(!tab4.multiselected, "Tab4 is not multiselected");
ok(tab4.pinned, "Tab4 is pinned");
is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
ok(tab5.multiselected, "Tab5 is multiselected");
ok(tab5.pinned, "Tab5 is pinned");
is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
is(gBrowser.selectedTab, tab1, "Tab1 is the active tab");
let closingTabs = [tab1, tab2, tab3];
@ -101,8 +109,10 @@ add_task(async function withNotAMultiSelectedTab() {
ok(tab2.closing, "Tab2 is closing");
ok(tab3.closing, "Tab3 is closing");
ok(!tab4.closing, "Tab4 is not closing");
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
ok(!tab5.closing, "Tab5 is not closing");
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs, selection is cleared");
is(gBrowser.selectedTab, initialTab, "InitialTab is the active tab now");
BrowserTestUtils.removeTab(tab4);
for (let tab of [tab4, tab5])
BrowserTestUtils.removeTab(tab);
});

View File

@ -0,0 +1,114 @@
const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
const PREF_WARN_ON_CLOSE = "browser.tabs.warnOnCloseOtherTabs";
add_task(async function setPref() {
await SpecialPowers.pushPrefEnv({
set: [
[PREF_MULTISELECT_TABS, true],
[PREF_WARN_ON_CLOSE, false]
]
});
});
add_task(async function withAMultiSelectedTab() {
let tab1 = await addTab();
let tab2 = await addTab();
let tab3 = await addTab();
let tab4 = await addTab();
let tab5 = await addTab();
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
await BrowserTestUtils.switchTab(gBrowser, tab1);
await triggerClickOn(tab3, { ctrlKey: true });
ok(tab1.multiselected, "Tab1 is multiselected");
ok(!tab2.multiselected, "Tab2 is not multiselected");
ok(tab3.multiselected, "Tab3 is multiselected");
ok(!tab4.multiselected, "Tab4 is not multiselected");
ok(!tab5.multiselected, "Tab5 is not multiselected");
is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
let closingTabs = [tab4, tab5];
let tabClosingPromises = [];
for (let tab of closingTabs) {
tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
}
gBrowser.removeTabsToTheEndFrom(tab1);
for (let promise of tabClosingPromises) {
await promise;
}
ok(!tab1.closing, "Tab1 is not closing");
ok(!tab2.closing, "Tab2 is not closing");
ok(!tab3.closing, "Tab3 is not closing");
ok(tab4.closing, "Tab4 is closing");
ok(tab5.closing, "Tab5 is closing");
is(gBrowser.multiSelectedTabsCount, 2, "Two multiselected tabs");
for (let tab of [tab1, tab2, tab3])
BrowserTestUtils.removeTab(tab);
});
add_task(async function withNotAMultiSelectedTab() {
let tab1 = await addTab();
let tab2 = await addTab();
let tab3 = await addTab();
let tab4 = await addTab();
let tab5 = await addTab();
is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
await BrowserTestUtils.switchTab(gBrowser, tab1);
await triggerClickOn(tab3, { ctrlKey: true });
await triggerClickOn(tab5, { ctrlKey: true });
ok(tab1.multiselected, "Tab1 is multiselected");
ok(!tab2.multiselected, "Tab2 is not multiselected");
ok(tab3.multiselected, "Tab3 is multiselected");
ok(!tab4.multiselected, "Tab4 is not multiselected");
ok(tab5.multiselected, "Tab5 is multiselected");
is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
let closingTabs = [tab5];
let tabClosingPromises = [];
for (let tab of closingTabs) {
tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
}
gBrowser.removeTabsToTheEndFrom(tab4);
for (let promise of tabClosingPromises) {
await promise;
}
ok(!tab1.closing, "Tab1 is not closing");
ok(!tab2.closing, "Tab2 is not closing");
ok(!tab3.closing, "Tab3 is not closing");
ok(!tab4.closing, "Tab4 is not closing");
ok(tab5.closing, "Tab5 is closing");
is(gBrowser.multiSelectedTabsCount, 2, "Selection is not cleared");
closingTabs = [tab3, tab4];
tabClosingPromises = [];
for (let tab of closingTabs) {
tabClosingPromises.push(BrowserTestUtils.waitForTabClosing(tab));
}
gBrowser.removeTabsToTheEndFrom(tab2);
for (let promise of tabClosingPromises) {
await promise;
}
ok(!tab1.closing, "Tab1 is not closing");
ok(!tab2.closing, "Tab2 is not closing");
ok(tab3.closing, "Tab3 is closing");
ok(tab4.closing, "Tab4 is closing");
is(gBrowser.multiSelectedTabsCount, 0, "Selection is cleared");
for (let tab of [tab1, tab2])
BrowserTestUtils.removeTab(tab);
});

View File

@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/></svg>
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill-opacity="context-fill-opacity" fill="context-fill" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 608 B

View File

@ -24,11 +24,6 @@ address-form[address-fields]:not([address-fields~='tel']) #tel-container {
display: none !important;
}
label[required] > span:first-of-type::after {
/* The asterisk should be localized, bug 1472278 */
content: "*";
}
.error-text:not(:empty) {
color: #fff;
background-color: #d70022;

View File

@ -153,18 +153,7 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ
this.formHandler.loadRecord(record);
// Add validation to some address fields
for (let formElement of this.form.elements) {
let container = formElement.closest(`#${formElement.id}-container`);
if (formElement.localName == "button" || !container) {
continue;
}
let required = formElement.required && !formElement.disabled;
if (required) {
container.setAttribute("required", "true");
} else {
container.removeAttribute("required");
}
}
this.updateRequiredState();
let shippingAddressErrors = request.paymentDetails.shippingAddressErrors;
for (let [errorName, errorSelector] of Object.entries(this._errorFieldMap)) {
@ -205,6 +194,7 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ
}
}
}
handleEvent(event) {
switch (event.type) {
case "click": {
@ -246,6 +236,23 @@ export default class AddressForm extends PaymentStateSubscriberMixin(PaymentRequ
}
}
updateRequiredState() {
for (let formElement of this.form.elements) {
let container = formElement.closest(`#${formElement.id}-container`);
if (formElement.localName == "button" || !container) {
continue;
}
let span = container.querySelector("span");
span.setAttribute("fieldRequiredSymbol", this.dataset.fieldRequiredSymbol);
let required = formElement.required && !formElement.disabled;
if (required) {
container.setAttribute("required", "true");
} else {
container.removeAttribute("required");
}
}
}
async saveRecord() {
let record = this.formHandler.buildFormObject();
let currentState = this.requestStore.getState();

View File

@ -181,6 +181,7 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
}
}
this.updateRequiredState();
this.updateSaveButtonState();
}
@ -299,6 +300,20 @@ export default class BasicCardForm extends PaymentStateSubscriberMixin(PaymentRe
this.saveButton.disabled = !this.form.checkValidity();
}
updateRequiredState() {
for (let formElement of this.form.elements) {
let container = formElement.closest("label") || formElement.closest("div");
let span = container.querySelector("span");
span.setAttribute("fieldRequiredSymbol", this.dataset.fieldRequiredSymbol);
let required = formElement.required && !formElement.disabled;
if (required) {
container.setAttribute("required", "true");
} else {
container.removeAttribute("required");
}
}
}
async saveRecord() {
let record = this.formHandler.buildFormObject();
let currentState = this.requestStore.getState();

View File

@ -0,0 +1,7 @@
/* 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/. */
:-moz-any(label, div)[required] > span:first-of-type::after {
content: attr(fieldRequiredSymbol);
}

View File

@ -18,7 +18,7 @@ export default class PaymentMethodPicker extends RichPicker {
this.dropdown.setAttribute("option-type", "basic-card-option");
this.securityCodeInput = document.createElement("input");
this.securityCodeInput.autocomplete = "off";
this.securityCodeInput.placeholder = "CVV"; /* XXX Bug 1473772 */
this.securityCodeInput.placeholder = this.dataset.cvvPlaceholder;
this.securityCodeInput.size = 3;
this.securityCodeInput.classList.add("security-code");
this.securityCodeInput.addEventListener("change", this);

View File

@ -11,6 +11,7 @@
<!ENTITY viewAllItems "View All Items">
<!ENTITY paymentSummaryTitle "Your Payment">
<!ENTITY header.payTo "Pay to">
<!ENTITY fieldRequiredSymbol "*">
<!ENTITY shippingAddressLabel "Shipping Address">
<!ENTITY deliveryAddressLabel "Delivery Address">
@ -33,6 +34,7 @@
<!ENTITY billingAddress.editPage.title "Edit Billing Address">
<!ENTITY basicCard.addPage.title "Add Credit Card">
<!ENTITY basicCard.editPage.title "Edit Credit Card">
<!ENTITY basicCard.cvv.placeholder "CVV&fieldRequiredSymbol;">
<!ENTITY payer.addPage.title "Add Payer Contact">
<!ENTITY payer.editPage.title "Edit Payer Contact">
<!ENTITY payerLabel "Contact Information">
@ -82,6 +84,7 @@
<link rel="stylesheet" href="components/basic-card-option.css"/>
<link rel="stylesheet" href="components/shipping-option.css"/>
<link rel="stylesheet" href="components/payment-details-item.css"/>
<link rel="stylesheet" href="containers/form.css"/>
<link rel="stylesheet" href="containers/address-form.css"/>
<link rel="stylesheet" href="containers/basic-card-form.css"/>
<link rel="stylesheet" href="containers/order-details.css"/>
@ -124,6 +127,7 @@
<payment-method-picker selected-state-key="selectedPaymentCard"
data-add-link-label="&basicCard.addLink.label;"
data-edit-link-label="&basicCard.editLink.label;"
data-cvv-placeholder="&basicCard.cvv.placeholder;"
label="&paymentMethodsLabel;">
</payment-method-picker>
<address-picker class="payer-related"
@ -162,6 +166,7 @@
data-update-button-label="&basicCardPage.updateButton.label;"
data-cancel-button-label="&cancelPaymentButton.label;"
data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
data-field-required-symbol="&fieldRequiredSymbol;"
hidden="hidden"></basic-card-form>
<address-form id="address-page"
@ -171,6 +176,7 @@
data-add-button-label="&addressPage.addButton.label;"
data-update-button-label="&addressPage.updateButton.label;"
data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
data-field-required-symbol="&fieldRequiredSymbol;"
hidden="hidden"></address-form>
<completion-error-page id="completion-timeout-error" class="illustrated"

View File

@ -17,6 +17,7 @@ Test the address-form element
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
<link rel="stylesheet" type="text/css" href="../../res/containers/form.css"/>
<link rel="stylesheet" type="text/css" href="../../res/containers/address-form.css"/>
</head>
<body>
@ -275,6 +276,7 @@ add_task(async function test_restricted_address_fields() {
add_task(async function test_field_validation() {
let form = new AddressForm();
form.dataset.fieldRequiredSymbol = "*";
await form.promiseReady;
display.appendChild(form);
await asyncElementRendered();
@ -298,8 +300,11 @@ add_task(async function test_field_validation() {
for (let field of requiredFields) {
let container = field.closest("label");
ok(container.hasAttribute("required"), "Container should have required attribute");
let label = field.closest("label").querySelector("span");
is(getComputedStyle(label, "::after").content, "\"*\"", "Asterisk should be on " + field.id);
let span = container.querySelector("span");
is(span.getAttribute("fieldRequiredSymbol"), "*",
"span should have asterisk as fieldRequiredSymbol");
is(getComputedStyle(span, "::after").content, "attr(fieldRequiredSymbol)",
"Asterisk should be on " + field.id);
}
countrySelect.selectedIndex = [...countrySelect.options].findIndex(o => o.value == "US");

View File

@ -136,6 +136,38 @@ add_task(async function test_saveButton() {
form.remove();
});
add_task(async function test_requiredAttributePropagated() {
let form = new BasicCardForm();
await form.promiseReady;
display.appendChild(form);
await asyncElementRendered();
let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
ok(requiredElements.length, "There should be at least one required element");
for (let element of requiredElements) {
let container = element.closest("label") || element.closest("div");
ok(container.hasAttribute("required"), "Container should also be marked as required");
}
// Now test that toggling the `required` attribute will affect the container.
let sampleRequiredElement = requiredElements[0];
let sampleRequiredContainer = sampleRequiredElement.closest("label") ||
sampleRequiredElement.closest("div");
sampleRequiredElement.removeAttribute("required");
await form.requestStore.setState({});
await asyncElementRendered();
ok(!sampleRequiredElement.hasAttribute("required"),
`"required" attribute should still be removed from element (${sampleRequiredElement.id})`);
ok(!sampleRequiredContainer.hasAttribute("required"),
`"required" attribute should be removed from container`);
sampleRequiredElement.setAttribute("required", "true");
await form.requestStore.setState({});
await asyncElementRendered();
ok(sampleRequiredContainer.hasAttribute("required"),
"`required` attribute is re-added to container");
form.remove();
});
add_task(async function test_genericError() {
let form = new BasicCardForm();
await form.requestStore.setState({
@ -262,6 +294,14 @@ add_task(async function test_edit() {
is(form.saveButton.textContent, "Update", "Check label");
checkCCForm(form, card1);
let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
ok(requiredElements.length, "There should be at least one required element");
for (let element of requiredElements) {
let container = element.closest("label") || element.closest("div");
ok(element.hasAttribute("required"), "Element should be marked as required");
ok(container.hasAttribute("required"), "Container should also be marked as required");
}
info("test future year");
card1["cc-exp-year"] = 2100;

View File

@ -48,9 +48,9 @@ function loadContentWindow(webNavigation, uri) {
async function takeScreenshot(fullWidth, fullHeight, contentWidth, contentHeight, path, url) {
try {
let windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
var webNavigation = windowlessBrowser.QueryInterface(Ci.nsIWebNavigation);
let contentWindow = await loadContentWindow(webNavigation, url);
var windowlessBrowser = Services.appShell.createWindowlessBrowser(false);
// nsIWindowlessBrowser inherits from nsIWebNavigation.
let contentWindow = await loadContentWindow(windowlessBrowser, url);
contentWindow.resizeTo(contentWidth, contentHeight);
let canvas = contentWindow.document.createElementNS("http://www.w3.org/1999/xhtml", "html:canvas");
@ -82,8 +82,8 @@ async function takeScreenshot(fullWidth, fullHeight, contentWidth, contentHeight
} catch (e) {
dump("Failure taking screenshot: " + e + "\n");
} finally {
if (webNavigation) {
webNavigation.close();
if (windowlessBrowser) {
windowlessBrowser.close();
}
}
}

View File

@ -344,10 +344,13 @@ var PocketReader = {
if (this.hidden) {
Services.mm.broadcastAsyncMessage("Reader:RemoveButton", { id: "pocket-button" });
} else {
Services.mm.broadcastAsyncMessage("Reader:AddButton",
{ id: "pocket-button",
title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
image: "chrome://pocket/content/panels/img/pocket.svg#pocket-mark" });
Services.mm.broadcastAsyncMessage("Reader:AddButton", {
id: "pocket-button",
title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
image: "chrome://pocket/content/panels/img/pocket-outline.svg",
width: 20,
height: 20,
});
}
},
receiveMessage(message) {
@ -357,9 +360,13 @@ var PocketReader = {
if (this.hidden)
break;
message.target.messageManager.
sendAsyncMessage("Reader:AddButton", { id: "pocket-button",
title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
image: "chrome://pocket/content/panels/img/pocket.svg#pocket-mark"});
sendAsyncMessage("Reader:AddButton", {
id: "pocket-button",
title: gPocketBundle.GetStringFromName("pocket-button.tooltiptext"),
image: "chrome://pocket/content/panels/img/pocket-outline.svg",
width: 20,
height: 20,
});
break;
}
case "Reader:Clicked-pocket-button": {

View File

@ -0,0 +1,12 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!--
This file is the same as skin/shared/pocket-outline.svg and the two should be
kept in sync. The only reason this file exists is that it lives in the
contentaccessible content directory so that Firefox's reader mode can use it.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill-opacity="context-fill-opacity" fill="context-fill" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 852 B

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24">
<style>
use:not(:target) {
display: none;
}
use {
fill: #808080;
}
use[id$="-added"] {
fill: #ee4056;
}
</style>
<defs>
<path id="pocket-mark-shape" d="M21.901,4.204C21.642,3.484,20.956,3,20.196,3h-0.01h-1.721H3.814C3.067,3,2.385,3.474,2.119,4.179 C2.04,4.388,2,4.606,2,4.828v6.082l0.069,1.21c0.29,2.751,1.707,5.155,3.899,6.832c0.039,0.03,0.079,0.06,0.119,0.089l0.025,0.018 c1.175,0.866,2.491,1.452,3.91,1.741C10.677,20.932,11.347,21,12.013,21c0.615,0,1.232-0.057,1.839-0.171 c0.073-0.014,0.145-0.028,0.219-0.044c0.02-0.004,0.042-0.012,0.064-0.023c1.359-0.299,2.621-0.87,3.753-1.704l0.025-0.018 c0.04-0.029,0.08-0.059,0.119-0.089c2.192-1.677,3.609-4.08,3.898-6.832L22,10.91V4.828C22,4.618,21.975,4.409,21.901,4.204z M17.667,10.539l-4.704,4.547c-0.266,0.256-0.608,0.385-0.949,0.385c-0.342,0-0.684-0.129-0.949-0.385l-4.705-4.547 c-0.547-0.528-0.565-1.403-0.04-1.954c0.524-0.551,1.392-0.569,1.939-0.041l3.756,3.63l3.755-3.63 c0.547-0.528,1.415-0.51,1.939,0.04C18.231,9.136,18.213,10.011,17.667,10.539z"/>
</defs>
<use id="pocket-mark" xlink:href="#pocket-mark-shape"/>
<use id="pocket-mark-added" xlink:href="#pocket-mark-shape"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,13 @@
<!-- 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/. -->
<!--
This file is the same as content/panels/img/pocket-outline.svg and the two
should be kept in sync. The only reason that file exists is that it lives in
the contentaccessible content directory so that Firefox's reader mode can use
it.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill-opacity="context-fill-opacity" fill="context-fill" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 861 B

View File

@ -7,9 +7,14 @@
fill: #fbfbfb;
}
#pocket-button,
#pageAction-panel-pocket {
list-style-image: url("chrome://pocket-shared/skin/pocket-outline.svg");
}
#appMenu-library-pocket-button,
#pageAction-panel-pocket,
#pocket-button {
#pocket-button-box[open="true"] > #pocket-button,
#pocket-button-box[pocketed="true"] > #pocket-button {
list-style-image: url("chrome://pocket-shared/skin/pocket.svg");
}

View File

@ -2,5 +2,5 @@
- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill-opacity="context-fill-opacity" fill="context-fill" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/>
<path fill-opacity="context-fill-opacity" fill="context-fill" fill-rule="evenodd" d="M14.496.933C15.323.933 16 1.61 16 2.436v4.497c0 4.418-3.581 8-8 8-4.418 0-8-3.582-8-8V2.436C0 1.61.677.933 1.504.933h12.992zM8.013 8.222L4.994 5.319c-.434-.418-1.125-.404-1.543.03-.418.434-.404 1.125.03 1.543l3.776 3.63c.423.406 1.09.406 1.513 0l3.775-3.63c.435-.418.448-1.109.03-1.543-.417-.434-1.108-.448-1.542-.03l-3.02 2.903z"/>
</svg>

Before

Width:  |  Height:  |  Size: 634 B

After

Width:  |  Height:  |  Size: 723 B

View File

@ -52,6 +52,7 @@ buildscript {
ext.kotlin_version = '1.2.41'
ext.support_library_version = '26.1.0'
ext.jacoco_version = '0.8.1'
if (gradle.mozconfig.substs.MOZ_ANDROID_GOOGLE_PLAY_SERVICES) {
ext.google_play_services_version = '15.0.1'

View File

@ -66,3 +66,10 @@ def javac_version(javac):
return version
except subprocess.CalledProcessError as e:
die('Failed to get javac version: %s', e.output)
# Java Code Coverage
# ========================================================
option('--enable-java-coverage', env='MOZ_JAVA_CODE_COVERAGE', help='Enable Java code coverage')
set_config('MOZ_JAVA_CODE_COVERAGE', depends('--enable-java-coverage')(lambda v: bool(v)))

View File

@ -408,6 +408,23 @@ void Elf::normalize()
}
}
}
ElfSegment* prevLoad = nullptr;
for (auto& it : segments) {
if (it->getType() == PT_LOAD) {
if (prevLoad) {
size_t alignedPrevEnd =
(prevLoad->getAddr() + prevLoad->getMemSize() + prevLoad->getAlign() - 1)
& ~(prevLoad->getAlign() - 1);
size_t alignedStart = it->getAddr() & ~(it->getAlign() - 1);
if (alignedPrevEnd > alignedStart) {
throw std::runtime_error("Segments overlap");
}
}
prevLoad = it;
}
}
// fixup ehdr before writing
if (ehdr->e_phnum != segments.size()) {
ehdr->e_phnum = segments.size();
@ -553,14 +570,6 @@ unsigned int ElfSection::getOffset()
if ((getType() != SHT_NOBITS) && (offset & (getAddrAlign() - 1)))
offset = (offset | (getAddrAlign() - 1)) + 1;
// Two subsequent sections can't be mapped in the same page in memory
// if they aren't in the same 4K block on disk.
if ((getType() != SHT_NOBITS) && getAddr()) {
if (((offset >> 12) != (previous->getOffset() >> 12)) &&
((getAddr() >> 12) == (previous->getAddr() >> 12)))
throw std::runtime_error("Moving section would require overlapping segments");
}
return (shdr.sh_offset = offset);
}

View File

@ -8,6 +8,7 @@
#include <cstdlib>
#include <cstdio>
#include "elfxx.h"
#include "mozilla/CheckedInt.h"
#define ver "0"
#define elfhack_data ".elfhack.data.v" ver
@ -196,12 +197,22 @@ public:
}
bool isRelocatable() {
return true;
return false;
}
unsigned int getEntryPoint() {
return entry_point;
}
void insertBefore(ElfSection *section, bool dirty = true) override {
// Adjust the address so that this section is adjacent to the one it's
// being inserted before. This avoids creating holes which subsequently
// might lead the PHDR-adjusting code to create unnecessary additional
// PT_LOADs.
shdr.sh_addr = (section->getAddr() - shdr.sh_size) & ~(shdr.sh_addralign - 1);
ElfSection::insertBefore(section, dirty);
}
private:
void add_code_section(ElfSection *section)
{
@ -517,6 +528,238 @@ void maybe_split_segment(Elf *elf, ElfSegment *segment, bool fill)
}
}
// EH_FRAME constants
static const char DW_EH_PE_absptr = 0x00;
static const char DW_EH_PE_omit = 0xff;
// Data size
static const char DW_EH_PE_LEB128 = 0x01;
static const char DW_EH_PE_data2 = 0x02;
static const char DW_EH_PE_data4 = 0x03;
static const char DW_EH_PE_data8 = 0x04;
// Data signedness
static const char DW_EH_PE_signed = 0x08;
// Modifiers
static const char DW_EH_PE_pcrel = 0x10;
// Return the data size part of the encoding value
static char encoding_data_size(char encoding)
{
return encoding & 0x07;
}
// Advance `step` bytes in the buffer at `data` with size `size`, returning
// the advanced buffer pointer and remaining size.
// Returns true if step <= size.
static bool advance_buffer(char** data, size_t* size, size_t step)
{
if (step > *size)
return false;
*data += step;
*size -= step;
return true;
}
// Advance in the given buffer, skipping the full length of the variable-length
// encoded LEB128 type in CIE/FDE data.
static bool skip_LEB128(char** data, size_t* size)
{
if (!*size)
return false;
while (*size && (*(*data)++ & (char)0x80)) {
(*size)--;
}
return true;
}
// Advance in the given buffer, skipping the full length of a pointer encoded
// with the given encoding.
static bool skip_eh_frame_pointer(char** data, size_t* size, char encoding)
{
switch (encoding_data_size(encoding)) {
case DW_EH_PE_data2:
return advance_buffer(data, size, 2);
case DW_EH_PE_data4:
return advance_buffer(data, size, 4);
case DW_EH_PE_data8:
return advance_buffer(data, size, 8);
case DW_EH_PE_LEB128:
return skip_LEB128(data, size);
}
throw std::runtime_error("unreachable");
}
// Specialized implementations for adjust_eh_frame_pointer().
template <typename T>
static bool adjust_eh_frame_sized_pointer(char** data, size_t* size, ElfSection* eh_frame,
unsigned int origAddr, Elf* elf)
{
if (*size < sizeof(T))
return false;
serializable<FixedSizeData<T>> pointer(*data, *size, elf->getClass(), elf->getData());
mozilla::CheckedInt<T> value = pointer.value;
if (origAddr < eh_frame->getAddr()) {
unsigned int diff = eh_frame->getAddr() - origAddr;
value -= diff;
} else {
unsigned int diff = origAddr - eh_frame->getAddr();
value += diff;
}
if (!value.isValid())
throw std::runtime_error("Overflow while adjusting eh_frame");
pointer.value = value.value();
pointer.serialize(*data, *size, elf->getClass(), elf->getData());
return advance_buffer(data, size, sizeof(T));
}
// In the given eh_frame section, adjust the pointer with the given encoding, pointed to
// by the given buffer (`data`, `size`), considering the eh_frame section was originally
// at `origAddr`.
// Also advances in the buffer.
static bool adjust_eh_frame_pointer(char** data, size_t* size, char encoding, ElfSection* eh_frame,
unsigned int origAddr, Elf* elf)
{
if ((encoding & 0x70) != DW_EH_PE_pcrel)
return skip_eh_frame_pointer(data, size, encoding);
if (encoding & DW_EH_PE_signed) {
switch (encoding_data_size(encoding)) {
case DW_EH_PE_data2:
return adjust_eh_frame_sized_pointer<int16_t>(data, size, eh_frame, origAddr, elf);
case DW_EH_PE_data4:
return adjust_eh_frame_sized_pointer<int32_t>(data, size, eh_frame, origAddr, elf);
case DW_EH_PE_data8:
return adjust_eh_frame_sized_pointer<int64_t>(data, size, eh_frame, origAddr, elf);
}
} else {
switch (encoding_data_size(encoding)) {
case DW_EH_PE_data2:
return adjust_eh_frame_sized_pointer<uint16_t>(data, size, eh_frame, origAddr, elf);
case DW_EH_PE_data4:
return adjust_eh_frame_sized_pointer<uint32_t>(data, size, eh_frame, origAddr, elf);
case DW_EH_PE_data8:
return adjust_eh_frame_sized_pointer<uint64_t>(data, size, eh_frame, origAddr, elf);
}
}
throw std::runtime_error("Unsupported eh_frame pointer encoding");
}
// The eh_frame section may contain "PC"-relative pointers. If we move the section,
// those need to be adjusted. Other type of pointers are relative to sections we
// don't touch.
static void adjust_eh_frame(ElfSection* eh_frame, unsigned int origAddr, Elf* elf)
{
if (eh_frame->getAddr() == origAddr) // nothing to do;
return;
char* data = const_cast<char*>(eh_frame->getData());
size_t size = eh_frame->getSize();
char LSDAencoding = DW_EH_PE_omit;
char FDEencoding = DW_EH_PE_absptr;
bool hasZ = false;
// Decoding of eh_frame based on https://www.airs.com/blog/archives/460
while (size) {
if (size < 2 * sizeof(uint32_t)) goto malformed;
serializable<FixedSizeData<uint32_t>> entryLength(data, size, elf->getClass(), elf->getData());
if (!advance_buffer(&data, &size, sizeof(uint32_t))) goto malformed;
char* cursor = data;
size_t length = entryLength.value;
serializable<FixedSizeData<uint32_t>> id(data, size, elf->getClass(), elf->getData());
if (!advance_buffer(&cursor, &length, sizeof(uint32_t))) goto malformed;
if (id.value == 0) {
// This is a Common Information Entry
if (length < 2) goto malformed;
// Reset LSDA and FDE encodings, and hasZ for subsequent FDEs.
LSDAencoding = DW_EH_PE_omit;
FDEencoding = DW_EH_PE_absptr;
hasZ = false;
// CIE version. Should only be 1 or 3.
char version = *cursor++; length--;
if (version != 1 && version != 3) {
throw std::runtime_error("Unsupported eh_frame version");
}
// NUL terminated string.
const char* augmentationString = cursor;
size_t l = strnlen(augmentationString, length - 1);
if (l == length - 1) goto malformed;
if (!advance_buffer(&cursor, &length, l + 1)) goto malformed;
// Skip code alignment factor (LEB128)
if (!skip_LEB128(&cursor, &length)) goto malformed;
// Skip data alignment factor (LEB128)
if (!skip_LEB128(&cursor, &length)) goto malformed;
// Skip return address register (single byte in CIE version 1, LEB128
// in CIE version 3)
if (version == 1) {
if (!advance_buffer(&cursor, &length, 1)) goto malformed;
} else {
if (!skip_LEB128(&cursor, &length)) goto malformed;
}
// Past this, it's data driven by the contents of the augmentation string.
for (size_t i = 0; i < l; i++) {
if (!length) goto malformed;
switch (augmentationString[i]) {
case 'z':
if (!skip_LEB128(&cursor, &length)) goto malformed;
hasZ = true;
break;
case 'L':
LSDAencoding = *cursor++;
length--;
break;
case 'R':
FDEencoding = *cursor++;
length--;
break;
case 'P':
{
char encoding = *cursor++;
length--;
if (!adjust_eh_frame_pointer(&cursor, &length, encoding, eh_frame, origAddr, elf))
goto malformed;
}
break;
default:
goto malformed;
}
}
} else {
// This is a Frame Description Entry
// Starting address
if (!adjust_eh_frame_pointer(&cursor, &length, FDEencoding, eh_frame, origAddr, elf))
goto malformed;
if (LSDAencoding != DW_EH_PE_omit) {
// Skip number of bytes, same size as the starting address.
if (!skip_eh_frame_pointer(&cursor, &length, FDEencoding)) goto malformed;
if (hasZ) {
if (!skip_LEB128(&cursor, &length)) goto malformed;
}
// pointer to the LSDA.
if (!adjust_eh_frame_pointer(&cursor, &length, LSDAencoding, eh_frame, origAddr, elf))
goto malformed;
}
}
data += entryLength.value; size -= entryLength.value;
}
return;
malformed:
throw std::runtime_error("malformed .eh_frame");
}
template <typename Rel_Type>
int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type2, bool force, bool fill)
{
@ -575,7 +818,8 @@ int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type
std::vector<Rel_Type> new_rels;
Elf_RelHack relhack_entry;
relhack_entry.r_offset = relhack_entry.r_info = 0;
size_t init_array_reloc = 0;
std::vector<Rel_Type> init_array_relocs;
size_t init_array_insert = 0;
for (typename std::vector<Rel_Type>::iterator i = section->rels.begin();
i != section->rels.end(); ++i) {
// We don't need to keep R_*_NONE relocations
@ -612,14 +856,11 @@ int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type
}
}
}
// Keep track of the relocation associated with the first init_array entry.
if (init_array && i->r_offset == init_array->getAddr()) {
if (init_array_reloc) {
fprintf(stderr, "Found multiple relocations for the first init_array entry. Skipping\n");
return -1;
}
new_rels.push_back(*i);
init_array_reloc = new_rels.size();
// Keep track of the relocations associated with the init_array section.
if (init_array && i->r_offset >= init_array->getAddr() &&
i->r_offset < init_array->getAddr() + init_array->getSize()) {
init_array_relocs.push_back(*i);
init_array_insert = new_rels.size();
} else if (!(loc.getSection()->getFlags() & SHF_WRITE) || (ELF32_R_TYPE(i->r_info) != rel_type)) {
// Don't pack relocations happening in non writable sections.
// Our injected code is likely not to be allowed to write there.
@ -656,49 +897,75 @@ int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type
relhack_entry.r_offset = relhack_entry.r_info = 0;
relhack->push_back(relhack_entry);
if (init_array && !init_array_reloc) {
if (init_array) {
// Some linkers create a DT_INIT_ARRAY section that, for all purposes,
// is empty: it only contains 0x0 or 0xffffffff pointers with no relocations.
// In some other cases, there can be null pointers with no relocations in
// the middle of the section. Example: crtend_so.o in the Android NDK contains
// a sized .init_array with a null pointer and no relocation, which ends up
// in all Android libraries, and in some cases it ends up in the middle of
// the final .init_array section.
// If we have such a reusable slot at the beginning of .init_array, we just
// use it. It we have one in the middle of .init_array, we slide its content
// to move the "hole" at the beginning and use it there (we need our injected
// code to run before any other).
// Otherwise, replace the first entry and keep the original pointer.
std::sort(init_array_relocs.begin(), init_array_relocs.end(),
[](Rel_Type& a, Rel_Type& b) { return a.r_offset < b.r_offset; });
size_t expected = init_array->getAddr();
const size_t zero = 0;
const size_t all = SIZE_MAX;
const char *data = init_array->getData();
size_t length = Elf_Addr::size(elf->getClass());
bool empty = true;
for (size_t off = 0; off < init_array->getSize(); off += length) {
if (memcmp(data + off, &zero, length) &&
memcmp(data + off, &all, length)) {
empty = false;
size_t off = 0;
for (; off < init_array_relocs.size(); off++) {
auto& r = init_array_relocs[off];
if (r.r_offset >= expected + length &&
(memcmp(data + off * length, &zero, length) == 0 ||
memcmp(data + off * length, &all, length) == 0)) {
// We found a hole, move the preceding entries.
while (off) {
auto& p = init_array_relocs[--off];
if (ELF32_R_TYPE(p.r_info) == rel_type) {
unsigned int addend = get_addend(&p, elf);
p.r_offset += length;
set_relative_reloc(&p, elf, addend);
} else {
fprintf(stderr, "Unsupported relocation type in DT_INIT_ARRAY. Skipping\n");
return -1;
}
}
break;
}
expected = r.r_offset + length;
}
// If we encounter such an empty DT_INIT_ARRAY section, we add a
// relocation for its first entry to point to our init. Code further
// below will take care of actually setting the right r_info and
// r_addend for the relocation, as if we had a normal DT_INIT_ARRAY
// section.
if (empty) {
new_rels.emplace_back();
init_array_reloc = new_rels.size();
Rel_Type *rel = &new_rels[init_array_reloc - 1];
rel->r_offset = init_array->getAddr();
if (off == 0) {
// We either found a hole above, and can now use the first entry,
// or the init_array section is effectively empty (see further above)
// and we also can use the first entry.
// Either way, code further below will take care of actually setting
// the right r_info and r_added for the relocation.
Rel_Type rel;
rel.r_offset = init_array->getAddr();
init_array_relocs.insert(init_array_relocs.begin(), rel);
} else {
fprintf(stderr, "Didn't find relocation for DT_INIT_ARRAY's first entry. Skipping\n");
return -1;
}
} else if (init_array) {
Rel_Type *rel = &new_rels[init_array_reloc - 1];
unsigned int addend = get_addend(rel, elf);
// Use relocated value of DT_INIT_ARRAY's first entry for the
// function to be called by the injected code.
if (ELF32_R_TYPE(rel->r_info) == rel_type) {
original_init = addend;
} else if (ELF32_R_TYPE(rel->r_info) == rel_type2) {
ElfSymtab_Section *symtab = (ElfSymtab_Section *)section->getLink();
original_init = symtab->syms[ELF32_R_SYM(rel->r_info)].value.getValue() + addend;
} else {
fprintf(stderr, "Unsupported relocation type for DT_INIT_ARRAY's first entry. Skipping\n");
return -1;
// Use relocated value of DT_INIT_ARRAY's first entry for the
// function to be called by the injected code.
auto& rel = init_array_relocs[0];
unsigned int addend = get_addend(&rel, elf);
if (ELF32_R_TYPE(rel.r_info) == rel_type) {
original_init = addend;
} else if (ELF32_R_TYPE(rel.r_info) == rel_type2) {
ElfSymtab_Section *symtab = (ElfSymtab_Section *)section->getLink();
original_init = symtab->syms[ELF32_R_SYM(rel.r_info)].value.getValue() + addend;
} else {
fprintf(stderr, "Unsupported relocation type for DT_INIT_ARRAY's first entry. Skipping\n");
return -1;
}
}
new_rels.insert(std::next(new_rels.begin(), init_array_insert), init_array_relocs.begin(), init_array_relocs.end());
}
unsigned int mprotect_cb = 0;
@ -779,6 +1046,8 @@ int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type
}
}
size_t old_size = section->getSize();
section->rels.assign(new_rels.begin(), new_rels.end());
section->shrink(new_rels.size() * section->getEntSize());
@ -800,20 +1069,48 @@ int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type
return -1;
}
unsigned int old_exec = first_executable->getOffset();
relhack->insertBefore(section);
relhackcode->insertBefore(first_executable);
// Trying to get first_executable->getOffset() now may throw if the new
// layout would require it to move, so we look at the end of the relhack
// code section instead, comparing it to where the first executable
// section used to start.
if (relhackcode->getOffset() + relhackcode->getSize() >= old_exec) {
// Don't try further if we can't gain from the relocation section size change.
size_t align = first_executable->getSegmentByType(PT_LOAD)->getAlign();
size_t new_size = relhack->getSize() + relhackcode->getSize();
if (!force && (new_size >= old_size || old_size - new_size < align)) {
fprintf(stderr, "No gain. Skipping\n");
return -1;
}
// .eh_frame/.eh_frame_hdr may be between the relocation sections and the
// executable sections. When that happens, we may end up creating a separate
// PT_LOAD for just both of them because they are not considered relocatable.
// But they are, in fact, kind of relocatable, albeit with some manual work.
// Which we'll do here.
ElfSegment* eh_frame_segment = elf->getSegmentByType(PT_GNU_EH_FRAME);
ElfSection* eh_frame_hdr = eh_frame_segment ? eh_frame_segment->getFirstSection() : nullptr;
// The .eh_frame section usually follows the eh_frame_hdr section.
ElfSection* eh_frame = eh_frame_hdr ? eh_frame_hdr->getNext() : nullptr;
if (eh_frame_hdr && !eh_frame) {
throw std::runtime_error("Expected to find an .eh_frame section after .eh_frame_hdr");
}
if (eh_frame && strcmp(eh_frame->getName(), ".eh_frame") == 0) {
// The distance between both sections needs to be preserved because eh_frame_hdr
// contains relative offsets to eh_frame. Well, they could be relocated too, but
// it's not worth the effort for the few number of bytes this would save.
size_t distance = eh_frame->getAddr() - eh_frame_hdr->getAddr();
ElfSection* previous = eh_frame_hdr->getPrevious();
eh_frame_hdr->getShdr().sh_addr =
(previous->getAddr() + previous->getSize() + eh_frame_hdr->getAddrAlign() - 1)
& ~(eh_frame_hdr->getAddrAlign() - 1);
unsigned int origAddr = eh_frame->getAddr();
eh_frame->getShdr().sh_addr =
(eh_frame_hdr->getAddr() + eh_frame_hdr->getSize() + eh_frame->getAddrAlign() - 1)
& ~(eh_frame->getAddrAlign() - 1);
// Re-adjust the eh_frame_hdr address to keep the original distance.
eh_frame_hdr->getShdr().sh_addr = eh_frame->getAddr() - distance;
eh_frame_hdr->markDirty();
adjust_eh_frame(eh_frame, origAddr, elf);
}
// Adjust PT_LOAD segments
for (ElfSegment *segment = elf->getSegmentByType(PT_LOAD); segment;
segment = elf->getSegmentByType(PT_LOAD, segment)) {
@ -827,9 +1124,9 @@ int do_relocation_section(Elf *elf, unsigned int rel_type, unsigned int rel_type
// Adjust the first DT_INIT_ARRAY entry to point at the injected code
// by transforming its relocation into a relative one pointing to the
// address of the injected code.
Rel_Type *rel = &section->rels[init_array_reloc - 1];
Rel_Type *rel = &section->rels[init_array_insert];
rel->r_info = ELF32_R_INFO(0, rel_type); // Set as a relative relocation
set_relative_reloc(&section->rels[init_array_reloc - 1], elf, init->getValue());
set_relative_reloc(rel, elf, init->getValue());
} else if (!dyn->setValueForType(DT_INIT, init)) {
fprintf(stderr, "Can't grow .dynamic section to set DT_INIT. Skipping\n");
return -1;

View File

@ -49,6 +49,21 @@ class Elf;
class ElfDynamic_Section;
class ElfStrtab_Section;
template <typename X>
class FixedSizeData {
public:
struct Wrapper {
X value;
};
typedef Wrapper Type32;
typedef Wrapper Type64;
template <class endian, typename R, typename T>
static void swap(T &t, R &r) {
r.value = endian::swap(t.value);
}
};
class Elf_Ehdr_Traits {
public:
typedef Elf32_Ehdr Type32;
@ -383,7 +398,7 @@ public:
insertInSegments(section->segments);
}
void insertBefore(ElfSection *section, bool dirty = true) {
virtual void insertBefore(ElfSection *section, bool dirty = true) {
if (previous != nullptr)
previous->next = next;
if (next != nullptr)

View File

@ -16,6 +16,7 @@ if not CONFIG['CROSS_COMPILE']:
for f in CONFIG['OS_CFLAGS']:
if f.startswith('-flto'):
SOURCES['dummy.c'].flags += ['-fno-lto']
SOURCES['test-array.c'].flags += ['-fno-lto']
SOURCES['test-ctors.c'].flags += ['-fno-lto']

View File

@ -16,9 +16,6 @@ if [ -e "$topsrcdir/gcc/bin/ld" ]; then
export CXX="$CXX -B $topsrcdir/gcc/bin"
fi
# Until Bug 1423822 is resolved
ac_add_options --disable-elf-hack
ac_add_options --enable-lto
. "$topsrcdir/build/unix/mozconfig.stdcxx"

View File

@ -270,6 +270,7 @@ subsuite = clipboard
[browser_webconsole_context_menu_object_in_sidebar.js]
[browser_webconsole_context_menu_open_url.js]
[browser_webconsole_context_menu_store_as_global.js]
[browser_webconsole_cors_errors.js]
[browser_webconsole_csp_ignore_reflected_xss_message.js]
[browser_webconsole_csp_violation.js]
[browser_webconsole_cspro.js]

View File

@ -0,0 +1,180 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Ensure that the different CORS error are logged to the console with the appropriate
// "Learn more" link.
"use strict";
const TEST_URI = "http://example.com/browser/devtools/client/webconsole/test/mochitest/test-network-request.html";
const BASE_CORS_ERROR_URL = "https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/";
const BASE_CORS_ERROR_URL_PARAMS = new URLSearchParams({
utm_source: "devtools",
utm_medium: "firefox-cors-errors",
utm_campaign: "default",
});
add_task(async function() {
await pushPref("devtools.webconsole.filter.netxhr", true);
const hud = await openNewTabAndConsole(TEST_URI);
let onCorsMessage;
let message;
info(`Setting "content.cors.disable" to true to test CORSDisabled message`);
await pushPref("content.cors.disable", true);
onCorsMessage = waitForMessage(hud, "Reason: CORS disabled");
makeFaultyCorsCall("CORSDisabled");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSDisabled");
await pushPref("content.cors.disable", false);
info("Test CORSPreflightDidNotSucceed");
onCorsMessage = waitForMessage(hud, `CORS preflight channel did not succeed`);
makeFaultyCorsCall("CORSPreflightDidNotSucceed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSPreflightDidNotSucceed");
info("Test CORS did not succeed");
onCorsMessage = waitForMessage(hud, "Reason: CORS request did not succeed");
makeFaultyCorsCall("CORSDidNotSucceed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSDidNotSucceed");
info("Test CORSExternalRedirectNotAllowed");
onCorsMessage = waitForMessage(hud,
"Reason: CORS request external redirect not allowed");
makeFaultyCorsCall("CORSExternalRedirectNotAllowed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSExternalRedirectNotAllowed");
info("Test CORSMissingAllowOrigin");
onCorsMessage = waitForMessage(hud,
`Reason: CORS header ${quote("Access-Control-Allow-Origin")} missing`);
makeFaultyCorsCall("CORSMissingAllowOrigin");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMissingAllowOrigin");
info("Test CORSMultipleAllowOriginNotAllowed");
onCorsMessage = waitForMessage(hud,
`Reason: Multiple CORS header ${quote("Access-Control-Allow-Origin")} not allowed`);
makeFaultyCorsCall("CORSMultipleAllowOriginNotAllowed");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMultipleAllowOriginNotAllowed");
info("Test CORSAllowOriginNotMatchingOrigin");
onCorsMessage = waitForMessage(hud, `Reason: CORS header ` +
`${quote("Access-Control-Allow-Origin")} does not match ${quote("mochi.test")}`);
makeFaultyCorsCall("CORSAllowOriginNotMatchingOrigin");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSAllowOriginNotMatchingOrigin");
info("Test CORSNotSupportingCredentials");
onCorsMessage = waitForMessage(hud, `Reason: Credential is not supported if the CORS ` +
`header ${quote("Access-Control-Allow-Origin")} is ${quote("*")}`);
makeFaultyCorsCall("CORSNotSupportingCredentials");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSNotSupportingCredentials");
info("Test CORSMethodNotFound");
onCorsMessage = waitForMessage(hud, `Reason: Did not find method in CORS header ` +
`${quote("Access-Control-Allow-Methods")}`);
makeFaultyCorsCall("CORSMethodNotFound");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMethodNotFound");
info("Test CORSMissingAllowCredentials");
onCorsMessage = waitForMessage(hud, `Reason: expected ${quote("true")} in CORS ` +
`header ${quote("Access-Control-Allow-Credentials")}`);
makeFaultyCorsCall("CORSMissingAllowCredentials");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMissingAllowCredentials");
info("Test CORSInvalidAllowMethod");
onCorsMessage = waitForMessage(hud, `Reason: invalid token ${quote("xyz;")} in CORS ` +
`header ${quote("Access-Control-Allow-Methods")}`);
makeFaultyCorsCall("CORSInvalidAllowMethod");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSInvalidAllowMethod");
info("Test CORSInvalidAllowHeader");
onCorsMessage = waitForMessage(hud, `Reason: invalid token ${quote("xyz;")} in CORS ` +
`header ${quote("Access-Control-Allow-Headers")}`);
makeFaultyCorsCall("CORSInvalidAllowHeader");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSInvalidAllowHeader");
info("Test CORSMissingAllowHeaderFromPreflight");
onCorsMessage = waitForMessage(hud, `Reason: missing token ${quote("xyz")} in CORS ` +
`header ${quote("Access-Control-Allow-Headers")} from CORS preflight channel`);
makeFaultyCorsCall("CORSMissingAllowHeaderFromPreflight");
message = await onCorsMessage;
await checkCorsMessage(message, "CORSMissingAllowHeaderFromPreflight");
// See Bug 1480671.
// XXX: how to make Origin to not be included in the request ?
// onCorsMessage = waitForMessage(hud,
// `Reason: CORS header ${quote("Origin")} cannot be added`);
// makeFaultyCorsCall("CORSOriginHeaderNotAdded");
// message = await onCorsMessage;
// await checkCorsMessage(message, "CORSOriginHeaderNotAdded");
// See Bug 1480672.
// XXX: Failing with another error: Console message: Security Error: Content at
// http://example.com/browser/devtools/client/webconsole/test/mochitest/test-network-request.html
// may not load or link to file:///Users/nchevobbe/Projects/mozilla-central/devtools/client/webconsole/test/mochitest/sjs_cors-test-server.sjs.
// info("Test CORSRequestNotHttp");
// onCorsMessage = waitForMessage(hud, "Reason: CORS request not http");
// const dir = getChromeDir(getResolvedURI(gTestPath));
// dir.append("sjs_cors-test-server.sjs");
// makeFaultyCorsCall("CORSRequestNotHttp", Services.io.newFileURI(dir).spec);
// message = await onCorsMessage;
// await checkCorsMessage(message, "CORSRequestNotHttp");
});
async function checkCorsMessage(message, category) {
const node = message.node;
ok(node.classList.contains("warn"), "The cors message has the expected classname");
const learnMoreLink = node.querySelector(".learn-more-link");
ok(learnMoreLink, "There is a Learn more link displayed");
const linkSimulation = await simulateLinkClick(learnMoreLink);
is(linkSimulation.link, getCategoryUrl(category),
"Click on the link opens the expected page");
}
function makeFaultyCorsCall(errorCategory, corsUrl) {
ContentTask.spawn(gBrowser.selectedBrowser, [errorCategory, corsUrl],
([category, url]) => {
if (!url) {
const baseUrl =
"http://mochi.test:8888/browser/devtools/client/webconsole/test/mochitest";
url = `${baseUrl}/sjs_cors-test-server.sjs?corsErrorCategory=${category}`;
}
// Preflight request are not made for GET requests, so let's do a PUT.
const method = "PUT";
const options = { method };
if (category === "CORSNotSupportingCredentials"
|| category === "CORSMissingAllowCredentials"
) {
options.credentials = "include";
}
if (category === "CORSMissingAllowHeaderFromPreflight") {
options.headers = new content.Headers({"xyz": true});
}
content.fetch(url, options);
});
}
function quote(str) {
const openingQuote = String.fromCharCode(8216);
const closingQuote = String.fromCharCode(8217);
return `${openingQuote}${str}${closingQuote}`;
}
function getCategoryUrl(category) {
return `${BASE_CORS_ERROR_URL}${category}?${BASE_CORS_ERROR_URL_PARAMS}`;
}

View File

@ -1,17 +1,150 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function handleRequest(request, response) {
response.setStatusLine(request.httpVersion, 200, "Och Aye");
"use strict";
function handleRequest(request, response) {
const params = new Map(
request.queryString
.replace("?", "")
.split("&")
.map(s => s.split("="))
);
if (!params.has("corsErrorCategory")) {
response.setStatusLine(request.httpVersion, 200, "Och Aye");
setCacheHeaders(response);
response.setHeader("Access-Control-Allow-Origin", "*", false);
response.setHeader("Access-Control-Allow-Headers", "content-type", false);
response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
response.write("Access-Control-Allow-Origin: *");
return;
}
const category = params.get("corsErrorCategory");
switch (category) {
case "CORSDidNotSucceed":
corsDidNotSucceed(request, response);
break;
case "CORSExternalRedirectNotAllowed":
corsExternalRedirectNotAllowed(request, response);
break;
case "CORSMissingAllowOrigin":
corsMissingAllowOrigin(request, response);
break;
case "CORSMultipleAllowOriginNotAllowed":
corsMultipleOriginNotAllowed(request, response);
break;
case "CORSAllowOriginNotMatchingOrigin":
corsAllowOriginNotMatchingOrigin(request, response);
break;
case "CORSNotSupportingCredentials":
corsNotSupportingCredentials(request, response);
break;
case "CORSMethodNotFound":
corsMethodNotFound(request, response);
break;
case "CORSMissingAllowCredentials":
corsMissingAllowCredentials(request, response);
break;
case "CORSPreflightDidNotSucceed":
corsPreflightDidNotSucceed(request, response);
break;
case "CORSInvalidAllowMethod":
corsInvalidAllowMethod(request, response);
break;
case "CORSInvalidAllowHeader":
corsInvalidAllowHeader(request, response);
break;
case "CORSMissingAllowHeaderFromPreflight":
corsMissingAllowHeaderFromPreflight(request, response);
break;
}
}
function corsDidNotSucceed(request, response) {
setCacheHeaders(response);
response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
response.setHeader("Location", "http://example.com");
}
function corsExternalRedirectNotAllowed(request, response) {
response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
response.setHeader("Access-Control-Allow-Origin", "*", false);
response.setHeader("Access-Control-Allow-Headers", "content-type", false);
response.setHeader("Location", "http://redirect.test/");
}
function corsMissingAllowOrigin(request, response) {
setCacheHeaders(response);
response.setStatusLine(request.httpVersion, 200, "corsMissingAllowOrigin");
}
function corsMultipleOriginNotAllowed(request, response) {
// We can't set the same header twice with response.setHeader, so we need to seizePower
// and write the response manually.
response.seizePower();
response.write("HTTP/1.0 200 OK\r\n");
response.write("Content-Type: text/plain\r\n");
response.write("Access-Control-Allow-Origin: *\r\n");
response.write("Access-Control-Allow-Origin: mochi.test\r\n");
response.write("\r\n");
response.finish();
setCacheHeaders(response);
}
function corsAllowOriginNotMatchingOrigin(request, response) {
response.setStatusLine(request.httpVersion, 200, "corsAllowOriginNotMatchingOrigin");
response.setHeader("Access-Control-Allow-Origin", "mochi.test");
}
function corsNotSupportingCredentials(request, response) {
response.setStatusLine(request.httpVersion, 200, "corsNotSupportingCredentials");
response.setHeader("Access-Control-Allow-Origin", "*");
}
function corsMethodNotFound(request, response) {
response.setStatusLine(request.httpVersion, 200, "corsMethodNotFound");
response.setHeader("Access-Control-Allow-Origin", "*");
// Will make the request fail since it is a "PUT".
response.setHeader("Access-Control-Allow-Methods", "POST");
}
function corsMissingAllowCredentials(request, response) {
response.setStatusLine(request.httpVersion, 200, "corsMissingAllowCredentials");
// Need to set an explicit origin (i.e. not "*") to make the request fail.
response.setHeader("Access-Control-Allow-Origin", "http://example.com");
}
function corsPreflightDidNotSucceed(request, response) {
const isPreflight = request.method == "OPTIONS";
if (isPreflight) {
response.setStatusLine(request.httpVersion, 500, "Preflight fail");
response.setHeader("Access-Control-Allow-Origin", "*");
}
}
function corsInvalidAllowMethod(request, response) {
response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowMethod");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "xyz;");
}
function corsInvalidAllowHeader(request, response) {
response.setStatusLine(request.httpVersion, 200, "corsInvalidAllowHeader");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "PUT");
response.setHeader("Access-Control-Allow-Headers", "xyz;");
}
function corsMissingAllowHeaderFromPreflight(request, response) {
response.setStatusLine(request.httpVersion, 200, "corsMissingAllowHeaderFromPreflight");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "PUT");
}
function setCacheHeaders(response) {
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
response.setHeader("Access-Control-Allow-Origin", "*", false);
response.setHeader("Access-Control-Allow-Headers", "content-type", false);
response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
response.write("Access-Control-Allow-Origin: *");
}

View File

@ -9,9 +9,10 @@
"use strict";
const baseURL = "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/";
const baseErrorURL = "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Errors/";
const params =
"?utm_source=mozilla&utm_medium=firefox-console-errors&utm_campaign=default";
const ErrorDocs = {
JSMSG_READ_ONLY: "Read-only",
JSMSG_BAD_ARRAY_LENGTH: "Invalid_array_length",
@ -108,6 +109,27 @@ const ErrorCategories = {
"source map": SOURCE_MAP_LEARN_MORE,
};
const baseCorsErrorUrl = "https://developer.mozilla.org/docs/Web/HTTP/CORS/Errors/";
const corsParams =
"?utm_source=devtools&utm_medium=firefox-cors-errors&utm_campaign=default";
const CorsErrorDocs = {
CORSDisabled: "CORSDisabled",
CORSDidNotSucceed: "CORSDidNotSucceed",
CORSOriginHeaderNotAdded: "CORSOriginHeaderNotAdded",
CORSExternalRedirectNotAllowed: "CORSExternalRedirectNotAllowed",
CORSRequestNotHttp: "CORSRequestNotHttp",
CORSMissingAllowOrigin: "CORSMissingAllowOrigin",
CORSMultipleAllowOriginNotAllowed: "CORSMultipleAllowOriginNotAllowed",
CORSAllowOriginNotMatchingOrigin: "CORSAllowOriginNotMatchingOrigin",
CORSNotSupportingCredentials: "CORSNotSupportingCredentials",
CORSMethodNotFound: "CORSMethodNotFound",
CORSMissingAllowCredentials: "CORSMissingAllowCredentials",
CORSPreflightDidNotSucceed: "CORSPreflightDidNotSucceed",
CORSInvalidAllowMethod: "CORSInvalidAllowMethod",
CORSInvalidAllowHeader: "CORSInvalidAllowHeader",
CORSMissingAllowHeaderFromPreflight: "CORSMissingAllowHeaderFromPreflight",
};
exports.GetURL = (error) => {
if (!error) {
return undefined;
@ -115,7 +137,12 @@ exports.GetURL = (error) => {
const doc = ErrorDocs[error.errorMessageName];
if (doc) {
return baseURL + doc + params;
return baseErrorURL + doc + params;
}
const corsDoc = CorsErrorDocs[error.category];
if (corsDoc) {
return baseCorsErrorUrl + corsDoc + corsParams;
}
const categoryURL = ErrorCategories[error.category];

View File

@ -54,8 +54,7 @@ function createFakeAddonWindow({addonId} = {}) {
const principal = Services.scriptSecurityManager
.createCodebasePrincipal(baseURI, {});
const chromeWebNav = Services.appShell.createWindowlessBrowser(true);
const docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
const { docShell } = chromeWebNav;
docShell.createAboutBlankContentViewer(principal);
const addonWindow = docShell.contentViewer.DOMDocument.defaultView;

View File

@ -619,11 +619,6 @@ nsDocShell::GetInterface(const nsIID& aIID, void** aSink)
*aSink = mFind;
NS_ADDREF((nsISupports*)*aSink);
return NS_OK;
} else if (aIID.Equals(NS_GET_IID(nsIEditingSession))) {
nsCOMPtr<nsIEditingSession> es;
GetEditingSession(getter_AddRefs(es));
es.forget(aSink);
return *aSink ? NS_OK : NS_NOINTERFACE;
} else if (aIID.Equals(NS_GET_IID(nsISelectionDisplay))) {
nsIPresShell* shell = GetPresShell();
if (shell) {

View File

@ -51,14 +51,12 @@ function test() {
}
}
var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"].
createInstance(Ci.nsIPrincipal);
var webNav = Cc["@mozilla.org/appshell/appShellService;1"].
getService(Ci.nsIAppShellService).
createWindowlessBrowser(true);
var docShell = webNav.
QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDocShell);
var systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
.getService(Ci.nsIPrincipal);
var webNav = Cc["@mozilla.org/appshell/appShellService;1"]
.getService(Ci.nsIAppShellService)
.createWindowlessBrowser(true);
var docShell = webNav.docShell;
docShell.createAboutBlankContentViewer(systemPrincipal);
var win = docShell.contentViewer.DOMDocument.defaultView;

View File

@ -33,7 +33,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=846906
var interfaceRequestor = windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor);
ok(interfaceRequestor, "Should be able to query interface requestor interface");
var docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
var docShell = windowlessBrowser.docShell;
ok(docShell, "Should be able to get doc shell interface");
var document = webNavigation.document;

View File

@ -71,10 +71,9 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=1342989
var webNav = Cc["@mozilla.org/appshell/appShellService;1"].
getService(Ci.nsIAppShellService).createWindowlessBrowser(true);
let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor).
getInterface(Ci.nsIDocShell);
let docShell = webNav.docShell;
docShell.createAboutBlankContentViewer(
Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal));
Cc["@mozilla.org/systemprincipal;1"].getService(Ci.nsIPrincipal));
progressListener.add(docShell, function(success) {
webNav.close();

View File

@ -6,10 +6,9 @@ ChromeUtils.import("resource://gre/modules/Services.jsm");
add_task(async function() {
let webNav = Services.appShell.createWindowlessBrowser(false);
let loadContext = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsILoadContext);
let docShell = webNav.docShell;
let docShell = webNav.getInterface(Ci.nsIDocShell);
let loadContext = docShell.QueryInterface(Ci.nsILoadContext);
equal(loadContext.usePrivateBrowsing, false, "Should start out in non-private mode");

View File

@ -12,10 +12,10 @@
#include "mozilla/Logging.h"
#include "nsContentPermissionHelper.h"
extern mozilla::LazyLogModule gMediaElementLog;
extern mozilla::LazyLogModule gAutoplayPermissionLog;
#define PLAY_REQUEST_LOG(msg, ...) \
MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
namespace mozilla {

View File

@ -9,10 +9,10 @@
#include "mozilla/Logging.h"
extern mozilla::LazyLogModule gMediaElementLog;
extern mozilla::LazyLogModule gAutoplayPermissionLog;
#define PLAY_REQUEST_LOG(msg, ...) \
MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
namespace mozilla {

View File

@ -127,6 +127,10 @@
mozilla::LazyLogModule gMediaElementLog("nsMediaElement");
static mozilla::LazyLogModule gMediaElementEventsLog("nsMediaElementEvents");
extern mozilla::LazyLogModule gAutoplayPermissionLog;
#define AUTOPLAY_LOG(msg, ...) \
MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
#define LOG(type, msg) MOZ_LOG(gMediaElementLog, type, msg)
#define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg)
@ -3068,6 +3072,7 @@ HTMLMediaElement::PauseIfShouldNotBePlaying()
return;
}
if (AutoplayPolicy::IsAllowedToPlay(*this) != nsIAutoplay::ALLOWED) {
AUTOPLAY_LOG("pause because not allowed to play, element=%p", this);
ErrorResult rv;
Pause(rv);
OwnerDoc()->SetDocTreeHadPlayRevoked();
@ -4103,7 +4108,7 @@ HTMLMediaElement::Play(ErrorResult& aRv)
break;
}
case nsIAutoplay::BLOCKED: {
LOG(LogLevel::Debug, ("%p play not blocked.", this));
AUTOPLAY_LOG("%p play blocked.", this);
promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);
if (StaticPrefs::MediaBlockEventEnabled()) {
DispatchAsyncEvent(NS_LITERAL_STRING("blocked"));
@ -4128,8 +4133,7 @@ HTMLMediaElement::EnsureAutoplayRequested(bool aHandlingUserInput)
// Await for the previous request to be approved or denied. This
// play request's promise will be fulfilled with all other pending
// promises when the permission prompt is resolved.
LOG(LogLevel::Debug,
("%p EnsureAutoplayRequested() existing request, bailing.", this));
AUTOPLAY_LOG("%p EnsureAutoplayRequested() existing request, bailing.", this);
return;
}
@ -4146,19 +4150,17 @@ HTMLMediaElement::EnsureAutoplayRequested(bool aHandlingUserInput)
[ self, handlingUserInput = aHandlingUserInput, request ](
bool aApproved) {
self->mAutoplayPermissionRequest.Complete();
LOG(LogLevel::Debug,
("%p Autoplay request approved request=%p",
self.get(),
request.get()));
AUTOPLAY_LOG("%p Autoplay request approved request=%p",
self.get(),
request.get());
self->PlayInternal(handlingUserInput);
self->UpdateCustomPolicyAfterPlayed();
},
[self, request](nsresult aError) {
self->mAutoplayPermissionRequest.Complete();
LOG(LogLevel::Debug,
("%p Autoplay request denied request=%p",
self.get(),
request.get()));
AUTOPLAY_LOG("%p Autoplay request denied request=%p",
self.get(),
request.get());
LOG(LogLevel::Debug, ("%s rejecting play promimses", __func__));
self->AsyncRejectPendingPlayPromises(
NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR);

View File

@ -7,6 +7,7 @@
#include "AutoplayPolicy.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/Logging.h"
#include "mozilla/Preferences.h"
#include "mozilla/dom/AudioContext.h"
#include "mozilla/AutoplayPermissionManager.h"
@ -20,6 +21,26 @@
#include "nsIDocShellTreeItem.h"
#include "nsPIDOMWindow.h"
mozilla::LazyLogModule gAutoplayPermissionLog("Autoplay");
#define AUTOPLAY_LOG(msg, ...) \
MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
static const char*
AllowAutoplayToStr(const uint32_t state)
{
switch (state) {
case nsIAutoplay::ALLOWED:
return "allowed";
case nsIAutoplay::BLOCKED:
return "blocked";
case nsIAutoplay::PROMPT:
return "prompt";
default:
return "unknown";
}
}
namespace mozilla {
namespace dom {
@ -62,17 +83,17 @@ IsWindowAllowedToPlay(nsPIDOMWindowInner* aWindow)
nsIDocument* approver = ApproverDocOf(*aWindow->GetExtantDoc());
if (nsContentUtils::IsExactSitePermAllow(approver->NodePrincipal(),
"autoplay-media")) {
// Autoplay permission has been granted already.
AUTOPLAY_LOG("Allow autoplay as document has autoplay permission.");
return true;
}
if (approver->HasBeenUserGestureActivated()) {
// Document has been activated by user gesture.
AUTOPLAY_LOG("Allow autoplay as document activated by user gesture.");
return true;
}
if (approver->IsExtensionPage()) {
// Always allow extension page to autoplay.
AUTOPLAY_LOG("Allow autoplay as in extension document.");
return true;
}
@ -108,10 +129,23 @@ DefaultAutoplayBehaviour()
static bool
IsMediaElementAllowedToPlay(const HTMLMediaElement& aElement)
{
return ((aElement.Volume() == 0.0 || aElement.Muted()) &&
Preferences::GetBool("media.autoplay.allow-muted", true)) ||
IsWindowAllowedToPlay(aElement.OwnerDoc()->GetInnerWindow()) ||
(aElement.OwnerDoc()->MediaDocumentKind() == nsIDocument::MediaDocumentKind::Video);
if ((aElement.Volume() == 0.0 || aElement.Muted()) &&
Preferences::GetBool("media.autoplay.allow-muted", true)) {
AUTOPLAY_LOG("Allow muted media %p to autoplay.", &aElement);
return true;
}
if (IsWindowAllowedToPlay(aElement.OwnerDoc()->GetInnerWindow())) {
AUTOPLAY_LOG("Autoplay allowed as activated/whitelisted window, media %p.", &aElement);
return true;
}
if (aElement.OwnerDoc()->MediaDocumentKind() == nsIDocument::MediaDocumentKind::Video) {
AUTOPLAY_LOG("Allow video document %p to autoplay\n", &aElement);
return true;
}
return false;
}
/* static */ bool
@ -134,11 +168,12 @@ AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement)
? nsIAutoplay::ALLOWED : nsIAutoplay::BLOCKED;
}
if (IsMediaElementAllowedToPlay(aElement)) {
return nsIAutoplay::ALLOWED;
}
const uint32_t result = IsMediaElementAllowedToPlay(aElement) ?
nsIAutoplay::ALLOWED : autoplayDefault;
return autoplayDefault;
AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s",
&aElement, AllowAutoplayToStr(result));
return result;
}
/* static */ bool

View File

@ -69,8 +69,7 @@ function test()
.createCodebasePrincipal(baseURI, {});
let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
let interfaceRequestor = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor);
let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell);
let docShell = chromeWebNav.docShell;
docShell.createAboutBlankContentViewer(principal);
info("fake webextension docShell created");

View File

@ -1710,6 +1710,7 @@ WebRenderBridgeParent::ClearResources()
RefPtr<WebRenderImageHost> host = entry.second;
host->ClearWrBridge();
mAsyncImageManager->RemoveAsyncImagePipeline(pipelineId, txn);
txn.RemovePipeline(pipelineId);
}
mAsyncCompositables.clear();
for (const auto& entry : mSharedSurfaceIds) {

View File

@ -4,8 +4,7 @@ add_task(async function test_windowlessBrowserTroubleshootCrash() {
let webNav = Services.appShell.createWindowlessBrowser(false);
let onLoaded = new Promise((resolve, reject) => {
let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
let docShell = webNav.docShell;
let listener = {
observe(contentWindow, topic, data) {
let observedDocShell = contentWindow.docShell

View File

@ -5,7 +5,7 @@
#include "Hal.h"
#include "HalLog.h"
#include <Windows.h>
#include <windows.h>
using namespace mozilla::hal;

View File

@ -18,8 +18,7 @@ add_task(async function() {
let webnav = Services.appShell.createWindowlessBrowser(false);
let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
let docShell = webnav.docShell;
docShell.createAboutBlankContentViewer(principal);

View File

@ -14,8 +14,7 @@ function getWindowlessBrowser(url) {
let webnav = Services.appShell.createWindowlessBrowser(false);
let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
let docShell = webnav.docShell;
docShell.createAboutBlankContentViewer(principal);

View File

@ -7,8 +7,7 @@ ChromeUtils.import("resource://gre/modules/Services.jsm");
add_task(async function() {
let webnav = Services.appShell.createWindowlessBrowser(false);
let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
let docShell = webnav.docShell;
docShell.createAboutBlankContentViewer(null);

View File

@ -34,6 +34,9 @@ ac_add_options --target=arm-linux-androideabi
ac_add_options --with-branding=mobile/android/branding/nightly
# Pull code coverage dependencies too.
ac_add_options --enable-java-coverage
export MOZILLA_OFFICIAL=1
export MOZ_TELEMETRY_REPORTING=1
export MOZ_ANDROID_MMA=1

View File

@ -0,0 +1,29 @@
. "$topsrcdir/build/mozconfig.artifact.automation"
NO_CACHE=1
NO_NDK=1
. "$topsrcdir/mobile/android/config/mozconfigs/common"
# Global options
ac_add_options --enable-debug
ac_add_options --enable-java-coverage
# Android
# Warning: Before increasing the with-android-min-sdk value, please note several places in and out
# of tree have to be changed. Otherwise, places like Treeherder or archive.mozilla.org will
# advertise a bad API level. This may confuse people. As an example, please look at bug 1384482.
# If you think you can't handle the whole set of changes, please reach out to the Release
# Engineering team.
ac_add_options --with-android-min-sdk=16
ac_add_options --target=arm-linux-androideabi
. "$topsrcdir/mobile/android/config/mozconfigs/android-api-16/nightly"
. "$topsrcdir/build/mozconfig.artifact"
ac_add_options --enable-artifact-build-symbols
ac_add_options --with-branding=mobile/android/branding/nightly
. "$topsrcdir/mobile/android/config/mozconfigs/common.override"

View File

@ -165,6 +165,11 @@
# See: http://stackoverflow.com/questions/22441366/note-android-support-v4-text-icucompatics-cant-find-dynamically-referenced-cl
-dontnote android.support.**
# Don't warn when classes referenced by JaCoCo are missing when running the build from android-dependencies.
-dontwarn java.lang.instrument.**
-dontwarn java.lang.management.**
-dontwarn javax.management.**
-include "adjust-keeps.cfg"
-include "leakcanary-keeps.cfg"

View File

@ -21,6 +21,7 @@ Contents:
shutdown
push
gradle
testcoverage
Indices and tables
==================

View File

@ -0,0 +1,117 @@
.. -*- Mode: rst; fill-column: 80; -*-
========================================================
Collecting code coverage information for Android tests
========================================================
The Android-specific test suites are outlined on MDN_. A more elaborate description can be found on
the Wiki_. This page describes how collecting code coverage information is implemented for these
test suites.
.. _MDN: https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites
.. _WIKI: https://wiki.mozilla.org/Mobile/Fennec/Android/Testing
Collecting and exporting code coverage information
==================================================
Relevant tools and libraries
----------------------------
JaCoCo_ is the tool used to gather code coverage data. JaCoCo uses class file
instrumentation to record execution coverage data. It has two operating modes:
- `online instrumentation`_: Class files are instrumented on-the-fly using a so
called Java agent. The JaCoCo agent collects execution information and dumps
it on request or when the JVM exits. This method is used for test suites
which run on the JVM (generally, `*UnitTest` Gradle tasks).
- `offline instrumentation`_: At runtime the pre-instrumented classes needs be
on the classpath instead of the original classes. In addition,
`jacocoagent.jar` must be put on the classpath. This method is used for test
suites with run on the Android emulator (generally, `*AndroidTest` Gradle
tasks, including `robocop`).
JaCoCo is integrated with Gradle in two ways: a Gradle plugin (activated in the
``build.gradle`` file as a top-level ``apply plugin: 'jacoco'``), and an
Android-specific Gradle plugin, activated with ``android { buildTypes { debug {
testCoverageEnabled true } } }``. These two methods of activating JaCoCo have a
different syntax of choosing the JaCoCo tool version, and both should be
configured to use the same version. See the
``mobile/android/geckoview/build.gradle`` file for example usage.
grcov_ is a tool implemented in Rust that transforms code coverage reports
between formats. Among others, it supports reading JaCoCo XML reports.
.. _JaCoCo: https://www.eclemma.org/jacoco/
.. _online instrumentation: https://www.jacoco.org/jacoco/trunk/doc/agent.html
.. _offline instrumentation: https://www.jacoco.org/jacoco/trunk/doc/offline.html
.. _grcov: https://github.com/mozilla/grcov/
Generating the coverage report artifacts
----------------------------------------
All tasks that output code coverage information do so by exporting an artifact
named ``code-coverage-grcov.zip`` which contains a single file named
``grcov_lcov_output.info``. The ``grcov_lcov_output.info`` file should contain
coverage information in the lcov format. The artifact, once uploaded, is picked
up and indexed by ActiveData_.
The code that generates the ``code-coverage-grcov.zip`` artifact after a
generally resides in the ``CodeCoverageMixin`` class, in the module
``testing/mozharness/mozharness/mozilla/testing/codecoverage.py``. This class is
responsible for downloading ``grcov`` along with other artifacts from the build
job. It is also responsible for running ``grcov`` after the tests are finished,
to convert and merge the coverage reports.
.. _ActiveData: https://wiki.mozilla.org/EngineeringProductivity/Projects/ActiveData
Code coverage for android-test
===============================
The `android-test` suite is a JUnit test suite that runs locally on the
host's JVM. It can be run with ``mach android test``. The test suite is
implemented as a build task, defined at
``taskcluster/ci/build/android-stuff.yml``.
To collect code coverage from this suite, a duplicate build task is defined,
called `android-test-ccov`. It can be run with ``mach android test-ccov``.
The mach subcommand is responsible for downloading and running `grcov`,
instead of the ``CodeCoverageMixin`` class. This is because the
`android-test-ccov` task is a build task (not a test task), so it doesn't use
mozharness to run the tests.
Code coverage for geckoview-junit
==================================
The geckoview-junit_ tests are on-device Android JUnit tests written for
GeckoView_. The tests are implemented with ``mochitest``. The automation
entry point is ``testing/mozharness/scripts/android_emulator_unittest.py``,
which then calls ``testing/mochitest/runjunit.py``. This is an out-of-tree
task, so a source tree clone is unavailable.
To generate the coverage report, we need three things:
- The classfiles before instrumentation. These are archived as a public
artifact during build time, and downloaded on the test machine during testing
time;
- The coverage.ec file with coverage counters. This is generated while running
the tests on the emulator, then downloaded with ``adb pull``;
- ``jacoco-cli``, a command-line package JaCoCo component that takes the
classfiles and the coverage counters as input and generates XML reports as
output.
The ``mach android archive-geckoview-coverage-artifacts`` command archives the
class files and exports the ``jacoco-cli`` jar file after the build is done.
These files are later saved as public artifacts of the build.
To enable offline instrumentation for the test suites, the mozconfig flag
``--enable-java-coverage`` should be set. When the flag is checked both during
build and test time. During test time, the flag instructs ``CodeCoverageMixin``
to download the coverage artifacts from the build task before the tests run,
and generate and export the reports after testing is finished. The flag also
instructs the ``runjunit.py`` script to insert the arguments ``-e coverage
true`` to ``am instrument``.
.. _GeckoView: https://wiki.mozilla.org/Mobile/GeckoView
.. _geckoview-junit: https://developer.mozilla.org/en-US/docs/Mozilla/Geckoview-Junit_Tests

View File

@ -372,3 +372,71 @@ apply from: "${topsrcdir}/mobile/android/gradle/jacoco_dependencies.gradle"
if (project.hasProperty('enable_code_coverage')) {
apply from: "${topsrcdir}/mobile/android/gradle/jacoco_for_junit.gradle"
}
// Set up code coverage for tests on emulators.
if (mozconfig.substs.MOZ_JAVA_CODE_COVERAGE) {
apply plugin: "jacoco"
jacoco {
toolVersion = "${project.jacoco_version}"
}
android {
jacoco {
version = "$jacoco_version"
}
buildTypes {
debug {
testCoverageEnabled true
}
}
}
configurations {
// This configuration is used for dependencies that are not needed at compilation or
// runtime, but need to be exported as artifacts of the build for usage on the testing
// machines.
coverageDependency
}
dependencies {
// This is required both in the instrumented application classes and the test classes,
// so `api` has to be used instead of `androidTestImplementation`.
api "org.jacoco:org.jacoco.agent:$jacoco_version:runtime"
coverageDependency ("org.jacoco:org.jacoco.cli:$jacoco_version:nodeps") {
exclude group: 'org.ow2.asm', module: '*'
}
}
// This task is used by `mach android archive-geckoview-coverage-artifacts`.
task copyCoverageDependencies(type: Copy) {
from(configurations.coverageDependency) {
include 'org.jacoco.cli-*-nodeps.jar'
rename { _ -> 'target.jacoco-cli.jar' }
}
into "$buildDir/coverage"
}
// Generate tasks to archive compiled classfiles for later use with JaCoCo report generation.
// One of these tasks is used by `mach android archive-geckoview-coverage-artifacts`.
android.libraryVariants.all { variant ->
def name = variant.name
def compileTask = tasks.getByName("compile${name.capitalize()}JavaWithJavac")
task "archiveClassfiles${name.capitalize()}"(type: Zip, dependsOn: compileTask) {
description = "Archive compiled classfiles for $name in order to export them as code coverage artifacts."
def fileFilter = ['**/androidTest/**',
'**/test/**',
'**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'**/*Test*.*',
'android/**/*.*']
from fileTree(dir: compileTask.destinationDir, excludes: fileFilter)
destinationDir = file("${buildDir}/coverage")
// Note: This task assumes only one variant of archiveClassfiles* will be used.
// Running multiple variants of this task will overwrite the output archive.
archiveName = 'target.geckoview_classfiles.zip'
}
}
}

View File

@ -237,6 +237,18 @@ def gradle_android_archive_geckoview_tasks(build_config):
set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS', gradle_android_archive_geckoview_tasks)
@depends(gradle_android_build_config)
def gradle_android_archive_geckoview_coverage_artifacts_tasks(build_config):
'''Gradle tasks run by |mach android archive-geckoview-coverage-artifacts|.'''
return [
'geckoview:archiveClassfiles{geckoview.variant.name}'.format(geckoview=build_config.geckoview),
'geckoview:copyCoverageDependencies',
]
set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_COVERAGE_ARTIFACTS_TASKS',
gradle_android_archive_geckoview_coverage_artifacts_tasks)
@depends(
gradle_android_app_tasks,
gradle_android_test_tasks,
@ -247,6 +259,7 @@ set_config('GRADLE_ANDROID_ARCHIVE_GECKOVIEW_TASKS', gradle_android_archive_geck
gradle_android_generate_sdk_bindings_tasks,
gradle_android_generate_generated_jni_wrappers_tasks,
gradle_android_generate_fennec_jni_wrappers_tasks,
gradle_android_archive_geckoview_coverage_artifacts_tasks,
)
@imports(_from='itertools', _import='imap')
@imports(_from='itertools', _import='chain')

View File

@ -3,11 +3,9 @@
* 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/. */
project.ext.jacoco_version = "0.7.8"
dependencies {
testImplementation "org.jacoco:org.jacoco.agent:${project.jacoco_version}"
testImplementation "org.jacoco:org.jacoco.ant:${project.jacoco_version}"
testImplementation "org.jacoco:org.jacoco.core:${project.jacoco_version}"
testImplementation "org.jacoco:org.jacoco.report:${project.jacoco_version}"
testImplementation "org.jacoco:org.jacoco.agent:$jacoco_version"
testImplementation "org.jacoco:org.jacoco.ant:$jacoco_version"
testImplementation "org.jacoco:org.jacoco.core:$jacoco_version"
testImplementation "org.jacoco:org.jacoco.report:$jacoco_version"
}

View File

@ -207,7 +207,7 @@ class MachCommands(MachCommandBase):
@SubCommand('android', 'test-ccov',
"""Run Android local unit tests in order to get a code coverage report.
See https://developer.mozilla.org/en-US/docs/Mozilla/Android-specific_test_suites#android-test""") # NOQA: E501
See https://firefox-source-docs.mozilla.org/mobile/android/fennec/testcoverage.html""") # NOQA: E501
@CommandArgument('args', nargs=argparse.REMAINDER)
def android_test_ccov(self, args):
enable_ccov = '-Penable_code_coverage'
@ -413,6 +413,16 @@ class MachCommands(MachCommandBase):
return 0
@SubCommand('android', 'archive-geckoview-coverage-artifacts',
"""Archive compiled geckoview classfiles to be used later in generating code
coverage reports. See https://firefox-source-docs.mozilla.org/mobile/android/fennec/testcoverage.html""") # NOQA: E501
@CommandArgument('args', nargs=argparse.REMAINDER)
def android_archive_geckoview_classfiles(self, args):
self.gradle(self.substs['GRADLE_ANDROID_ARCHIVE_GECKOVIEW_COVERAGE_ARTIFACTS_TASKS'] +
["--continue"] + args, verbose=True)
return 0
@SubCommand('android', 'archive-geckoview',
"""Create GeckoView archives.
See http://firefox-source-docs.mozilla.org/build/buildsystem/toolchains.html#firefox-for-android-with-gradle""") # NOQA: E501

View File

@ -2,7 +2,7 @@
* 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/. */
#include <Windows.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

View File

@ -2,7 +2,7 @@
* 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/. */
#include <Windows.h>
#include <windows.h>
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD aReason, LPVOID)
{

View File

@ -2,7 +2,7 @@
* 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/. */
#include <Windows.h>
#include <windows.h>
#include <winternl.h>
#include "gtest/gtest.h"
#include "nsReadableUtils.h"

View File

@ -512,6 +512,7 @@ class TestChecksConfigure(unittest.TestCase):
'JAR': jar,
'JARSIGNER': jarsigner,
'KEYTOOL': keytool,
'MOZ_JAVA_CODE_COVERAGE': False,
})
self.assertEqual(out, textwrap.dedent('''\
checking for java... %s
@ -555,6 +556,7 @@ class TestChecksConfigure(unittest.TestCase):
'JAR': alt_jar,
'JARSIGNER': alt_jarsigner,
'KEYTOOL': alt_keytool,
'MOZ_JAVA_CODE_COVERAGE': False,
})
self.assertEqual(out, textwrap.dedent('''\
checking for java... %s
@ -584,6 +586,7 @@ class TestChecksConfigure(unittest.TestCase):
'JAR': alt_jar,
'JARSIGNER': alt_jarsigner,
'KEYTOOL': alt_keytool,
'MOZ_JAVA_CODE_COVERAGE': False,
})
self.assertEqual(out, textwrap.dedent('''\
checking for java... %s
@ -614,6 +617,7 @@ class TestChecksConfigure(unittest.TestCase):
'JAR': alt_jar,
'JARSIGNER': alt_jarsigner,
'KEYTOOL': alt_keytool,
'MOZ_JAVA_CODE_COVERAGE': False,
})
self.assertEqual(out, textwrap.dedent('''\
checking for java... %s
@ -626,6 +630,26 @@ class TestChecksConfigure(unittest.TestCase):
''' % (alt_java, alt_javah, alt_jar, alt_jarsigner,
alt_keytool, alt_javac)))
# --enable-java-coverage should set MOZ_JAVA_CODE_COVERAGE.
config, out, status = self.get_result(
args=['--enable-java-coverage'],
includes=includes,
extra_paths=paths,
environ={
'PATH': mozpath.dirname(java),
'JAVA_HOME': mozpath.dirname(mozpath.dirname(java)),
})
self.assertEqual(status, 0)
self.assertEqual(config, {
'JAVA': java,
'JAVAH': javah,
'JAVAC': javac,
'JAR': jar,
'JARSIGNER': jarsigner,
'KEYTOOL': keytool,
'MOZ_JAVA_CODE_COVERAGE': True,
})
def mock_old_javac(_, args):
if len(args) == 1 and args[0] == '-version':
return 0, '1.6.9', ''

View File

@ -32,6 +32,7 @@ const TOPICS = [
"fxaccounts:onlogin", // Defined in FxAccountsCommon, pulling it is expensive.
"fxaccounts:onlogout",
"fxaccounts:profilechange",
"fxaccounts:statechange",
];
const ON_UPDATE = "sync-ui-state:update";

View File

@ -48,6 +48,62 @@ android-api-16/debug:
- linux64-sccache
- linux64-node
android-api-16-ccov/debug:
description: "Android 4.0 api-16+ Debug Coverage"
index:
product: mobile
job-name: android-api-16-ccov-debug
treeherder:
platform: android-4-0-armv7-api16-ccov/debug
symbol: B
worker-type: aws-provisioner-v1/gecko-{level}-b-android
worker:
docker-image: {in-tree: android-build}
max-run-time: 7200
env:
GRADLE_USER_HOME: "/builds/worker/workspace/build/src/mobile/android/gradle/dotgradle-offline"
TOOLTOOL_MANIFEST: "mobile/android/config/tooltool-manifests/android/releng.manifest"
artifacts:
- name: public/android/R
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/app/R
type: directory
- name: public/android/maven
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/maven/
type: directory
- name: public/build/geckoview-androidTest.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/outputs/apk/androidTest/officialWithGeckoBinariesNoMinApi/debug/geckoview-official-withGeckoBinaries-noMinApi-debug-androidTest.apk
type: file
- name: public/build/geckoview_example.apk
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview_example/outputs/apk/officialWithGeckoBinariesNoMinApi/debug/geckoview_example-official-withGeckoBinaries-noMinApi-debug.apk
type: file
- name: public/build/target.geckoview_classfiles.zip
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/coverage/target.geckoview_classfiles.zip
type: file
- name: public/build/target.jacoco-cli.jar
path: /builds/worker/workspace/build/src/obj-firefox/gradle/build/mobile/android/geckoview/coverage/target.jacoco-cli.jar
type: file
- name: public/build
path: /builds/worker/artifacts/
type: directory
run:
using: mozharness
actions: [get-secrets build multi-l10n update]
config:
- builds/releng_base_android_64_builds.py
script: "mozharness/scripts/fx_desktop_build.py"
secrets: true
custom-build-variant-cfg: api-16-debug-ccov
tooltool-downloads: internal
toolchains:
- android-gradle-dependencies
- android-ndk-linux
- android-sdk-linux
- linux64-clang
- linux64-rust-android
- linux64-rust-size
- linux64-sccache
- linux64-node
android-x86/opt:
description: "Android 4.2 x86 Opt"
index:

View File

@ -100,10 +100,10 @@ jobs:
run:
using: debian-package
tarball:
url: https://www.mercurial-scm.org/release/mercurial-4.5.2.tar.gz
sha256: a44a9ffd1c9502a4f97298a6bbcb8a79fc8192424c760c67f17b45c12114e390
url: https://www.mercurial-scm.org/release/mercurial-4.7.tar.gz
sha256: 098cb1437f77fb7f75dc2f008742933c729ec0b63cfa3e4e2f0a8fbc3d0d349f
pre-build-command: >-
cp -r contrib/debian debian &&
cp -r contrib/packaging/debian debian &&
sed -i -e "s/__VERSION__/$(awk -F\" '$2 {print $2}' mercurial/__version__.py)-1.deb7moz1/" \
-e "s/__DATE__/$(date --rfc-2822)/" \
-e "s/__CODENAME__/wheezy/" debian/changelog
@ -116,10 +116,10 @@ jobs:
using: debian-package
dist: stretch
tarball:
url: https://www.mercurial-scm.org/release/mercurial-4.5.2.tar.gz
sha256: a44a9ffd1c9502a4f97298a6bbcb8a79fc8192424c760c67f17b45c12114e390
url: https://www.mercurial-scm.org/release/mercurial-4.7.tar.gz
sha256: 098cb1437f77fb7f75dc2f008742933c729ec0b63cfa3e4e2f0a8fbc3d0d349f
pre-build-command: >-
cp -r contrib/debian debian &&
cp -r contrib/packaging/debian debian &&
sed -i -e "s/__VERSION__/$(awk -F\" '$2 {print $2}' mercurial/__version__.py)-1.deb9moz1/" \
-e "s/__DATE__/$(date --rfc-2822)/" \
-e "s/__CODENAME__/stretch/" debian/changelog

View File

@ -33,6 +33,7 @@ geckoview-junit:
max-run-time: 3600
chunks:
by-test-platform:
android-em-4.3-arm7-api-16-ccov/debug: 4
android-em-4.3-arm7-api-16/debug: 4
android-em-4.3-arm7-api-16/opt: 2
default: 1

View File

@ -322,6 +322,11 @@ android-em-4.3-arm7-api-16/debug:
- android-common-tests
- android-gradle-tests
android-em-4.3-arm7-api-16-ccov/debug:
build-platform: android-api-16-ccov/debug
test-sets:
- android-ccov-tests
android-em-4.3-arm7-api-16/opt:
build-platform: android-api-16/opt
test-sets:

View File

@ -391,6 +391,9 @@ android-x86-kvm-tests:
- reftest
- test-verify
android-ccov-tests:
- geckoview-junit
devtools-tests:
- mochitest-devtools-chrome

View File

@ -454,6 +454,7 @@ linux64-android-gradle-dependencies:
toolchains:
# Aliases aren't allowed for toolchains depending on toolchains.
- linux64-android-sdk-linux-repack
- linux64-node
linux64-rust-1.27:
description: "rust repack"
@ -494,8 +495,7 @@ linux64-rust-1.28:
using: toolchain-script
script: repack_rust.py
arguments: [
# 1.28.0-beta.6
'--channel', 'beta-2018-06-30',
'--channel', '1.28.0',
'--host', 'x86_64-unknown-linux-gnu',
'--target', 'x86_64-unknown-linux-gnu',
'--target', 'i686-unknown-linux-gnu',
@ -542,8 +542,7 @@ linux64-rust-macos-1.28:
using: toolchain-script
script: repack_rust.py
arguments: [
# 1.28.0-beta.6
'--channel', 'beta-2018-06-30',
'--channel', '1.28.0',
'--host', 'x86_64-unknown-linux-gnu',
'--target', 'x86_64-unknown-linux-gnu',
'--target', 'x86_64-apple-darwin',
@ -567,8 +566,7 @@ linux64-rust-android-1.28:
using: toolchain-script
script: repack_rust.py
arguments: [
# 1.28.0-beta.6
'--channel', 'beta-2018-06-30',
'--channel', '1.28.0',
'--host', 'x86_64-unknown-linux-gnu',
'--target', 'x86_64-unknown-linux-gnu',
'--target', 'armv7-linux-androideabi',

View File

@ -135,8 +135,7 @@ win64-rust-1.28:
using: toolchain-script
script: repack_rust.py
arguments: [
# 1.28.0-beta.6
'--channel', 'beta-2018-06-30',
'--channel', '1.28.0',
'--host', 'x86_64-pc-windows-msvc',
'--target', 'x86_64-pc-windows-msvc',
'--target', 'i686-pc-windows-msvc',
@ -202,8 +201,7 @@ win32-rust-1.28:
using: toolchain-script
script: repack_rust.py
arguments: [
# 1.28.0-beta.6
'--channel', 'beta-2018-06-30',
'--channel', '1.28.0',
'--host', 'i686-pc-windows-msvc',
'--target', 'i686-pc-windows-msvc',
]
@ -227,12 +225,12 @@ mingw32-rust-1.28:
using: toolchain-script
script: repack_rust.py
arguments: [
# 1.28.0-beta.6
'--channel', 'beta-2018-06-30',
'--host', 'i686-unknown-linux-gnu',
'--target', 'i686-pc-windows-gnu',
'--target', 'x86_64-unknown-linux-gnu',
'--channel', '1.28.0',
'--host', 'x86_64-unknown-linux-gnu',
'--target', 'i686-unknown-linux-gnu',
'--target', 'i686-pc-windows-gnu',
'--target', 'x86_64-pc-windows-gnu',
'--target', 'x86_64-unknown-linux-gnu',
]
toolchain-alias: mingw32-rust
toolchain-artifact: public/build/rustc.tar.xz

View File

@ -16,13 +16,13 @@ if [ -f /etc/lsb-release ]; then
if [ "${DISTRIB_ID}" = "Ubuntu" ] && [[ "${DISTRIB_RELEASE}" = "16.04" || "${DISTRIB_RELEASE}" = "17.10" || "${DISTRIB_RELEASE}" = "18.04" ]]
then
HG_DEB=1
HG_DIGEST=e58ecb78fb6856161f8af1b7a50a024f1d6aa6efe50e18666aae0368ee1a44eead2f0c52e5088304b4c0e89dd74a058128e9bd184efab0142275a228aa8e0f45
HG_SIZE=193382
HG_FILENAME=mercurial_4.5.2_amd64.deb
HG_DIGEST=21a5ca8170bdb05527b04cbc93f00ecef39680a2c7b80aa625f9add8b7dba0a7bff0ad58e0ba40b1956ae310f681d04302d033e398eca0e001dce10d74d0dbdc
HG_SIZE=250748
HG_FILENAME=mercurial_4.7_amd64.deb
HG_COMMON_DIGEST=b69d94c91ad78a26318e3bbd2f0fda7eb3f3295755a727cde677bf143312c5dfc27ac47e800772d624ea88c6f2576fa2f585c1b8b9930ba83ddc8356661627b8
HG_COMMON_SIZE=2141554
HG_COMMON_FILENAME=mercurial-common_4.5.2_all.deb
HG_COMMON_DIGEST=521e0c150b142d0bbb69fa100b96bac5bee7a108605eea1c484e8544540dcf764efffab2e3a9ad640f56674ad85c1b47647e3d504c96f7567d7d7c21299c2c25
HG_COMMON_SIZE=2315590
HG_COMMON_FILENAME=mercurial-common_4.7_all.deb
elif [ "${DISTRIB_ID}" = "Ubuntu" ] && [ "${DISTRIB_RELEASE}" = "12.04" ]
then
echo "Ubuntu 12.04 not supported"
@ -106,15 +106,15 @@ elif [ -n "${PIP_PATH}" ]; then
tooltool_fetch <<EOF
[
{
"size": 5779915,
"digest": "f70e40cba72b7955f0ecec9c1f53ffffac26f206188617cb182e22ce4f43dc8b970ce46d12c516ef88480c3fa076a59afcddd736dffb642d8e23befaf45b4941",
"size": 6476268,
"digest": "a08dfc4e296b5d162097769ab38ab85b7c5de16710bce0b6dce2a39f56cb517455c0ed634f689d07e9bd082fb7641501b7da51963844aee7ab28233cf721dec8",
"algorithm": "sha512",
"filename": "mercurial-4.5.2.tar.gz"
"filename": "mercurial-4.7.tar.gz"
}
]
EOF
${PIP_PATH} install mercurial-4.5.2.tar.gz
${PIP_PATH} install mercurial-4.7.tar.gz
else
echo "Do not know how to install Mercurial on this OS"
exit 1

View File

@ -560,6 +560,7 @@ def set_treeherder_machine_platform(config, tests):
# The build names for Android platforms have partially evolved over the
# years and need to be translated.
'android-api-16/debug': 'android-em-4-3-armv7-api16/debug',
'android-api-16-ccov/debug': 'android-em-4-3-armv7-api16-ccov/debug',
'android-api-16/opt': 'android-em-4-3-armv7-api16/opt',
'android-x86/opt': 'android-em-4-2-x86/opt',
'android-api-16-gradle/opt': 'android-api-16-gradle/opt',
@ -731,6 +732,12 @@ def enable_code_coverage(config, tests):
if 'opt' in test['build-platform'] or 'fuzzing' in test['build-platform']:
test['run-on-projects'] = []
continue
# Skip this transform for android code coverage builds.
if 'android' in test['build-platform']:
test.setdefault('fetches', {}).setdefault('fetch', []).append('grcov-linux-x86_64')
test['mozharness'].setdefault('extra-options', []).append('--java-code-coverage')
yield test
continue
test['mozharness'].setdefault('extra-options', []).append('--code-coverage')
test['instance-size'] = 'xlarge'
# Ensure we always run on the projects defined by the build, unless the test

View File

@ -51,6 +51,11 @@ class JUnitTestRunner(MochitestDesktop):
self.log.debug("options=%s" % vars(options))
update_mozinfo()
self.remote_profile = posixpath.join(self.device.test_root, 'junit-profile')
if self.options.coverage and not self.options.coverage_output_path:
raise Exception("--coverage-output-path is required when using --enable-coverage")
self.remote_coverage_output_path = posixpath.join(self.device.test_root,
'junit-coverage.ec')
self.server_init()
self.cleanup()
@ -145,6 +150,10 @@ class JUnitTestRunner(MochitestDesktop):
for f in test_filters:
# filter can be class-name or 'class-name#method-name' (single test)
cmd = cmd + " -e class %s" % f
# enable code coverage reports
if self.options.coverage:
cmd = cmd + " -e coverage true"
cmd = cmd + " -e coverageFile %s" % self.remote_coverage_output_path
# environment
env = {}
env["MOZ_CRASHREPORTER"] = "1"
@ -262,6 +271,9 @@ class JUnitTestRunner(MochitestDesktop):
if self.check_for_crashes():
self.fail_count = 1
if self.options.coverage:
self.device.pull(self.remote_coverage_output_path, self.options.coverage_output_path)
return 1 if self.fail_count else 0
def check_for_crashes(self):
@ -364,6 +376,17 @@ class JunitArgumentParser(argparse.ArgumentParser):
dest="thisChunk",
default=None,
help="If running tests by chunks, the chunk number to run.")
self.add_argument("--enable-coverage",
action="store_true",
dest="coverage",
default=False,
help="Enable code coverage collection.")
self.add_argument("--coverage-output-path",
action="store",
type=str,
dest="coverage_output_path",
default=None,
help="If collecting code coverage, save the report file to this path.")
# Additional options for server.
self.add_argument("--certificate-path",
action="store",

View File

@ -0,0 +1,15 @@
config = {
'base_name': 'Android armv7 api-16+ %(branch)s debug coverage',
'stage_platform': 'android-api-16-debug-ccov',
'src_mozconfig': 'mobile/android/config/mozconfigs/android-api-16/debug-ccov',
'multi_locale_config_platform': 'android',
'debug_build': True,
'postflight_build_mach_commands': [
['android',
'archive-geckoview',
],
['android',
'archive-geckoview-coverage-artifacts',
],
],
}

View File

@ -49,7 +49,7 @@ except ImportError:
# Causes worker to purge caches on process exit and for task to retry.
EXIT_PURGE_CACHE = 72
testedwith = '3.7 3.8 3.9 4.0 4.1 4.2 4.3 4.4 4.5'
testedwith = '3.7 3.8 3.9 4.0 4.1 4.2 4.3 4.4 4.5 4.6 4.7'
minimumhgversion = '3.7'
cmdtable = {}
@ -86,6 +86,17 @@ def getsparse():
return sparse
def supported_hg():
'''Returns True if the Mercurial version is supported for robustcheckout'''
return util.versiontuple(n=2) in (
(4, 3),
(4, 4),
(4, 5),
(4, 6),
(4, 7),
)
if os.name == 'nt':
import ctypes
@ -228,7 +239,7 @@ def robustcheckout(ui, url, dest, upstream=None, revision=None, branch=None,
# However, given that sparse has performance implications, we want to fail
# fast if we can't satisfy the desired checkout request.
if sparseprofile:
if util.versiontuple(n=2) not in ((4, 3), (4, 4), (4, 5)):
if not supported_hg():
raise error.Abort('sparse profile support only available for '
'Mercurial versions greater than 4.3 (using %s)' % util.version())
@ -575,16 +586,21 @@ def _docheckout(ui, url, dest, upstream, revision, branch, purge, sharebase,
# We only pull if we are using symbolic names or the requested revision
# doesn't exist.
havewantedrev = False
if revision and revision in repo:
ctx = repo[revision]
if not ctx.hex().startswith(revision):
raise error.Abort('--revision argument is ambiguous',
hint='must be the first 12+ characters of a '
'SHA-1 fragment')
if revision:
try:
ctx = scmutil.revsingle(repo, revision)
except error.RepoLookupError:
ctx = None
checkoutrevision = ctx.hex()
havewantedrev = True
if ctx:
if not ctx.hex().startswith(revision):
raise error.Abort('--revision argument is ambiguous',
hint='must be the first 12+ characters of a '
'SHA-1 fragment')
checkoutrevision = ctx.hex()
havewantedrev = True
if not havewantedrev:
ui.write('(pulling to obtain %s)\n' % (revision or branch,))
@ -635,7 +651,7 @@ def _docheckout(ui, url, dest, upstream, revision, branch, purge, sharebase,
try:
old_sparse_fn = getattr(repo.dirstate, '_sparsematchfn', None)
if old_sparse_fn is not None:
assert util.versiontuple(n=2) in ((4, 3), (4, 4), (4, 5))
assert supported_hg(), 'Mercurial version not supported (must be 4.3+)'
repo.dirstate._sparsematchfn = lambda: matchmod.always(repo.root, '')
with timeit('purge'):

View File

@ -428,6 +428,7 @@ class BuildOptionParser(object):
'api-16': 'builds/releng_sub_%s_configs/%s_api_16.py',
'api-16-artifact': 'builds/releng_sub_%s_configs/%s_api_16_artifact.py',
'api-16-debug': 'builds/releng_sub_%s_configs/%s_api_16_debug.py',
'api-16-debug-ccov': 'builds/releng_sub_%s_configs/%s_api_16_debug_ccov.py',
'api-16-debug-artifact': 'builds/releng_sub_%s_configs/%s_api_16_debug_artifact.py',
'api-16-gradle': 'builds/releng_sub_%s_configs/%s_api_16_gradle.py',
'api-16-gradle-artifact': 'builds/releng_sub_%s_configs/%s_api_16_gradle_artifact.py',

View File

@ -45,6 +45,12 @@ code_coverage_config_options = [
"default": False,
"help": "Whether JSDebugger code coverage should be run."
}],
[["--java-code-coverage"],
{"action": "store_true",
"dest": "java_code_coverage",
"default": False,
"help": "Whether Java code coverage should be run."
}],
]
@ -89,11 +95,14 @@ class CodeCoverageMixin(SingleTestMixin):
except (AttributeError, KeyError, TypeError):
return False
@PostScriptAction('download-and-extract')
def setup_coverage_tools(self, action, success=None):
if not self.code_coverage_enabled:
return
@property
def java_code_coverage_enabled(self):
try:
return bool(self.config.get('java_code_coverage'))
except (AttributeError, KeyError, TypeError):
return False
def _setup_cpp_js_coverage_tools(self):
if mozinfo.os == 'linux' or mozinfo.os == 'mac':
self.prefix = '/builds/worker/workspace/build/src/'
strip_count = self.prefix.count('/')
@ -107,11 +116,34 @@ class CodeCoverageMixin(SingleTestMixin):
os.environ['GCOV_PREFIX_STRIP'] = str(strip_count)
# Install grcov on the test machine
# Get the path to the build machines gcno files.
self.url_to_gcno = self.query_build_dir_url('target.code-coverage-gcno.zip')
self.url_to_chrome_map = self.query_build_dir_url('chrome-map.json')
# Download the gcno archive from the build machine.
url_to_gcno = self.query_build_dir_url('target.code-coverage-gcno.zip')
self.download_file(url_to_gcno, parent_dir=self.grcov_dir)
# Download the chrome-map.json file from the build machine.
url_to_chrome_map = self.query_build_dir_url('chrome-map.json')
self.download_file(url_to_chrome_map, parent_dir=self.grcov_dir)
def _setup_java_coverage_tools(self):
# Download and extract jacoco-cli from the build task.
url_to_jacoco = self.query_build_dir_url('target.jacoco-cli.jar')
self.jacoco_jar = os.path.join(tempfile.mkdtemp(), 'target.jacoco-cli.jar')
self.download_file(url_to_jacoco, self.jacoco_jar)
# Download and extract class files from the build task.
self.classfiles_dir = tempfile.mkdtemp()
url_to_classfiles = self.query_build_dir_url('target.geckoview_classfiles.zip')
classfiles_zip_path = os.path.join(self.classfiles_dir, 'target.geckoview_classfiles.zip')
self.download_file(url_to_classfiles, classfiles_zip_path)
with zipfile.ZipFile(classfiles_zip_path, 'r') as z:
z.extractall(self.classfiles_dir)
os.remove(classfiles_zip_path)
# Create the directory where the emulator coverage file will be placed.
self.java_coverage_output_path = os.path.join(tempfile.mkdtemp(),
'junit-coverage.ec')
def _download_grcov(self):
fetches_dir = os.environ.get('MOZ_FETCHES_DIR')
if fetches_dir and os.path.isfile(os.path.join(fetches_dir, 'grcov')):
self.grcov_dir = fetches_dir
@ -130,11 +162,18 @@ class CodeCoverageMixin(SingleTestMixin):
tar.extractall(self.grcov_dir)
os.remove(os.path.join(self.grcov_dir, filename))
# Download the gcno archive from the build machine.
self.download_file(self.url_to_gcno, parent_dir=self.grcov_dir)
@PostScriptAction('download-and-extract')
def setup_coverage_tools(self, action, success=None):
if not self.code_coverage_enabled and not self.java_code_coverage_enabled:
return
# Download the chrome-map.json file from the build machine.
self.download_file(self.url_to_chrome_map, parent_dir=self.grcov_dir)
self._download_grcov()
if self.code_coverage_enabled:
self._setup_cpp_js_coverage_tools()
if self.java_code_coverage_enabled:
self._setup_java_coverage_tools()
@PostScriptAction('download-and-extract')
def find_tests_for_coverage(self, action, success=None):
@ -440,6 +479,49 @@ class CodeCoverageMixin(SingleTestMixin):
shutil.rmtree(self.grcov_dir)
@PostScriptAction('run-tests')
def process_java_coverage_data(self, action, success=None):
'''
Run JaCoCo on the coverage.ec file in order to get a XML report.
After that, run grcov on the XML report to get a lcov report.
Finally, archive the lcov file and upload it, as process_coverage_data is doing.
'''
if not self.java_code_coverage_enabled:
return
# If the emulator became unresponsive, the task has failed and we don't
# have the coverage report file, so stop running this function and
# allow the task to be retried automatically.
if not success and not os.path.exists(self.java_coverage_output_path):
return
dirs = self.query_abs_dirs()
xml_path = tempfile.mkdtemp()
jacoco_command = ['java', '-jar', self.jacoco_jar, 'report',
self.java_coverage_output_path,
'--classfiles', self.classfiles_dir,
'--name', 'geckoview-junit',
'--xml', os.path.join(xml_path, 'geckoview-junit.xml')]
self.run_command(jacoco_command, halt_on_failure=True)
grcov_command = [
os.path.join(self.grcov_dir, 'grcov'),
'-t', 'lcov',
xml_path,
]
tmp_output_file, _ = self.get_output_from_command(
grcov_command,
silent=True,
save_tmpfiles=True,
return_type='files',
throw_exception=True,
)
if not self.ccov_upload_disabled:
grcov_zip_path = os.path.join(dirs['abs_blob_upload_dir'], 'code-coverage-grcov.zip')
with zipfile.ZipFile(grcov_zip_path, 'w', zipfile.ZIP_DEFLATED) as z:
z.write(tmp_output_file, 'grcov_lcov_output.info')
def rm_baseline_cov(baseline_coverage, test_coverage):
'''

View File

@ -26,7 +26,10 @@ from mozharness.base.script import BaseScript, PreScriptAction, PostScriptAction
from mozharness.mozilla.automation import TBPL_RETRY, EXIT_STATUS_DICT
from mozharness.mozilla.mozbase import MozbaseMixin
from mozharness.mozilla.testing.testbase import TestingMixin, testing_config_options
from mozharness.mozilla.testing.codecoverage import CodeCoverageMixin
from mozharness.mozilla.testing.codecoverage import (
CodeCoverageMixin,
code_coverage_config_options
)
class AndroidEmulatorTest(TestingMixin, BaseScript, MozbaseMixin, CodeCoverageMixin):
@ -77,7 +80,8 @@ class AndroidEmulatorTest(TestingMixin, BaseScript, MozbaseMixin, CodeCoverageMi
"default": "info",
"help": "Set log level (debug|info|warning|error|critical|fatal)",
}
]] + copy.deepcopy(testing_config_options)
]] + copy.deepcopy(testing_config_options) + \
copy.deepcopy(code_coverage_config_options)
app_name = None
@ -487,6 +491,10 @@ class AndroidEmulatorTest(TestingMixin, BaseScript, MozbaseMixin, CodeCoverageMi
None,
try_tests))
if self.java_code_coverage_enabled:
cmd.extend(['--enable-coverage',
'--coverage-output-path', self.java_coverage_output_path])
return cmd
def _get_repo_url(self, path):

View File

@ -1077,14 +1077,12 @@ class HiddenXULWindow {
// The windowless browser is a thin wrapper around a docShell that keeps
// its related resources alive. It implements nsIWebNavigation and
// forwards its methods to the underlying docShell, but cannot act as a
// docShell itself. Calling `getInterface(nsIDocShell)` gives us the
// docShell itself. Getting .docShell gives us the
// underlying docShell, and `QueryInterface(nsIWebNavigation)` gives us
// access to the webNav methods that are already available on the
// windowless browser, but contrary to appearances, they are not the same
// object.
this.chromeShell = this._windowlessBrowser
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
this.chromeShell = this._windowlessBrowser.docShell
.QueryInterface(Ci.nsIWebNavigation);
if (PrivateBrowsingUtils.permanentPrivateBrowsing) {

View File

@ -118,9 +118,8 @@ class ContentPage {
let system = Services.scriptSecurityManager.getSystemPrincipal();
let chromeShell = this.windowlessBrowser.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIWebNavigation);
let chromeShell = this.windowlessBrowser.docShell
.QueryInterface(Ci.nsIWebNavigation);
chromeShell.createAboutBlankContentViewer(system);
chromeShell.useGlobalHistory = false;

View File

@ -67,10 +67,10 @@ public:
void UnregisterContentScript(const WebExtensionContentScript& script,
ErrorResult& aRv);
bool CanAccessURI(const URLInfo& aURI, bool aExplicit = false) const
bool CanAccessURI(const URLInfo& aURI, bool aExplicit = false, bool aCheckRestricted = true) const
{
return (!IsRestrictedURI(aURI) &&
mHostPermissions && mHostPermissions->Matches(aURI, aExplicit));
return (!aCheckRestricted || !IsRestrictedURI(aURI)) &&
mHostPermissions && mHostPermissions->Matches(aURI, aExplicit);
}
bool IsPathWebAccessible(const nsAString& aPath) const

View File

@ -708,9 +708,7 @@ this.downloads = class extends ExtensionAPI {
return new Promise((resolve, reject) => {
let chromeWebNav = Services.appShell.createWindowlessBrowser(true);
chromeWebNav
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
chromeWebNav.docShell
.createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
let img = chromeWebNav.document.createElement("img");

View File

@ -808,8 +808,7 @@ function loadImage(img, data) {
add_task(async function test_getFileIcon() {
let webNav = Services.appShell.createWindowlessBrowser(false);
let docShell = webNav.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell);
let docShell = webNav.docShell;
let system = Services.scriptSecurityManager.getSystemPrincipal();
docShell.createAboutBlankContentViewer(system);

View File

@ -8,10 +8,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "gProxyService",
const TRANSPARENT_PROXY_RESOLVES_HOST = Ci.nsIProxyInfo.TRANSPARENT_PROXY_RESOLVES_HOST;
function getProxyInfo() {
function getProxyInfo(url = "http://www.mozilla.org/") {
return new Promise((resolve, reject) => {
let channel = NetUtil.newChannel({
uri: "http://www.mozilla.org/",
uri: url,
loadUsingSystemPrincipal: true,
});
@ -176,13 +176,14 @@ async function getExtension(expectedProxyInfo) {
add_task(async function test_passthrough() {
let ext1 = await getExtension(null);
let ext2 = await getExtension({host: "1.2.3.4", port: 8888, type: "http"});
let ext2 = await getExtension({host: "1.2.3.4", port: 8888, type: "https"});
let proxyInfo = await getProxyInfo();
// Also use a restricted url to test the ability to proxy those.
let proxyInfo = await getProxyInfo("https://addons.mozilla.org/");
equal(proxyInfo.host, "1.2.3.4", `second extension won`);
equal(proxyInfo.port, "8888", `second extension won`);
equal(proxyInfo.type, "http", `second extension won`);
equal(proxyInfo.type, "https", `second extension won`);
await ext2.unload();

View File

@ -17,7 +17,7 @@ skip-if = os == "android" # Android does not use Places for history.
skip-if = os == "android"
[test_ext_browserSettings.js]
[test_ext_browserSettings_homepage.js]
skip-if = os == "android"
skip-if = appname == "thunderbird" || os == "android"
[test_ext_cookieBehaviors.js]
[test_ext_cookies_samesite.js]
[test_ext_content_security_policy.js]
@ -31,21 +31,25 @@ skip-if = os == 'android' && debug # The generated script takes too long to load
[test_ext_contentscript_restrictSchemes.js]
[test_ext_contentscript_teardown.js]
[test_ext_contextual_identities.js]
skip-if = os == "android" # Containers are not exposed to android.
skip-if = appname == "thunderbird" || os == "android" # Containers are not exposed to android.
[test_ext_debugging_utils.js]
[test_ext_dns.js]
[test_ext_downloads.js]
[test_ext_downloads_download.js]
fail-if = appname == "thunderbird"
skip-if = os == "android"
[test_ext_downloads_misc.js]
fail-if = appname == "thunderbird"
skip-if = os == "android" || (os=='linux' && bits==32) # linux32: bug 1324870
[test_ext_downloads_private.js]
skip-if = os == "android"
skip-if = appname == "thunderbird" || os == "android"
[test_ext_downloads_search.js]
fail-if = appname == "thunderbird"
skip-if = os == "android"
[test_ext_error_location.js]
[test_ext_eventpage_warning.js]
[test_ext_experiments.js]
fail-if = appname == "thunderbird"
[test_ext_extension.js]
[test_ext_extensionPreferencesManager.js]
[test_ext_extensionSettingsStore.js]
@ -61,18 +65,22 @@ skip-if = os == "android" # checking for telemetry needs to be updated: 1384923
skip-if = (os == "win" && !debug) #Bug 1419183 disable on Windows
[test_ext_management_uninstall_self.js]
[test_ext_messaging_startup.js]
skip-if = appname == "thunderbird"
[test_ext_onmessage_removelistener.js]
skip-if = true # This test no longer tests what it is meant to test.
[test_ext_permission_xhr.js]
[test_ext_persistent_events.js]
[test_ext_privacy.js]
skip-if = appname == "thunderbird"
[test_ext_privacy_disable.js]
skip-if = appname == "thunderbird"
[test_ext_privacy_update.js]
[test_ext_proxy_auth.js]
[test_ext_proxy_config.js]
skip-if = appname == "thunderbird"
[test_ext_proxy_onauthrequired.js]
[test_ext_proxy_settings.js]
skip-if = os == "android" # proxy settings are not supported on android
skip-if = appname == "thunderbird" || os == "android" # proxy settings are not supported on android
[test_ext_proxy_socks.js]
[test_ext_proxy_speculative.js]
[test_ext_proxy_startup.js]
@ -103,9 +111,9 @@ skip-if = os == "android"
skip-if = os == "android"
[test_ext_storage_sync.js]
head = head.js head_sync.js
skip-if = os == "android"
skip-if = appname == "thunderbird" || os == "android"
[test_ext_storage_sync_crypto.js]
skip-if = os == "android"
skip-if = appname == "thunderbird" || os == "android"
[test_ext_storage_tab.js]
[test_ext_storage_telemetry.js]
skip-if = os == "android" # checking for telemetry needs to be updated: 1384923
@ -121,18 +129,20 @@ skip-if = true # Too frequent intermittent failures
[test_ext_webRequest_permission.js]
[test_ext_webRequest_responseBody.js]
[test_ext_webRequest_set_cookie.js]
skip-if = appname == "thunderbird"
[test_ext_webRequest_startup.js]
[test_ext_webRequest_suspend.js]
[test_ext_webRequest_webSocket.js]
skip-if = appname == "thunderbird"
[test_ext_xhr_capabilities.js]
[test_native_manifests.js]
subprocess = true
skip-if = os == "android"
[test_ext_permissions.js]
skip-if = os == "android" # Bug 1350559
skip-if = appname == "thunderbird" || os == "android" # Bug 1350559
[test_proxy_listener.js]
[test_proxy_scripts.js]
[test_proxy_scripts_results.js]
[test_ext_brokenlinks.js]
[test_ext_performance_counters.js]
skip-if = os == "android"
skip-if = appname == "thunderbird" || os == "android"

View File

@ -1,7 +1,6 @@
[DEFAULT]
head = head.js head_telemetry.js head_storage.js
firefox-appdir = browser
skip-if = appname == "thunderbird"
dupe-manifest =
support-files =
data/**

View File

@ -521,13 +521,14 @@ ChannelWrapper::Matches(const dom::MozRequestFilter& aFilter,
}
if (aExtension) {
if (!aExtension->CanAccessURI(urlInfo)) {
bool isProxy = aOptions.mIsProxy && aExtension->HasPermission(nsGkAtoms::proxy);
// Proxies are allowed access to all urls, including restricted urls.
if (!aExtension->CanAccessURI(urlInfo, false, !isProxy)) {
return false;
}
// If this isn't the proxy phase of the request, check that the extension
// has origin permissions for origin that originated the request.
bool isProxy = aOptions.mIsProxy && aExtension->HasPermission(nsGkAtoms::proxy);
if (!isProxy) {
if (IsSystemLoad()) {
return false;

View File

@ -212,6 +212,8 @@ AboutReader.prototype = {
btn.title = message.data.title;
if (message.data.text)
btn.textContent = message.data.text;
if (message.data.width && message.data.height)
btn.style.backgroundSize = `${message.data.width}px ${message.data.height}px`;
let tb = this._toolbarElement;
tb.appendChild(btn);
this._setupButton(message.data.id, button => {

View File

@ -59,9 +59,7 @@ find-menu appears in editor element which has had makeEditable() called but desi
async function onDocumentLoaded() {
await ContentTask.spawn(gBrowser, null, async function() {
var edsession = content.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIEditingSession);
var edsession = content.docShell.editingSession;
edsession.makeWindowEditable(content, "html", false, true, false);
content.focus();
});

View File

@ -131,7 +131,7 @@
onget="return this.docShell.contentViewer;"/>
<property name="editingSession"
readonly="true"
onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIEditingSession);"/>
onget="return this.docShell.editingSession"/>
<property name="commandManager"
readonly="true"
onget="return this.webNavigation.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsICommandManager);"/>

View File

@ -104,7 +104,7 @@ HiddenFrame.prototype = {
}
};
this._webProgress.addProgressListener(this._listener, Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
let docShell = this._browser.getInterface(Ci.nsIDocShell);
let docShell = this._browser.docShell;
docShell.createAboutBlankContentViewer(Services.scriptSecurityManager.getSystemPrincipal());
docShell.useGlobalHistory = false;
this._browser.loadURI(XUL_PAGE, 0, null, null, null);

View File

@ -419,9 +419,7 @@ var SpellCheckHelper = {
if (win) {
var isSpellcheckable = false;
try {
var editingSession = win.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIEditingSession);
var editingSession = win.docShell.editingSession;
if (editingSession.windowIsEditable(win) &&
this.getComputedStyle(element, "-moz-user-modify") == "read-write") {
isSpellcheckable = true;

View File

@ -21,9 +21,7 @@ var InlineSpellCheckerContent = {
if (!(editFlags & (SpellCheckHelper.TEXTAREA | SpellCheckHelper.INPUT))) {
// Get the editor off the window.
let win = event.target.ownerGlobal;
let editingSession = win.docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIEditingSession);
let editingSession = win.docShell.editingSession;
spellChecker = this._spellChecker =
new InlineSpellChecker(editingSession.getEditorForWindow(win));
} else {

View File

@ -18,6 +18,26 @@ XPCOMUtils.defineLazyModuleGetters(this, {
Preferences: "resource://gre/modules/Preferences.jsm",
});
// The current platform as specified in the AMO API:
// http://addons-server.readthedocs.io/en/latest/topics/api/addons.html#addon-detail-platform
XPCOMUtils.defineLazyGetter(this, "PLATFORM", () => {
let platform = Services.appinfo.OS;
switch (platform) {
case "Darwin":
return "mac";
case "Linux":
return "linux";
case "Android":
return "android";
case "WINNT":
return "windows";
}
return platform;
});
var EXPORTED_SYMBOLS = [ "AddonRepository" ];
const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
@ -582,7 +602,7 @@ var AddonRepository = {
addon.version = String(aEntry.current_version.version);
if (Array.isArray(aEntry.current_version.files)) {
for (let file of aEntry.current_version.files) {
if (file.platform == "all" || file.platform == Services.appinfo.OS.toLowerCase()) {
if (file.platform == "all" || file.platform == PLATFORM) {
if (file.url) {
addon.sourceURI = NetUtil.newURI(file.url);
}

View File

@ -59,6 +59,21 @@
"weekly_downloads": 3333,
"last_updated": "2010-02-01T14:04:05Z"
},
{
"name": "PASS",
"type": "extension",
"guid": "test2@tests.mozilla.org",
"current_version": {
"version": "2.0",
"files": [
{
"platform": "XPCShell",
"url": "http://example.com/addons/bleah.xpi",
"size": 1000
}
]
}
},
{
"name": "FAIL",
"type": "extension",

View File

@ -123,6 +123,12 @@ var GET_RESULTS = [{
weeklyDownloads: 3333,
sourceURI: BASE_URL + INSTALL_URL2,
updateDate: new Date(1265033045000),
}, {
id: "test2@tests.mozilla.org",
type: "extension",
version: "2.0",
icons: {},
sourceURI: "http://example.com/addons/bleah.xpi",
}, {
id: "test_AddonRepository_1@tests.mozilla.org",
type: "theme",
@ -137,9 +143,11 @@ var GET_TEST = {
failedIDs: ["test1@tests.mozilla.org"],
failedURL: "/XPCShell/1/test1%40tests.mozilla.org",
successfulIDs: ["test1@tests.mozilla.org",
"{00000000-1111-2222-3333-444444444444}",
"test_AddonRepository_1@tests.mozilla.org"],
"test2@tests.mozilla.org",
"{00000000-1111-2222-3333-444444444444}",
"test_AddonRepository_1@tests.mozilla.org"],
successfulURL: "/XPCShell/1/test1%40tests.mozilla.org%2C" +
"test2%40tests.mozilla.org%2C" +
"%7B00000000-1111-2222-3333-444444444444%7D%2C" +
"test_AddonRepository_1%40tests.mozilla.org"
};

View File

@ -44,8 +44,6 @@ async function tearDownAddon(addon) {
}
add_task(async function test_reloading_a_temp_addon() {
if (AppConstants.MOZ_APP_NAME == "thunderbird")
return;
await promiseRestartManager();
let xpi = AddonTestUtils.createTempXPIFile(ADDONS.webextension_1);
const addon = await AddonManager.installTemporaryAddon(xpi);

View File

@ -763,8 +763,7 @@ add_task(async function test_replace_permanent_disabled() {
});
// Tests that XPIs with a .zip extension work when loaded temporarily.
add_task({ skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
async function test_zip_extension() {
add_task(async function test_zip_extension() {
let xpi = createTempWebExtensionFile({
background() {
/* globals browser */

View File

@ -84,7 +84,6 @@ tags = blocklist
head =
[test_delay_update.js]
[test_delay_update_webextension.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_dependencies.js]
[test_dictionary.js]
@ -95,7 +94,6 @@ tags = webextensions
skip-if = os == "android"
[test_error.js]
[test_ext_management.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_general.js]
[test_getresource.js]
@ -222,7 +220,6 @@ skip-if = os == "mac" && debug
[test_system_update_empty.js]
skip-if = true # Failing intermittently due to a race condition in the test, see bug 1348981
[test_system_update_enterprisepolicy.js]
skip-if = appname == "thunderbird"
[test_system_update_fail.js]
[test_system_update_newset.js]
[test_system_update_overlapping.js]
@ -258,25 +255,18 @@ skip-if = os == "android"
run-sequentially = Uses global XCurProcD dir.
[test_upgrade_incompatible.js]
[test_webextension.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_embedded.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_events.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_icons.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_install.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_install_syntax_error.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_langpack.js]
skip-if = appname == "thunderbird"
tags = webextensions
[test_webextension_theme.js]
tags = webextensions

Some files were not shown because too many files have changed in this diff Show More