Merge m-c to inbound, a=merge CLOSED TREE

This commit is contained in:
Wes Kocher 2016-09-08 15:28:31 -07:00
commit c067786818
55 changed files with 1280 additions and 367 deletions

View File

@ -4837,6 +4837,12 @@ var TabsProgressListener = {
if (!Object.getOwnPropertyDescriptor(window, "PopupNotifications").get)
PopupNotifications.locationChange(aBrowser);
let tab = gBrowser.getTabForBrowser(aBrowser);
if (tab && tab._sharingState) {
gBrowser.setBrowserSharing(aBrowser, {});
webrtcUI.forgetStreamsFromBrowser(aBrowser);
}
gBrowser.getNotificationBox(aBrowser).removeTransientNotifications();
FullZoom.onLocationChange(aLocationURI, false, aBrowser);

View File

@ -2663,6 +2663,8 @@
if (aOtherTab.hasAttribute("sharing")) {
aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
modifiedAttrs.push("sharing");
aOurTab._sharingState = aOtherTab._sharingState;
webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
}
// If the other tab is pending (i.e. has not been restored, yet)

View File

@ -8,3 +8,4 @@ support-files =
skip-if = buildapp == 'mulet' || (os == "linux" && debug) # linux: bug 976544
[browser_devices_get_user_media_anim.js]
[browser_devices_get_user_media_in_frame.js]
[browser_devices_get_user_media_tear_off_tab.js]

View File

@ -197,6 +197,41 @@ var gTests = [
yield expectNoObserverCalled();
yield checkNotSharing();
}
},
{
desc: "getUserMedia audio+video: reloading the top level page removes all sharing UI",
run: function* checkReloading() {
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true, "frame1");
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({video: true, audio: true});
info("reloading the web page");
promise = promiseObserverCalled("recording-device-events");
content.location.reload();
yield promise;
if ((yield promiseTodoObserverNotCalled("recording-device-events")) == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
}
yield expectObserverCalled("recording-window-ended");
yield expectNoObserverCalled();
yield checkNotSharing();
}
}
];

View File

