diff options
author | Pale Moon <git-repo@palemoon.org> | 2016-09-01 13:39:08 +0200 |
---|---|---|
committer | Pale Moon <git-repo@palemoon.org> | 2016-09-01 13:39:08 +0200 |
commit | 3d8ce1a11a7347cc94a937719c4bc8df46fb8d14 (patch) | |
tree | 8c26ca375a6312751c00a27e1653fb6f189f0463 /dom/resourcestats | |
parent | e449bdb1ec3a82f204bffdd9c3c54069d086eee3 (diff) | |
download | palemoon-gre-3d8ce1a11a7347cc94a937719c4bc8df46fb8d14.tar.gz |
Base import of Tycho code (warning: huge commit)
Diffstat (limited to 'dom/resourcestats')
16 files changed, 3956 insertions, 0 deletions
diff --git a/dom/resourcestats/ResourceStatsDB.jsm b/dom/resourcestats/ResourceStatsDB.jsm new file mode 100644 index 000000000..a4ee943ca --- /dev/null +++ b/dom/resourcestats/ResourceStatsDB.jsm @@ -0,0 +1,538 @@ +/* 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 = ['ResourceStatsDB']; + +const DEBUG = false; +function debug(s) { dump("-*- ResourceStatsDB: " + s + "\n"); } + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.importGlobalProperties(["indexedDB"]); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +const DB_NAME = "resource_stats"; +const DB_VERSION = 1; +const POWER_STATS_STORE = "power_stats_store"; +const NETWORK_STATS_STORE = "network_stats_store"; +const ALARM_STORE = "alarm_store"; + +const statsStoreNames = { + power: POWER_STATS_STORE, + network: NETWORK_STATS_STORE +}; + +// Constant defining the sampling rate. +const SAMPLE_RATE = 24 * 60 * 60 * 1000; // 1 day. + +// Constant defining the MAX age of stored stats. +const MAX_STORAGE_AGE = 180 * SAMPLE_RATE; // 180 days. + +this.ResourceStatsDB = function ResourceStatsDB() { + if (DEBUG) { + debug("Constructor()"); + } + + this.initDBHelper(DB_NAME, DB_VERSION, + [POWER_STATS_STORE, NETWORK_STATS_STORE, ALARM_STORE]); +}; + +ResourceStatsDB.prototype = { + __proto__: IndexedDBHelper.prototype, + + _dbNewTxn: function(aStoreName, aTxnType, aCallback, aTxnCb) { + function successCb(aResult) { + aTxnCb(null, aResult); + } + function errorCb(aError) { + aTxnCb(aError, null); + } + return this.newTxn(aTxnType, aStoreName, aCallback, successCb, errorCb); + }, + + upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) { + if (DEBUG) { + debug("Upgrade DB from ver." + aOldVersion + " to ver." + aNewVersion); + } + + let objectStore; + + // Create PowerStatsStore. + objectStore = aDb.createObjectStore(POWER_STATS_STORE, { + keyPath: ["appId", "serviceType", "component", "timestamp"] + }); + objectStore.createIndex("component", "component", { unique: false }); + + // Create NetworkStatsStore. + objectStore = aDb.createObjectStore(NETWORK_STATS_STORE, { + keyPath: ["appId", "serviceType", "component", "timestamp"] + }); + objectStore.createIndex("component", "component", { unique: false }); + + // Create AlarmStore. + objectStore = aDb.createObjectStore(ALARM_STORE, { + keyPath: "alarmId", + autoIncrement: true + }); + objectStore.createIndex("type", "type", { unique: false }); + // Index for resource control target. + objectStore.createIndex("controlTarget", + ["type", "manifestURL", "serviceType", "component"], + { unique: false }); + }, + + // Convert to UTC according to the current timezone and the filter timestamp + // to get SAMPLE_RATE precission. + _normalizeTime: function(aTime, aOffset) { + let time = Math.floor((aTime - aOffset) / SAMPLE_RATE) * SAMPLE_RATE; + + return time; + }, + + /** + * aRecordArray contains an array of json objects storing network stats. + * The structure of the json object = + * { + * appId: XX, + * serviceType: "XX", + * componentStats: { + * "component_1": { receivedBytes: XX, sentBytes: XX }, + * "component_2": { receivedBytes: XX, sentBytes: XX }, + * ... + * }, + * } + */ + saveNetworkStats: function(aRecordArray, aTimestamp, aResultCb) { + if (DEBUG) { + debug("saveNetworkStats()"); + } + + let offset = (new Date()).getTimezoneOffset() * 60 * 1000; + let timestamp = this._normalizeTime(aTimestamp, offset); + + this._dbNewTxn(NETWORK_STATS_STORE, "readwrite", function(aTxn, aStore) { + aRecordArray.forEach(function(aRecord) { + let stats = { + appId: aRecord.appId, + serviceType: aRecord.serviceType, + component: "", + timestamp: timestamp, + receivedBytes: 0, + sentBytes: 0 + }; + + let totalReceivedBytes = 0; + let totalSentBytes = 0; + + // Save stats of each component. + let data = aRecord.componentStats; + for (let component in data) { + // Save stats to database. + stats.component = component; + stats.receivedBytes = data[component].receivedBytes; + stats.sentBytes = data[component].sentBytes; + aStore.put(stats); + if (DEBUG) { + debug("Save network stats: " + JSON.stringify(stats)); + } + + // Accumulated to tatal stats. + totalReceivedBytes += stats.receivedBytes; + totalSentBytes += stats.sentBytes; + } + + // Save total stats. + stats.component = ""; + stats.receivedBytes = totalReceivedBytes; + stats.sentBytes = totalSentBytes; + aStore.put(stats); + if (DEBUG) { + debug("Save network stats: " + JSON.stringify(stats)); + } + }); + }, aResultCb); + }, + + /** + * aRecordArray contains an array of json objects storing power stats. + * The structure of the json object = + * { + * appId: XX, + * serviceType: "XX", + * componentStats: { + * "component_1": XX, // consumedPower + * "component_2": XX, + * ... + * }, + * } + */ + savePowerStats: function(aRecordArray, aTimestamp, aResultCb) { + if (DEBUG) { + debug("savePowerStats()"); + } + let offset = (new Date()).getTimezoneOffset() * 60 * 1000; + let timestamp = this._normalizeTime(aTimestamp, offset); + + this._dbNewTxn(POWER_STATS_STORE, "readwrite", function(aTxn, aStore) { + aRecordArray.forEach(function(aRecord) { + let stats = { + appId: aRecord.appId, + serviceType: aRecord.serviceType, + component: "", + timestamp: timestamp, + consumedPower: aRecord.totalStats + }; + + let totalConsumedPower = 0; + + // Save stats of each component to database. + let data = aRecord.componentStats; + for (let component in data) { + // Save stats to database. + stats.component = component; + stats.consumedPower = data[component]; + aStore.put(stats); + if (DEBUG) { + debug("Save power stats: " + JSON.stringify(stats)); + } + // Accumulated to total stats. + totalConsumedPower += stats.consumedPower; + } + + // Save total stats. + stats.component = ""; + stats.consumedPower = totalConsumedPower; + aStore.put(stats); + if (DEBUG) { + debug("Save power stats: " + JSON.stringify(stats)); + } + }); + }, aResultCb); + }, + + // Get stats from a store. + getStats: function(aType, aManifestURL, aServiceType, aComponent, + aStart, aEnd, aResultCb) { + if (DEBUG) { + debug(aType + "Mgr.getStats()"); + } + + let offset = (new Date()).getTimezoneOffset() * 60 * 1000; + + // Get appId and check whether manifestURL is a valid app. + let appId = 0; + if (aManifestURL) { + appId = appsService.getAppLocalIdByManifestURL(aManifestURL); + + if (!appId) { + aResultCb("Invalid manifestURL", null); + return; + } + } + + // Get store name. + let storeName = statsStoreNames[aType]; + + // Normalize start time and end time to SAMPLE_RATE precission. + let start = this._normalizeTime(aStart, offset); + let end = this._normalizeTime(aEnd, offset); + if (DEBUG) { + debug("Query time range: " + start + " to " + end); + debug("[appId, serviceType, component] = [" + appId + ", " + aServiceType + + ", " + aComponent + "]"); + } + + // Create filters. + let lowerFilter = [appId, aServiceType, aComponent, start]; + let upperFilter = [appId, aServiceType, aComponent, end]; + + // Execute DB query. + this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) { + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + + let statsData = []; + + if (!aTxn.result) { + aTxn.result = Object.create(null); + } + aTxn.result.type = aType; + aTxn.result.component = aComponent; + aTxn.result.serviceType = aServiceType; + aTxn.result.manifestURL = aManifestURL; + aTxn.result.start = start + offset; + aTxn.result.end = end + offset; + // Since ResourceStats() would require SAMPLE_RATE when filling the empty + // entries of statsData array, we append SAMPLE_RATE to the result field + // to save an IPC call. + aTxn.result.sampleRate = SAMPLE_RATE; + + let request = aStore.openCursor(range, "prev"); + if (aType == "power") { + request.onsuccess = function(aEvent) { + var cursor = aEvent.target.result; + if (cursor) { + if (DEBUG) { + debug("Get " + JSON.stringify(cursor.value)); + } + + // Covert timestamp to current timezone. + statsData.push({ + timestamp: cursor.value.timestamp + offset, + consumedPower: cursor.value.consumedPower + }); + cursor.continue(); + return; + } + aTxn.result.statsData = statsData; + }; + } else if (aType == "network") { + request.onsuccess = function(aEvent) { + var cursor = aEvent.target.result; + if (cursor) { + if (DEBUG) { + debug("Get " + JSON.stringify(cursor.value)); + } + + // Covert timestamp to current timezone. + statsData.push({ + timestamp: cursor.value.timestamp + offset, + receivedBytes: cursor.value.receivedBytes, + sentBytes: cursor.value.sentBytes + }); + cursor.continue(); + return; + } + aTxn.result.statsData = statsData; + }; + } + }, aResultCb); + }, + + // Delete the stats of a specific app/service (within a specified time range). + clearStats: function(aType, aAppId, aServiceType, aComponent, + aStart, aEnd, aResultCb) { + if (DEBUG) { + debug(aType + "Mgr.clearStats()"); + } + + let offset = (new Date()).getTimezoneOffset() * 60 * 1000; + + // Get store name. + let storeName = statsStoreNames[aType]; + + // Normalize start and end time to SAMPLE_RATE precission. + let start = this._normalizeTime(aStart, offset); + let end = this._normalizeTime(aEnd, offset); + if (DEBUG) { + debug("Query time range: " + start + " to " + end); + debug("[appId, serviceType, component] = [" + aAppId + ", " + aServiceType + + ", " + aComponent + "]"); + } + + // Create filters. + let lowerFilter = [aAppId, aServiceType, aComponent, start]; + let upperFilter = [aAppId, aServiceType, aComponent, end]; + + // Execute clear operation. + this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) { + let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); + let request = aStore.openCursor(range).onsuccess = function(aEvent) { + let cursor = aEvent.target.result; + if (cursor) { + if (DEBUG) { + debug("Delete " + JSON.stringify(cursor.value) + " from database"); + } + cursor.delete(); + cursor.continue(); + return; + } + }; + }, aResultCb); + }, + + // Delete all stats saved in a store. + clearAllStats: function(aType, aResultCb) { + if (DEBUG) { + debug(aType + "Mgr.clearAllStats()"); + } + + let storeName = statsStoreNames[aType]; + + // Execute clear operation. + this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) { + if (DEBUG) { + debug("Clear " + aType + " stats from datastore"); + } + aStore.clear(); + }, aResultCb); + }, + + addAlarm: function(aAlarm, aResultCb) { + if (DEBUG) { + debug(aAlarm.type + "Mgr.addAlarm()"); + debug("alarm = " + JSON.stringify(aAlarm)); + } + + this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) { + aStore.put(aAlarm).onsuccess = function setResult(aEvent) { + // Get alarmId. + aTxn.result = aEvent.target.result; + if (DEBUG) { + debug("New alarm ID: " + aTxn.result); + } + }; + }, aResultCb); + }, + + // Convert DB record to alarm object. + _recordToAlarm: function(aRecord) { + let alarm = { + alarmId: aRecord.alarmId, + type: aRecord.type, + component: aRecord.component, + serviceType: aRecord.serviceType, + manifestURL: aRecord.manifestURL, + threshold: aRecord.threshold, + data: aRecord.data + }; + + return alarm; + }, + + getAlarms: function(aType, aOptions, aResultCb) { + if (DEBUG) { + debug(aType + "Mgr.getAlarms()"); + debug("[appId, serviceType, component] = [" + aOptions.appId + ", " + + aOptions.serviceType + ", " + aOptions.component + "]"); + } + + // Execute clear operation. + this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) { + if (!aTxn.result) { + aTxn.result = []; + } + + let indexName = null; + let range = null; + + if (aOptions) { // Get alarms associated to specified statsOptions. + indexName = "controlTarget"; + range = IDBKeyRange.only([aType, aOptions.manifestURL, + aOptions.serviceType, aOptions.component]); + } else { // Get all alarms of the specified type. + indexName = "type"; + range = IDBKeyRange.only(aType); + } + + let request = aStore.index(indexName).openCursor(range); + request.onsuccess = function onsuccess(aEvent) { + let cursor = aEvent.target.result; + if (cursor) { + aTxn.result.push(this._recordToAlarm(cursor.value)); + cursor.continue(); + return; + } + }.bind(this); + }.bind(this), aResultCb); + }, + + removeAlarm: function(aType, aAlarmId, aResultCb) { + if (DEBUG) { + debug("removeAlarms(" + aAlarmId + ")"); + } + + // Execute clear operation. + this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) { + aStore.get(aAlarmId).onsuccess = function onsuccess(aEvent) { + let alarm = aEvent.target.result; + aTxn.result = false; + if (!alarm || aType !== alarm.type) { + return; + } + + if (DEBUG) { + debug("Remove alarm " + JSON.stringify(alarm) + " from datastore"); + } + aStore.delete(aAlarmId); + aTxn.result = true; + }; + }, aResultCb); + }, + + removeAllAlarms: function(aType, aResultCb) { + if (DEBUG) { + debug(aType + "Mgr.removeAllAlarms()"); + } + + // Execute clear operation. + this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) { + if (DEBUG) { + debug("Remove all " + aType + " alarms from datastore."); + } + + let range = IDBKeyRange.only(aType); + let request = aStore.index("type").openCursor(range); + request.onsuccess = function onsuccess(aEvent) { + let cursor = aEvent.target.result; + if (cursor) { + if (DEBUG) { + debug("Remove " + JSON.stringify(cursor.value) + " from database."); + } + cursor.delete(); + cursor.continue(); + return; + } + }; + }, aResultCb); + }, + + // Get all index keys of the component. + getComponents: function(aType, aResultCb) { + if (DEBUG) { + debug(aType + "Mgr.getComponents()"); + } + + let storeName = statsStoreNames[aType]; + + this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) { + if (!aTxn.result) { + aTxn.result = []; + } + + let request = aStore.index("component").openKeyCursor(null, "nextunique"); + request.onsuccess = function onsuccess(aEvent) { + let cursor = aEvent.target.result; + if (cursor) { + aTxn.result.push(cursor.key); + cursor.continue(); + return; + } + + // Remove "" from the result, which indicates sum of all + // components' stats. + let index = aTxn.result.indexOf(""); + if (index > -1) { + aTxn.result.splice(index, 1); + } + }; + }, aResultCb); + }, + + get sampleRate () { + return SAMPLE_RATE; + }, + + get maxStorageAge() { + return MAX_STORAGE_AGE; + }, +}; + diff --git a/dom/resourcestats/ResourceStatsManager.js b/dom/resourcestats/ResourceStatsManager.js new file mode 100644 index 000000000..649f1873a --- /dev/null +++ b/dom/resourcestats/ResourceStatsManager.js @@ -0,0 +1,480 @@ +/* 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"; + +const DEBUG = false; +function debug(s) { dump("-*- ResourceStatsManager: " + s + "\n"); } + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); + +// Constant defines supported statistics. +const resourceTypeList = ["network", "power"]; + +function NetworkStatsData(aStatsData) { + if (DEBUG) { + debug("NetworkStatsData(): " + JSON.stringify(aStatsData)); + } + + this.receivedBytes = aStatsData.receivedBytes || 0; + this.sentBytes = aStatsData.sentBytes || 0; + this.timestamp = aStatsData.timestamp; +} + +NetworkStatsData.prototype = { + classID: Components.ID("{dce5729a-ba92-4185-8854-e29e71b9e8a2}"), + contractID: "@mozilla.org/networkStatsData;1", + QueryInterface: XPCOMUtils.generateQI([]) +}; + +function PowerStatsData(aStatsData) { + if (DEBUG) { + debug("PowerStatsData(): " + JSON.stringify(aStatsData)); + } + + this.consumedPower = aStatsData.consumedPower || 0; + this.timestamp = aStatsData.timestamp; +} + +PowerStatsData.prototype = { + classID: Components.ID("{acb9af6c-8143-4e59-bc18-4bb1736a4004}"), + contractID: "@mozilla.org/powerStatsData;1", + QueryInterface: XPCOMUtils.generateQI([]) +}; + +function ResourceStats(aWindow, aStats) { + if (DEBUG) { + debug("ResourceStats(): " + JSON.stringify(aStats)); + } + + this._window = aWindow; + this.type = aStats.type; + this.component = aStats.component || null; + this.serviceType = aStats.serviceType || null; + this.manifestURL = aStats.manifestURL || null; + this.start = aStats.start; + this.end = aStats.end; + this.statsData = new aWindow.Array(); + + // A function creates a StatsData object according to type. + let createStatsDataObject = null; + let self = this; + switch (this.type) { + case "power": + createStatsDataObject = function(aStats) { + let chromeObj = new PowerStatsData(aStats); + return self._window.PowerStatsData._create(self._window, chromeObj); + }; + break; + case "network": + createStatsDataObject = function(aStats) { + let chromeObj = new NetworkStatsData(aStats); + return self._window.NetworkStatsData._create(self._window, chromeObj); + }; + break; + } + + let sampleRate = aStats.sampleRate; + let queryResult = aStats.statsData; + let stats = queryResult.pop(); // Pop out the last element. + let timestamp = this.start; + + // Push query result to statsData, and fill empty elements so that: + // 1. the timestamp of the first element of statsData is equal to start; + // 2. the timestamp of the last element of statsData is equal to end; + // 3. the timestamp difference of every neighboring elements is SAMPLE_RATE. + for (; timestamp <= this.end; timestamp += sampleRate) { + if (!stats || stats.timestamp != timestamp) { + // If dataArray is empty or timestamp are not equal, push a dummy object + // (which stats are set to 0) to statsData. + this.statsData.push(createStatsDataObject({ timestamp: timestamp })); + } else { + // Push stats to statsData and pop a new element form queryResult. + this.statsData.push(createStatsDataObject(stats)); + stats = queryResult.pop(); + } + } +} + +ResourceStats.prototype = { + getData: function() { + return this.statsData; + }, + + classID: Components.ID("{b7c970f2-3d58-4966-9633-2024feb5132b}"), + contractID: "@mozilla.org/resourceStats;1", + QueryInterface: XPCOMUtils.generateQI([]) +}; + +function ResourceStatsAlarm(aWindow, aAlarm) { + if (DEBUG) { + debug("ResourceStatsAlarm(): " + JSON.stringify(aAlarm)); + } + + this.alarmId = aAlarm.alarmId; + this.type = aAlarm.type; + this.component = aAlarm.component || null; + this.serviceType = aAlarm.serviceType || null; + this.manifestURL = aAlarm.manifestURL || null; + this.threshold = aAlarm.threshold; + + // Clone data object using structured clone algorithm. + this.data = null; + if (aAlarm.data) { + this.data = Cu.cloneInto(aAlarm.data, aWindow); + } +} + +ResourceStatsAlarm.prototype = { + classID: Components.ID("{e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a}"), + contractID: "@mozilla.org/resourceStatsAlarm;1", + QueryInterface: XPCOMUtils.generateQI([]), +}; + +function ResourceStatsManager() { + if (DEBUG) { + debug("constructor()"); + } +} + +ResourceStatsManager.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + _getPromise: function(aCallback) { + let self = this; + return this.createPromise(function(aResolve, aReject) { + let resolverId = self.getPromiseResolverId({ + resolve: aResolve, + reject: aReject + }); + + aCallback(resolverId); + }); + }, + + // Check time range. + _checkTimeRange: function(aStart, aEnd) { + if (DEBUG) { + debug("_checkTimeRange(): " + aStart + " to " + aEnd); + } + + let start = aStart ? aStart : 0; + let end = aEnd ? aEnd : Date.now(); + + if (start > end) { + throw Cr.NS_ERROR_INVALID_ARG; + } + + return { start: start, end: end }; + }, + + getStats: function(aStatsOptions, aStart, aEnd) { + // Check time range. + let { start: start, end: end } = this._checkTimeRange(aStart, aEnd); + + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:GetStats", { + resolverId: aResolverId, + type: self.type, + statsOptions: aStatsOptions, + manifestURL: self._manifestURL, + start: start, + end: end + }); + }); + }, + + clearStats: function(aStatsOptions, aStart, aEnd) { + // Check time range. + let {start: start, end: end} = this._checkTimeRange(aStart, aEnd); + + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:ClearStats", { + resolverId: aResolverId, + type: self.type, + statsOptions: aStatsOptions, + manifestURL: self._manifestURL, + start: start, + end: end + }); + }); + }, + + clearAllStats: function() { + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:ClearAllStats", { + resolverId: aResolverId, + type: self.type, + manifestURL: self._manifestURL + }); + }); + }, + + addAlarm: function(aThreshold, aStatsOptions, aAlarmOptions) { + if (DEBUG) { + debug("aStatsOptions: " + JSON.stringify(aAlarmOptions)); + debug("aAlarmOptions: " + JSON.stringify(aAlarmOptions)); + } + + // Parse alarm options. + let startTime = aAlarmOptions.startTime || 0; + + // Clone data object using structured clone algorithm. + let data = null; + if (aAlarmOptions.data) { + data = Cu.cloneInto(aAlarmOptions.data, this._window); + } + + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:AddAlarm", { + resolverId: aResolverId, + type: self.type, + threshold: aThreshold, + statsOptions: aStatsOptions, + manifestURL: self._manifestURL, + startTime: startTime, + data: data + }); + }); + }, + + getAlarms: function(aStatsOptions) { + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:GetAlarms", { + resolverId: aResolverId, + type: self.type, + statsOptions: aStatsOptions, + manifestURL: self._manifestURL + }); + }); + }, + + removeAlarm: function(aAlarmId) { + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:RemoveAlarm", { + resolverId: aResolverId, + type: self.type, + manifestURL: self._manifestURL, + alarmId: aAlarmId + }); + }); + }, + + removeAllAlarms: function() { + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:RemoveAllAlarms", { + resolverId: aResolverId, + type: self.type, + manifestURL: self._manifestURL + }); + }); + }, + + getAvailableComponents: function() { + // Create Promise. + let self = this; + return this._getPromise(function(aResolverId) { + self.cpmm.sendAsyncMessage("ResourceStats:GetComponents", { + resolverId: aResolverId, + type: self.type, + manifestURL: self._manifestURL + }); + }); + }, + + get resourceTypes() { + let types = new this._window.Array(); + resourceTypeList.forEach(function(aType) { + types.push(aType); + }); + + return types; + }, + + get sampleRate() { + let msg = { manifestURL: this._manifestURL }; + + return this.cpmm.sendSyncMessage("ResourceStats:SampleRate", msg)[0]; + }, + + get maxStorageAge() { + let msg = { manifestURL: this._manifestURL }; + + return this.cpmm.sendSyncMessage("ResourceStats:MaxStorageAge", msg)[0]; + }, + + receiveMessage: function(aMessage) { + let data = aMessage.data; + let chromeObj = null; + let webidlObj = null; + let self = this; + + if (DEBUG) { + debug("receiveMessage(): " + aMessage.name + " " + data.resolverId); + } + + let resolver = this.takePromiseResolver(data.resolverId); + if (!resolver) { + return; + } + + switch (aMessage.name) { + case "ResourceStats:GetStats:Resolved": + if (DEBUG) { + debug("data.value = " + JSON.stringify(data.value)); + } + + chromeObj = new ResourceStats(this._window, data.value); + webidlObj = this._window.ResourceStats._create(this._window, chromeObj); + resolver.resolve(webidlObj); + break; + + case "ResourceStats:AddAlarm:Resolved": + if (DEBUG) { + debug("data.value = " + JSON.stringify(data.value)); + } + + resolver.resolve(data.value); // data.value is alarmId. + break; + + case "ResourceStats:GetAlarms:Resolved": + if (DEBUG) { + debug("data.value = " + JSON.stringify(data.value)); + } + + let alarmArray = this._window.Array(); + data.value.forEach(function(aAlarm) { + chromeObj = new ResourceStatsAlarm(self._window, aAlarm); + webidlObj = self._window.ResourceStatsAlarm._create(self._window, + chromeObj); + alarmArray.push(webidlObj); + }); + resolver.resolve(alarmArray); + break; + + case "ResourceStats:GetComponents:Resolved": + if (DEBUG) { + debug("data.value = " + JSON.stringify(data.value)); + } + + let components = this._window.Array(); + data.value.forEach(function(aComponent) { + components.push(aComponent); + }); + resolver.resolve(components); + break; + + case "ResourceStats:ClearStats:Resolved": + case "ResourceStats:ClearAllStats:Resolved": + case "ResourceStats:RemoveAlarm:Resolved": + case "ResourceStats:RemoveAllAlarms:Resolved": + if (DEBUG) { + debug("data.value = " + JSON.stringify(data.value)); + } + + resolver.resolve(data.value); + break; + + case "ResourceStats:GetStats:Rejected": + case "ResourceStats:ClearStats:Rejected": + case "ResourceStats:ClearAllStats:Rejected": + case "ResourceStats:AddAlarm:Rejected": + case "ResourceStats:GetAlarms:Rejected": + case "ResourceStats:RemoveAlarm:Rejected": + case "ResourceStats:RemoveAllAlarms:Rejected": + case "ResourceStats:GetComponents:Rejected": + if (DEBUG) { + debug("data.reason = " + JSON.stringify(data.reason)); + } + + resolver.reject(data.reason); + break; + + default: + if (DEBUG) { + debug("Could not find a handler for " + aMessage.name); + } + resolver.reject(); + } + }, + + __init: function(aType) { + if (resourceTypeList.indexOf(aType) < 0) { + if (DEBUG) { + debug("Do not support resource statistics for " + aType); + } + throw Cr.NS_ERROR_INVALID_ARG; + } else { + if (DEBUG) { + debug("Create " + aType + "Mgr"); + } + this.type = aType; + } + }, + + init: function(aWindow) { + if (DEBUG) { + debug("init()"); + } + + // Get the manifestURL if this is an installed app + let principal = aWindow.document.nodePrincipal; + let appsService = Cc["@mozilla.org/AppsService;1"] + .getService(Ci.nsIAppsService); + this._manifestURL = appsService.getManifestURLByLocalId(principal.appId); + + const messages = ["ResourceStats:GetStats:Resolved", + "ResourceStats:ClearStats:Resolved", + "ResourceStats:ClearAllStats:Resolved", + "ResourceStats:AddAlarm:Resolved", + "ResourceStats:GetAlarms:Resolved", + "ResourceStats:RemoveAlarm:Resolved", + "ResourceStats:RemoveAllAlarms:Resolved", + "ResourceStats:GetComponents:Resolved", + "ResourceStats:GetStats:Rejected", + "ResourceStats:ClearStats:Rejected", + "ResourceStats:ClearAllStats:Rejected", + "ResourceStats:AddAlarm:Rejected", + "ResourceStats:GetAlarms:Rejected", + "ResourceStats:RemoveAlarm:Rejected", + "ResourceStats:RemoveAllAlarms:Rejected", + "ResourceStats:GetComponents:Rejected"]; + this.initDOMRequestHelper(aWindow, messages); + + this.cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"] + .getService(Ci.nsISyncMessageSender); + }, + + classID: Components.ID("{101ed1f8-31b3-491c-95ea-04091e6e8027}"), + contractID: "@mozilla.org/resourceStatsManager;1", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsISupportsWeakReference, + Ci.nsIObserver]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsData, + PowerStatsData, + ResourceStats, + ResourceStatsAlarm, + ResourceStatsManager]); + diff --git a/dom/resourcestats/ResourceStatsManager.manifest b/dom/resourcestats/ResourceStatsManager.manifest new file mode 100644 index 000000000..985126ff2 --- /dev/null +++ b/dom/resourcestats/ResourceStatsManager.manifest @@ -0,0 +1,14 @@ +component {dce5729a-ba92-4185-8854-e29e71b9e8a2} ResourceStatsManager.js +contract @mozilla.org/networkStatsData;1 {dce5729a-ba92-4185-8854-e29e71b9e8a2} + +component {acb9af6c-8143-4e59-bc18-4bb1736a4004} ResourceStatsManager.js +contract @mozilla.org/powerStatsData;1 {acb9af6c-8143-4e59-bc18-4bb1736a4004} + +component {b7c970f2-3d58-4966-9633-2024feb5132b} ResourceStatsManager.js +contract @mozilla.org/resourceStats;1 {b7c970f2-3d58-4966-9633-2024feb5132b} + +component {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a} ResourceStatsManager.js +contract @mozilla.org/resourceStatsAlarm;1 {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a} + +component {101ed1f8-31b3-491c-95ea-04091e6e8027} ResourceStatsManager.js +contract @mozilla.org/resourceStatsManager;1 {101ed1f8-31b3-491c-95ea-04091e6e8027} diff --git a/dom/resourcestats/ResourceStatsService.jsm b/dom/resourcestats/ResourceStatsService.jsm new file mode 100644 index 000000000..df4481eab --- /dev/null +++ b/dom/resourcestats/ResourceStatsService.jsm @@ -0,0 +1,334 @@ +/* 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 = ["ResourceStatsService"]; + +const DEBUG = false; +function debug(s) { dump("-*- ResourceStatsService: " + s + "\n"); } + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +// Load ResourceStatsDB. +Cu.import("resource://gre/modules/ResourceStatsDB.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "gIDBManager", + "@mozilla.org/dom/indexeddb/manager;1", + "nsIIndexedDatabaseManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageListenerManager"); + +XPCOMUtils.defineLazyServiceGetter(this, "appsService", + "@mozilla.org/AppsService;1", + "nsIAppsService"); + +this.ResourceStatsService = { + + init: function() { + if (DEBUG) { + debug("Service started"); + } + + // Set notification to observe. + Services.obs.addObserver(this, "xpcom-shutdown", false); + + // Add message listener. + this.messages = ["ResourceStats:GetStats", + "ResourceStats:ClearStats", + "ResourceStats:ClearAllStats", + "ResourceStats:AddAlarm", + "ResourceStats:GetAlarms", + "ResourceStats:RemoveAlarm", + "ResourceStats:RemoveAllAlarms", + "ResourceStats:GetComponents", + "ResourceStats:SampleRate", + "ResourceStats:MaxStorageAge"]; + + this.messages.forEach(function(aMessageName){ + ppmm.addMessageListener(aMessageName, this); + }, this); + + // Create indexedDB. + this._db = new ResourceStatsDB(); + }, + + receiveMessage: function(aMessage) { + if (DEBUG) { + debug("receiveMessage(): " + aMessage.name); + } + + let mm = aMessage.target; + let data = aMessage.data; + + if (DEBUG) { + debug("received aMessage.data = " + JSON.stringify(data)); + } + + // To prevent the hacked child process from sending commands to parent, + // we need to check its permission and manifest URL. + if (!aMessage.target.assertPermission("resourcestats-manage")) { + return; + } + if (!aMessage.target.assertContainApp(data.manifestURL)) { + if (DEBUG) { + debug("Got msg from a child process containing illegal manifestURL."); + } + return; + } + + switch (aMessage.name) { + case "ResourceStats:GetStats": + this.getStats(mm, data); + break; + case "ResourceStats:ClearStats": + this.clearStats(mm, data); + break; + case "ResourceStats:ClearAllStats": + this.clearAllStats(mm, data); + break; + case "ResourceStats:AddAlarm": + this.addAlarm(mm, data); + break; + case "ResourceStats:GetAlarms": + this.getAlarms(mm, data); + break; + case "ResourceStats:RemoveAlarm": + this.removeAlarm(mm, data); + break; + case "ResourceStats:RemoveAllAlarms": + this.removeAllAlarms(mm, data); + break; + case "ResourceStats:GetComponents": + this.getComponents(mm, data); + break; + case "ResourceStats:SampleRate": + // This message is sync. + return this._db.sampleRate; + case "ResourceStats:MaxStorageAge": + // This message is sync. + return this._db.maxStorageAge; + } + }, + + observe: function(aSubject, aTopic, aData) { + switch (aTopic) { + case "xpcom-shutdown": + if (DEBUG) { + debug("Service shutdown " + aData); + } + + this.messages.forEach(function(aMessageName) { + ppmm.removeMessageListener(aMessageName, this); + }, this); + + Services.obs.removeObserver(this, "xpcom-shutdown"); + break; + default: + return; + } + }, + + // Closure generates callback function for DB request. + _createDbCallback: function(aMm, aId, aMessage) { + let resolveMsg = aMessage + ":Resolved"; + let rejectMsg = aMessage + ":Rejected"; + + return function(aError, aResult) { + if (aError) { + aMm.sendAsyncMessage(rejectMsg, { resolverId: aId, reason: aError }); + return; + } + aMm.sendAsyncMessage(resolveMsg, { resolverId: aId, value: aResult }); + }; + }, + + getStats: function(aMm, aData) { + if (DEBUG) { + debug("getStats(): " + JSON.stringify(aData)); + } + + // Note: we validate the manifestURL in _db.getStats(). + let manifestURL = aData.statsOptions.manifestURL || ""; + let serviceType = aData.statsOptions.serviceType || ""; + let component = aData.statsOptions.component || ""; + + // Execute DB operation. + let onStatsGot = this._createDbCallback(aMm, aData.resolverId, + "ResourceStats:GetStats"); + this._db.getStats(aData.type, manifestURL, serviceType, component, + aData.start, aData.end, onStatsGot); + }, + + clearStats: function(aMm, aData) { + if (DEBUG) { + debug("clearStats(): " + JSON.stringify(aData)); + } + + // Get appId and check whether manifestURL is a valid app. + let appId = 0; + let manifestURL = aData.statsOptions.manifestURL || ""; + if (manifestURL) { + appId = appsService.getAppLocalIdByManifestURL(manifestURL); + + if (!appId) { + aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", { + resolverId: aData.resolverId, + reason: "Invalid manifestURL" + }); + return; + } + } + + let serviceType = aData.statsOptions.serviceType || ""; + let component = aData.statsOptions.component || ""; + + // Execute DB operation. + let onStatsCleared = this._createDbCallback(aMm, aData.resolverId, + "ResourceStats:ClearStats"); + this._db.clearStats(aData.type, appId, serviceType, component, + aData.start, aData.end, onStatsCleared); + }, + + clearAllStats: function(aMm, aData) { + if (DEBUG) { + debug("clearAllStats(): " + JSON.stringify(aData)); + } + + // Execute DB operation. + let onAllStatsCleared = this._createDbCallback(aMm, aData.resolverId, + "ResourceStats:ClearAllStats"); + this._db.clearAllStats(aData.type, onAllStatsCleared); + }, + + addAlarm: function(aMm, aData) { + if (DEBUG) { + debug("addAlarm(): " + JSON.stringify(aData)); + } + + // Get appId and check whether manifestURL is a valid app. + let manifestURL = aData.statsOptions.manifestURL; + if (manifestURL) { + let appId = appsService.getAppLocalIdByManifestURL(manifestURL); + + if (!appId) { + aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", { + resolverId: aData.resolverId, + reason: "Invalid manifestURL" + }); + return; + } + } + + // Create an alarm object. + let newAlarm = { + type: aData.type, + component: aData.statsOptions.component || "", + serviceType: aData.statsOptions.serviceType || "", + manifestURL: manifestURL || "", + threshold: aData.threshold, + startTime: aData.startTime, + data: aData.data + }; + + // Execute DB operation. + let onAlarmAdded = this._createDbCallback(aMm, aData.resolverId, + "ResourceStats:AddAlarm"); + this._db.addAlarm(newAlarm, onAlarmAdded); + }, + + getAlarms: function(aMm, aData) { + if (DEBUG) { + debug("getAlarms(): " + JSON.stringify(aData)); + } + + let options = null; + let statsOptions = aData.statsOptions; + // If all keys in statsOptions are set to null, treat this call as quering + // all alarms; otherwise, resolve the statsOptions and perform DB query + // according to the resolved result. + if (statsOptions.manifestURL || statsOptions.serviceType || + statsOptions.component) { + // Validate manifestURL. + let manifestURL = statsOptions.manifestURL || ""; + if (manifestURL) { + let appId = appsService.getAppLocalIdByManifestURL(manifestURL); + + if (!appId) { + aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", { + resolverId: aData.resolverId, + reason: "Invalid manifestURL" + }); + return; + } + } + + options = { + manifestURL: manifestURL, + serviceType: statsOptions.serviceType || "", + component: statsOptions.component || "" + }; + } + + // Execute DB operation. + let onAlarmsGot = this._createDbCallback(aMm, aData.resolverId, + "ResourceStats:GetAlarms"); + this._db.getAlarms(aData.type, options, onAlarmsGot); + }, + + removeAlarm: function(aMm, aData) { + if (DEBUG) { + debug("removeAlarm(): " + JSON.stringify(aData)); + } + + // Execute DB operation. + let onAlarmRemoved = function(aError, aResult) { + if (aError) { + aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected", + { resolverId: aData.resolverId, reason: aError }); + } + + if (!aResult) { + aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected", + { resolverId: aData.resolverId, + reason: "alarm not existed" }); + } + + aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Resolved", + { resolverId: aData.resolverId, value: aResult }); + }; + + this._db.removeAlarm(aData.type, aData.alarmId, onAlarmRemoved); + }, + + removeAllAlarms: function(aMm, aData) { + if (DEBUG) { + debug("removeAllAlarms(): " + JSON.stringify(aData)); + } + + // Execute DB operation. + let onAllAlarmsRemoved = this._createDbCallback(aMm, aData.resolverId, + "ResourceStats:RemoveAllAlarms"); + this._db.removeAllAlarms(aData.type, onAllAlarmsRemoved); + }, + + getComponents: function(aMm, aData) { + if (DEBUG) { + debug("getComponents(): " + JSON.stringify(aData)); + } + + // Execute DB operation. + let onComponentsGot = this._createDbCallback(aMm, aData.resolverId, + "ResourceStats:GetComponents"); + this._db.getComponents(aData.type, onComponentsGot); + }, +}; + +this.ResourceStatsService.init(); + diff --git a/dom/resourcestats/moz.build b/dom/resourcestats/moz.build new file mode 100644 index 000000000..437457a63 --- /dev/null +++ b/dom/resourcestats/moz.build @@ -0,0 +1,22 @@ +# -*- 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/. + +EXTRA_COMPONENTS += [ + 'ResourceStatsManager.js', + 'ResourceStatsManager.manifest', +] + +EXTRA_JS_MODULES += [ + 'ResourceStatsDB.jsm', + 'ResourceStatsService.jsm', +] + +FINAL_LIBRARY = 'xul' + +if CONFIG['MOZ_B2G_RIL']: + XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini'] + +MOCHITEST_MANIFESTS += ['tests/mochitest/mochitest.ini'] diff --git a/dom/resourcestats/tests/mochitest/mochitest.ini b/dom/resourcestats/tests/mochitest/mochitest.ini new file mode 100644 index 000000000..dbcb05a5f --- /dev/null +++ b/dom/resourcestats/tests/mochitest/mochitest.ini @@ -0,0 +1,16 @@ +[test_basic.html] +skip-if = toolkit != "gonk" +[test_network_stats.html] +skip-if = toolkit != "gonk" +[test_power_stats.html] +skip-if = toolkit != "gonk" +[test_network_alarm.html] +skip-if = toolkit != "gonk" +[test_power_alarm.html] +skip-if = toolkit != "gonk" +[test_disabled_pref.html] +skip-if = toolkit != "gonk" +[test_no_perm.html] +skip-if = toolkit != "gonk" +[test_not_supported_type.html] +skip-if = toolkit != "gonk" diff --git a/dom/resourcestats/tests/mochitest/test_basic.html b/dom/resourcestats/tests/mochitest/test_basic.html new file mode 100644 index 000000000..d01762ddf --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_basic.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test accessibility of interfaces</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// Test accessibility of interfaces. +SpecialPowers.addPermission("resourcestats-manage", true, document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", true], + ["dom.ignore_webidl_scope_checks", true] + ]}, function() { + ok(SpecialPowers.hasPermission("resourcestats-manage", document), + "Has permission 'resourcestats-manage'."); + ok(SpecialPowers.getBoolPref("dom.resource_stats.enabled"), + "Preference 'dom.resource_stats.enabled' is true."); + + // Check all interfaces are accessible. + ok('ResourceStatsManager' in window, "ResourceStatsManager exist."); + ok('ResourceStatsAlarm' in window, "ResourceStatsAlarm exist."); + ok('ResourceStats' in window, "ResourceStats exist."); + ok('NetworkStatsData' in window, "NetworkStatsData exist."); + ok('PowerStatsData' in window, "PowerStatsData exist."); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/mochitest/test_disabled_pref.html b/dom/resourcestats/tests/mochitest/test_disabled_pref.html new file mode 100644 index 000000000..1708ac0ae --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_disabled_pref.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test to ensure interface is not accessible when preference is disabled</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// Test to ensure interface is not accessible when preference is disabled. +SpecialPowers.addPermission("resourcestats-manage", true, document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", false], + ["dom.ignore_webidl_scope_checks", true] + ]}, function() { + ok(SpecialPowers.hasPermission("resourcestats-manage", document), + "Has permission 'resourcestats-manage'."); + ok(!(SpecialPowers.getBoolPref("dom.resource_stats.enabled")), + "Preference 'dom.resource_stats.enabled' is false."); + + // Check accessibility. + is('ResourceStatsManager' in window, false, "ResourceStatsManager should not exist."); + is('ResourceStatsAlarm' in window, false, "ResourceStatsAlarm should not exist."); + is('ResourceStats' in window, false, "ResourceStats should not exist."); + is('NetworkStatsData' in window, false, "NetworkStatsData should not exist."); + is('PowerStatsData' in window, false, "PowerStatsData should not exist."); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/mochitest/test_network_alarm.html b/dom/resourcestats/tests/mochitest/test_network_alarm.html new file mode 100644 index 000000000..2b4afd677 --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_network_alarm.html @@ -0,0 +1,356 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for ResourceStats methods realted to network resource control</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp"; +const wifiComponent = "wifi:0"; +const mobileComponent = "mobile:1"; +var networkStatsMgr = null; // ResourceStatsManager for network statistics. + +function errorCb(reason) { + ok(false, reason); +} + +// Check the content returned by getAlarms. +function checkAlarmsArray(alarms) { + // Check if data is an array. + if (!Array.isArray(alarms)) { + throw "getAlarms does not return an array."; + } else { + ok(true, "getAlarms returns an array.") + } + + // Iterate the array and check the type of each element. + var obj = null; + var message = null; // Message for exception + + for (var i = 0; i < alarms.length; i++) { + obj = alarms[i]; + + // Check if obj is an instance os ResourceStatsAlarm. + if (!(obj instanceof ResourceStatsAlarm)) { + message = "The array contains a non-ResourceStatsAlarm object."; + break; + } + + // Check if obj.type is network. + if (obj.type != "network") { + message = "The type of a ResourceStatsAlarm object is not network."; + break; + } + } + + if (message) { + throw message; + } + + ok(true, "The return is an array of ResourceStatsAlarm objects."); +} + +// Test Cases for testing WebIDL methods related to resource control. +var testCases = [ + function() { + // Test removeAllAlarms. + var promise = networkStatsMgr.removeAllAlarms(); + promise.then(function() { + ok(true, "removeAllAlarms deleted all network alarms."); + testMethods(); + }, function() { + ok(false, "removeAllAlarms failed to delete network alarms."); + }); + }, + + function() { + // Test addAlarm. + var threshold = Math.floor(Math.random() * 1000); + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent }, + { 'startTime': Date.now() }); + promise.then(function(value) { + // Check the value (alarmId). + if (value < 0) { + ok(false, "addAlarm failed to create an alarm."); + } else { + ok(true, "addAlarm created an alarm."); + testMethods(); + } + }, function() { + ok(false, "addAlarm failed to create an alarm."); + }); + }, + + function() { + // Test addAlarm with negative threshold. + var threshold = -1; + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent }, + { 'startTime': Date.now() }); + promise.then(function() { + // Check the value. + ok(false, + "addAlarm did not throw an exception when negative threshold is set."); + }, function() { + ok(true, "addAlarm threw an exception when negative threshold is set."); + testMethods(); + }); + }, + + function() { + // Test addAlarm with no threshold. + var promise = networkStatsMgr.addAlarm(); + promise.then(function() { + // Check the value. + ok(false, "addAlarm did not throw an exception when no threshold."); + }, function() { + ok(true, "addAlarm threw an exception when no threshold."); + testMethods(); + }); + }, + + function() { + // Test addAlarm with negative startTime. + var threshold = Math.floor(Math.random() * 1000); + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent }, + { 'startTime': -1 }); + promise.then(function() { + // Check the value. + ok(false, + "addAlarm did not throw an exception when negative startTime is set."); + }, function() { + ok(true, "addAlarm threw an exception when negative startTime is set."); + testMethods(); + }); + }, + + function() { + // Test addAlarm when manifestURL is invalid. + var threshold = Math.floor(Math.random() * 1000); + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent, + 'manifestURL': invalidManifestURL }, + { 'startTime': Date.now() }); + promise.then(function() { + // Check the value. + ok(false, "addAlarm did not throw an exception when manifestURL is invalid."); + }, function() { + ok(true, "addAlarm threw an exception when manifestURL is invalid."); + testMethods(); + }); + }, + + function() { + // Test getAlarms. + var alarmId; + var alarms; + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent }, + { 'startTime': Date.now() }); + + // 2. Test getAlarms if new alarm is added. + var runGetAlarms = function(value) { + alarmId = value; + return networkStatsMgr.getAlarms({ 'component': wifiComponent }); + }; + + // 3. Check the content returned by getAlarms. + var checkGetAlarmsReturn = function(value) { + alarms = value; + checkAlarmsArray(value); + }; + + // 4. Check the alarm added in step 1 is inside the return of getAlarms. + var checkAlarm = function (value) { + // Find the alarm. + var index = alarms.map(function(e) { return e.alarmId; }) + .indexOf(alarmId); + if (index < 0) { + throw "getAlarms does not get the alarm added in previous step."; + } + var alarm = alarms[index]; + + // Evaluate the alarm. + ok(alarm.threshold == threshold, "threshold is equal."); + ok(alarm.component == wifiComponent, "component is equal."); + ok(alarm.serviceType == null, "serviceType should be null."); + ok(alarm.manifestURL == null, "manifestURL should be null."); + }; + + // Create promise chaining. + promise.then(runGetAlarms) + .then(checkGetAlarmsReturn) + .then(checkAlarm) + .then(testMethods, errorCb); // Execute next test case. + }, + + function() { + // Test getAlarms with invalid manifestURL. + var threshold = Math.floor(Math.random() * 1000); + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent, + 'manifestURL': invalidManifestURL }, + { 'startTime': Date.now() }); + + promise.then(function() { + // Check the value. + ok(false, "getAlarms did not throw an exception when manifestURL is invalid."); + }, function() { + ok(true, "getAlarms threw an exception when manifestURL is invalid."); + testMethods(); + }); + }, + + function() { + // Test getAlarms with incorrect parameter. + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent }, + { 'startTime': Date.now() }); + + // 2. Call getAlarms with incorrect parameter. + var runGetAlarms = function() { + return networkStatsMgr.getAlarms({ 'component': mobileComponent }); + }; + + // 3. check the content returned by getAlarms. + var checkGetAlarmsReturn = function(value) { + // Check array length + if (value.length) { + throw "getAlarms gets an alarm when using incorrect parameter."; + } else { + ok(true, + "getAlarms returns an empty array when using incorrect parameter."); + } + }; + + // Create pomise chaining. + promise.then(runGetAlarms) + .then(checkGetAlarmsReturn) + .then(testMethods, errorCb); // Execute next test case. + }, + + function() { + // Test removeAlarm + var alarmId; + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent }, + { 'startTime': Date.now() }); + + // 2. Try to remove the new alarm. + var runRemoveAlarm = function(value) { + alarmId = value; + return networkStatsMgr.removeAlarm(alarmId); + } + + // Create promise chaining. + promise.then(runRemoveAlarm) + .then(function() { + ok(true, "removeAlarm deleted the alarm."); + testMethods(); + }, errorCb); + }, + + function() { + // Test removeAlarm with negative alarmId + var alarmId = -1; + var promise = networkStatsMgr.removeAlarm(alarmId); + promise.then(function() { + ok(false, + "removeAlarm did not throw an exception when negative alarmId is set."); + }, function() { + ok(true, "removeAlarm threw an exception when negative alarmId is set."); + testMethods(); + }); + }, + + function() { + // Test removeAlarm with invalid alarmId + var alarmId; + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = networkStatsMgr.addAlarm(threshold, + { 'component': wifiComponent }, + { 'startTime': Date.now() }); + + // 2. Try to remove an invalid alarm. + var runRemoveAlarm = function(value) { + alarmId = value; + // Because alarmId is auto-increment, any alarmId larger than the + // latest alarmId should be invalid. + return networkStatsMgr.removeAlarm(alarmId + 10); + } + + // Create promise chaining. + promise.then(runRemoveAlarm) + .then(function() { + // Input with incorrect alarmId should not be resolved. + throw "removeAlarm should fail with invalid alarmId."; + }, function(reason) { + if (reason == "alarm not existed") { + ok(true, "removeAlarm with invalid alarmId should fail.") + } else { + throw reason; + } + }) + .then(testMethods, errorCb); + } +]; + +// Test WebIDL methods related stats operation. +function testMethods() { + if (!testCases.length) { + ok(true, "Done."); + SpecialPowers.removePermission("resourcestats-manage", document); + SimpleTest.finish(); + return; + } + + var testCase = testCases.shift(); + testCase(); +} + +function startTest() { + // Create an instance of ResourceStatsManager for network. + networkStatsMgr = new ResourceStatsManager("network"); + ok(networkStatsMgr, "Create networkStatsMgr."); + + // Test WebIDL methods related to resource control. + testMethods(); +} + +SimpleTest.waitForExplicitFinish(); + +// Enable permission and preference. +SpecialPowers.addPermission("resourcestats-manage", true, document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", true], + ["dom.ignore_webidl_scope_checks", true] + ]}, startTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/mochitest/test_network_stats.html b/dom/resourcestats/tests/mochitest/test_network_stats.html new file mode 100644 index 000000000..bee1914b3 --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_network_stats.html @@ -0,0 +1,345 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for ResourceStats methods realted to network statistics</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp"; +var networkStatsMgr = null; // ResourceStatsManager for network statistics. +var sampleRate = 0; + +// Test WebIDL attributes. +function testAttributes() { + // Test sampleRate. + ok('sampleRate' in networkStatsMgr, + "sampleRate should be a ResourceStatsManager attribute."); + sampleRate = networkStatsMgr.sampleRate; + ok(sampleRate > 0, "sampleRate is greater than 0."); + + // Test maxStorageAge. + ok('maxStorageAge' in networkStatsMgr, + "maxStorageAge should be a ResourceStatsManager attribute."); + ok(networkStatsMgr.maxStorageAge > 0, + "maxStorageAge is greater than 0."); + + // Test whether "network" in resourceTypes array. + ok('resourceTypes' in networkStatsMgr, + "resourceTypes should be a ResourceStatsManager attribute."); + ok(Array.isArray(networkStatsMgr.resourceTypes), + "networkStatsMgr.resourceTypes is an array."); + ok(networkStatsMgr.resourceTypes.indexOf("network") > -1, + "'network' is an element of networkStatsMgr.resourceTypes."); +} + +// Check the content returned by ResourceStats.getData(). +function checkData(data, start, end) { + // Check if data is an array. + if (!Array.isArray(data)) { + ok(false, "getData does not return an array.") + return; + } else { + ok(true, "getData returns an array.") + } + + // Iterate the array and check the timestamp and type of each element. + var success = true; + var obj = null; + var timestamp = start; + var i = 0; + var length = data.length; + + do { + obj = data[i++]; + + // Check object type. + if (!(obj instanceof NetworkStatsData)) { + success = false; + ok(false, "The array contains a non-NetworkStatsData object."); + break; + } + + // Check if the timestamp is continuous. + if (obj.timestamp !== timestamp) { + success = false; + ok(false, "The timestamp of NetworkStatsData object is correct."); + break; + } + + timestamp += sampleRate; + } while (i < length); + + if (!success) { + return; + } + + // Check the timestamp of the last element is equal to end. + if (obj.timestamp != end) { + ok(false, + "The timestamp of the last element of the array is equal to end."); + return; + } + + // Execute next test case. + ok(true, "The return of getData is an array of NetworkStatsData objects."); + testMethods(); +} + +// Test Cases for testing WebIDL methods. +var testCases = [ + function() { + // Test clearAllStats. + var promise = networkStatsMgr.clearAllStats(); + promise.then(function() { + ok(true, "clearAllStats clears the network store."); + testMethods(); + }, function() { + ok(false, "clearAllStats fails to clear the network store."); + }); + }, + + function() { + // Test clearStats. + var promise = networkStatsMgr.clearStats(); + promise.then(function() { + ok(true, "clearStats clears the network store."); + testMethods(); + }, function() { + ok(false, "clearStats fails to clear the network store."); + }); + }, + + function() { + // Check if clearStats throw exception when start is great than end. + var end = Date.now(); + var start = end + 1000; + var promise = networkStatsMgr.clearStats(null, start, end); + promise.then(function() { + ok(false, + "clearStats does not throw exception when start is great than end."); + }, function() { + ok(true, "clearStats throw exception when start is great than end."); + testMethods(); + }); + }, + + function() { + // Check if clearStats throw exception when start is less than 0. + var end = Date.now(); + var start = -1; + var promise = networkStatsMgr.clearStats(null, start, end); + promise.then(function() { + ok(false, + "clearStats dose not throw exception when start is less than 0."); + }, function() { + ok(true, "clearStats throw exception when start is less than 0."); + testMethods(); + }); + }, + + function() { + // Check if clearStats throw exception when manifestURL is invalid. + var options = {manifestURL: invalidManifestURL}; + var promise = networkStatsMgr.clearStats(options); + promise.then(function() { + ok(false, + "clearStats does not throw exception when manifestURL is invalid."); + }, function() { + ok(true, "clearStats throw exception when manifestURL is invalid."); + testMethods(); + }); + }, + + function() { + // Test getAvailableComponents. + var promise = networkStatsMgr.getAvailableComponents(); + promise.then(function(value) { + if (Array.isArray(value)) { + ok(true, "getAvailableComponents returns an array."); + testMethods(); + } else { + ok(false, "getAvailableComponents does not return an array."); + } + }, function() { + ok(false, "Fail to execute getAvailableComponents."); + }); + }, + + function() { + // Test getStats. + ok(true, "Get system stats when start and end are adapted to sampleRate."); + + // Prepare start and end. + var offset = (new Date()).getTimezoneOffset() * 60 * 1000; + var end = Math.floor((Date.now() - offset) / sampleRate) * sampleRate + offset; + var start = end - sampleRate * 10; + + // Launch request. + var promise = networkStatsMgr.getStats(null, start, end); + promise.then(function(value) { + // Check the object type. + if (value instanceof ResourceStats) { + ok(true, "Get a ResourceStats object."); + } else { + ok(false, "Fail to get a ResourceStats object."); + return; + } + + // Check attributes of ResourceStats. + ok(value.type == "network", "type should be network."); + ok(value.component == null, "component should be null."); + ok(value.serviceType == null, "serviceType should be null."); + ok(value.manifestURL == null, "manifestURL should be null."); + + // Check if the time range of ResourceStats is equal to the request. + ok(value.start == start, "start timestamp should be equal."); + ok(value.end == end, "end timestamp should be equal."); + + // Check stats stored inside ResourceStats. + if ('getData' in value) { + checkData(value.getData(), start, end); + } else { + ok(false, "getData is not a method of ResourceStats."); + return; + } + }, function() { + ok(false, "Get network stats failed."); + }); + }, + + function() { + // Test getStats when start and end are not adapted to sampleRate. + ok(true, + "Get system stats when start and end are not adapted to sampleRate."); + + // Prepare start and end. + var end = Date.now(); + var start = end - sampleRate * 10; + + // Normalize start and end. + var offset = (new Date()).getTimezoneOffset() * 60 * 1000; + var normEnd = Math.floor((end - offset) / sampleRate) + * sampleRate + offset; + var normStart = Math.floor((start - offset) / sampleRate) + * sampleRate + offset; + + // Launch request. + var promise = networkStatsMgr.getStats(null, start, end); + promise.then(function(value) { + // Check the object type. + if (value instanceof ResourceStats) { + ok(true, "Get a ResourceStats object."); + } else { + ok(false, "Fail to get a ResourceStats object."); + return; + } + + // Check attributes of ResourceStats. + ok(value.type == "network", "type should be network."); + ok(value.component == null, "component should be null."); + ok(value.serviceType == null, "serviceType should be null."); + ok(value.manifestURL == null, "manifestURL should be null."); + + // Check if time range of ResourceStats are normalized. + ok(value.start == normStart, "start timestamp should be normalized."); + ok(value.end == normEnd, "end timestamp should be normalized."); + + // Check stats stored inside ResourceStats. + if ('getData' in value) { + checkData(value.getData(), normStart, normEnd); + } else { + ok(false, "getData is not a method of ResourceStats."); + return; + } + }, function() { + ok(false, "Get network stats failed."); + }); + }, + + function () { + // Check if getStats throw exception when start is greater than end. + var end = Date.now(); + var start = end + 1000; + var promise = networkStatsMgr.getStats(null, start, end); + promise.then(function() { + ok(false, + "getStats dose not throw exception when start is great than end."); + }, function() { + ok(true, "getStats throw exception when start is great than end."); + testMethods(); + }); + }, + + function() { + // Check if getStats throw exception when start is less than 0. + var end = Date.now(); + var start = -1; + var promise = networkStatsMgr.getStats(null, start, end); + promise.then(function() { + ok(false, + "getStats dose not throw exception when start is less than 0."); + }, function() { + ok(true, "getStats throw exception when start is less than 0."); + testMethods(); + }); + }, + + function() { + // Check if getStats throw exception when manifestURL is invalid. + var options = {manifestURL: invalidManifestURL}; + var promise = networkStatsMgr.getStats(options); + promise.then(function(value) { + ok(false, + "getStats does not throw exception when manifestURL is invalid."); + }, function() { + ok(true, "getStats throw exception when manifestURL is invalid."); + testMethods(); + }); + } +]; + +// Test WebIDL methods related stats operation. +function testMethods() { + if (!testCases.length) { + ok(true, "Done."); + SpecialPowers.removePermission("resourcestats-manage", document); + SimpleTest.finish(); + return; + } + + var testCase = testCases.shift(); + testCase(); +} + +function startTest() { + // Create an instance of ResourceStatsManager for network stats. + networkStatsMgr = new window.ResourceStatsManager("network"); + ok(networkStatsMgr, "Create networkStatsMgr."); + + // Test WebIDL attributes. + testAttributes(); + + // Test WebIDL methods related to stats operation. + testMethods(); +} + +SimpleTest.waitForExplicitFinish(); + +// Enable permission and preference. +SpecialPowers.addPermission("resourcestats-manage", true, document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", true], + ["dom.ignore_webidl_scope_checks", true] + ]}, startTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/mochitest/test_no_perm.html b/dom/resourcestats/tests/mochitest/test_no_perm.html new file mode 100644 index 000000000..d32c5ee88 --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_no_perm.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test to ensure ResourceStatsManager is not accessible without permission</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// Test to ensure ResourceStatsManager is not accessible without permission. +SpecialPowers.removePermission("resourcestats-manage", document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", true], + ["dom.ignore_webidl_scope_checks", true] + ]}, function() { + ok(!(SpecialPowers.hasPermission("resourcestats-manage", document)), + "Do not have permission 'resourcestats-manage'."); + ok(SpecialPowers.getBoolPref("dom.resource_stats.enabled"), + "Preference 'dom.resource_stats.enabled' is true."); + + // Check ResourceStatsManager exist. + ok(!('ResourceStatsManager' in window), + "Cannot access window.ResourceStatsManager when have no permission"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/mochitest/test_not_supported_type.html b/dom/resourcestats/tests/mochitest/test_not_supported_type.html new file mode 100644 index 000000000..50ffb3b59 --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_not_supported_type.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test to ensure ResourceStatsManager does not create an instance for non-supported type</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +const type = "non-supported"; + +SimpleTest.waitForExplicitFinish(); + +// Test to ensure ResourceStatsManager does not create an instance for +// non-supported type. +SpecialPowers.addPermission("resourcestats-manage", true, document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", true], + ["dom.ignore_webidl_scope_checks", true] + ]}, function() { + try { + var mgr = ResourceStatsManager(type); + ok(false, + "Creating an instance for non-supported type should throw an exeception."); + } catch (ex) { + ok(ex, + "Got an exception when creating an instance for non-supported type."); + } + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/mochitest/test_power_alarm.html b/dom/resourcestats/tests/mochitest/test_power_alarm.html new file mode 100644 index 000000000..abce5cfa4 --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_power_alarm.html @@ -0,0 +1,355 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for ResourceStats methods realted to power resource control</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp"; +const cpuComponent = "cpu:0"; +const gpsComponent = "gps:0"; +var powerStatsMgr = null; // ResourceStatsManager for power statistics. + +function errorCb(reason) { + ok(false, reason); +} + +// Check the content returned by getAlarms. +function checkAlarmsArray(alarms) { + // Check if data is an array. + if (!Array.isArray(alarms)) { + throw "getAlarms does not return an array."; + } else { + ok(true, "getAlarms returns an array.") + } + + // Iterate the array and check the type of each element. + var obj = null; + var message = null; // Message for exception + + for (var i = 0; i < alarms.length; i++) { + obj = alarms[i]; + + // Check if obj is an instance os ResourceStatsAlarm. + if (!(obj instanceof ResourceStatsAlarm)) { + message = "The array contains a non-ResourceStatsAlarm object."; + break; + } + + // Check if obj.type is power. + if (obj.type != "power") { + message = "The type of a ResourceStatsAlarm object is not power."; + break; + } + } + + if (message) { + throw message; + } + + ok(true, "The return is an array of ResourceStatsAlarm objects."); +} + +// Test Cases for testing WebIDL methods related to resource control. +var testCases = [ + function() { + // Test removeAllAlarms. + var promise = powerStatsMgr.removeAllAlarms(); + promise.then(function() { + ok(true, "removeAllAlarms deleted all power alarms."); + testMethods(); + }, function() { + ok(false, "removeAllAlarms failed to delete power alarms."); + }); + }, + + function() { + // Test addAlarm. + var threshold = Math.floor(Math.random() * 1000); + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent }, + { 'startTime': Date.now() }); + promise.then(function(value) { + // Check the value (alarmId). + if (value < 0) { + ok(false, "addAlarm failed to create an alarm."); + } else { + ok(true, "addAlarm created an alarm."); + testMethods(); + } + }, function() { + ok(false, "addAlarm failed to create an alarm."); + }); + }, + + function() { + // Test addAlarm with negative threshold. + var threshold = -1; + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent }, + { 'startTime': Date.now() }); + promise.then(function() { + // Check the value. + ok(false, + "addAlarm did not throw an exception when negative threshold is set."); + }, function() { + ok(true, "addAlarm threw an exception when negative threshold is set."); + testMethods(); + }); + }, + + function() { + // Test addAlarm with no threshold. + var promise = powerStatsMgr.addAlarm(); + promise.then(function() { + // Check the value. + ok(false, "addAlarm did not throw an exception when no threshold."); + }, function() { + ok(true, "addAlarm threw an exception when no threshold."); + testMethods(); + }); + }, + + function() { + // Test addAlarm with negative startTime. + var threshold = Math.floor(Math.random() * 1000); + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent }, + { 'startTime': -1 }); + promise.then(function() { + // Check the value. + ok(false, + "addAlarm did not throw an exception when negative startTime is set."); + }, function() { + ok(true, "addAlarm threw an exception when negative startTime is set."); + testMethods(); + }); + }, + + function() { + // Test addAlarm when manifestURL is invalid. + var threshold = Math.floor(Math.random() * 1000); + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent, + 'manifestURL': invalidManifestURL }, + { 'startTime': Date.now() }); + promise.then(function() { + // Check the value. + ok(false, "addAlarm did not throw an exception when manifestURL is invalid."); + }, function() { + ok(true, "addAlarm threw an exception when manifestURL is invalid."); + testMethods(); + }); + }, + + function() { + // Test getAlarms. + var alarmId; + var alarms; + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent }, + { 'startTime': Date.now() }); + + // 2. Test getAlarms if new alarm is added. + var runGetAlarms = function(value) { + alarmId = value; + return powerStatsMgr.getAlarms({ 'component': cpuComponent }); + }; + + // 3. Check the content returned by getAlarms. + var checkGetAlarmsReturn = function(value) { + alarms = value; + checkAlarmsArray(value); + }; + + // 4. Check the alarm added in step 1 is inside the return of getAlarms. + var checkAlarm = function (value) { + // Find the alarm. + var index = alarms.map(function(e) { return e.alarmId; }) + .indexOf(alarmId); + if (index < 0) { + throw "getAlarms does not get the alarm added in previous step."; + } + var alarm = alarms[index]; + + // Evaluate the alarm. + ok(alarm.threshold == threshold, "threshold is equal."); + ok(alarm.component == cpuComponent, "component is equal."); + ok(alarm.serviceType == null, "serviceType should be null."); + ok(alarm.manifestURL == null, "manifestURL should be null."); + }; + + // Create promise chaining. + promise.then(runGetAlarms) + .then(checkGetAlarmsReturn) + .then(checkAlarm) + .then(testMethods, errorCb); // Execute next test case. + }, + + function() { + // Test getAlarms with invalid manifestURL. + var threshold = Math.floor(Math.random() * 1000); + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent, + 'manifestURL': invalidManifestURL }, + { 'startTime': Date.now() }); + + promise.then(function() { + // Check the value. + ok(false, "getAlarms did not throw an exception when manifestURL is invalid."); + }, function() { + ok(true, "getAlarms threw an exception when manifestURL is invalid."); + testMethods(); + }); + }, + + function() { + // Test getAlarms with incorrect parameter. + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent }, + { 'startTime': Date.now() }); + + // 2. Call getAlarms with incorrect parameter. + var runGetAlarms = function() { + return powerStatsMgr.getAlarms({ 'component': gpsComponent }); + }; + + // 3. check the content returned by getAlarms. + var checkGetAlarmsReturn = function(value) { + // Check array length + if (value.length) { + throw "getAlarms gets an alarm when using incorrect parameter."; + } else { + ok(true, + "getAlarms returns an empty array when using incorrect parameter."); + } + }; + + // Create pomise chaining. + promise.then(runGetAlarms) + .then(checkGetAlarmsReturn) + .then(testMethods, errorCb); // Execute next test case. + }, + + function() { + // Test removeAlarm + var alarmId; + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent }, + { 'startTime': Date.now() }); + + // 2. Try to remove the new alarm. + var runRemoveAlarm = function(value) { + alarmId = value; + return powerStatsMgr.removeAlarm(alarmId); + } + + // Create promise chaining. + promise.then(runRemoveAlarm) + .then(function() { + ok(true, "removeAlarm deleted the alarm."); + testMethods(); + }, errorCb); + }, + + function() { + // Test removeAlarm with negative alarmId + var alarmId = -1; + var promise = powerStatsMgr.removeAlarm(alarmId); + promise.then(function() { + ok(false, + "removeAlarm did not throw an exception when negative alarmId is set."); + }, function() { + ok(true, "removeAlarm threw an exception when negative alarmId is set."); + testMethods(); + }); + }, + + function() { + // Test removeAlarm with invalid alarmId + var alarmId; + var threshold = Math.floor(Math.random() * 1000); + + // Execution steps: + // 1. Add a new alarm. + var promise = powerStatsMgr.addAlarm(threshold, + { 'component': cpuComponent }, + { 'startTime': Date.now() }); + + // 2. Try to remove an invalid alarm. + var runRemoveAlarm = function(value) { + alarmId = value; + // Because alarmId is auto-increment, any alarmId larger than the + // latest alarmId should be invalid. + return powerStatsMgr.removeAlarm(alarmId + 10); + } + + // Create promise chaining. + promise.then(runRemoveAlarm) + .then(function() { + // Input with incorrect alarmId should not be resolved. + throw "removeAlarm should fail with invalid alarmId."; + }, function(reason) { + if (reason == "alarm not existed") { + ok(true, "removeAlarm with invalid alarmId should fail.") + } else { + throw reason; + } + }) + .then(testMethods, errorCb); + } +]; + +// Test WebIDL methods related stats operation. +function testMethods() { + if (!testCases.length) { + ok(true, "Done."); + SpecialPowers.removePermission("resourcestats-manage", document); + SimpleTest.finish(); + return; + } + + var testCase = testCases.shift(); + testCase(); +} + +function startTest() { + // Create an instance of ResourceStatsManager for power. + powerStatsMgr = new ResourceStatsManager("power"); + ok(powerStatsMgr, "Create powerStatsMgr."); + + // Test WebIDL methods related to resource control. + testMethods(); +} + +SimpleTest.waitForExplicitFinish(); + +// Enable permission and preference. +SpecialPowers.addPermission("resourcestats-manage", true, document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", true], + ["dom.ignore_webidl_scope_checks", true] + ]}, startTest); +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/mochitest/test_power_stats.html b/dom/resourcestats/tests/mochitest/test_power_stats.html new file mode 100644 index 000000000..a1327df59 --- /dev/null +++ b/dom/resourcestats/tests/mochitest/test_power_stats.html @@ -0,0 +1,345 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for ResourceStats methods realted to power statistics</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +const invalidManifestURL = "app://invalid.gaiamobile.org/manifest.webapp"; +var powerStatsMgr = null; // ResourceStatsManager for power statistics. +var sampleRate = 0; + +// Test WebIDL attributes. +function testAttributes() { + // Test sampleRate. + ok('sampleRate' in powerStatsMgr, + "sampleRate should be a ResourceStatsManager attribute."); + sampleRate = powerStatsMgr.sampleRate; + ok(sampleRate > 0, "sampleRate is greater than 0."); + + // Test maxStorageAge. + ok('maxStorageAge' in powerStatsMgr, + "maxStorageAge should be a ResourceStatsManager attribute."); + ok(powerStatsMgr.maxStorageAge > 0, + "maxStorageAge is greater than 0."); + + // Test whether "power" in resourceTypes array. + ok('resourceTypes' in powerStatsMgr, + "resourceTypes should be a ResourceStatsManager attribute."); + ok(Array.isArray(powerStatsMgr.resourceTypes), + "powerStatsMgr.resourceTypes is an array."); + ok(powerStatsMgr.resourceTypes.indexOf("power") > -1, + "'power' is an element of powerStatsMgr.resourceTypes."); +} + +// Check the content returned by ResourceStats.getData(). +function checkData(data, start, end) { + // Check if data is an array. + if (!Array.isArray(data)) { + ok(false, "getData does not return an array.") + return; + } else { + ok(true, "getData returns an array.") + } + + // Iterate the array and check the timestamp and type of each element. + var success = true; + var obj = null; + var timestamp = start; + var i = 0; + var length = data.length; + + do { + obj = data[i++]; + + // Check object type. + if (!(obj instanceof PowerStatsData)) { + success = false; + ok(false, "The array contains a non-PowerStatsData object."); + break; + } + + // Check if the timestamp is continuous. + if (obj.timestamp != timestamp) { + success = false; + ok(false, "The timestamp of PowerStatsData object is correct."); + break; + } + + timestamp += sampleRate; + } while (i < length); + + if (!success) { + return; + } + + // Check the timestamp of the last element is equal to end. + if (obj.timestamp != end) { + ok(false, + "The timestamp of the last element of the array is equal to end."); + return; + } + + // Execute next test case. + ok(true, "The return of getData is an array of PowerStatsData objects."); + testMethods(); +} + +// Test Cases for testing WebIDL methods. +var testCases = [ + function() { + // Test clearAllStats. + var promise = powerStatsMgr.clearAllStats(); + promise.then(function() { + ok(true, "clearAllStats clears the power store."); + testMethods(); + }, function() { + ok(false, "clearAllStats fails to clear the power store."); + }); + }, + + function() { + // Test clearStats. + var promise = powerStatsMgr.clearStats(); + promise.then(function() { + ok(true, "clearStats clears the power store."); + testMethods(); + }, function() { + ok(false, "clearStats fails to clear the power store."); + }); + }, + + function() { + // Check if clearStats throw exception when start is great than end. + var end = Date.now(); + var start = end + 1000; + var promise = powerStatsMgr.clearStats(null, start, end); + promise.then(function() { + ok(false, + "clearStats does not throw exception when start is great than end."); + }, function() { + ok(true, "clearStats throw exception when start is great than end."); + testMethods(); + }); + }, + + function() { + // Check if clearStats throw exception when start is less than 0. + var end = Date.now(); + var start = -1; + var promise = powerStatsMgr.clearStats(null, start, end); + promise.then(function() { + ok(false, + "clearStats dose not throw exception when start is less than 0."); + }, function() { + ok(true, "clearStats throw exception when start is less than 0."); + testMethods(); + }); + }, + + function() { + // Check if clearStats throw exception when manifestURL is invalid. + var options = {manifestURL: invalidManifestURL}; + var promise = powerStatsMgr.clearStats(options); + promise.then(function() { + ok(false, + "clearStats does not throw exception when manifestURL is invalid."); + }, function() { + ok(true, "clearStats throw exception when manifestURL is invalid."); + testMethods(); + }); + }, + + function() { + // Test getAvailableComponents. + var promise = powerStatsMgr.getAvailableComponents(); + promise.then(function(value) { + if (Array.isArray(value)) { + ok(true, "getAvailableComponents returns an array."); + testMethods(); + } else { + ok(false, "getAvailableComponents does not return an array."); + } + }, function() { + ok(false, "Fail to execute getAvailableComponents."); + }); + }, + + function() { + // Test getStats. + ok(true, "Get system stats when start and end are adapted to sampleRate."); + + // Prepare start and end. + var offset = (new Date()).getTimezoneOffset() * 60 * 1000; + var end = Math.floor((Date.now() - offset) / sampleRate) * sampleRate + offset; + var start = end - sampleRate * 10; + + // Launch request. + var promise = powerStatsMgr.getStats(null, start, end); + promise.then(function(value) { + // Check the object type. + if (value instanceof ResourceStats) { + ok(true, "Get a ResourceStats object."); + } else { + ok(false, "Fail to get a ResourceStats object."); + return; + } + + // Check attributes of ResourceStats. + ok(value.type == "power", "type should be power."); + ok(value.component == null, "component should be null."); + ok(value.serviceType == null, "serviceType should be null."); + ok(value.manifestURL == null, "manifestURL should be null."); + + // Check if the time range of ResourceStats is equal to the request. + ok(value.start == start, "start timestamp should be equal."); + ok(value.end == end, "end timestamp should be equal."); + + // Check stats stored inside ResourceStats. + if ('getData' in value) { + checkData(value.getData(), start, end); + } else { + ok(false, "getData is not a method of ResourceStats."); + return; + } + }, function() { + ok(false, "Get power stats failed."); + }); + }, + + function() { + // Test getStats when start and end are not adapted to sampleRate. + ok(true, + "Get system stats when start and end are not adapted to sampleRate."); + + // Prepare start and end. + var end = Date.now(); + var start = end - sampleRate * 10; + + // Normalize start and end. + var offset = (new Date()).getTimezoneOffset() * 60 * 1000; + var normEnd = Math.floor((end - offset) / sampleRate) + * sampleRate + offset; + var normStart = Math.floor((start - offset) / sampleRate) + * sampleRate + offset; + + // Launch request. + var promise = powerStatsMgr.getStats(null, start, end); + promise.then(function(value) { + // Check the object type. + if (value instanceof ResourceStats) { + ok(true, "Get a ResourceStats object."); + } else { + ok(false, "Fail to get a ResourceStats object."); + return; + } + + // Check attributes of ResourceStats. + ok(value.type == "power", "type should be power."); + ok(value.component == null, "component should be null."); + ok(value.serviceType == null, "serviceType should be null."); + ok(value.manifestURL == null, "manifestURL should be null."); + + // Check if time range of ResourceStats are normalized. + ok(value.start == normStart, "start timestamp should be normalized."); + ok(value.end == normEnd, "end timestamp should be normalized."); + + // Check stats stored inside ResourceStats. + if ('getData' in value) { + checkData(value.getData(), normStart, normEnd); + } else { + ok(false, "getData is not a method of ResourceStats."); + return; + } + }, function() { + ok(false, "Get power stats failed."); + }); + }, + + function() { + // Check if getStats throw exception when start is greater than end. + var end = Date.now(); + var start = end + 1000; + var promise = powerStatsMgr.getStats(null, start, end); + promise.then(function() { + ok(false, + "getStats dose not throw exception when start is great than end."); + }, function() { + ok(true, "getStats throw exception when start is great than end."); + testMethods(); + }); + }, + + function() { + // Check if getStats throw exception when start is less than 0. + var end = Date.now(); + var start = -1; + var promise = powerStatsMgr.getStats(null, start, end); + promise.then(function() { + ok(false, + "getStats dose not throw exception when start is less than 0."); + }, function() { + ok(true, "getStats throw exception when start is less than 0."); + testMethods(); + }); + }, + + function() { + // Check if getStats throw exception when manifestURL is invalid. + var options = {manifestURL: invalidManifestURL}; + var promise = powerStatsMgr.getStats(options); + promise.then(function(value) { + ok(false, + "getStats does not throw exception when manifestURL is invalid."); + }, function() { + ok(true, "getStats throw exception when manifestURL is invalid."); + testMethods(); + }); + } +]; + +// Test WebIDL methods related stats operation. +function testMethods() { + if (!testCases.length) { + ok(true, "Done."); + SpecialPowers.removePermission("resourcestats-manage", document); + SimpleTest.finish(); + return; + } + + var testCase = testCases.shift(); + testCase(); +} + +function startTest() { + // Create an instance of ResourceStatsManager for power stats. + powerStatsMgr = new ResourceStatsManager("power"); + ok(powerStatsMgr, "Create powerStatsMgr."); + + // Test WebIDL attributes. + testAttributes(); + + // Test WebIDL methods related to stats operation. + testMethods(); +} + +SimpleTest.waitForExplicitFinish(); + +// Enable permission and preference. +SpecialPowers.addPermission("resourcestats-manage", true, document); +SpecialPowers.pushPrefEnv({ 'set': [ + ["dom.resource_stats.enabled", true], + ["dom.ignore_webidl_scope_checks", true] + ]}, startTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/resourcestats/tests/xpcshell/test_resourcestats_db.js b/dom/resourcestats/tests/xpcshell/test_resourcestats_db.js new file mode 100644 index 000000000..c38fbf75f --- /dev/null +++ b/dom/resourcestats/tests/xpcshell/test_resourcestats_db.js @@ -0,0 +1,985 @@ +/* Any: copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/ResourceStatsDB.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +const db = new ResourceStatsDB(); + +// Components. +const wifiComponent = "wifi:0"; +const mobileComponent = "mobile:1"; +const cpuComponent = "cpu:0"; +const gpsComponent = "gps:0"; + +// List of available components. +const networkComponents = [wifiComponent, mobileComponent]; +const powerComponents = [cpuComponent, gpsComponent]; +const offset = (new Date()).getTimezoneOffset() * 60 * 1000; + +// Clear store. +function clearStore(store, callback) { + db._dbNewTxn(store, "readwrite", function(aTxn, aStore) { + aStore.openCursor().onsuccess = function (event) { + let cursor = event.target.result; + if (cursor){ + cursor.delete(); + cursor.continue(); + } + }; + }, callback); +} + +// Clear all stores to avoid starting tests with unknown state. +add_test(function prepareDatabase() { + clearStore('power_stats_store', function() { + clearStore('network_stats_store', function() { + clearStore('alarm_store', function() { + run_next_test(); + }); + }); + }); +}); + +// Dump data saved in a store. +function dumpStore(store, callback) { + db._dbNewTxn(store, "readonly", function(aTxn, aStore) { + aStore.mozGetAll().onsuccess = function onsuccess(event) { + aTxn.result = event.target.result; + }; + }, callback); +} + +// Check sampleRate is unchangeable. +add_test(function test_sampleRate() { + var sampleRate = db.sampleRate; + do_check_true(sampleRate > 0); + + db.sampleRate = 0; + sampleRate = db.sampleRate; + do_check_true(sampleRate > 0); + + run_next_test(); +}); + +// Test maxStorageAge is unchangeable. +add_test(function test_maxStorageAge() { + var maxStorageAge = db.maxStorageAge; + do_check_true(maxStorageAge > 0); + + db.maxStorageAge = 0; + maxStorageAge = db.maxStorageAge; + do_check_true(maxStorageAge > 0); + + run_next_test(); +}); + +// Normalize timestamp to sampleRate precision. +function normalizeTime(aTimeStamp) { + var sampleRate = db.sampleRate; + + return Math.floor((aTimeStamp - offset) / sampleRate) * sampleRate; +} + +// Generte record as input for saveNetworkStats() as well as the expected +// result when calling getStats(). +function generateNetworkRecord(aAppId, aServiceType, aComponents) { + var result = []; + var componentStats = {}; + var receivedBytes; + var sentBytes; + var totalReceivedBytes = 0; + var totalSentBytes = 0; + + aComponents.forEach(function(comp) { + // Step 1: generate random value for receivedBytes and sentBytes. + receivedBytes = Math.floor(Math.random() * 1000); + sentBytes = Math.floor(Math.random() * 1000); + totalReceivedBytes += receivedBytes; + totalSentBytes += sentBytes; + + // Step 2: add stats to record.componentStats. + componentStats[comp] = { + receivedBytes: receivedBytes, + sentBytes: sentBytes + }; + + // Step 3: generate expected results. + result.push({ + appId: aAppId, + serviceType: aServiceType, + component: comp, + receivedBytes: receivedBytes, + sentBytes: sentBytes + }); + }); + + // Step 4: generate expected total stats. + result.push({ + appId: aAppId, + serviceType: aServiceType, + component: "", + receivedBytes: totalReceivedBytes, + sentBytes: totalSentBytes + }); + + // Step 5: get record. + var record = { appId: aAppId, + serviceType: aServiceType, + componentStats: componentStats }; + + return { record: record, result: result }; +} + +// Generte record as input for savePowerStats() as well as the expected +// result when calling getStats(). +function generatePowerRecord(aAppId, aServiceType, aComponents) { + var result = []; + var componentStats = {}; + var consumedPower; + var totalConsumedPower = 0; + + aComponents.forEach(function(comp) { + // Step 1: generate random value for consumedPower. + consumedPower = Math.floor(Math.random() * 1000); + totalConsumedPower += consumedPower; + + // Step 2: add stats to record.componentStats. + componentStats[comp] = consumedPower; + + // Step 3: generate expected results. + result.push({ + appId: aAppId, + serviceType: aServiceType, + component: comp, + consumedPower: consumedPower + }); + }); + + // Step 4: generate expected total stats. + result.push({ + appId: aAppId, + serviceType: aServiceType, + component: "", + consumedPower: totalConsumedPower + }); + + // Step 5: get record. + var record = { appId: aAppId, + serviceType: aServiceType, + componentStats: componentStats }; + + return { record: record, result: result }; +} + +// Compare stats saved in network store with expected results. +function checkNetworkStatsStore(aExpectedResult, aDumpResult, aTimestamp) { + // Step 1: a quick check for the length of arrays first. + do_check_eq(aExpectedResult.length, aDumpResult.length); + + // Step 2: create a map array for search by receivedBytes. + var mapArray = aExpectedResult.map(function(e) {return e.receivedBytes;}); + + // Step 3: compare each element to make sure both array are equal. + var index; + var target; + + aDumpResult.forEach(function(stats) { + index = 0; + // Find the first equal receivedBytes since index. + while ((index = mapArray.indexOf(stats.receivedBytes, index)) > -1) { + // Compare all attributes. + target = aExpectedResult[index]; + if (target.appId != stats.appId || + target.serviceType != stats.serviceType || + target.component != stats.component || + target.sentBytes != stats.sentBytes || + aTimestamp != stats.timestamp) { + index += 1; + continue; + } else { + // If found, remove that element from aExpectedResult and mapArray. + aExpectedResult.splice(index, 1); + mapArray.splice(index, 1); + break; + } + } + do_check_neq(index, -1); + }); + run_next_test(); +} + +// Compare stats saved in power store with expected results. +function checkPowerStatsStore(aExpectedResult, aDumpResult, aTimestamp) { + // Step 1: a quick check for the length of arrays first. + do_check_eq(aExpectedResult.length, aDumpResult.length); + + // Step 2: create a map array for search by consumedPower. + var mapArray = aExpectedResult.map(function(e) {return e.consumedPower;}); + + // Step 3: compare each element to make sure both array are equal. + var index; + var target; + + aDumpResult.forEach(function(stats) { + index = 0; + // Find the first equal consumedPower since index. + while ((index = mapArray.indexOf(stats.consumedPower, index)) > -1) { + // Compare all attributes. + target = aExpectedResult[index]; + if (target.appId != stats.appId || + target.serviceType != stats.serviceType || + target.component != stats.component || + aTimestamp != stats.timestamp) { + index += 1; + continue; + } else { + // If found, remove that element from aExpectedResult and mapArray. + aExpectedResult.splice(index, 1); + mapArray.splice(index, 1); + break; + } + } + do_check_neq(index, -1); + }); + run_next_test(); +} + +// Prepare network store for testing. +function prepareNetworkStatsStore(recordArray, timestamp, callback) { + // Step 1: clear store. + clearStore("network_stats_store", function() { + // Step 2: save record to store. + db.saveNetworkStats(recordArray, timestamp, callback); + }); +} + +// Prepare power store for testing. +function preparePowerStatsStore(recordArray, timestamp, callback) { + // Step 1: clear store. + clearStore("power_stats_store", function() { + // Step 2: save record to store. + db.savePowerStats(recordArray, timestamp, callback); + }); +} + +// Test saveNetworkStats. +add_test(function test_saveNetworkStats() { + var appId = 1; + var serviceType = ""; + + // Step 1: generate data saved to store. + var { record: record, result: expectedResult } = + generateNetworkRecord(appId, serviceType, networkComponents); + var recordArray = [record]; + var timestamp = Date.now(); + + // Step 2: save recordArray to network store. + prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: check if the function call succeed. + do_check_eq(error, null); + // Step 4: dump store for comparison. + dumpStore("network_stats_store", function(error, result) { + do_check_eq(error, null); + checkNetworkStatsStore(expectedResult, result, normalizeTime(timestamp)); + }); + }); +}); + +// Test savePowerStats. +add_test(function test_savePowerStats() { + var appId = 1; + var serviceType = ""; + + // Step 1: generate data saved to store. + var { record: record, result: expectedResult } = + generatePowerRecord(appId, serviceType, powerComponents); + var recordArray = [record]; + var timestamp = Date.now(); + + // Step 2: save recordArray to power store. + preparePowerStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: check if the function call succeed. + do_check_eq(error, null); + // Step 4: dump store for comparison. + dumpStore("power_stats_store", function(error, result) { + do_check_eq(error, null); + checkPowerStatsStore(expectedResult, result, normalizeTime(timestamp)); + }); + }); +}); + +// Test getting network stats via getStats. +add_test(function test_getNetworkStats() { + var appId = 0; + var manifestURL = ""; + var serviceType = ""; + var component = ""; + + // Step 1: generate data saved to store. + var { record: record, result: result } = + generateNetworkRecord(appId, serviceType, networkComponents); + var recordArray = [record]; + var expectedStats = result[result.length - 1]; // Check total stats only. + var timestamp = Date.now(); + var end = normalizeTime(timestamp) + offset; + var start = end; + + // Step 2: save record and prepare network store. + prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: get network stats. + db.getStats("network", manifestURL, serviceType, component, start, end, + function(error, result) { + do_check_eq(error, null); + + // Step 4: verify result. + do_check_eq(result.type, "network"); + do_check_eq(result.manifestURL, manifestURL); + do_check_eq(result.serviceType, serviceType); + do_check_eq(result.component, component); + do_check_eq(result.start, start); + do_check_eq(result.end, end); + do_check_eq(result.sampleRate, db.sampleRate); + do_check_true(Array.isArray(result.statsData)); + do_check_eq(result.statsData.length, 1); + var stats = result.statsData[0]; + do_check_eq(stats.receivedBytes, expectedStats.receivedBytes); + do_check_eq(stats.sentBytes, expectedStats.sentBytes); + + run_next_test(); // If success, run next test. + }); + }); +}); + +// Test getting power stats via getStats. +add_test(function test_getPowerStats() { + var appId = 0; + var manifestURL = ""; + var serviceType = ""; + var component = ""; + + // Step 1: generate data saved to store. + var { record: record, result: result } = + generatePowerRecord(appId, serviceType, powerComponents); + var recordArray = [record]; + var expectedStats = result[result.length - 1]; // Check total stats only. + var timestamp = Date.now(); + var end = normalizeTime(timestamp) + offset; + var start = end; + + // Step 2: save record and prepare power store. + preparePowerStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: get power stats. + db.getStats("power", manifestURL, serviceType, component, start, end, + function(error, result) { + do_check_eq(error, null); + + // Step 4: verify result + do_check_eq(result.type, "power"); + do_check_eq(result.manifestURL, manifestURL); + do_check_eq(result.serviceType, serviceType); + do_check_eq(result.component, component); + do_check_eq(result.start, start); + do_check_eq(result.end, end); + do_check_eq(result.sampleRate, db.sampleRate); + do_check_true(Array.isArray(result.statsData)); + do_check_eq(result.statsData.length, 1); + var stats = result.statsData[0]; + do_check_eq(stats.consumedPower, expectedStats.consumedPower); + + run_next_test(); // If success, run next test. + }); + }); +}); + +// Test deleting network stats via clearStats. +add_test(function test_clearNetworkStats() { + var appId = 0; + var manifestURL = ""; + var serviceType = ""; + var component = ""; + + // Step 1: genrate data saved to network store. + var { record: record, result: result } = + generateNetworkRecord(appId, serviceType, networkComponents); + var recordArray = [record]; + var timestamp = Date.now(); + var end = normalizeTime(timestamp) + offset; + var start = end; + + // Step 2: save record and prepare network store. + prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: clear network stats. + db.clearStats("network", appId, serviceType, component, start, end, + function(error, result) { + do_check_eq(error, null); + + // Step 4: check if the stats is deleted. + db.getStats("network", manifestURL, serviceType, component, start, end, + function(error, result) { + do_check_eq(result.statsData.length, 0); + run_next_test(); + }); + }); + }); +}); + +// Test deleting power stats via clearStats. +add_test(function test_clearPowerStats() { + var appId = 0; + var manifestURL = ""; + var serviceType = ""; + var component = ""; + + // Step 1: genrate data saved to power store. + var { record: record, result: result } = + generatePowerRecord(appId, serviceType, powerComponents); + var recordArray = [record]; + var timestamp = Date.now(); + var end = normalizeTime(timestamp) + offset; + var start = end; + + // Step 2: save record and prepare power store. + preparePowerStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: clear power stats. + db.clearStats("power", appId, serviceType, component, start, end, + function(error, result) { + do_check_eq(error, null); + + // Step 4: check if the stats is deleted. + db.getStats("power", manifestURL, serviceType, component, start, end, + function(error, result) { + do_check_eq(result.statsData.length, 0); + run_next_test(); + }); + }); + }); +}); + +// Test clearing all network stats. +add_test(function test_clearAllNetworkStats() { + db.clearAllStats("network", function(error, result) { + do_check_eq(error, null); + run_next_test(); + }); +}); + +// Test clearing all power stats. +add_test(function test_clearAllPowerStats() { + db.clearAllStats("power", function(error, result) { + do_check_eq(error, null); + run_next_test(); + }); +}); + +// Test getting network components. +add_test(function test_getNetworkComponents() { + var appId = 0; + var serviceType = ""; + + // Step 1: generate data saved to store. + var { record: record, result: expectedResult } = + generateNetworkRecord(appId, serviceType, networkComponents); + var recordArray = [record]; + var timestamp = Date.now(); + + // Step 2: save recordArray to network store. + prepareNetworkStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: call getComponents. + db.getComponents("network", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_eq(result.length, networkComponents.length); + + // Check each element in result array is an element of networkComponents. + result.forEach(function(component) { + do_check_true(networkComponents.indexOf(component) > -1); + }); + + run_next_test(); // If success, run next test. + }); + }); +}); + +// Test getting power components. +add_test(function test_getPowerComponents() { + var appId = 0; + var serviceType = ""; + + // Step 1: generate data saved to store. + var { record: record, result: expectedResult } = + generatePowerRecord(appId, serviceType, powerComponents); + var recordArray = [record]; + var timestamp = Date.now(); + + // Step 2: save recordArray to power store. + preparePowerStatsStore(recordArray, timestamp, function(error, callback) { + // Step 3: call getComponents. + db.getComponents("power", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_eq(result.length, powerComponents.length); + + // Check each element in result array is an element of powerComponents. + result.forEach(function(component) { + do_check_true(powerComponents.indexOf(component) > -1); + }); + + run_next_test(); // If success, run next test. + }); + }); +}); + +// Generate alarm object for addAlarm(). +function generateAlarmObject(aType, aManifestURL, aServiceType, aComponent) { + let alarm = { + type: aType, + component: aComponent, + serviceType: aServiceType, + manifestURL: aManifestURL, + threshold: Math.floor(Math.random() * 1000), + startTime: Math.floor(Math.random() * 1000), + data: null + }; + + return alarm; +} + +// Test adding a network alarm. +add_test(function test_addNetowrkAlarm() { + var manifestURL = ""; + var serviceType = ""; + + // Step 1: generate a network alarm. + var alarm = + generateAlarmObject("network", manifestURL, serviceType, wifiComponent); + + // Step 2: clear store. + clearStore("alarm_store", function() { + // Step 3: save the alarm to store. + db.addAlarm(alarm, function(error, result) { + // Step 4: check if the function call succeed. + do_check_eq(error, null); + do_check_true(result > -1); + let alarmId = result; + + // Step 5: dump store for comparison. + dumpStore("alarm_store", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_true(result.length == 1); + do_check_eq(result[0].type, alarm.type); + do_check_eq(result[0].manifestURL, alarm.manifestURL); + do_check_eq(result[0].serviceType, alarm.serviceType); + do_check_eq(result[0].component, alarm.component); + do_check_eq(result[0].threshold, alarm.threshold); + do_check_eq(result[0].startTime, alarm.startTime); + do_check_eq(result[0].data, alarm.data); + do_check_eq(result[0].alarmId, alarmId); + + run_next_test(); // If success, run next test. + }); + }); + }); +}); + +// Test adding a power alarm. +add_test(function test_addPowerAlarm() { + var manifestURL = ""; + var serviceType = ""; + + // Step 1: generate a power alarm. + var alarm = + generateAlarmObject("power", manifestURL, serviceType, cpuComponent); + + // Step 2: clear store. + clearStore("alarm_store", function() { + // Step 3: save the alarm to store. + db.addAlarm(alarm, function(error, result) { + // Step 4: check if the function call succeed. + do_check_eq(error, null); + do_check_true(result > -1); + let alarmId = result; + + // Step 5: dump store for comparison. + dumpStore("alarm_store", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_true(result.length == 1); + do_check_eq(result[0].type, alarm.type); + do_check_eq(result[0].manifestURL, alarm.manifestURL); + do_check_eq(result[0].serviceType, alarm.serviceType); + do_check_eq(result[0].component, alarm.component); + do_check_eq(result[0].threshold, alarm.threshold); + do_check_eq(result[0].startTime, alarm.startTime); + do_check_eq(result[0].data, alarm.data); + do_check_eq(result[0].alarmId, alarmId); + + run_next_test(); // If success, run next test. + }); + }); + }); +}); + +// Add multiple alarms to store and record the obtained alarmId in each +// alarm object. +function addAlarmsToStore(alarms, index, callback) { + var alarm = alarms[index++]; + if (index < alarms.length) { + db.addAlarm(alarm, function(error, result) { + alarm.alarmId = result; + addAlarmsToStore(alarms, index, callback); + }); + } else { + db.addAlarm(alarm, function(error, result) { + alarm.alarmId = result; + callback(error, result); + }); + } +} + +// Prepare alarm store for testging. +function prepareAlarmStore(alarms, callback) { + // Step 1: clear srore. + clearStore("alarm_store", function() { + // Step 2: save alarms to store one by one. + addAlarmsToStore(alarms, 0, callback); + }); +} + +// Compare alrams returned by getAlarms(). +function compareAlarms(aExpectedResult, aResult) { + // Step 1: a quick check for the length of arrays first. + do_check_eq(aExpectedResult.length, aResult.length); + + // Step 2: create a map array for search by threshold. + var mapArray = aExpectedResult.map(function(e) {return e.threshold;}); + + // Step 3: compare each element to make sure both array are equal. + var index; + var target; + + aResult.forEach(function(alarm) { + index = 0; + // Find the first equal receivedBytes since index. + while ((index = mapArray.indexOf(alarm.threshold, index)) > -1) { + // Compare all attributes. + target = aExpectedResult[index]; + if (target.alarmId != alarm.alarmId || + target.type != alarm.type || + target.manifestURL != alarm.manifestURL || + target.serviceType != alarm.serviceType || + target.component != alarm.component || + target.data != alarm.data) { + index += 1; + continue; + } else { + // If found, remove that element from aExpectedResult and mapArray. + aExpectedResult.splice(index, 1); + mapArray.splice(index, 1); + break; + } + } + do_check_neq(index, -1); + }); + run_next_test(); +} + +// Test getting designate network alarms from store. +add_test(function test_getNetworkAlarms() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate two network alarms using same parameters. + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + wifiComponent)); + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + wifiComponent)); + + // Step 2: generate another network alarm using different parameters. + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + mobileComponent)); + + // Step 3: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + // Step 4: call getAlarms. + let options = { + manifestURL: manifestURL, + serviceType: serviceType, + component: wifiComponent + }; + db.getAlarms("network", options, function(error, result) { + // Step 5: check if the function call succeed. + do_check_eq(error, null); + + // Step 6: check results. + // The last element in alarms array is not our expected result, + // so pop that out first. + alarms.pop(); + compareAlarms(alarms, result); + }); + }); +}); + +// Test getting designate power alarms from store. +add_test(function test_getPowerAlarms() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate two power alarms using same parameters. + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + cpuComponent)); + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + cpuComponent)); + + // Step 2: generate another power alarm using different parameters. + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + gpsComponent)); + + // Step 3: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + // Step 4: call getAlarms. + let options = { + manifestURL: manifestURL, + serviceType: serviceType, + component: cpuComponent + }; + db.getAlarms("power", options, function(error, result) { + // Step 5: check if the function call succeed. + do_check_eq(error, null); + + // Step 6: check results. + // The last element in alarms array is not our expected result, + // so pop that out first. + alarms.pop(); + compareAlarms(alarms, result); + }); + }); +}); + +// Test getting all network alarms from store. +add_test(function test_getAllNetworkAlarms() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate two network alarms. + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + wifiComponent)); + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + mobileComponent)); + + // Step 2: generate another power alarm. + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + cpuComponent)); + + // Step 3: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + // Step 4: call getAlarms. + let options = null; + db.getAlarms("network", options, function(error, result) { + // Step 5: check if the function call succeed. + do_check_eq(error, null); + + // Step 6: check results. + // The last element in alarms array is not our expected result, + // so pop that out first. + alarms.pop(); + compareAlarms(alarms, result); + }); + }); +}); + +// Test getting all power alarms from store. +add_test(function test_getAllPowerAlarms() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate two power alarms. + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + cpuComponent)); + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + gpsComponent)); + + // Step 2: generate another network alarm. + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + wifiComponent)); + + // Step 3: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + // Step 4: call getAlarms. + let options = null; + db.getAlarms("power", options, function(error, result) { + // Step 5: check if the function call succeed. + do_check_eq(error, null); + + // Step 6: check results. + // The last element in alarms array is not our expected result, + // so pop that out first. + alarms.pop(); + compareAlarms(alarms, result); + }); + }); +}); + +// Test removing designate network alarm from store. +add_test(function test_removeNetworkAlarm() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate one network alarm. + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + wifiComponent)); + + // Step 2: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + var alarmId = result; + // Step 3: remove the alarm. + db.removeAlarm("network", alarmId, function(error, result) { + // Step 4: check if the function call succeed. + do_check_eq(result, true); + + // Step 5: dump store to check if the alarm is removed. + dumpStore("alarm_store", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_true(result.length === 0); + + run_next_test(); // If success, run next test. + }); + }); + }); +}); + +// Test removing designate power alarm from store. +add_test(function test_removePowerAlarm() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate one power alarm. + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + cpuComponent)); + + // Step 2: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + var alarmId = result; + // Step 3: remove the alarm. + db.removeAlarm("power", alarmId, function(error, result) { + // Step 4: check if the function call succeed. + do_check_eq(result, true); + + // Step 5: dump store to check if the alarm is removed. + dumpStore("alarm_store", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_true(result.length === 0); + + run_next_test(); // If success, run next test. + }); + }); + }); +}); + +// Test removing designate network alarm from store. +add_test(function test_removeAllNetworkAlarms() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate two network alarms. + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + wifiComponent)); + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + mobileComponent)); + + // Step 2: generate another power alarm. + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + cpuComponent)); + + // Step 3: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + // Step 4: remove all network alarms. + db.removeAllAlarms("network", function(error, result) { + // Step 5: check if the function call succeed. + do_check_eq(error, null); + + // Step 6: dump store for comparison. + // Because the power alarm should not be removed, so it would be the + // only result returned by dumpStore. + var alarm = alarms.pop(); // The expected result. + dumpStore("alarm_store", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_true(result.length == 1); + do_check_eq(result[0].type, alarm.type); + do_check_eq(result[0].manifestURL, alarm.manifestURL); + do_check_eq(result[0].serviceType, alarm.serviceType); + do_check_eq(result[0].component, alarm.component); + do_check_eq(result[0].threshold, alarm.threshold); + do_check_eq(result[0].startTime, alarm.startTime); + do_check_eq(result[0].data, alarm.data); + do_check_eq(result[0].alarmId, alarm.alarmId); + + run_next_test(); // If success, run next test. + }); + }); + }); +}); + +// Test removing designate power alarm from store. +add_test(function test_removeAllPowerAlarms() { + var manifestURL = ""; + var serviceType = ""; + var alarms = []; + + // Step 1: generate two power alarms. + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + cpuComponent)); + alarms.push(generateAlarmObject("power", manifestURL, serviceType, + gpsComponent)); + + // Step 2: generate another network alarm. + alarms.push(generateAlarmObject("network", manifestURL, serviceType, + wifiComponent)); + + // Step 3: clear alarm store and save new alarms to store. + prepareAlarmStore(alarms, function(error, result) { + // Step 4: remove all power alarms. + db.removeAllAlarms("power", function(error, result) { + // Step 5: check if the function call succeed. + do_check_eq(error, null); + + // Step 6: dump store for comparison. + // Because the network alarm should not be removed, so it would be the + // only result returned by dumpStore. + var alarm = alarms.pop(); // The expected result. + dumpStore("alarm_store", function(error, result) { + do_check_eq(error, null); + do_check_true(Array.isArray(result)); + do_check_true(result.length == 1); + do_check_eq(result[0].type, alarm.type); + do_check_eq(result[0].manifestURL, alarm.manifestURL); + do_check_eq(result[0].serviceType, alarm.serviceType); + do_check_eq(result[0].component, alarm.component); + do_check_eq(result[0].threshold, alarm.threshold); + do_check_eq(result[0].startTime, alarm.startTime); + do_check_eq(result[0].data, alarm.data); + do_check_eq(result[0].alarmId, alarm.alarmId); + + run_next_test(); // If success, run next test. + }); + }); + }); +}); + +function run_test() { + do_get_profile(); + run_next_test(); +} diff --git a/dom/resourcestats/tests/xpcshell/xpcshell.ini b/dom/resourcestats/tests/xpcshell/xpcshell.ini new file mode 100644 index 000000000..c1848a540 --- /dev/null +++ b/dom/resourcestats/tests/xpcshell/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = +tail = + +[test_resourcestats_db.js] |