1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-10-31 00:07:48 +02:00

Test sync

This commit is contained in:
Laurent Cozic
2017-06-13 23:39:45 +01:00
parent 18adbeea27
commit 226353ecb7
10 changed files with 261 additions and 54 deletions

View File

@@ -1,4 +1,5 @@
build/ build/
node_modules/ node_modules/
app/src app/src
spec-build/ tests-build/
tests/src

View File

@@ -33,6 +33,6 @@
"babelbuild": "babel app -d build", "babelbuild": "babel app -d build",
"build": "babel-changed app -d build --source-maps && babel-changed app/src/models -d build/src/models --source-maps && babel-changed app/src/services -d build/src/services --source-maps", "build": "babel-changed app -d build --source-maps && babel-changed app/src/models -d build/src/models --source-maps && babel-changed app/src/services -d build/src/services --source-maps",
"clean": "babel-changed --reset", "clean": "babel-changed --reset",
"test": "babel-changed spec -d spec-build --source-maps && jasmine" "test": "babel-changed tests -d tests-build --source-maps && jasmine"
} }
} }

View File

@@ -1,7 +1,8 @@
#!/bin/bash #!/bin/bash
CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" CLIENT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
rm -f "$CLIENT_DIR/spec-build/src" rm -f "$CLIENT_DIR/tests-build/src"
ln -s "$CLIENT_DIR/build/src" "$CLIENT_DIR/spec-build" mkdir -p "$CLIENT_DIR/tests-build"
ln -s "$CLIENT_DIR/build/src" "$CLIENT_DIR/tests-build"
npm build && NODE_PATH="$CLIENT_DIR/spec-build/" npm test spec-build/synchronizer.js npm run build && NODE_PATH="$CLIENT_DIR/tests-build/" npm test tests-build/synchronizer.js

View File

@@ -1 +0,0 @@
/home/laurent/src/notes/CliClient/../ReactNativeClient/src

View File

@@ -1,32 +0,0 @@
import { Synchronizer } from 'src/synchronizer.js';
import { FileApi } from 'src/file-api.js';
import { FileApiDriverMemory } from 'src/file-api-driver-memory.js';
describe("syncActions", function() {
let fileDriver = new FileApiDriverMemory();
let fileApi = new FileApi('/root', fileDriver);
let synchronizer = new Synchronizer(null, fileApi);
it("and so is a spec", function() {
let localItems = [];
localItems.push({ path: 'test', isDir: true, updatedTime: 1497370000 });
localItems.push({ path: 'test/un', updatedTime: 1497370000 });
localItems.push({ path: 'test/deux', updatedTime: 1497370000 });
let remoteItems = [];
let actions = synchronizer.syncActions(localItems, remoteItems, 0);
expect(actions.length).toBe(3);
// synchronizer.format();
// synchronizer.mkdir('test');
// synchronizer.touch('test/un');
// synchronizer.touch('test/deux');
// synchronizer.touch('test/trois');
});
});

View File

