import changes from `dev' branch of rmottola/Arctic-Fox:

- Bug 937810 - disable application reputation check on b2g r=mmc,mossop (403f8e0353)
- Bug 1216537 - Check and request storage permission if file download is started. r=nalexander,paolo (9233e7193d)
- Bug 1163937 - Added forceSave function to DownloadIntegration and ensured that downloads removed in Sanitizer do not persist. r=margaret (9610017b5d)
- Bug 989960 - Unhandled rejections in DOM Promises should cause xpcshell tests to fail. r=Yoric (be260d55d2)
- Bug 1177237 - Implement OS query parsing and user search choice. r=jaws (86c807e606)
- Bug 1177443 - Add 'system' purpose for searches coming from outside Firefox, r=MattN. (f6ac86ec2a)
- Bug 1072037, part 2 - Tests for the effect of setting CSS animation's AnimationPlayer.currentTime. r=birtles (79639d2ea4)
- bits of Bug 1145246, part 2 (849519f25d)
- bits of Bug 1145246, part 7 (3ec5500e08)
- bits of Bug 1109390 part 5 (6d849b9b3c)
- Bug 1074630, part 2 - CSS animations tests for Web Animations finishing behavior. r=birtles (bd0aa575bd)
- Bug 1073379, part 6 - Tests for the effect of setting CSS animation's AnimationPlayer.startTime. r=dholbert (9d8aad8d28)
- Bug 1073379, part 6b - Re-enable the player.playState check in checkStateAtActiveIntervalEndTime, but disable the first checkStateAtActiveIntervalEndTime call in the 'Skipping backwards through animation' tests. r=orange (3a9ea58cef)
- Bug 1141710, part 1 - Update to the new version of addDiv() in testcommon.js. r=dholbert (262b9dc018)
- Bug 1141710, part 2 - Update comments to be non-Mozilla specific. r=dholbert (177e8818f8)
- Bug 1141710, part 3 - Stop using ECMAScript 6 features in test_animation-player-starttime.html, since other browsers don't support them. r=dholbert (6f0c6ab687)
- Bug 1141710, part 4 - Avoid race condition when taking timestamps by reusing the original timestamp. r=dholbert (bc6ff2f3b1)
- Bug 1141710, part 5 - Change from assert_true() to the new assert_unreached(). r=dholbert (0c1608c4c4)
- Bug 1141710, part 7 - Create helpers to get the startTime corresponding to various points through the active duration. r=dholbert (568cf9a054)
- Bug 1141710, part 7 - Store the generated 'animation' property string in a global constant and reuse that constant. r=dholbert (ce85c0d7a9)
- Bug 1141710, part 8 - Create helpers to get the startTime corresponding to various points through the active duration. r=dholbert (6d6b1625ff)
- Bug 1141710, part 9 - Get the timeline from the player instead of from the document. r=dholbert (9660c00f1d)
- Bug 1141710, part 10 - Update some assertion text and comments. r=birtles (d238a482b5)
- Bug 1141710, part 11 - Assert that seeking over the before and active phases worked. r=birtles (4aedf388f9)
- Bug 1141710, part 12 - Check that the hold time is updated when the startTime is set to null. r=birtles (0473baeabd)
- Bug 1141710, part 13 - Wrap all lines at the 80 column mark. r=birtles (7d3ed6b453)
- Bug 1141710, part 14 - Get rid of the checks at 90% through the active duration. r=birtles (0f3c4cb7ca)
- part of Bug 1145246, part 2 - (ec09bf3fdd)
- half of Bug 1109390 part 1 - Add tests for getting the startTime; r=jwatt (a682851ba6)
- Bug 1149832 - Replace the Web Animations test helper waitForTwoAnimationFrames() with a helper that takes a frame count. r=birtles (da312eae7d)
- Bug 1235286 - Part 1: Add an argument to waitForAnimationFrames to run a task in each requestAnimationFrame callback. r=birtles (199a789546)
- Bug 1235286 - Part 2: Tests for animation optimizations. r=birtles (e510c68a7c)
- Bug 1235286 - Part 3: Comment out some compositor animations tests. r=birtles (61791d5b69)
- Bug 1235345 - Remove services/metrics. r=gfritzsche (29ac4a5895)
- Bug 1204846 - Modify the NetworkStatsDB to allow getSamples returns expired data at first sample and modify the test case. r=ethan (3e9d1f9a77)
- Bug 1237227 - Check the return of context->GetDisplayRootPresContext() for validity. r=roc (2ad8dcf545)
- sync parts of AppConstants.jsm for isPlatformAndVersion* functions
This commit is contained in:
2023-10-11 11:06:39 +08:00
parent c57ebb477f
commit 30f7e10db1
57 changed files with 2763 additions and 5445 deletions
+11
View File
@@ -77,6 +77,11 @@ XPCOMUtils.defineLazyServiceGetter(Services, 'captivePortalDetector',
'@mozilla.org/toolkit/captive-detector;1',
'nsICaptivePortalDetector');
if (AppConstants.MOZ_SAFE_BROWSING) {
XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
"resource://gre/modules/SafeBrowsing.jsm");
}
XPCOMUtils.defineLazyModuleGetter(this, "SafeMode",
"resource://gre/modules/SafeMode.jsm");
@@ -387,6 +392,12 @@ var shell = {
ppmm.addMessageListener("sms-handler", this);
ppmm.addMessageListener("mail-handler", this);
ppmm.addMessageListener("file-picker", this);
if (AppConstants.MOZ_SAFE_BROWSING) {
setTimeout(function() {
SafeBrowsing.init();
}, 5000);
}
},
stop: function shell_stop() {
+1 -1
View File
@@ -17,8 +17,8 @@ MOZ_BRANDING_DIRECTORY=b2g/branding/unofficial
MOZ_OFFICIAL_BRANDING_DIRECTORY=b2g/branding/official
# MOZ_APP_DISPLAYNAME is set by branding/configure.sh
MOZ_SAFE_BROWSING=1
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_METRICS=1
MOZ_WEBSMS_BACKEND=1
MOZ_NO_SMART_CARDS=1
+6
View File
@@ -390,6 +390,12 @@ pref("browser.search.suggest.enabled", true);
pref("browser.search.official", true);
#endif
#ifdef XP_WIN
pref("browser.search.redirectWindowsSearch", true);
#else
pref("browser.search.redirectWindowsSearch", false);
#endif
pref("browser.sessionhistory.max_entries", 50);
// Built-in default permissions.
+34 -1
View File
@@ -4,6 +4,7 @@
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
@@ -250,7 +251,7 @@ function doSearch(searchTerm, cmdLine) {
var ss = Components.classes["@mozilla.org/browser/search-service;1"]
.getService(nsIBrowserSearchService);
var submission = ss.defaultEngine.getSubmission(searchTerm);
var submission = ss.defaultEngine.getSubmission(searchTerm, null, "system");
// fill our nsISupportsArray with uri-as-wstring, null, null, postData
var sa = Components.classes["@mozilla.org/supports-array;1"]
@@ -666,10 +667,42 @@ nsDefaultCommandLineHandler.prototype = {
}
#endif
let redirectWinSearch = false;
if (AppConstants.isPlatformAndVersionAtLeast("win", "10")) {
redirectWinSearch = Services.prefs.getBoolPref("browser.search.redirectWindowsSearch");
}
try {
var ar;
while ((ar = cmdLine.handleFlagWithParam("url", false))) {
var uri = resolveURIInternal(cmdLine, ar);
// Searches in the Windows 10 task bar searchbox simply open the default browser
// with a URL for a search on Bing. Here we extract the search term and use the
// user's default search engine instead.
if (redirectWinSearch && uri.spec.startsWith("https://www.bing.com/search")) {
try {
var url = uri.QueryInterface(Components.interfaces.nsIURL);
var params = new URLSearchParams(url.query);
// We don't want to rewrite all Bing URLs coming from external apps. Look
// for the magic URL parm that's present in searches from the task bar.
// (Typed searches use "form=WNSGPH", Cortana voice searches use "FORM=WNSBOX")
var formParam = params.get("form");
if (!formParam) {
formParam = params.get("FORM");
}
if (formParam == "WNSGPH" || formParam == "WNSBOX") {
var term = params.get("q");
var ss = Components.classes["@mozilla.org/browser/search-service;1"]
.getService(nsIBrowserSearchService);
var submission = ss.defaultEngine.getSubmission(term, null, "system");
uri = submission.uri;
}
} catch (e) {
Components.utils.reportError("Couldn't redirect Windows search: " + e);
}
}
urilist.push(uri);
}
}
@@ -0,0 +1,79 @@
<preferences id="searchPreferences" hidden="true" data-category="paneSearch">
<preference id="browser.search.suggest.enabled"
name="browser.search.suggest.enabled"
type="bool"/>
<preference id="browser.search.hiddenOneOffs"
name="browser.search.hiddenOneOffs"
type="unichar"/>
<preference id="browser.search.redirectWindowsSearch"
name="browser.search.redirectWindowsSearch"
type="bool"/>
</preferences>
<script type="application/javascript"
src="chrome://browser/content/preferences/in-content/search.js"/>
<stringbundle id="engineManagerBundle" src="chrome://browser/locale/engineManager.properties"/>
<hbox id="header-search"
class="header"
hidden="true"
data-category="paneSearch">
<label class="header-name">&paneSearch.title;</label>
</hbox>
<!-- Default Search Engine -->
<groupbox id="defaultEngineGroup" align="start" data-category="paneSearch">
<caption label="&defaultSearchEngine.label;"/>
<label>&chooseYourDefaultSearchEngine.label;</label>
<menulist id="defaultEngine">
<menupopup/>
</menulist>
<checkbox id="suggestionsInSearchFieldsCheckbox"
label="&provideSearchSuggestions.label;"
accesskey="&provideSearchSuggestions.accesskey;"
preference="browser.search.suggest.enabled"/>
<checkbox id="redirectSearchCheckbox"
label="&redirectWindowsSearch.label;"
accesskey="&redirectWindowsSearch.accesskey;"
preference="browser.search.redirectWindowsSearch"/>
</groupbox>
<groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch">
<caption label="&oneClickSearchEngines.label;"/>
<label>&chooseWhichOneToDisplay.label;</label>
<tree id="engineList" flex="1" rows="8" hidecolumnpicker="true" editable="true"
seltype="single">
<treechildren id="engineChildren" flex="1"/>
<treecols>
<treecol id="engineShown" type="checkbox" editable="true" sortable="false"/>
<treecol id="engineName" flex="4" label="&engineNameColumn.label;" sortable="false"/>
<treecol id="engineKeyword" flex="1" label="&engineKeywordColumn.label;" editable="true"
sortable="false"/>
</treecols>
</tree>
<hbox>
<button id="restoreDefaultSearchEngines"
label="&restoreDefaultSearchEngines.label;"
accesskey="&restoreDefaultSearchEngines.accesskey;"
/>
<spacer flex="1"/>
<button id="removeEngineButton"
label="&removeEngine.label;"
accesskey="&removeEngine.accesskey;"
disabled="true"
/>
</hbox>
<separator class="thin"/>
<hbox id="addEnginesBox" pack="start">
<label id="addEngines" class="text-link" value="&addMoreSearchEngines.label;"/>
</hbox>
</groupbox>
File diff suppressed because one or more lines are too long
+1
View File
@@ -4,5 +4,6 @@ support-files =
../../imptests/testharness.js
../../imptests/testharnessreport.js
[chrome/test_animation_observers.html]
[chrome/test_restyles.html]
[chrome/test_running_on_compositor.html]
skip-if = buildapp == 'b2g'
@@ -0,0 +1,328 @@
<!doctype html>
<head>
<meta charset=utf-8>
<title>Tests restyles caused by animations</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/paint_listener.js"></script>
<script src="../testcommon.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
<style>
@keyframes opacity {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes background-color {
from { background-color: red; }
to { background-color: blue; }
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
div {
/* Element needs geometry to be eligible for layerization */
width: 100px;
height: 100px;
background-color: white;
}
</style>
</head>
<body>
<script>
'use strict';
function observeStyling(frameCount, onFrame) {
var docShell = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
.getInterface(SpecialPowers.Ci.nsIWebNavigation)
.QueryInterface(SpecialPowers.Ci.nsIDocShell);
docShell.recordProfileTimelineMarkers = true;
docShell.popProfileTimelineMarkers();
return new Promise(function(resolve) {
return waitForAnimationFrames(frameCount, onFrame).then(function() {
var markers = docShell.popProfileTimelineMarkers();
docShell.recordProfileTimelineMarkers = false;
var stylingMarkers = markers.filter(function(marker, index) {
return marker.name == 'Styles';
});
resolve(stylingMarkers);
});
});
}
SimpleTest.waitForExplicitFinish();
const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations';
var omtaEnabled = SpecialPowers.DOMWindowUtils.layerManagerRemote &&
SpecialPowers.getBoolPref(OMTAPrefKey);
function add_task_if_omta_enabled(test) {
if (!omtaEnabled) {
info(test.name + " is skipped because OMTA is disabled");
return;
}
add_task(test);
}
// We need to wait for all paints before running tests to avoid contaminations
// from styling of this document itself.
waitForAllPaints(function() {
add_task_if_omta_enabled(function* no_restyling_for_compositor_animations() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'CSS animations running on the compositor should not update style ' +
'on the main thread');
div.remove(div);
});
add_task_if_omta_enabled(function* no_restyling_for_compositor_transitions() {
var div = addDiv(null, { style: 'transition: opacity 100s; opacity: 0' });
getComputedStyle(div).opacity;
div.style.opacity = 1;
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
is(markers.length, 0,
'CSS transitions running on the compositor should not update style ' +
'on the main thread');
div.remove(div);
});
add_task_if_omta_enabled(function* no_restyling_when_animation_duration_is_changed() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
div.animationDuration = '200s';
var markers = yield observeStyling(5);
is(markers.length, 0,
'Animations running on the compositor should not update style ' +
'on the main thread');
div.remove(div);
});
add_task_if_omta_enabled(function* only_one_restyling_after_finish_is_called() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
animation.finish();
var markers = yield observeStyling(5);
is(markers.length, 1,
'Animations running on the compositor should only update style ' +
'once after finish() is called');
div.remove(div);
});
add_task(function* no_restyling_mouse_movement_on_finished_transition() {
var div = addDiv(null, { style: 'transition: opacity 1ms; opacity: 0' });
getComputedStyle(div).opacity;
div.style.opacity = 1;
var animation = div.getAnimations()[0];
var initialRect = div.getBoundingClientRect();
yield animation.finished;
var mouseX = initialRect.left + initialRect.width / 2;
var mouseY = initialRect.top + initialRect.height / 2;
var markers = yield observeStyling(5, function() {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(markers.length, 0,
'Bug 1219236: Finished transitions should never cause restyles ' +
'when mouse is moved on the animations');
div.remove(div);
});
add_task(function* no_restyling_mouse_movement_on_finished_animation() {
var div = addDiv(null, { style: 'animation: opacity 1ms' });
var animation = div.getAnimations()[0];
var initialRect = div.getBoundingClientRect();
yield animation.finished;
var mouseX = initialRect.left + initialRect.width / 2;
var mouseY = initialRect.top + initialRect.height / 2;
var markers = yield observeStyling(5, function() {
// We can't use synthesizeMouse here since synthesizeMouse causes
// layout flush.
synthesizeMouseAtPoint(mouseX++, mouseY++,
{ type: 'mousemove' }, window);
});
is(markers.length, 0,
'Bug 1219236: Finished animations should never cause restyles ' +
'when mouse is moved on the animations');
div.remove(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_out_of_view_element() {
var div = addDiv(null,
{ style: 'animation: opacity 100s; transform: translateY(-400px);' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1166500: Animations running on the compositor in out of ' +
'view element should never cause restyles');
div.remove(div);
});
add_task(function* no_restyling_main_thread_animations_out_of_view_element() {
var div = addDiv(null,
{ style: 'animation: background-color 100s; transform: translateY(-400px);' });
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1166500: Animations running on the main-thread in out of ' +
'view element should never cause restyles');
div.remove(div);
});
/*
Disabled for now since, on Android, the opacity animation runs on the
compositor even if it is scrolled out of view.
We will fix this in bug 1166500 or a follow-up bug
add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_scrolled_out_element() {
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: opacity 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1166500: Animations running on the compositor in elements ' +
'which are scrolled out should never cause restyles');
parentElement.remove(div);
});
*/
add_task(function* no_restyling_main_thread_animations_in_scrolled_out_element() {
var parentElement = addDiv(null,
{ style: 'overflow-y: scroll; height: 20px;' });
var div = addDiv(null,
{ style: 'animation: background-color 100s; position: relative; top: 100px;' });
parentElement.appendChild(div);
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1166500: Animations running on the main-thread in elements ' +
'which are scrolled out should never cause restyles');
parentElement.remove(div);
});
/*
Disabled for now since, on Android and B2G, the opacity animation runs on the
compositor even if the associated element has visibility:hidden.
We will fix this in bug 1237454 or a follow-up bug.
add_task_if_omta_enabled(function* no_restyling_compositor_animations_in_visiblily_hidden_element() {
var div = addDiv(null,
{ style: 'animation: opacity 100s; visibility: hidden' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(!animation.isRunningOnCompositor);
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1237454: Animations running on the compositor in ' +
'visibility hidden element should never cause restyles');
div.remove(div);
});
*/
add_task(function* no_restyling_main_thread_animations_in_visiblily_hidden_element() {
var div = addDiv(null,
{ style: 'animation: background-color 100s; visibility: hidden' });
var animation = div.getAnimations()[0];
yield animation.ready;
var markers = yield observeStyling(5);
todo_is(markers.length, 0,
'Bug 1237454: Animations running on the main-thread in ' +
'visibility hidden element should never cause restyles');
div.remove(div);
});
add_task_if_omta_enabled(function* no_restyling_compositor_animations_after_pause_is_called() {
var div = addDiv(null, { style: 'animation: opacity 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
ok(animation.isRunningOnCompositor);
animation.pause();
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Bug 1232563: Paused animations running on the compositor should ' +
'never cause restyles once after pause() is called');
div.remove(div);
});
add_task(function* no_restyling_matn_thread_animations_after_pause_is_called() {
var div = addDiv(null, { style: 'animation: background-color 100s' });
var animation = div.getAnimations()[0];
yield animation.ready;
animation.pause();
yield animation.ready;
var markers = yield observeStyling(5);
is(markers.length, 0,
'Bug 1232563: Paused animations running on the main-thread should ' +
'never cause restyles after pause() is called');
div.remove(div);
});
});
</script>
</body>
@@ -0,0 +1,657 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>Tests for the effect of setting a CSS animation's
Animation.currentTime</title>
<style>
.animated-div {
margin-left: 10px;
/* Make it easier to calculate expected values: */
animation-timing-function: linear ! important;
}
@keyframes anim {
from { margin-left: 100px; }
to { margin-left: 200px; }
}
</style>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../testcommon.js"></script>
</head>
<body>
<div id="log"></div>
<script type="text/javascript">
'use strict';
// TODO: add equivalent tests without an animation-delay, but first we need to
// change the timing of animationstart dispatch. (Right now the animationstart
// event will fire before the ready Promise is resolved if there is no
// animation-delay.)
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1134163
// TODO: Once the computedTiming property is implemented, add checks to the
// checker helpers to ensure that computedTiming's properties are updated as
// expected.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
const CSS_ANIM_EVENTS =
['animationstart', 'animationiteration', 'animationend'];
const ANIM_DELAY_MS = 1000000; // 1000s
const ANIM_DUR_MS = 1000000; // 1000s
const ANIM_PROPERTY_VAL = 'anim ' + ANIM_DUR_MS + 'ms ' + ANIM_DELAY_MS + 'ms';
/**
* These helpers get the value that the currentTime needs to be set to, to put
* an animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into
* the middle of various phases or points through the active duration.
*/
function currentTimeForBeforePhase(timeline) {
return ANIM_DELAY_MS / 2;
}
function currentTimeForActivePhase(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS / 2;
}
function currentTimeForAfterPhase(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS + ANIM_DELAY_MS / 2;
}
function currentTimeForStartOfActiveInterval(timeline) {
return ANIM_DELAY_MS;
}
function currentTimeForFiftyPercentThroughActiveInterval(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS * 0.5;
}
function currentTimeForEndOfActiveInterval(timeline) {
return ANIM_DELAY_MS + ANIM_DUR_MS;
}
// Expected computed 'margin-left' values at points during the active interval:
// When we assert_between_inclusive using these values we could in theory cause
// intermittent failure due to very long delays between paints, but since the
// active duration is 1000s long, a delay would need to be around 100s to cause
// that. If that's happening then there are likely other issues that should be
// fixed, so a failure to make us look into that seems like a good thing.
const UNANIMATED_POSITION = 10;
const INITIAL_POSITION = 100;
const TEN_PCT_POSITION = 110;
const FIFTY_PCT_POSITION = 150;
const END_POSITION = 200;
/**
* CSS animation events fire asynchronously after we set 'startTime'. This
* helper class allows us to handle such events using Promises.
*
* To use this class:
*
* var eventWatcher = new EventWatcher(watchedNode, eventTypes);
* eventWatcher.waitForEvent(eventType).then(function() {
* // Promise fulfilled
* checkStuff();
* makeSomeChanges();
* return eventWatcher.waitForEvent(nextEventType);
* }).then(function() {
* // Promise fulfilled
* checkMoreStuff();
* eventWatcher.stopWatching(); // all done - stop listening for events
* });
*
* This class will assert_unreached() if an event occurs when there is no
* Promise created by a waitForEvent() call waiting to be fulfilled, or if the
* event is of a different type to the type passed to waitForEvent. This helps
* provide test coverage to ensure that only events that are expected occur, in
* the correct order and with the correct timing. It also helps vastly simplify
* the already complex code below by avoiding lots of gnarly error handling
* code.
*/
function EventWatcher(watchedNode, eventTypes)
{
if (typeof eventTypes == 'string') {
eventTypes = [eventTypes];
}
var waitingFor = null;
function eventHandler(evt) {
if (!waitingFor) {
assert_unreached('Not expecting event, but got: ' + evt.type +
' targeting element #' + evt.target.getAttribute('id'));
return;
}
if (evt.type != waitingFor.types[0]) {
assert_unreached('Expected ' + waitingFor.types[0] + ' event but got ' +
evt.type + ' event');
return;
}
if (waitingFor.types.length > 1) {
// Pop first event from array
waitingFor.types.shift();
return;
}
// We need to null out waitingFor before calling the resolve function since
// the Promise's resolve handlers may call waitForEvent() which will need
// to set waitingFor.
var resolveFunc = waitingFor.resolve;
waitingFor = null;
resolveFunc(evt);
}
for (var i = 0; i < eventTypes.length; i++) {
watchedNode.addEventListener(eventTypes[i], eventHandler);
}
this.waitForEvent = function(type) {
if (typeof type != 'string') {
return Promise.reject('Event type not a string');
}
return this.waitForEvents([type]);
};
/**
* This is useful when two events are expected to fire one immediately after
* the other. This happens when we skip over the entire active interval for
* instance. In this case an 'animationstart' and an 'animationend' are fired
* and due to the asynchronous nature of Promise callbacks this won't work:
*
* eventWatcher.waitForEvent('animationstart').then(function() {
* return waitForEvent('animationend');
* }).then(...);
*
* It doesn't work because the 'animationend' listener is added too late,
* because the resolve handler for the first Promise is called asynchronously
* some time after the 'animationstart' event is called, rather than at the
* time the event reaches the watched element.
*/
this.waitForEvents = function(types) {
if (waitingFor) {
return Promise.reject('Already waiting for an event');
}
return new Promise(function(resolve, reject) {
waitingFor = {
types: types,
resolve: resolve,
reject: reject
};
});
};
this.stopWatching = function() {
for (var i = 0; i < eventTypes.length; i++) {
watchedNode.removeEventListener(eventTypes[i], eventHandler);
}
};
return this;
}
// The terms used for the naming of the following helper functions refer to
// terms used in the Web Animations specification for specific phases of an
// animation. The terms can be found here:
//
// http://w3c.github.io/web-animations/#animation-node-phases-and-states
//
// Note the distinction between "player start time" and "animation start time".
// The former is the start of the start delay. The latter is the start of the
// active interval. (If there is no delay, they are the same.)
// Called when currentTime is set to zero (the beginning of the start delay).
function checkStateOnSettingCurrentTimeToZero(animation)
{
// We don't test animation.currentTime since our caller just set it.
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" at the start of ' +
'the start delay');
assert_equals(animation.source.target.style.animationPlayState, 'running',
'Animation.source.target.style.animationPlayState should be ' +
'"running" at the start of the start delay');
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'at the beginning of the start delay');
}
// Called when the ready Promise's callbacks should happen
function checkStateOnReadyPromiseResolved(animation)
{
// the 0.0001 here is for rounding error
assert_less_than_equal(animation.currentTime,
animation.timeline.currentTime - animation.startTime + 0.0001,
'Animation.currentTime should be less than the local time ' +
'equivalent of the timeline\'s currentTime on the first paint tick ' +
'after animation creation');
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" on the first paint ' +
'tick after animation creation');
assert_equals(animation.source.target.style.animationPlayState, 'running',
'Animation.source.target.style.animationPlayState should be ' +
'"running" on the first paint tick after animation creation');
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'by an animation with a delay on ready Promise resolve');
}
// Called when currentTime is set to the time the active interval starts.
function checkStateAtActiveIntervalStartTime(animation)
{
// We don't test animation.currentTime since our caller just set it.
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" at the start of ' +
'the active interval');
assert_equals(animation.source.target.style.animationPlayState, 'running',
'Animation.source.target.style.animationPlayState should be ' +
'"running" at the start of the active interval');
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION,
'the computed value of margin-left should be close to the value at the ' +
'beginning of the animation');
}
function checkStateAtFiftyPctOfActiveInterval(animation)
{
// We don't test animation.currentTime since our caller just set it.
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, FIFTY_PCT_POSITION,
'the computed value of margin-left should be half way through the ' +
'animation at the midpoint of the active interval');
}
// Called when currentTime is set to the time the active interval ends.
function checkStateAtActiveIntervalEndTime(animation)
{
// We don't test animation.currentTime since our caller just set it.
assert_equals(animation.playState, 'finished',
'Animation.playState should be "finished" at the end of ' +
'the active interval');
assert_equals(animation.source.target.style.animationPlayState, "running",
'Animation.source.target.style.animationPlayState should be ' +
'"finished" at the end of the active interval');
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'by the animation at the end of the active duration when the ' +
'animation-fill-mode is none');
}
test(function(t)
{
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
// Animations shouldn't start until the next paint tick, so:
assert_equals(animation.currentTime, 0,
'Animation.currentTime should be zero when an animation ' +
'is initially created');
assert_equals(animation.playState, "pending",
'Animation.playState should be "pending" when an animation ' +
'is initially created');
assert_equals(animation.source.target.style.animationPlayState, 'running',
'Animation.source.target.style.animationPlayState should be ' +
'"running" when an animation is initially created');
// XXX Ideally we would have a test to check the ready Promise is initially
// unresolved, but currently there is no Web API to do that. Waiting for the
// ready Promise with a timeout doesn't work because the resolved callback
// will be called (async) regardless of whether the Promise was resolved in
// the past or is resolved in the future.
// So that animation is running instead of paused when we set currentTime:
animation.startTime = animation.timeline.currentTime;
assert_approx_equals(animation.currentTime, 0, 0.0001, // rounding error
'Check setting of currentTime actually works');
checkStateOnSettingCurrentTimeToZero(animation);
}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
'currentTime');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func(function() {
checkStateOnReadyPromiseResolved(animation);
animation.currentTime =
currentTimeForStartOfActiveInterval(animation.timeline);
return eventWatcher.waitForEvent('animationstart');
})).then(t.step_func(function() {
checkStateAtActiveIntervalStartTime(animation);
animation.currentTime =
currentTimeForFiftyPercentThroughActiveInterval(animation.timeline);
checkStateAtFiftyPctOfActiveInterval(animation);
animation.currentTime =
currentTimeForEndOfActiveInterval(animation.timeline);
return eventWatcher.waitForEvent('animationend');
})).then(t.step_func(function() {
checkStateAtActiveIntervalEndTime(animation);
eventWatcher.stopWatching();
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'Skipping forward through animation');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
// So that animation is running instead of paused when we set currentTime:
animation.startTime = animation.timeline.currentTime;
animation.currentTime = currentTimeForEndOfActiveInterval(animation.timeline);
var previousTimelineTime = animation.timeline.currentTime;
// Skipping over the active interval will dispatch an 'animationstart' then
// an 'animationend' event. We need to wait for these events before we start
// testing going backwards since EventWatcher will fail the test if it gets
// an event that we haven't told it about.
eventWatcher.waitForEvents(['animationstart',
'animationend']).then(t.step_func(function() {
assert_true(document.timeline.currentTime - previousTimelineTime <
ANIM_DUR_MS,
'Sanity check that seeking worked rather than the events ' +
'firing after normal playback through the very long ' +
'animation duration');
// Now we can start the tests for skipping backwards, but first we check
// that after the events we're still in the same end time state:
checkStateAtActiveIntervalEndTime(animation);
animation.currentTime =
currentTimeForFiftyPercentThroughActiveInterval(animation.timeline);
// Despite going backwards from after the end of the animation (to being
// in the active interval), we now expect an 'animationstart' event
// because the animation should go from being inactive to active.
//
// Calling checkStateAtFiftyPctOfActiveInterval will check computed style,
// causing computed style to be updated and the 'animationstart' event to
// be dispatched synchronously. We need to call waitForEvent first
// otherwise eventWatcher will assert that the event was unexpected.
var promise = eventWatcher.waitForEvent('animationstart');
checkStateAtFiftyPctOfActiveInterval(animation);
return promise;
})).then(t.step_func(function() {
animation.currentTime =
currentTimeForStartOfActiveInterval(animation.timeline);
checkStateAtActiveIntervalStartTime(animation);
animation.currentTime = 0;
// Despite going backwards from just after the active interval starts to
// the animation start time, we now expect an animationend event
// because we went from inside to outside the active interval.
return eventWatcher.waitForEvent('animationend');
})).then(t.step_func(function() {
checkStateOnReadyPromiseResolved(animation);
eventWatcher.stopWatching();
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
// This must come after we've set up the Promise chain, since requesting
// computed style will force events to be dispatched.
// XXX For some reason this fails occasionally (either the animation.playState
// check or the marginLeft check).
//checkStateAtActiveIntervalEndTime(animation);
}, 'Skipping backwards through animation');
// Next we have multiple tests to check that redundant currentTime changes do
// NOT dispatch events. It's impossible to distinguish between events not being
// dispatched and events just taking an incredibly long time to dispatch
// without waiting an infinitely long time. Obviously we don't want to do that
// (block this test from finishing forever), so instead we just listen for
// events until two animation frames (i.e. requestAnimationFrame callbacks)
// have happened, then assume that no events will ever be dispatched for the
// redundant changes if no events were detected in that time.
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.currentTime = currentTimeForActivePhase(animation.timeline);
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
}, 'Redundant change, before -> active, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
}, 'Redundant change, before -> after, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvent('animationstart').then(function() {
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
animation.currentTime = currentTimeForActivePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForActivePhase(animation.timeline);
}, 'Redundant change, active -> before, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvent('animationstart').then(function() {
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
animation.currentTime = currentTimeForActivePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForActivePhase(animation.timeline);
}, 'Redundant change, active -> after, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvents(['animationstart',
'animationend']).then(function() {
animation.currentTime = currentTimeForBeforePhase(animation.timeline);
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
}, 'Redundant change, after -> before, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvents(['animationstart',
'animationend']).then(function() {
animation.currentTime = currentTimeForActivePhase(animation.timeline);
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.currentTime = currentTimeForAfterPhase(animation.timeline);
}, 'Redundant change, after -> active, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func(function() {
var exception;
try {
animation.currentTime = null;
} catch (e) {
exception = e;
}
assert_equals(exception.name, 'TypeError',
'Expect TypeError exception on trying to set ' +
'Animation.currentTime to null');
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'Setting currentTime to null');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var player = div.getAnimationPlayers()[0];
var pauseTime;
player.ready.then(t.step_func(function() {
assert_not_equals(player.currentTime, null,
'AnimationPlayer.currentTime not null on ready Promise resolve');
player.pause();
return player.ready;
})).then(t.step_func(function() {
pauseTime = player.currentTime;
return waitForFrame();
})).then(t.step_func(function() {
assert_equals(player.currentTime, pauseTime,
'AnimationPlayer.currentTime is unchanged after pausing');
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'AnimationPlayer.currentTime after pausing');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(function() {
// just before animation ends:
animation.currentTime = ANIM_DELAY_MS + ANIM_DUR_MS - 1;
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
assert_equals(animation.currentTime, ANIM_DELAY_MS + ANIM_DUR_MS,
'Animation.currentTime should not continue to increase after the ' +
'animation has finished');
t.done();
}));
}, 'Test Animation.currentTime clamping');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(function() {
// play backwards:
animation.playbackRate = -1;
// just before animation ends (at the "start"):
animation.currentTime = 1;
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
assert_equals(animation.currentTime, 0,
'Animation.currentTime should not continue to decrease after an ' +
'animation running in reverse has finished and currentTime is zero');
t.done();
}));
}, 'Test Animation.currentTime clamping for reversed animation');
</script>
</body>
</html>
@@ -0,0 +1,290 @@
<!doctype html>
<meta charset=utf-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../testcommon.js"></script>
<div id="log"></div>
<style>
@keyframes abc {
to { transform: translate(10px) }
}
@keyframes def {}
</style>
<script>
'use strict';
const ANIM_PROP_VAL = 'abc 100s';
const ANIM_DURATION = 100000; // ms
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
animation.ready.then(t.step_func(function() {
assert_equals(animation.finished, previousFinishedPromise,
'Finished promise is the same object when playing starts');
animation.pause();
assert_equals(animation.finished, previousFinishedPromise,
'Finished promise does not change when pausing');
animation.play();
assert_equals(animation.finished, previousFinishedPromise,
'Finished promise does not change when play() unpauses');
animation.currentTime = ANIM_DURATION;
return animation.finished;
})).then(t.step_func(function() {
assert_equals(animation.finished, previousFinishedPromise,
'Finished promise is the same object when playing completes');
t.done();
}));
}, 'Test pausing then playing does not change the finished promise');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
animation.currentTime = ANIM_DURATION;
animation.finished.then(t.step_func(function() {
assert_equals(animation.finished, previousFinishedPromise,
'Finished promise is the same object when playing completes');
animation.play();
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise changes when replaying animation');
previousFinishedPromise = animation.finished;
animation.play();
assert_equals(animation.finished, previousFinishedPromise,
'Finished promise is the same after redundant play() call');
t.done();
}));
}, 'Test restarting a finished animation');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise;
animation.currentTime = ANIM_DURATION;
animation.finished.then(function() {
previousFinishedPromise = animation.finished;
animation.playbackRate = -1;
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise should be replaced when reversing a ' +
'finished promise');
animation.currentTime = 0;
return animation.finished;
}).then(t.step_func(function() {
previousFinishedPromise = animation.finished;
animation.play();
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise is replaced after play() call on ' +
'finished, reversed animation');
t.done();
}));
}, 'Test restarting a reversed finished animation');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
animation.currentTime = ANIM_DURATION;
animation.finished.then(t.step_func(function() {
animation.currentTime = ANIM_DURATION + 1000;
assert_equals(animation.finished, previousFinishedPromise,
'Finished promise is unchanged jumping past end of ' +
'finished animation');
t.done();
}));
}, 'Test redundant finishing of animation');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
animation.currentTime = ANIM_DURATION;
animation.finished.then(t.step_func(function(resolvedAnimation) {
assert_equals(resolvedAnimation, animation,
'Object identity of animation passed to Promise callback'
+ ' matches the animation object owning the Promise');
t.done();
}));
}, 'The finished promise is fulfilled with its Animation');
async_test(function(t) {
var div = addDiv(t);
// Set up pending animation
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
// Set up listeners on finished promise
animation.finished.then(t.step_func(function() {
assert_unreached('finished promise is fulfilled');
})).catch(t.step_func(function(err) {
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise should change after the original is ' +
'rejected');
assert_equals(err.name, 'AbortError',
'finished promise is rejected with AbortError');
assert_equals(animation.playState, 'idle',
'Animation is idle after animation was cancelled');
})).then(t.step_func(function() {
t.done();
}));
// Now cancel the animation and flush styles
div.style.animation = '';
window.getComputedStyle(div).animation;
}, 'finished promise is rejected when an animation is cancelled by resetting ' +
'the animation property');
async_test(function(t) {
var div = addDiv(t);
// As before, but this time instead of removing all animations, simply update
// the list of animations. At least for Firefox, updating is a different
// code path.
// Set up pending animation
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
// Set up listeners on finished promise
animation.finished.then(t.step_func(function() {
assert_unreached('finished promise was fulfilled');
})).catch(t.step_func(function(err) {
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise should change after the original is ' +
'rejected');
assert_equals(err.name, 'AbortError',
'finished promise is rejected with AbortError');
assert_equals(animation.playState, 'idle',
'Animation is idle after animation was cancelled');
})).then(t.step_func(function() {
t.done();
}));
// Now update the animation and flush styles
div.style.animation = 'def 100s';
window.getComputedStyle(div).animation;
}, 'finished promise is rejected when an animation is cancelled by changing ' +
'the animation property');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
const HALF_DUR = ANIM_DURATION / 2;
const QUARTER_DUR = ANIM_DURATION / 4;
animation.currentTime = HALF_DUR;
div.style.animationDuration = QUARTER_DUR + 'ms';
// Animation should now be finished
// Below we use gotNextFrame to check that shortening of the animation
// duration causes the finished promise to resolve, rather than it just
// getting resolved on the next animation frame. This relies on the fact
// that the promises are resolved as a micro-task before the next frame
// happens.
window.getComputedStyle(div).animationDuration; // flush style
var gotNextFrame = false;
waitForFrame().then(function() {
gotNextFrame = true;
});
animation.finished.then(t.step_func(function() {
assert_false(gotNextFrame, 'shortening of the animation duration should ' +
'resolve the finished promise');
assert_equals(animation.currentTime, HALF_DUR,
'currentTime should be unchanged when duration shortened');
var previousFinishedPromise = animation.finished;
div.style.animationDuration = ANIM_DURATION + 'ms'; // now active again
window.getComputedStyle(div).animationDuration; // flush style
assert_not_equals(animation.finished, previousFinishedPromise,
'Finished promise should change after lengthening the ' +
'duration causes the animation to become active');
t.done();
}));
}, 'Test finished promise changes for animation duration changes');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(function() {
animation.playbackRate = 0;
animation.currentTime = ANIM_DURATION + 1000;
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
t.done();
}));
animation.finished.then(t.step_func(function() {
assert_unreached('finished promise should not resolve when playbackRate ' +
'is zero');
}));
}, 'Test finished promise changes when playbackRate == 0');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(function() {
animation.playbackRate = -1;
return animation.finished;
}).then(t.step_func(function() {
t.done();
}));
}, 'Test finished promise resolves when playbackRate set to a negative value');
async_test(function(t) {
var div = addDiv(t);
div.style.animation = ANIM_PROP_VAL;
var animation = div.getAnimations()[0];
var previousFinishedPromise = animation.finished;
animation.currentTime = ANIM_DURATION;
animation.finished.then(function() {
div.style.animationPlayState = 'running';
return waitForAnimationFrames(2);
}).then(t.step_func(function() {
assert_equals(animation.finished, previousFinishedPromise,
'Should not replay when animation-play-state changes to ' +
'"running" on finished animation');
assert_equals(animation.currentTime, ANIM_DURATION,
'currentTime should not change when animation-play-state ' +
'changes to "running" on finished animation');
t.done();
}));
}, 'Test finished promise changes when animationPlayState set to running');
</script>
@@ -0,0 +1,680 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8>
<title>Tests for the effect of setting a CSS animation's
Animation.startTime</title>
<style>
.animated-div {
margin-left: 10px;
/* Make it easier to calculate expected values: */
animation-timing-function: linear ! important;
}
@keyframes anim {
from { margin-left: 100px; }
to { margin-left: 200px; }
}
</style>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../testcommon.js"></script>
</head>
<body>
<div id="log"></div>
<script type="text/javascript">
'use strict';
// TODO: add equivalent tests without an animation-delay, but first we need to
// change the timing of animationstart dispatch. (Right now the animationstart
// event will fire before the ready Promise is resolved if there is no
// animation-delay.)
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1134163
// TODO: Once the computedTiming property is implemented, add checks to the
// checker helpers to ensure that computedTiming's properties are updated as
// expected.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1108055
const CSS_ANIM_EVENTS =
['animationstart', 'animationiteration', 'animationend'];
const ANIM_DELAY_MS = 1000000; // 1000s
const ANIM_DUR_MS = 1000000; // 1000s
const ANIM_PROPERTY_VAL = 'anim ' + ANIM_DUR_MS + 'ms ' + ANIM_DELAY_MS + 'ms';
/**
* These helpers get the value that the startTime needs to be set to, to put an
* animation that uses the above ANIM_DELAY_MS and ANIM_DUR_MS values into the
* middle of various phases or points through the active duration.
*/
function startTimeForBeforePhase(timeline) {
return timeline.currentTime - ANIM_DELAY_MS / 2;
}
function startTimeForActivePhase(timeline) {
return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS / 2;
}
function startTimeForAfterPhase(timeline) {
return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS - ANIM_DELAY_MS / 2;
}
function startTimeForStartOfActiveInterval(timeline) {
return timeline.currentTime - ANIM_DELAY_MS;
}
function startTimeForFiftyPercentThroughActiveInterval(timeline) {
return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS * 0.5;
}
function startTimeForEndOfActiveInterval(timeline) {
return timeline.currentTime - ANIM_DELAY_MS - ANIM_DUR_MS;
}
// Expected computed 'margin-left' values at points during the active interval:
// When we assert_between_inclusive using these values we could in theory cause
// intermittent failure due to very long delays between paints, but since the
// active duration is 1000s long, a delay would need to be around 100s to cause
// that. If that's happening then there are likely other issues that should be
// fixed, so a failure to make us look into that seems like a good thing.
const UNANIMATED_POSITION = 10;
const INITIAL_POSITION = 100;
const TEN_PCT_POSITION = 110;
const FIFTY_PCT_POSITION = 150;
const END_POSITION = 200;
/**
* CSS animation events fire asynchronously after we set 'startTime'. This
* helper class allows us to handle such events using Promises.
*
* To use this class:
*
* var eventWatcher = new EventWatcher(watchedNode, eventTypes);
* eventWatcher.waitForEvent(eventType).then(function() {
* // Promise fulfilled
* checkStuff();
* makeSomeChanges();
* return eventWatcher.waitForEvent(nextEventType);
* }).then(function() {
* // Promise fulfilled
* checkMoreStuff();
* eventWatcher.stopWatching(); // all done - stop listening for events
* });
*
* This class will assert_unreached() if an event occurs when there is no
* Promise created by a waitForEvent() call waiting to be fulfilled, or if the
* event is of a different type to the type passed to waitForEvent. This helps
* provide test coverage to ensure that only events that are expected occur, in
* the correct order and with the correct timing. It also helps vastly simplify
* the already complex code below by avoiding lots of gnarly error handling
* code.
*/
function EventWatcher(watchedNode, eventTypes)
{
if (typeof eventTypes == 'string') {
eventTypes = [eventTypes];
}
var waitingFor = null;
function eventHandler(evt) {
if (!waitingFor) {
assert_unreached('Not expecting event, but got: ' + evt.type +
' targeting element #' + evt.target.getAttribute('id'));
return;
}
if (evt.type != waitingFor.types[0]) {
assert_unreached('Expected ' + waitingFor.types[0] + ' event but got ' +
evt.type + ' event');
return;
}
if (waitingFor.types.length > 1) {
// Pop first event from array
waitingFor.types.shift();
return;
}
// We need to null out waitingFor before calling the resolve function since
// the Promise's resolve handlers may call waitForEvent() which will need
// to set waitingFor.
var resolveFunc = waitingFor.resolve;
waitingFor = null;
resolveFunc(evt);
}
for (var i = 0; i < eventTypes.length; i++) {
watchedNode.addEventListener(eventTypes[i], eventHandler);
}
this.waitForEvent = function(type) {
if (typeof type != 'string') {
return Promise.reject('Event type not a string');
}
return this.waitForEvents([type]);
};
/**
* This is useful when two events are expected to fire one immediately after
* the other. This happens when we skip over the entire active interval for
* instance. In this case an 'animationstart' and an 'animationend' are fired
* and due to the asynchronous nature of Promise callbacks this won't work:
*
* eventWatcher.waitForEvent('animationstart').then(function() {
* return waitForEvent('animationend');
* }).then(...);
*
* It doesn't work because the 'animationend' listener is added too late,
* because the resolve handler for the first Promise is called asynchronously
* some time after the 'animationstart' event is called, rather than at the
* time the event reaches the watched element.
*/
this.waitForEvents = function(types) {
if (waitingFor) {
return Promise.reject('Already waiting for an event');
}
return new Promise(function(resolve, reject) {
waitingFor = {
types: types,
resolve: resolve,
reject: reject
};
});
};
this.stopWatching = function() {
for (var i = 0; i < eventTypes.length; i++) {
watchedNode.removeEventListener(eventTypes[i], eventHandler);
}
};
return this;
}
// The terms used for the naming of the following helper functions refer to
// terms used in the Web Animations specification for specific phases of an
// animation. The terms can be found here:
//
// http://w3c.github.io/web-animations/#animation-node-phases-and-states
//
// Note the distinction between "player start time" and "animation start time".
// The former is the start of the start delay. The latter is the start of the
// active interval. (If there is no delay, they are the same.)
// Called when startTime is set to the time the start delay would ideally
// start (not accounting for any delay to next paint tick).
function checkStateOnSettingStartTimeToAnimationCreationTime(player)
{
// We don't test player.startTime since our caller just set it.
assert_equals(player.playState, 'running',
'AnimationPlayer.playState should be "running" at the start of ' +
'the start delay');
assert_equals(player.source.target.style.animationPlayState, 'running',
'AnimationPlayer.source.target.style.animationPlayState should be ' +
'"running" at the start of the start delay');
var div = player.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'at the beginning of the start delay');
}
// Called when the ready Promise's callbacks should happen
function checkStateOnReadyPromiseResolved(animation)
{
assert_less_than_equal(animation.startTime, animation.timeline.currentTime,
'Animation.startTime should be less than the timeline\'s ' +
'currentTime on the first paint tick after animation creation');
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" on the first paint ' +
'tick after animation creation');
assert_equals(animation.source.target.style.animationPlayState, 'running',
'Animation.source.target.style.animationPlayState should be ' +
'"running" on the first paint tick after animation creation');
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'by an animation with a delay on ready Promise resolve');
}
// Called when startTime is set to the time the active interval starts.
function checkStateAtActiveIntervalStartTime(animation)
{
// We don't test animation.startTime since our caller just set it.
assert_equals(animation.playState, 'running',
'Animation.playState should be "running" at the start of ' +
'the active interval');
assert_equals(animation.source.target.style.animationPlayState, 'running',
'Animation.source.target.style.animationPlayState should be ' +
'"running" at the start of the active interval');
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_between_inclusive(marginLeft, INITIAL_POSITION, TEN_PCT_POSITION,
'the computed value of margin-left should be close to the value at the ' +
'beginning of the animation');
}
function checkStateAtFiftyPctOfActiveInterval(animation)
{
// We don't test animation.startTime since our caller just set it.
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, FIFTY_PCT_POSITION,
'the computed value of margin-left should be half way through the ' +
'animation at the midpoint of the active interval');
}
// Called when startTime is set to the time the active interval ends.
function checkStateAtActiveIntervalEndTime(animation)
{
// We don't test animation.startTime since our caller just set it.
assert_equals(animation.playState, 'finished',
'Animation.playState should be "finished" at the end of ' +
'the active interval');
assert_equals(animation.source.target.style.animationPlayState, "running",
'Animation.source.target.style.animationPlayState should be ' +
'"finished" at the end of the active interval');
var div = animation.source.target;
var marginLeft = parseFloat(getComputedStyle(div).marginLeft);
assert_equals(marginLeft, UNANIMATED_POSITION,
'the computed value of margin-left should be unaffected ' +
'by the animation at the end of the active duration when the ' +
'animation-fill-mode is none');
}
test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var player = div.getAnimationPlayers()[0];
assert_equals(player.startTime, null, 'startTime is unresolved');
}, 'startTime of a newly created (play-pending) animation is unresolved');
test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
var player = div.getAnimationPlayers()[0];
assert_equals(player.startTime, null, 'startTime is unresolved');
}, 'startTime of a newly created (pause-pending) animation is unresolved');
async_test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var player = div.getAnimationPlayers()[0];
player.ready.then(t.step_func(function() {
assert_true(player.startTime > 0,
'startTime is resolved when running');
t.done();
}));
}, 'startTime is resolved when running');
async_test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s paused' });
var player = div.getAnimationPlayers()[0];
player.ready.then(t.step_func(function() {
assert_equals(player.startTime, null,
'startTime is unresolved when paused');
t.done();
}));
}, 'startTime is unresolved when paused');
async_test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var player = div.getAnimationPlayers()[0];
player.ready.then(t.step_func(function() {
div.style.animationPlayState = 'paused';
getComputedStyle(div).animationPlayState;
/* FIXME: Switch this on once deferred pausing is enabled
assert_not_equals(player.startTime, null,
'startTime is resolved when pause-pending');
*/
div.style.animationPlayState = 'running';
getComputedStyle(div).animationPlayState;
assert_equals(player.startTime, null,
'startTime is unresolved when play-pending');
t.done();
}));
}, 'startTime while pause-pending and play-pending');
async_test(function(t)
{
var div = addDiv(t, { 'style': 'animation: anim 100s' });
var player = div.getAnimationPlayers()[0];
// Seek to end to put us in the finished state
// FIXME: Once we implement finish(), use that here.
player.currentTime = 100 * 1000;
player.ready.then(t.step_func(function() {
// Call play() which puts us back in the running state
player.play();
// FIXME: Enable this once we implement finishing behavior (bug 1074630)
/*
assert_equals(player.startTime, null, 'startTime is unresolved');
*/
t.done();
}));
}, 'startTime while play-pending from finished state');
test(function(t)
{
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var player = div.getAnimationPlayers()[0];
// Animations shouldn't start until the next paint tick, so:
assert_equals(player.startTime, null,
'AnimationPlayer.startTime should be unresolved when an animation ' +
'is initially created');
assert_equals(player.playState, "pending",
'AnimationPlayer.playState should be "pending" when an animation ' +
'is initially created');
assert_equals(player.source.target.style.animationPlayState, 'running',
'AnimationPlayer.source.target.style.animationPlayState should be ' +
'"running" when an animation is initially created');
// XXX Ideally we would have a test to check the ready Promise is initially
// unresolved, but currently there is no Web API to do that. Waiting for the
// ready Promise with a timeout doesn't work because the resolved callback
// will be called (async) regardless of whether the Promise was resolved in
// the past or is resolved in the future.
var currentTime = player.timeline.currentTime;
player.startTime = currentTime;
assert_approx_equals(player.startTime, currentTime, 0.0001, // rounding error
'Check setting of startTime actually works');
checkStateOnSettingStartTimeToAnimationCreationTime(player);
}, 'Sanity test to check round-tripping assigning to new animation\'s ' +
'startTime');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.ready.then(t.step_func(function() {
checkStateOnReadyPromiseResolved(animation);
animation.startTime = startTimeForStartOfActiveInterval(animation.timeline);
return eventWatcher.waitForEvent('animationstart');
})).then(t.step_func(function() {
checkStateAtActiveIntervalStartTime(animation);
animation.startTime =
startTimeForFiftyPercentThroughActiveInterval(animation.timeline);
checkStateAtFiftyPctOfActiveInterval(animation);
animation.startTime = startTimeForEndOfActiveInterval(animation.timeline);
return eventWatcher.waitForEvent('animationend');
})).then(t.step_func(function() {
checkStateAtActiveIntervalEndTime(animation);
eventWatcher.stopWatching();
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'Skipping forward through animation');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.startTime = startTimeForEndOfActiveInterval(animation.timeline);
var previousTimelineTime = animation.timeline.currentTime;
// Skipping over the active interval will dispatch an 'animationstart' then
// an 'animationend' event. We need to wait for these events before we start
// testing going backwards since EventWatcher will fail the test if it gets
// an event that we haven't told it about.
eventWatcher.waitForEvents(['animationstart',
'animationend']).then(t.step_func(function() {
assert_true(document.timeline.currentTime - previousTimelineTime <
ANIM_DUR_MS,
'Sanity check that seeking worked rather than the events ' +
'firing after normal playback through the very long ' +
'animation duration');
// Now we can start the tests for skipping backwards, but first we check
// that after the events we're still in the same end time state:
checkStateAtActiveIntervalEndTime(animation);
animation.startTime =
startTimeForFiftyPercentThroughActiveInterval(animation.timeline);
// Despite going backwards from after the end of the animation (to being
// in the active interval), we now expect an 'animationstart' event
// because the animation should go from being inactive to active.
//
// Calling checkStateAtFiftyPctOfActiveInterval will check computed style,
// causing computed style to be updated and the 'animationstart' event to
// be dispatched synchronously. We need to call waitForEvent first
// otherwise eventWatcher will assert that the event was unexpected.
var promise = eventWatcher.waitForEvent('animationstart');
checkStateAtFiftyPctOfActiveInterval(animation);
return promise;
})).then(t.step_func(function() {
animation.startTime = startTimeForStartOfActiveInterval(animation.timeline);
checkStateAtActiveIntervalStartTime(animation);
animation.startTime = animation.timeline.currentTime;
// Despite going backwards from just after the active interval starts to
// the animation start time, we now expect an animationend event
// because we went from inside to outside the active interval.
return eventWatcher.waitForEvent('animationend');
})).then(t.step_func(function() {
checkStateOnReadyPromiseResolved(animation);
eventWatcher.stopWatching();
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
// This must come after we've set up the Promise chain, since requesting
// computed style will force events to be dispatched.
// XXX For some reason this fails occasionally (either the animation.playState
// check or the marginLeft check).
//checkStateAtActiveIntervalEndTime(animation);
}, 'Skipping backwards through animation');
// Next we have multiple tests to check that redundant startTime changes do NOT
// dispatch events. It's impossible to distinguish between events not being
// dispatched and events just taking an incredibly long time to dispatch
// without waiting an infinitely long time. Obviously we don't want to do that
// (block this test from finishing forever), so instead we just listen for
// events until two animation frames (i.e. requestAnimationFrame callbacks)
// have happened, then assume that no events will ever be dispatched for the
// redundant changes if no events were detected in that time.
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.startTime = startTimeForActivePhase(animation.timeline);
animation.startTime = startTimeForBeforePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
}, 'Redundant change, before -> active, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
animation.startTime = startTimeForAfterPhase(animation.timeline);
animation.startTime = startTimeForBeforePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
}, 'Redundant change, before -> after, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvent('animationstart').then(function() {
animation.startTime = startTimeForBeforePhase(animation.timeline);
animation.startTime = startTimeForActivePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.startTime = startTimeForActivePhase(animation.timeline);
}, 'Redundant change, active -> before, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvent('animationstart').then(function() {
animation.startTime = startTimeForAfterPhase(animation.timeline);
animation.startTime = startTimeForActivePhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.startTime = startTimeForActivePhase(animation.timeline);
}, 'Redundant change, active -> after, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvents(['animationstart',
'animationend']).then(function() {
animation.startTime = startTimeForBeforePhase(animation.timeline);
animation.startTime = startTimeForAfterPhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.startTime = startTimeForAfterPhase(animation.timeline);
}, 'Redundant change, after -> before, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
var eventWatcher = new EventWatcher(div, CSS_ANIM_EVENTS);
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
eventWatcher.waitForEvents(['animationstart',
'animationend']).then(function() {
animation.startTime = startTimeForActivePhase(animation.timeline);
animation.startTime = startTimeForAfterPhase(animation.timeline);
waitForAnimationFrames(2).then(function() {
eventWatcher.stopWatching();
t.done();
});
});
// get us into the initial state:
animation.startTime = startTimeForAfterPhase(animation.timeline);
}, 'Redundant change, after -> active, then back');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = ANIM_PROPERTY_VAL;
var animation = div.getAnimations()[0];
var storedCurrentTime;
animation.ready.then(t.step_func(function() {
storedCurrentTime = animation.currentTime;
animation.startTime = null;
return animation.ready;
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
assert_equals(animation.currentTime, storedCurrentTime,
'Test that hold time is correct');
t.done();
});
}, 'Setting startTime to null');
async_test(function(t) {
var div = addDiv(t, {'class': 'animated-div'});
div.style.animation = 'anim 100s';
var player = div.getAnimationPlayers()[0];
player.ready.then(t.step_func(function() {
var savedStartTime = player.startTime;
assert_not_equals(player.startTime, null,
'AnimationPlayer.startTime not null on ready Promise resolve');
player.pause();
// After bug 1109390 we will need to wait here for the ready promise again
assert_equals(player.startTime, null,
'AnimationPlayer.startTime is null after paused');
assert_equals(player.playState, 'paused',
'AnimationPlayer.playState is "paused" after pause() call');
})).catch(t.step_func(function(reason) {
assert_unreached(reason);
})).then(function() {
t.done();
});
}, 'AnimationPlayer.startTime after paused');
</script>
</body>
</html>
+23
View File
@@ -38,6 +38,29 @@ function waitForFrame() {
});
}
/**
* Returns a Promise that is resolved after the given number of consecutive
* animation frames have occured (using requestAnimationFrame callbacks).
*
* @param frameCount The number of animation frames.
* @param onFrame An optional function to be processed in each animation frame.
*/
function waitForAnimationFrames(frameCount, onFrame) {
return new Promise(function(resolve, reject) {
function handleFrame() {
if (--frameCount <= 0) {
resolve();
} else {
if (onFrame && typeof onFrame === 'function') {
onFrame();
}
window.requestAnimationFrame(handleFrame); // wait another frame
}
}
window.requestAnimationFrame(handleFrame);
});
}
/**
* Wrapper that takes a sequence of N animations and returns:
*
+28 -6
View File
@@ -948,9 +948,20 @@ NetworkStatsDB.prototype = {
let request = aStore.openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor){
data.push({ rxBytes: cursor.value.rxBytes,
txBytes: cursor.value.txBytes,
date: new Date(cursor.value.timestamp + offset) });
// We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes for
// the first (oldest) sample. The rx/txTotalBytes fields record
// accumulative usage amount, which means even if old samples were
// expired and removed from the Database, we can still obtain the
// correct network usage.
if (data.length == 0) {
data.push({ rxBytes: cursor.value.rxTotalBytes,
txBytes: cursor.value.txTotalBytes,
date: new Date(cursor.value.timestamp + offset) });
} else {
data.push({ rxBytes: cursor.value.rxBytes,
txBytes: cursor.value.txBytes,
date: new Date(cursor.value.timestamp + offset) });
}
cursor.continue();
return;
}
@@ -981,9 +992,20 @@ NetworkStatsDB.prototype = {
foundData.rxBytes += cursor.value.rxBytes;
foundData.txBytes += cursor.value.txBytes;
} else {
data.push({ rxBytes: cursor.value.rxBytes,
txBytes: cursor.value.txBytes,
date: new Date(cursor.value.timestamp + offset) });
// We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes
// for the first (oldest) sample. The rx/txTotalBytes fields
// record accumulative usage amount, which means even if old
// samples were expired and removed from the Database, we can
// still obtain the correct network usage.
if (data.length == 0) {
data.push({ rxBytes: cursor.value.rxTotalBytes,
txBytes: cursor.value.txTotalBytes,
date: new Date(cursor.value.timestamp + offset) });
} else {
data.push({ rxBytes: cursor.value.rxBytes,
txBytes: cursor.value.txBytes,
date: new Date(cursor.value.timestamp + offset) });
}
}
cursor.continue();
return;
@@ -797,22 +797,26 @@ add_test(function test_findBrowsingTrafficStats() {
stats.push({ appId: 1008, isInBrowser: 0,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 200, txBytes: 100});
rxBytes: 200, txBytes: 100,
rxTotalBytes: 200, txTotalBytes: 100});
// Browser of system app.
stats.push({ appId: 1008, isInBrowser: 1,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 1000, txBytes: 500});
rxBytes: 1000, txBytes: 500,
rxTotalBytes: 1000, txTotalBytes: 500});
// Another app.
stats.push({ appId: 1021, isInBrowser: 0,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 300, txBytes: 150});
rxBytes: 300, txBytes: 150,
rxTotalBytes: 300, txTotalBytes: 150});
// Browser of another app.
stats.push({ appId: 1021, isInBrowser: 1,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 600, txBytes: 300});
rxBytes: 600, txBytes: 300,
rxTotalBytes: 600, txTotalBytes: 300});
}
prepareFind(stats, function(error, result) {
@@ -853,22 +857,26 @@ add_test(function test_findAppTrafficStats() {
stats.push({ appId: 1008, isInBrowser: 0,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 200, txBytes: 100});
rxBytes: 200, txBytes: 100,
rxTotalBytes: 200, txTotalBytes: 100});
// Browser of system app.
stats.push({ appId: 1008, isInBrowser: 1,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 1000, txBytes: 500});
rxBytes: 1000, txBytes: 500,
rxTotalBytes: 1000, txTotalBytes: 500});
// Another app.
stats.push({ appId: 1021, isInBrowser: 0,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 300, txBytes: 150});
rxBytes: 300, txBytes: 150,
rxTotalBytes: 300, txTotalBytes: 150});
// Browser of another app.
stats.push({ appId: 1021, isInBrowser: 1,
serviceType: serviceType, network: networkMobile,
timestamp: saveDate + (sampleRate * i),
rxBytes: 600, txBytes: 300});
rxBytes: 600, txBytes: 300,
rxTotalBytes: 600, txTotalBytes: 300});
}
prepareFind(stats, function(error, result) {
@@ -4,9 +4,13 @@
"use strict";
const { utils: Cu } = Components;
var { utils: Cu } = Components;
Cu.import("resource://gre/modules/Timer.jsm", this);
Cu.import("resource://testing-common/PromiseTestUtils.jsm", this);
// Prevent test failures due to the unhandled rejections in this test file.
PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();
add_task(function* test_globals() {
Assert.equal(Promise.defer || undefined, undefined, "We are testing DOM Promise.");
+1 -1
View File
@@ -458,7 +458,7 @@ this.PushService = {
// Before completing the activation check prefs. This will first check
// connection.enabled pref and then check offline state.
this._changeStateConnectionEnabledEvent(prefs.get("connection.enabled"));
});
}).catch(Cu.reportError);
} else {
// This is only used for testing. Different tests require connecting to
+6 -3
View File
@@ -734,12 +734,15 @@ this.PushServiceHttp2 = {
.then(record => this._subscribeResource(record)
.then(recordNew => {
if (this._mainPushService) {
this._mainPushService.updateRegistrationAndNotifyApp(aSubscriptionUri,
recordNew);
this._mainPushService
.updateRegistrationAndNotifyApp(aSubscriptionUri, recordNew)
.catch(Cu.reportError);
}
}, error => {
if (this._mainPushService) {
this._mainPushService.dropRegistrationAndNotifyApp(aSubscriptionUri);
this._mainPushService
.dropRegistrationAndNotifyApp(aSubscriptionUri)
.catch(Cu.reportError);
}
})
);
@@ -4,6 +4,15 @@
'use strict';
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/PromiseTestUtils.jsm");
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
// Instances of the rejection "record is undefined" may or may not appear.
PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed();
const {PushDB, PushService, PushServiceHttp2} = serviceExports;
@@ -4,6 +4,15 @@
'use strict';
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://testing-common/PromiseTestUtils.jsm");
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
// Instances of the rejection "record is undefined" may or may not appear.
PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed();
const {PushDB, PushService, PushServiceHttp2} = serviceExports;
+11
View File
@@ -10,6 +10,17 @@ const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://testing-common/PromiseTestUtils.jsm");
///////////////////
//
// Whitelisting these tests.
// As part of bug 1077403, the shutdown crash should be fixed.
//
// These tests may crash intermittently on shutdown if the DOM Promise uncaught
// rejection observers are still registered when the watchdog operates.
PromiseTestUtils.thisTestLeaksUncaughtRejectionsAndShouldBeFixed();
var gPrefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
function setWatchdogEnabled(enabled) {
-16
View File
@@ -1,16 +0,0 @@
=======================
Firefox Services Module
=======================
The ``/services`` directory contains code for a variety of application
features that communicate with external services - hence its name.
It was originally created to hold code for Firefox Sync. Later, it
became the location for code written by the Mozilla Services Client team
and thus includes :ref:`healthreport`. This team no longer exists, but
the directory remains.
.. toctree::
:maxdepth: 1
metrics
-130
View File
@@ -1,130 +0,0 @@
.. _services_metrics:
============================
Metrics Collection Framework
============================
The ``services/metrics`` directory contains a generic data metrics
collecting and persisting framework for Gecko applications.
Overview
========
The Metrics framework by itself doesn't do much: it simply provides a
generic mechanism for collecting and persisting data. It is up to users
of this framework to drive collection and do something with the obtained
data. A consumer of this framework is :ref:`healthreport`.
Relationship to Telemetry
-------------------------
Telemetry provides similar features to code in this directory. The two
may be unified in the future.
Usage
=====
To use the code in this directory, import Metrics.jsm. e.g.
Components.utils.import("resource://gre/modules/Metrics.jsm");
This exports a *Metrics* object which holds references to the main JS
types and functions provided by this feature. Read below for what those
types are.
Metrics Types
=============
``Metrics.jsm`` exports a number of types. They are documented in the
sections below.
Metrics.Provider
----------------
``Metrics.Provider`` is an entity that collects and manages data. Providers
are typically domain-specific: if you need to collect a new type of data,
you create a ``Metrics.Provider`` type that does this.
Metrics.Measurement
-------------------
A ``Metrics.Measurement`` represents a collection of related pieces/fields
of data.
All data recorded by the metrics framework is modeled as
``Metrics.Measurement`` instances. Instances of ``Metrics.Measurement``
are essentially data structure descriptors.
Each ``Metrics.Measurement`` consists of a name and version to identify
itself (and its data) as well as a list of *fields* that this measurement
holds. A *field* is effectively an entry in a data structure. It consists
of a name and strongly enumerated type.
Metrics.Storage
---------------
This entity is responsible for persisting collected data and state.
It currently uses SQLite to store data, but this detail is abstracted away
in order to facilitate swapping of storage backends.
Metrics.ProviderManager
-----------------------
High-level entity coordinating activity among several ``Metrics.Provider``
instances.
Providers and Measurements
==========================
The most important types in this framework are ``Metrics.Provider`` and
``Metrics.Measurement``, henceforth known as ``Provider`` and
``Measurement``, respectively. As you will see, these two types go
hand in hand.
A ``Provider`` is an entity that *provides* data about a specific subsystem
or feature. They do this by recording data to specific ``Measurement``
types. Both ``Provider`` and ``Measurement`` are abstract base types.
A ``Measurement`` implementation defines a name and version. More
importantly, it also defines its storage requirements and how
previously-stored values are serialized.
Storage allocation is performed by communicating with the SQLite
backend. There is a startup function that tells SQLite what fields the
measurement is recording. The storage backend then registers these in
the database. Internally, this is creating a new primary key for
individual fields so later storage operations can directly reference
these primary keys in order to retrieve data without having to perform
complicated joins.
A ``Provider`` can be thought of as a collection of ``Measurement``
implementations. e.g. an Addons provider may consist of a measurement
for all *current* add-ons as well as a separate measurement for
historical counts of add-ons. A provider's primary role is to take
metrics data and write it to various measurements. This effectively
persists the data to SQLite.
Data is emitted from providers in either a push or pull based mechanism.
In push-based scenarios, the provider likely subscribes to external
events (e.g. observer notifications). An event of interest can occur at
any time. When it does, the provider immediately writes the event of
interest to storage or buffers it for eventual writing. In pull-based
scenarios, the provider is periodically queried and asked to populate
data.
SQLite Storage
==============
``Metrics.Storage`` provides an interface for persisting metrics data to a
SQLite database.
The storage API organizes values by fields. A field is a named member of
a ``Measurement`` that has specific type and retention characteristics.
Some example field types include:
* Last text value
* Last numeric value for a given day
* Discrete text values for a given day
See ``storage.jsm`` for more.
-38
View File
@@ -1,38 +0,0 @@
/* 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/. */
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = ["Metrics"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
#endif
// We concatenate the JSMs together to eliminate compartment overhead.
// This is a giant hack until compartment overhead is no longer an
// issue.
#define MERGED_COMPARTMENT
#include providermanager.jsm
;
#include dataprovider.jsm
;
#include storage.jsm
;
this.Metrics = {
ProviderManager: ProviderManager,
DailyValues: DailyValues,
Measurement: Measurement,
Provider: Provider,
Storage: MetricsStorageBackend,
dateToDays: dateToDays,
daysToDate: daysToDate,
};
-727
View File
@@ -1,727 +0,0 @@
/* 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/. */
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = [
"Measurement",
"Provider",
];
const {utils: Cu} = Components;
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
#endif
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
/**
* Represents a collection of related pieces/fields of data.
*
* This is an abstract base type.
*
* This type provides the primary interface for storing, retrieving, and
* serializing data.
*
* Each measurement consists of a set of named fields. Each field is primarily
* identified by a string name, which must be unique within the measurement.
*
* Each derived type must define the following properties:
*
* name -- String name of this measurement. This is the primary way
* measurements are distinguished within a provider.
*
* version -- Integer version of this measurement. This is a secondary
* identifier for a measurement within a provider. The version denotes
* the behavior of this measurement and the composition of its fields over
* time. When a new field is added or the behavior of an existing field
* changes, the version should be incremented. The initial version of a
* measurement is typically 1.
*
* fields -- Object defining the fields this measurement holds. Keys in the
* object are string field names. Values are objects describing how the
* field works. The following properties are recognized:
*
* type -- The string type of this field. This is typically one of the
* FIELD_* constants from the Metrics.Storage type.
*
*
* FUTURE: provide hook points for measurements to supplement with custom
* storage needs.
*/
this.Measurement = function () {
if (!this.name) {
throw new Error("Measurement must have a name.");
}
if (!this.version) {
throw new Error("Measurement must have a version.");
}
if (!Number.isInteger(this.version)) {
throw new Error("Measurement's version must be an integer: " + this.version);
}
if (!this.fields) {
throw new Error("Measurement must define fields.");
}
for (let [name, info] in Iterator(this.fields)) {
if (!info) {
throw new Error("Field does not contain metadata: " + name);
}
if (!info.type) {
throw new Error("Field is missing required type property: " + name);
}
}
this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name);
this.id = null;
this.storage = null;
this._fields = {};
this._serializers = {};
this._serializers[this.SERIALIZE_JSON] = {
singular: this._serializeJSONSingular.bind(this),
daily: this._serializeJSONDay.bind(this),
};
}
Measurement.prototype = Object.freeze({
SERIALIZE_JSON: "json",
/**
* Obtain a serializer for this measurement.
*
* Implementations should return an object with the following keys:
*
* singular -- Serializer for singular data.
* daily -- Serializer for daily data.
*
* Each item is a function that takes a single argument: the data to
* serialize. The passed data is a subset of that returned from
* this.getValues(). For "singular," data.singular is passed. For "daily",
* data.days.get(<day>) is passed.
*
* This function receives a single argument: the serialization format we
* are requesting. This is one of the SERIALIZE_* constants on this base type.
*
* For SERIALIZE_JSON, the function should return an object that
* JSON.stringify() knows how to handle. This could be an anonymous object or
* array or any object with a property named `toJSON` whose value is a
* function. The returned object will be added to a larger document
* containing the results of all `serialize` calls.
*
* The default implementation knows how to serialize built-in types using
* very simple logic. If small encoding size is a goal, the default
* implementation may not be suitable. If an unknown field type is
* encountered, the default implementation will error.
*
* @param format
* (string) A SERIALIZE_* constant defining what serialization format
* to use.
*/
serializer: function (format) {
if (!(format in this._serializers)) {
throw new Error("Don't know how to serialize format: " + format);
}
return this._serializers[format];
},
/**
* Whether this measurement contains the named field.
*
* @param name
* (string) Name of field.
*
* @return bool
*/
hasField: function (name) {
return name in this.fields;
},
/**
* The unique identifier for a named field.
*
* This will throw if the field is not known.
*
* @param name
* (string) Name of field.
*/
fieldID: function (name) {
let entry = this._fields[name];
if (!entry) {
throw new Error("Unknown field: " + name);
}
return entry[0];
},
fieldType: function (name) {
let entry = this._fields[name];
if (!entry) {
throw new Error("Unknown field: " + name);
}
return entry[1];
},
_configureStorage: function () {
let missing = [];
for (let [name, info] in Iterator(this.fields)) {
if (this.storage.hasFieldFromMeasurement(this.id, name)) {
this._fields[name] =
[this.storage.fieldIDFromMeasurement(this.id, name), info.type];
continue;
}
missing.push([name, info.type]);
}
if (!missing.length) {
return CommonUtils.laterTickResolvingPromise();
}
// We only perform a transaction if we have work to do (to avoid
// extra SQLite overhead).
return this.storage.enqueueTransaction(function registerFields() {
for (let [name, type] of missing) {
this._log.debug("Registering field: " + name + " " + type);
let id = yield this.storage.registerField(this.id, name, type);
this._fields[name] = [id, type];
}
}.bind(this));
},
//---------------------------------------------------------------------------
// Data Recording Functions
//
// Functions in this section are used to record new values against this
// measurement instance.
//
// Generally speaking, these functions will throw if the specified field does
// not exist or if the storage function requested is not appropriate for the
// type of that field. These functions will also return a promise that will
// be resolved when the underlying storage operation has completed.
//---------------------------------------------------------------------------
/**
* Increment a daily counter field in this measurement by 1.
*
* By default, the counter for the current day will be incremented.
*
* If the field is not known or is not a daily counter, this will throw.
*
*
*
* @param field
* (string) The name of the field whose value to increment.
* @param date
* (Date) Day on which to increment the counter.
* @param by
* (integer) How much to increment by.
* @return Promise<>
*/
incrementDailyCounter: function (field, date=new Date(), by=1) {
return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
date, by);
},
/**
* Record a new numeric value for a daily discrete numeric field.
*
* @param field
* (string) The name of the field to append a value to.
* @param value
* (Number) Number to append.
* @param date
* (Date) Day on which to append the value.
*
* @return Promise<>
*/
addDailyDiscreteNumeric: function (field, value, date=new Date()) {
return this.storage.addDailyDiscreteNumericFromFieldID(
this.fieldID(field), value, date);
},
/**
* Record a new text value for a daily discrete text field.
*
* This is like `addDailyDiscreteNumeric` but for daily discrete text fields.
*/
addDailyDiscreteText: function (field, value, date=new Date()) {
return this.storage.addDailyDiscreteTextFromFieldID(
this.fieldID(field), value, date);
},
/**
* Record the last seen value for a last numeric field.
*
* @param field
* (string) The name of the field to set the value of.
* @param value
* (Number) The value to set.
* @param date
* (Date) When this value was recorded.
*
* @return Promise<>
*/
setLastNumeric: function (field, value, date=new Date()) {
return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
date);
},
/**
* Record the last seen value for a last text field.
*
* This is like `setLastNumeric` except for last text fields.
*/
setLastText: function (field, value, date=new Date()) {
return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
date);
},
/**
* Record the most recent value for a daily last numeric field.
*
* @param field
* (string) The name of a daily last numeric field.
* @param value
* (Number) The value to set.
* @param date
* (Date) Day on which to record the last value.
*
* @return Promise<>
*/
setDailyLastNumeric: function (field, value, date=new Date()) {
return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
value, date);
},
/**
* Record the most recent value for a daily last text field.
*
* This is like `setDailyLastNumeric` except for a daily last text field.
*/
setDailyLastText: function (field, value, date=new Date()) {
return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
value, date);
},
//---------------------------------------------------------------------------
// End of data recording APIs.
//---------------------------------------------------------------------------
/**
* Obtain all values stored for this measurement.
*
* The default implementation obtains all known types from storage. If the
* measurement provides custom types or stores values somewhere other than
* storage, it should define its own implementation.
*
* This returns a promise that resolves to a data structure which is
* understood by the measurement's serialize() function.
*/
getValues: function () {
return this.storage.getMeasurementValues(this.id);
},
deleteLastNumeric: function (field) {
return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
},
deleteLastText: function (field) {
return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
},
/**
* This method is used by the default serializers to control whether a field
* is included in the output.
*
* There could be legacy fields in storage we no longer care about.
*
* This method is a hook to allow measurements to change this behavior, e.g.,
* to implement a dynamic fieldset.
*
* You will also need to override `fieldType`.
*
* @return (boolean) true if the specified field should be included in
* payload output.
*/
shouldIncludeField: function (field) {
return field in this._fields;
},
_serializeJSONSingular: function (data) {
let result = {"_v": this.version};
for (let [field, value] of data) {
// There could be legacy fields in storage we no longer care about.
if (!this.shouldIncludeField(field)) {
continue;
}
let type = this.fieldType(field);
switch (type) {
case this.storage.FIELD_LAST_NUMERIC:
case this.storage.FIELD_LAST_TEXT:
result[field] = value[1];
break;
case this.storage.FIELD_DAILY_COUNTER:
case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
case this.storage.FIELD_DAILY_DISCRETE_TEXT:
case this.storage.FIELD_DAILY_LAST_NUMERIC:
case this.storage.FIELD_DAILY_LAST_TEXT:
continue;
default:
throw new Error("Unknown field type: " + type);
}
}
return result;
},
_serializeJSONDay: function (data) {
let result = {"_v": this.version};
for (let [field, value] of data) {
if (!this.shouldIncludeField(field)) {
continue;
}
let type = this.fieldType(field);
switch (type) {
case this.storage.FIELD_DAILY_COUNTER:
case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
case this.storage.FIELD_DAILY_DISCRETE_TEXT:
case this.storage.FIELD_DAILY_LAST_NUMERIC:
case this.storage.FIELD_DAILY_LAST_TEXT:
result[field] = value;
break;
case this.storage.FIELD_LAST_NUMERIC:
case this.storage.FIELD_LAST_TEXT:
continue;
default:
throw new Error("Unknown field type: " + type);
}
}
return result;
},
});
/**
* An entity that emits data.
*
* A `Provider` consists of a string name (must be globally unique among all
* known providers) and a set of `Measurement` instances.
*
* The main role of a `Provider` is to produce metrics data and to store said
* data in the storage backend.
*
* Metrics data collection is initiated either by a manager calling a
* `collect*` function on `Provider` instances or by the `Provider` registering
* to some external event and then reacting whenever they occur.
*
* `Provider` implementations interface directly with a storage backend. For
* common stored values (daily counters, daily discrete values, etc),
* implementations should interface with storage via the various helper
* functions on the `Measurement` instances. For custom stored value types,
* implementations will interact directly with the low-level storage APIs.
*
* Because multiple providers exist and could be responding to separate
* external events simultaneously and because not all operations performed by
* storage can safely be performed in parallel, writing directly to storage at
* event time is dangerous. Therefore, interactions with storage must be
* deferred until it is safe to perform them.
*
* This typically looks something like:
*
* // This gets called when an external event worthy of recording metrics
* // occurs. The function receives a numeric value associated with the event.
* function onExternalEvent (value) {
* let now = new Date();
* let m = this.getMeasurement("foo", 1);
*
* this.enqueueStorageOperation(function storeExternalEvent() {
*
* // We interface with storage via the `Measurement` helper functions.
* // These each return a promise that will be resolved when the
* // operation finishes. We rely on behavior of storage where operations
* // are executed single threaded and sequentially. Therefore, we only
* // need to return the final promise.
* m.incrementDailyCounter("foo", now);
* return m.addDailyDiscreteNumericValue("my_value", value, now);
* }.bind(this));
*
* }
*
*
* `Provider` is an abstract base class. Implementations must define a few
* properties:
*
* name
* The `name` property should be a string defining the provider's name. The
* name must be globally unique for the application. The name is used as an
* identifier to distinguish providers from each other.
*
* measurementTypes
* This must be an array of `Measurement`-derived types. Note that elements
* in the array are the type functions, not instances. Instances of the
* `Measurement` are created at run-time by the `Provider` and are bound
* to the provider and to a specific storage backend.
*/
this.Provider = function () {
if (!this.name) {
throw new Error("Provider must define a name.");
}
if (!Array.isArray(this.measurementTypes)) {
throw new Error("Provider must define measurement types.");
}
this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name);
this.measurements = null;
this.storage = null;
}
Provider.prototype = Object.freeze({
/**
* Whether the provider only pulls data from other sources.
*
* If this is true, the provider pulls data from other sources. By contrast,
* "push-based" providers subscribe to foreign sources and record/react to
* external events as they happen.
*
* Pull-only providers likely aren't instantiated until a data collection
* is performed. Thus, implementations cannot rely on a provider instance
* always being alive. This is an optimization so provider instances aren't
* dead weight while the application is running.
*
* This must be set on the prototype to have an effect.
*/
pullOnly: false,
/**
* Obtain a `Measurement` from its name and version.
*
* If the measurement is not found, an Error is thrown.
*/
getMeasurement: function (name, version) {
if (!Number.isInteger(version)) {
throw new Error("getMeasurement expects an integer version. Got: " + version);
}
let m = this.measurements.get([name, version].join(":"));
if (!m) {
throw new Error("Unknown measurement: " + name + " v" + version);
}
return m;
},
init: function (storage) {
if (this.storage !== null) {
throw new Error("Provider() not called. Did the sub-type forget to call it?");
}
if (this.storage) {
throw new Error("Provider has already been initialized.");
}
this.measurements = new Map();
this.storage = storage;
let self = this;
return Task.spawn(function init() {
let pre = self.preInit();
if (!pre || typeof(pre.then) != "function") {
throw new Error("preInit() does not return a promise.");
}
yield pre;
for (let measurementType of self.measurementTypes) {
let measurement = new measurementType();
measurement.provider = self;
measurement.storage = self.storage;
let id = yield storage.registerMeasurement(self.name, measurement.name,
measurement.version);
measurement.id = id;
yield measurement._configureStorage();
self.measurements.set([measurement.name, measurement.version].join(":"),
measurement);
}
let post = self.postInit();
if (!post || typeof(post.then) != "function") {
throw new Error("postInit() does not return a promise.");
}
yield post;
});
},
shutdown: function () {
let promise = this.onShutdown();
if (!promise || typeof(promise.then) != "function") {
throw new Error("onShutdown implementation does not return a promise.");
}
return promise;
},
/**
* Hook point for implementations to perform pre-initialization activity.
*
* This method will be called before measurement registration.
*
* Implementations should return a promise which is resolved when
* initialization activities have completed.
*/
preInit: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Hook point for implementations to perform post-initialization activity.
*
* This method will be called after `preInit` and measurement registration,
* but before initialization is finished.
*
* If a `Provider` instance needs to register observers, etc, it should
* implement this function.
*
* Implementations should return a promise which is resolved when
* initialization activities have completed.
*/
postInit: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Hook point for shutdown of instances.
*
* This is the opposite of `onInit`. If a `Provider` needs to unregister
* observers, etc, this is where it should do it.
*
* Implementations should return a promise which is resolved when
* shutdown activities have completed.
*/
onShutdown: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Collects data that doesn't change during the application's lifetime.
*
* Implementations should return a promise that resolves when all data has
* been collected and storage operations have been finished.
*
* @return Promise<>
*/
collectConstantData: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Collects data approximately every day.
*
* For long-running applications, this is called approximately every day.
* It may or may not be called every time the application is run. It also may
* be called more than once per day.
*
* Implementations should return a promise that resolves when all data has
* been collected and storage operations have completed.
*
* @return Promise<>
*/
collectDailyData: function () {
return CommonUtils.laterTickResolvingPromise();
},
/**
* Queue a deferred storage operation.
*
* Deferred storage operations are the preferred method for providers to
* interact with storage. When collected data is to be added to storage,
* the provider creates a function that performs the necessary storage
* interactions and then passes that function to this function. Pending
* storage operations will be executed sequentially by a coordinator.
*
* The passed function should return a promise which will be resolved upon
* completion of storage interaction.
*/
enqueueStorageOperation: function (func) {
return this.storage.enqueueOperation(func);
},
/**
* Obtain persisted provider state.
*
* Provider state consists of key-value pairs of string names and values.
* Providers can stuff whatever they want into state. They are encouraged to
* store as little as possible for performance reasons.
*
* State is backed by storage and is robust.
*
* These functions do not enqueue on storage automatically, so they should
* be guarded by `enqueueStorageOperation` or some other mutex.
*
* @param key
* (string) The property to retrieve.
*
* @return Promise<string|null> String value on success. null if no state
* is available under this key.
*/
getState: function (key) {
return this.storage.getProviderState(this.name, key);
},
/**
* Set state for this provider.
*
* This is the complementary API for `getState` and obeys the same
* storage restrictions.
*/
setState: function (key, value) {
return this.storage.setProviderState(this.name, key, value);
},
_dateToDays: function (date) {
return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
},
_daysToDate: function (days) {
return new Date(days * MILLISECONDS_PER_DAY);
},
});
-154
View File
@@ -1,154 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = [
"DummyMeasurement",
"DummyProvider",
"DummyConstantProvider",
"DummyPullOnlyThrowsOnInitProvider",
"DummyThrowOnInitProvider",
"DummyThrowOnShutdownProvider",
];
const {utils: Cu} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Task.jsm");
this.DummyMeasurement = function DummyMeasurement(name="DummyMeasurement") {
this.name = name;
Metrics.Measurement.call(this);
}
DummyMeasurement.prototype = {
__proto__: Metrics.Measurement.prototype,
version: 1,
fields: {
"daily-counter": {type: Metrics.Storage.FIELD_DAILY_COUNTER},
"daily-discrete-numeric": {type: Metrics.Storage.FIELD_DAILY_DISCRETE_NUMERIC},
"daily-discrete-text": {type: Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT},
"daily-last-numeric": {type: Metrics.Storage.FIELD_DAILY_LAST_NUMERIC},
"daily-last-text": {type: Metrics.Storage.FIELD_DAILY_LAST_TEXT},
"last-numeric": {type: Metrics.Storage.FIELD_LAST_NUMERIC},
"last-text": {type: Metrics.Storage.FIELD_LAST_TEXT},
},
};
this.DummyProvider = function DummyProvider(name="DummyProvider") {
Object.defineProperty(this, "name", {
value: name,
});
this.measurementTypes = [DummyMeasurement];
Metrics.Provider.call(this);
this.constantMeasurementName = "DummyMeasurement";
this.collectConstantCount = 0;
this.throwDuringCollectConstantData = null;
this.throwDuringConstantPopulate = null;
this.collectDailyCount = 0;
this.havePushedMeasurements = true;
}
DummyProvider.prototype = {
__proto__: Metrics.Provider.prototype,
name: "DummyProvider",
collectConstantData: function () {
this.collectConstantCount++;
if (this.throwDuringCollectConstantData) {
throw new Error(this.throwDuringCollectConstantData);
}
return this.enqueueStorageOperation(function doStorage() {
if (this.throwDuringConstantPopulate) {
throw new Error(this.throwDuringConstantPopulate);
}
let m = this.getMeasurement("DummyMeasurement", 1);
let now = new Date();
m.incrementDailyCounter("daily-counter", now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 1, now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 2, now);
m.addDailyDiscreteText("daily-discrete-text", "foo", now);
m.addDailyDiscreteText("daily-discrete-text", "bar", now);
m.setDailyLastNumeric("daily-last-numeric", 3, now);
m.setDailyLastText("daily-last-text", "biz", now);
m.setLastNumeric("last-numeric", 4, now);
return m.setLastText("last-text", "bazfoo", now);
}.bind(this));
},
collectDailyData: function () {
this.collectDailyCount++;
return Promise.resolve();
},
};
this.DummyConstantProvider = function () {
DummyProvider.call(this, this.name);
}
DummyConstantProvider.prototype = {
__proto__: DummyProvider.prototype,
name: "DummyConstantProvider",
pullOnly: true,
};
this.DummyThrowOnInitProvider = function () {
DummyProvider.call(this, "DummyThrowOnInitProvider");
throw new Error("Dummy Error");
};
this.DummyThrowOnInitProvider.prototype = {
__proto__: DummyProvider.prototype,
name: "DummyThrowOnInitProvider",
};
this.DummyPullOnlyThrowsOnInitProvider = function () {
DummyConstantProvider.call(this);
throw new Error("Dummy Error");
};
this.DummyPullOnlyThrowsOnInitProvider.prototype = {
__proto__: DummyConstantProvider.prototype,
name: "DummyPullOnlyThrowsOnInitProvider",
};
this.DummyThrowOnShutdownProvider = function () {
DummyProvider.call(this, "DummyThrowOnShutdownProvider");
};
this.DummyThrowOnShutdownProvider.prototype = {
__proto__: DummyProvider.prototype,
name: "DummyThrowOnShutdownProvider",
pullOnly: true,
onShutdown: function () {
throw new Error("Dummy shutdown error");
},
};
-23
View File
@@ -1,23 +0,0 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
# We install Metrics.jsm into the "main" JSM repository and the rest in
# services. External consumers should only go through Metrics.jsm.
EXTRA_PP_JS_MODULES += [
'Metrics.jsm',
]
EXTRA_PP_JS_MODULES.services.metrics += [
'dataprovider.jsm',
'providermanager.jsm',
'storage.jsm',
]
TESTING_JS_MODULES.services.metrics += [
'modules-testing/mocks.jsm',
]
-562
View File
@@ -1,562 +0,0 @@
/* 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/. */
#ifndef MERGED_COMPARTMENT
"use strict";
this.EXPORTED_SYMBOLS = ["ProviderManager"];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
#endif
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://services-common/utils.js");
/**
* Handles and coordinates the collection of metrics data from providers.
*
* This provides an interface for managing `Metrics.Provider` instances. It
* provides APIs for bulk collection of data.
*/
this.ProviderManager = function (storage) {
this._log = Log.repository.getLogger("Services.Metrics.ProviderManager");
this._providers = new Map();
this._storage = storage;
this._providerInitQueue = [];
this._providerInitializing = false;
this._pullOnlyProviders = {};
this._pullOnlyProvidersRegisterCount = 0;
this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED;
this._pullOnlyProvidersCurrentPromise = null;
// Callback to allow customization of providers after they are constructed
// but before they call out into their initialization code.
this.onProviderInit = null;
}
this.ProviderManager.prototype = Object.freeze({
PULL_ONLY_NOT_REGISTERED: "none",
PULL_ONLY_REGISTERING: "registering",
PULL_ONLY_UNREGISTERING: "unregistering",
PULL_ONLY_REGISTERED: "registered",
get providers() {
let providers = [];
for (let [name, entry] of this._providers) {
providers.push(entry.provider);
}
return providers;
},
/**
* Obtain a provider from its name.
*/
getProvider: function (name) {
let provider = this._providers.get(name);
if (!provider) {
return null;
}
return provider.provider;
},
/**
* Registers providers from a category manager category.
*
* This examines the specified category entries and registers found
* providers.
*
* Category entries are essentially JS modules and the name of the symbol
* within that module that is a `Metrics.Provider` instance.
*
* The category entry name is the name of the JS type for the provider. The
* value is the resource:// URI to import which makes this type available.
*
* Example entry:
*
* FooProvider resource://gre/modules/foo.jsm
*
* One can register entries in the application's .manifest file. e.g.
*
* category healthreport-js-provider-default FooProvider resource://gre/modules/foo.jsm
* category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm
*
* Then to load them:
*
* let reporter = getHealthReporter("healthreport.");
* reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default");
*
* If the category has no defined members, this call has no effect, and no error is raised.
*
* @param category
* (string) Name of category from which to query and load.
* @param providerDiagnostic
* (function) Optional, called with the name of the provider currently being initialized.
* @return a newly spawned Task.
*/
registerProvidersFromCategoryManager: function (category, providerDiagnostic) {
this._log.info("Registering providers from category: " + category);
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
let promiseList = [];
let enumerator = cm.enumerateCategory(category);
while (enumerator.hasMoreElements()) {
let entry = enumerator.getNext()
.QueryInterface(Ci.nsISupportsCString)
.toString();
let uri = cm.getCategoryEntry(category, entry);
this._log.info("Attempting to load provider from category manager: " +
entry + " from " + uri);
try {
let ns = {};
Cu.import(uri, ns);
let promise = this.registerProviderFromType(ns[entry]);
if (promise) {
promiseList.push({name: entry, promise: promise});
}
} catch (ex) {
this._recordProviderError(entry,
"Error registering provider from category manager",
ex);
continue;
}
}
return Task.spawn(function* wait() {
for (let entry of promiseList) {
if (providerDiagnostic) {
providerDiagnostic(entry.name);
}
yield entry.promise;
}
});
},
/**
* Registers a `MetricsProvider` with this manager.
*
* Once a `MetricsProvider` is registered, data will be collected from it
* whenever we collect data.
*
* The returned value is a promise that will be resolved once registration
* is complete.
*
* Providers are initialized as part of registration by calling
* provider.init().
*
* @param provider
* (Metrics.Provider) The provider instance to register.
*
* @return Promise<null>
*/
registerProvider: function (provider) {
// We should perform an instanceof check here. However, due to merged
// compartments, the Provider type may belong to one of two JSMs
// isinstance gets confused depending on which module Provider comes
// from. Some code references Provider from dataprovider.jsm; others from
// Metrics.jsm.
if (!provider.name) {
throw new Error("Provider is not valid: does not have a name.");
}
if (this._providers.has(provider.name)) {
return CommonUtils.laterTickResolvingPromise();
}
let deferred = Promise.defer();
this._providerInitQueue.push([provider, deferred]);
if (this._providerInitQueue.length == 1) {
this._popAndInitProvider();
}
return deferred.promise;
},
/**
* Registers a provider from its constructor function.
*
* If the provider is pull-only, it will be stashed away and
* initialized later. Null will be returned.
*
* If it is not pull-only, it will be initialized immediately and a
* promise will be returned. The promise will be resolved when the
* provider has finished initializing.
*/
registerProviderFromType: function (type) {
let proto = type.prototype;
if (proto.pullOnly) {
this._log.info("Provider is pull-only. Deferring initialization: " +
proto.name);
this._pullOnlyProviders[proto.name] = type;
return null;
}
let provider = this._initProviderFromType(type);
return this.registerProvider(provider);
},
/**
* Initializes a provider from its type.
*
* This is how a constructor function should be turned into a provider
* instance.
*
* A side-effect is the provider is registered with the manager.
*/
_initProviderFromType: function (type) {
let provider = new type();
if (this.onProviderInit) {
this.onProviderInit(provider);
}
return provider;
},
/**
* Remove a named provider from the manager.
*
* It is the caller's responsibility to shut down the provider
* instance.
*/
unregisterProvider: function (name) {
this._providers.delete(name);
},
/**
* Ensure that pull-only providers are registered.
*/
ensurePullOnlyProvidersRegistered: function () {
let state = this._pullOnlyProvidersState;
this._pullOnlyProvidersRegisterCount++;
if (state == this.PULL_ONLY_REGISTERED) {
this._log.debug("Requested pull-only provider registration and " +
"providers are already registered.");
return CommonUtils.laterTickResolvingPromise();
}
// If we're in the process of registering, chain off that request.
if (state == this.PULL_ONLY_REGISTERING) {
this._log.debug("Requested pull-only provider registration and " +
"registration is already in progress.");
return this._pullOnlyProvidersCurrentPromise;
}
this._log.debug("Pull-only provider registration requested.");
// A side-effect of setting this is that an active unregistration will
// effectively short circuit and finish as soon as the in-flight
// unregistration (if any) finishes.
this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERING;
let inFlightPromise = this._pullOnlyProvidersCurrentPromise;
this._pullOnlyProvidersCurrentPromise =
Task.spawn(function registerPullProviders() {
if (inFlightPromise) {
this._log.debug("Waiting for in-flight pull-only provider activity " +
"to finish before registering.");
try {
yield inFlightPromise;
} catch (ex) {
this._log.warn("Error when waiting for existing pull-only promise: " +
CommonUtils.exceptionStr(ex));
}
}
for (let name in this._pullOnlyProviders) {
let providerType = this._pullOnlyProviders[name];
// Short-circuit if we're no longer registering.
if (this._pullOnlyProvidersState != this.PULL_ONLY_REGISTERING) {
this._log.debug("Aborting pull-only provider registration.");
break;
}
try {
let provider = this._initProviderFromType(providerType);
// This is a no-op if the provider is already registered. So, the
// only overhead is constructing an instance. This should be cheap
// and isn't worth optimizing.
yield this.registerProvider(provider);
} catch (ex) {
this._recordProviderError(providerType.prototype.name,
"Error registering pull-only provider",
ex);
}
}
// It's possible we changed state while registering. Only mark as
// registered if we didn't change state.
if (this._pullOnlyProvidersState == this.PULL_ONLY_REGISTERING) {
this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERED;
this._pullOnlyProvidersCurrentPromise = null;
}
}.bind(this));
return this._pullOnlyProvidersCurrentPromise;
},
ensurePullOnlyProvidersUnregistered: function () {
let state = this._pullOnlyProvidersState;
// If we're not registered, this is a no-op.
if (state == this.PULL_ONLY_NOT_REGISTERED) {
this._log.debug("Requested pull-only provider unregistration but none " +
"are registered.");
return CommonUtils.laterTickResolvingPromise();
}
// If we're currently unregistering, recycle the promise from last time.
if (state == this.PULL_ONLY_UNREGISTERING) {
this._log.debug("Requested pull-only provider unregistration and " +
"unregistration is in progress.");
this._pullOnlyProvidersRegisterCount =
Math.max(0, this._pullOnlyProvidersRegisterCount - 1);
return this._pullOnlyProvidersCurrentPromise;
}
// We ignore this request while multiple entities have requested
// registration because we don't want a request from an "inner,"
// short-lived request to overwrite the desire of the "parent,"
// longer-lived request.
if (this._pullOnlyProvidersRegisterCount > 1) {
this._log.debug("Requested pull-only provider unregistration while " +
"other callers still want them registered. Ignoring.");
this._pullOnlyProvidersRegisterCount--;
return CommonUtils.laterTickResolvingPromise();
}
// We are either fully registered or registering with a single consumer.
// In both cases we are authoritative and can commence unregistration.
this._log.debug("Pull-only providers being unregistered.");
this._pullOnlyProvidersRegisterCount =
Math.max(0, this._pullOnlyProvidersRegisterCount - 1);
this._pullOnlyProvidersState = this.PULL_ONLY_UNREGISTERING;
let inFlightPromise = this._pullOnlyProvidersCurrentPromise;
this._pullOnlyProvidersCurrentPromise =
Task.spawn(function unregisterPullProviders() {
if (inFlightPromise) {
this._log.debug("Waiting for in-flight pull-only provider activity " +
"to complete before unregistering.");
try {
yield inFlightPromise;
} catch (ex) {
this._log.warn("Error when waiting for existing pull-only promise: " +
CommonUtils.exceptionStr(ex));
}
}
for (let provider of this.providers) {
if (this._pullOnlyProvidersState != this.PULL_ONLY_UNREGISTERING) {
return;
}
if (!provider.pullOnly) {
continue;
}
this._log.info("Shutting down pull-only provider: " +
provider.name);
try {
yield provider.shutdown();
} catch (ex) {
this._recordProviderError(provider.name,
"Error when shutting down provider",
ex);
} finally {
this.unregisterProvider(provider.name);
}
}
if (this._pullOnlyProvidersState == this.PULL_ONLY_UNREGISTERING) {
this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED;
this._pullOnlyProvidersCurrentPromise = null;
}
}.bind(this));
return this._pullOnlyProvidersCurrentPromise;
},
_popAndInitProvider: function () {
if (!this._providerInitQueue.length || this._providerInitializing) {
return;
}
let [provider, deferred] = this._providerInitQueue.shift();
this._providerInitializing = true;
this._log.info("Initializing provider with storage: " + provider.name);
Task.spawn(function initProvider() {
try {
let result = yield provider.init(this._storage);
this._log.info("Provider successfully initialized: " + provider.name);
this._providers.set(provider.name, {
provider: provider,
constantsCollected: false,
});
deferred.resolve(result);
} catch (ex) {
this._recordProviderError(provider.name, "Failed to initialize", ex);
deferred.reject(ex);
} finally {
this._providerInitializing = false;
this._popAndInitProvider();
}
}.bind(this));
},
/**
* Collects all constant measurements from all providers.
*
* Returns a Promise that will be fulfilled once all data providers have
* provided their constant data. A side-effect of this promise fulfillment
* is that the manager is populated with the obtained collection results.
* The resolved value to the promise is this `ProviderManager` instance.
*
* @param providerDiagnostic
* (function) Optional, called with the name of the provider currently being initialized.
*/
collectConstantData: function (providerDiagnostic=null) {
let entries = [];
for (let [name, entry] of this._providers) {
if (entry.constantsCollected) {
this._log.trace("Provider has already provided constant data: " +
name);
continue;
}
entries.push(entry);
}
let onCollect = function (entry, result) {
entry.constantsCollected = true;
};
return this._callCollectOnProviders(entries, "collectConstantData",
onCollect, providerDiagnostic);
},
/**
* Calls collectDailyData on all providers.
*/
collectDailyData: function (providerDiagnostic=null) {
return this._callCollectOnProviders(this._providers.values(),
"collectDailyData",
null,
providerDiagnostic);
},
_callCollectOnProviders: function (entries, fnProperty, onCollect=null, providerDiagnostic=null) {
let promises = [];
for (let entry of entries) {
let provider = entry.provider;
let collectPromise;
try {
collectPromise = provider[fnProperty].call(provider);
} catch (ex) {
this._recordProviderError(provider.name, "Exception when calling " +
"collect function: " + fnProperty, ex);
continue;
}
if (!collectPromise) {
this._recordProviderError(provider.name, "Does not return a promise " +
"from " + fnProperty + "()");
continue;
}
let promise = collectPromise.then(function onCollected(result) {
if (onCollect) {
try {
onCollect(entry, result);
} catch (ex) {
this._log.warn("onCollect callback threw: " +
CommonUtils.exceptionStr(ex));
}
}
return CommonUtils.laterTickResolvingPromise(result);
});
promises.push([provider.name, promise]);
}
return this._handleCollectionPromises(promises, providerDiagnostic);
},
/**
* Handles promises returned by the collect* functions.
*
* This consumes the data resolved by the promises and returns a new promise
* that will be resolved once all promises have been resolved.
*
* The promise is resolved even if one of the underlying collection
* promises is rejected.
*/
_handleCollectionPromises: function (promises, providerDiagnostic=null) {
return Task.spawn(function waitForPromises() {
for (let [name, promise] of promises) {
if (providerDiagnostic) {
providerDiagnostic(name);
}
try {
yield promise;
this._log.debug("Provider collected successfully: " + name);
} catch (ex) {
this._recordProviderError(name, "Failed to collect", ex);
}
}
throw new Task.Result(this);
}.bind(this));
},
/**
* Record an error that occurred operating on a provider.
*/
_recordProviderError: function (name, msg, ex) {
msg = "Provider error: " + name + ": " + msg;
if (ex) {
msg += ": " + CommonUtils.exceptionStr(ex);
}
this._log.warn(msg);
if (this.onProviderError) {
try {
this.onProviderError(msg);
} catch (callError) {
this._log.warn("Exception when calling onProviderError callback: " +
CommonUtils.exceptionStr(callError));
}
}
},
});
File diff suppressed because it is too large Load Diff
-15
View File
@@ -1,15 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
(function initMetricsTestingInfrastructure() {
do_get_profile();
let ns = {};
Components.utils.import("resource://testing-common/services/common/logging.js",
ns);
ns.initTestLogging("Trace");
}).call(this);
@@ -1,31 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const modules = [
"dataprovider.jsm",
"providermanager.jsm",
"storage.jsm",
];
const test_modules = [
"mocks.jsm",
];
function run_test() {
for (let m of modules) {
let resource = "resource://gre/modules/services/metrics/" + m;
Components.utils.import(resource, {});
}
Components.utils.import("resource://gre/modules/Metrics.jsm", {});
for (let m of test_modules) {
let resource = "resource://testing-common/services/metrics/" + m;
Components.utils.import(resource, {});
}
Components.utils.import("resource://gre/modules/Metrics.jsm", {});
}
@@ -1,297 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://gre/modules/Preferences.jsm");
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://testing-common/services/metrics/mocks.jsm");
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
function getProvider(storageName) {
return Task.spawn(function () {
let provider = new DummyProvider();
let storage = yield Metrics.Storage(storageName);
yield provider.init(storage);
throw new Task.Result(provider);
});
}
function run_test() {
run_next_test();
};
add_test(function test_constructor() {
let failed = false;
try {
new Metrics.Provider();
} catch(ex) {
do_check_true(ex.message.startsWith("Provider must define a name"));
failed = true;
}
finally {
do_check_true(failed);
}
run_next_test();
});
add_task(function test_init() {
let provider = new DummyProvider();
let storage = yield Metrics.Storage("init");
yield provider.init(storage);
let m = provider.getMeasurement("DummyMeasurement", 1);
do_check_true(m instanceof Metrics.Measurement);
do_check_eq(m.id, 1);
do_check_eq(Object.keys(m._fields).length, 7);
do_check_true(m.hasField("daily-counter"));
do_check_false(m.hasField("does-not-exist"));
yield storage.close();
});
add_task(function test_default_collectors() {
let provider = new DummyProvider();
let storage = yield Metrics.Storage("default_collectors");
yield provider.init(storage);
for (let property in Metrics.Provider.prototype) {
if (!property.startsWith("collect")) {
continue;
}
let result = provider[property]();
do_check_neq(result, null);
do_check_eq(typeof(result.then), "function");
}
yield storage.close();
});
add_task(function test_measurement_storage_basic() {
let provider = yield getProvider("measurement_storage_basic");
let m = provider.getMeasurement("DummyMeasurement", 1);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
// Daily counter.
let counterID = m.fieldID("daily-counter");
yield m.incrementDailyCounter("daily-counter", now);
yield m.incrementDailyCounter("daily-counter", now);
yield m.incrementDailyCounter("daily-counter", yesterday);
let count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, now);
do_check_eq(count, 2);
count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, yesterday);
do_check_eq(count, 1);
yield m.incrementDailyCounter("daily-counter", now, 4);
count = yield provider.storage.getDailyCounterCountFromFieldID(counterID, now);
do_check_eq(count, 6);
// Daily discrete numeric.
let dailyDiscreteNumericID = m.fieldID("daily-discrete-numeric");
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 5, now);
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 6, now);
yield m.addDailyDiscreteNumeric("daily-discrete-numeric", 7, yesterday);
let values = yield provider.storage.getDailyDiscreteNumericFromFieldID(
dailyDiscreteNumericID, now);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
let actual = values.getDay(now);
do_check_eq(actual.length, 2);
do_check_eq(actual[0], 5);
do_check_eq(actual[1], 6);
values = yield provider.storage.getDailyDiscreteNumericFromFieldID(
dailyDiscreteNumericID, yesterday);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(yesterday));
do_check_eq(values.getDay(yesterday)[0], 7);
// Daily discrete text.
let dailyDiscreteTextID = m.fieldID("daily-discrete-text");
yield m.addDailyDiscreteText("daily-discrete-text", "foo", now);
yield m.addDailyDiscreteText("daily-discrete-text", "bar", now);
yield m.addDailyDiscreteText("daily-discrete-text", "biz", yesterday);
values = yield provider.storage.getDailyDiscreteTextFromFieldID(
dailyDiscreteTextID, now);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
actual = values.getDay(now);
do_check_eq(actual.length, 2);
do_check_eq(actual[0], "foo");
do_check_eq(actual[1], "bar");
values = yield provider.storage.getDailyDiscreteTextFromFieldID(
dailyDiscreteTextID, yesterday);
do_check_true(values.hasDay(yesterday));
do_check_eq(values.getDay(yesterday)[0], "biz");
// Daily last numeric.
let lastDailyNumericID = m.fieldID("daily-last-numeric");
yield m.setDailyLastNumeric("daily-last-numeric", 5, now);
yield m.setDailyLastNumeric("daily-last-numeric", 6, yesterday);
let result = yield provider.storage.getDailyLastNumericFromFieldID(
lastDailyNumericID, now);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(now));
do_check_eq(result.getDay(now), 5);
result = yield provider.storage.getDailyLastNumericFromFieldID(
lastDailyNumericID, yesterday);
do_check_true(result.hasDay(yesterday));
do_check_eq(result.getDay(yesterday), 6);
yield m.setDailyLastNumeric("daily-last-numeric", 7, now);
result = yield provider.storage.getDailyLastNumericFromFieldID(
lastDailyNumericID, now);
do_check_eq(result.getDay(now), 7);
// Daily last text.
let lastDailyTextID = m.fieldID("daily-last-text");
yield m.setDailyLastText("daily-last-text", "foo", now);
yield m.setDailyLastText("daily-last-text", "bar", yesterday);
result = yield provider.storage.getDailyLastTextFromFieldID(
lastDailyTextID, now);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(now));
do_check_eq(result.getDay(now), "foo");
result = yield provider.storage.getDailyLastTextFromFieldID(
lastDailyTextID, yesterday);
do_check_true(result.hasDay(yesterday));
do_check_eq(result.getDay(yesterday), "bar");
yield m.setDailyLastText("daily-last-text", "biz", now);
result = yield provider.storage.getDailyLastTextFromFieldID(
lastDailyTextID, now);
do_check_eq(result.getDay(now), "biz");
// Last numeric.
let lastNumericID = m.fieldID("last-numeric");
yield m.setLastNumeric("last-numeric", 1, now);
result = yield provider.storage.getLastNumericFromFieldID(lastNumericID);
do_check_eq(result[1], 1);
do_check_true(result[0].getTime() < now.getTime());
do_check_true(result[0].getTime() > yesterday.getTime());
yield m.setLastNumeric("last-numeric", 2, now);
result = yield provider.storage.getLastNumericFromFieldID(lastNumericID);
do_check_eq(result[1], 2);
// Last text.
let lastTextID = m.fieldID("last-text");
yield m.setLastText("last-text", "foo", now);
result = yield provider.storage.getLastTextFromFieldID(lastTextID);
do_check_eq(result[1], "foo");
do_check_true(result[0].getTime() < now.getTime());
do_check_true(result[0].getTime() > yesterday.getTime());
yield m.setLastText("last-text", "bar", now);
result = yield provider.storage.getLastTextFromFieldID(lastTextID);
do_check_eq(result[1], "bar");
yield provider.storage.close();
});
add_task(function test_serialize_json_default() {
let provider = yield getProvider("serialize_json_default");
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
let m = provider.getMeasurement("DummyMeasurement", 1);
m.incrementDailyCounter("daily-counter", now);
m.incrementDailyCounter("daily-counter", now);
m.incrementDailyCounter("daily-counter", yesterday);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 1, now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 2, now);
m.addDailyDiscreteNumeric("daily-discrete-numeric", 3, yesterday);
m.addDailyDiscreteText("daily-discrete-text", "foo", now);
m.addDailyDiscreteText("daily-discrete-text", "bar", now);
m.addDailyDiscreteText("daily-discrete-text", "baz", yesterday);
m.setDailyLastNumeric("daily-last-numeric", 4, now);
m.setDailyLastNumeric("daily-last-numeric", 5, yesterday);
m.setDailyLastText("daily-last-text", "apple", now);
m.setDailyLastText("daily-last-text", "orange", yesterday);
m.setLastNumeric("last-numeric", 6, now);
yield m.setLastText("last-text", "hello", now);
let data = yield provider.storage.getMeasurementValues(m.id);
let serializer = m.serializer(m.SERIALIZE_JSON);
let formatted = serializer.singular(data.singular);
do_check_eq(Object.keys(formatted).length, 3); // Our keys + _v.
do_check_true("last-numeric" in formatted);
do_check_true("last-text" in formatted);
do_check_eq(formatted["last-numeric"], 6);
do_check_eq(formatted["last-text"], "hello");
do_check_eq(formatted["_v"], 1);
formatted = serializer.daily(data.days.getDay(now));
do_check_eq(Object.keys(formatted).length, 6); // Our keys + _v.
do_check_eq(formatted["daily-counter"], 2);
do_check_eq(formatted["_v"], 1);
do_check_true(Array.isArray(formatted["daily-discrete-numeric"]));
do_check_eq(formatted["daily-discrete-numeric"].length, 2);
do_check_eq(formatted["daily-discrete-numeric"][0], 1);
do_check_eq(formatted["daily-discrete-numeric"][1], 2);
do_check_true(Array.isArray(formatted["daily-discrete-text"]));
do_check_eq(formatted["daily-discrete-text"].length, 2);
do_check_eq(formatted["daily-discrete-text"][0], "foo");
do_check_eq(formatted["daily-discrete-text"][1], "bar");
do_check_eq(formatted["daily-last-numeric"], 4);
do_check_eq(formatted["daily-last-text"], "apple");
formatted = serializer.daily(data.days.getDay(yesterday));
do_check_eq(formatted["daily-last-numeric"], 5);
do_check_eq(formatted["daily-last-text"], "orange");
// Now let's turn off a field so that it's present in the DB
// but not present in the output.
let called = false;
let excluded = "daily-last-numeric";
Object.defineProperty(m, "shouldIncludeField", {
value: function fakeShouldIncludeField(field) {
called = true;
return field != excluded;
},
});
let limited = serializer.daily(data.days.getDay(yesterday));
do_check_true(called);
do_check_false(excluded in limited);
do_check_eq(formatted["daily-last-text"], "orange");
yield provider.storage.close();
});
@@ -1,357 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://testing-common/services/metrics/mocks.jsm");
const PULL_ONLY_TESTING_CATEGORY = "testing-only-pull-only-providers";
function run_test() {
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
cm.addCategoryEntry(PULL_ONLY_TESTING_CATEGORY, "DummyProvider",
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
cm.addCategoryEntry(PULL_ONLY_TESTING_CATEGORY, "DummyConstantProvider",
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
run_next_test();
};
add_task(function test_constructor() {
let storage = yield Metrics.Storage("constructor");
let manager = new Metrics.ProviderManager(storage);
yield storage.close();
});
add_task(function test_register_provider() {
let storage = yield Metrics.Storage("register_provider");
let manager = new Metrics.ProviderManager(storage);
let dummy = new DummyProvider();
yield manager.registerProvider(dummy);
do_check_eq(manager._providers.size, 1);
yield manager.registerProvider(dummy);
do_check_eq(manager._providers.size, 1);
do_check_eq(manager.getProvider(dummy.name), dummy);
let failed = false;
try {
manager.registerProvider({});
} catch (ex) {
do_check_true(ex.message.startsWith("Provider is not valid"));
failed = true;
} finally {
do_check_true(failed);
failed = false;
}
manager.unregisterProvider(dummy.name);
do_check_eq(manager._providers.size, 0);
do_check_null(manager.getProvider(dummy.name));
yield storage.close();
});
add_task(function test_register_providers_from_category_manager() {
const category = "metrics-providers-js-modules";
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
cm.addCategoryEntry(category, "DummyProvider",
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
let storage = yield Metrics.Storage("register_providers_from_category_manager");
let manager = new Metrics.ProviderManager(storage);
try {
do_check_eq(manager._providers.size, 0);
yield manager.registerProvidersFromCategoryManager(category);
do_check_eq(manager._providers.size, 1);
} finally {
yield storage.close();
}
});
add_task(function test_collect_constant_data() {
let storage = yield Metrics.Storage("collect_constant_data");
let errorCount = 0;
let manager= new Metrics.ProviderManager(storage);
manager.onProviderError = function () { errorCount++; }
let provider = new DummyProvider();
yield manager.registerProvider(provider);
do_check_eq(provider.collectConstantCount, 0);
yield manager.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
do_check_true(manager._providers.get("DummyProvider").constantsCollected);
yield storage.close();
do_check_eq(errorCount, 0);
});
add_task(function test_collect_constant_throws() {
let storage = yield Metrics.Storage("collect_constant_throws");
let manager = new Metrics.ProviderManager(storage);
let errors = [];
manager.onProviderError = function (error) { errors.push(error); };
let provider = new DummyProvider();
provider.throwDuringCollectConstantData = "Fake error during collect";
yield manager.registerProvider(provider);
yield manager.collectConstantData();
do_check_eq(errors.length, 1);
do_check_true(errors[0].includes(provider.throwDuringCollectConstantData));
yield storage.close();
});
add_task(function test_collect_constant_populate_throws() {
let storage = yield Metrics.Storage("collect_constant_populate_throws");
let manager = new Metrics.ProviderManager(storage);
let errors = [];
manager.onProviderError = function (error) { errors.push(error); };
let provider = new DummyProvider();
provider.throwDuringConstantPopulate = "Fake error during constant populate";
yield manager.registerProvider(provider);
yield manager.collectConstantData();
do_check_eq(errors.length, 1);
do_check_true(errors[0].includes(provider.throwDuringConstantPopulate));
do_check_false(manager._providers.get(provider.name).constantsCollected);
yield storage.close();
});
add_task(function test_collect_constant_onetime() {
let storage = yield Metrics.Storage("collect_constant_onetime");
let manager = new Metrics.ProviderManager(storage);
let provider = new DummyProvider();
yield manager.registerProvider(provider);
yield manager.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
yield manager.collectConstantData();
do_check_eq(provider.collectConstantCount, 1);
yield storage.close();
});
add_task(function test_collect_multiple() {
let storage = yield Metrics.Storage("collect_multiple");
let manager = new Metrics.ProviderManager(storage);
for (let i = 0; i < 10; i++) {
yield manager.registerProvider(new DummyProvider("provider" + i));
}
do_check_eq(manager._providers.size, 10);
yield manager.collectConstantData();
yield storage.close();
});
add_task(function test_collect_daily() {
let storage = yield Metrics.Storage("collect_daily");
let manager = new Metrics.ProviderManager(storage);
let provider1 = new DummyProvider("DP1");
let provider2 = new DummyProvider("DP2");
yield manager.registerProvider(provider1);
yield manager.registerProvider(provider2);
yield manager.collectDailyData();
do_check_eq(provider1.collectDailyCount, 1);
do_check_eq(provider2.collectDailyCount, 1);
yield manager.collectDailyData();
do_check_eq(provider1.collectDailyCount, 2);
do_check_eq(provider2.collectDailyCount, 2);
yield storage.close();
});
add_task(function test_pull_only_not_initialized() {
let storage = yield Metrics.Storage("pull_only_not_initialized");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
do_check_eq(manager.providers.length, 1);
do_check_eq(manager.providers[0].name, "DummyProvider");
yield storage.close();
});
add_task(function test_pull_only_registration() {
let storage = yield Metrics.Storage("pull_only_registration");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
do_check_eq(manager.providers.length, 1);
// Simple registration and unregistration.
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
do_check_neq(manager.getProvider("DummyConstantProvider"), null);
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
do_check_null(manager.getProvider("DummyConstantProvider"));
// Multiple calls to register work.
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
// Unregister with 2 requests for registration should not unregister.
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 2);
// But the 2nd one will.
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
yield storage.close();
});
add_task(function test_pull_only_register_while_registering() {
let storage = yield Metrics.Storage("pull_only_register_will_registering");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
manager.ensurePullOnlyProvidersRegistered();
manager.ensurePullOnlyProvidersRegistered();
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
manager.ensurePullOnlyProvidersUnregistered();
manager.ensurePullOnlyProvidersUnregistered();
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
yield storage.close();
});
add_task(function test_pull_only_unregister_while_registering() {
let storage = yield Metrics.Storage("pull_only_unregister_while_registering");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
manager.ensurePullOnlyProvidersRegistered();
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(manager.providers.length, 1);
yield storage.close();
});
add_task(function test_pull_only_register_while_unregistering() {
let storage = yield Metrics.Storage("pull_only_register_while_unregistering");
let manager = new Metrics.ProviderManager(storage);
yield manager.registerProvidersFromCategoryManager(PULL_ONLY_TESTING_CATEGORY);
yield manager.ensurePullOnlyProvidersRegistered();
manager.ensurePullOnlyProvidersUnregistered();
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(manager.providers.length, 2);
yield storage.close();
});
// Re-use database for perf reasons.
const REGISTRATION_ERRORS_DB = "registration_errors";
add_task(function test_category_manager_registration_error() {
let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
let manager = new Metrics.ProviderManager(storage);
let cm = Cc["@mozilla.org/categorymanager;1"]
.getService(Ci.nsICategoryManager);
cm.addCategoryEntry("registration-errors", "DummyThrowOnInitProvider",
"resource://testing-common/services/metrics/mocks.jsm",
false, true);
let deferred = Promise.defer();
let errorCount = 0;
manager.onProviderError = function (msg) {
errorCount++;
deferred.resolve(msg);
};
yield manager.registerProvidersFromCategoryManager("registration-errors");
do_check_eq(manager.providers.length, 0);
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.includes("Provider error: DummyThrowOnInitProvider: "
+ "Error registering provider from category manager: "
+ "Error: Dummy Error"));
yield storage.close();
});
add_task(function test_pull_only_registration_error() {
let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
let manager = new Metrics.ProviderManager(storage);
let deferred = Promise.defer();
let errorCount = 0;
manager.onProviderError = function (msg) {
errorCount++;
deferred.resolve(msg);
};
yield manager.registerProviderFromType(DummyPullOnlyThrowsOnInitProvider);
do_check_eq(errorCount, 0);
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.includes("Provider error: DummyPullOnlyThrowsOnInitProvider: " +
"Error registering pull-only provider: Error: Dummy Error"));
yield storage.close();
});
add_task(function test_error_during_shutdown() {
let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
let manager = new Metrics.ProviderManager(storage);
let deferred = Promise.defer();
let errorCount = 0;
manager.onProviderError = function (msg) {
errorCount++;
deferred.resolve(msg);
};
yield manager.registerProviderFromType(DummyThrowOnShutdownProvider);
yield manager.registerProviderFromType(DummyProvider);
do_check_eq(errorCount, 0);
do_check_eq(manager.providers.length, 1);
yield manager.ensurePullOnlyProvidersRegistered();
do_check_eq(errorCount, 0);
yield manager.ensurePullOnlyProvidersUnregistered();
do_check_eq(errorCount, 1);
let msg = yield deferred.promise;
do_check_true(msg.includes("Provider error: DummyThrowOnShutdownProvider: " +
"Error when shutting down provider: Error: Dummy shutdown error"));
yield storage.close();
});
@@ -1,839 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var {utils: Cu} = Components;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/Metrics.jsm");
Cu.import("resource://services-common/utils.js");
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
function run_test() {
run_next_test();
}
add_test(function test_days_date_conversion() {
let toDays = Metrics.dateToDays;
let toDate = Metrics.daysToDate;
let d = new Date(0);
do_check_eq(toDays(d), 0);
d = new Date(MILLISECONDS_PER_DAY);
do_check_eq(toDays(d), 1);
d = new Date(MILLISECONDS_PER_DAY - 1);
do_check_eq(toDays(d), 0);
d = new Date("1970-12-31T23:59:59.999Z");
do_check_eq(toDays(d), 364);
d = new Date("1971-01-01T00:00:00Z");
do_check_eq(toDays(d), 365);
d = toDate(0);
do_check_eq(d.getTime(), 0);
d = toDate(1);
do_check_eq(d.getTime(), MILLISECONDS_PER_DAY);
d = toDate(365);
do_check_eq(d.getUTCFullYear(), 1971);
do_check_eq(d.getUTCMonth(), 0);
do_check_eq(d.getUTCDate(), 1);
do_check_eq(d.getUTCHours(), 0);
do_check_eq(d.getUTCMinutes(), 0);
do_check_eq(d.getUTCSeconds(), 0);
do_check_eq(d.getUTCMilliseconds(), 0);
run_next_test();
});
add_task(function test_get_sqlite_backend() {
let backend = yield Metrics.Storage("get_sqlite_backend.sqlite");
do_check_neq(backend._connection, null);
// Ensure WAL and auto checkpoint are enabled.
do_check_neq(backend._enabledWALCheckpointPages, null);
let rows = yield backend._connection.execute("PRAGMA journal_mode");
do_check_eq(rows[0].getResultByIndex(0), "wal");
rows = yield backend._connection.execute("PRAGMA wal_autocheckpoint");
do_check_eq(rows[0].getResultByIndex(0), backend._enabledWALCheckpointPages);
yield backend.close();
do_check_null(backend._connection);
});
add_task(function test_reconnect() {
let backend = yield Metrics.Storage("reconnect");
yield backend.close();
let backend2 = yield Metrics.Storage("reconnect");
yield backend2.close();
});
add_task(function test_future_schema_errors() {
let backend = yield Metrics.Storage("future_schema_errors");
yield backend._connection.setSchemaVersion(2);
yield backend.close();
let backend2;
let failed = false;
try {
backend2 = yield Metrics.Storage("future_schema_errors");
} catch (ex) {
failed = true;
do_check_true(ex.message.startsWith("Unknown database schema"));
}
do_check_null(backend2);
do_check_true(failed);
});
add_task(function test_checkpoint_apis() {
let backend = yield Metrics.Storage("checkpoint_apis");
let c = backend._connection;
let count = c._connectionData._statementCounter;
yield backend.setAutoCheckpoint(0);
do_check_eq(c._connectionData._statementCounter, count + 1);
let rows = yield c.execute("PRAGMA wal_autocheckpoint");
do_check_eq(rows[0].getResultByIndex(0), 0);
count = c._connectionData._statementCounter;
yield backend.setAutoCheckpoint(1);
do_check_eq(c._connectionData._statementCounter, count + 1);
rows = yield c.execute("PRAGMA wal_autocheckpoint");
do_check_eq(rows[0].getResultByIndex(0), backend._enabledWALCheckpointPages);
count = c._connectionData._statementCounter;
yield backend.checkpoint();
do_check_eq(c._connectionData._statementCounter, count + 1);
yield backend.checkpoint();
do_check_eq(c._connectionData._statementCounter, count + 2);
yield backend.close();
});
add_task(function test_measurement_registration() {
let backend = yield Metrics.Storage("measurement_registration");
do_check_false(backend.hasProvider("foo"));
do_check_false(backend.hasMeasurement("foo", "bar", 1));
let id = yield backend.registerMeasurement("foo", "bar", 1);
do_check_eq(id, 1);
do_check_true(backend.hasProvider("foo"));
do_check_true(backend.hasMeasurement("foo", "bar", 1));
do_check_eq(backend.measurementID("foo", "bar", 1), id);
do_check_false(backend.hasMeasurement("foo", "bar", 2));
let id2 = yield backend.registerMeasurement("foo", "bar", 2);
do_check_eq(id2, 2);
do_check_true(backend.hasMeasurement("foo", "bar", 2));
do_check_eq(backend.measurementID("foo", "bar", 2), id2);
yield backend.close();
});
add_task(function test_field_registration_basic() {
let backend = yield Metrics.Storage("field_registration_basic");
do_check_false(backend.hasField("foo", "bar", 1, "baz"));
let mID = yield backend.registerMeasurement("foo", "bar", 1);
do_check_false(backend.hasField("foo", "bar", 1, "baz"));
do_check_false(backend.hasFieldFromMeasurement(mID, "baz"));
let bazID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
do_check_true(backend.hasField("foo", "bar", 1, "baz"));
do_check_true(backend.hasFieldFromMeasurement(mID, "baz"));
let bar2ID = yield backend.registerMeasurement("foo", "bar2", 1);
yield backend.registerField(bar2ID, "baz",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
do_check_true(backend.hasField("foo", "bar2", 1, "baz"));
yield backend.close();
});
// Ensure changes types of fields results in fatal error.
add_task(function test_field_registration_changed_type() {
let backend = yield Metrics.Storage("field_registration_changed_type");
let mID = yield backend.registerMeasurement("bar", "bar", 1);
let id = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let caught = false;
try {
yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
} catch (ex) {
caught = true;
do_check_true(ex.message.startsWith("Field already defined with different type"));
}
do_check_true(caught);
yield backend.close();
});
add_task(function test_field_registration_repopulation() {
let backend = yield Metrics.Storage("field_registration_repopulation");
let mID1 = yield backend.registerMeasurement("foo", "bar", 1);
let mID2 = yield backend.registerMeasurement("foo", "bar", 2);
let mID3 = yield backend.registerMeasurement("foo", "biz", 1);
let mID4 = yield backend.registerMeasurement("baz", "foo", 1);
let fID1 = yield backend.registerField(mID1, "foo", backend.FIELD_DAILY_COUNTER);
let fID2 = yield backend.registerField(mID1, "bar", backend.FIELD_DAILY_DISCRETE_NUMERIC);
let fID3 = yield backend.registerField(mID4, "foo", backend.FIELD_LAST_TEXT);
yield backend.close();
backend = yield Metrics.Storage("field_registration_repopulation");
do_check_true(backend.hasProvider("foo"));
do_check_true(backend.hasProvider("baz"));
do_check_true(backend.hasMeasurement("foo", "bar", 1));
do_check_eq(backend.measurementID("foo", "bar", 1), mID1);
do_check_true(backend.hasMeasurement("foo", "bar", 2));
do_check_eq(backend.measurementID("foo", "bar", 2), mID2);
do_check_true(backend.hasMeasurement("foo", "biz", 1));
do_check_eq(backend.measurementID("foo", "biz", 1), mID3);
do_check_true(backend.hasMeasurement("baz", "foo", 1));
do_check_eq(backend.measurementID("baz", "foo", 1), mID4);
do_check_true(backend.hasField("foo", "bar", 1, "foo"));
do_check_eq(backend.fieldID("foo", "bar", 1, "foo"), fID1);
do_check_true(backend.hasField("foo", "bar", 1, "bar"));
do_check_eq(backend.fieldID("foo", "bar", 1, "bar"), fID2);
do_check_true(backend.hasField("baz", "foo", 1, "foo"));
do_check_eq(backend.fieldID("baz", "foo", 1, "foo"), fID3);
yield backend.close();
});
add_task(function test_enqueue_operation_execution_order() {
let backend = yield Metrics.Storage("enqueue_operation_execution_order");
let executionCount = 0;
let fns = {
op1: function () {
do_check_eq(executionCount, 1);
},
op2: function () {
do_check_eq(executionCount, 2);
},
op3: function () {
do_check_eq(executionCount, 3);
},
};
function enqueuedOperation(fn) {
let deferred = Promise.defer();
CommonUtils.nextTick(function onNextTick() {
executionCount++;
fn();
deferred.resolve();
});
return deferred.promise;
}
let promises = [];
for (let i = 1; i <= 3; i++) {
let fn = fns["op" + i];
promises.push(backend.enqueueOperation(enqueuedOperation.bind(this, fn)));
}
for (let promise of promises) {
yield promise;
}
yield backend.close();
});
add_task(function test_enqueue_operation_many() {
let backend = yield Metrics.Storage("enqueue_operation_many");
let promises = [];
for (let i = 0; i < 100; i++) {
promises.push(backend.registerMeasurement("foo", "bar" + i, 1));
}
for (let promise of promises) {
yield promise;
}
yield backend.close();
});
// If the operation did not return a promise, everything should still execute.
add_task(function test_enqueue_operation_no_return_promise() {
let backend = yield Metrics.Storage("enqueue_operation_no_return_promise");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
let promises = [];
for (let i = 0; i < 10; i++) {
promises.push(backend.enqueueOperation(function op() {
backend.incrementDailyCounterFromFieldID(fID, now);
}));
}
let deferred = Promise.defer();
let finished = 0;
for (let promise of promises) {
promise.then(
do_throw.bind(this, "Unexpected resolve."),
function onError() {
finished++;
if (finished == promises.length) {
backend.getDailyCounterCountFromFieldID(fID, now).then(function onCount(count) {
// There should not be a race condition here because storage
// serializes all statements. So, for the getDailyCounterCount
// query to finish means that all counter update statements must
// have completed.
do_check_eq(count, promises.length);
deferred.resolve();
});
}
}
);
}
yield deferred.promise;
yield backend.close();
});
// If an operation throws, subsequent operations should still execute.
add_task(function test_enqueue_operation_throw_exception() {
let backend = yield Metrics.Storage("enqueue_operation_rejected_promise");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
let deferred = Promise.defer();
backend.enqueueOperation(function bad() {
throw new Error("I failed.");
}).then(do_throw, function onError(error) {
do_check_true(error.message.includes("I failed."));
deferred.resolve();
});
let promise = backend.enqueueOperation(function () {
return backend.incrementDailyCounterFromFieldID(fID, now);
});
yield deferred.promise;
yield promise;
let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 1);
yield backend.close();
});
// If an operation rejects, subsequent operations should still execute.
add_task(function test_enqueue_operation_reject_promise() {
let backend = yield Metrics.Storage("enqueue_operation_reject_promise");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
let deferred = Promise.defer();
backend.enqueueOperation(function reject() {
let d = Promise.defer();
CommonUtils.nextTick(function nextTick() {
d.reject("I failed.");
});
return d.promise;
}).then(do_throw, function onError(error) {
deferred.resolve();
});
let promise = backend.enqueueOperation(function () {
return backend.incrementDailyCounterFromFieldID(fID, now);
});
yield deferred.promise;
yield promise;
let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 1);
yield backend.close();
});
add_task(function test_enqueue_transaction() {
let backend = yield Metrics.Storage("enqueue_transaction");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fID = yield backend.registerField(mID, "baz", backend.FIELD_DAILY_COUNTER);
let now = new Date();
yield backend.incrementDailyCounterFromFieldID(fID, now);
yield backend.enqueueTransaction(function transaction() {
yield backend.incrementDailyCounterFromFieldID(fID, now);
});
let count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 2);
let errored = false;
try {
yield backend.enqueueTransaction(function aborted() {
yield backend.incrementDailyCounterFromFieldID(fID, now);
throw new Error("Some error.");
});
} catch (ex) {
errored = true;
} finally {
do_check_true(errored);
}
count = yield backend.getDailyCounterCountFromFieldID(fID, now);
do_check_eq(count, 2);
yield backend.close();
});
add_task(function test_increment_daily_counter_basic() {
let backend = yield Metrics.Storage("increment_daily_counter_basic");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fieldID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let now = new Date();
yield backend.incrementDailyCounterFromFieldID(fieldID, now);
let count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
do_check_eq(count, 1);
yield backend.incrementDailyCounterFromFieldID(fieldID, now);
count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
do_check_eq(count, 2);
yield backend.incrementDailyCounterFromFieldID(fieldID, now, 10);
count = yield backend.getDailyCounterCountFromFieldID(fieldID, now);
do_check_eq(count, 12);
yield backend.close();
});
add_task(function test_increment_daily_counter_multiple_days() {
let backend = yield Metrics.Storage("increment_daily_counter_multiple_days");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let fieldID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let days = [];
let now = Date.now();
for (let i = 0; i < 100; i++) {
days.push(new Date(now - i * MILLISECONDS_PER_DAY));
}
for (let day of days) {
yield backend.incrementDailyCounterFromFieldID(fieldID, day);
}
let result = yield backend.getDailyCounterCountsFromFieldID(fieldID);
do_check_eq(result.size, 100);
for (let day of days) {
do_check_true(result.hasDay(day));
do_check_eq(result.getDay(day), 1);
}
let fields = yield backend.getMeasurementDailyCountersFromMeasurementID(mID);
do_check_eq(fields.size, 1);
do_check_true(fields.has("baz"));
do_check_eq(fields.get("baz").size, 100);
for (let day of days) {
do_check_true(fields.get("baz").hasDay(day));
do_check_eq(fields.get("baz").getDay(day), 1);
}
yield backend.close();
});
add_task(function test_last_values() {
let backend = yield Metrics.Storage("set_last");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let numberID = yield backend.registerField(mID, "number",
backend.FIELD_LAST_NUMERIC);
let textID = yield backend.registerField(mID, "text",
backend.FIELD_LAST_TEXT);
let now = new Date();
let nowDay = new Date(Math.floor(now.getTime() / MILLISECONDS_PER_DAY) * MILLISECONDS_PER_DAY);
yield backend.setLastNumericFromFieldID(numberID, 42, now);
yield backend.setLastTextFromFieldID(textID, "hello world", now);
let result = yield backend.getLastNumericFromFieldID(numberID);
do_check_true(Array.isArray(result));
do_check_eq(result[0].getTime(), nowDay.getTime());
do_check_eq(typeof(result[1]), "number");
do_check_eq(result[1], 42);
result = yield backend.getLastTextFromFieldID(textID);
do_check_true(Array.isArray(result));
do_check_eq(result[0].getTime(), nowDay.getTime());
do_check_eq(typeof(result[1]), "string");
do_check_eq(result[1], "hello world");
let missingID = yield backend.registerField(mID, "missing",
backend.FIELD_LAST_NUMERIC);
do_check_null(yield backend.getLastNumericFromFieldID(missingID));
let fields = yield backend.getMeasurementLastValuesFromMeasurementID(mID);
do_check_eq(fields.size, 2);
do_check_true(fields.has("number"));
do_check_true(fields.has("text"));
do_check_eq(fields.get("number")[1], 42);
do_check_eq(fields.get("text")[1], "hello world");
yield backend.close();
});
add_task(function test_discrete_values_basic() {
let backend = yield Metrics.Storage("discrete_values_basic");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let numericID = yield backend.registerField(mID, "numeric",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
let textID = yield backend.registerField(mID, "text",
backend.FIELD_DAILY_DISCRETE_TEXT);
let now = new Date();
let expectedNumeric = [];
let expectedText = [];
for (let i = 0; i < 100; i++) {
expectedNumeric.push(i);
expectedText.push("value" + i);
yield backend.addDailyDiscreteNumericFromFieldID(numericID, i, now);
yield backend.addDailyDiscreteTextFromFieldID(textID, "value" + i, now);
}
let values = yield backend.getDailyDiscreteNumericFromFieldID(numericID);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
do_check_true(Array.isArray(values.getDay(now)));
do_check_eq(values.getDay(now).length, expectedNumeric.length);
for (let i = 0; i < expectedNumeric.length; i++) {
do_check_eq(values.getDay(now)[i], expectedNumeric[i]);
}
values = yield backend.getDailyDiscreteTextFromFieldID(textID);
do_check_eq(values.size, 1);
do_check_true(values.hasDay(now));
do_check_true(Array.isArray(values.getDay(now)));
do_check_eq(values.getDay(now).length, expectedText.length);
for (let i = 0; i < expectedText.length; i++) {
do_check_eq(values.getDay(now)[i], expectedText[i]);
}
let fields = yield backend.getMeasurementDailyDiscreteValuesFromMeasurementID(mID);
do_check_eq(fields.size, 2);
do_check_true(fields.has("numeric"));
do_check_true(fields.has("text"));
let numeric = fields.get("numeric");
let text = fields.get("text");
do_check_true(numeric.hasDay(now));
do_check_true(text.hasDay(now));
do_check_eq(numeric.getDay(now).length, expectedNumeric.length);
do_check_eq(text.getDay(now).length, expectedText.length);
for (let i = 0; i < expectedNumeric.length; i++) {
do_check_eq(numeric.getDay(now)[i], expectedNumeric[i]);
}
for (let i = 0; i < expectedText.length; i++) {
do_check_eq(text.getDay(now)[i], expectedText[i]);
}
yield backend.close();
});
add_task(function test_discrete_values_multiple_days() {
let backend = yield Metrics.Storage("discrete_values_multiple_days");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let id = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_DISCRETE_NUMERIC);
let now = new Date();
let dates = [];
for (let i = 0; i < 50; i++) {
let date = new Date(now.getTime() + i * MILLISECONDS_PER_DAY);
dates.push(date);
yield backend.addDailyDiscreteNumericFromFieldID(id, i, date);
}
let values = yield backend.getDailyDiscreteNumericFromFieldID(id);
do_check_eq(values.size, 50);
let i = 0;
for (let date of dates) {
do_check_true(values.hasDay(date));
do_check_eq(values.getDay(date)[0], i);
i++;
}
let fields = yield backend.getMeasurementDailyDiscreteValuesFromMeasurementID(mID);
do_check_eq(fields.size, 1);
do_check_true(fields.has("baz"));
let baz = fields.get("baz");
do_check_eq(baz.size, 50);
i = 0;
for (let date of dates) {
do_check_true(baz.hasDay(date));
do_check_eq(baz.getDay(date).length, 1);
do_check_eq(baz.getDay(date)[0], i);
i++;
}
yield backend.close();
});
add_task(function test_daily_last_values() {
let backend = yield Metrics.Storage("daily_last_values");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let numericID = yield backend.registerField(mID, "numeric",
backend.FIELD_DAILY_LAST_NUMERIC);
let textID = yield backend.registerField(mID, "text",
backend.FIELD_DAILY_LAST_TEXT);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
let dayBefore = new Date(yesterday.getTime() - MILLISECONDS_PER_DAY);
yield backend.setDailyLastNumericFromFieldID(numericID, 1, yesterday);
yield backend.setDailyLastNumericFromFieldID(numericID, 2, now);
yield backend.setDailyLastNumericFromFieldID(numericID, 3, dayBefore);
yield backend.setDailyLastTextFromFieldID(textID, "foo", now);
yield backend.setDailyLastTextFromFieldID(textID, "bar", yesterday);
yield backend.setDailyLastTextFromFieldID(textID, "baz", dayBefore);
let days = yield backend.getDailyLastNumericFromFieldID(numericID);
do_check_eq(days.size, 3);
do_check_eq(days.getDay(yesterday), 1);
do_check_eq(days.getDay(now), 2);
do_check_eq(days.getDay(dayBefore), 3);
days = yield backend.getDailyLastTextFromFieldID(textID);
do_check_eq(days.size, 3);
do_check_eq(days.getDay(now), "foo");
do_check_eq(days.getDay(yesterday), "bar");
do_check_eq(days.getDay(dayBefore), "baz");
yield backend.setDailyLastNumericFromFieldID(numericID, 4, yesterday);
days = yield backend.getDailyLastNumericFromFieldID(numericID);
do_check_eq(days.getDay(yesterday), 4);
yield backend.setDailyLastTextFromFieldID(textID, "biz", yesterday);
days = yield backend.getDailyLastTextFromFieldID(textID);
do_check_eq(days.getDay(yesterday), "biz");
days = yield backend.getDailyLastNumericFromFieldID(numericID, yesterday);
do_check_eq(days.size, 1);
do_check_eq(days.getDay(yesterday), 4);
days = yield backend.getDailyLastTextFromFieldID(textID, yesterday);
do_check_eq(days.size, 1);
do_check_eq(days.getDay(yesterday), "biz");
let fields = yield backend.getMeasurementDailyLastValuesFromMeasurementID(mID);
do_check_eq(fields.size, 2);
do_check_true(fields.has("numeric"));
do_check_true(fields.has("text"));
let numeric = fields.get("numeric");
let text = fields.get("text");
do_check_true(numeric.hasDay(yesterday));
do_check_true(numeric.hasDay(dayBefore));
do_check_true(numeric.hasDay(now));
do_check_true(text.hasDay(yesterday));
do_check_true(text.hasDay(dayBefore));
do_check_true(text.hasDay(now));
do_check_eq(numeric.getDay(yesterday), 4);
do_check_eq(text.getDay(yesterday), "biz");
yield backend.close();
});
add_task(function test_prune_data_before() {
let backend = yield Metrics.Storage("prune_data_before");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let counterID = yield backend.registerField(mID, "baz",
backend.FIELD_DAILY_COUNTER);
let text1ID = yield backend.registerField(mID, "one_text_1",
backend.FIELD_LAST_TEXT);
let text2ID = yield backend.registerField(mID, "one_text_2",
backend.FIELD_LAST_TEXT);
let numeric1ID = yield backend.registerField(mID, "one_numeric_1",
backend.FIELD_LAST_NUMERIC);
let numeric2ID = yield backend.registerField(mID, "one_numeric_2",
backend.FIELD_LAST_NUMERIC);
let text3ID = yield backend.registerField(mID, "daily_last_text_1",
backend.FIELD_DAILY_LAST_TEXT);
let text4ID = yield backend.registerField(mID, "daily_last_text_2",
backend.FIELD_DAILY_LAST_TEXT);
let numeric3ID = yield backend.registerField(mID, "daily_last_numeric_1",
backend.FIELD_DAILY_LAST_NUMERIC);
let numeric4ID = yield backend.registerField(mID, "daily_last_numeric_2",
backend.FIELD_DAILY_LAST_NUMERIC);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
let dayBefore = new Date(yesterday.getTime() - MILLISECONDS_PER_DAY);
yield backend.incrementDailyCounterFromFieldID(counterID, now);
yield backend.incrementDailyCounterFromFieldID(counterID, yesterday);
yield backend.incrementDailyCounterFromFieldID(counterID, dayBefore);
yield backend.setLastTextFromFieldID(text1ID, "hello", dayBefore);
yield backend.setLastTextFromFieldID(text2ID, "world", yesterday);
yield backend.setLastNumericFromFieldID(numeric1ID, 42, dayBefore);
yield backend.setLastNumericFromFieldID(numeric2ID, 43, yesterday);
yield backend.setDailyLastTextFromFieldID(text3ID, "foo", dayBefore);
yield backend.setDailyLastTextFromFieldID(text3ID, "bar", yesterday);
yield backend.setDailyLastTextFromFieldID(text4ID, "hello", dayBefore);
yield backend.setDailyLastTextFromFieldID(text4ID, "world", yesterday);
yield backend.setDailyLastNumericFromFieldID(numeric3ID, 40, dayBefore);
yield backend.setDailyLastNumericFromFieldID(numeric3ID, 41, yesterday);
yield backend.setDailyLastNumericFromFieldID(numeric4ID, 42, dayBefore);
yield backend.setDailyLastNumericFromFieldID(numeric4ID, 43, yesterday);
let days = yield backend.getDailyCounterCountsFromFieldID(counterID);
do_check_eq(days.size, 3);
yield backend.pruneDataBefore(yesterday);
days = yield backend.getDailyCounterCountsFromFieldID(counterID);
do_check_eq(days.size, 2);
do_check_false(days.hasDay(dayBefore));
do_check_null(yield backend.getLastTextFromFieldID(text1ID));
do_check_null(yield backend.getLastNumericFromFieldID(numeric1ID));
let result = yield backend.getLastTextFromFieldID(text2ID);
do_check_true(Array.isArray(result));
do_check_eq(result[1], "world");
result = yield backend.getLastNumericFromFieldID(numeric2ID);
do_check_true(Array.isArray(result));
do_check_eq(result[1], 43);
result = yield backend.getDailyLastNumericFromFieldID(numeric3ID);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(yesterday));
result = yield backend.getDailyLastTextFromFieldID(text3ID);
do_check_eq(result.size, 1);
do_check_true(result.hasDay(yesterday));
yield backend.close();
});
add_task(function test_provider_state() {
let backend = yield Metrics.Storage("provider_state");
yield backend.registerMeasurement("foo", "bar", 1);
yield backend.setProviderState("foo", "apple", "orange");
let value = yield backend.getProviderState("foo", "apple");
do_check_eq(value, "orange");
yield backend.setProviderState("foo", "apple", "pear");
value = yield backend.getProviderState("foo", "apple");
do_check_eq(value, "pear");
yield backend.close();
});
add_task(function test_get_measurement_values() {
let backend = yield Metrics.Storage("get_measurement_values");
let mID = yield backend.registerMeasurement("foo", "bar", 1);
let id1 = yield backend.registerField(mID, "id1", backend.FIELD_DAILY_COUNTER);
let id2 = yield backend.registerField(mID, "id2", backend.FIELD_DAILY_DISCRETE_NUMERIC);
let id3 = yield backend.registerField(mID, "id3", backend.FIELD_DAILY_DISCRETE_TEXT);
let id4 = yield backend.registerField(mID, "id4", backend.FIELD_DAILY_LAST_NUMERIC);
let id5 = yield backend.registerField(mID, "id5", backend.FIELD_DAILY_LAST_TEXT);
let id6 = yield backend.registerField(mID, "id6", backend.FIELD_LAST_NUMERIC);
let id7 = yield backend.registerField(mID, "id7", backend.FIELD_LAST_TEXT);
let now = new Date();
let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
yield backend.incrementDailyCounterFromFieldID(id1, now);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 3, now);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 4, now);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 5, yesterday);
yield backend.addDailyDiscreteNumericFromFieldID(id2, 6, yesterday);
yield backend.addDailyDiscreteTextFromFieldID(id3, "1", now);
yield backend.addDailyDiscreteTextFromFieldID(id3, "2", now);
yield backend.addDailyDiscreteTextFromFieldID(id3, "3", yesterday);
yield backend.addDailyDiscreteTextFromFieldID(id3, "4", yesterday);
yield backend.setDailyLastNumericFromFieldID(id4, 1, now);
yield backend.setDailyLastNumericFromFieldID(id4, 2, yesterday);
yield backend.setDailyLastTextFromFieldID(id5, "foo", now);
yield backend.setDailyLastTextFromFieldID(id5, "bar", yesterday);
yield backend.setLastNumericFromFieldID(id6, 42, now);
yield backend.setLastTextFromFieldID(id7, "foo", now);
let fields = yield backend.getMeasurementValues(mID);
do_check_eq(Object.keys(fields).length, 2);
do_check_true("days" in fields);
do_check_true("singular" in fields);
do_check_eq(fields.days.size, 2);
do_check_true(fields.days.hasDay(now));
do_check_true(fields.days.hasDay(yesterday));
do_check_eq(fields.days.getDay(now).size, 5);
do_check_eq(fields.days.getDay(yesterday).size, 4);
do_check_eq(fields.days.getDay(now).get("id3")[0], 1);
do_check_eq(fields.days.getDay(yesterday).get("id4"), 2);
do_check_eq(fields.singular.size, 2);
do_check_eq(fields.singular.get("id6")[1], 42);
do_check_eq(fields.singular.get("id7")[1], "foo");
yield backend.close();
});
@@ -1,9 +0,0 @@
[DEFAULT]
head = head.js
tail =
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_load_modules.js]
[test_metrics_provider.js]
[test_metrics_provider_manager.js]
[test_metrics_storage.js]
-5
View File
@@ -10,9 +10,6 @@ DIRS += [
'fxaccounts',
]
if CONFIG['MOZ_SERVICES_METRICS']:
DIRS += ['metrics']
if CONFIG['MOZ_SERVICES_SYNC']:
DIRS += ['sync']
@@ -21,5 +18,3 @@ if CONFIG['MOZ_B2G'] or CONFIG['MOZ_B2GDROID']:
if CONFIG['MOZ_SERVICES_CLOUDSYNC']:
DIRS += ['cloudsync']
SPHINX_TREES['services'] = 'docs'
@@ -460,6 +460,12 @@ this.Download.prototype = {
throw new DownloadError({ becauseBlockedByParentalControls: true });
}
// Disallow download if needed runtime permissions have not been granted
// by user.
if (yield DownloadIntegration.shouldBlockForRuntimePermissions()) {
throw new DownloadError({ becauseBlockedByRuntimePermissions: true });
}
// We should check if we have been canceled in the meantime, after all
// the previous asynchronous operations have been executed and just
// before we call the "execute" method of the saver.
@@ -1495,7 +1501,8 @@ this.DownloadError = function (aProperties)
this.message = aProperties.message;
} else if (aProperties.becauseBlocked ||
aProperties.becauseBlockedByParentalControls ||
aProperties.becauseBlockedByReputationCheck) {
aProperties.becauseBlockedByReputationCheck ||
aProperties.becauseBlockedByRuntimePermissions) {
this.message = "Download blocked.";
} else {
let exception = new Components.Exception("", this.result);
@@ -1522,6 +1529,9 @@ this.DownloadError = function (aProperties)
} else if (aProperties.becauseBlockedByReputationCheck) {
this.becauseBlocked = true;
this.becauseBlockedByReputationCheck = true;
} else if (aProperties.becauseBlockedByRuntimePermissions) {
this.becauseBlocked = true;
this.becauseBlockedByRuntimePermissions = true;
} else if (aProperties.becauseBlocked) {
this.becauseBlocked = true;
}
@@ -1569,6 +1579,15 @@ this.DownloadError.prototype = {
*/
becauseBlockedByReputationCheck: false,
/**
* Indicates the download was blocked because a runtime permission required to
* download files was not granted.
*
* This does not apply to all systems. On Android this flag is set to true if
* a needed runtime permission (storage) has not been granted by the user.
*/
becauseBlockedByRuntimePermissions: false,
/**
* If this DownloadError was caused by an exception this property will
* contain the original exception. This will not be serialized when saving
@@ -1591,6 +1610,7 @@ this.DownloadError.prototype = {
becauseBlocked: this.becauseBlocked,
becauseBlockedByParentalControls: this.becauseBlockedByParentalControls,
becauseBlockedByReputationCheck: this.becauseBlockedByReputationCheck,
becauseBlockedByRuntimePermissions: this.becauseBlockedByRuntimePermissions,
};
serializeUnknownProperties(this, serializable);
@@ -1615,7 +1635,8 @@ this.DownloadError.fromSerializable = function (aSerializable) {
property != "becauseTargetFailed" &&
property != "becauseBlocked" &&
property != "becauseBlockedByParentalControls" &&
property != "becauseBlockedByReputationCheck");
property != "becauseBlockedByReputationCheck" &&
property != "becauseBlockedByRuntimePermissions");
return e;
};
@@ -2080,6 +2101,9 @@ this.DownloadCopySaver.prototype = {
// In case an error occurs while setting up the chain of objects for
// the download, ensure that we release the resources of the saver.
backgroundFileSaver.finish(Cr.NS_ERROR_FAILURE);
// Since we're not going to handle deferSaveComplete.promise below,
// we need to make sure that the rejection is handled.
deferSaveComplete.promise.catch(() => {});
throw ex;
}
@@ -2316,14 +2340,18 @@ this.DownloadLegacySaver.prototype = {
*/
onProgressBytes: function DLS_onProgressBytes(aCurrentBytes, aTotalBytes)
{
this.progressWasNotified = true;
// Ignore progress notifications until we are ready to process them.
if (!this.setProgressBytesFn) {
// Keep the data from the last progress notification that was received.
this.currentBytes = aCurrentBytes;
this.totalBytes = aTotalBytes;
return;
}
let hasPartFile = !!this.download.target.partFilePath;
this.progressWasNotified = true;
this.setProgressBytesFn(aCurrentBytes, aTotalBytes,
aCurrentBytes > 0 && hasPartFile);
},
@@ -2433,6 +2461,9 @@ this.DownloadLegacySaver.prototype = {
}
this.setProgressBytesFn = aSetProgressBytesFn;
if (this.progressWasNotified) {
this.onProgressBytes(this.currentBytes, this.totalBytes);
}
return Task.spawn(function* task_DLS_execute() {
try {
@@ -174,7 +174,7 @@ this.DownloadImport.prototype = {
yield this.list.add(download);
if (resumeDownload) {
download.start();
download.start().catch(() => {});
} else {
yield download.refresh();
}
@@ -68,6 +68,10 @@ XPCOMUtils.defineLazyServiceGetter(this, "gMIMEService",
XPCOMUtils.defineLazyServiceGetter(this, "gExternalProtocolService",
"@mozilla.org/uriloader/external-protocol-service;1",
"nsIExternalProtocolService");
#ifdef MOZ_WIDGET_ANDROID
XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions",
"resource://gre/modules/RuntimePermissions.jsm");
#endif
XPCOMUtils.defineLazyGetter(this, "gParentalControlsService", function() {
if ("@mozilla.org/parental-controls-service;1" in Cc) {
@@ -136,7 +140,13 @@ this.DownloadIntegration = {
dontLoadObservers: false,
dontCheckParentalControls: false,
shouldBlockInTest: false,
dontCheckRuntimePermissions: false,
shouldBlockInTestForRuntimePermissions: false,
#ifdef MOZ_URL_CLASSIFIER
dontCheckApplicationReputation: false,
#else
dontCheckApplicationReputation: true,
#endif
shouldBlockInTestForApplicationReputation: false,
shouldKeepBlockedDataInTest: false,
dontOpenFileAndFolder: false,
@@ -492,6 +502,25 @@ this.DownloadIntegration = {
return Promise.resolve(shouldBlock);
},
/**
* Checks to determine whether to block downloads for not granted runtime permissions.
*
* @return {Promise}
* @resolves The boolean indicates to block downloads or not.
*/
shouldBlockForRuntimePermissions: function DI_shouldBlockForRuntimePermissions() {
if (this.dontCheckRuntimePermissions) {
return Promise.resolve(this.shouldBlockInTestForRuntimePermissions);
}
#ifdef MOZ_WIDGET_ANDROID
return RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE)
.then(permissionGranted => !permissionGranted);
#else
return Promise.resolve(false);
#endif
},
/**
* Checks to determine whether to block downloads because they might be
* malware, based on application reputation checks.
@@ -928,6 +957,20 @@ this.DownloadIntegration = {
return Promise.resolve();
},
/**
* Force a save on _store if it exists. Used to ensure downloads do not
* persist after being sanitized on Android.
*
* @return {Promise}
* @resolves When _store.save() completes.
*/
forceSave: function DI_forceSave() {
if (this._store) {
return this._store.save();
}
return Promise.resolve();
},
/**
* Checks if we have already imported (or attempted to import)
* the downloads database from the previous SQLite storage.
@@ -1054,7 +1097,7 @@ this.DownloadObserver = {
this._wakeTimer = null;
for (let download of this._canceledOfflineDownloads) {
download.start();
download.start().catch(() => {});
}
},
@@ -243,7 +243,7 @@ DownloadLegacyTransfer.prototype = {
}
// Start the download before allowing it to be controlled. Ignore errors.
aDownload.start().then(null, () => {});
aDownload.start().catch(() => {});
// Start processing all the other events received through nsITransfer.
this._deferDownload.resolve(aDownload);
@@ -124,8 +124,8 @@ this.DownloadStore.prototype = {
try {
if (!download.succeeded && !download.canceled && !download.error) {
// Try to restart the download if it was in progress during the
// previous session.
download.start();
// previous session. Ignore errors.
download.start().catch(() => {});
} else {
// If the download was not in progress, try to update the current
// progress from disk. This is relevant in case we retained
@@ -88,7 +88,7 @@ add_task(function* test_cancel_pdf_download() {
});
yield test_download_windowRef(tab, download);
download.start();
download.start().catch(() => {});
// Immediately cancel the download to test that it is erased correctly.
yield download.cancel();
@@ -32,7 +32,7 @@ function promiseStartDownload(aSourceUrl) {
}
return promiseNewDownload(aSourceUrl).then(download => {
download.start();
download.start().catch(() => {});
return download;
});
}
@@ -64,7 +64,7 @@ function promiseStartDownload_tryToKeepPartialData() {
partFilePath: targetFilePath + ".part" },
});
download.tryToKeepPartialData = true;
download.start();
download.start().catch(() => {});
} else {
// Start a download using nsIExternalHelperAppService, that is configured
// to keep partially downloaded data by default.
@@ -435,7 +435,7 @@ add_task(function* test_empty_progress_tryToKeepPartialData()
partFilePath: targetFilePath + ".part" },
});
download.tryToKeepPartialData = true;
download.start();
download.start().catch(() => {});
} else {
// Start a download using nsIExternalHelperAppService, that is configured
// to keep partially downloaded data by default.
@@ -491,7 +491,7 @@ add_task(function* test_empty_noprogress()
}
};
download.start();
download.start().catch(() => {});
} else {
// When testing DownloadLegacySaver, the download is already started when it
// is created, and it may have already made all needed property change
@@ -856,7 +856,7 @@ add_task(function* test_cancel_midway_restart_tryToKeepPartialData_false()
// Restart the download from the beginning.
mustInterruptResponses();
download.start();
download.start().catch(() => {});
yield promiseDownloadMidway(download);
yield promisePartFileReady(download);
@@ -1143,7 +1143,7 @@ add_task(function* test_whenSucceeded_after_restart()
// we can verify getting a reference before the first download attempt.
download = yield promiseNewDownload(httpUrl("interruptible.txt"));
promiseSucceeded = download.whenSucceeded();
download.start();
download.start().catch(() => {});
} else {
// When testing DownloadLegacySaver, the download is already started when it
// is created, thus we cannot get the reference before the first attempt.
@@ -1156,7 +1156,7 @@ add_task(function* test_whenSucceeded_after_restart()
// The second request is allowed to complete.
continueResponses();
download.start();
download.start().catch(() => {});
// Wait for the download to finish by waiting on the whenSucceeded promise.
yield promiseSucceeded;
@@ -1343,7 +1343,7 @@ add_task(function* test_error_restart()
source: httpUrl("source.txt"),
target: targetFile,
});
download.start();
download.start().catch(() => {});
} else {
download = yield promiseStartLegacyDownload(null,
{ targetFile: targetFile });
@@ -1640,6 +1640,47 @@ add_task(function* test_blocked_parental_controls_httpstatus450()
do_check_false(yield OS.File.exists(download.target.path));
});
/**
* Download with runtime permissions
*/
add_task(function* test_blocked_runtime_permissions()
{
function cleanup() {
DownloadIntegration.shouldBlockInTestForRuntimePermissions = false;
}
do_register_cleanup(cleanup);
DownloadIntegration.shouldBlockInTestForRuntimePermissions = true;
let download;
try {
if (!gUseLegacySaver) {
// When testing DownloadCopySaver, we want to check that the promise
// returned by the "start" method is rejected.
download = yield promiseNewDownload();
yield download.start();
} else {
// When testing DownloadLegacySaver, we cannot be sure whether we are
// testing the promise returned by the "start" method or we are testing
// the "error" property checked by promiseDownloadStopped. This happens
// because we don't have control over when the download is started.
download = yield promiseStartLegacyDownload();
yield promiseDownloadStopped(download);
}
do_throw("The download should have blocked.");
} catch (ex) {
if (!(ex instanceof Downloads.Error) || !ex.becauseBlocked) {
throw ex;
}
do_check_true(ex.becauseBlockedByRuntimePermissions);
do_check_true(download.error.becauseBlockedByRuntimePermissions);
}
// Now that the download stopped, the target file should not exist.
do_check_false(yield OS.File.exists(download.target.path));
cleanup();
});
/**
* Check that DownloadCopySaver can always retrieve the hash.
* DownloadLegacySaver can only retrieve the hash when
@@ -2145,7 +2186,7 @@ add_task(function* test_platform_integration()
source: httpUrl("source.txt"),
target: targetFile,
});
download.start();
download.start().catch(() => {});
}
// Wait for the whenSucceeded promise to be resolved first.
@@ -788,6 +788,8 @@ add_task(function test_common_initialize()
DownloadIntegration.dontOpenFileAndFolder = true;
DownloadIntegration._deferTestOpenFile = Promise.defer();
DownloadIntegration._deferTestShowDir = Promise.defer();
// Disable checking runtime permissions.
DownloadIntegration.dontCheckRuntimePermissions = true;
// Avoid leaking uncaught promise errors
DownloadIntegration._deferTestOpenFile.promise.then(null, () => undefined);
@@ -215,7 +215,7 @@ add_task(function* test_notifications()
let download3 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let promiseAttempt1 = download1.start();
let promiseAttempt2 = download2.start();
download3.start();
download3.start().catch(() => {});
// Add downloads to list.
yield list.add(download1);
@@ -250,8 +250,8 @@ add_task(function* test_no_notifications()
let list = yield promiseNewList(isPrivate);
let download1 = yield promiseNewDownload(httpUrl("interruptible.txt"));
let download2 = yield promiseNewDownload(httpUrl("interruptible.txt"));
download1.start();
download2.start();
download1.start().catch(() => {});
download2.start().catch(() => {});
// Add downloads to list.
yield list.add(download1);
@@ -316,7 +316,7 @@ add_task(function* test_suspend_resume()
{
return Task.spawn(function* () {
let download = yield promiseNewDownload(httpUrl("interruptible.txt"));
download.start();
download.start().catch(() => {});
list.add(download);
return download;
});
@@ -348,7 +348,7 @@ add_task(function* test_history_expiration()
// Work with one finished download and one canceled download.
yield downloadOne.start();
downloadTwo.start();
downloadTwo.start().catch(() => {});
yield downloadTwo.cancel();
// We must replace the visits added while executing the downloads with visits
@@ -471,7 +471,7 @@ add_task(function* test_DownloadSummary()
// Add a public download that has been canceled midway.
let canceledPublicDownload =
yield promiseNewDownload(httpUrl("interruptible.txt"));
canceledPublicDownload.start();
canceledPublicDownload.start().catch(() => {});
yield promiseDownloadMidway(canceledPublicDownload);
yield canceledPublicDownload.cancel();
yield publicList.add(canceledPublicDownload);
@@ -479,7 +479,7 @@ add_task(function* test_DownloadSummary()
// Add a public download that is in progress.
let inProgressPublicDownload =
yield promiseNewDownload(httpUrl("interruptible.txt"));
inProgressPublicDownload.start();
inProgressPublicDownload.start().catch(() => {});
yield promiseDownloadMidway(inProgressPublicDownload);
yield publicList.add(inProgressPublicDownload);
@@ -488,7 +488,7 @@ add_task(function* test_DownloadSummary()
source: { url: httpUrl("interruptible.txt"), isPrivate: true },
target: getTempFile(TEST_TARGET_FILE_NAME).path,
});
inProgressPrivateDownload.start();
inProgressPrivateDownload.start().catch(() => {});
yield promiseDownloadMidway(inProgressPrivateDownload);
yield privateList.add(inProgressPrivateDownload);
@@ -1006,6 +1006,10 @@ EngineURL.prototype = {
// (purpose="") work consistently rather than having to define "null" and "" purposes.
var purpose = aPurpose || "";
// If the 'system' purpose isn't defined in the plugin, fallback to 'searchbar'.
if (purpose == "system" && !this.params.some(p => p.purpose == "system"))
purpose = "searchbar";
// Create an application/x-www-form-urlencoded representation of our params
// (name=value&name=value&name=value)
var dataString = "";
@@ -4202,6 +4206,13 @@ SearchService.prototype = {
},
_addObservers: function SRCH_SVC_addObservers() {
if (this._observersAdded) {
// There might be a race between synchronous and asynchronous
// initialization for which we try to register the observers twice.
return;
}
this._observersAdded = true;
Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC, false);
Services.obs.addObserver(this, QUIT_APPLICATION_TOPIC, false);
@@ -4249,6 +4260,7 @@ SearchService.prototype = {
() => shutdownState
);
},
_observersAdded: false,
_removeObservers: function SRCH_SVC_removeObservers() {
Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
<ShortName>engine-system-purpose</ShortName>
<Url type="text/html" method="GET" template="http://www.google.com/search">
<Param name="q" value="{searchTerms}"/>
<!-- Dynamic parameters -->
<MozParam name="channel" condition="purpose" purpose="searchbar" value="sb"/>
<MozParam name="channel" condition="purpose" purpose="system" value="sys"/>
</Url>
</SearchPlugin>
@@ -54,5 +54,16 @@ add_task(function* test_purpose() {
check_submission("&channel=sb", "", null, "searchbar");
check_submission("&channel=sb", "", "text/html", "searchbar");
// verify that the 'system' purpose falls back to the 'searchbar' purpose.
base = "http://www.google.com/search?q=foo";
check_submission("&channel=sb", "foo", "text/html", "system");
check_submission("&channel=sb", "foo", "text/html", "searchbar");
// Add an engine that actually defines the 'system' purpose...
[engine] = yield addTestEngines([
{ name: "engine-system-purpose", xmlFileName: "engine-system-purpose.xml" }
]);
// ... and check that the system purpose is used correctly.
check_submission("&channel=sys", "foo", "text/html", "system");
do_test_finished();
});
@@ -13,6 +13,7 @@ support-files =
data/engine-rel-searchform.xml
data/engine-rel-searchform-post.xml
data/engine-rel-searchform-purpose.xml
data/engine-system-purpose.xml
data/engineImages.xml
data/ico-size-16x16-png.ico
data/invalid-engine.xml
+15
View File
@@ -5,6 +5,9 @@
"use strict";
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
this.EXPORTED_SYMBOLS = ["AppConstants"];
// Immutable for export.
@@ -122,6 +125,18 @@ let AppConstants = Object.freeze({
"other",
#endif
isPlatformAndVersionAtLeast(platform, version) {
let platformVersion = Services.sysinfo.getProperty("version");
return platform == this.platform &&
Services.vc.compare(platformVersion, version) >= 0;
},
isPlatformAndVersionAtMost(platform, version) {
let platformVersion = Services.sysinfo.getProperty("version");
return platform == this.platform &&
Services.vc.compare(platformVersion, version) <= 0;
},
MOZ_CRASHREPORTER:
#ifdef MOZ_CRASHREPORTER
true,
+4
View File
@@ -9,6 +9,10 @@ BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini']
MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
TESTING_JS_MODULES += [
'tests/PromiseTestUtils.jsm',
]
SPHINX_TREES['toolkit_modules'] = 'docs'
EXTRA_JS_MODULES += [
+241
View File
@@ -0,0 +1,241 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Detects and reports unhandled rejections during test runs. Test harnesses
* will fail tests in this case, unless the test whitelists itself.
*/
"use strict";
this.EXPORTED_SYMBOLS = [
"PromiseTestUtils",
];
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
Cu.import("resource://gre/modules/Services.jsm", this);
// Keep "JSMPromise" separate so "Promise" still refers to DOM Promises.
let JSMPromise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
// For now, we need test harnesses to provide a reference to Assert.jsm.
let Assert = null;
this.PromiseTestUtils = {
/**
* Array of objects containing the details of the Promise rejections that are
* currently left uncaught. This includes DOM Promise and Promise.jsm. When
* rejections in DOM Promises are consumed, they are removed from this list.
*
* The objects contain at least the following properties:
* {
* message: The error message associated with the rejection, if any.
* date: Date object indicating when the rejection was observed.
* id: For DOM Promise only, the Promise ID from PromiseDebugging. This is
* only used for tracking and should not be checked by the callers.
* stack: nsIStackFrame, SavedFrame, or string indicating the stack at the
* time the rejection was triggered. May also be null if the
* rejection was triggered while a script was on the stack.
* }
*/
_rejections: [],
/**
* When an uncaught rejection is detected, it is ignored if one of the
* functions in this array returns true when called with the rejection details
* as its only argument. When a function matches an expected rejection, it is
* then removed from the array.
*/
_rejectionIgnoreFns: [],
/**
* Called only by the test infrastructure, registers the rejection observers.
*
* This should be called only once, and a matching "uninit" call must be made
* or the tests will crash on shutdown.
*/
init() {
if (this._initialized) {
Cu.reportError("This object was already initialized.");
return;
}
PromiseDebugging.addUncaughtRejectionObserver(this);
// Promise.jsm rejections are only reported to this observer when requested,
// so we don't have to store a key to remove them when consumed.
JSMPromise.Debugging.addUncaughtErrorObserver(
rejection => this._rejections.push(rejection));
this._initialized = true;
},
_initialized: false,
/**
* Called only by the test infrastructure, unregisters the observers.
*/
uninit() {
if (!this._initialized) {
return;
}
PromiseDebugging.removeUncaughtRejectionObserver(this);
JSMPromise.Debugging.clearUncaughtErrorObservers();
this._initialized = false;
},
/**
* Called only by the test infrastructure, spins the event loop until the
* messages for pending DOM Promise rejections have been processed.
*/
ensureDOMPromiseRejectionsProcessed() {
let observed = false;
let observer = {
onLeftUncaught: promise => {
if (PromiseDebugging.getState(promise).reason ===
this._ensureDOMPromiseRejectionsProcessedReason) {
observed = true;
}
},
onConsumed() {},
};
PromiseDebugging.addUncaughtRejectionObserver(observer);
Promise.reject(this._ensureDOMPromiseRejectionsProcessedReason);
while (!observed) {
Services.tm.mainThread.processNextEvent(true);
}
PromiseDebugging.removeUncaughtRejectionObserver(observer);
},
_ensureDOMPromiseRejectionsProcessedReason: {},
/**
* Called only by the tests for PromiseDebugging.addUncaughtRejectionObserver
* and for JSMPromise.Debugging, disables the observers in this module.
*/
disableUncaughtRejectionObserverForSelfTest() {
this.uninit();
},
/**
* Called by tests that have been whitelisted, disables the observers in this
* module. For new tests where uncaught rejections are expected, you should
* use the more granular expectUncaughtRejection function instead.
*/
thisTestLeaksUncaughtRejectionsAndShouldBeFixed() {
this.uninit();
},
/**
* Sets or updates the Assert object instance to be used for error reporting.
*/
set Assert(assert) {
Assert = assert;
},
// UncaughtRejectionObserver
onLeftUncaught(promise) {
let message = "(Unable to convert rejection reason to string.)";
try {
let reason = PromiseDebugging.getState(promise).reason;
if (reason === this._ensureDOMPromiseRejectionsProcessedReason) {
// Ignore the special promise for ensureDOMPromiseRejectionsProcessed.
return;
}
message = reason.message || ("" + reason);
} catch (ex) {}
// It's important that we don't store any reference to the provided Promise
// object or its value after this function returns in order to avoid leaks.
this._rejections.push({
id: PromiseDebugging.getPromiseID(promise),
message,
date: new Date(),
stack: PromiseDebugging.getRejectionStack(promise),
});
},
// UncaughtRejectionObserver
onConsumed(promise) {
// We don't expect that many unhandled rejections will appear at the same
// time, so the algorithm doesn't need to be optimized for that case.
let id = PromiseDebugging.getPromiseID(promise);
let index = this._rejections.findIndex(rejection => rejection.id == id);
// If we get a consumption notification for a rejection that was left
// uncaught before this module was initialized, we can safely ignore it.
if (index != -1) {
this._rejections.splice(index, 1);
}
},
/**
* Informs the test suite that the test code will generate a Promise rejection
* that will still be unhandled when the test file terminates.
*
* This method must be called once for each instance of Promise that is
* expected to be uncaught, even if the rejection reason is the same for each
* instance.
*
* If the expected rejection does not occur, the test will fail.
*
* @param regExpOrCheckFn
* This can either be a regular expression that should match the error
* message of the rejection, or a check function that is invoked with
* the rejection details object as its first argument.
*/
expectUncaughtRejection(regExpOrCheckFn) {
let checkFn = !("test" in regExpOrCheckFn) ? regExpOrCheckFn :
rejection => regExpOrCheckFn.test(rejection.message);
this._rejectionIgnoreFns.push(checkFn);
},
/**
* Fails the test if there are any uncaught rejections at this time that have
* not been whitelisted using expectUncaughtRejection.
*
* Depending on the configuration of the test suite, this function might only
* report the details of the first uncaught rejection that was generated.
*
* This is called by the test suite at the end of each test function.
*/
assertNoUncaughtRejections() {
// Ask Promise.jsm to report all uncaught rejections to the observer now.
JSMPromise.Debugging.flushUncaughtErrors();
// If there is any uncaught rejection left at this point, the test fails.
while (this._rejections.length > 0) {
let rejection = this._rejections.shift();
// If one of the ignore functions matches, ignore the rejection, then
// remove the function so that each function only matches one rejection.
let index = this._rejectionIgnoreFns.findIndex(f => f(rejection));
if (index != -1) {
this._rejectionIgnoreFns.splice(index, 1);
continue;
}
// Report the error. This operation can throw an exception, depending on
// the configuration of the test suite that handles the assertion.
Assert.ok(false,
`A promise chain failed to handle a rejection:` +
` ${rejection.message} - rejection date: ${rejection.date}`+
` - stack: ${rejection.stack}`);
}
},
/**
* Fails the test if any rejection indicated by expectUncaughtRejection has
* not yet been reported at this time.
*
* This is called by the test suite at the end of each test file.
*/
assertNoMoreExpectedRejections() {
// Only log this condition is there is a failure.
if (this._rejectionIgnoreFns.length > 0) {
Assert.equal(this._rejectionIgnoreFns.length, 0,
"Unable to find a rejection expected by expectUncaughtRejection.");
}
},
};
@@ -5,10 +5,10 @@
Components.utils.import("resource://gre/modules/Promise.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/Task.jsm");
Components.utils.import("resource://testing-common/PromiseTestUtils.jsm");
// Deactivate the standard xpcshell observer, as it turns uncaught
// rejections into failures, which we don't want here.
Promise.Debugging.clearUncaughtErrorObservers();
// Prevent test failures due to the unhandled rejections in this test file.
PromiseTestUtils.disableUncaughtRejectionObserverForSelfTest();
////////////////////////////////////////////////////////////////////////////////
//// Test runner
@@ -5,6 +5,7 @@
Components.utils.import("resource://gre/modules/PromiseUtils.jsm");
Components.utils.import("resource://gre/modules/Timer.jsm");
Components.utils.import("resource://testing-common/PromiseTestUtils.jsm");
// Tests for PromiseUtils.jsm
function run_test() {
@@ -98,8 +99,9 @@ add_task(function* test_reject_resolved_promise() {
/* Test for the case when a rejected Promise is
* passed to the reject method */
add_task(function* test_reject_resolved_promise() {
PromiseTestUtils.expectUncaughtRejection(/This one rejects/);
let def = PromiseUtils.defer();
let p = new Promise((resolve, reject) => reject(new Error("This on rejects")));
let p = new Promise((resolve, reject) => reject(new Error("This one rejects")));
def.reject(p);
yield Assert.rejects(def.promise, Promise, "Rejection with a rejected promise uses the passed promise itself as the reason of rejection");
});
+3 -1
View File
@@ -1086,7 +1086,9 @@ nsView::DidCompositeWindow(const TimeStamp& aCompositeStart,
nsAutoScriptBlocker scriptBlocker;
nsPresContext* context = presShell->GetPresContext();
context->GetRootPresContext()->NotifyDidPaintForSubtree(nsIPresShell::PAINT_COMPOSITE);
nsRootPresContext* rootContext = context->GetRootPresContext();
MOZ_ASSERT(rootContext, "rootContext must be valid.");
rootContext->NotifyDidPaintForSubtree(nsIPresShell::PAINT_COMPOSITE);
// If the two timestamps are identical, this was likely a fake composite
// event which wouldn't be terribly useful to display.
+2
View File
@@ -33,6 +33,8 @@ fail-if = os == "android"
fail-if = os == "android"
[test_file_createUnique.js]
[test_file_equality.js]
# Bug 1144393: fails consistently on Android 4.3 emulator
fail-if = android_version == "18"
[test_hidden_files.js]
[test_home.js]
# Bug 676998: test fails consistently on Android
+1 -1
View File
@@ -11,9 +11,9 @@ MOZ_CHROME_FILE_FORMAT=omni
MOZ_APP_VERSION=$MOZILLA_VERSION
MOZ_PLACES=1
MOZ_EXTENSIONS_DEFAULT=" gio"
MOZ_URL_CLASSIFIER=1
MOZ_SERVICES_COMMON=1
MOZ_SERVICES_CRYPTO=1
MOZ_SERVICES_METRICS=1
MOZ_SERVICES_SYNC=1
MOZ_MEDIA_NAVIGATOR=1
MOZ_SERVICES_HEALTHREPORT=1