Merge m-c to inbound, a=merge CLOSED TREE
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
];
|
||||
|
@ -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";
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.0 KiB |
@ -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
|
||||
|
@ -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));
|
||||
|
14
layout/generic/crashtests/1297427-non-equal-centers.html
Normal 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>
|
@ -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
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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{" +
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -34,7 +34,7 @@ public class NativeZip implements NativeReference {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalize() {
|
||||
protected void finalize() {
|
||||
release();
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 25 KiB |
@ -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]
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
92
testing/mozbase/mozlog/mozlog/pytest_mozlog/plugin.py
Normal 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)
|
@ -32,5 +32,8 @@ setup(name=PACKAGE_NAME,
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"structlog = mozlog.scripts:main"
|
||||
],
|
||||
'pytest11': [
|
||||
'mozlog = mozlog.pytest_mozlog.plugin',
|
||||
]}
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|