@@ -0,0 +1,160 @@
import { Synchronizer } from 'src/synchronizer.js';
import { FileApi } from 'src/file-api.js';
import { FileApiDriverMemory } from 'src/file-api-driver-memory.js';
import { time } from 'src/time-utils.js';
describe('Synchronizer syncActions', function() {
let fileDriver = new FileApiDriverMemory();
let fileApi = new FileApi('/root', fileDriver);
let synchronizer = new Synchronizer(null, fileApi);
// Note: set 1 matches set 1 of createRemoteItems()
function createLocalItems(id, updatedTime, lastSyncTime) {
let output = [];
if (id === 1) {
output.push({ path: 'test', isDir: true, updatedTime: updatedTime, lastSyncTime: lastSyncTime });
output.push({ path: 'test/un', updatedTime: updatedTime, lastSyncTime: lastSyncTime });
} else {
throw new Error('Invalid ID');
}
return output;
}
function createRemoteItems(id = 1, updatedTime = null) {
if (!updatedTime) updatedTime = time.unix();
if (id === 1) {
return fileApi.format()
.then(() => fileApi.mkdir('test'))
.then(() => fileApi.put('test/un', 'abcd'))
.then(() => fileApi.list('', true))
.then((items) => {
for (let i = 0; i < items.length; i++) {
items[i].updatedTime = updatedTime;
}
return items;
});
} else {
throw new Error('Invalid ID');
}
}
it('should create remote items', function() {
let localItems = createLocalItems(1, time.unix(), 0);
let remoteItems = [];
let actions = synchronizer.syncActions(localItems, remoteItems, []);
expect(actions.length).toBe(2);
for (let i = 0; i < actions.length; i++) {
expect(actions[i].type).toBe('create');
expect(actions[i].dest).toBe('remote');
}
});
it('should update remote items', function(done) {
createRemoteItems(1).then((remoteItems) => {
let lastSyncTime = time.unix() + 1000;
let localItems = createLocalItems(1, lastSyncTime + 1000, lastSyncTime);
let actions = synchronizer.syncActions(localItems, remoteItems, []);
expect(actions.length).toBe(2);
for (let i = 0; i < actions.length; i++) {
expect(actions[i].type).toBe('update');
expect(actions[i].dest).toBe('remote');
}
done();
});
});
it('should detect conflict', function(done) {
// Simulate this scenario:
// - Client 1 create items
// - Client 1 sync
// - Client 2 sync
// - Client 2 change items
// - Client 2 sync
// - Client 1 change items
// - Client 1 sync
// => Conflict
createRemoteItems(1).then((remoteItems) => {
let localItems = createLocalItems(1, time.unix() + 1000, time.unix() - 1000);
let actions = synchronizer.syncActions(localItems, remoteItems, []);
expect(actions.length).toBe(2);
for (let i = 0; i < actions.length; i++) {
expect(actions[i].type).toBe('conflict');
}
done();
});
});
it('should create local file', function(done) {
createRemoteItems(1).then((remoteItems) => {
let localItems = [];
let actions = synchronizer.syncActions(localItems, remoteItems, []);
expect(actions.length).toBe(2);
for (let i = 0; i < actions.length; i++) {
expect(actions[i].type).toBe('create');
expect(actions[i].dest).toBe('local');
}
done();
});
});
it('should delete remote files', function(done) {
createRemoteItems(1).then((remoteItems) => {
let localItems = createLocalItems(1, time.unix(), time.unix());
let deletedItemPaths = [localItems[0].path, localItems[1].path];
let actions = synchronizer.syncActions([], remoteItems, deletedItemPaths);
expect(actions.length).toBe(2);
for (let i = 0; i < actions.length; i++) {
expect(actions[i].type).toBe('delete');
expect(actions[i].dest).toBe('remote');
}
done();
});
});
it('should delete local files', function(done) {
let lastSyncTime = time.unix();
createRemoteItems(1, lastSyncTime - 1000).then((remoteItems) => {
let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime);
let actions = synchronizer.syncActions(localItems, [], []);
expect(actions.length).toBe(2);
for (let i = 0; i < actions.length; i++) {
expect(actions[i].type).toBe('delete');
expect(actions[i].dest).toBe('local');
}
done();
});
});
it('should update local files', function(done) {
let lastSyncTime = time.unix();
createRemoteItems(1, lastSyncTime + 1000).then((remoteItems) => {
let localItems = createLocalItems(1, lastSyncTime - 1000, lastSyncTime);
let actions = synchronizer.syncActions(localItems, remoteItems, []);
expect(actions.length).toBe(2);
for (let i = 0; i < actions.length; i++) {
expect(actions[i].type).toBe('update');
expect(actions[i].dest).toBe('local');
}
done();
});
});
});

View File

