Commit 122da4b3 authored by Romain Courteaud's avatar Romain Courteaud

Rewrite replicateStorage.

Synchronisation status is based on document signature (sha) comparison.
Signature are stored each time a document is successfully synchronised.

Conflict are detected but reported as an error for now, as no solve process is implemented.
parent ece651db
...@@ -171,6 +171,9 @@ module.exports = function (grunt) { ...@@ -171,6 +171,9 @@ module.exports = function (grunt) {
'src/jio.js', 'src/jio.js',
'node_modules/rusha/rusha.js',
'src/jio.storage/replicatestorage.js',
'src/jio.storage/uuidstorage.js', 'src/jio.storage/uuidstorage.js',
'src/jio.storage/memorystorage.js', 'src/jio.storage/memorystorage.js',
'src/jio.storage/localstorage.js', 'src/jio.storage/localstorage.js',
......
...@@ -31,7 +31,8 @@ ...@@ -31,7 +31,8 @@
"dependencies": { "dependencies": {
"rsvp": "git+http://git.erp5.org/repos/rsvp.js.git", "rsvp": "git+http://git.erp5.org/repos/rsvp.js.git",
"uritemplate": "git+http://git.erp5.org/repos/uritemplate-js.git", "uritemplate": "git+http://git.erp5.org/repos/uritemplate-js.git",
"moment": "2.8.3" "moment": "2.8.3",
"rusha": "0.8.2"
}, },
"devDependencies": { "devDependencies": {
"renderjs": "git+http://git.erp5.org/repos/renderjs.git", "renderjs": "git+http://git.erp5.org/repos/renderjs.git",
......
/* /*
* JIO extension for resource replication. * JIO extension for resource replication.
* Copyright (C) 2013 Nexedi SA * Copyright (C) 2013, 2015 Nexedi SA
* *
* This library is free software: you can redistribute it and/or modify * This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by * it under the terms of the GNU Lesser General Public License as published by
...@@ -16,405 +16,314 @@ ...@@ -16,405 +16,314 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
/*jslint indent: 2, maxlen: 80, nomen: true */ /*jslint nomen: true*/
/*global define, module, require, jIO, RSVP */ /*global jIO, RSVP, Rusha*/
(function (factory) { (function (jIO, RSVP, Rusha) {
"use strict"; "use strict";
if (typeof define === 'function' && define.amd) {
return define(["jio", "rsvp"], function () {
return factory(require);
});
}
if (typeof require === 'function') {
module.exports = factory(require);
return;
}
factory(function (name) {
return {
"jio": jIO,
"rsvp": RSVP
}[name];
});
}(function (require) {
"use strict";
var Promise = require('rsvp').Promise,
all = require('rsvp').all,
addStorageFunction = require('jio').addStorage,
uniqueJSONStringify = require('jio').util.uniqueJSONStringify;
function success(promise) {
return promise.then(null, function (reason) { return reason; });
}
/**
* firstFulfilled(promises): promises< last_fulfilment_value >
*
* Responds with the first resolved promise answer recieved. If all promises
* are rejected, it returns the latest rejected promise answer
* received. Promises are cancelled only by calling
* `firstFulfilled(promises).cancel()`.
*
* @param {Array} promises An array of promises
* @return {Promise} A new promise
*/
function firstFulfilled(promises) {
var length = promises.length;
function onCancel() { var rusha = new Rusha();
var i, l, promise;
for (i = 0, l = promises.length; i < l; i += 1) {
promise = promises[i];
if (typeof promise.cancel === "function") {
promise.cancel();
}
}
}
return new Promise(function (resolve, reject, notify) { /****************************************************
var i, count = 0; Use a local jIO to read/write/search documents
function resolver(answer) { Synchronize in background those document with a remote jIO.
resolve(answer); Synchronization status is stored for each document as an local attachment.
onCancel(); ****************************************************/
}
function rejecter(answer) { function generateHash(content) {
count += 1; // XXX Improve performance by moving calculation to WebWorker
if (count === length) { return rusha.digestFromString(content);
return reject(answer);
} }
}
for (i = 0; i < length; i += 1) {
promises[i].then(resolver, rejecter, notify);
}
}, onCancel);
}
// //////////////////////////////////////////////////////////////////////
// /**
// * An Universal Unique ID generator
// *
// * @return {String} The new UUID.
// */
// function generateUuid() {
// function S4() {
// return ('0000' + Math.floor(
// Math.random() * 0x10000 /* 65536 */
// ).toString(16)).slice(-4);
// }
// return S4() + S4() + "-" +
// S4() + "-" +
// S4() + "-" +
// S4() + "-" +
// S4() + S4() + S4();
// }
function ReplicateStorage(spec) { function ReplicateStorage(spec) {
if (!Array.isArray(spec.storage_list)) { this._local_sub_storage = jIO.createJIO(spec.local_sub_storage);
throw new TypeError("ReplicateStorage(): " + this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage);
"storage_list is not of type array");
} this._signature_hash = "_replicate_" + generateHash(
this._storage_list = spec.storage_list; JSON.stringify(spec.local_sub_storage) +
} JSON.stringify(spec.remote_sub_storage)
);
ReplicateStorage.prototype.syncGetAnswerList = function (command, this._signature_sub_storage = jIO.createJIO({
answer_list) { type: "document",
var i, l, answer, answer_modified_date, winner, winner_modified_date, document_id: this._signature_hash,
winner_str, promise_list = [], winner_index, winner_id; sub_storage: spec.local_sub_storage
/*jslint continue: true */ });
for (i = 0, l = answer_list.length; i < l; i += 1) {
answer = answer_list[i];
if (!answer || answer === 404) { continue; }
if (!winner) {
winner = answer;
winner_index = i;
winner_modified_date = new Date(answer.data.modified).getTime();
} else {
answer_modified_date = new Date(answer.data.modified).getTime();
if (isFinite(answer_modified_date) &&
answer_modified_date > winner_modified_date) {
winner = answer;
winner_index = i;
winner_modified_date = answer_modified_date;
}
}
} }
winner = winner.data;
if (!winner) { return; }
// winner_attachments = winner._attachments;
delete winner._attachments;
winner_id = winner._id;
winner_str = uniqueJSONStringify(winner);
// document synchronisation ReplicateStorage.prototype.remove = function (id) {
for (i = 0, l = answer_list.length; i < l; i += 1) { if (id === this._signature_hash) {
answer = answer_list[i]; throw new jIO.util.jIOError(this._signature_hash + " is frozen",
if (!answer) { continue; } 403);
if (i === winner_index) { continue; }
if (answer === 404) {
delete winner._id;
promise_list.push(success(
command.storage(this._storage_list[i]).post(winner)
));
winner._id = winner_id;
// delete _id AND reassign _id -> avoid modifying document before
// resolving the get method.
continue;
}
delete answer._attachments;
if (uniqueJSONStringify(answer.data) !== winner_str) {
promise_list.push(success(
command.storage(this._storage_list[i]).put(winner)
));
} }
} return this._local_sub_storage.remove.apply(this._local_sub_storage,
return all(promise_list); arguments);
// XXX .then synchronize attachments
}; };
ReplicateStorage.prototype.post = function () {
ReplicateStorage.prototype.post = function (command, metadata, option) { return this._local_sub_storage.post.apply(this._local_sub_storage,
var promise_list = [], index, length = this._storage_list.length; arguments);
// if (!isDate(metadata.modified)) {
// command.error(
// 409,
// "invalid 'modified' metadata",
// "The metadata 'modified' should be a valid date string or date object"
// );
// return;
// }
for (index = 0; index < length; index += 1) {
promise_list[index] =
command.storage(this._storage_list[index]).post(metadata, option);
}
firstFulfilled(promise_list).
then(command.success, command.error, command.notify);
}; };
ReplicateStorage.prototype.put = function (id) {
ReplicateStorage.prototype.put = function (command, metadata, option) { if (id === this._signature_hash) {
var promise_list = [], index, length = this._storage_list.length; throw new jIO.util.jIOError(this._signature_hash + " is frozen",
// if (!isDate(metadata.modified)) { 403);
// command.error(
// 409,
// "invalid 'modified' metadata",
// "The metadata 'modified' should be a valid date string or date object"
// );
// return;
// }
for (index = 0; index < length; index += 1) {
promise_list[index] =
command.storage(this._storage_list[index]).put(metadata, option);
} }
firstFulfilled(promise_list). return this._local_sub_storage.put.apply(this._local_sub_storage,
then(command.success, command.error, command.notify); arguments);
}; };
ReplicateStorage.prototype.get = function () {
ReplicateStorage.prototype.putAttachment = function (command, param, option) { return this._local_sub_storage.get.apply(this._local_sub_storage,
var promise_list = [], index, length = this._storage_list.length; arguments);
for (index = 0; index < length; index += 1) {
promise_list[index] =
command.storage(this._storage_list[index]).putAttachment(param, option);
}
firstFulfilled(promise_list).
then(command.success, command.error, command.notify);
}; };
ReplicateStorage.prototype.hasCapacity = function () {
ReplicateStorage.prototype.remove = function (command, param, option) { return this._local_sub_storage.hasCapacity.apply(this._local_sub_storage,
var promise_list = [], index, length = this._storage_list.length; arguments);
for (index = 0; index < length; index += 1) {
promise_list[index] =
command.storage(this._storage_list[index]).remove(param, option);
}
firstFulfilled(promise_list).
then(command.success, command.error, command.notify);
}; };
ReplicateStorage.prototype.buildQuery = function () {
ReplicateStorage.prototype.removeAttachment = function ( // XXX Remove signature document?
command, return this._local_sub_storage.buildQuery.apply(this._local_sub_storage,
param, arguments);
option
) {
var promise_list = [], index, length = this._storage_list.length;
for (index = 0; index < length; index += 1) {
promise_list[index] =
command.storage(this._storage_list[index]).
removeAttachment(param, option);
}
firstFulfilled(promise_list).
then(command.success, command.error, command.notify);
}; };
/** ReplicateStorage.prototype.repair = function () {
* Respond with the first get answer received and synchronize the document to var context = this,
* the other storages in the background. argument_list = arguments,
*/ skip_document_dict = {};
ReplicateStorage.prototype.get = function (command, param, option) {
var promise_list = [], index, length = this._storage_list.length,
answer_list = [], this_ = this;
for (index = 0; index < length; index += 1) {
promise_list[index] =
command.storage(this._storage_list[index]).get(param, option);
}
new Promise(function (resolve, reject, notify) { // Do not sync the signature document
var count = 0, error_count = 0; skip_document_dict[context._signature_hash] = null;
function resolver(index) {
return function (answer) {
count += 1;
if (count === 1) {
resolve(answer);
}
answer_list[index] = answer;
if (count + error_count === length && count > 0) {
this_.syncGetAnswerList(command, answer_list);
}
};
}
function rejecter(index) { function propagateModification(destination, doc, hash, id) {
return function (reason) { return destination.put(id, doc)
error_count += 1; .push(function () {
if (reason.status === 404) { return context._signature_sub_storage.put(id, {
answer_list[index] = 404; "hash": hash
} });
if (error_count === length) { })
reject(reason); .push(function () {
} skip_document_dict[id] = null;
if (count + error_count === length && count > 0) { });
this_.syncGetAnswerList(command, answer_list);
}
};
} }
for (index = 0; index < length; index += 1) { function checkLocalCreation(queue, source, destination, id) {
promise_list[index].then(resolver(index), rejecter(index), notify); var remote_doc;
queue
.push(function () {
return destination.get(id);
})
.push(function (doc) {
remote_doc = doc;
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// This document was never synced.
// Push it to the remote storage and store sync information
return;
} }
}, function () { throw error;
for (index = 0; index < length; index += 1) { })
promise_list[index].cancel(); .push(function () {
// This document was never synced.
// Push it to the remote storage and store sync information
return source.get(id);
})
.push(function (doc) {
var local_hash = generateHash(JSON.stringify(doc)),
remote_hash;
if (remote_doc === undefined) {
return propagateModification(destination, doc, local_hash, id);
}
remote_hash = generateHash(JSON.stringify(remote_doc));
if (local_hash === remote_hash) {
// Same document
return context._signature_sub_storage.put(id, {
"hash": local_hash
})
.push(function () {
skip_document_dict[id] = null;
});
} }
}).then(command.success, command.error, command.notify); // Already exists on destination
}; throw new jIO.util.jIOError("Conflict on '" + id + "'",
409);
ReplicateStorage.prototype.getAttachment = function (command, param, option) { });
var promise_list = [], index, length = this._storage_list.length;
for (index = 0; index < length; index += 1) {
promise_list[index] =
command.storage(this._storage_list[index]).getAttachment(param, option);
} }
firstFulfilled(promise_list).
then(command.success, command.error, command.notify);
};
ReplicateStorage.prototype.allDocs = function (command, param, option) { function checkLocalDeletion(queue, destination, id, source) {
var promise_list = [], index, length = this._storage_list.length; var status_hash;
for (index = 0; index < length; index += 1) { queue
promise_list[index] = .push(function () {
success(command.storage(this._storage_list[index]).allDocs(option)); return context._signature_sub_storage.get(id);
} })
all(promise_list).then(function (answers) { .push(function (result) {
// merge responses status_hash = result.hash;
var i, j, k, found, rows; return destination.get(id)
// browsing answers .push(function (doc) {
for (i = 0; i < answers.length; i += 1) { var remote_hash = generateHash(JSON.stringify(doc));
if (answers[i].result === "success") { if (remote_hash === status_hash) {
rows = answers[i].data.rows; return destination.remove(id)
break; .push(function () {
} return context._signature_sub_storage.remove(id);
} })
for (i += 1; i < answers.length; i += 1) { .push(function () {
if (answers[i].result === "success") { skip_document_dict[id] = null;
// browsing answer rows });
for (j = 0; j < answers[i].data.rows.length; j += 1) {
found = false;
// browsing result rows
for (k = 0; k < rows.length; k += 1) {
if (rows[k].id === answers[i].data.rows[j].id) {
found = true;
break;
}
}
if (!found) {
rows.push(answers[i].data.rows[j]);
}
}
}
} }
return {"data": {"total_rows": (rows || []).length, "rows": rows || []}}; // Modifications on remote side
}).then(command.success, command.error, command.notify); // Push them locally
/*jslint unparam: true */ return propagateModification(source, doc, remote_hash, id);
}; }, function (error) {
if ((error instanceof jIO.util.jIOError) &&
ReplicateStorage.prototype.check = function (command, param, option) { (error.status_code === 404)) {
var promise_list = [], index, length = this._storage_list.length; return context._signature_sub_storage.remove(id)
for (index = 0; index < length; index += 1) { .push(function () {
promise_list[index] = skip_document_dict[id] = null;
command.storage(this._storage_list[index]).check(param, option); });
} }
return all(promise_list). throw error;
then(function () { return; }). });
then(command.success, command.error, command.notify); });
};
ReplicateStorage.prototype.repair = function (command, param, option) {
var storage_list = this._storage_list, length = storage_list.length,
this_ = this;
if (typeof param._id !== 'string' || !param._id) {
command.error("bad_request");
return;
} }
storage_list = storage_list.map(function (description) { function checkSignatureDifference(queue, source, destination, id) {
return command.storage(description); queue
.push(function () {
return RSVP.all([
source.get(id),
context._signature_sub_storage.get(id)
]);
})
.push(function (result_list) {
var doc = result_list[0],
local_hash = generateHash(JSON.stringify(doc)),
status_hash = result_list[1].hash;
if (local_hash !== status_hash) {
// Local modifications
return destination.get(id)
.push(function (remote_doc) {
var remote_hash = generateHash(JSON.stringify(remote_doc));
if (remote_hash !== status_hash) {
// Modifications on both sides
if (local_hash === remote_hash) {
// Same modifications on both side \o/
return context._signature_sub_storage.put(id, {
"hash": local_hash
})
.push(function () {
skip_document_dict[id] = null;
}); });
function repairSubStorages() {
var promise_list = [], i;
for (i = 0; i < length; i += 1) {
promise_list[i] = success(storage_list[i].repair(param, option));
}
return all(promise_list);
} }
throw new jIO.util.jIOError("Conflict on '" + id + "'",
function returnThe404ReasonsElseNull(reason) { 409);
if (reason.status === 404) {
return 404;
} }
return null; return propagateModification(destination, doc, local_hash, id);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// Document has been deleted remotely
return propagateModification(destination, doc, local_hash,
id);
} }
throw error;
function getSubStoragesDocument() { });
var promise_list = [], i;
for (i = 0; i < length; i += 1) {
promise_list[i] =
storage_list[i].get(param).then(null, returnThe404ReasonsElseNull);
} }
return all(promise_list); });
} }
function synchronizeDocument(answers) { function pushStorage(source, destination) {
return this_.syncGetAnswerList(command, answers); var queue = new RSVP.Queue();
return queue
.push(function () {
return RSVP.all([
source.allDocs(),
context._signature_sub_storage.allDocs()
]);
})
.push(function (result_list) {
var i,
local_dict = {},
signature_dict = {},
key;
for (i = 0; i < result_list[0].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty(
result_list[0].data.rows[i].id
)) {
local_dict[result_list[0].data.rows[i].id] = i;
}
}
for (i = 0; i < result_list[1].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty(
result_list[1].data.rows[i].id
)) {
signature_dict[result_list[1].data.rows[i].id] = i;
}
}
for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) {
if (!signature_dict.hasOwnProperty(key)) {
checkLocalCreation(queue, source, destination, key);
}
}
}
for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) {
if (local_dict.hasOwnProperty(key)) {
checkSignatureDifference(queue, source, destination, key);
} else {
checkLocalDeletion(queue, destination, key, source);
} }
function checkAnswers(answers) {
var i;
for (i = 0; i < answers.length; i += 1) {
if (answers[i].result !== "success") {
throw answers[i];
} }
} }
});
} }
return repairSubStorages(). return new RSVP.Queue()
then(getSubStoragesDocument). .push(function () {
then(synchronizeDocument). // Ensure that the document storage is usable
then(checkAnswers). return context._signature_sub_storage.__storage._sub_storage.get(
then(command.success, command.error, command.notify); context._signature_hash
);
})
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return context._signature_sub_storage.__storage._sub_storage.put(
context._signature_hash,
{}
);
}
throw error;
})
.push(function () {
return RSVP.all([
// Don't repair local_sub_storage twice
// context._signature_sub_storage.repair.apply(
// context._signature_sub_storage,
// argument_list
// ),
context._local_sub_storage.repair.apply(
context._local_sub_storage,
argument_list
),
context._remote_sub_storage.repair.apply(
context._remote_sub_storage,
argument_list
)
]);
})
.push(function () {
return pushStorage(context._local_sub_storage,
context._remote_sub_storage);
})
.push(function () {
return pushStorage(context._remote_sub_storage,
context._local_sub_storage);
});
}; };
addStorageFunction('replicate', ReplicateStorage); jIO.addStorage('replicate', ReplicateStorage);
})); }(jIO, RSVP, Rusha));
/*jslint indent: 2, maxlen: 80, nomen: true */ /*jslint nomen: true*/
/*global define, RSVP, jIO, fake_storage, module, test, stop, start, deepEqual, (function (jIO, QUnit) {
setTimeout, clearTimeout, XMLHttpRequest, window, ok */
(function (dependencies, factory) {
"use strict"; "use strict";
if (typeof define === 'function' && define.amd) { var test = QUnit.test,
return define(dependencies, factory); stop = QUnit.stop,
start = QUnit.start,
ok = QUnit.ok,
expect = QUnit.expect,
deepEqual = QUnit.deepEqual,
equal = QUnit.equal,
module = QUnit.module,
throws = QUnit.throws;
/////////////////////////////////////////////////////////////////
// Custom test substorage definition
/////////////////////////////////////////////////////////////////
function Storage200() {
return this;
}
jIO.addStorage('replicatestorage200', Storage200);
function Storage500() {
return this;
}
jIO.addStorage('replicatestorage500', Storage500);
/////////////////////////////////////////////////////////////////
// replicateStorage.constructor
/////////////////////////////////////////////////////////////////
module("replicateStorage.constructor");
test("create substorage", function () {
var jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "replicatestorage200"
},
remote_sub_storage: {
type: "replicatestorage500"
} }
factory(RSVP, jIO, fake_storage);
}([
"rsvp",
"jio",
"fakestorage",
"replicatestorage"
], function (RSVP, jIO, fake_storage) {
"use strict";
var all = RSVP.all, chain = RSVP.resolve, Promise = RSVP.Promise;
/**
* sleep(delay, [value]): promise< value >
*
* Produces a new promise which will resolve with `value` after `delay`
* milliseconds.
*
* @param {Number} delay The time to sleep.
* @param {Any} [value] The value to resolve.
* @return {Promise} A new promise.
*/
function sleep(delay, value) {
var ident;
return new Promise(function (resolve) {
ident = setTimeout(resolve, delay, value);
}, function () {
clearTimeout(ident);
}); });
}
function jsonClone(object, replacer) { ok(jio.__storage._local_sub_storage instanceof jio.constructor);
if (object === undefined) { equal(jio.__storage._local_sub_storage.__type, "replicatestorage200");
return undefined; ok(jio.__storage._remote_sub_storage instanceof jio.constructor);
} equal(jio.__storage._remote_sub_storage.__type, "replicatestorage500");
return JSON.parse(JSON.stringify(object, replacer));
}
function reverse(promise) { equal(jio.__storage._signature_hash,
return promise.then(function (a) { throw a; }, function (e) { return e; }); "_replicate_7b54b9b5183574854e5870beb19b15152a36ef4e");
}
function orderRowsById(a, b) { ok(jio.__storage._signature_sub_storage instanceof jio.constructor);
return a.id > b.id ? 1 : b.id > a.id ? -1 : 0; equal(jio.__storage._signature_sub_storage.__type, "document");
}
module("Replicate + GID + Local"); equal(jio.__storage._signature_sub_storage.__storage._document_id,
jio.__storage._signature_hash);
test("Get", function () { ok(jio.__storage._signature_sub_storage.__storage._sub_storage
var shared = {}, i, jio_list, replicate_jio; instanceof jio.constructor);
equal(jio.__storage._signature_sub_storage.__storage._sub_storage.__type,
"replicatestorage200");
// this test can work with at least 2 sub storages });
shared.gid_description = {
"type": "gid",
"constraints": {
"default": {
"identifier": "list"
}
},
"sub_storage": null
};
shared.storage_description_list = []; /////////////////////////////////////////////////////////////////
for (i = 0; i < 4; i += 1) { // replicateStorage.get
shared.storage_description_list[i] = jsonClone(shared.gid_description); /////////////////////////////////////////////////////////////////
shared.storage_description_list[i].sub_storage = { module("replicateStorage.get");
"type": "local", test("get called substorage get", function () {
"username": "replicate scenario test for get method - " + (i + 1), stop();
"mode": "memory" expect(2);
};
}
shared.replicate_storage_description = { var jio = jIO.createJIO({
"type": "replicate", type: "replicate",
"storage_list": shared.storage_description_list local_sub_storage: {
}; type: "replicatestorage200"
},
remote_sub_storage: {
type: "replicatestorage500"
}
});
shared.workspace = {}; Storage200.prototype.get = function (id) {
shared.jio_option = { equal(id, "bar", "get 200 called");
"workspace": shared.workspace, return {title: "foo"};
"max_retry": 0
}; };
jio_list = shared.storage_description_list.map(function (description) { jio.get("bar")
return jIO.createJIO(description, shared.jio_option); .then(function (result) {
deepEqual(result, {
"title": "foo"
}, "Check document");
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
}); });
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
/////////////////////////////////////////////////////////////////
// replicateStorage.post
/////////////////////////////////////////////////////////////////
module("replicateStorage.post");
test("post called substorage post", function () {
stop(); stop();
expect(2);
shared.modified_date_list = [ var jio = jIO.createJIO({
new Date("1995"), type: "replicate",
new Date("2000"), local_sub_storage: {
null, type: "replicatestorage200"
new Date("Invalid Date") },
]; remote_sub_storage: {
shared.winner_modified_date = shared.modified_date_list[1]; type: "replicatestorage500"
function setFakeStorage() {
setFakeStorage.original = shared.storage_description_list[0].sub_storage;
shared.storage_description_list[0].sub_storage = {
"type": "fake",
"id": "replicate scenario test for get method - 1"
};
jio_list[0] = jIO.createJIO(
shared.storage_description_list[0],
shared.jio_option
);
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
} }
});
function unsetFakeStorage() { Storage200.prototype.post = function (param) {
shared.storage_description_list[0].sub_storage = setFakeStorage.original; deepEqual(param, {title: "bar"}, "post 200 called");
jio_list[0] = jIO.createJIO( return "foo";
shared.storage_description_list[0], };
shared.jio_option
);
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
}
function putSimilarDocuments() { jio.post({title: "bar"})
return all(jio_list.map(function (jio) { .then(function (result) {
return jio.post({ equal(result, "foo", "Check id");
"identifier": "a", })
"modified": shared.modified_date_list[0] .fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
}); });
}));
}
function getDocumentNothingToSynchronize() { /////////////////////////////////////////////////////////////////
return replicate_jio.get({"_id": "{\"identifier\":[\"a\"]}"}); // replicateStorage.hasCapacity
/////////////////////////////////////////////////////////////////
module("replicateStorage.hasCapacity");
test("hasCapacity return substorage value", function () {
var jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "replicatestorage200"
},
remote_sub_storage: {
type: "replicatestorage500"
} }
});
function getDocumentNothingToSynchronizeTest(answer) { delete Storage200.prototype.hasCapacity;
deepEqual(answer, {
"data": { throws(
"_id": "{\"identifier\":[\"a\"]}", function () {
"identifier": "a", jio.hasCapacity("foo");
"modified": shared.modified_date_list[0].toJSON()
},
"id": "{\"identifier\":[\"a\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Get document, nothing to synchronize.");
// check storage state
return sleep(1000).
// possible synchronization in background (should not occur)
then(function () {
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"a\"]}"});
}));
}).then(function (answers) {
answers.forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"a\"]}",
"identifier": "a",
"modified": shared.modified_date_list[0].toJSON()
}, },
"id": "{\"identifier\":[\"a\"]}", function (error) {
"method": "get", ok(error instanceof jIO.util.jIOError);
"result": "success", equal(error.status_code, 501);
"status": 200, equal(error.message,
"statusText": "Ok" "Capacity 'foo' is not implemented on 'replicatestorage200'");
}, "Check storage content"); return true;
});
});
} }
);
function putDifferentDocuments() {
return all(jio_list.map(function (jio, i) {
return jio.post({
"identifier": "b",
"modified": shared.modified_date_list[i]
}); });
}));
}
function getDocumentWithSynchronization() { /////////////////////////////////////////////////////////////////
return replicate_jio.get({"_id": "{\"identifier\":[\"b\"]}"}); // replicateStorage.buildQuery
} /////////////////////////////////////////////////////////////////
module("replicateStorage.buildQuery");
function getDocumentWithSynchronizationTest(answer) { test("buildQuery return substorage buildQuery", function () {
if (answer && answer.data) { stop();
ok(shared.modified_date_list.map(function (v) { expect(2);
return (v && v.toJSON()) || undefined;
}).indexOf(answer.data.modified) !== -1, "Should be a known date"); var jio = jIO.createJIO({
delete answer.data.modified; type: "replicate",
} local_sub_storage: {
deepEqual(answer, { type: "replicatestorage200"
"data": {
"_id": "{\"identifier\":[\"b\"]}",
"identifier": "b"
},
"id": "{\"identifier\":[\"b\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Get document, pending synchronization.");
// check storage state
return sleep(1000).
// synchronizing in background
then(function () {
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"b\"]}"});
}));
}).then(function (answers) {
answers.forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"b\"]}",
"identifier": "b",
"modified": shared.winner_modified_date.toJSON()
}, },
"id": "{\"identifier\":[\"b\"]}", remote_sub_storage: {
"method": "get", type: "replicatestorage500"
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
});
});
} }
function putOneDocument() {
return jio_list[1].post({
"identifier": "c",
"modified": shared.modified_date_list[1]
}); });
}
function getDocumentWith404Synchronization() { Storage200.prototype.hasCapacity = function () {
return replicate_jio.get({"_id": "{\"identifier\":[\"c\"]}"}); return true;
} };
Storage200.prototype.buildQuery = function (options) {
deepEqual(options, {
include_docs: false,
sort_on: [["title", "ascending"]],
limit: [5],
select_list: ["title", "id"],
replicate: 'title: "two"'
}, "allDocs parameter");
return "bar";
};
function getDocumentWith404SynchronizationTest(answer) { jio.allDocs({
if (answer && answer.data) { include_docs: false,
ok(shared.modified_date_list.map(function (v) { sort_on: [["title", "ascending"]],
return (v && v.toJSON()) || undefined; limit: [5],
}).indexOf(answer.data.modified) !== -1, "Should be a known date"); select_list: ["title", "id"],
delete answer.data.modified; replicate: 'title: "two"'
})
.then(function (result) {
deepEqual(result, {
data: {
rows: "bar",
total_rows: 3
} }
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"c\"]}",
"identifier": "c"
},
"id": "{\"identifier\":[\"c\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Get document, synchronizing with not found document.");
// check storage state
return sleep(1000).
// synchronizing in background
then(function () {
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"c\"]}"});
}));
}).then(function (answers) {
answers.forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"c\"]}",
"identifier": "c",
"modified": shared.winner_modified_date.toJSON()
},
"id": "{\"identifier\":[\"c\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
}); });
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
}
function putDifferentDocuments2() {
return all(jio_list.map(function (jio, i) {
return jio.post({
"identifier": "d",
"modified": shared.modified_date_list[i]
}); });
}));
}
function getDocumentWithUnavailableStorage() { /////////////////////////////////////////////////////////////////
setFakeStorage(); // replicateStorage.put
setTimeout(function () { /////////////////////////////////////////////////////////////////
fake_storage.commands[ module("replicateStorage.put");
"replicate scenario test for get method - 1/allDocs" test("put called substorage put", function () {
].error({"status": 0}); stop();
}, 100); expect(3);
return replicate_jio.get({"_id": "{\"identifier\":[\"d\"]}"});
}
function getDocumentWithUnavailableStorageTest(answer) { var jio = jIO.createJIO({
if (answer && answer.data) { type: "replicate",
ok(shared.modified_date_list.map(function (v) { local_sub_storage: {
return (v && v.toJSON()) || undefined; type: "replicatestorage200"
}).indexOf(answer.data.modified) !== -1, "Should be a known date");
delete answer.data.modified;
}
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"d\"]}",
"identifier": "d"
},
"id": "{\"identifier\":[\"d\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Get document, synchronizing with unavailable storage.");
unsetFakeStorage();
// check storage state
return sleep(1000).
// synchronizing in background
then(function () {
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"d\"]}"});
}));
}).then(function (answers) {
deepEqual(answers[0], {
"data": {
"_id": "{\"identifier\":[\"d\"]}",
"identifier": "d",
"modified": shared.modified_date_list[0].toJSON()
},
"id": "{\"identifier\":[\"d\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
answers.slice(1).forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"d\"]}",
"identifier": "d",
"modified": shared.winner_modified_date.toJSON()
}, },
"id": "{\"identifier\":[\"d\"]}", remote_sub_storage: {
"method": "get", type: "replicatestorage500"
"result": "success", }
"status": 200, });
"statusText": "Ok" Storage200.prototype.put = function (id, param) {
}, "Check storage content"); equal(id, "bar", "put 200 called");
deepEqual(param, {"title": "foo"}, "put 200 called");
return id;
};
jio.put("bar", {"title": "foo"})
.then(function (result) {
equal(result, "bar");
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
}); });
}
function unexpectedError(error) { test("put can not modify the signature", function () {
if (error instanceof Error) { stop();
deepEqual([ expect(3);
error.name + ": " + error.message,
error
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain(). var jio = jIO.createJIO({
// get without synchronizing anything type: "replicate",
then(putSimilarDocuments). local_sub_storage: {
then(getDocumentNothingToSynchronize). type: "replicatestorage200"
then(getDocumentNothingToSynchronizeTest).
// get with synchronization
then(putDifferentDocuments).
then(getDocumentWithSynchronization).
then(getDocumentWithSynchronizationTest).
// get with 404 synchronization
then(putOneDocument).
then(getDocumentWith404Synchronization).
then(getDocumentWith404SynchronizationTest).
// XXX get with attachment synchronization
// get with unavailable storage
then(putDifferentDocuments2).
then(getDocumentWithUnavailableStorage).
then(getDocumentWithUnavailableStorageTest).
// End of scenario
then(null, unexpectedError).
then(start, start);
});
test("Post + Put", function () {
var shared = {}, i, jio_list, replicate_jio;
// this test can work with at least 2 sub storages
shared.gid_description = {
"type": "gid",
"constraints": {
"default": {
"identifier": "list"
}
}, },
"sub_storage": null remote_sub_storage: {
}; type: "replicatestorage500"
shared.storage_description_list = [];
for (i = 0; i < 4; i += 1) {
shared.storage_description_list[i] = jsonClone(shared.gid_description);
shared.storage_description_list[i].sub_storage = {
"type": "local",
"username": "replicate scenario test for post method - " + (i + 1),
"mode": "memory"
};
} }
});
delete Storage200.prototype.put;
shared.replicate_storage_description = { jio.put(jio.__storage._signature_hash, {"title": "foo"})
"type": "replicate", .then(function () {
"storage_list": shared.storage_description_list ok(false);
}; })
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
equal(error.message, jio.__storage._signature_hash + " is frozen");
equal(error.status_code, 403);
})
.always(function () {
start();
});
});
/////////////////////////////////////////////////////////////////
// replicateStorage.remove
/////////////////////////////////////////////////////////////////
module("replicateStorage.remove");
test("remove called substorage remove", function () {
stop();
expect(2);
shared.workspace = {}; var jio = jIO.createJIO({
shared.jio_option = { type: "replicate",
"workspace": shared.workspace, local_sub_storage: {
"max_retry": 0 type: "replicatestorage200"
},
remote_sub_storage: {
type: "replicatestorage500"
}
});
Storage200.prototype.remove = function (id) {
equal(id, "bar", "remove 200 called");
return id;
}; };
jio_list = shared.storage_description_list.map(function (description) { jio.remove("bar", {"title": "foo"})
return jIO.createJIO(description, shared.jio_option); .then(function (result) {
equal(result, "bar");
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
}); });
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
test("remove can not modify the signature", function () {
stop(); stop();
expect(3);
function setFakeStorage() { var jio = jIO.createJIO({
setFakeStorage.original = shared.storage_description_list[0].sub_storage; type: "replicate",
shared.storage_description_list[0].sub_storage = { local_sub_storage: {
"type": "fake", type: "replicatestorage200"
"id": "replicate scenario test for post method - 1" },
}; remote_sub_storage: {
jio_list[0] = jIO.createJIO( type: "replicatestorage500"
shared.storage_description_list[0],
shared.jio_option
);
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
} }
});
delete Storage200.prototype.remove;
function unsetFakeStorage() { jio.remove(jio.__storage._signature_hash)
shared.storage_description_list[0].sub_storage = setFakeStorage.original; .then(function () {
jio_list[0] = jIO.createJIO( ok(false);
shared.storage_description_list[0], })
shared.jio_option .fail(function (error) {
); ok(error instanceof jIO.util.jIOError);
replicate_jio = jIO.createJIO( equal(error.message, jio.__storage._signature_hash + " is frozen");
shared.replicate_storage_description, equal(error.status_code, 403);
shared.jio_option })
); .always(function () {
start();
});
});
/////////////////////////////////////////////////////////////////
// replicateStorage.repair use cases
/////////////////////////////////////////////////////////////////
module("replicateStorage.repair", {
setup: function () {
// Uses memory substorage, so that it is flushed after each run
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
}
},
remote_sub_storage: {
type: "uuid",
sub_storage: {
type: "memory"
} }
}
});
function createDocument() {
return replicate_jio.post({"identifier": "a"});
} }
});
function createDocumentTest(answer) { test("local document creation", function () {
deepEqual(answer, { stop();
"id": "{\"identifier\":[\"a\"]}", expect(2);
"method": "post",
"result": "success",
"status": 201,
"statusText": "Created"
}, "Post document");
return sleep(100); var id,
} context = this;
function checkStorageContent() { context.jio.post({"title": "foo"})
// check storage state .then(function (result) {
return all(jio_list.map(function (jio) { id = result;
return jio.get({"_id": "{\"identifier\":[\"a\"]}"}); return context.jio.repair();
})).then(function (answers) { })
answers.forEach(function (answer) { .then(function () {
deepEqual(answer, { return context.jio.__storage._remote_sub_storage.get(id);
"data": { })
"_id": "{\"identifier\":[\"a\"]}", .then(function (result) {
"identifier": "a" deepEqual(result, {
}, title: "foo"
"id": "{\"identifier\":[\"a\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
}); });
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "5ea9013447539ad65de308cbd75b5826a2ae30e5"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
}
function updateDocument() {
return replicate_jio.put({
"_id": "{\"identifier\":[\"a\"]}",
"identifier": "a",
"title": "b"
}); });
}
function updateDocumentTest(answer) { test("remote document creation", function () {
deepEqual(answer, { stop();
"id": "{\"identifier\":[\"a\"]}", expect(2);
"method": "put",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Update document");
return sleep(100); var id,
} context = this;
function checkStorageContent3() { context.jio.__storage._remote_sub_storage.post({"title": "bar"})
// check storage state .then(function (result) {
return all(jio_list.map(function (jio) { id = result;
return jio.get({"_id": "{\"identifier\":[\"a\"]}"}); return context.jio.repair();
})).then(function (answers) { })
answers.forEach(function (answer) { .then(function () {
deepEqual(answer, { return context.jio.get(id);
"data": { })
"_id": "{\"identifier\":[\"a\"]}", .then(function (result) {
"identifier": "a", deepEqual(result, {
"title": "b" title: "bar"
}, });
"id": "{\"identifier\":[\"a\"]}", })
"method": "get", .then(function () {
"result": "success", return context.jio.__storage._signature_sub_storage.get(id);
"status": 200, })
"statusText": "Ok" .then(function (result) {
}, "Check storage content"); deepEqual(result, {
hash: "6799f3ea80e325b89f19589282a343c376c1f1af"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
}); });
}
function createDocumentWithUnavailableStorage() { test("local and remote document creations", function () {
setFakeStorage(); stop();
setTimeout(function () { expect(5);
fake_storage.commands[
"replicate scenario test for post method - 1/allDocs"
].error({"status": 0});
}, 100);
return replicate_jio.post({"identifier": "b"});
}
function createDocumentWithUnavailableStorageTest(answer) { var context = this;
deepEqual(answer, {
"id": "{\"identifier\":[\"b\"]}",
"method": "post",
"result": "success",
"status": 201,
"statusText": "Created"
}, "Post document with unavailable storage");
return sleep(100); RSVP.all([
} context.jio.put("conflict", {"title": "foo"}),
context.jio.__storage._remote_sub_storage.put("conflict",
{"title": "bar"})
])
.then(function () {
return context.jio.repair();
})
.then(function () {
ok(false);
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
equal(error.message, "Conflict on 'conflict'");
equal(error.status_code, 409);
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get("conflict");
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
// equal(error.message, "Cannot find document: conflict");
equal(error.status_code, 404);
})
.always(function () {
start();
});
});
function checkStorageContent2() { test("local and remote same document creations", function () {
unsetFakeStorage(); stop();
// check storage state expect(1);
return all(jio_list.map(function (jio, i) {
if (i === 0) { var context = this;
return reverse(jio.get({"_id": "{\"identifier\":[\"b\"]}"}));
} RSVP.all([
return jio.get({"_id": "{\"identifier\":[\"b\"]}"}); context.jio.put("conflict", {"title": "foo"}),
})).then(function (answers) { context.jio.__storage._remote_sub_storage.put("conflict",
deepEqual(answers[0], { {"title": "foo"})
"error": "not_found", ])
"id": "{\"identifier\":[\"b\"]}", .then(function () {
"message": "Cannot get document", return context.jio.repair();
"method": "get", })
"reason": "missing", .then(function () {
"result": "error", return context.jio.__storage._signature_sub_storage.get("conflict");
"status": 404, })
"statusText": "Not Found" .then(function (result) {
}, "Check storage content"); deepEqual(result, {
answers.slice(1).forEach(function (answer) { hash: "5ea9013447539ad65de308cbd75b5826a2ae30e5"
deepEqual(answer, { });
"data": { })
"_id": "{\"identifier\":[\"b\"]}", .fail(function (error) {
"identifier": "b" ok(false, error);
}, })
"id": "{\"identifier\":[\"b\"]}", .always(function () {
"method": "get", start();
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
}); });
}); });
}
function unexpectedError(error) { test("no modification", function () {
if (error instanceof Error) { stop();
deepEqual([ expect(2);
error.name + ": " + error.message,
error
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain(). var id,
// create a document context = this;
then(createDocument).
then(createDocumentTest).
then(checkStorageContent).
// update document
then(updateDocument).
then(updateDocumentTest).
then(checkStorageContent3).
// create a document with unavailable storage
then(createDocumentWithUnavailableStorage).
then(createDocumentWithUnavailableStorageTest).
then(checkStorageContent2).
// End of scenario
then(null, unexpectedError).
then(start, start);
});
test("Remove", function () {
var shared = {}, i, jio_list, replicate_jio;
// this test can work with at least 2 sub storages
shared.gid_description = {
"type": "gid",
"constraints": {
"default": {
"identifier": "list"
}
},
"sub_storage": null
};
shared.storage_description_list = []; context.jio.post({"title": "foo"})
for (i = 0; i < 4; i += 1) { .then(function (result) {
shared.storage_description_list[i] = jsonClone(shared.gid_description); id = result;
shared.storage_description_list[i].sub_storage = { return context.jio.repair();
"type": "local", })
"username": "replicate scenario test for remove method - " + (i + 1), .then(function () {
"mode": "memory" return context.jio.repair();
}; })
} .then(function () {
return context.jio.__storage._remote_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo"
});
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "5ea9013447539ad65de308cbd75b5826a2ae30e5"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
shared.replicate_storage_description = { test("local document modification", function () {
"type": "replicate", stop();
"storage_list": shared.storage_description_list expect(2);
};
shared.workspace = {}; var id,
shared.jio_option = { context = this;
"workspace": shared.workspace,
"max_retry": 0
};
jio_list = shared.storage_description_list.map(function (description) { context.jio.post({"title": "foo"})
return jIO.createJIO(description, shared.jio_option); .then(function (result) {
id = result;
return context.jio.repair();
})
.then(function () {
return context.jio.put(id, {"title": "foo2"});
})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo2"
});
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "9819187e39531fdc9bcfd40dbc6a7d3c78fe8dab"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
}); });
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
test("remote document modification", function () {
stop(); stop();
expect(2);
function setFakeStorage() { var id,
setFakeStorage.original = shared.storage_description_list[0].sub_storage; context = this;
shared.storage_description_list[0].sub_storage = {
"type": "fake",
"id": "replicate scenario test for remove method - 1"
};
jio_list[0] = jIO.createJIO(
shared.storage_description_list[0],
shared.jio_option
);
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
}
function unsetFakeStorage() { context.jio.post({"title": "foo"})
shared.storage_description_list[0].sub_storage = setFakeStorage.original; .then(function (result) {
jio_list[0] = jIO.createJIO( id = result;
shared.storage_description_list[0], return context.jio.repair();
shared.jio_option })
); .then(function () {
replicate_jio = jIO.createJIO( return context.jio.__storage._remote_sub_storage.put(
shared.replicate_storage_description, id,
shared.jio_option {"title": "foo3"}
); );
} })
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo3"
});
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "4b1dde0f80ac38514771a9d25b5278e38f560e0f"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
function putSomeDocuments() { test("local and remote document modifications", function () {
return all(jio_list.map(function (jio) { stop();
return jio.post({"identifier": "a"}); expect(4);
}));
}
function removeDocument() { var id,
return replicate_jio.remove({"_id": "{\"identifier\":[\"a\"]}"}); context = this;
}
context.jio.post({"title": "foo"})
.then(function (result) {
id = result;
return context.jio.repair();
})
.then(function () {
return RSVP.all([
context.jio.put(id, {"title": "foo4"}),
context.jio.__storage._remote_sub_storage.put(id, {"title": "foo5"})
]);
})
.then(function () {
return context.jio.repair();
})
.then(function () {
ok(false);
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
equal(error.message, "Conflict on '" + id + "'");
equal(error.status_code, 409);
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "5ea9013447539ad65de308cbd75b5826a2ae30e5"
});
})
.always(function () {
start();
});
});
function removeDocumentTest(answer) { test("local and remote document same modifications", function () {
deepEqual(answer, { stop();
"id": "{\"identifier\":[\"a\"]}", expect(1);
"method": "remove",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Remove document");
return sleep(100); var id,
} context = this;
function checkStorageContent() { context.jio.post({"title": "foo"})
// check storage state .then(function (result) {
return all(jio_list.map(function (jio) { id = result;
return reverse(jio.get({"_id": "{\"identifier\":[\"a\"]}"})); return context.jio.repair();
})).then(function (answers) { })
answers.forEach(function (answer) { .then(function () {
deepEqual(answer, { return RSVP.all([
"error": "not_found", context.jio.put(id, {"title": "foo99"}),
"id": "{\"identifier\":[\"a\"]}", context.jio.__storage._remote_sub_storage.put(id, {"title": "foo99"})
"message": "Cannot get document", ]);
"method": "get", })
"reason": "missing", .then(function () {
"result": "error", return context.jio.repair();
"status": 404, })
"statusText": "Not Found" .then(function () {
}, "Check storage content"); return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "8ed3a474128b6e0c0c7d3dd51b1a06ebfbf6722f"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
}); });
}
function putSomeDocuments2() { test("local document deletion", function () {
return all(jio_list.map(function (jio) { stop();
return jio.post({"identifier": "b"}); expect(6);
}));
}
function removeDocumentWithUnavailableStorage() { var id,
setFakeStorage(); context = this;
setTimeout(function () {
fake_storage.commands[
"replicate scenario test for remove method - 1/allDocs"
].error({"status": 0});
}, 100);
return replicate_jio.remove({"_id": "{\"identifier\":[\"b\"]}"});
}
function removeDocumentWithUnavailableStorageTest(answer) { context.jio.post({"title": "foo"})
deepEqual(answer, { .then(function (result) {
"id": "{\"identifier\":[\"b\"]}", id = result;
"method": "remove", return context.jio.repair();
"result": "success", })
"status": 204, .then(function () {
"statusText": "No Content" return context.jio.remove(id);
}, "Remove document with unavailable storage"); })
.then(function () {
return context.jio.repair();
})
.then(function () {
ok(true, "Removal correctly synced");
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(id);
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
equal(error.message, "Cannot find document: " + id);
equal(error.status_code, 404);
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id)
.then(function () {
ok(false, "Signature should be deleted");
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
// equal(error.message, "Cannot find document: " + id);
equal(error.status_code, 404);
});
})
.always(function () {
start();
});
});
return sleep(100); test("remote document deletion", function () {
} stop();
expect(6);
function checkStorageContent2() { var id,
unsetFakeStorage(); context = this;
// check storage state
return all(jio_list.map(function (jio, i) { context.jio.post({"title": "foo"})
if (i === 0) { .then(function (result) {
return jio.get({"_id": "{\"identifier\":[\"b\"]}"}); id = result;
} return context.jio.repair();
return reverse(jio.get({"_id": "{\"identifier\":[\"b\"]}"})); })
})).then(function (answers) { .then(function () {
deepEqual(answers[0], { return context.jio.__storage._remote_sub_storage.remove(id);
"data": { })
"_id": "{\"identifier\":[\"b\"]}", .then(function () {
"identifier": "b" return context.jio.repair();
}, })
"id": "{\"identifier\":[\"b\"]}", .then(function () {
"method": "get", ok(true, "Removal correctly synced");
"result": "success", })
"status": 200, .then(function () {
"statusText": "Ok" return context.jio.get(id);
}, "Check storage content"); })
answers.slice(1).forEach(function (answer) { .fail(function (error) {
deepEqual(answer, { ok(error instanceof jIO.util.jIOError);
"error": "not_found", equal(error.message, "Cannot find document: " + id);
"id": "{\"identifier\":[\"b\"]}", equal(error.status_code, 404);
"message": "Cannot get document", })
"method": "get", .then(function () {
"reason": "missing", return context.jio.__storage._signature_sub_storage.get(id)
"result": "error", .then(function () {
"status": 404, ok(false, "Signature should be deleted");
"statusText": "Not Found" })
}, "Check storage content"); .fail(function (error) {
ok(error instanceof jIO.util.jIOError);
// equal(error.message, "Cannot find document: " + id);
equal(error.status_code, 404);
});
})
.always(function () {
start();
}); });
}); });
}
function unexpectedError(error) { test("local and remote document deletions", function () {
if (error instanceof Error) { stop();
deepEqual([ expect(8);
error.name + ": " + error.message,
error
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain(). var id,
// remove document context = this;
then(putSomeDocuments).
then(removeDocument).
then(removeDocumentTest).
then(checkStorageContent).
// remove document with unavailable storage
then(putSomeDocuments2).
then(removeDocumentWithUnavailableStorage).
then(removeDocumentWithUnavailableStorageTest).
then(checkStorageContent2).
// End of scenario
then(null, unexpectedError).
then(start, start);
});
test("AllDocs", function () {
var shared = {}, i, jio_list, replicate_jio;
// this test can work with at least 2 sub storages
shared.gid_description = {
"type": "gid",
"constraints": {
"default": {
"identifier": "list"
}
},
"sub_storage": null
};
shared.storage_description_list = []; context.jio.post({"title": "foo"})
for (i = 0; i < 2; i += 1) { .then(function (result) {
shared.storage_description_list[i] = jsonClone(shared.gid_description); id = result;
shared.storage_description_list[i].sub_storage = { return context.jio.repair();
"type": "local", })
"username": "replicate scenario test for allDocs method - " + (i + 1), .then(function () {
"mode": "memory" return RSVP.all([
}; context.jio.remove(id),
} context.jio.__storage._remote_sub_storage.remove(id)
]);
})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.get(id)
.then(function () {
ok(false, "Document should be locally deleted");
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
equal(error.message, "Cannot find document: " + id);
equal(error.status_code, 404);
});
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(id)
.then(function () {
ok(false, "Document should be remotely deleted");
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
equal(error.message, "Cannot find document: " + id);
equal(error.status_code, 404);
});
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id)
.then(function () {
ok(false, "Signature should be deleted");
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
// equal(error.message, "Cannot find document: " + id);
equal(error.status_code, 404);
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
shared.replicate_storage_description = { test("local deletion and remote modifications", function () {
"type": "replicate", stop();
"storage_list": shared.storage_description_list expect(2);
};
shared.workspace = {}; var id,
shared.jio_option = { context = this;
"workspace": shared.workspace,
"max_retry": 0
};
jio_list = shared.storage_description_list.map(function (description) { context.jio.post({"title": "foo"})
return jIO.createJIO(description, shared.jio_option); .then(function (result) {
id = result;
return context.jio.repair();
})
.then(function () {
return RSVP.all([
context.jio.remove(id),
context.jio.__storage._remote_sub_storage.put(id, {"title": "foo99"})
]);
})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo99"
});
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "8ed3a474128b6e0c0c7d3dd51b1a06ebfbf6722f"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
}); });
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
test("local modifications and remote deletion", function () {
stop(); stop();
expect(2);
var id,
context = this;
shared.modified_date_list = [ context.jio.post({"title": "foo"})
new Date("2000"), .then(function (result) {
new Date("1995"), id = result;
null, return context.jio.repair();
new Date("Invalid Date")
];
function postSomeDocuments() {
return all([
jio_list[0].post({
"identifier": "a",
"modified": shared.modified_date_list[0]
}),
jio_list[0].post({
"identifier": "b",
"modified": shared.modified_date_list[1]
}),
jio_list[1].post({
"identifier": "b",
"modified": shared.modified_date_list[0]
}) })
.then(function () {
return RSVP.all([
context.jio.put(id, {"title": "foo99"}),
context.jio.__storage._remote_sub_storage.remove(id)
]); ]);
} })
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
title: "foo99"
});
})
.then(function () {
return context.jio.__storage._signature_sub_storage.get(id);
})
.then(function (result) {
deepEqual(result, {
hash: "8ed3a474128b6e0c0c7d3dd51b1a06ebfbf6722f"
});
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
function listDocuments() { test("signature document is not synced", function () {
return replicate_jio.allDocs({"include_docs": true}); stop();
} expect(6);
function listDocumentsTest(answer) { var context = this;
answer.data.rows.sort(orderRowsById);
deepEqual(answer, { // Uses sessionstorage substorage, so that signature are stored
"data": { // in the same local sub storage
"total_rows": 2, this.jio = jIO.createJIO({
"rows": [ type: "replicate",
{ local_sub_storage: {
"id": "{\"identifier\":[\"a\"]}", type: "uuid",
"doc": { sub_storage: {
"_id": "{\"identifier\":[\"a\"]}", type: "document",
"identifier": "a", document_id: "/",
"modified": shared.modified_date_list[0].toJSON() sub_storage: {
}, type: "local",
"value": {} sessiononly: true
}, }
{
"id": "{\"identifier\":[\"b\"]}",
"doc": {
"_id": "{\"identifier\":[\"b\"]}",
"identifier": "b",
"modified": shared.modified_date_list[1].toJSON()
// there's no winner detection here
},
"value": {}
} }
]
}, },
"method": "allDocs", remote_sub_storage: {
"result": "success", type: "memory"
"status": 200,
"statusText": "Ok"
}, "Document list should be merged correctly");
} }
});
function setFakeStorage() { context.jio.post({"title": "foo"})
shared.storage_description_list[0].sub_storage = { .then(function () {
"type": "fake", return context.jio.repair();
"id": "replicate scenario test for allDocs method - 1" })
}; .then(function () {
jio_list[0] = jIO.createJIO( return context.jio.__storage._remote_sub_storage.get(
shared.storage_description_list[0], context.jio.__storage._signature_hash
shared.jio_option
); );
replicate_jio = jIO.createJIO( })
shared.replicate_storage_description, .fail(function (error) {
shared.jio_option ok(error instanceof jIO.util.jIOError);
equal(error.message, "Cannot find document: " +
"_replicate_2be6c0851d60bcd9afe829e7133a136d266c779c");
equal(error.status_code, 404);
})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(
context.jio.__storage._signature_hash
); );
} })
.fail(function (error) {
function listDocumentsWithUnavailableStorage() { ok(error instanceof jIO.util.jIOError);
setTimeout(function () { equal(error.message, "Cannot find document: " +
fake_storage.commands[ "_replicate_2be6c0851d60bcd9afe829e7133a136d266c779c");
"replicate scenario test for allDocs method - 1/allDocs" equal(error.status_code, 404);
].error({"status": 0}); })
}, 100); .fail(function (error) {
return replicate_jio.allDocs({"include_docs": true}); ok(false, error);
} })
.always(function () {
start();
});
});
function listDocumentsWithUnavailableStorageTest(answer) { test("substorages are repaired too", function () {
deepEqual(answer, { stop();
"data": { expect(9);
"total_rows": 1,
"rows": [
{
"id": "{\"identifier\":[\"b\"]}",
"doc": {
"_id": "{\"identifier\":[\"b\"]}",
"identifier": "b",
"modified": shared.modified_date_list[0].toJSON()
},
"value": {}
}
]
},
"method": "allDocs",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Document list with only one available storage");
}
function unexpectedError(error) { var context = this,
if (error instanceof Error) { first_call = true,
deepEqual([ options = {foo: "bar"};
error.name + ": " + error.message,
error
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain(). function Storage200CheckRepair() {
// list documents return this;
then(postSomeDocuments).
then(listDocuments).
then(listDocumentsTest).
// set fake storage
then(setFakeStorage).
// list documents with unavailable storage
then(listDocumentsWithUnavailableStorage).
then(listDocumentsWithUnavailableStorageTest).
// End of scenario
then(null, unexpectedError).
then(start);
});
test("Repair", function () {
var shared = {}, i, jio_list, replicate_jio;
// this test can work with at least 2 sub storages
shared.gid_description = {
"type": "gid",
"constraints": {
"default": {
"identifier": "list"
} }
}, Storage200CheckRepair.prototype.get = function () {
"sub_storage": null ok(true, "get 200 check repair called");
}; return {};
shared.storage_description_list = [];
for (i = 0; i < 4; i += 1) {
shared.storage_description_list[i] = jsonClone(shared.gid_description);
shared.storage_description_list[i].sub_storage = {
"type": "local",
"username": "replicate scenario test for repair method - " + (i + 1),
"mode": "memory"
}; };
} Storage200CheckRepair.prototype.hasCapacity = function () {
return true;
shared.replicate_storage_description = {
"type": "replicate",
"storage_list": shared.storage_description_list
}; };
Storage200CheckRepair.prototype.buildQuery = function () {
shared.workspace = {}; ok(true, "buildQuery 200 check repair called");
shared.jio_option = { return [];
"workspace": shared.workspace,
"max_retry": 0
}; };
Storage200CheckRepair.prototype.allAttachments = function () {
jio_list = shared.storage_description_list.map(function (description) { ok(true, "allAttachments 200 check repair called");
return jIO.createJIO(description, shared.jio_option); return {};
});
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
stop();
shared.modified_date_list = [
new Date("1995"),
new Date("2000"),
null,
new Date("Invalid Date")
];
shared.winner_modified_date = shared.modified_date_list[1];
function setFakeStorage() {
setFakeStorage.original = shared.storage_description_list[0].sub_storage;
shared.storage_description_list[0].sub_storage = {
"type": "fake",
"id": "replicate scenario test for repair method - 1"
}; };
jio_list[0] = jIO.createJIO( Storage200CheckRepair.prototype.repair = function (kw) {
shared.storage_description_list[0], if (first_call) {
shared.jio_option deepEqual(
this,
context.jio.__storage._local_sub_storage.__storage,
"local substorage repair"
); );
replicate_jio = jIO.createJIO( first_call = false;
shared.replicate_storage_description, } else {
shared.jio_option deepEqual(
this,
context.jio.__storage._remote_sub_storage.__storage,
"remote substorage repair"
); );
} }
deepEqual(kw, options, "substorage repair parameters provided");
};
function unsetFakeStorage() { jIO.addStorage(
shared.storage_description_list[0].sub_storage = setFakeStorage.original; 'replicatestorage200chechrepair',
jio_list[0] = jIO.createJIO( Storage200CheckRepair
shared.storage_description_list[0],
shared.jio_option
);
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
); );
}
function putSimilarDocuments() {
return all(jio_list.map(function (jio) {
return jio.post({
"identifier": "a",
"modified": shared.modified_date_list[0]
});
}));
}
function repairDocumentNothingToSynchronize() {
return replicate_jio.repair({"_id": "{\"identifier\":[\"a\"]}"});
}
function repairDocumentNothingToSynchronizeTest(answer) { this.jio = jIO.createJIO({
deepEqual(answer, { type: "replicate",
"id": "{\"identifier\":[\"a\"]}", local_sub_storage: {
"method": "repair", type: "replicatestorage200chechrepair"
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Repair document, nothing to synchronize.");
// check storage state
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"a\"]}"});
})).then(function (answers) {
answers.forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"a\"]}",
"identifier": "a",
"modified": shared.modified_date_list[0].toJSON()
}, },
"id": "{\"identifier\":[\"a\"]}", remote_sub_storage: {
"method": "get", type: "replicatestorage200chechrepair"
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
});
});
} }
function putDifferentDocuments() {
return all(jio_list.map(function (jio, i) {
return jio.post({
"identifier": "b",
"modified": shared.modified_date_list[i]
}); });
}));
}
function repairDocumentWithSynchronization() {
return replicate_jio.repair({"_id": "{\"identifier\":[\"b\"]}"});
}
function repairDocumentWithSynchronizationTest(answer) { context.jio.repair(options)
deepEqual(answer, { .fail(function (error) {
"id": "{\"identifier\":[\"b\"]}", ok(false, error);
"method": "repair", })
"result": "success", .always(function () {
"status": 204, start();
"statusText": "No Content"
}, "Repair document, synchronization should be done.");
// check storage state
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"b\"]}"});
})).then(function (answers) {
answers.forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"b\"]}",
"identifier": "b",
"modified": shared.winner_modified_date.toJSON()
},
"id": "{\"identifier\":[\"b\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
});
});
}
function putOneDocument() {
return jio_list[1].post({
"identifier": "c",
"modified": shared.modified_date_list[1]
});
}
function repairDocumentWith404Synchronization() {
return replicate_jio.repair({"_id": "{\"identifier\":[\"c\"]}"});
}
function repairDocumentWith404SynchronizationTest(answer) {
deepEqual(answer, {
"id": "{\"identifier\":[\"c\"]}",
"method": "repair",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Repair document, synchronizing with not found document.");
// check storage state
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"c\"]}"});
})).then(function (answers) {
answers.forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"c\"]}",
"identifier": "c",
"modified": shared.winner_modified_date.toJSON()
},
"id": "{\"identifier\":[\"c\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
});
});
}
function putDifferentDocuments2() {
return all(jio_list.map(function (jio, i) {
return jio.post({
"identifier": "d",
"modified": shared.modified_date_list[i]
});
}));
}
function repairDocumentWithUnavailableStorage() {
setFakeStorage();
setTimeout(function () {
fake_storage.commands[
"replicate scenario test for repair method - 1/allDocs"
].error({"status": 0});
}, 250);
setTimeout(function () {
fake_storage.commands[
"replicate scenario test for repair method - 1/allDocs"
].error({"status": 0});
}, 500);
return replicate_jio.repair({"_id": "{\"identifier\":[\"d\"]}"});
}
function repairDocumentWithUnavailableStorageTest(answer) {
deepEqual(answer, {
"id": "{\"identifier\":[\"d\"]}",
"method": "repair",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Repair document, synchronizing with unavailable storage.");
unsetFakeStorage();
// check storage state
return all(jio_list.map(function (jio) {
return jio.get({"_id": "{\"identifier\":[\"d\"]}"});
})).then(function (answers) {
deepEqual(answers[0], {
"data": {
"_id": "{\"identifier\":[\"d\"]}",
"identifier": "d",
"modified": shared.modified_date_list[0].toJSON()
},
"id": "{\"identifier\":[\"d\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
answers.slice(1).forEach(function (answer) {
deepEqual(answer, {
"data": {
"_id": "{\"identifier\":[\"d\"]}",
"identifier": "d",
"modified": shared.winner_modified_date.toJSON()
},
"id": "{\"identifier\":[\"d\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
}); });
}); });
}
function unexpectedError(error) {
if (error instanceof Error) {
deepEqual([
error.name + ": " + error.message,
error
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain(). }(jIO, QUnit));
// get without synchronizing anything
then(putSimilarDocuments).
then(repairDocumentNothingToSynchronize).
then(repairDocumentNothingToSynchronizeTest).
// repair with synchronization
then(putDifferentDocuments).
then(repairDocumentWithSynchronization).
then(repairDocumentWithSynchronizationTest).
// repair with 404 synchronization
then(putOneDocument).
then(repairDocumentWith404Synchronization).
then(repairDocumentWith404SynchronizationTest).
// XXX repair with attachment synchronization
// repair with unavailable storage
then(putDifferentDocuments2).
then(repairDocumentWithUnavailableStorage).
then(repairDocumentWithUnavailableStorageTest).
// End of scenario
then(null, unexpectedError).
then(start, start);
});
}));
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
<script src="jio.storage/erp5storage.tests.js"></script> <script src="jio.storage/erp5storage.tests.js"></script>
<script src="jio.storage/indexeddbstorage.tests.js"></script> <script src="jio.storage/indexeddbstorage.tests.js"></script>
<script src="jio.storage/uuidstorage.tests.js"></script> <script src="jio.storage/uuidstorage.tests.js"></script>
<script src="jio.storage/replicatestorage.tests.js"></script>
<!--script src="jio.storage/indexstorage.tests.js"></script--> <!--script src="jio.storage/indexstorage.tests.js"></script-->
<!--script src="jio.storage/dropboxstorage.tests.js"></script--> <!--script src="jio.storage/dropboxstorage.tests.js"></script-->
...@@ -58,11 +59,7 @@ ...@@ -58,11 +59,7 @@
<script src="../test/jio.storage/replicaterevisionstorage.tests.js"></script> <script src="../test/jio.storage/replicaterevisionstorage.tests.js"></script>
<script src="../src/jio.storage/splitstorage.js"></script> <script src="../src/jio.storage/splitstorage.js"></script>
<script src="../test/jio.storage/splitstorage.tests.js"></script> <script src="../test/jio.storage/splitstorage.tests.js"></script-->
<script src="../src/jio.storage/replicatestorage.js"></script>
<script src="../test/jio.storage/replicatestorage.tests.js"></script-->
</head> </head>
<body> <body>
<h1 id="qunit-header">jIO Tests</h1> <h1 id="qunit-header">jIO Tests</h1>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment