author | Gregory Szorc <gps@mozilla.com> |
Fri, 09 Nov 2012 13:59:40 -0800 | |
changeset 113828 | 525e8539150a138f18783e461b22cfebc2bb8582 |
parent 113827 | 6b2eb103766a470894c40f432acd567ee9f0a884 |
child 113829 | 6f544baffff0389ece855da3fa9cdc45eea922e6 |
push id | 23890 |
push user | ryanvm@gmail.com |
push date | Wed, 21 Nov 2012 02:43:32 +0000 |
treeherder | mozilla-central@4f19e7fd8bea [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | rnewman |
bugs | 810132 |
milestone | 20.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/services/healthreport/modules-testing/mocks.jsm +++ b/services/healthreport/modules-testing/mocks.jsm @@ -10,28 +10,37 @@ const {utils: Cu} = Components; Cu.import("resource://services-common/log4moz.js"); this.MockPolicyListener = function MockPolicyListener() { this._log = Log4Moz.repository.getLogger("HealthReport.Testing.MockPolicyListener"); this._log.level = Log4Moz.Level["Debug"]; - this.requestDataSubmissionCount = 0; + this.requestDataUploadCount = 0; this.lastDataRequest = null; + this.requestRemoteDeleteCount = 0; + this.lastRemoteDeleteRequest = null; + this.notifyUserCount = 0; this.lastNotifyRequest = null; } MockPolicyListener.prototype = { - onRequestDataSubmission: function onRequestDataSubmission(request) { - this._log.info("onRequestDataSubmission invoked."); - this.requestDataSubmissionCount++; + onRequestDataUpload: function onRequestDataUpload(request) { + this._log.info("onRequestDataUpload invoked."); + this.requestDataUploadCount++; this.lastDataRequest = request; }, + onRequestRemoteDelete: function onRequestRemoteDelete(request) { + this._log.info("onRequestRemoteDelete invoked."); + this.requestRemoteDeleteCount++; + this.lastRemoteDeleteRequest = request; + }, + onNotifyDataPolicy: function onNotifyDataPolicy(request) { this._log.info("onNotifyUser invoked."); this.notifyUserCount++; this.lastNotifyRequest = request; }, };
--- a/services/healthreport/policy.jsm +++ b/services/healthreport/policy.jsm @@ -102,49 +102,59 @@ NotifyPolicyRequest.prototype = { }, }; Object.freeze(NotifyPolicyRequest.prototype); /** * Represents a request to submit data. * - * Instances of this are created when the policy requests data submission. + * Instances of this are created when the policy requests data upload or + * deletion. + * * Receivers are expected to call one of the provided on* functions to signal * completion of the request. * * Instances of this type should not be instantiated outside of this file. * Receivers of instances of this type should not attempt to do anything with * the instance except call one of the on* methods. */ -function DataSubmissionRequest(promise, expiresDate) { +function DataSubmissionRequest(promise, expiresDate, isDelete) { this.promise = promise; this.expiresDate = expiresDate; + this.isDelete = isDelete; this.state = null; this.reason = null; } DataSubmissionRequest.prototype = { NO_DATA_AVAILABLE: "no-data-available", SUBMISSION_SUCCESS: "success", SUBMISSION_FAILURE_SOFT: "failure-soft", SUBMISSION_FAILURE_HARD: "failure-hard", /** * No submission was attempted because no data was available. + * + * In the case of upload, this means there is no data to upload (perhaps + * it isn't available yet). In case of remote deletion, it means that there + * is no remote data to delete. */ onNoDataAvailable: function onNoDataAvailable() { this.state = this.NO_DATA_AVAILABLE; this.promise.resolve(this); }, /** * Data submission has completed successfully. * + * In case of upload, this means the upload completed successfully. In case + * of deletion, the data was deleted successfully. + * * @param date * (Date) When data submission occurred. */ onSubmissionSuccess: function onSubmissionSuccess(date) { this.state = this.SUBMISSION_SUCCESS; this.submissionDate = date; this.promise.resolve(this); }, @@ -199,21 +209,26 @@ Object.freeze(DataSubmissionRequest.prot * 6. Display of notification without any explicit user action constitutes * implicit consent after a certain duration of time. * 7. If data submission fails, try at most 2 additional times before giving * up on that day's submission. * * The listener passed into the instance must have the following properties * (which are callbacks that will be invoked at certain key events): * - * * onRequestDataSubmission(request) - Called when the policy is requesting + * * onRequestDataUpload(request) - Called when the policy is requesting * data to be submitted. The function is passed a `DataSubmissionRequest`. * The listener should call one of the special resolving functions on that * instance (see the documentation for that type). * + * * onRequestRemoteDelete(request) - Called when the policy is requesting + * deletion of remotely stored data. The function is passed a + * `DataSubmissionRequest`. The listener should call one of the special + * resolving functions on that instance (just like `onRequestDataUpload`). + * * * onNotifyDataPolicy(request) - Called when the policy is requesting the * user to be notified that data submission will occur. The function * receives a `NotifyPolicyRequest` instance. The callee should call one or * more of the functions on that instance when specific events occur. See * the documentation for that type for more. * * Note that the notification method is abstracted. Different applications * can have different mechanisms by which they notify the user of data @@ -316,17 +331,21 @@ HealthReportPolicy.prototype = { /** * State of user notification of data submission. */ STATE_NOTIFY_UNNOTIFIED: "not-notified", STATE_NOTIFY_WAIT: "waiting", STATE_NOTIFY_COMPLETE: "ok", - REQUIRED_LISTENERS: ["onRequestDataSubmission", "onNotifyDataPolicy"], + REQUIRED_LISTENERS: [ + "onRequestDataUpload", + "onRequestRemoteDelete", + "onNotifyDataPolicy", + ], /** * The first time the health report policy came into existence. * * This is used for scheduling of the initial submission. */ get firstRunDate() { return CommonUtils.getDatePref(this._prefs, "firstRunTime", 0, this._log, @@ -397,29 +416,45 @@ HealthReportPolicy.prototype = { } this._prefs.set("dataSubmissionPolicyResponseType", value); }, /** * Whether submission of data is allowed. * - * This is the master switch for data submission. If it is off, we will - * never submit data, even if the user has agreed to it. + * This is the master switch for remote server communication. If it is + * false, we never request upload or deletion. */ get dataSubmissionEnabled() { // Default is true because we are opt-out. return this._prefs.get("dataSubmissionEnabled", true); }, set dataSubmissionEnabled(value) { this._prefs.set("dataSubmissionEnabled", !!value); }, /** + * Whether upload of data is allowed. + * + * This is a kill switch for upload. It is meant to reflect a system or + * deployment policy decision. User intent should be reflected in the + * "dataSubmissionPolicy" prefs. + */ + get dataUploadEnabled() { + // Default is true because we are opt-out. + return this._prefs.get("dataUploadEnabled", true); + }, + + set dataUploadEnabled(value) { + this._prefs.set("dataUploadEnabled", !!value); + }, + + /** * Whether the user has accepted that data submission can occur. * * This overrides dataSubmissionEnabled. */ get dataSubmissionPolicyAccepted() { // Be conservative and default to false. return this._prefs.get("dataSubmissionPolicyAccepted", false); }, @@ -537,16 +572,31 @@ HealthReportPolicy.prototype = { if (!Number.isInteger(value)) { throw new Error("Value must be integer: " + value); } this._prefs.set("currentDaySubmissionFailureCount", value); }, /** + * Whether a request to delete remote data is awaiting completion. + * + * If this is true, the policy will request that remote data be deleted. + * Furthermore, no new data will be uploaded (if it's even allowed) until + * the remote deletion is fulfilled. + */ + get pendingDeleteRemoteData() { + return !!this._prefs.get("pendingDeleteRemoteData", false); + }, + + set pendingDeleteRemoteData(value) { + this._prefs.set("pendingDeleteRemoteData", !!value); + }, + + /** * Record user acceptance of data submission policy. * * Data submission will not be allowed to occur until this is called. * * This is typically called through the `onUserAccept` property attached to * the promise passed to `onUserNotify` in the policy listener. But, it can * be called through other interfaces at any time and the call will have * an impact on future data submissions. @@ -574,16 +624,35 @@ HealthReportPolicy.prototype = { recordUserRejection: function recordUserRejection(reason="no-reason") { this._log.info("User rejected data submission policy: " + reason); this.dataSubmissionPolicyResponseDate = this.now(); this.dataSubmissionPolicyResponseType = "rejected-" + reason; this.dataSubmissionPolicyAccepted = false; }, /** + * Request that remote data be deleted. + * + * This will record an intent that previously uploaded data is to be deleted. + * The policy will eventually issue a request to the listener for data + * deletion. It will keep asking for deletion until the listener acknowledges + * that data has been deleted. + */ + deleteRemoteData: function deleteRemoteData(reason="no-reason") { + this._log.info("Remote data deletion requested: " + reason); + + this.pendingDeleteRemoteData = true; + + // We want delete deletion to occur as soon as possible. Move up any + // pending scheduled data submission and try to trigger. + this.nextDataSubmissionDate = this.now(); + this.checkStateAndTrigger(); + }, + + /** * Start background polling for activity. * * This will set up a recurring timer that will periodically check if * activity is warranted. * * You typically call this function for each constructed instance. */ startPolling: function startPolling() { @@ -649,17 +718,39 @@ HealthReportPolicy.prototype = { if (nextSubmissionDate.getTime() >= nowT + 3 * MILLISECONDS_PER_DAY) { this._log.warn("Next data submission time is far away. Was the system " + "clock recently readjusted? " + nextSubmissionDate); // It shouldn't really matter what we set this to. 1 day in the future // should be pretty safe. this._moveScheduleForward24h(); - // Fall through and prompt for user notification, if necessary. + // Fall through since we may have other actions. + } + + // Tend to any in progress work. + if (this._processInProgressSubmission()) { + return; + } + + // Requests to delete remote data take priority above everything else. + if (this.pendingDeleteRemoteData) { + if (nowT < nextSubmissionDate.getTime()) { + this._log.debug("Deletion request is scheduled for the future: " + + nextSubmissionDate); + return; + } + + this._dispatchSubmissionRequest("onRequestRemoteDelete", true); + return; + } + + if (!this.dataUploadEnabled) { + this._log.debug("Data upload is disabled. Doing nothing."); + return; } // If the user hasn't responded to the data policy, don't do anything. if (!this.ensureNotifyResponse(now)) { return; } // User has opted out of data submission. @@ -672,63 +763,17 @@ HealthReportPolicy.prototype = { // comes the scheduling part. if (nowT < nextSubmissionDate.getTime()) { this._log.debug("Next data submission is scheduled in the future: " + nextSubmissionDate); return; } - if (this._inProgressSubmissionRequest) { - if (this._inProgressSubmissionRequest.expiresDate.getTime() > nowT) { - this._log.info("Waiting on in-progress submission request to finish."); - return; - } - - this._log.warn("Old submission request has expired from no activity."); - this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired.")); - this._inProgressSubmissionRequest = null; - if (!this._handleSubmissionFailure()) { - return; - } - } - - // We're past our scheduled next data submission date, so let's do it! - this.lastDataSubmissionRequestedDate = now; - let deferred = Promise.defer(); - let requestExpiresDate = - this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC); - this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred, - requestExpiresDate); - - let onSuccess = function onSuccess(result) { - this._inProgressSubmissionRequest = null; - this._handleSubmissionResult(result); - }.bind(this); - - let onError = function onError(error) { - this._log.error("Error when handling data submission result: " + - CommonUtils.exceptionStr(result)); - this._inProgressSubmissionRequest = null; - this._handleSubmissionFailure(); - }.bind(this); - - deferred.promise.then(onSuccess, onError); - - this._log.info("Requesting data submission. Will expire at " + - requestExpiresDate); - try { - this._listener.onRequestDataSubmission(this._inProgressSubmissionRequest); - } catch (ex) { - this._log.warn("Exception when calling onRequestDataSubmission: " + - CommonUtils.exceptionStr(ex)); - this._inProgressSubmissionRequest = null; - this._handleSubmissionFailure(); - return; - } + this._dispatchSubmissionRequest("onRequestDataUpload", false); }, /** * Ensure user has responded to data submission policy. * * This must be called before data submission. If the policy has not been * responded to, data submission must not occur. * @@ -792,36 +837,119 @@ HealthReportPolicy.prototype = { // If this happens, we have a coding error in this file. if (notifyState != this.STATE_NOTIFY_COMPLETE) { throw new Error("Unknown notification state: " + notifyState); } return true; }, + _processInProgressSubmission: function _processInProgressSubmission() { + if (!this._inProgressSubmissionRequest) { + return false; + } + + let now = this.now().getTime(); + if (this._inProgressSubmissionRequest.expiresDate.getTime() > now) { + this._log.info("Waiting on in-progress submission request to finish."); + return true; + } + + this._log.warn("Old submission request has expired from no activity."); + this._inProgressSubmissionRequest.promise.reject(new Error("Request has expired.")); + this._inProgressSubmissionRequest = null; + this._handleSubmissionFailure(); + + return false; + }, + + _dispatchSubmissionRequest: function _dispatchSubmissionRequest(handler, isDelete) { + let now = this.now(); + + // We're past our scheduled next data submission date, so let's do it! + this.lastDataSubmissionRequestedDate = now; + let deferred = Promise.defer(); + let requestExpiresDate = + this._futureDate(this.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC); + this._inProgressSubmissionRequest = new DataSubmissionRequest(deferred, + requestExpiresDate, + isDelete); + + let onSuccess = function onSuccess(result) { + this._inProgressSubmissionRequest = null; + this._handleSubmissionResult(result); + }.bind(this); + + let onError = function onError(error) { + this._log.error("Error when handling data submission result: " + + CommonUtils.exceptionStr(result)); + this._inProgressSubmissionRequest = null; + this._handleSubmissionFailure(); + }.bind(this); + + deferred.promise.then(onSuccess, onError); + + this._log.info("Requesting data submission. Will expire at " + + requestExpiresDate); + try { + this._listener[handler](this._inProgressSubmissionRequest); + } catch (ex) { + this._log.warn("Exception when calling " + handler + ": " + + CommonUtils.exceptionStr(ex)); + this._inProgressSubmissionRequest = null; + this._handleSubmissionFailure(); + return; + } + }, + _handleSubmissionResult: function _handleSubmissionResult(request) { let state = request.state; let reason = request.reason || "no reason"; this._log.info("Got submission request result: " + state); if (state == request.SUBMISSION_SUCCESS) { - this._log.info("Successful data submission reported."); + if (request.isDelete) { + this.pendingDeleteRemoteData = false; + this._log.info("Successful data delete reported."); + } else { + this._log.info("Successful data upload reported."); + } + this.lastDataSubmissionSuccessfulDate = request.submissionDate; - this.nextDataSubmissionDate = + + let nextSubmissionDate = new Date(request.submissionDate.getTime() + MILLISECONDS_PER_DAY); + + // Schedule pending deletes immediately. This has potential to overload + // the server. However, the frequency of delete requests across all + // clients should be low, so this shouldn't pose a problem. + if (this.pendingDeleteRemoteData) { + nextSubmissionDate = this.now(); + } + + this.nextDataSubmissionDate = nextSubmissionDate; this.currentDaySubmissionFailureCount = 0; return; } if (state == request.NO_DATA_AVAILABLE) { + if (request.isDelete) { + this._log.info("Remote data delete requested but no remote data was stored."); + this.pendingDeleteRemoteData = false; + return; + } + this._log.info("No data was available to submit. May try later."); this._handleSubmissionFailure(); return; } + // We don't special case request.isDelete for these failures because it + // likely means there was a server error. + if (state == request.SUBMISSION_FAILURE_SOFT) { this._log.warn("Soft error submitting data: " + reason); this.lastDataSubmissionFailureDate = this.now(); this._handleSubmissionFailure(); return; } if (state == request.SUBMISSION_FAILURE_HARD) { @@ -857,8 +985,9 @@ HealthReportPolicy.prototype = { }, _futureDate: function _futureDate(offset) { return new Date(this.now().getTime() + offset); }, }; Object.freeze(HealthReportPolicy.prototype); +
--- a/services/healthreport/tests/xpcshell/test_policy.js +++ b/services/healthreport/tests/xpcshell/test_policy.js @@ -29,17 +29,18 @@ function defineNow(policy, now) { function run_test() { run_next_test(); } add_test(function test_constructor() { let prefs = new Preferences("foo.bar"); let listener = { - onRequestDataSubmission: function() {}, + onRequestDataUpload: function() {}, + onRequestRemoteDelete: function() {}, onNotifyDataPolicy: function() {}, }; let policy = new HealthReportPolicy(prefs, listener); do_check_true(Date.now() - policy.firstRunDate.getTime() < 1000); let tomorrow = Date.now() + 24 * 60 * 60 * 1000; do_check_true(tomorrow - policy.nextDataSubmissionDate.getTime() < 1000); @@ -94,16 +95,20 @@ add_test(function test_prefs() { policy.nextDataSubmissionDate = now; do_check_eq(prefs.get("nextDataSubmissionTime"), nowT); do_check_eq(policy.nextDataSubmissionDate.getTime(), nowT); policy.currentDaySubmissionFailureCount = 2; do_check_eq(prefs.get("currentDaySubmissionFailureCount", 0), 2); do_check_eq(policy.currentDaySubmissionFailureCount, 2); + policy.pendingDeleteRemoteData = true; + do_check_true(prefs.get("pendingDeleteRemoteData")); + do_check_true(policy.pendingDeleteRemoteData); + run_next_test(); }); add_test(function test_notify_state_prefs() { let [policy, prefs, listener] = getPolicy("notify_state_prefs"); do_check_eq(policy.notifyState, policy.STATE_NOTIFY_UNNOTIFIED); @@ -228,80 +233,97 @@ add_test(function test_notification_reje listener.lastNotifyRequest.onUserReject(); do_check_eq(policy.notifyState, policy.STATE_NOTIFY_COMPLETE); do_check_eq(policy.dataSubmissionPolicyResponseType, "rejected-no-reason"); do_check_false(policy.dataSubmissionPolicyAccepted); // No requests for submission should occur if user has rejected. defineNow(policy, new Date(policy.nextDataSubmissionDate.getTime() + 10000)); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 0); + do_check_eq(listener.requestDataUploadCount, 0); run_next_test(); }); add_test(function test_submission_kill_switch() { let [policy, prefs, listener] = getPolicy("submission_kill_switch"); policy.firstRunDate = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000); policy.nextDataSubmissionDate = new Date(Date.now() - 24 * 60 * 60 * 1000); policy.recordUserAcceptance("accept-old-ack"); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); defineNow(policy, new Date(Date.now() + policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC + 100)); policy.dataSubmissionEnabled = false; policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); + + run_next_test(); +}); + +add_test(function test_upload_kill_switch() { + let [policy, prefs, listener] = getPolicy("upload_kill_switch"); + + defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000)); + policy.recordUserAcceptance(); + defineNow(policy, policy.nextDataSubmissionDate); + + policy.dataUploadEnabled = false; + policy.checkStateAndTrigger(); + do_check_eq(listener.requestDataUploadCount, 0); + policy.dataUploadEnabled = true; + policy.checkStateAndTrigger(); + do_check_eq(listener.requestDataUploadCount, 1); run_next_test(); }); add_test(function test_data_submission_no_data() { let [policy, prefs, listener] = getPolicy("data_submission_no_data"); policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000); policy.dataSubmissionPolicyAccepted = true; let now = new Date(policy.nextDataSubmissionDate.getTime() + 1); defineNow(policy, now); - do_check_eq(listener.requestDataSubmissionCount, 0); + do_check_eq(listener.requestDataUploadCount, 0); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); listener.lastDataRequest.onNoDataAvailable(); // The next trigger should try again. defineNow(policy, new Date(now.getTime() + 155 * 60 * 1000)); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 2); + do_check_eq(listener.requestDataUploadCount, 2); run_next_test(); }); add_test(function test_data_submission_submit_failure_hard() { let [policy, prefs, listener] = getPolicy("data_submission_submit_failure_hard"); policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000); policy.dataSubmissionPolicyAccepted = true; let nextDataSubmissionDate = policy.nextDataSubmissionDate; let now = new Date(policy.nextDataSubmissionDate.getTime() + 1); defineNow(policy, now); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); listener.lastDataRequest.onSubmissionFailureHard(); do_check_eq(listener.lastDataRequest.state, listener.lastDataRequest.SUBMISSION_FAILURE_HARD); let expected = new Date(now.getTime() + 24 * 60 * 60 * 1000); do_check_eq(policy.nextDataSubmissionDate.getTime(), expected.getTime()); defineNow(policy, new Date(now.getTime() + 10)); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); run_next_test(); }); add_test(function test_data_submission_submit_try_again() { let [policy, prefs, listener] = getPolicy("data_submission_failure_soft"); policy.recordUserAcceptance(); @@ -322,38 +344,38 @@ add_test(function test_submission_daily_ policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000); policy.dataSubmissionPolicyAccepted = true; let nextDataSubmissionDate = policy.nextDataSubmissionDate; // Skip ahead to next submission date. We should get a submission request. let now = new Date(policy.nextDataSubmissionDate.getTime()); defineNow(policy, now); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); do_check_eq(policy.lastDataSubmissionRequestedDate.getTime(), now.getTime()); let finishedDate = new Date(now.getTime() + 250); defineNow(policy, new Date(finishedDate.getTime() + 50)); listener.lastDataRequest.onSubmissionSuccess(finishedDate); do_check_eq(policy.lastDataSubmissionSuccessfulDate.getTime(), finishedDate.getTime()); // Next scheduled submission should be exactly 1 day after the reported // submission success. let nextScheduled = new Date(finishedDate.getTime() + 24 * 60 * 60 * 1000); do_check_eq(policy.nextDataSubmissionDate.getTime(), nextScheduled.getTime()); // Fast forward some arbitrary time. We shouldn't do any work yet. defineNow(policy, new Date(now.getTime() + 40000)); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); defineNow(policy, nextScheduled); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 2); + do_check_eq(listener.requestDataUploadCount, 2); listener.lastDataRequest.onSubmissionSuccess(new Date(nextScheduled.getTime() + 200)); do_check_eq(policy.nextDataSubmissionDate.getTime(), new Date(nextScheduled.getTime() + 24 * 60 * 60 * 1000 + 200).getTime()); run_next_test(); }); add_test(function test_submission_far_future_scheduling() { @@ -363,22 +385,22 @@ add_test(function test_submission_far_fu defineNow(policy, now); policy.recordUserAcceptance(); now = new Date(); defineNow(policy, now); let nextDate = policy._futureDate(3 * 24 * 60 * 60 * 1000 - 1); policy.nextDataSubmissionDate = nextDate; policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 0); + do_check_eq(listener.requestDataUploadCount, 0); do_check_eq(policy.nextDataSubmissionDate.getTime(), nextDate.getTime()); policy.nextDataSubmissionDate = new Date(nextDate.getTime() + 1); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 0); + do_check_eq(listener.requestDataUploadCount, 0); do_check_eq(policy.nextDataSubmissionDate.getTime(), policy._futureDate(24 * 60 * 60 * 1000).getTime()); run_next_test(); }); add_test(function test_submission_backoff() { let [policy, prefs, listener] = getPolicy("submission_backoff"); @@ -386,54 +408,54 @@ add_test(function test_submission_backof do_check_eq(policy.FAILURE_BACKOFF_INTERVALS.length, 2); policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000); policy.dataSubmissionPolicyAccepted = true; let now = new Date(policy.nextDataSubmissionDate.getTime()); defineNow(policy, now); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); do_check_eq(policy.currentDaySubmissionFailureCount, 0); now = new Date(now.getTime() + 5000); defineNow(policy, now); // On first soft failure we should back off by scheduled interval. listener.lastDataRequest.onSubmissionFailureSoft(); do_check_eq(policy.currentDaySubmissionFailureCount, 1); do_check_eq(policy.nextDataSubmissionDate.getTime(), new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[0]).getTime()); do_check_eq(policy.lastDataSubmissionFailureDate.getTime(), now.getTime()); // Should not request submission until scheduled. now = new Date(policy.nextDataSubmissionDate.getTime() - 1); defineNow(policy, now); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); // 2nd request for submission. now = new Date(policy.nextDataSubmissionDate.getTime()); defineNow(policy, now); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 2); + do_check_eq(listener.requestDataUploadCount, 2); now = new Date(now.getTime() + 5000); defineNow(policy, now); // On second failure we should back off by more. listener.lastDataRequest.onSubmissionFailureSoft(); do_check_eq(policy.currentDaySubmissionFailureCount, 2); do_check_eq(policy.nextDataSubmissionDate.getTime(), new Date(now.getTime() + policy.FAILURE_BACKOFF_INTERVALS[1]).getTime()); now = new Date(policy.nextDataSubmissionDate.getTime()); defineNow(policy, now); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 3); + do_check_eq(listener.requestDataUploadCount, 3); now = new Date(now.getTime() + 5000); defineNow(policy, now); // On 3rd failure we should back off by a whole day. listener.lastDataRequest.onSubmissionFailureSoft(); do_check_eq(policy.currentDaySubmissionFailureCount, 0); do_check_eq(policy.nextDataSubmissionDate.getTime(), @@ -447,26 +469,136 @@ add_test(function test_submission_expiri let [policy, prefs, listener] = getPolicy("submission_expiring"); policy.dataSubmissionPolicyResponseDate = new Date(Date.now() - 24 * 60 * 60 * 1000); policy.dataSubmissionPolicyAccepted = true; let nextDataSubmission = policy.nextDataSubmissionDate; let now = new Date(policy.nextDataSubmissionDate.getTime()); defineNow(policy, now); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); defineNow(policy, new Date(now.getTime() + 500)); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); defineNow(policy, new Date(policy.now().getTime() + policy.SUBMISSION_REQUEST_EXPIRE_INTERVAL_MSEC)); policy.checkStateAndTrigger(); - do_check_eq(listener.requestDataSubmissionCount, 2); + do_check_eq(listener.requestDataUploadCount, 2); + + run_next_test(); +}); + +add_test(function test_delete_remote_data() { + let [policy, prefs, listener] = getPolicy("delete_remote_data"); + + do_check_false(policy.pendingDeleteRemoteData); + let nextSubmissionDate = policy.nextDataSubmissionDate; + + let now = new Date(); + defineNow(policy, now); + + policy.deleteRemoteData(); + do_check_true(policy.pendingDeleteRemoteData); + do_check_neq(nextSubmissionDate.getTime(), + policy.nextDataSubmissionDate.getTime()); + do_check_eq(now.getTime(), policy.nextDataSubmissionDate.getTime()); + + do_check_eq(listener.requestRemoteDeleteCount, 1); + do_check_true(listener.lastRemoteDeleteRequest.isDelete); + defineNow(policy, policy._futureDate(1000)); + + listener.lastRemoteDeleteRequest.onSubmissionSuccess(policy.now()); + do_check_false(policy.pendingDeleteRemoteData); + + run_next_test(); +}); + +// Ensure that deletion requests take priority over regular data submission. +add_test(function test_delete_remote_data_priority() { + let [policy, prefs, listener] = getPolicy("delete_remote_data_priority"); + + let now = new Date(); + defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000)); + policy.recordUserAcceptance(); + defineNow(policy, new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000)); + + policy.checkStateAndTrigger(); + do_check_eq(listener.requestDataUploadCount, 1); + policy._inProgressSubmissionRequest = null; + + policy.deleteRemoteData(); + policy.checkStateAndTrigger(); + + do_check_eq(listener.requestRemoteDeleteCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); + + run_next_test(); +}); + +add_test(function test_delete_remote_data_backoff() { + let [policy, prefs, listener] = getPolicy("delete_remote_data_backoff"); + + let now = new Date(); + defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000)); + policy.recordUserAcceptance(); + defineNow(policy, now); + policy.nextDataSubmissionDate = now; + policy.deleteRemoteData(); + + policy.checkStateAndTrigger(); + do_check_eq(listener.requestRemoteDeleteCount, 1); + defineNow(policy, policy._futureDate(1000)); + policy.checkStateAndTrigger(); + do_check_eq(listener.requestDataUploadCount, 0); + do_check_eq(listener.requestRemoteDeleteCount, 1); + + defineNow(policy, policy._futureDate(500)); + listener.lastRemoteDeleteRequest.onSubmissionFailureSoft(); + defineNow(policy, policy._futureDate(50)); + + policy.checkStateAndTrigger(); + do_check_eq(listener.requestRemoteDeleteCount, 1); + + defineNow(policy, policy._futureDate(policy.FAILURE_BACKOFF_INTERVALS[0] - 50)); + policy.checkStateAndTrigger(); + do_check_eq(listener.requestRemoteDeleteCount, 2); + + run_next_test(); +}); + +// If we request delete while an upload is in progress, delete should be +// scheduled immediately after upload. +add_test(function test_delete_remote_data_in_progress_upload() { + let [policy, prefs, listener] = getPolicy("delete_remote_data_in_progress_upload"); + + let now = new Date(); + defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000)); + policy.recordUserAcceptance(); + defineNow(policy, policy.nextDataSubmissionDate); + + policy.checkStateAndTrigger(); + do_check_eq(listener.requestDataUploadCount, 1); + defineNow(policy, policy._futureDate(50 * 1000)); + + // If we request a delete during a pending request, nothing should be done. + policy.deleteRemoteData(); + policy.checkStateAndTrigger(); + do_check_eq(listener.requestDataUploadCount, 1); + do_check_eq(listener.requestRemoteDeleteCount, 0); + + // Now wait a little bit and finish the request. + defineNow(policy, policy._futureDate(10 * 1000)); + listener.lastDataRequest.onSubmissionSuccess(policy._futureDate(1000)); + defineNow(policy, policy._futureDate(5000)); + + policy.checkStateAndTrigger(); + do_check_eq(listener.requestDataUploadCount, 1); + do_check_eq(listener.requestRemoteDeleteCount, 1); run_next_test(); }); add_test(function test_polling() { let [policy, prefs, listener] = getPolicy("polling"); // Ensure checkStateAndTrigger is called at a regular interval. @@ -484,17 +616,17 @@ add_test(function test_polling() { do_check_true(now2.getTime() - now.getTime() >= 500); now = now2; HealthReportPolicy.prototype.checkStateAndTrigger.call(policy); if (count >= 2) { policy.stopPolling(); do_check_eq(listener.notifyUserCount, 0); - do_check_eq(listener.requestDataSubmissionCount, 0); + do_check_eq(listener.requestDataUploadCount, 0); run_next_test(); } } }); policy.startPolling(); }); @@ -534,26 +666,26 @@ add_test(function test_polling_implicit_ do_check_eq(listener.notifyUserCount, 1); if (count == 1) { listener.lastNotifyRequest.onUserNotifyComplete(); } if (count < 4) { do_check_false(policy.dataSubmissionPolicyAccepted); - do_check_eq(listener.requestDataSubmissionCount, 0); + do_check_eq(listener.requestDataUploadCount, 0); } else { do_check_true(policy.dataSubmissionPolicyAccepted); do_check_eq(policy.dataSubmissionPolicyResponseType, "accepted-implicit-time-elapsed"); - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); } if (count > 4) { - do_check_eq(listener.requestDataSubmissionCount, 1); + do_check_eq(listener.requestDataUploadCount, 1); policy.stopPolling(); run_next_test(); } } }); policy.firstRunDate = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000); policy.nextDataSubmissionDate = new Date(Date.now());