356 lines
9.1 KiB
JavaScript
356 lines
9.1 KiB
JavaScript
/**
|
|
* Tests the JSONFile object.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
// Globals
|
|
ChromeUtils.defineESModuleGetters(this, {
|
|
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
|
|
FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
|
|
JSONFile: "resource://gre/modules/JSONFile.sys.mjs",
|
|
sinon: "resource://testing-common/Sinon.sys.mjs",
|
|
});
|
|
|
|
/**
|
|
* Returns a reference to a temporary file that is guaranteed not to exist and
|
|
* is cleaned up later. See FileTestUtils.getTempFile for details.
|
|
*/
|
|
function getTempFile(leafName) {
|
|
return FileTestUtils.getTempFile(leafName);
|
|
}
|
|
|
|
const TEST_STORE_FILE_NAME = "test-store.json";
|
|
|
|
const TEST_DATA = {
|
|
number: 123,
|
|
string: "test",
|
|
object: {
|
|
prop1: 1,
|
|
prop2: 2,
|
|
},
|
|
};
|
|
|
|
// Tests
|
|
|
|
add_task(async function test_save_reload() {
|
|
let storeForSave = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
});
|
|
|
|
await storeForSave.load();
|
|
|
|
Assert.ok(storeForSave.dataReady);
|
|
Assert.deepEqual(storeForSave.data, {});
|
|
|
|
Object.assign(storeForSave.data, TEST_DATA);
|
|
|
|
await new Promise(resolve => {
|
|
let save = storeForSave._save.bind(storeForSave);
|
|
storeForSave._save = () => {
|
|
save();
|
|
resolve();
|
|
};
|
|
storeForSave.saveSoon();
|
|
});
|
|
|
|
let storeForLoad = new JSONFile({
|
|
path: storeForSave.path,
|
|
});
|
|
|
|
await storeForLoad.load();
|
|
|
|
Assert.deepEqual(storeForLoad.data, TEST_DATA);
|
|
});
|
|
|
|
add_task(async function test_load_sync() {
|
|
let storeForSave = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
});
|
|
await storeForSave.load();
|
|
Object.assign(storeForSave.data, TEST_DATA);
|
|
await storeForSave._save();
|
|
|
|
let storeForLoad = new JSONFile({
|
|
path: storeForSave.path,
|
|
});
|
|
storeForLoad.ensureDataReady();
|
|
|
|
Assert.deepEqual(storeForLoad.data, TEST_DATA);
|
|
});
|
|
|
|
add_task(async function test_load_with_dataPostProcessor() {
|
|
let storeForSave = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
});
|
|
await storeForSave.load();
|
|
Object.assign(storeForSave.data, TEST_DATA);
|
|
await storeForSave._save();
|
|
|
|
let random = Math.random();
|
|
let storeForLoad = new JSONFile({
|
|
path: storeForSave.path,
|
|
dataPostProcessor: data => {
|
|
Assert.deepEqual(data, TEST_DATA);
|
|
|
|
data.test = random;
|
|
return data;
|
|
},
|
|
});
|
|
|
|
await storeForLoad.load();
|
|
|
|
Assert.equal(storeForLoad.data.test, random);
|
|
});
|
|
|
|
add_task(async function test_load_with_dataPostProcessor_fails() {
|
|
let store = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
dataPostProcessor: () => {
|
|
throw new Error("dataPostProcessor fails.");
|
|
},
|
|
});
|
|
|
|
await Assert.rejects(store.load(), /dataPostProcessor fails\./);
|
|
|
|
Assert.ok(!store.dataReady);
|
|
});
|
|
|
|
add_task(async function test_load_sync_with_dataPostProcessor_fails() {
|
|
let store = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
dataPostProcessor: () => {
|
|
throw new Error("dataPostProcessor fails.");
|
|
},
|
|
});
|
|
|
|
Assert.throws(() => store.ensureDataReady(), /dataPostProcessor fails\./);
|
|
|
|
Assert.ok(!store.dataReady);
|
|
});
|
|
|
|
/**
|
|
* Loads data from a string in a predefined format. The purpose of this test is
|
|
* to verify that the JSON format used in previous versions can be loaded.
|
|
*/
|
|
add_task(async function test_load_string_predefined() {
|
|
let store = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
});
|
|
|
|
let string = '{"number":123,"string":"test","object":{"prop1":1,"prop2":2}}';
|
|
|
|
await IOUtils.writeUTF8(store.path, string, {
|
|
tmpPath: store.path + ".tmp",
|
|
});
|
|
|
|
await store.load();
|
|
|
|
Assert.deepEqual(store.data, TEST_DATA);
|
|
});
|
|
|
|
/**
|
|
* Loads data from a malformed JSON string.
|
|
*/
|
|
add_task(async function test_load_string_malformed() {
|
|
let store = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
});
|
|
|
|
let string = '{"number":123,"string":"test","object":{"prop1":1,';
|
|
|
|
await IOUtils.writeUTF8(store.path, string, {
|
|
tmpPath: store.path + ".tmp",
|
|
});
|
|
|
|
await store.load();
|
|
|
|
// A backup file should have been created.
|
|
Assert.ok(await IOUtils.exists(store.path + ".corrupt"));
|
|
await IOUtils.remove(store.path + ".corrupt");
|
|
|
|
// The store should be ready to accept new data.
|
|
Assert.ok(store.dataReady);
|
|
Assert.deepEqual(store.data, {});
|
|
});
|
|
|
|
/**
|
|
* Loads data from a malformed JSON string, using the synchronous initialization
|
|
* path.
|
|
*/
|
|
add_task(async function test_load_string_malformed_sync() {
|
|
let store = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
});
|
|
|
|
let string = '{"number":123,"string":"test","object":{"prop1":1,';
|
|
|
|
await IOUtils.writeUTF8(store.path, string, {
|
|
tmpPath: store.path + ".tmp",
|
|
});
|
|
|
|
store.ensureDataReady();
|
|
|
|
// A backup file should have been created.
|
|
Assert.ok(await IOUtils.exists(store.path + ".corrupt"));
|
|
await IOUtils.remove(store.path + ".corrupt");
|
|
|
|
// The store should be ready to accept new data.
|
|
Assert.ok(store.dataReady);
|
|
Assert.deepEqual(store.data, {});
|
|
});
|
|
|
|
add_task(async function test_overwrite_data() {
|
|
let storeForSave = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
});
|
|
|
|
let string = `{"number":456,"string":"tset","object":{"prop1":3,"prop2":4}}`;
|
|
|
|
await IOUtils.writeUTF8(storeForSave.path, string, {
|
|
tmpPath: storeForSave.path + ".tmp",
|
|
});
|
|
|
|
Assert.ok(!storeForSave.dataReady);
|
|
storeForSave.data = TEST_DATA;
|
|
Assert.ok(storeForSave.dataReady);
|
|
Assert.equal(storeForSave.data, TEST_DATA);
|
|
|
|
await new Promise(resolve => {
|
|
let save = storeForSave._save.bind(storeForSave);
|
|
storeForSave._save = () => {
|
|
save();
|
|
resolve();
|
|
};
|
|
storeForSave.saveSoon();
|
|
});
|
|
|
|
let storeForLoad = new JSONFile({
|
|
path: storeForSave.path,
|
|
});
|
|
|
|
await storeForLoad.load();
|
|
|
|
Assert.deepEqual(storeForLoad.data, TEST_DATA);
|
|
});
|
|
|
|
add_task(async function test_beforeSave() {
|
|
let store;
|
|
let promiseBeforeSave = new Promise(resolve => {
|
|
store = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
beforeSave: resolve,
|
|
saveDelayMs: 250,
|
|
});
|
|
});
|
|
|
|
store.saveSoon();
|
|
|
|
await promiseBeforeSave;
|
|
});
|
|
|
|
add_task(async function test_beforeSave_rejects() {
|
|
let storeForSave = new JSONFile({
|
|
path: getTempFile(TEST_STORE_FILE_NAME).path,
|
|
beforeSave() {
|
|
return Promise.reject(new Error("oops"));
|
|
},
|
|
saveDelayMs: 250,
|
|
});
|
|
|
|
let promiseSave = new Promise((resolve, reject) => {
|
|
let save = storeForSave._save.bind(storeForSave);
|
|
storeForSave._save = () => {
|
|
save().then(resolve, reject);
|
|
};
|
|
storeForSave.saveSoon();
|
|
});
|
|
|
|
await Assert.rejects(promiseSave, function (ex) {
|
|
return ex.message == "oops";
|
|
});
|
|
});
|
|
|
|
add_task(async function test_finalize() {
|
|
let path = getTempFile(TEST_STORE_FILE_NAME).path;
|
|
|
|
let barrier = new AsyncShutdown.Barrier("test-auto-finalize");
|
|
let storeForSave = new JSONFile({
|
|
path,
|
|
saveDelayMs: 2000,
|
|
finalizeAt: barrier.client,
|
|
});
|
|
await storeForSave.load();
|
|
storeForSave.data = TEST_DATA;
|
|
storeForSave.saveSoon();
|
|
|
|
let promiseFinalize = storeForSave.finalize();
|
|
await Assert.rejects(storeForSave.finalize(), /has already been finalized$/);
|
|
await promiseFinalize;
|
|
Assert.ok(!storeForSave.dataReady);
|
|
|
|
// Finalization removes the blocker, so waiting should not log an unhandled
|
|
// error even though the object has been explicitly finalized.
|
|
await barrier.wait();
|
|
|
|
let storeForLoad = new JSONFile({ path });
|
|
await storeForLoad.load();
|
|
Assert.deepEqual(storeForLoad.data, TEST_DATA);
|
|
});
|
|
|
|
add_task(async function test_finalize_on_shutdown() {
|
|
let path = getTempFile(TEST_STORE_FILE_NAME).path;
|
|
|
|
let barrier = new AsyncShutdown.Barrier("test-finalize-shutdown");
|
|
let storeForSave = new JSONFile({
|
|
path,
|
|
saveDelayMs: 2000,
|
|
finalizeAt: barrier.client,
|
|
});
|
|
await storeForSave.load();
|
|
storeForSave.data = TEST_DATA;
|
|
// Arm the saver, then simulate shutdown and ensure the file is
|
|
// automatically finalized.
|
|
storeForSave.saveSoon();
|
|
|
|
await barrier.wait();
|
|
// It's possible for `finalize` to reject when called concurrently with
|
|
// shutdown. We don't distinguish between explicit `finalize` calls and
|
|
// finalization on shutdown because we expect most consumers to rely on the
|
|
// latter. However, this behavior can be safely changed if needed.
|
|
await Assert.rejects(storeForSave.finalize(), /has already been finalized$/);
|
|
Assert.ok(!storeForSave.dataReady);
|
|
|
|
let storeForLoad = new JSONFile({ path });
|
|
await storeForLoad.load();
|
|
Assert.deepEqual(storeForLoad.data, TEST_DATA);
|
|
});
|
|
|
|
add_task(async function test_save_failure_handler() {
|
|
let path = getTempFile(TEST_STORE_FILE_NAME).path;
|
|
let saveFailureHandler = sinon.stub();
|
|
let storeForSave = new JSONFile({
|
|
path,
|
|
saveDelayMs: 10,
|
|
saveFailureHandler,
|
|
});
|
|
await storeForSave.load();
|
|
|
|
// Let's now add some invalid data that we'll fail to save.
|
|
let circularThing = {};
|
|
circularThing.circle = circularThing;
|
|
|
|
storeForSave.data = {
|
|
circularThing,
|
|
};
|
|
await storeForSave._save();
|
|
Assert.ok(saveFailureHandler.calledOnce, "Failure handler was called");
|
|
Assert.ok(
|
|
saveFailureHandler.calledWith(
|
|
sinon.match(error => {
|
|
return error.message == "cyclic object value";
|
|
})
|
|
),
|
|
"Handler was passed Error"
|
|
);
|
|
});
|