Bug 1063710 - Make Reset Profile write the reset's timestamp to times.json. r=gps, a=lsblakk
authorMark Hammond <mhammond@skippinet.com.au>
Thu, 23 Oct 2014 15:00:23 +1100
changeset 233540 84ef9c8e5eef47ca9bde7a33fa2d96e2b418783c
parent 233539 547fe9ee7948becf5171d888f1211a7ee672ca88
child 233541 42d38ca6114d3fdebbf913830151603593925784
push id4187
push userbhearsum@mozilla.com
push dateFri, 28 Nov 2014 15:29:12 +0000
treeherdermozilla-beta@f23cc6a30c11 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps, lsblakk
bugs1063710
milestone35.0a2
Bug 1063710 - Make Reset Profile write the reset's timestamp to times.json. r=gps, a=lsblakk
browser/components/migration/FirefoxProfileMigrator.js
browser/components/migration/tests/unit/test_fx_fhr.js
services/healthreport/docs/dataformat.rst
services/healthreport/profile.jsm
services/healthreport/tests/xpcshell/test_profile.js
--- a/browser/components/migration/FirefoxProfileMigrator.js
+++ b/browser/components/migration/FirefoxProfileMigrator.js
@@ -19,16 +19,18 @@ Components.utils.import("resource://gre/
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
                                   "resource://gre/modules/PlacesBackups.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "SessionMigration",
                                   "resource:///modules/sessionstore/SessionMigration.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ProfileTimesAccessor",
+                                  "resource://gre/modules/services/healthreport/profile.jsm");
 
 
 function FirefoxProfileMigrator() {
   this.wrappedJSObject = this; // for testing...
 }
 
 FirefoxProfileMigrator.prototype = Object.create(MigratorPrototype);
 
@@ -127,17 +129,32 @@ FirefoxProfileMigrator.prototype._getRes
         }, function() {
           aCallback(false);
         });
       }
     }
   }
 
   // FHR related migrations.