@@ -96,35 +96,103 @@ class Synchronizer {
}; };
} }
itemIsSameDate(item, date) {
return Math.abs(item.updatedTime - date) <= 1;
}
itemIsNewerThan(item, date) { itemIsNewerThan(item, date) {
if (this.itemIsSameDate(item, date)) return false;
return item.updatedTime > date; return item.updatedTime > date;
} }
itemIsOlderThan(item, date) { itemIsOlderThan(item, date) {
return !this.itemIsNewerThan(item, date); if (this.itemIsSameDate(item, date)) return false;
return item.updatedTime < date;
} }
syncActions(localItems, remoteItems, lastSyncTime) { // Assumption: it's not possible to, for example, have a directory one the dest
// and a file with the same name on the source. It's not possible because the
// file and directory names are UUID so should be unique.
syncActions(localItems, remoteItems, deletedLocalPaths) {
let output = []; let output = [];
let donePaths = [];
for (let i = 0; i < localItems.length; i++) { for (let i = 0; i < localItems.length; i++) {
let item = localItems[i]; let local = localItems[i];
let remoteItem = this.itemByPath(remoteItems, item.path); let remote = this.itemByPath(remoteItems, local.path);
let action = { let action = {
localItem: item, local: local,
remoteItem: remoteItem, remote: remote,
}; };
if (!remoteItem) {
action.type = 'create'; if (!remote) {
action.where = 'there'; if (local.lastSyncTime) {
} else { // The item has been synced previously and now is no longer in the dest
if (this.itemIsOlderThan(remoteItem, lastSyncTime)) { // which means it has been deleted.
action.type = 'update'; action.type = 'delete';
action.where = 'there'; action.dest = 'local';
} else { } else {
action.type = 'conflict'; // Move local to /Conflict; Copy remote here // The item has never been synced and is not present in the dest
action.where = 'here'; // which means it is new
action.type = 'create';
action.dest = 'remote';
} }
} else {
if (this.itemIsOlderThan(local, local.lastSyncTime)) continue;
if (this.itemIsOlderThan(remote, local.lastSyncTime)) {
action.type = 'update';
action.dest = 'remote';
} else {
action.type = 'conflict';
if (local.isDir) {
// For folders, currently we don't completely handle conflicts, we just
// we just update the local dir (.folder metadata file) with the remote
// version. It means the local version is lost but shouldn't be a big deal
// and should be rare (at worst, the folder name needs to renamed).
action.solution = [
{ type: 'update', dest: 'local' },
];
} else {
action.solution = [
{ type: 'copy-to-remote-conflict-dir', dest: 'local' },
{ type: 'copy-to-local-conflict-dir', dest: 'local' },
{ type: 'update', dest: 'local' },
];
}
}
}
donePaths.push(local.path);
output.push(action);
}
for (let i = 0; i < remoteItems.length; i++) {
let remote = remoteItems[i];
if (donePaths.indexOf(remote.path) >= 0) continue; // Already handled in the previous loop
let local = this.itemByPath(localItems, remote.path);
let action = {
local: local,
remote: remote,
};
if (!local) {
if (deletedLocalPaths.indexOf(remote.path) >= 0) {
action.type = 'delete';
action.dest = 'remote';
} else {
action.type = 'create';
action.dest = 'local';
}
} else {
if (this.itemIsOlderThan(remote, local.lastSyncTime)) continue; // Already have this version
// Note: no conflict is possible here since if the local item has been
// modified since the last sync, it's been processed in the previous loop.
action.type = 'update';
action.dest = 'local';
} }
output.push(action); output.push(action);

View File

@@ -0,0 +1,9 @@
let time = {
unix() {
return Math.round((new Date()).getTime() / 1000);
}
}
export { time };

View File

@@ -11,8 +11,9 @@
"app/data/uploads", "app/data/uploads",
"CliClient/node_modules", "CliClient/node_modules",
"CliClient/build", "CliClient/build",
"CliClient/spec-build", "CliClient/tests-build",
"CliClient/app/src", "CliClient/app/src",
"CliClient/tests/src",
"ReactNativeClient/node_modules", "ReactNativeClient/node_modules",
"ReactNativeClient/android/app/build", "ReactNativeClient/android/app/build",
"ReactNativeClient/android/build", "ReactNativeClient/android/build",