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) {
count += 1;
if (count === length) {
return reject(answer);
}
}
for (i = 0; i < length; i += 1) { function generateHash(content) {
promises[i].then(resolver, rejecter, notify); // XXX Improve performance by moving calculation to WebWorker
} return rusha.digestFromString(content);
}, 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)
);
this._signature_sub_storage = jIO.createJIO({
type: "document",
document_id: this._signature_hash,
sub_storage: spec.local_sub_storage
});
} }
ReplicateStorage.prototype.syncGetAnswerList = function (command, ReplicateStorage.prototype.remove = function (id) {
answer_list) { if (id === this._signature_hash) {
var i, l, answer, answer_modified_date, winner, winner_modified_date, throw new jIO.util.jIOError(this._signature_hash + " is frozen",
winner_str, promise_list = [], winner_index, winner_id; 403);
/*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
for (i = 0, l = answer_list.length; i < l; i += 1) {
answer = answer_list[i];
if (!answer) { continue; }
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 all(promise_list); return this._local_sub_storage.remove.apply(this._local_sub_storage,
// XXX .then synchronize attachments arguments);
}; };
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, // Do not sync the signature document
answer_list = [], this_ = this; skip_document_dict[context._signature_hash] = null;
for (index = 0; index < length; index += 1) {
promise_list[index] = function propagateModification(destination, doc, hash, id) {
command.storage(this._storage_list[index]).get(param, option); return destination.put(id, doc)
.push(function () {
return context._signature_sub_storage.put(id, {
"hash": hash
});
})
.push(function () {
skip_document_dict[id] = null;
});
} }
new Promise(function (resolve, reject, notify) { function checkLocalCreation(queue, source, destination, id) {
var count = 0, error_count = 0; var remote_doc;
function resolver(index) { queue
return function (answer) { .push(function () {
count += 1; return destination.get(id);
if (count === 1) { })
resolve(answer); .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;
} }
answer_list[index] = answer; throw error;
if (count + error_count === length && count > 0) { })
this_.syncGetAnswerList(command, answer_list); .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);
} }
};
}
function rejecter(index) { remote_hash = generateHash(JSON.stringify(remote_doc));
return function (reason) { if (local_hash === remote_hash) {
error_count += 1; // Same document
if (reason.status === 404) { return context._signature_sub_storage.put(id, {
answer_list[index] = 404; "hash": local_hash
})
.push(function () {
skip_document_dict[id] = null;
});
} }
if (error_count === length) { // Already exists on destination
reject(reason); throw new jIO.util.jIOError("Conflict on '" + id + "'",
} 409);
if (count + error_count === length && count > 0) { });
this_.syncGetAnswerList(command, answer_list); }
}
};
}
for (index = 0; index < length; index += 1) {
promise_list[index].then(resolver(index), rejecter(index), notify);
}
}, function () {
for (index = 0; index < length; index += 1) {
promise_list[index].cancel();
}
}).then(command.success, command.error, command.notify);
};
ReplicateStorage.prototype.getAttachment = 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 () {
command.storage(this._storage_list[index]).getAttachment(param, option); return context._signature_sub_storage.get(id);
})
.push(function (result) {
status_hash = result.hash;
return destination.get(id)
.push(function (doc) {
var remote_hash = generateHash(JSON.stringify(doc));
if (remote_hash === status_hash) {
return destination.remove(id)
.push(function () {
return context._signature_sub_storage.remove(id);
})
.push(function () {
skip_document_dict[id] = null;
});
}
// Modifications on remote side
// Push them locally
return propagateModification(source, doc, remote_hash, id);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return context._signature_sub_storage.remove(id)
.push(function () {
skip_document_dict[id] = null;
});
}
throw error;
});
});
} }
firstFulfilled(promise_list).
then(command.success, command.error, command.notify);
};
ReplicateStorage.prototype.allDocs = function (command, param, option) { function checkSignatureDifference(queue, source, destination, id) {
var promise_list = [], index, length = this._storage_list.length; queue
for (index = 0; index < length; index += 1) { .push(function () {
promise_list[index] = return RSVP.all([
success(command.storage(this._storage_list[index]).allDocs(option)); 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;
});
}
throw new jIO.util.jIOError("Conflict on '" + id + "'",
409);
}
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;
});
}
});
} }
all(promise_list).then(function (answers) {
// merge responses function pushStorage(source, destination) {
var i, j, k, found, rows; var queue = new RSVP.Queue();
// browsing answers return queue
for (i = 0; i < answers.length; i += 1) { .push(function () {
if (answers[i].result === "success") { return RSVP.all([
rows = answers[i].data.rows; source.allDocs(),
break; context._signature_sub_storage.allDocs()
} ]);
} })
for (i += 1; i < answers.length; i += 1) { .push(function (result_list) {
if (answers[i].result === "success") { var i,
// browsing answer rows local_dict = {},
for (j = 0; j < answers[i].data.rows.length; j += 1) { signature_dict = {},
found = false; key;
// browsing result rows for (i = 0; i < result_list[0].data.total_rows; i += 1) {
for (k = 0; k < rows.length; k += 1) { if (!skip_document_dict.hasOwnProperty(
if (rows[k].id === answers[i].data.rows[j].id) { result_list[0].data.rows[i].id
found = true; )) {
break; 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);
} }
} }
if (!found) { }
rows.push(answers[i].data.rows[j]); 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);
}
} }
} }
} });
}
return {"data": {"total_rows": (rows || []).length, "rows": rows || []}};
}).then(command.success, command.error, command.notify);
/*jslint unparam: true */
};
ReplicateStorage.prototype.check = 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]).check(param, option);
}
return all(promise_list).
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) {
return command.storage(description);
});
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);
} }
function returnThe404ReasonsElseNull(reason) { return new RSVP.Queue()
if (reason.status === 404) { .push(function () {
return 404; // Ensure that the document storage is usable
} return context._signature_sub_storage.__storage._sub_storage.get(
return null; context._signature_hash
} );
})
function getSubStoragesDocument() { .push(undefined, function (error) {
var promise_list = [], i; if ((error instanceof jIO.util.jIOError) &&
for (i = 0; i < length; i += 1) { (error.status_code === 404)) {
promise_list[i] = return context._signature_sub_storage.__storage._sub_storage.put(
storage_list[i].get(param).then(null, returnThe404ReasonsElseNull); context._signature_hash,
} {}
return all(promise_list); );
}
function synchronizeDocument(answers) {
return this_.syncGetAnswerList(command, answers);
}
function checkAnswers(answers) {
var i;
for (i = 0; i < answers.length; i += 1) {
if (answers[i].result !== "success") {
throw answers[i];
} }
} throw error;
} })
return repairSubStorages(). .push(function () {
then(getSubStoragesDocument). return RSVP.all([
then(synchronizeDocument). // Don't repair local_sub_storage twice
then(checkAnswers). // context._signature_sub_storage.repair.apply(
then(command.success, command.error, command.notify); // 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;
} }
factory(RSVP, jIO, fake_storage); jIO.addStorage('replicatestorage200', Storage200);
}([
"rsvp",
"jio",
"fakestorage",
"replicatestorage"
], function (RSVP, jIO, fake_storage) {
"use strict";
var all = RSVP.all, chain = RSVP.resolve, Promise = RSVP.Promise; function Storage500() {
return this;
/**
* 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);
});
} }
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"
}
});
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, {
replicate_jio = jIO.createJIO( "title": "foo"
shared.replicate_storage_description, }, "Check document");
shared.jio_option })
); .fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
/////////////////////////////////////////////////////////////////
// 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() {
shared.storage_description_list[0].sub_storage = setFakeStorage.original;
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 putSimilarDocuments() {
return all(jio_list.map(function (jio) {
return jio.post({
"identifier": "a",
"modified": shared.modified_date_list[0]
});
}));
}
function getDocumentNothingToSynchronize() { Storage200.prototype.post = function (param) {
return replicate_jio.get({"_id": "{\"identifier\":[\"a\"]}"}); deepEqual(param, {title: "bar"}, "post 200 called");
} return "foo";
};
function getDocumentNothingToSynchronizeTest(answer) { jio.post({title: "bar"})
deepEqual(answer, { .then(function (result) {
"data": { equal(result, "foo", "Check id");
"_id": "{\"identifier\":[\"a\"]}", })
"identifier": "a", .fail(function (error) {
"modified": shared.modified_date_list[0].toJSON() ok(false, error);
}, })
"id": "{\"identifier\":[\"a\"]}", .always(function () {
"method": "get", start();
"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\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
});
});
}
function putDifferentDocuments() { /////////////////////////////////////////////////////////////////
return all(jio_list.map(function (jio, i) { // replicateStorage.hasCapacity
return jio.post({ /////////////////////////////////////////////////////////////////
"identifier": "b", module("replicateStorage.hasCapacity");
"modified": shared.modified_date_list[i] test("hasCapacity return substorage value", function () {
}); var jio = jIO.createJIO({
})); type: "replicate",
} local_sub_storage: {
type: "replicatestorage200"
},
remote_sub_storage: {
type: "replicatestorage500"
}
});
function getDocumentWithSynchronization() { delete Storage200.prototype.hasCapacity;
return replicate_jio.get({"_id": "{\"identifier\":[\"b\"]}"});
}
function getDocumentWithSynchronizationTest(answer) { throws(
if (answer && answer.data) { function () {
ok(shared.modified_date_list.map(function (v) { jio.hasCapacity("foo");
return (v && v.toJSON()) || undefined; },
}).indexOf(answer.data.modified) !== -1, "Should be a known date"); function (error) {
delete answer.data.modified; ok(error instanceof jIO.util.jIOError);
equal(error.status_code, 501);
equal(error.message,
"Capacity 'foo' is not implemented on 'replicatestorage200'");
return true;
} }
deepEqual(answer, { );
"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\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
});
});
}
function putOneDocument() { /////////////////////////////////////////////////////////////////
return jio_list[1].post({ // replicateStorage.buildQuery
"identifier": "c", /////////////////////////////////////////////////////////////////
"modified": shared.modified_date_list[1] module("replicateStorage.buildQuery");
});
}
function getDocumentWith404Synchronization() { test("buildQuery return substorage buildQuery", function () {
return replicate_jio.get({"_id": "{\"identifier\":[\"c\"]}"}); stop();
} expect(2);
function getDocumentWith404SynchronizationTest(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; remote_sub_storage: {
type: "replicatestorage500"
} }
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");
});
});
}
function putDifferentDocuments2() { Storage200.prototype.hasCapacity = function () {
return all(jio_list.map(function (jio, i) { return true;
return jio.post({ };
"identifier": "d",
"modified": shared.modified_date_list[i]
});
}));
}
function getDocumentWithUnavailableStorage() { Storage200.prototype.buildQuery = function (options) {
setFakeStorage(); deepEqual(options, {
setTimeout(function () { include_docs: false,
fake_storage.commands[ sort_on: [["title", "ascending"]],
"replicate scenario test for get method - 1/allDocs" limit: [5],
].error({"status": 0}); select_list: ["title", "id"],
}, 100); replicate: 'title: "two"'
return replicate_jio.get({"_id": "{\"identifier\":[\"d\"]}"}); }, "allDocs parameter");
} return "bar";
};
function getDocumentWithUnavailableStorageTest(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"'
} })
deepEqual(answer, { .then(function (result) {
"data": { deepEqual(result, {
"_id": "{\"identifier\":[\"d\"]}", data: {
"identifier": "d" rows: "bar",
}, total_rows: 3
"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\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
});
}); });
} })
.fail(function (error) {
function unexpectedError(error) { ok(false, error);
if (error instanceof Error) { })
deepEqual([ .always(function () {
error.name + ": " + error.message, start();
error });
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain().
// get without synchronizing anything
then(putSimilarDocuments).
then(getDocumentNothingToSynchronize).
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; // replicateStorage.put
/////////////////////////////////////////////////////////////////
module("replicateStorage.put");
test("put called substorage put", function () {
stop();
expect(3);
// this test can work with at least 2 sub storages var jio = jIO.createJIO({
shared.gid_description = { type: "replicate",
"type": "gid", local_sub_storage: {
"constraints": { type: "replicatestorage200"
"default": {
"identifier": "list"
}
}, },
"sub_storage": null remote_sub_storage: {
type: "replicatestorage500"
}
});
Storage200.prototype.put = function (id, param) {
equal(id, "bar", "put 200 called");
deepEqual(param, {"title": "foo"}, "put 200 called");
return id;
}; };
shared.storage_description_list = []; jio.put("bar", {"title": "foo"})
for (i = 0; i < 4; i += 1) { .then(function (result) {
shared.storage_description_list[i] = jsonClone(shared.gid_description); equal(result, "bar");
shared.storage_description_list[i].sub_storage = { })
"type": "local", .fail(function (error) {
"username": "replicate scenario test for post method - " + (i + 1), ok(false, error);
"mode": "memory" })
}; .always(function () {
} start();
});
shared.replicate_storage_description = { });
"type": "replicate",
"storage_list": shared.storage_description_list
};
shared.workspace = {}; test("put can not modify the signature", function () {
shared.jio_option = { stop();
"workspace": shared.workspace, expect(3);
"max_retry": 0
};
jio_list = shared.storage_description_list.map(function (description) { var jio = jIO.createJIO({
return jIO.createJIO(description, shared.jio_option); type: "replicate",
local_sub_storage: {
type: "replicatestorage200"
},
remote_sub_storage: {
type: "replicatestorage500"
}
}); });
replicate_jio = jIO.createJIO( delete Storage200.prototype.put;
shared.replicate_storage_description,
shared.jio_option jio.put(jio.__storage._signature_hash, {"title": "foo"})
); .then(function () {
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(); stop();
expect(2);
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 });
); Storage200.prototype.remove = function (id) {
replicate_jio = jIO.createJIO( equal(id, "bar", "remove 200 called");
shared.replicate_storage_description, return id;
shared.jio_option };
);
}
function unsetFakeStorage() {
shared.storage_description_list[0].sub_storage = setFakeStorage.original;
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 createDocument() {
return replicate_jio.post({"identifier": "a"});
}
function createDocumentTest(answer) { jio.remove("bar", {"title": "foo"})
deepEqual(answer, { .then(function (result) {
"id": "{\"identifier\":[\"a\"]}", equal(result, "bar");
"method": "post", })
"result": "success", .fail(function (error) {
"status": 201, ok(false, error);
"statusText": "Created" })
}, "Post document"); .always(function () {
start();
});
});
return sleep(100); test("remove can not modify the signature", function () {
} stop();
expect(3);
function checkStorageContent() { var jio = jIO.createJIO({
// check storage state type: "replicate",
return all(jio_list.map(function (jio) { local_sub_storage: {
return jio.get({"_id": "{\"identifier\":[\"a\"]}"}); type: "replicatestorage200"
})).then(function (answers) { },
answers.forEach(function (answer) { remote_sub_storage: {
deepEqual(answer, { type: "replicatestorage500"
"data": { }
"_id": "{\"identifier\":[\"a\"]}", });
"identifier": "a" delete Storage200.prototype.remove;
},
"id": "{\"identifier\":[\"a\"]}", jio.remove(jio.__storage._signature_hash)
"method": "get", .then(function () {
"result": "success", ok(false);
"status": 200, })
"statusText": "Ok" .fail(function (error) {
}, "Check storage content"); ok(error instanceof jIO.util.jIOError);
}); equal(error.message, jio.__storage._signature_hash + " is frozen");
equal(error.status_code, 403);
})
.always(function () {
start();
}); });
} });
function updateDocument() { /////////////////////////////////////////////////////////////////
return replicate_jio.put({ // replicateStorage.repair use cases
"_id": "{\"identifier\":[\"a\"]}", /////////////////////////////////////////////////////////////////
"identifier": "a", module("replicateStorage.repair", {
"title": "b" 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 updateDocumentTest(answer) {
deepEqual(answer, {
"id": "{\"identifier\":[\"a\"]}",
"method": "put",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Update document");
return sleep(100);
} }
});
function checkStorageContent3() { test("local document creation", function () {
// check storage state stop();
return all(jio_list.map(function (jio) { expect(2);
return jio.get({"_id": "{\"identifier\":[\"a\"]}"});
})).then(function (answers) { var id,
answers.forEach(function (answer) { context = this;
deepEqual(answer, {
"data": { context.jio.post({"title": "foo"})
"_id": "{\"identifier\":[\"a\"]}", .then(function (result) {
"identifier": "a", id = result;
"title": "b" return context.jio.repair();
}, })
"id": "{\"identifier\":[\"a\"]}", .then(function () {
"method": "get", return context.jio.__storage._remote_sub_storage.get(id);
"result": "success", })
"status": 200, .then(function (result) {
"statusText": "Ok" deepEqual(result, {
}, "Check storage content"); 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();
}); });
} });
function createDocumentWithUnavailableStorage() {
setFakeStorage();
setTimeout(function () {
fake_storage.commands[
"replicate scenario test for post method - 1/allDocs"
].error({"status": 0});
}, 100);
return replicate_jio.post({"identifier": "b"});
}
function createDocumentWithUnavailableStorageTest(answer) {
deepEqual(answer, {
"id": "{\"identifier\":[\"b\"]}",
"method": "post",
"result": "success",
"status": 201,
"statusText": "Created"
}, "Post document with unavailable storage");
return sleep(100);
}
function checkStorageContent2() { test("remote document creation", function () {
unsetFakeStorage(); stop();
// check storage state expect(2);
return all(jio_list.map(function (jio, i) {
if (i === 0) { var id,
return reverse(jio.get({"_id": "{\"identifier\":[\"b\"]}"})); context = this;
}
return jio.get({"_id": "{\"identifier\":[\"b\"]}"}); context.jio.__storage._remote_sub_storage.post({"title": "bar"})
})).then(function (answers) { .then(function (result) {
deepEqual(answers[0], { id = result;
"error": "not_found", return context.jio.repair();
"id": "{\"identifier\":[\"b\"]}", })
"message": "Cannot get document", .then(function () {
"method": "get", return context.jio.get(id);
"reason": "missing", })
"result": "error", .then(function (result) {
"status": 404, deepEqual(result, {
"statusText": "Not Found" title: "bar"
}, "Check storage content"); });
answers.slice(1).forEach(function (answer) { })
deepEqual(answer, { .then(function () {
"data": { return context.jio.__storage._signature_sub_storage.get(id);
"_id": "{\"identifier\":[\"b\"]}", })
"identifier": "b" .then(function (result) {
}, deepEqual(result, {
"id": "{\"identifier\":[\"b\"]}", hash: "6799f3ea80e325b89f19589282a343c376c1f1af"
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
}); });
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
}
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().
// create a document
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 () { test("local and remote document creations", 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 = [];
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 remove method - " + (i + 1),
"mode": "memory"
};
}
shared.replicate_storage_description = {
"type": "replicate",
"storage_list": shared.storage_description_list
};
shared.workspace = {};
shared.jio_option = {
"workspace": shared.workspace,
"max_retry": 0
};
jio_list = shared.storage_description_list.map(function (description) {
return jIO.createJIO(description, shared.jio_option);
});
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
stop(); stop();
expect(5);
var context = this;
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 setFakeStorage() { test("local and remote same document creations", function () {
setFakeStorage.original = shared.storage_description_list[0].sub_storage; stop();
shared.storage_description_list[0].sub_storage = { expect(1);
"type": "fake",
"id": "replicate scenario test for remove method - 1" var context = this;
};
jio_list[0] = jIO.createJIO( RSVP.all([
shared.storage_description_list[0], context.jio.put("conflict", {"title": "foo"}),
shared.jio_option context.jio.__storage._remote_sub_storage.put("conflict",
); {"title": "foo"})
replicate_jio = jIO.createJIO( ])
shared.replicate_storage_description, .then(function () {
shared.jio_option return context.jio.repair();
); })
} .then(function () {
return context.jio.__storage._signature_sub_storage.get("conflict");
function unsetFakeStorage() { })
shared.storage_description_list[0].sub_storage = setFakeStorage.original; .then(function (result) {
jio_list[0] = jIO.createJIO( deepEqual(result, {
shared.storage_description_list[0], hash: "5ea9013447539ad65de308cbd75b5826a2ae30e5"
shared.jio_option
);
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
}
function putSomeDocuments() {
return all(jio_list.map(function (jio) {
return jio.post({"identifier": "a"});
}));
}
function removeDocument() {
return replicate_jio.remove({"_id": "{\"identifier\":[\"a\"]}"});
}
function removeDocumentTest(answer) {
deepEqual(answer, {
"id": "{\"identifier\":[\"a\"]}",
"method": "remove",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Remove document");
return sleep(100);
}
function checkStorageContent() {
// check storage state
return all(jio_list.map(function (jio) {
return reverse(jio.get({"_id": "{\"identifier\":[\"a\"]}"}));
})).then(function (answers) {
answers.forEach(function (answer) {
deepEqual(answer, {
"error": "not_found",
"id": "{\"identifier\":[\"a\"]}",
"message": "Cannot get document",
"method": "get",
"reason": "missing",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "Check storage content");
}); });
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
} });
function putSomeDocuments2() {
return all(jio_list.map(function (jio) {
return jio.post({"identifier": "b"});
}));
}
function removeDocumentWithUnavailableStorage() {
setFakeStorage();
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) {
deepEqual(answer, {
"id": "{\"identifier\":[\"b\"]}",
"method": "remove",
"result": "success",
"status": 204,
"statusText": "No Content"
}, "Remove document with unavailable storage");
return sleep(100);
}
function checkStorageContent2() { test("no modification", function () {
unsetFakeStorage(); stop();
// check storage state expect(2);
return all(jio_list.map(function (jio, i) {
if (i === 0) { var id,
return jio.get({"_id": "{\"identifier\":[\"b\"]}"}); context = this;
}
return reverse(jio.get({"_id": "{\"identifier\":[\"b\"]}"})); context.jio.post({"title": "foo"})
})).then(function (answers) { .then(function (result) {
deepEqual(answers[0], { id = result;
"data": { return context.jio.repair();
"_id": "{\"identifier\":[\"b\"]}", })
"identifier": "b" .then(function () {
}, return context.jio.repair();
"id": "{\"identifier\":[\"b\"]}", })
"method": "get", .then(function () {
"result": "success", return context.jio.__storage._remote_sub_storage.get(id);
"status": 200, })
"statusText": "Ok" .then(function (result) {
}, "Check storage content"); deepEqual(result, {
answers.slice(1).forEach(function (answer) { title: "foo"
deepEqual(answer, {
"error": "not_found",
"id": "{\"identifier\":[\"b\"]}",
"message": "Cannot get document",
"method": "get",
"reason": "missing",
"result": "error",
"status": 404,
"statusText": "Not Found"
}, "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 unexpectedError(error) {
if (error instanceof Error) {
deepEqual([
error.name + ": " + error.message,
error
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain().
// remove document
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 () { test("local document modification", 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 = [];
for (i = 0; i < 2; 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 allDocs method - " + (i + 1),
"mode": "memory"
};
}
shared.replicate_storage_description = {
"type": "replicate",
"storage_list": shared.storage_description_list
};
shared.workspace = {};
shared.jio_option = {
"workspace": shared.workspace,
"max_retry": 0
};
jio_list = shared.storage_description_list.map(function (description) {
return jIO.createJIO(description, shared.jio_option);
});
replicate_jio = jIO.createJIO(
shared.replicate_storage_description,
shared.jio_option
);
stop(); stop();
expect(2);
shared.modified_date_list = [
new Date("2000"), var id,
new Date("1995"), context = this;
null,
new Date("Invalid Date") context.jio.post({"title": "foo"})
]; .then(function (result) {
id = result;
function postSomeDocuments() { return context.jio.repair();
return all([ })
jio_list[0].post({ .then(function () {
"identifier": "a", return context.jio.put(id, {"title": "foo2"});
"modified": shared.modified_date_list[0] })
}), .then(function () {
jio_list[0].post({ return context.jio.repair();
"identifier": "b", })
"modified": shared.modified_date_list[1] .then(function () {
}), return context.jio.__storage._remote_sub_storage.get(id);
jio_list[1].post({ })
"identifier": "b", .then(function (result) {
"modified": shared.modified_date_list[0] deepEqual(result, {
}) title: "foo2"
]); });
} })
.then(function () {
function listDocuments() { return context.jio.__storage._signature_sub_storage.get(id);
return replicate_jio.allDocs({"include_docs": true}); })
} .then(function (result) {
deepEqual(result, {
function listDocumentsTest(answer) { hash: "9819187e39531fdc9bcfd40dbc6a7d3c78fe8dab"
answer.data.rows.sort(orderRowsById); });
deepEqual(answer, { })
"data": { .fail(function (error) {
"total_rows": 2, ok(false, error);
"rows": [ })
{ .always(function () {
"id": "{\"identifier\":[\"a\"]}", start();
"doc": { });
"_id": "{\"identifier\":[\"a\"]}",
"identifier": "a",
"modified": shared.modified_date_list[0].toJSON()
},
"value": {}
},
{
"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",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Document list should be merged correctly");
}
function setFakeStorage() {
shared.storage_description_list[0].sub_storage = {
"type": "fake",
"id": "replicate scenario test for allDocs 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 listDocumentsWithUnavailableStorage() {
setTimeout(function () {
fake_storage.commands[
"replicate scenario test for allDocs method - 1/allDocs"
].error({"status": 0});
}, 100);
return replicate_jio.allDocs({"include_docs": true});
}
function listDocumentsWithUnavailableStorageTest(answer) {
deepEqual(answer, {
"data": {
"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) {
if (error instanceof Error) {
deepEqual([
error.name + ": " + error.message,
error
], "NO ERROR", "Unexpected error");
} else {
deepEqual(error, "NO ERROR", "Unexpected error");
}
}
chain().
// list documents
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 () { test("remote document modification", function () {
var shared = {}, i, jio_list, replicate_jio; stop();
expect(2);
// this test can work with at least 2 sub storages
shared.gid_description = { var id,
"type": "gid", context = this;
"constraints": {
"default": { context.jio.post({"title": "foo"})
"identifier": "list" .then(function (result) {
} id = result;
}, return context.jio.repair();
"sub_storage": null })
}; .then(function () {
return context.jio.__storage._remote_sub_storage.put(
shared.storage_description_list = []; id,
for (i = 0; i < 4; i += 1) { {"title": "foo3"}
shared.storage_description_list[i] = jsonClone(shared.gid_description); );
shared.storage_description_list[i].sub_storage = { })
"type": "local", .then(function () {
"username": "replicate scenario test for repair method - " + (i + 1), return context.jio.repair();
"mode": "memory" })
}; .then(function () {
} return context.jio.get(id);
})
shared.replicate_storage_description = { .then(function (result) {
"type": "replicate", deepEqual(result, {
"storage_list": shared.storage_description_list 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();
});
});
shared.workspace = {}; test("local and remote document modifications", function () {
shared.jio_option = { stop();
"workspace": shared.workspace, expect(4);
"max_retry": 0
}; var id,
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();
});
});
jio_list = shared.storage_description_list.map(function (description) { test("local and remote document same modifications", function () {
return jIO.createJIO(description, shared.jio_option); stop();
}); expect(1);
replicate_jio = jIO.createJIO(
shared.replicate_storage_description, var id,
shared.jio_option 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": "foo99"}),
context.jio.__storage._remote_sub_storage.put(id, {"title": "foo99"})
]);
})
.then(function () {
return context.jio.repair();
})
.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();
});
});
test("local document deletion", function () {
stop(); stop();
expect(6);
var id,
context = this;
context.jio.post({"title": "foo"})
.then(function (result) {
id = result;
return context.jio.repair();
})
.then(function () {
return context.jio.remove(id);
})
.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();
});
});
shared.modified_date_list = [ test("remote document deletion", function () {
new Date("1995"), stop();
new Date("2000"), expect(6);
null,
new Date("Invalid Date") var id,
]; context = this;
shared.winner_modified_date = shared.modified_date_list[1];
context.jio.post({"title": "foo"})
function setFakeStorage() { .then(function (result) {
setFakeStorage.original = shared.storage_description_list[0].sub_storage; id = result;
shared.storage_description_list[0].sub_storage = { return context.jio.repair();
"type": "fake", })
"id": "replicate scenario test for repair method - 1" .then(function () {
}; return context.jio.__storage._remote_sub_storage.remove(id);
jio_list[0] = jIO.createJIO( })
shared.storage_description_list[0], .then(function () {
shared.jio_option return context.jio.repair();
); })
replicate_jio = jIO.createJIO( .then(function () {
shared.replicate_storage_description, ok(true, "Removal correctly synced");
shared.jio_option })
); .then(function () {
} return context.jio.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();
});
});
function unsetFakeStorage() { test("local and remote document deletions", function () {
shared.storage_description_list[0].sub_storage = setFakeStorage.original; stop();
jio_list[0] = jIO.createJIO( expect(8);
shared.storage_description_list[0],
shared.jio_option var id,
); context = this;
replicate_jio = jIO.createJIO(
shared.replicate_storage_description, context.jio.post({"title": "foo"})
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.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();
});
});
function putSimilarDocuments() { test("local deletion and remote modifications", function () {
return all(jio_list.map(function (jio) { stop();
return jio.post({ expect(2);
"identifier": "a",
"modified": shared.modified_date_list[0] var id,
context = this;
context.jio.post({"title": "foo"})
.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);
function repairDocumentNothingToSynchronize() { })
return replicate_jio.repair({"_id": "{\"identifier\":[\"a\"]}"}); .then(function (result) {
} deepEqual(result, {
hash: "8ed3a474128b6e0c0c7d3dd51b1a06ebfbf6722f"
function repairDocumentNothingToSynchronizeTest(answer) {
deepEqual(answer, {
"id": "{\"identifier\":[\"a\"]}",
"method": "repair",
"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\"]}",
"method": "get",
"result": "success",
"status": 200,
"statusText": "Ok"
}, "Check storage content");
}); });
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
} });
function putDifferentDocuments() { test("local modifications and remote deletion", function () {
return all(jio_list.map(function (jio, i) { stop();
return jio.post({ expect(2);
"identifier": "b",
"modified": shared.modified_date_list[i] var id,
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": "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);
function repairDocumentWithSynchronization() { })
return replicate_jio.repair({"_id": "{\"identifier\":[\"b\"]}"}); .then(function (result) {
} deepEqual(result, {
hash: "8ed3a474128b6e0c0c7d3dd51b1a06ebfbf6722f"
function repairDocumentWithSynchronizationTest(answer) {
deepEqual(answer, {
"id": "{\"identifier\":[\"b\"]}",
"method": "repair",
"result": "success",
"status": 204,
"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");
}); });
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
} });
function putOneDocument() { test("signature document is not synced", function () {
return jio_list[1].post({ stop();
"identifier": "c", expect(6);
"modified": shared.modified_date_list[1]
var context = this;
// Uses sessionstorage substorage, so that signature are stored
// in the same local sub storage
this.jio = jIO.createJIO({
type: "replicate",
local_sub_storage: {
type: "uuid",
sub_storage: {
type: "document",
document_id: "/",
sub_storage: {
type: "local",
sessiononly: true
}
}
},
remote_sub_storage: {
type: "memory"
}
});
context.jio.post({"title": "foo"})
.then(function () {
return context.jio.repair();
})
.then(function () {
return context.jio.__storage._remote_sub_storage.get(
context.jio.__storage._signature_hash
);
})
.fail(function (error) {
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) {
ok(error instanceof jIO.util.jIOError);
equal(error.message, "Cannot find document: " +
"_replicate_2be6c0851d60bcd9afe829e7133a136d266c779c");
equal(error.status_code, 404);
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
}); });
} });
function repairDocumentWith404Synchronization() { test("substorages are repaired too", function () {
return replicate_jio.repair({"_id": "{\"identifier\":[\"c\"]}"}); stop();
} expect(9);
function repairDocumentWith404SynchronizationTest(answer) { var context = this,
deepEqual(answer, { first_call = true,
"id": "{\"identifier\":[\"c\"]}", options = {foo: "bar"};
"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() { function Storage200CheckRepair() {
return all(jio_list.map(function (jio, i) { return this;
return jio.post({
"identifier": "d",
"modified": shared.modified_date_list[i]
});
}));
} }
Storage200CheckRepair.prototype.get = function () {
ok(true, "get 200 check repair called");
return {};
};
Storage200CheckRepair.prototype.hasCapacity = function () {
return true;
};
Storage200CheckRepair.prototype.buildQuery = function () {
ok(true, "buildQuery 200 check repair called");
return [];
};
Storage200CheckRepair.prototype.allAttachments = function () {
ok(true, "allAttachments 200 check repair called");
return {};
};
Storage200CheckRepair.prototype.repair = function (kw) {
if (first_call) {
deepEqual(
this,
context.jio.__storage._local_sub_storage.__storage,
"local substorage repair"
);
first_call = false;
} else {
deepEqual(
this,
context.jio.__storage._remote_sub_storage.__storage,
"remote substorage repair"
);
}
deepEqual(kw, options, "substorage repair parameters provided");
};
function repairDocumentWithUnavailableStorage() { jIO.addStorage(
setFakeStorage(); 'replicatestorage200chechrepair',
setTimeout(function () { Storage200CheckRepair
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) { this.jio = jIO.createJIO({
if (error instanceof Error) { type: "replicate",
deepEqual([ local_sub_storage: {
error.name + ": " + error.message, type: "replicatestorage200chechrepair"
error },
], "NO ERROR", "Unexpected error"); remote_sub_storage: {
} else { type: "replicatestorage200chechrepair"
deepEqual(error, "NO ERROR", "Unexpected error");
} }
} });
chain(). context.jio.repair(options)
// get without synchronizing anything .fail(function (error) {
then(putSimilarDocuments). ok(false, error);
then(repairDocumentNothingToSynchronize). })
then(repairDocumentNothingToSynchronizeTest). .always(function () {
// repair with synchronization start();
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);
}); });
})); }(jIO, QUnit));
...@@ -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