-  let times = getFileResource(types.OTHERDATA, ["times.json"]);
+  let times = {
+    name: "times", // name is used only by tests.
+    type: types.OTHERDATA,
+    migrate: aCallback => {
+      let file = this._getFileObject(sourceProfileDir, "times.json");
+      if (file) {
+        file.copyTo(currentProfileDir, "");
+      }
+      // And record the fact a migration (ie, a reset) happened.
+      let timesAccessor = new ProfileTimesAccessor(currentProfileDir.path);
+      timesAccessor.recordProfileReset().then(
+        () => aCallback(true),
+        () => aCallback(false)
+      );
+    }
+  };
   let healthReporter = {
     name: "healthreporter", // name is used only by tests...
     type: types.OTHERDATA,
     migrate: aCallback => {
       // the health-reporter can't have been initialized yet so it's safe to
       // copy the SQL file.
 
       // We only support the default database name - copied from healthreporter.jsm
--- a/browser/components/migration/tests/unit/test_fx_fhr.js
+++ b/browser/components/migration/tests/unit/test_fx_fhr.js
@@ -2,16 +2,29 @@
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
 
 function run_test() {
   run_next_test();
 }
 
+function readFile(file) {
+  let stream = Cc['@mozilla.org/network/file-input-stream;1']
+               .createInstance(Ci.nsIFileInputStream);
+  stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+  let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+            .createInstance(Ci.nsIScriptableInputStream);
+  sis.init(stream);
+  let contents = sis.read(file.fileSize);
+  sis.close();
+  return contents;
+}
+
 function checkDirectoryContains(dir, files) {
   print("checking " + dir.path + " - should contain " + Object.keys(files));
   let seen = new Set();
   let enumerator = dir.directoryEntries;
   while (enumerator.hasMoreElements()) {
     let file = enumerator.getNext().QueryInterface(Ci.nsIFile);
     print("found file: " + file.path);
     Assert.ok(file.leafName in files, file.leafName + " exists, but shouldn't");
@@ -20,26 +33,17 @@ function checkDirectoryContains(dir, fil
     if (typeof expectedContents != "string") {
       // it's a subdir - recurse!
       Assert.ok(file.isDirectory(), "should be a subdir");
       let newDir = dir.clone();
       newDir.append(file.leafName);
       checkDirectoryContains(newDir, expectedContents);
     } else {
       Assert.ok(!file.isDirectory(), "should be a regular file");
-      let stream = Cc['@mozilla.org/network/file-input-stream;1']
-                   .createInstance(Ci.nsIFileInputStream);
-      stream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
-
-      let sis = Cc["@mozilla.org/scriptableinputstream;1"]
-                .createInstance(Ci.nsIScriptableInputStream);
-      sis.init(stream);
-      let contents = sis.read(file.fileSize);
-      sis.close();
-
+      let contents = readFile(file);
       Assert.equal(contents, expectedContents);
     }
     seen.add(file.leafName);
   }
   let missing = [x for (x in files) if (!seen.has(x))];
   Assert.deepEqual(missing, [], "no missing files in " + dir.path);
 }
 
@@ -63,29 +67,33 @@ function writeToFile(dir, leafName, cont
   let file = dir.clone();
   file.append(leafName);
 
   let outputStream = FileUtils.openFileOutputStream(file);
   outputStream.write(contents, contents.length);
   outputStream.close();
 }
 
-function promiseFHRMigrator(srcDir, targetDir) {
+function promiseMigrator(name, srcDir, targetDir) {
   let migrator = Cc["@mozilla.org/profile/migrator;1?app=browser&type=firefox"]
                  .createInstance(Ci.nsISupports)
                  .wrappedJSObject;
   let migrators = migrator._getResourcesInternal(srcDir, targetDir);
   for (let m of migrators) {
-    if (m.name == "healthreporter") {
+    if (m.name == name) {
       return new Promise((resolve, reject) => {
         m.migrate(resolve);
       });
     }
   }
-  throw new Error("failed to find the fhr migrator");
+  throw new Error("failed to find the " + name + " migrator");
+}
+
+function promiseFHRMigrator(srcDir, targetDir) {
+  return promiseMigrator("healthreporter", srcDir, targetDir);
 }
 
 add_task(function* test_empty() {
   let [srcDir, targetDir] = getTestDirs();
   let ok = yield promiseFHRMigrator(srcDir, targetDir);
   Assert.ok(ok, "callback should have been true with empty directories");
   // check both are empty
   checkDirectoryContains(srcDir, {});
@@ -221,8 +229,30 @@ add_task(function* test_datareporting_ma
   Assert.ok(ok, "callback should have been true");
 
   checkDirectoryContains(targetDir, {
     "datareporting" : {
       "state.json": "should be copied",
     }
   });
 });
+
+add_task(function* test_times_migration() {
+  let [srcDir, targetDir] = getTestDirs();
+
+  // create a times.json in the source directory.
+  let contents = JSON.stringify({created: 1234});
+  writeToFile(srcDir, "times.json", contents);
+
+  let earliest = Date.now();
+  let ok = yield promiseMigrator("times", srcDir, targetDir);
+  Assert.ok(ok, "callback should have been true");
+  let latest = Date.now();
+
+  let timesFile = targetDir.clone();
+  timesFile.append("times.json");
+
+  let raw = readFile(timesFile);
+  let times = JSON.parse(raw);
+  Assert.ok(times.reset >= earliest && times.reset <= latest);
+  // and it should have left the creation time alone.
+  Assert.equal(times.created, 1234);
+});
--- a/services/healthreport/docs/dataformat.rst
+++ b/services/healthreport/docs/dataformat.rst
@@ -1355,38 +1355,49 @@ Example
       "_v": 1,
       "bookmarks": 388,
       "pages": 94870
     }
 
 org.mozilla.profile.age
 -----------------------
 
-This measurement contains information about the current profile's age.
+This measurement contains information about the current profile's age (and
+in version 2, the profile's most recent reset date)
+
+Version 2
+^^^^^^^^^
+
+*profileCreation* and *profileReset* properties are present.  Both define
+the integer days since UNIX epoch that the current profile was created or
+reset accordingly.
 
 Version 1
 ^^^^^^^^^
 
 A single *profileCreation* property is present. It defines the integer
 days since UNIX epoch that the current profile was created.
 
 Notes
 ^^^^^
 
 It is somewhat difficult to obtain a reliable *profile born date* due to a
-number of factors.
+number of factors, but since Version 2, improvements have been made - on a
+"profile reset" we copy the profileCreation date from the old profile and
+record the time of the reset in profileReset.
 
 Example
 ^^^^^^^
 
 ::
 
     "org.mozilla.profile.age": {
-      "_v": 1,
+      "_v": 2,
       "profileCreation": 15176
+      "profileReset": 15576
     }
 
 org.mozilla.searches.counts
 ---------------------------
 
 This measurement contains information about searches performed in the
 application.
 
--- a/services/healthreport/profile.jsm
+++ b/services/healthreport/profile.jsm
@@ -2,94 +2,106 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef MERGED_COMPARTMENT
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
-  "ProfileCreationTimeAccessor",
+  "ProfileTimesAccessor",
   "ProfileMetadataProvider",
 ];
 
 const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
 
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 Cu.import("resource://gre/modules/Metrics.jsm");
 
 #endif
 
 const DEFAULT_PROFILE_MEASUREMENT_NAME = "age";
+const DEFAULT_PROFILE_MEASUREMENT_VERSION = 2;
 const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
 
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/osfile.jsm")
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://services-common/utils.js");
 
-// Profile creation time access.
+// Profile access to times.json (eg, creation/reset time).
 // This is separate from the provider to simplify testing and enable extraction
 // to a shared location in the future.
-this.ProfileCreationTimeAccessor = function(profile, log) {
+this.ProfileTimesAccessor = function(profile, log) {
   this.profilePath = profile || OS.Constants.Path.profileDir;
   if (!this.profilePath) {
     throw new Error("No profile directory.");
   }
   this._log = log || {"debug": function (s) { dump(s + "\n"); }};
 }
-this.ProfileCreationTimeAccessor.prototype = {
+this.ProfileTimesAccessor.prototype = {
   /**
    * There are three ways we can get our creation time:
    *
    * 1. From our own saved value (to avoid redundant work).
    * 2. From the on-disk JSON file.
    * 3. By calculating it from the filesystem.
    *
    * If we have to calculate, we write out the file; if we have
    * to touch the file, we persist in-memory.
    *
    * @return a promise that resolves to the profile's creation time.
    */
   get created() {
-    if (this._created) {
-      return Promise.resolve(this._created);
-    }
-
     function onSuccess(times) {
-      if (times && times.created) {
-        return this._created = times.created;
+      if (times.created) {
+        return times.created;
       }
       return onFailure.call(this, null, times);
     }
 
     function onFailure(err, times) {
-      return this.computeAndPersistTimes(times)
+      return this.computeAndPersistCreated(times)
                  .then(function onSuccess(created) {
-                         return this._created = created;
+                         return created;
                        }.bind(this));
     }
 
-    return this.readTimes()
+    return this.getTimes()
                .then(onSuccess.bind(this),
                      onFailure.bind(this));
   },
 
   /**
    * Explicitly make `file`, a filename, a full path
    * relative to our profile path.
    */
   getPath: function (file) {
     return OS.Path.join(this.profilePath, file);
   },
 
   /**
    * Return a promise which resolves to the JSON contents
+   * of the time file, using the already read value if possible.
+   */
+  getTimes: function (file="times.json") {
+    if (this._times) {
+      return Promise.resolve(this._times);
+    }
+    return this.readTimes(file).then(
+      times => {
+        return this.times = times || {};
+      }
+    );
+  },
+
+  /**
+   * Return a promise which resolves to the JSON contents
    * of the time file in this accessor's profile.
    */
   readTimes: function (file="times.json") {
     return CommonUtils.readJSON(this.getPath(file));
   },
 
   /**
    * Return a promise representing the writing of `contents`
@@ -98,21 +110,22 @@ this.ProfileCreationTimeAccessor.prototy
   writeTimes: function (contents, file="times.json") {
     return CommonUtils.writeJSON(contents, this.getPath(file));
   },
 
   /**
    * Merge existing contents with a 'created' field, writing them
    * to the specified file. Promise, naturally.
    */
-  computeAndPersistTimes: function (existingContents, file="times.json") {
+  computeAndPersistCreated: function (existingContents, file="times.json") {
     let path = this.getPath(file);
     function onOldest(oldest) {
       let contents = existingContents || {};
       contents.created = oldest;
+      this._times = contents;
       return this.writeTimes(contents, path)
                  .then(function onSuccess() {
                    return oldest;
                  });
     }
 
     return this.getOldestProfileTimestamp()
                .then(onOldest.bind(this));
@@ -172,73 +185,131 @@ this.ProfileCreationTimeAccessor.prototy
 
     function onFailure(reason) {
       iterator.close();
       throw new Error("Unable to fetch oldest profile entry: " + reason);
     }
 
     return promise.then(onSuccess, onFailure);
   },
+
+  /**
+   * Record (and persist) when a profile reset happened.  We just store a
+   * single value - the timestamp of the most recent reset - but there is scope
+   * to keep a list of reset times should our health-reporter successor
+   * be able to make use of that.
+   * Returns a promise that is resolved once the file has been written.
+   */
+  recordProfileReset: function (time=Date.now(), file="times.json") {
+    return this.getTimes(file).then(
+      times => {
+        times.reset = time;
+        return this.writeTimes(times, file);
+      }
+    );
+  },
+
+  /* Returns a promise that resolves to the time the profile was reset,
+   * or undefined if not recorded.
+   */
+  get reset() {
+    return this.getTimes().then(
+      times => times.reset
+    );
+  },
 }
 
 /**
  * Measurements pertaining to the user's profile.
  */
+// This is "version 1" of the metadata measurement - it must remain, but
+// it's currently unused - see bug 1063714 comment 12 for why.
 function ProfileMetadataMeasurement() {
   Metrics.Measurement.call(this);
 }
 ProfileMetadataMeasurement.prototype = {
   __proto__: Metrics.Measurement.prototype,
 
   name: DEFAULT_PROFILE_MEASUREMENT_NAME,
   version: 1,
 
   fields: {
     // Profile creation date. Number of days since Unix epoch.
     profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
   },
 };
 
+// This is the current measurement - it adds the profileReset value.
+function ProfileMetadataMeasurement2() {
+  Metrics.Measurement.call(this);
+}
+ProfileMetadataMeasurement2.prototype = {
+  __proto__: Metrics.Measurement.prototype,
+
+  name: DEFAULT_PROFILE_MEASUREMENT_NAME,
+  version: DEFAULT_PROFILE_MEASUREMENT_VERSION,
+
+  fields: {
+    // Profile creation date. Number of days since Unix epoch.
+    profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
+    // Profile reset date. Number of days since Unix epoch.
+    profileReset: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
+  },
+};
+
 /**
  * Turn a millisecond timestamp into a day timestamp.
  *
  * @param msec a number of milliseconds since epoch.
  * @return the number of whole days denoted by the input.
  */
 function truncate(msec) {
   return Math.floor(msec / MILLISECONDS_PER_DAY);
 }
 
 /**
- * A Metrics.Provider for profile metadata, such as profile creation time.
+ * A Metrics.Provider for profile metadata, such as profile creation and
+ * reset time.
  */
 this.ProfileMetadataProvider = function() {
   Metrics.Provider.call(this);
 }
 this.ProfileMetadataProvider.prototype = {
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.profile",
 
-  measurementTypes: [ProfileMetadataMeasurement],
+  measurementTypes: [ProfileMetadataMeasurement2],
 
   pullOnly: true,
 
-  getProfileCreationDays: function () {
-    let accessor = new ProfileCreationTimeAccessor(null, this._log);
+  getProfileDays: Task.async(function* () {
+    let result = {};
+    let accessor = new ProfileTimesAccessor(null, this._log);
 
-    return accessor.created
-                   .then(truncate);
-  },
+    let created = yield accessor.created;
+    result["profileCreation"] = truncate(created);
+    let reset = yield accessor.reset;
+    if (reset) {
+      result["profileReset"] = truncate(reset);
+    }
+    return result;
+  }),
 
   collectConstantData: function () {
-    let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1);
+    let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME,
+                                DEFAULT_PROFILE_MEASUREMENT_VERSION);
 
-    return Task.spawn(function collectConstant() {
-      let createdDays = yield this.getProfileCreationDays();
+    return Task.spawn(function* collectConstants() {
+      let days = yield this.getProfileDays();
 
       yield this.enqueueStorageOperation(function storeDays() {
-        return m.setLastNumeric("profileCreation", createdDays);
+        return Task.spawn(function* () {
+          yield m.setLastNumeric("profileCreation", days["profileCreation"]);
+          if (days["profileReset"]) {
+            yield m.setLastNumeric("profileReset", days["profileReset"]);
+          }
+        });
       });
     }.bind(this));
   },
 };
 
--- a/services/healthreport/tests/xpcshell/test_profile.js
+++ b/services/healthreport/tests/xpcshell/test_profile.js
@@ -19,19 +19,24 @@ Cu.import("resource://gre/modules/Task.j
 
 
 function MockProfileMetadataProvider(name="MockProfileMetadataProvider") {
   this.name = name;
   ProfileMetadataProvider.call(this);
 }
 MockProfileMetadataProvider.prototype = {
   __proto__: ProfileMetadataProvider.prototype,
+  includeProfileReset: false,
 
-  getProfileCreationDays: function getProfileCreationDays() {
-    return Promise.resolve(1234);
+  getProfileDays: function getProfileDays() {
+    let result = {profileCreation: 1234};
+    if (this.includeProfileReset) {
+      result.profileReset = 5678;
+    }
+    return Promise.resolve(result);
   },
 };
 
 
 function run_test() {
   run_next_test();
 }
 
@@ -54,17 +59,17 @@ add_test(function use_os_file() {
     run_next_test();
   }, function onFail() {
     iterator.close();
     do_throw("Iterating over current directory failed.");
   });
 });
 
 function getAccessor() {
-  let acc = new ProfileCreationTimeAccessor();
+  let acc = new ProfileTimesAccessor();
   print("Profile is " + acc.profilePath);
   return acc;
 }
 
 add_test(function test_time_accessor_no_file() {
   let acc = getAccessor();
 
   // There should be no file yet.
@@ -92,17 +97,17 @@ add_task(function test_time_accessor_cre
 
   // Ensure that provided contents are merged, and existing
   // files can be overwritten. These two things occur if we
   // read and then decide that we have to write.
   let acc = getAccessor();
   let existing = {abc: "123", easy: "abc"};
   let expected;
 
-  let created = yield acc.computeAndPersistTimes(existing, "test2.json")
+  let created = yield acc.computeAndPersistCreated(existing, "test2.json")
   let upper = Date.now() + 1000;
   print(lower + " < " + created + " <= " + upper);
   do_check_true(lower < created);
   do_check_true(upper >= created);
   expected = created;
 
   let json = yield acc.readTimes("test2.json")
   print("Read: " + JSON.stringify(json));
@@ -120,77 +125,133 @@ add_task(function test_time_accessor_all
   do_check_true(lower < created);
   do_check_true(upper >= created);
   expected = created;
 
   let again = yield acc.created
   do_check_eq(expected, again);
 });
 
+add_task(function* test_time_reset() {
+  let lower = profile_creation_lower;
+  let acc = getAccessor();
+  let testTime = 100000;
+  yield acc.recordProfileReset(testTime);
+  let reset = yield acc.reset;
+  Assert.equal(reset, testTime);
+});
+
 add_test(function test_constructor() {
   let provider = new ProfileMetadataProvider("named");
   run_next_test();
 });
 
 add_test(function test_profile_files() {
   let provider = new ProfileMetadataProvider();
 
   function onSuccess(answer) {
     let now = Date.now() / MILLISECONDS_PER_DAY;
-    print("Got " + answer + ", versus now = " + now);
-    do_check_true(answer < now);
+    print("Got " + answer.profileCreation + ", versus now = " + now);
+    Assert.ok(answer.profileCreation < now);
     run_next_test();
   }
 
   function onFailure(ex) {
     do_throw("Directory iteration failed: " + ex);
   }
 
-  provider.getProfileCreationDays().then(onSuccess, onFailure);
+  provider.getProfileDays().then(onSuccess, onFailure);
 });
 
 // A generic test helper. We use this with both real
 // and mock providers in these tests.
-function test_collect_constant(provider) {
-  return Task.spawn(function () {
+function test_collect_constant(provider, expectReset) {
+  return Task.spawn(function* () {
     yield provider.collectConstantData();
 
-    let m = provider.getMeasurement("age", 1);
-    do_check_neq(m, null);
+    let m = provider.getMeasurement("age", 2);
+    Assert.notEqual(m, null);
     let values = yield m.getValues();
-    do_check_eq(values.singular.size, 1);
-    do_check_true(values.singular.has("profileCreation"));
-
-    throw new Task.Result(values.singular.get("profileCreation")[1]);
+    Assert.ok(values.singular.has("profileCreation"));
+    let createValue = values.singular.get("profileCreation")[1];
+    let resetValue;
+    if (expectReset) {
+      Assert.equal(values.singular.size, 2);
+      Assert.ok(values.singular.has("profileReset"));
+      resetValue = values.singular.get("profileReset")[1];
+    } else {
+      Assert.equal(values.singular.size, 1);
+      Assert.ok(!values.singular.has("profileReset"));
+    }
+    return [createValue, resetValue];
   });
 }
 
-add_task(function test_collect_constant_mock() {
+add_task(function* test_collect_constant_mock_no_reset() {
   let storage = yield Metrics.Storage("collect_constant_mock");
   let provider = new MockProfileMetadataProvider();
   yield provider.init(storage);
 
-  let v = yield test_collect_constant(provider);
-  do_check_eq(v, 1234);
+  let v = yield test_collect_constant(provider, false);
+  Assert.equal(v.length, 2);
+  Assert.equal(v[0], 1234);
+  Assert.equal(v[1], undefined);
 
   yield storage.close();
 });
 
-add_task(function test_collect_constant_real() {
+add_task(function* test_collect_constant_mock_with_reset() {
+  let storage = yield Metrics.Storage("collect_constant_mock");
+  let provider = new MockProfileMetadataProvider();
+  provider.includeProfileReset = true;
+  yield provider.init(storage);
+
+  let v = yield test_collect_constant(provider, true);
+  Assert.equal(v.length, 2);
+  Assert.equal(v[0], 1234);
+  Assert.equal(v[1], 5678);
+
+  yield storage.close();
+});
+
+add_task(function* test_collect_constant_real_no_reset() {
   let provider = new ProfileMetadataProvider();
   let storage = yield Metrics.Storage("collect_constant_real");
   yield provider.init(storage);
 
-  let v = yield test_collect_constant(provider);
+  let vals = yield test_collect_constant(provider, false);
+  let created = vals[0];
+  let reset = vals[1];
+  Assert.equal(reset, undefined);
 
-  let ms = v * MILLISECONDS_PER_DAY;
+  let ms = created * MILLISECONDS_PER_DAY;
   let lower = profile_creation_lower;
   let upper = Date.now() + 1000;
-  print("Day:   " + v);
+  print("Day:   " + created);
   print("msec:  " + ms);
   print("Lower: " + lower);
   print("Upper: " + upper);
-  do_check_true(lower <= ms);
-  do_check_true(upper >= ms);
+  Assert.ok(lower <= ms);
+  Assert.ok(upper >= ms);
 
   yield storage.close();
 });
 
+add_task(function* test_collect_constant_real_with_reset() {
+  let now = Date.now();
+  let acc = getAccessor();
+  yield acc.writeTimes({created: now-MILLISECONDS_PER_DAY, // yesterday
+                        reset: Date.now()}); // today
+
+  let provider = new ProfileMetadataProvider();
+  let storage = yield Metrics.Storage("collect_constant_real");
+  yield provider.init(storage);
+
+  let [created, reset] = yield test_collect_constant(provider, true);
+  // we've already tested truncate() works as expected, so here just check
+  // we got values.
+  Assert.ok(created);
+  Assert.ok(reset);
+  Assert.ok(created <= reset);
+
+  yield storage.close();
+});
+