@ -0,0 +1,106 @@
/* 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/. */
registerCleanupFunction(function() {
gBrowser.removeCurrentTab();
});
var gTests = [
{
desc: "getUserMedia: tearing-off a tab keeps sharing indicators",
run: function* checkTearingOff() {
let promise = promisePopupNotificationShown("webRTC-shareDevices");
yield promiseRequestDevice(true, true);
yield promise;
yield expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
yield expectObserverCalled("getUserMedia:response:allow");
yield expectObserverCalled("recording-device-events");
is((yield getMediaCaptureState()), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({video: true, audio: true});
info("tearing off the tab");
let win = gBrowser.replaceTabWithWindow(gBrowser.selectedTab);
yield whenDelayedStartupFinished(win);
yield checkSharingUI({audio: true, video: true}, win);
// Clicking the global sharing indicator should open the control center in
// the second window.
ok(win.gIdentityHandler._identityPopup.hidden, "control center should be hidden");
let activeStreams = webrtcUI.getActiveStreams(true, false, false);
webrtcUI.showSharingDoorhanger(activeStreams[0], "Devices");
ok(!win.gIdentityHandler._identityPopup.hidden,
"control center should be open in the second window");
ok(gIdentityHandler._identityPopup.hidden,
"control center should be hidden in the first window");
win.gIdentityHandler._identityPopup.hidden = true;
// Closing the new window should remove all sharing indicators.
// We need to load the content script in the first window so that we can
// catch the notifications fired globally when closing the second window.
gBrowser.selectedBrowser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
yield BrowserTestUtils.closeWindow(win);
if ((yield promiseTodoObserverNotCalled("recording-device-events")) == 1) {
todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719");
}
yield expectObserverCalled("recording-window-ended");
yield expectNoObserverCalled();
yield checkNotSharing();
}
}
];
function test() {
waitForExplicitFinish();
// An empty tab where we can load the content script without leaving it
// behind at the end of the test.
gBrowser.addTab();
let tab = gBrowser.addTab();
gBrowser.selectedTab = tab;
let browser = tab.linkedBrowser;
browser.messageManager.loadFrameScript(CONTENT_SCRIPT_HELPER, true);
browser.addEventListener("load", function onload() {
browser.removeEventListener("load", onload, true);
is(PopupNotifications._currentNotifications.length, 0,
"should start the test without any prior popup notification");
ok(gIdentityHandler._identityPopup.hidden,
"should start the test with the control center hidden");
Task.spawn(function* () {
yield SpecialPowers.pushPrefEnv({"set": [[PREF_PERMISSION_FAKE, true]]});
for (let test of gTests) {
info(test.desc);
yield test.run();
// Cleanup before the next test
yield expectNoObserverCalled();
}
}).then(finish, ex => {
ok(false, "Unexpected Exception: " + ex);
finish();
});
}, true);
let rootDir = getRootDirectory(gTestPath);
rootDir = rootDir.replace("chrome://mochitests/content/",
"https://example.com/");
content.location = rootDir + "get_user_media.html";
}

View File

@ -63,6 +63,18 @@ function promiseWindow(url) {
});
}
function whenDelayedStartupFinished(aWindow) {
return new Promise(resolve => {
info("Waiting for delayed startup to finish");
Services.obs.addObserver(function observer(aSubject, aTopic) {
if (aWindow == aSubject) {
Services.obs.removeObserver(observer, aTopic);
resolve();
}
}, "browser-delayed-startup-finished", false);
});
}
function promiseIndicatorWindow() {
// We don't show the indicator window on Mac.
if ("nsISystemStatusBar" in Ci)
@ -407,9 +419,10 @@ function checkDeviceSelectors(aAudio, aVideo) {
ok(cameraSelector.hidden, "camera selector hidden");
}
function* checkSharingUI(aExpected) {
function* checkSharingUI(aExpected, aWin = window) {
let doc = aWin.document;
// First check the icon above the control center (i) icon.
let identityBox = document.getElementById("identity-box");
let identityBox = doc.getElementById("identity-box");
ok(identityBox.hasAttribute("sharing"), "sharing attribute is set");
let sharing = identityBox.getAttribute("sharing");
if (aExpected.video)
@ -419,7 +432,7 @@ function* checkSharingUI(aExpected) {
// Then check the sharing indicators inside the control center panel.
identityBox.click();
let permissions = document.getElementById("identity-popup-permission-list");
let permissions = doc.getElementById("identity-popup-permission-list");
for (let id of ["microphone", "camera", "screen"]) {
let convertId = id => {
if (id == "camera")
@ -429,7 +442,7 @@ function* checkSharingUI(aExpected) {
return id;
};
let expected = aExpected[convertId(id)];
is(!!gIdentityHandler._sharingState[id], !!expected,
is(!!aWin.gIdentityHandler._sharingState[id], !!expected,
"sharing state for " + id + " as expected");
let icon = permissions.querySelectorAll(
".identity-popup-permission-icon." + id + "-icon");
@ -445,7 +458,7 @@ function* checkSharingUI(aExpected) {
is(icon.length, 1, "should not show more than 1 " + id + " icon");
}
}
gIdentityHandler._identityPopup.hidden = true;
aWin.gIdentityHandler._identityPopup.hidden = true;
// Check the global indicators.
yield* assertWebRTCIndicatorStatus(aExpected);

View File

@ -311,6 +311,20 @@ const DownloadsPanel = {
return this._onKeyDown(aEvent);
case "keypress":
return this._onKeyPress(aEvent);
case "popupshown":
if (this.setHeightToFitOnShow) {
this.setHeightToFitOnShow = false;
this.setHeightToFit();
}
break;
}
},
setHeightToFit() {
if (this._state == this.kStateShown) {
DownloadsBlockedSubview.view.setHeightToFit();
} else {
this.setHeightToFitOnShow = true;
}
},
@ -429,6 +443,8 @@ const DownloadsPanel = {
// Handle keypress to be able to preventDefault() events before they reach
// the richlistbox, for keyboard navigation.
this.panel.addEventListener("keypress", this, false);
// Handle height adjustment on show.
this.panel.addEventListener("popupshown", this, false);
},
/**
@ -438,6 +454,7 @@ const DownloadsPanel = {
_unattachEventListeners() {
this.panel.removeEventListener("keydown", this, false);
this.panel.removeEventListener("keypress", this, false);
this.panel.removeEventListener("popupshown", this, false);
},
_onKeyPress(aEvent) {
@ -742,8 +759,6 @@ const DownloadsView = {
DownloadsPanel.panel.removeAttribute("hasdownloads");
}
DownloadsBlockedSubview.view.setHeightToFit();
// If we've got some hidden downloads, we should activate the
// DownloadsSummary. The DownloadsSummary will determine whether or not
// it's appropriate to actually display the summary.
@ -874,6 +889,9 @@ const DownloadsView = {
}
this._itemCountChanged();
// Adjust the panel height if we removed items.
DownloadsPanel.setHeightToFit();
},
/**
@ -1528,7 +1546,7 @@ const DownloadsFooter = {
}
if (!aValue && this._showingSummary) {
// Make sure the panel's height shrinks when the summary is hidden.
DownloadsBlockedSubview.view.setHeightToFit();
DownloadsPanel.setHeightToFit();
}
this._showingSummary = aValue;
}

View File

@ -128,6 +128,10 @@ this.webrtcUI = {
}
},
forgetStreamsFromBrowser: function(aBrowser) {
this._streams = this._streams.filter(stream => stream.browser != aBrowser);
},
showSharingDoorhanger: function(aActiveStream, aType) {
let browserWindow = aActiveStream.browser.ownerGlobal;
if (aActiveStream.tab) {

View File

@ -120,7 +120,6 @@
<html:div id="layout-header">
<html:div id="layout-expander" class="expander theme-twisty expandable" open=""></html:div>
<html:span>&layoutViewTitle;</html:span>
<html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
</html:div>
<html:div id="layout-container">
@ -158,6 +157,7 @@
<html:div id="layout-info">
<html:span id="layout-element-size"></html:span>
<html:section id="layout-position-group">
<html:button class="devtools-button" id="layout-geometry-editor" title="&geometry.button.tooltip;"></html:button>
<html:span id="layout-element-position"></html:span>
</html:section>
</html:div>

View File

@ -51,7 +51,6 @@ const CM_STYLES = [
];
const CM_SCRIPTS = [
"chrome://devtools/content/shared/theme-switching.js",
"chrome://devtools/content/sourceeditor/codemirror/lib/codemirror.js",
"chrome://devtools/content/sourceeditor/codemirror/addon/dialog/dialog.js",
"chrome://devtools/content/sourceeditor/codemirror/addon/search/searchcursor.js",
@ -252,6 +251,21 @@ Editor.prototype = {
config: null,
Doc: null,
/**
* Exposes the CodeMirror instance. We want to get away from trying to
* abstract away the API entirely, and this makes it easier to integrate in
* various environments and do complex things.
*/
get codeMirror() {
if (!editors.has(this)) {
throw new Error(
"CodeMirror instance does not exist. You must wait " +
"for it to be appended to the DOM."
);
}
return editors.get(this);
},
/**
* Appends the current Editor instance to the element specified by
* 'el'. You can also provide your won iframe to host the editor as
@ -275,199 +289,19 @@ Editor.prototype = {
}
let onLoad = () => {
// Once the iframe is loaded, we can inject CodeMirror
// and its dependencies into its DOM.
env.removeEventListener("load", onLoad, true);
let win = env.contentWindow.wrappedJSObject;
if (!this.config.themeSwitching) {
win.document.documentElement.setAttribute("force-theme", "light");
}
let scriptsToInject = CM_SCRIPTS.concat(this.config.externalScripts);
scriptsToInject.forEach(url => {
if (url.startsWith("chrome://")) {
Services.scriptloader.loadSubScript(url, win, "utf8");
}
});
// Replace the propertyKeywords, colorKeywords and valueKeywords
// properties of the CSS MIME type with the values provided by the CSS properties
// database.
const {
propertyKeywords,
colorKeywords,
valueKeywords
} = getCSSKeywords(this.config.cssProperties);
let cssSpec = win.CodeMirror.resolveMode("text/css");
cssSpec.propertyKeywords = propertyKeywords;
cssSpec.colorKeywords = colorKeywords;
cssSpec.valueKeywords = valueKeywords;
win.CodeMirror.defineMIME("text/css", cssSpec);
let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
scssSpec.propertyKeywords = propertyKeywords;
scssSpec.colorKeywords = colorKeywords;
scssSpec.valueKeywords = valueKeywords;
win.CodeMirror.defineMIME("text/x-scss", scssSpec);
win.CodeMirror.commands.save = () => this.emit("saveRequested");
// Create a CodeMirror instance add support for context menus,
// overwrite the default controller (otherwise items in the top and
// context menus won't work).
cm = win.CodeMirror(win.document.body, this.config);
this.Doc = win.CodeMirror.Doc;
// Disable APZ for source editors. It currently causes the line numbers to
// "tear off" and swim around on top of the content. Bug 1160601 tracks
// finding a solution that allows APZ to work with CodeMirror.
cm.getScrollerElement().addEventListener("wheel", ev => {
// By handling the wheel events ourselves, we force the platform to
// scroll synchronously, like it did before APZ. However, we lose smooth
// scrolling for users with mouse wheels. This seems acceptible vs.
// doing nothing and letting the gutter slide around.
ev.preventDefault();
let { deltaX, deltaY } = ev;
if (ev.deltaMode == ev.DOM_DELTA_LINE) {
deltaX *= cm.defaultCharWidth();
deltaY *= cm.defaultTextHeight();
} else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
deltaX *= cm.getWrapperElement().clientWidth;
deltaY *= cm.getWrapperElement().clientHeight;
}
cm.getScrollerElement().scrollBy(deltaX, deltaY);
});
cm.getWrapperElement().addEventListener("contextmenu", ev => {
ev.preventDefault();
if (!this.config.contextMenu) {
return;
}
let popup = this.config.contextMenu;
if (typeof popup == "string") {
popup = el.ownerDocument.getElementById(this.config.contextMenu);
}
this.emit("popupOpen", ev, popup);
popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
}, false);
// Intercept the find and find again keystroke on CodeMirror, to avoid
// the browser's search
let findKey = L10N.getStr("find.commandkey");
let findAgainKey = L10N.getStr("findAgain.commandkey");
let [accel, modifier] = OS === "Darwin"
? ["metaKey", "altKey"]
: ["ctrlKey", "shiftKey"];
cm.getWrapperElement().addEventListener("keydown", ev => {
let key = ev.key.toUpperCase();
let node = ev.originalTarget;
let isInput = node.tagName === "INPUT";
let isSearchInput = isInput && node.type === "search";
// replace box is a different input instance than search, and it is
// located in a code mirror dialog
let isDialogInput = isInput &&
node.parentNode &&
node.parentNode.classList.contains("CodeMirror-dialog");
if (!ev[accel] || !(isSearchInput || isDialogInput)) {
return;
}
if (key === findKey) {
ev.preventDefault();
if (isSearchInput || ev[modifier]) {
node.select();
}
} else if (key === findAgainKey) {
ev.preventDefault();
if (!isSearchInput) {
return;
}
let query = node.value;
// If there isn't a search state, or the text in the input does not
// match with the current search state, we need to create a new one
if (!cm.state.search || cm.state.search.query !== query) {
cm.state.search = {
posFrom: null,
posTo: null,
overlay: null,
query
};
}
if (ev.shiftKey) {
cm.execCommand("findPrev");
} else {
cm.execCommand("findNext");
}
}
});
cm.on("focus", () => this.emit("focus"));
cm.on("scroll", () => this.emit("scroll"));
cm.on("change", () => {
this.emit("change");
if (!this._lastDirty) {
this._lastDirty = true;
this.emit("dirty-change");
}
});
cm.on("cursorActivity", () => this.emit("cursorActivity"));
cm.on("gutterClick", (cmArg, line, gutter, ev) => {
let head = { line: line, ch: 0 };
let tail = { line: line, ch: this.getText(line).length };
// Shift-click on a gutter selects the whole line.
if (ev.shiftKey) {
cmArg.setSelection(head, tail);
return;
}
this.emit("gutterClick", line, ev.button);
});
win.CodeMirror.defineExtension("l10n", (name) => {
return L10N.getStr(name);
});
cm.getInputField().controllers.insertControllerAt(0, controller(this));
Services.scriptloader.loadSubScript(
"chrome://devtools/content/shared/theme-switching.js",
win, "utf8"
);
this.container = env;
editors.set(this, cm);
this.reloadPreferences = this.reloadPreferences.bind(this);
this._prefObserver = new PrefObserver("devtools.editor.");
this._prefObserver.on(TAB_SIZE, this.reloadPreferences);
this._prefObserver.on(EXPAND_TAB, this.reloadPreferences);
this._prefObserver.on(KEYMAP, this.reloadPreferences);
this._prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
this._prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
this._prefObserver.on(DETECT_INDENT, this.reloadPreferences);
this._prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
this.reloadPreferences();
win.editor = this;
let editorReadyEvent = new win.CustomEvent("editorReady");
win.dispatchEvent(editorReadyEvent);
this._setup(win.document.body);
env.removeEventListener("load", onLoad, true);
def.resolve();
};
@ -480,6 +314,202 @@ Editor.prototype = {
return def.promise;
},
appendToLocalElement: function (el) {
this._setup(el);
},
/**
* Do the actual appending and configuring of the CodeMirror instance. This is
* used by both append functions above, and does all the hard work to
* configure CodeMirror with all the right options/modes/etc.
*/
_setup: function (el) {
let win = el.ownerDocument.defaultView;
let scriptsToInject = CM_SCRIPTS.concat(this.config.externalScripts);
scriptsToInject.forEach(url => {
if (url.startsWith("chrome://")) {
Services.scriptloader.loadSubScript(url, win, "utf8");
}
});
// Replace the propertyKeywords, colorKeywords and valueKeywords
// properties of the CSS MIME type with the values provided by the CSS properties
// database.
const {
propertyKeywords,
colorKeywords,
valueKeywords
} = getCSSKeywords(this.config.cssProperties);
let cssSpec = win.CodeMirror.resolveMode("text/css");
cssSpec.propertyKeywords = propertyKeywords;
cssSpec.colorKeywords = colorKeywords;
cssSpec.valueKeywords = valueKeywords;
win.CodeMirror.defineMIME("text/css", cssSpec);
let scssSpec = win.CodeMirror.resolveMode("text/x-scss");
scssSpec.propertyKeywords = propertyKeywords;
scssSpec.colorKeywords = colorKeywords;
scssSpec.valueKeywords = valueKeywords;
win.CodeMirror.defineMIME("text/x-scss", scssSpec);
win.CodeMirror.commands.save = () => this.emit("saveRequested");
// Create a CodeMirror instance add support for context menus,
// overwrite the default controller (otherwise items in the top and
// context menus won't work).
let cm = win.CodeMirror(el, this.config);
this.Doc = win.CodeMirror.Doc;
// Disable APZ for source editors. It currently causes the line numbers to
// "tear off" and swim around on top of the content. Bug 1160601 tracks
// finding a solution that allows APZ to work with CodeMirror.
cm.getScrollerElement().addEventListener("wheel", ev => {
// By handling the wheel events ourselves, we force the platform to
// scroll synchronously, like it did before APZ. However, we lose smooth
// scrolling for users with mouse wheels. This seems acceptible vs.
// doing nothing and letting the gutter slide around.
ev.preventDefault();
let { deltaX, deltaY } = ev;
if (ev.deltaMode == ev.DOM_DELTA_LINE) {
deltaX *= cm.defaultCharWidth();
deltaY *= cm.defaultTextHeight();
} else if (ev.deltaMode == ev.DOM_DELTA_PAGE) {
deltaX *= cm.getWrapperElement().clientWidth;
deltaY *= cm.getWrapperElement().clientHeight;
}
cm.getScrollerElement().scrollBy(deltaX, deltaY);
});
cm.getWrapperElement().addEventListener("contextmenu", ev => {
ev.preventDefault();
if (!this.config.contextMenu) {
return;
}
let popup = this.config.contextMenu;
if (typeof popup == "string") {
popup = el.ownerDocument.getElementById(this.config.contextMenu);
}
this.emit("popupOpen", ev, popup);
popup.openPopupAtScreen(ev.screenX, ev.screenY, true);
}, false);
// Intercept the find and find again keystroke on CodeMirror, to avoid
// the browser's search
let findKey = L10N.getStr("find.commandkey");
let findAgainKey = L10N.getStr("findAgain.commandkey");
let [accel, modifier] = OS === "Darwin"
? ["metaKey", "altKey"]
: ["ctrlKey", "shiftKey"];
cm.getWrapperElement().addEventListener("keydown", ev => {
let key = ev.key.toUpperCase();
let node = ev.originalTarget;
let isInput = node.tagName === "INPUT";
let isSearchInput = isInput && node.type === "search";
// replace box is a different input instance than search, and it is
// located in a code mirror dialog
let isDialogInput = isInput &&
node.parentNode &&
node.parentNode.classList.contains("CodeMirror-dialog");
if (!ev[accel] || !(isSearchInput || isDialogInput)) {
return;
}
if (key === findKey) {
ev.preventDefault();
if (isSearchInput || ev[modifier]) {
node.select();
}
} else if (key === findAgainKey) {
ev.preventDefault();
if (!isSearchInput) {
return;
}
let query = node.value;
// If there isn't a search state, or the text in the input does not
// match with the current search state, we need to create a new one
if (!cm.state.search || cm.state.search.query !== query) {
cm.state.search = {
posFrom: null,
posTo: null,
overlay: null,
query
};
}
if (ev.shiftKey) {
cm.execCommand("findPrev");
} else {
cm.execCommand("findNext");
}
}
});
cm.on("focus", () => this.emit("focus"));
cm.on("scroll", () => this.emit("scroll"));
cm.on("change", () => {
this.emit("change");
if (!this._lastDirty) {
this._lastDirty = true;
this.emit("dirty-change");
}
});
cm.on("cursorActivity", () => this.emit("cursorActivity"));
cm.on("gutterClick", (cmArg, line, gutter, ev) => {
let head = { line: line, ch: 0 };
let tail = { line: line, ch: this.getText(line).length };
// Shift-click on a gutter selects the whole line.
if (ev.shiftKey) {
cmArg.setSelection(head, tail);
return;
}
this.emit("gutterClick", line, ev.button);
});
win.CodeMirror.defineExtension("l10n", (name) => {
return L10N.getStr(name);
});
cm.getInputField().controllers.insertControllerAt(0, controller(this));
editors.set(this, cm);
this.reloadPreferences = this.reloadPreferences.bind(this);
this._prefObserver = new PrefObserver("devtools.editor.");
this._prefObserver.on(TAB_SIZE, this.reloadPreferences);
this._prefObserver.on(EXPAND_TAB, this.reloadPreferences);
this._prefObserver.on(KEYMAP, this.reloadPreferences);
this._prefObserver.on(AUTO_CLOSE, this.reloadPreferences);
this._prefObserver.on(AUTOCOMPLETE, this.reloadPreferences);
this._prefObserver.on(DETECT_INDENT, this.reloadPreferences);
this._prefObserver.on(ENABLE_CODE_FOLDING, this.reloadPreferences);
this.reloadPreferences();
win.editor = this;
let editorReadyEvent = new win.CustomEvent("editorReady");
win.dispatchEvent(editorReadyEvent);
},
/**
* Returns a boolean indicating whether the editor is ready to
* use. Use appendTo(el).then(() => {}) for most cases

View File

@ -460,10 +460,9 @@ var StyleSheetActor = protocol.ActorClassWithSpec(styleSheetSpec, {
// require system principal to load. At meanwhile, we strip the loadGroup
// for preventing the assertion of the userContextId mismatching.
// The default internal stylesheets load from the 'resource:' URL.
// Bug 1287607 - The 'chrome:' URL will be also loaded from here, so we do
// the same thing for such URLs as well.
if (!/^resource:\/\//.test(this.href) &&
!/^chrome:\/\//.test(this.href)) {
// Bug 1287607, 1291321 - 'chrome' and 'file' protocols should also be handled in the
// same way.
if (!/^(chrome|file|resource):\/\//.test(this.href)) {
options.window = this.window;
options.principal = this.document.nodePrincipal;
}

View File

@ -204,6 +204,197 @@ SuspendBackgroundVideoDelay()
MediaPrefs::MDSMSuspendBackgroundVideoDelay());
}
class MediaDecoderStateMachine::StateObject
{
public:
virtual ~StateObject() {}
virtual void Enter() {}; // Entry action.
virtual void Exit() {}; // Exit action.
virtual void Step() {} // Perform a 'cycle' of this state object.
virtual State GetState() const = 0;
protected:
using Master = MediaDecoderStateMachine;
explicit StateObject(Master* aPtr) : mMaster(aPtr) {}
// Take a raw pointer in order not to change the life cycle of MDSM.
// It is guaranteed to be valid by MDSM.
Master* mMaster;
};
class MediaDecoderStateMachine::DecodeMetadataState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit DecodeMetadataState(Master* aPtr) : StateObject(aPtr) {}
void Enter() override
{
mMaster->ReadMetadata();
}
State GetState() const override
{
return DECODER_STATE_DECODING_METADATA;
}
};
class MediaDecoderStateMachine::WaitForCDMState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit WaitForCDMState(Master* aPtr) : StateObject(aPtr) {}
State GetState() const override
{
return DECODER_STATE_WAIT_FOR_CDM;
}
};
class MediaDecoderStateMachine::DormantState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit DormantState(Master* aPtr) : StateObject(aPtr) {}
void Enter() override
{
mMaster->DiscardSeekTaskIfExist();
if (mMaster->IsPlaying()) {
mMaster->StopPlayback();
}
mMaster->Reset();
mMaster->mReader->ReleaseResources();
}
State GetState() const override
{
return DECODER_STATE_DORMANT;
}
};
class MediaDecoderStateMachine::DecodingFirstFrameState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit DecodingFirstFrameState(Master* aPtr) : StateObject(aPtr) {}
void Enter() override
{
mMaster->DecodeFirstFrame();
}
State GetState() const override
{
return DECODER_STATE_DECODING_FIRSTFRAME;
}
};
class MediaDecoderStateMachine::DecodingState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit DecodingState(Master* aPtr) : StateObject(aPtr) {}
void Enter() override
{
mMaster->StartDecoding();
}
void Step() override
{
mMaster->StepDecoding();
}
State GetState() const override
{
return DECODER_STATE_DECODING;
}
};
class MediaDecoderStateMachine::SeekingState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit SeekingState(Master* aPtr) : StateObject(aPtr) {}
State GetState() const override
{
return DECODER_STATE_SEEKING;
}
};
class MediaDecoderStateMachine::BufferingState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit BufferingState(Master* aPtr) : StateObject(aPtr) {}
void Enter() override
{
mMaster->StartBuffering();
}
void Step() override
{
mMaster->StepBuffering();
}
State GetState() const override
{
return DECODER_STATE_BUFFERING;
}
};
class MediaDecoderStateMachine::CompletedState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit CompletedState(Master* aPtr) : StateObject(aPtr) {}
void Enter() override
{
mMaster->ScheduleStateMachine();
}
void Exit() override
{
mMaster->mSentPlaybackEndedEvent = false;
}
void Step() override
{
mMaster->StepCompleted();
}
State GetState() const override
{
return DECODER_STATE_COMPLETED;
}
};
class MediaDecoderStateMachine::ShutdownState
: public MediaDecoderStateMachine::StateObject
{
public:
explicit ShutdownState(Master* aPtr) : StateObject(aPtr) {}
void Enter() override
{
mMaster->mIsShutdown = true;
}
void Exit() override
{
MOZ_DIAGNOSTIC_ASSERT(false, "Shouldn't escape the SHUTDOWN state.");
}
State GetState() const override
{
return DECODER_STATE_SHUTDOWN;
}
};
#define INIT_WATCHABLE(name, val) \
name(val, "MediaDecoderStateMachine::" #name)
#define INIT_MIRROR(name, val) \
@ -223,6 +414,7 @@ MediaDecoderStateMachine::MediaDecoderStateMachine(MediaDecoder* aDecoder,
mDispatchedStateMachine(false),
mDelayedScheduler(mTaskQueue),
INIT_WATCHABLE(mState, DECODER_STATE_DECODING_METADATA),
mStateObj(new DecodeMetadataState(this)),
mCurrentFrameID(0),
INIT_WATCHABLE(mObservedDuration, TimeUnit()),
mFragmentEndTime(-1),
@ -872,8 +1064,10 @@ nsresult MediaDecoderStateMachine::Init(MediaDecoder* aDecoder)
nsresult rv = mReader->Init();
NS_ENSURE_SUCCESS(rv, rv);
OwnerThread()->Dispatch(
NewRunnableMethod(this, &MediaDecoderStateMachine::EnterState));
RefPtr<MediaDecoderStateMachine> self = this;
OwnerThread()->Dispatch(NS_NewRunnableFunction([self] () {
self->mStateObj->Enter();
}));
return NS_OK;
}
@ -1063,61 +1257,45 @@ MediaDecoderStateMachine::SetState(State aState)
DECODER_LOG("MDSM state: %s -> %s", ToStateStr(), ToStateStr(aState));
ExitState();
MOZ_ASSERT(mState == mStateObj->GetState());
mStateObj->Exit();
mState = aState;
EnterState();
}
void
MediaDecoderStateMachine::ExitState()
{
MOZ_ASSERT(OnTaskQueue());
switch (mState) {
case DECODER_STATE_COMPLETED:
mSentPlaybackEndedEvent = false;
break;
case DECODER_STATE_SHUTDOWN:
MOZ_DIAGNOSTIC_ASSERT(false, "Shouldn't escape the SHUTDOWN state.");
break;
default:
break;
}
}
void
MediaDecoderStateMachine::EnterState()
{
MOZ_ASSERT(OnTaskQueue());
switch (mState) {
case DECODER_STATE_DECODING_METADATA:
ReadMetadata();
mStateObj = MakeUnique<DecodeMetadataState>(this);
break;
case DECODER_STATE_WAIT_FOR_CDM:
mStateObj = MakeUnique<WaitForCDMState>(this);
break;
case DECODER_STATE_DORMANT:
DiscardSeekTaskIfExist();
if (IsPlaying()) {
StopPlayback();
}
Reset();
mReader->ReleaseResources();
mStateObj = MakeUnique<DormantState>(this);
break;
case DECODER_STATE_DECODING_FIRSTFRAME:
DecodeFirstFrame();
mStateObj = MakeUnique<DecodingFirstFrameState>(this);
break;
case DECODER_STATE_DECODING:
StartDecoding();
mStateObj = MakeUnique<DecodingState>(this);
break;
case DECODER_STATE_SEEKING:
mStateObj = MakeUnique<SeekingState>(this);
break;
case DECODER_STATE_BUFFERING:
StartBuffering();
mStateObj = MakeUnique<BufferingState>(this);
break;
case DECODER_STATE_COMPLETED:
ScheduleStateMachine();
mStateObj = MakeUnique<CompletedState>(this);
break;
case DECODER_STATE_SHUTDOWN:
mIsShutdown = true;
mStateObj = MakeUnique<ShutdownState>(this);
break;
default:
MOZ_ASSERT_UNREACHABLE("Invalid state.");
break;
}
MOZ_ASSERT(mState == mStateObj->GetState());
mStateObj->Enter();
}
void MediaDecoderStateMachine::VolumeChanged()
@ -2262,20 +2440,7 @@ MediaDecoderStateMachine::RunStateMachine()
mDelayedScheduler.Reset(); // Must happen on state machine task queue.
mDispatchedStateMachine = false;
switch (mState) {
case DECODER_STATE_DECODING:
StepDecoding();
return;
case DECODER_STATE_BUFFERING:
StepBuffering();
return;
case DECODER_STATE_COMPLETED:
StepCompleted();
return;
default:
return;
}
mStateObj->Step();
}
void

View File

@ -251,6 +251,17 @@ public:
size_t SizeOfAudioQueue() const;
private:
class StateObject;
class DecodeMetadataState;
class WaitForCDMState;
class DormantState;
class DecodingFirstFrameState;
class DecodingState;
class SeekingState;
class BufferingState;
class CompletedState;
class ShutdownState;
static const char* ToStateStr(State aState);
const char* ToStateStr();
@ -346,8 +357,6 @@ protected:
virtual ~MediaDecoderStateMachine();
void SetState(State aState);
void ExitState();
void EnterState();
void BufferedRangeUpdated();
@ -603,6 +612,8 @@ private:
// Accessed on state machine, audio, main, and AV thread.
Watchable<State> mState;
UniquePtr<StateObject> mStateObj;
// Time that buffering started. Used for buffering timeout and only
// accessed on the state machine thread. This is null while we're not
// buffering.

View File

@ -102,8 +102,8 @@ TrackBuffersManager::TrackBuffersManager(MediaSourceDecoder* aParentDecoder,
, mVideoEvictionThreshold(Preferences::GetUint("media.mediasource.eviction_threshold.video",
100 * 1024 * 1024))
, mAudioEvictionThreshold(Preferences::GetUint("media.mediasource.eviction_threshold.audio",
30 * 1024 * 1024))
, mEvictionOccurred(false)
10 * 1024 * 1024))
, mEvictionState(EvictionState::NO_EVICTION_NEEDED)
, mMonitor("TrackBuffersManager")
{
MOZ_ASSERT(NS_IsMainThread(), "Must be instanciated on the main thread");
@ -278,19 +278,23 @@ TrackBuffersManager::EvictData(const TimeUnit& aPlaybackTime, int64_t aSize)
GetSize() / 1024, EvictionThreshold() / 1024, toEvict / 1024);
if (toEvict <= 0) {
mEvictionState = EvictionState::NO_EVICTION_NEEDED;
return EvictDataResult::NO_DATA_EVICTED;
}
if (toEvict <= 512*1024) {
// Don't bother evicting less than 512KB.
mEvictionState = EvictionState::NO_EVICTION_NEEDED;
return EvictDataResult::CANT_EVICT;
}
if (mBufferFull && mEvictionOccurred) {
if (mBufferFull && mEvictionState == EvictionState::EVICTION_COMPLETED) {
return EvictDataResult::BUFFER_FULL;
}
MSE_DEBUG("Reaching our size limit, schedule eviction of %lld bytes", toEvict);
mEvictionState = EvictionState::EVICTION_NEEDED;
QueueTask(new EvictDataTask(aPlaybackTime, toEvict));
return EvictDataResult::NO_DATA_EVICTED;
@ -411,6 +415,8 @@ TrackBuffersManager::DoEvictData(const TimeUnit& aPlaybackTime,
{
MOZ_ASSERT(OnTaskQueue());
mEvictionState = EvictionState::EVICTION_COMPLETED;
// Video is what takes the most space, only evict there if we have video.
const auto& track = HasVideo() ? mVideoTracks : mAudioTracks;
const auto& buffer = track.mBuffers.LastElement();
@ -452,11 +458,21 @@ TrackBuffersManager::DoEvictData(const TimeUnit& aPlaybackTime,
toEvict = mSizeSourceBuffer - finalSize;
// Still some to remove. Remove data starting from the end, up to 30s ahead
// of the later of the playback time or the next sample to be demuxed.
// 30s is a value chosen as it appears to work with YouTube.
TimeUnit upperLimit =
std::max(aPlaybackTime, track.mNextSampleTime) + TimeUnit::FromSeconds(30);
// See if we can evict data into the future.
// We do not evict data from the currently used buffered interval.
TimeUnit currentPosition = std::max(aPlaybackTime, track.mNextSampleTime);
TimeIntervals futureBuffered(TimeInterval(currentPosition, TimeUnit::FromInfinity()));
futureBuffered.Intersection(track.mBufferedRanges);
futureBuffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
if (futureBuffered.Length() <= 1) {
// We have one continuous segment ahead of us:
// nothing further can be evicted.
return;
}
// Don't evict before the end of the current segment
TimeUnit upperLimit = futureBuffered[0].mEnd;
uint32_t evictedFramesStartIndex = buffer.Length();
for (int32_t i = buffer.Length() - 1; i >= 0; i--) {
const auto& frame = buffer[i];
@ -563,7 +579,6 @@ TrackBuffersManager::CodedFrameRemoval(TimeInterval aInterval)
if (mBufferFull && mSizeSourceBuffer < EvictionThreshold()) {
mBufferFull = false;
}
mEvictionOccurred = true;
return dataRemoved;
}
@ -1269,7 +1284,6 @@ TrackBuffersManager::CompleteCodedFrameProcessing()
// 4. If this SourceBuffer is full and cannot accept more media data, then set the buffer full flag to true.
if (mSizeSourceBuffer >= EvictionThreshold()) {
mBufferFull = true;
mEvictionOccurred = false;
}
// 5. If the input buffer does not contain a complete media segment, then jump to the need more data step below.

View File

@ -435,7 +435,13 @@ private:
Atomic<int64_t> mSizeSourceBuffer;
const int64_t mVideoEvictionThreshold;
const int64_t mAudioEvictionThreshold;
Atomic<bool> mEvictionOccurred;
enum class EvictionState
{
NO_EVICTION_NEEDED,
EVICTION_NEEDED,
EVICTION_COMPLETED,
};
Atomic<EvictionState> mEvictionState;
// Monitor to protect following objects accessed across multipple threads.
mutable Monitor mMonitor;

View File

@ -37,12 +37,13 @@ public:
virtual HRESULT Output(int64_t aStreamOffset,
RefPtr<MediaData>& aOutput) = 0;
void Flush() {
virtual void Flush()
{
mDecoder->Flush();
mSeekTargetThreshold.reset();
}
void Drain()
virtual void Drain()
{
if (FAILED(mDecoder->SendMFTMessage(MFT_MESSAGE_COMMAND_DRAIN, 0))) {
NS_WARNING("Failed to send DRAIN command to MFT");

View File

@ -525,6 +525,8 @@ WMFVideoMFTManager::Input(MediaRawData* aSample)
NS_ENSURE_TRUE(SUCCEEDED(hr) && mLastInput != nullptr, hr);
mLastDuration = aSample->mDuration;
mLastTime = aSample->mTime;
mSamplesCount++;
// Forward sample data to the decoder.
return mDecoder->Input(mLastInput);
@ -832,6 +834,15 @@ WMFVideoMFTManager::Output(int64_t aStreamOffset,
HRESULT hr;
aOutData = nullptr;
int typeChangeCount = 0;
bool wasDraining = mDraining;
int64_t sampleCount = mSamplesCount;
if (wasDraining) {
mSamplesCount = 0;
mDraining = false;
}
media::TimeUnit pts;
media::TimeUnit duration;
// Loop until we decode a sample, or an unexpected error that we can't
// handle occurs.
@ -873,12 +884,21 @@ WMFVideoMFTManager::Output(int64_t aStreamOffset,
}
continue;
}
pts = GetSampleTime(sample);
duration = GetSampleDuration(sample);
if (!pts.IsValid() || !duration.IsValid()) {
return E_FAIL;
}
if (wasDraining && sampleCount == 1 && pts == media::TimeUnit()) {
// WMF is unable to calculate a duration if only a single sample
// was parsed. Additionally, the pts always comes out at 0 under those
// circumstances.
// Seeing that we've only fed the decoder a single frame, the pts
// and duration are known, it's of the last sample.
pts = media::TimeUnit::FromMicroseconds(mLastTime);
duration = media::TimeUnit::FromMicroseconds(mLastDuration);
}
if (mSeekTargetThreshold.isSome()) {
media::TimeUnit pts = GetSampleTime(sample);
media::TimeUnit duration = GetSampleDuration(sample);
if (!pts.IsValid() || !duration.IsValid()) {
return E_FAIL;
}
if ((pts + duration) < mSeekTargetThreshold.ref()) {
LOG("Dropping video frame which pts is smaller than seek target.");
// It is necessary to clear the pointer to release the previous output
@ -907,6 +927,9 @@ WMFVideoMFTManager::Output(int64_t aStreamOffset,
NS_ENSURE_TRUE(frame, E_FAIL);
aOutData = frame;
// Set the potentially corrected pts and duration.
aOutData->mTime = pts.ToMicroseconds();
aOutData->mDuration = duration.ToMicroseconds();
if (mNullOutputCount) {
mGotValidOutputAfterNullOutput = true;

View File

@ -49,6 +49,19 @@ public:
? "wmf hardware video decoder" : "wmf software video decoder";
}
void Flush() override
{
MFTManager::Flush();
mDraining = false;
mSamplesCount = 0;
}
void Drain() override
{
MFTManager::Drain();
mDraining = true;
}
private:
bool ValidateVideoInfo();
@ -81,6 +94,9 @@ private:
RefPtr<IMFSample> mLastInput;
float mLastDuration;
int64_t mLastTime = 0;
bool mDraining = false;
int64_t mSamplesCount = 0;
bool mDXVAEnabled;
const layers::LayersBackend mLayersBackend;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -251,6 +251,8 @@ void
PathBuilderD2D::Arc(const Point &aOrigin, Float aRadius, Float aStartAngle,
Float aEndAngle, bool aAntiClockwise)
{
MOZ_ASSERT(aRadius >= 0);
if (aAntiClockwise && aStartAngle < aEndAngle) {
// D2D does things a little differently, and draws the arc by specifying an
// beginning and an end point. This means the circle will be the wrong way

View File

@ -2407,7 +2407,7 @@ nsCSSBorderRenderer::DrawDottedCornerSlow(mozilla::css::Side aSide,
DottedCornerFinder::Result result = finder.Next();
if (marginedDirtyRect.Contains(result.C)) {
if (marginedDirtyRect.Contains(result.C) && result.r > 0) {
entered = true;
builder->MoveTo(Point(result.C.x + result.r, result.C.y));
builder->Arc(result.C, result.r, 0, Float(2.0 * M_PI));

View File

@ -0,0 +1,14 @@
<!doctype html>
<html>
<meta charset="utf-8" />
<style>
#box {
border-radius: 335px;
width: 600px;
height: 401px;
border-style: dotted;
border-width: 41px 1px;
}
</style>
<div id="box"></div>
</html>

View File

@ -635,3 +635,4 @@ load large-border-radius-dashed.html
load large-border-radius-dashed2.html
load large-border-radius-dotted.html
load large-border-radius-dotted2.html
load 1297427-non-equal-centers.html

View File

@ -6,11 +6,13 @@ package org.mozilla.gecko.icons.decoders;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.VisibleForTesting;
import android.util.SparseArray;
import java.util.Iterator;
import java.util.NoSuchElementException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.R;
@ -90,6 +92,7 @@ public class ICODecoder implements Iterable<Bitmap> {
private boolean hasDecoded;
private int largestFaviconSize;
@RobocopTarget
public ICODecoder(Context context, byte[] decodand, int offset, int len) {
this.decodand = decodand;
this.offset = offset;
@ -351,6 +354,18 @@ public class ICODecoder implements Iterable<Bitmap> {
return result;
}
@VisibleForTesting
@RobocopTarget
public IconDirectoryEntry[] getIconDirectory() {
return iconDirectory;
}
@VisibleForTesting
@RobocopTarget
public int getLargestFaviconSize() {
return largestFaviconSize;
}
/**
* Inner class to iterate over the elements in the ICO represented by the enclosing instance.
*/

View File

@ -4,6 +4,10 @@
package org.mozilla.gecko.icons.decoders;
import android.support.annotation.VisibleForTesting;
import org.mozilla.gecko.annotation.RobocopTarget;
/**
* Representation of an ICO file ICONDIRENTRY structure.
*/
@ -23,6 +27,7 @@ public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
int index;
boolean isErroneous;
@RobocopTarget
public IconDirectoryEntry(int width, int height, int paletteSize, int bitsPerPixel, int payloadSize, int payloadOffset, boolean payloadIsPNG) {
this.width = width;
this.height = height;
@ -185,6 +190,12 @@ public class IconDirectoryEntry implements Comparable<IconDirectoryEntry> {
IconDirectoryEntry.maxBPP = maxBPP;
}
@VisibleForTesting
@RobocopTarget
public int getWidth() {
return width;
}
@Override
public String toString() {
return "IconDirectoryEntry{" +

View File

@ -41,7 +41,7 @@ public class ReaderViewBookmarkPromotion extends BrowserAppDelegateWithReference
case LOCATION_CHANGE:
// old url: data
// new url: tab.getURL()
final boolean enteringReaderMode = ReaderModeUtils.isEnteringReaderMode(tab.getURL(), data);
final boolean enteringReaderMode = ReaderModeUtils.isEnteringReaderMode(data, tab.getURL());
if (!hasEnteredReaderMode && enteringReaderMode) {
hasEnteredReaderMode = true;

View File

@ -24,21 +24,21 @@ public class ReaderModeUtils {
return StringUtils.getQueryParameter(aboutReaderUrl, "url");
}
public static boolean isEnteringReaderMode(String currentUrl, String newUrl) {
if (currentUrl == null || newUrl == null) {
public static boolean isEnteringReaderMode(String oldURL, String newURL) {
if (oldURL == null || newURL == null) {
return false;
}
if (!AboutPages.isAboutReader(newUrl)) {
if (!AboutPages.isAboutReader(newURL)) {
return false;
}
String urlFromAboutReader = getUrlFromAboutReader(newUrl);
String urlFromAboutReader = getUrlFromAboutReader(newURL);
if (urlFromAboutReader == null) {
return false;
}
return urlFromAboutReader.equals(currentUrl);
return urlFromAboutReader.equals(oldURL);
}
public static String getAboutReaderForUrl(String url) {

View File

@ -34,7 +34,7 @@ public class NativeZip implements NativeReference {
}
@Override
public void finalize() {
protected void finalize() {
release();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -114,3 +114,4 @@ skip-if = android_version == "18"
skip-if = android_version == "18"
[src/org/mozilla/gecko/tests/testLoginsProvider.java]
[src/org/mozilla/gecko/tests/testICODecoder.java]

View File

@ -0,0 +1,238 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
import android.graphics.Bitmap;
import org.mozilla.gecko.icons.decoders.ICODecoder;
import org.mozilla.gecko.icons.decoders.IconDirectoryEntry;
import org.mozilla.gecko.icons.decoders.LoadFaviconResult;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNull;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
public class testICODecoder extends UITest {
private int mGolemNumIconDirEntries;
public void testICODecoder() throws IOException {
testMicrosoftFavicon();
testNvidiaFavicon();
testGolemFavicon();
testMissingHeader();
testCorruptIconDirectory();
}
/**
* Decode and verify a Microsoft favicon with six different sizes:
* 128x128, 72x72, 48x48, 32x32, 24x24, 16x16
* Each of the six BMPs supposedly has zero colour depth.
*/
private void testMicrosoftFavicon() throws IOException {
byte[] icoBytes = readICO("microsoft_favicon.ico");
fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length);
ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
icoBytes.length);
LoadFaviconResult result = decoder.decode();
fAssertNotNull("Expecting Microsoft favicon to not fail decoding.", result);
int largestBitmap = Integer.MAX_VALUE;
int[] possibleSizes = {16, 24, 32, 48, 72, 128};
for (int i = 0; i < possibleSizes.length; i++) {
if (possibleSizes[i] > decoder.getLargestFaviconSize()) {
largestBitmap = possibleSizes[i];
// Verify that all bitmaps but the smallest larger than Favicons.largestFaviconSize
// have been discarded.
for (int j = i + 1; j < possibleSizes.length; j++) {
Bitmap selectedBitmap = result.getBestBitmap(possibleSizes[j]);
fAssertNotNull("Expecting a best bitmap to be found for " +
possibleSizes[j] + "x" + possibleSizes[j], selectedBitmap);
fAssertEquals("Expecting best bitmap to have width " + possibleSizes[i],
possibleSizes[i], selectedBitmap.getWidth());
fAssertEquals("Expecting best bitmap to have height " + possibleSizes[i],
possibleSizes[i], selectedBitmap.getHeight());
// Reset the result's bitmap iterator.
result = decoder.decode();
}
break;
}
}
int[] expectedSizes = {
// If we request a 33x33 we should get a 48x48.
33, 48,
// If we request a 24x24 we should get a 24x24.
24, 24,
// If we request a 8x8 we should get a 16x16.
8, 16,
};
for (int i = 0; i < expectedSizes.length - 1; i += 2) {
if (expectedSizes[i + 1] > largestBitmap) {
// This bitmap has been discarded.
continue;
}
Bitmap selectedBitmap = result.getBestBitmap(expectedSizes[i]);
fAssertNotNull("Expecting a best bitmap to have been found for " +
expectedSizes[i] + "x" + expectedSizes[i], selectedBitmap);
fAssertEquals("Expecting best bitmap to have width " + expectedSizes[i + 1],
expectedSizes[i + 1], selectedBitmap.getWidth());
fAssertEquals("Expecting best bitmap to have height " + expectedSizes[i + 1],
expectedSizes[i + 1], selectedBitmap.getHeight());
// Reset the result's bitmap iterator.
result = decoder.decode();
}
}
/**
* Decode and verify a NVIDIA favicon with three different colour depths,
* and three different sizes for each colour depth. All payloads are BMP.
*/
private void testNvidiaFavicon() throws IOException {
byte[] icoBytes = readICO("nvidia_favicon.ico");
fAssertEquals("Expecting NVIDIA favicon to be 25214 bytes.", 25214, icoBytes.length);
ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
icoBytes.length);
fAssertNotNull("Expecting NVIDIA favicon to not fail decoding.", decoder.decode());
// Verify the best entry is correctly chosen for each width.
// We expect 32 bpp in all cases even if 32 bpp exceeds IconDirectoryEntry.maxBPP.
// This is okay because IconDirectoryEntry.maxBPP is a "desired bpp" not the absolute max.
// This was chosen because we think it gives better results to select a higher bpp and let
// Android downscale the bpp, rather than showing a bitmap of potentially significantly
// lower color depth.
IconDirectoryEntry[] expectedEntries = {
new IconDirectoryEntry(16, 16, 0, 32, 1128, 24086, false),
new IconDirectoryEntry(32, 32, 0, 32, 4264, 19822, false),
new IconDirectoryEntry(48, 48, 0, 32, 9640, 10182, false)
};
IconDirectoryEntry[] directory = decoder.getIconDirectory();
fAssertTrue("NVIDIA icon directory must contain at least one entry.", directory.length > 0);
for (int i = 0; i < directory.length; i++) {
if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) {
// This test-case has been discarded due to being over-sized. Next.
// All subsequent cases will be too.
fAssertTrue("At least one test-case should not have been discarded.", i > 0);
break;
}
// Verify the actual Icon Directory entry was as expected.
fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i],
0, directory[i].compareTo(expectedEntries[i]));
}
}
/**
* Decode and verify a Golem.de favicon with five bitmaps: 256x256, 48x48, 32x32, 24x24, 16x16
* Only the 256x256 is a PNG payload. All others are BMP.
*/
private void testGolemFavicon() throws IOException {
byte[] icoBytes = readICO("golem_favicon.ico");
fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length);
ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes, 0,
icoBytes.length);
fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode());
// Verify the five entries were correctly identified.
IconDirectoryEntry[] expectedEntries = {
new IconDirectoryEntry(16, 16, 0, 32, 1128, 39250, false),
new IconDirectoryEntry(24, 24, 0, 32, 2488, 37032, false),
new IconDirectoryEntry(32, 32, 0, 32, 4392, 32640, false),
new IconDirectoryEntry(48, 48, 0, 32, 9832, 22808, false),
new IconDirectoryEntry(256, 256, 0, 32, 22722, 86, true)
};
IconDirectoryEntry[] directory = decoder.getIconDirectory();
fAssertTrue("Golem icon directory must contain at least one entry.", directory.length > 0);
for (int i = 0; i < directory.length; i++) {
if (expectedEntries[i].getWidth() > directory[directory.length - 1].getWidth()) {
// This test-case has been discarded due to being over-sized.
// All subsequent cases will be too.
fAssertTrue("At least one test-case should not have been discarded.", i > 0);
break;
}
// Verify the actual Icon Directory entry was as expected.
fAssertEquals(directory[i] + " is expected to be equal to " + expectedEntries[i],
0, directory[i].compareTo(expectedEntries[i]));
}
// How many icon directory entries in the non-maimed favicon?
mGolemNumIconDirEntries = directory.length;
}
/**
* Verify that deleting the header will make decoding fail.
*/
private void testMissingHeader() throws IOException {
byte[] icoBytes = readICO("microsoft_favicon.ico");
fAssertEquals("Expecting Microsoft favicon to be 17174 bytes.", 17174, icoBytes.length);
int offsetNoHeader = ICODecoder.ICO_HEADER_LENGTH_BYTES;
int lenNoHeader = icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES;
ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoBytes,
offsetNoHeader, lenNoHeader);
fAssertNull("Expecting Microsoft favicon to fail decoding.", decoder.decode());
}
/**
* Verify that decoding does not fail if the number of icon directory entries is smaller than
* the number given in the header.
*/
private void testCorruptIconDirectory() throws IOException {
byte[] icoBytes = readICO("golem_favicon.ico");
fAssertEquals("Expecting Golem favicon to be 40648 bytes.", 40648, icoBytes.length);
byte[] icoMaimed = new byte[icoBytes.length - ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES];
// Copy the header and first four icon directory entries into icoMaimed.
System.arraycopy(icoBytes, 0, icoMaimed, 0,
ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
// Skip the last icon directory entry.
System.arraycopy(icoBytes,
ICODecoder.ICO_HEADER_LENGTH_BYTES + 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES,
icoMaimed,
ICODecoder.ICO_HEADER_LENGTH_BYTES + 4 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES,
icoBytes.length - ICODecoder.ICO_HEADER_LENGTH_BYTES - 5 * ICODecoder.ICO_ICONDIRENTRY_LENGTH_BYTES);
ICODecoder decoder = new ICODecoder(getInstrumentation().getTargetContext(), icoMaimed, 0,
icoMaimed.length);
fAssertNotNull("Expecting Golem favicon to not fail decoding.", decoder.decode());
fAssertEquals("Expecting Golem favicon icon directory to contain one less bitmap.",
mGolemNumIconDirEntries - 1, decoder.getIconDirectory().length);
}
private byte[] readICO(String fileName) throws IOException {
String filePath = "ico_decoder_favicons" + File.separator + fileName;
InputStream icoStream = getInstrumentation().getContext().getAssets().open(filePath);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream(icoStream.available());
int readByte;
while ((readByte = icoStream.read()) != -1) {
byteStream.write(readByte);
}
return byteStream.toByteArray();
}
}

View File

@ -16,6 +16,9 @@ import os
import sys
import time
from manifestparser import TestManifest
from reftest import ReftestManifest
from mozbuild.util import ensureParentDir
from mozpack.files import FileFinder
from mozpack.mozjar import JarWriter
@ -315,6 +318,15 @@ ARCHIVE_FILES = {
'pattern': 'mozinfo.json',
'dest': 'reftest',
},
{
'source': buildconfig.topsrcdir,
'base': '',
'manifests': [
'layout/reftests/reftest.list',
'testing/crashtest/crashtests.list',
],
'dest': 'reftest/tests',
}
],
'talos': [
{
@ -414,15 +426,28 @@ for k, v in ARCHIVE_FILES.items():
def find_files(archive):
for entry in ARCHIVE_FILES[archive]:
source = entry['source']
dest = entry.get('dest')
base = entry.get('base', '')
pattern = entry.get('pattern')
patterns = entry.get('patterns', [])
if pattern:
patterns.append(pattern)
dest = entry.get('dest')
manifest = entry.get('manifest')
manifests = entry.get('manifests', [])
if manifest:
manifests.append(manifest)
if manifests:
dirs = find_manifest_dirs(buildconfig.topsrcdir, manifests)
patterns.extend({'{}/**'.format(d) for d in dirs})
ignore = list(entry.get('ignore', []))
ignore.append('**/.mkdir.done')
ignore.append('**/*.pyc')
ignore.extend([
'**/.flake8',
'**/.mkdir.done',
'**/*.pyc',
])
common_kwargs = {
'find_executables': False,
@ -439,14 +464,29 @@ def find_files(archive):
yield p, f
def find_reftest_dirs(topsrcdir, manifests):
from reftest import ReftestManifest
def find_manifest_dirs(topsrcdir, manifests):
"""Routine to retrieve directories specified in a manifest, relative to topsrcdir.
It does not recurse into manifests, as we currently have no need for that.
"""
dirs = set()
for p in manifests:
m = ReftestManifest()
m.load(os.path.join(topsrcdir, p))
dirs |= m.dirs
p = os.path.join(topsrcdir, p)
if p.endswith('.ini'):
test_manifest = TestManifest()
test_manifest.read(p)
dirs |= set([os.path.dirname(m) for m in test_manifest.manifests()])
elif p.endswith('.list'):
m = ReftestManifest()
m.load(p)
dirs |= m.dirs
else:
raise Exception('"{}" is not a supported manifest format.'.format(
os.path.splitext(p)[1]))
dirs = {mozpath.normpath(d[len(topsrcdir):]).lstrip('/') for d in dirs}
@ -467,27 +507,6 @@ def find_reftest_dirs(topsrcdir, manifests):
return sorted(seen)
def insert_reftest_entries(entries):
"""Reftests have their own mechanism for defining tests and locations.
This function is called when processing the reftest archive to process
reftest test manifests and insert the results into the existing list of
archive entries.
"""
manifests = (
'layout/reftests/reftest.list',
'testing/crashtest/crashtests.list',
)
for base in find_reftest_dirs(buildconfig.topsrcdir, manifests):
entries.append({
'source': buildconfig.topsrcdir,
'base': '',
'pattern': '%s/**' % base,
'dest': 'reftest/tests',
})
def main(argv):
parser = argparse.ArgumentParser(
description='Produce test archives')
@ -499,11 +518,6 @@ def main(argv):
if not args.outputfile.endswith('.zip'):
raise Exception('expected zip output file')
# Adjust reftest entries only if processing reftests (because it is
# unnecessary overhead otherwise).
if args.archive == 'reftest':
insert_reftest_entries(ARCHIVE_FILES['reftest'])
file_count = 0
t_start = time.time()
ensureParentDir(args.outputfile)
@ -515,8 +529,8 @@ def main(argv):
with JarWriter(fileobj=fh, optimize=False, compress_level=5) as writer:
res = find_files(args.archive)
for p, f in res:
writer.add(p.encode('utf-8'), f.read(), mode=f.mode, skip_duplicates=True)
file_count += 1
writer.add(p.encode('utf-8'), f.read(), mode=f.mode)
duration = time.time() - t_start
zip_size = os.path.getsize(args.outputfile)

View File

@ -570,7 +570,7 @@ class JarWriter(object):
self._data.write(end.serialize())
self._data.close()
def add(self, name, data, compress=None, mode=None):
def add(self, name, data, compress=None, mode=None, skip_duplicates=False):
'''
Add a new member to the jar archive, with the given name and the given
data.
@ -582,13 +582,15 @@ class JarWriter(object):
than the uncompressed size.
The mode option gives the unix permissions that should be stored
for the jar entry.
If a duplicated member is found skip_duplicates will prevent raising
an exception if set to True.
The given data may be a buffer, a file-like instance, a Deflater or a
JarFileReader instance. The latter two allow to avoid uncompressing
data to recompress it.
'''
name = mozpath.normsep(name)
if name in self._contents:
if name in self._contents and not skip_duplicates:
raise JarWriterError("File %s already in JarWriter" % name)
if compress is None:
compress = self._compress

View File

@ -879,15 +879,13 @@ class Marionette(object):
with self.using_context("content"):
self.execute_async_script("""
let start = new Date();
let end = new Date(start.valueOf() + 5000);
let wait = function() {
let now = new Date();
if (window.wrappedJSObject.permChanged || end >= now) {
if (window.wrappedJSObject.permChanged) {
marionetteScriptFinished();
} else {
window.setTimeout(wait, 100);
}
};
window.setTimeout(wait, 100);
}();
""", sandbox="system")
@contextmanager

View File

@ -74,8 +74,23 @@ def expected_driver_args(runner):
return expected
class ManifestFixture:
def __init__(self, name='mock_manifest',
tests=[{'path': u'test_something.py', 'expected': 'pass'}]):
self.filepath = "/path/to/fake/manifest.ini"
self.n_disabled = len([t for t in tests if 'disabled' in t])
self.n_enabled = len(tests) - self.n_disabled
mock_manifest = Mock(spec=manifestparser.TestManifest,
active_tests=Mock(return_value=tests))
self.manifest_class = Mock(return_value=mock_manifest)
self.__repr__ = lambda: "<ManifestFixture {}>".format(name)
@pytest.fixture
def manifest():
return ManifestFixture()
@pytest.fixture(params=['enabled', 'disabled', 'enabled_disabled', 'empty'])
def manifest_fixture(request):
def manifest_with_tests(request):
'''
Fixture for the contents of mock_manifest, where a manifest
can include enabled tests, disabled tests, both, or neither (empty)
@ -90,16 +105,6 @@ def manifest_fixture(request):
keys = ('path', 'expected', 'disabled')
active_tests = [dict(zip(keys, values)) for values in included]
class ManifestFixture:
def __init__(self, name, tests):
self.filepath = "/path/to/fake/manifest.ini"
self.n_disabled = len([t for t in tests if 'disabled' in t])
self.n_enabled = len(tests) - self.n_disabled
mock_manifest = Mock(spec=manifestparser.TestManifest,
active_tests=Mock(return_value=tests))
self.mock_manifest = Mock(return_value=mock_manifest)
self.__repr__ = lambda: "<ManifestFixture {}>".format(name)
return ManifestFixture(request.param, active_tests)
@ -288,31 +293,65 @@ def test_add_test_directory(runner):
@pytest.mark.parametrize("test_files_exist", [True, False])
def test_add_test_manifest(mock_runner, manifest_fixture, monkeypatch, test_files_exist):
monkeypatch.setattr('marionette.runner.base.TestManifest', manifest_fixture.mock_manifest)
def test_add_test_manifest(mock_runner, manifest_with_tests, monkeypatch, test_files_exist):
monkeypatch.setattr('marionette.runner.base.TestManifest', manifest_with_tests.manifest_class)
with patch('marionette.runner.base.os.path.exists', return_value=test_files_exist):
if test_files_exist or manifest_fixture.n_enabled == 0:
mock_runner.add_test(manifest_fixture.filepath)
assert len(mock_runner.tests) == manifest_fixture.n_enabled
assert len(mock_runner.manifest_skipped_tests) == manifest_fixture.n_disabled
if test_files_exist or manifest_with_tests.n_enabled == 0:
mock_runner.add_test(manifest_with_tests.filepath)
assert len(mock_runner.tests) == manifest_with_tests.n_enabled
assert len(mock_runner.manifest_skipped_tests) == manifest_with_tests.n_disabled
for test in mock_runner.tests:
assert test['filepath'].endswith(test['expected'] + '.py')
else:
pytest.raises(IOError, "mock_runner.add_test(manifest_fixture.filepath)")
assert manifest_fixture.mock_manifest().read.called
assert manifest_fixture.mock_manifest().active_tests.called
args, kwargs = manifest_fixture.mock_manifest().active_tests.call_args
assert kwargs['app'] == mock_runner._appName
pytest.raises(IOError, "mock_runner.add_test(manifest_with_tests.filepath)")
assert manifest_with_tests.manifest_class().read.called
assert manifest_with_tests.manifest_class().active_tests.called
def test_cleanup_with_manifest(mock_runner, manifest_fixture, monkeypatch):
monkeypatch.setattr('marionette.runner.base.TestManifest', manifest_fixture.mock_manifest)
if manifest_fixture.n_enabled > 0:
def get_kwargs_passed_to_manifest(mock_runner, manifest, monkeypatch, **kwargs):
'''Helper function for test_manifest_* tests.
Returns the kwargs passed to the call to manifest.active_tests.'''
monkeypatch.setattr('marionette.runner.base.TestManifest', manifest.manifest_class)
monkeypatch.setattr('marionette.runner.base.mozinfo.info', {'mozinfo_key': 'mozinfo_val'})
for attr in kwargs:
setattr(mock_runner, attr, kwargs[attr])
with patch('marionette.runner.base.os.path.exists', return_value=True):
mock_runner.add_test(manifest.filepath)
call_args, call_kwargs = manifest.manifest_class().active_tests.call_args
return call_kwargs
def test_manifest_basic_args(mock_runner, manifest, monkeypatch):
kwargs = get_kwargs_passed_to_manifest(mock_runner, manifest, monkeypatch)
assert kwargs['exists'] is False
assert kwargs['disabled'] is True
assert kwargs['app'] == 'fake_app'
assert 'mozinfo_key' in kwargs and kwargs['mozinfo_key'] == 'mozinfo_val'
@pytest.mark.parametrize('e10s', (True, False))
def test_manifest_with_e10s(mock_runner, manifest, monkeypatch, e10s):
kwargs = get_kwargs_passed_to_manifest(mock_runner, manifest, monkeypatch, e10s=e10s)
assert kwargs['e10s'] == e10s
@pytest.mark.parametrize('test_tags', (None, ['tag', 'tag2']))
def test_manifest_with_test_tags(mock_runner, manifest, monkeypatch, test_tags):
kwargs = get_kwargs_passed_to_manifest(mock_runner, manifest, monkeypatch, test_tags=test_tags)
if test_tags is None:
assert kwargs['filters'] == []
else:
assert len(kwargs['filters']) == 1 and kwargs['filters'][0].tags == test_tags
def test_cleanup_with_manifest(mock_runner, manifest_with_tests, monkeypatch):
monkeypatch.setattr('marionette.runner.base.TestManifest', manifest_with_tests.manifest_class)
if manifest_with_tests.n_enabled > 0:
context = patch('marionette.runner.base.os.path.exists', return_value=True)
else:
context = pytest.raises(Exception)
with context:
mock_runner.run_tests([manifest_fixture.filepath])
mock_runner.run_tests([manifest_with_tests.filepath])
assert mock_runner.marionette is None
assert mock_runner.httpd is None

View File

@ -142,7 +142,7 @@ def add_logging_group(parser, include_formatters=None):
help=help_str, default=None)
def setup_handlers(logger, formatters, formatter_options):
def setup_handlers(logger, formatters, formatter_options, allow_unused_options=False):
"""
Add handlers to the given logger according to the formatters and
options provided.
@ -153,7 +153,7 @@ def setup_handlers(logger, formatters, formatter_options):
to use when configuring formatters.
"""
unused_options = set(formatter_options.keys()) - set(formatters.keys())
if unused_options:
if unused_options and not allow_unused_options:
msg = ("Options specified for unused formatter(s) (%s) have no effect" %
list(unused_options))
raise ValueError(msg)
@ -182,7 +182,7 @@ def setup_handlers(logger, formatters, formatter_options):
logger.add_handler(handler)
def setup_logging(logger, args, defaults=None, formatter_defaults=None):
def setup_logging(logger, args, defaults=None, formatter_defaults=None, allow_unused_options=False):
"""
Configure a structuredlogger based on command line arguments.
@ -268,7 +268,7 @@ def setup_logging(logger, args, defaults=None, formatter_defaults=None):
if args.get('valgrind', None) is not None:
for name in formatters:
formatter_options[name]['valgrind'] = True
setup_handlers(logger, formatters, formatter_options)
setup_handlers(logger, formatters, formatter_options, allow_unused_options)
set_default_logger(logger)
return logger

View File

@ -0,0 +1,92 @@
# 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/.
import mozlog
import os
import time
def pytest_addoption(parser):
# We can't simply use mozlog.commandline.add_logging_group(parser) here because
# Pytest's parser doesn't have the add_argument_group method Mozlog expects.
group = parser.getgroup('mozlog')
for name, (_class, _help) in mozlog.commandline.log_formatters.iteritems():
group.addoption('--log-{0}'.format(name), action='append', help=_help)
formatter_options = mozlog.commandline.fmt_options.iteritems()
for name, (_class, _help, formatters, action) in formatter_options:
for formatter in formatters:
if formatter in mozlog.commandline.log_formatters:
group.addoption(
'--log-{0}-{1}'.format(formatter, name),
action=action,
help=_help)
def pytest_configure(config):
# If using pytest-xdist for parallelization, only register plugin on master process
if not hasattr(config, 'slaveinput'):
config.pluginmanager.register(MozLog())
class MozLog(object):
def __init__(self):
self.results = {}
self.start_time = int(time.time() * 1000) # in ms for Mozlog compatibility
def format_nodeid(self, nodeid):
'''Helper to Reformat/shorten a "::"-separated pytest test nodeid'''
testfile, testname = nodeid.split("::")
return " ".join([os.path.basename(testfile), testname])
def pytest_configure(self, config):
mozlog.commandline.setup_logging('pytest', config.known_args_namespace,
defaults={}, allow_unused_options=True)
self.logger = mozlog.get_default_logger(component='pytest')
def pytest_sessionstart(self, session):
'''Called before test collection; records suite start time to log later'''
self.start_time = int(time.time() * 1000) # in ms for Mozlog compatibility
def pytest_collection_modifyitems(self, items):
'''Called after test collection is completed, just before tests are run (suite start)'''
self.logger.suite_start(tests=items, time=self.start_time)
def pytest_sessionfinish(self, session, exitstatus):
self.logger.suite_end()
def pytest_runtest_logstart(self, nodeid, location):
self.logger.test_start(test=self.format_nodeid(nodeid))
def pytest_runtest_logreport(self, report):
'''Called 3 times per test (setup, call, teardown), indicated by report.when'''
test = report.nodeid
status = expected = 'PASS'
message = stack = None
if hasattr(report, 'wasxfail'):
# Pytest reporting for xfail tests is somewhat counterinutitive:
# If an xfail test fails as expected, its 'call' report has .skipped,
# so we record status FAIL (== expected) and log an expected result.
# If an xfail unexpectedly passes, the 'call' report has .failed (Pytest 2)
# or .passed (Pytest 3), so we leave status as PASS (!= expected)
# to log an unexpected result.
expected = 'FAIL'
if report.skipped: # indicates expected failure (passing test)
status = 'FAIL'
elif report.failed:
status = 'FAIL' if report.when == 'call' else 'ERROR'
crash = report.longrepr.reprcrash # here longrepr is a ReprExceptionInfo
message = "{0} (line {1})".format(crash.message, crash.lineno)
stack = report.longrepr.reprtraceback
elif report.skipped: # indicates true skip
status = expected = 'SKIP'
message = report.longrepr[-1] # here longrepr is a tuple (file, lineno, reason)
if status != expected or expected != 'PASS':
self.results[test] = (status, expected, message, stack)
if report.when == 'teardown':
defaults = ('PASS', 'PASS', None, None)
status, expected, message, stack = self.results.get(test, defaults)
self.logger.test_end(test=self.format_nodeid(test),
status=status, expected=expected,
message=message, stack=stack)

View File

@ -32,5 +32,8 @@ setup(name=PACKAGE_NAME,
entry_points={
"console_scripts": [
"structlog = mozlog.scripts:main"
],
'pytest11': [
'mozlog = mozlog.pytest_mozlog.plugin',
]}
)

View File

@ -121,11 +121,18 @@ class MarionetteHarnessTests(VirtualenvMixin, BuildbotMixin, BaseScript):
test_path = os.path.join(dirs['abs_src_dir'], test_relpath)
self.activate_virtualenv()
import pytest
log_path = os.path.join(dirs['abs_log_dir'], 'pytest.log')
command = ['--resultlog', log_path, '--verbose', test_path]
command = ['-p', 'no:terminalreporter', # disable pytest logging
test_path]
logs = {}
for fmt in ['tbpl', 'mach', 'raw']:
logs[fmt] = os.path.join(dirs['abs_log_dir'],
'mn-harness_{}.log'.format(fmt))
command.extend(['--log-'+fmt, logs[fmt]])
self.info('Calling pytest.main with the following arguments: %s' % command)
status = self._get_pytest_status(pytest.main(command))
self.read_from_file(log_path)
self.read_from_file(logs['tbpl'])
for log in logs.values():
self.copy_to_upload_dir(log, dest='logs/')
self.buildbot_status(status)

View File

@ -1390,26 +1390,28 @@ nsAutoCompleteController::EnterMatch(bool aIsPopupSelection,
int32_t selectedIndex;
popup->GetSelectedIndex(&selectedIndex);
if (selectedIndex >= 0) {
nsAutoString inputValue;
input->GetTextValue(inputValue);
nsAutoString finalValue;
if (!completeSelection || aIsPopupSelection ||
(mDefaultIndexCompleted &&
inputValue.Equals(mPlaceholderCompletionString,
nsCaseInsensitiveStringComparator()))) {
bool defaultCompleted = mDefaultIndexCompleted &&
inputValue.Equals(mPlaceholderCompletionString,
nsCaseInsensitiveStringComparator());
if (aIsPopupSelection || (!completeSelection && !defaultCompleted)) {
// We need to fill-in the value if:
// * completeselectedindex is false
// * completeselectedindex is false and we didn't defaultComplete
// * A row in the popup was confirmed
// * The default index completion was confirmed
GetResultValueAt(selectedIndex, true, finalValue);
value = finalValue;
GetResultValueAt(selectedIndex, true, value);
} else if (defaultCompleted) {
// We also need to fill-in the value if the default index completion was
// confirmed, though we cannot use the selectedIndex cause the selection
// may have been changed by the mouse in the meanwhile.
GetFinalDefaultCompleteValue(value);
} else if (mCompletedSelectionIndex != -1) {
// If completeselectedindex is true, and EnterMatch was not invoked by
// mouse-clicking a match (for example the user pressed Enter),
// don't fill in the value as it will have already been filled in as
// needed, unless the selected match has a final complete value that
// differs from the user-facing value.
nsAutoString finalValue;
GetResultValueAt(mCompletedSelectionIndex, true, finalValue);
nsAutoString completedValue;
GetResultValueAt(mCompletedSelectionIndex, false, completedValue);

View File

@ -18,7 +18,7 @@ add_test(function test_handleEnter() {
["mozilla.com", "https://www.mozilla.com"],
["gomozilla.org", "http://www.gomozilla.org"],
];
doSearch("moz", results, controller => {
doSearch("moz", results, 0, controller => {
let input = controller.input;
Assert.equal(input.textValue, "mozilla.com");
Assert.equal(controller.getFinalCompleteValueAt(0), results[0][1]);
@ -32,7 +32,30 @@ add_test(function test_handleEnter() {
});
});
function doSearch(aSearchString, aResults, aOnCompleteCallback) {
add_test(function test_handleEnter_otherSelected() {
// The popup selection may not coincide with what is filled into the input
// field, for example if the user changed it with the mouse and then pressed
// Enter. In such a case we should still use the inputField value and not the
// popup selected value.
let results = [
["mozilla.com", "https://www.mozilla.com"],
["gomozilla.org", "http://www.gomozilla.org"],
];
doSearch("moz", results, 1, controller => {
let input = controller.input;
Assert.equal(input.textValue, "mozilla.com");
Assert.equal(controller.getFinalCompleteValueAt(0), results[0][1]);
Assert.equal(controller.getFinalCompleteValueAt(1), results[1][1]);
Assert.equal(input.popup.selectedIndex, 1);
controller.handleEnter(false);
// Verify that the keyboard-selected thing got inserted,
// and not the mouse selection:
Assert.equal(controller.input.textValue, "https://www.mozilla.com");
});
});
function doSearch(aSearchString, aResults, aSelectedIndex, aOnCompleteCallback) {
let search = new AutoCompleteSearchBase(
"search",
new AutoCompleteResult(aResults)
@ -41,6 +64,7 @@ function doSearch(aSearchString, aResults, aOnCompleteCallback) {
let input = new AutoCompleteInput([ search.name ]);
input.textValue = aSearchString;
input.popup.selectedIndex = aSelectedIndex;
// Needed for defaultIndex completion.
input.selectTextRange(aSearchString.length, aSearchString.length);