2017-07-03 20:58:01 +02:00
|
|
|
"use strict"
|
|
|
|
|
2017-11-03 02:09:34 +02:00
|
|
|
const { time } = require('lib/time-utils.js');
|
|
|
|
const { Logger } = require('lib/logger.js');
|
2017-12-14 20:12:14 +02:00
|
|
|
const Resource = require('lib/models/Resource.js');
|
2017-11-03 02:09:34 +02:00
|
|
|
const { dirname } = require('lib/path-utils.js');
|
|
|
|
const { FsDriverNode } = require('./fs-driver-node.js');
|
|
|
|
const lodash = require('lodash');
|
2017-07-01 00:53:22 +02:00
|
|
|
const exec = require('child_process').exec
|
|
|
|
const fs = require('fs-extra');
|
|
|
|
|
2017-07-03 20:03:14 +02:00
|
|
|
const baseDir = dirname(__dirname) + '/tests/fuzzing';
|
2017-07-01 00:53:22 +02:00
|
|
|
const syncDir = baseDir + '/sync';
|
|
|
|
const joplinAppPath = __dirname + '/main.js';
|
2017-07-01 14:12:00 +02:00
|
|
|
let syncDurations = [];
|
2017-07-01 00:53:22 +02:00
|
|
|
|
2017-07-06 00:29:03 +02:00
|
|
|
const fsDriver = new FsDriverNode();
|
|
|
|
Logger.fsDriver_ = fsDriver;
|
|
|
|
Resource.fsDriver_ = fsDriver;
|
|
|
|
|
2017-07-01 00:53:22 +02:00
|
|
|
const logger = new Logger();
|
|
|
|
logger.addTarget('console');
|
|
|
|
logger.setLevel(Logger.LEVEL_DEBUG);
|
|
|
|
|
2017-07-01 14:12:00 +02:00
|
|
|
process.on('unhandledRejection', (reason, p) => {
|
|
|
|
console.error('Unhandled promise rejection', p, 'reason:', reason);
|
|
|
|
});
|
|
|
|
|
2017-07-01 00:53:22 +02:00
|
|
|
function createClient(id) {
|
|
|
|
return {
|
|
|
|
'id': id,
|
|
|
|
'profileDir': baseDir + '/client' + id,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
async function createClients() {
|
|
|
|
let output = [];
|
|
|
|
let promises = [];
|
|
|
|
for (let clientId = 0; clientId < 2; clientId++) {
|
|
|
|
let client = createClient(clientId);
|
|
|
|
promises.push(fs.remove(client.profileDir));
|
2017-07-24 20:58:11 +02:00
|
|
|
promises.push(execCommand(client, 'config sync.target 2').then(() => { return execCommand(client, 'config sync.2.path ' + syncDir); }));
|
2017-07-01 00:53:22 +02:00
|
|
|
output.push(client);
|
|
|
|
}
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
function randomElement(array) {
|
|
|
|
if (!array.length) return null;
|
|
|
|
return array[Math.floor(Math.random() * array.length)];
|
|
|
|
}
|
|
|
|
|
|
|
|
function randomWord() {
|
2017-07-02 20:38:34 +02:00
|
|
|
const words = ['belief','scandalous','flawless','wrestle','sort','moldy','carve','incompetent','cruel','awful','fang','holistic','makeshift','synonymous','questionable','soft','drop','boot','whimsical','stir','idea','adhesive','present','hilarious','unusual','divergent','probable','depend','suck','belong','advise','straight','encouraging','wing','clam','serve','fill','nostalgic','dysfunctional','aggressive','floor','baby','grease','sisters','print','switch','control','victorious','cracker','dream','wistful','adaptable','reminiscent','inquisitive','pushy','unaccountable','receive','guttural','two','protect','skin','unbiased','plastic','loutish','zip','used','divide','communicate','dear','muddled','dinosaurs','grip','trees','well-off','calendar','chickens','irate','deranged','trip','stream','white','poison','attack','obtain','theory','laborer','omniscient','brake','maniacal','curvy','smoke','babies','punch','hammer','toothbrush','same','crown','jagged','peep','difficult','reject','merciful','useless','doctor','mix','wicked','plant','quickest','roll','suffer','curly','brother','frighten','cold','tremendous','move','knot','lame','imaginary','capricious','raspy','aunt','loving','wink','wooden','hop','free','drab','fire','instrument','border','frame','silent','glue','decorate','distance','powerful','pig','admit','fix','pour','flesh','profuse','skinny','learn','filthy','dress','bloody','produce','innocent','meaty','pray','slimy','sun','kindhearted','dime','exclusive','boast','neat','ruthless','recess','grieving','daily','hateful','ignorant','fence','spring','slim','education','overflow','plastic','gaping','chew','detect','right','lunch','gainful','argue','cloistered','horses','orange','shame','bitter','able','sail','magical','exist','force','wheel','best','suit','spurious','partner','request','dog','gusty','money','gaze','lonely','company','pale','tempt','rat','flame','wobble','superficial','stop','protective','stare','tongue','heal','railway','idiotic','roll','puffy','turn','meeting','new','frightening','sophisticated','poke','elderly','room','stimulating','increase','moor','secret','lean','occur','country','damp','evanescent','alluring','oafish','join','thundering','cars','awesome','advice','unruly','ray','wind','anxious','fly','hammer','adventurous','shop','cook','trucks','nonchalant','addition','base','abashed','excuse','giants','dramatic','piquant','coach','possess','poor','finger','wide-eyed','aquatic','welcome','instruct','expert','evasive','hug','cute','return','mice','damage','turkey','quiet','bewildered','tidy','pointless','outrageous','medical','foolish','curve','grandiose','gullible','hapless','gleaming','third','grin','pipe','egg','act','physical','eager','side','milk','tearful','fertile','average','glamorous','strange','yak','terrific','thin','near','snails','flowery','authority','fish','curious','perpetual','healthy','health','match','fade','chemical','economic','drawer','avoid','lying','minister','lick','powder','decay','desire','furry','faint','beam','sordid','fax','tail','bawdy','cherry','letter','clover','ladybug','teeth','behavior','black','amazing','pink','waste','island','forgetful','needless','lock','waves','boundary','receipt','handy','religion','hypnotic','aftermath','explain','sense','mundane','rambunctious','second','preserve','alarm','dusty','event','blow','weigh','value','glorious','jail','sigh','cemetery','serious','yummy','cattle','understood','limit','alert','fear','lucky','tested','surround','dolls','pleasant','disillusioned','discover','tray','night','seemly','liquid','worry','pen','bent','gruesome','war','teeny-tiny','common','judge','symptomatic','bed','trot','unequaled','flowers','friends','damaged','peel','skip','show','twist','worthless','brush','look','behave','imperfect','week','petite','direction','soda','lively','coal','coil','release','berserk','books','impossible','replace','cough','chunky','torpid','discreet','material','bomb','soothe','crack','hope','license','frightened','breathe','maddening','calculator','committee','paltry','green','subsequent','arrest','gigantic','tasty','met
|
2017-07-01 00:53:22 +02:00
|
|
|
return randomElement(words);
|
|
|
|
}
|
|
|
|
|
2017-07-01 14:12:00 +02:00
|
|
|
function execCommand(client, command, options = {}) {
|
2017-07-01 00:53:22 +02:00
|
|
|
let exePath = 'node ' + joplinAppPath;
|
2017-07-23 16:11:44 +02:00
|
|
|
let cmd = exePath + ' --update-geolocation-disabled --env dev --log-level debug --profile ' + client.profileDir + ' ' + command;
|
2017-07-02 12:34:07 +02:00
|
|
|
logger.info(client.id + ': ' + command);
|
2017-07-01 00:53:22 +02:00
|
|
|
|
2017-07-01 14:12:00 +02:00
|
|
|
if (options.killAfter) {
|
|
|
|
logger.info('Kill after: ' + options.killAfter);
|
|
|
|
}
|
|
|
|
|
2017-07-01 00:53:22 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
2017-07-01 14:12:00 +02:00
|
|
|
let childProcess = exec(cmd, (error, stdout, stderr) => {
|
2017-07-01 00:53:22 +02:00
|
|
|
if (error) {
|
2017-07-02 12:34:07 +02:00
|
|
|
if (error.signal == 'SIGTERM') {
|
|
|
|
resolve('Process was killed');
|
|
|
|
} else {
|
|
|
|
logger.error(stderr);
|
|
|
|
reject(error);
|
|
|
|
}
|
2017-07-01 00:53:22 +02:00
|
|
|
} else {
|
2017-07-08 00:25:03 +02:00
|
|
|
resolve(stdout.trim());
|
2017-07-01 00:53:22 +02:00
|
|
|
}
|
|
|
|
});
|
2017-07-01 14:12:00 +02:00
|
|
|
|
|
|
|
if (options.killAfter) {
|
|
|
|
setTimeout(() => {
|
|
|
|
logger.info('Sending kill signal...');
|
|
|
|
childProcess.kill();
|
|
|
|
}, options.killAfter);
|
|
|
|
}
|
2017-07-01 00:53:22 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-07-03 20:03:14 +02:00
|
|
|
async function clientItems(client) {
|
|
|
|
let itemsJson = await execCommand(client, 'dump');
|
|
|
|
try {
|
|
|
|
return JSON.parse(itemsJson);
|
|
|
|
} catch (error) {
|
|
|
|
throw new Error('Cannot parse JSON: ' + itemsJson);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-03 20:58:01 +02:00
|
|
|
function randomTag(items) {
|
|
|
|
let tags = [];
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
if (items[i].type_ != 5) continue;
|
|
|
|
tags.push(items[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return randomElement(tags);
|
|
|
|
}
|
|
|
|
|
|
|
|
function randomNote(items) {
|
|
|
|
let notes = [];
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
if (items[i].type_ != 1) continue;
|
|
|
|
notes.push(items[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return randomElement(notes);
|
|
|
|
}
|
|
|
|
|
2017-07-01 00:53:22 +02:00
|
|
|
async function execRandomCommand(client) {
|
|
|
|
let possibleCommands = [
|
2017-07-02 12:34:07 +02:00
|
|
|
['mkbook {word}', 40], // CREATE FOLDER
|
2017-07-02 20:38:34 +02:00
|
|
|
['mknote {word}', 70], // CREATE NOTE
|
2017-07-02 12:34:07 +02:00
|
|
|
[async () => { // DELETE RANDOM ITEM
|
2017-07-03 20:58:01 +02:00
|
|
|
let items = await clientItems(client);
|
2017-07-01 00:53:22 +02:00
|
|
|
let item = randomElement(items);
|
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
if (item.type_ == 1) {
|
2017-07-10 22:59:58 +02:00
|
|
|
return execCommand(client, 'rm -f ' + item.id);
|
2017-07-01 00:53:22 +02:00
|
|
|
} else if (item.type_ == 2) {
|
2017-07-11 20:17:23 +02:00
|
|
|
return execCommand(client, 'rm -r -f ' + item.id);
|
2017-07-03 20:58:01 +02:00
|
|
|
} else if (item.type_ == 5) {
|
|
|
|
// tag
|
2017-07-01 00:53:22 +02:00
|
|
|
} else {
|
|
|
|
throw new Error('Unknown type: ' + item.type_);
|
|
|
|
}
|
2017-07-02 12:34:07 +02:00
|
|
|
}, 30],
|
|
|
|
[async () => { // SYNC
|
2017-07-01 14:12:00 +02:00
|
|
|
let avgSyncDuration = averageSyncDuration();
|
|
|
|
let options = {};
|
|
|
|
if (!isNaN(avgSyncDuration)) {
|
2017-07-02 20:38:34 +02:00
|
|
|
if (Math.random() >= 0.5) {
|
|
|
|
options.killAfter = avgSyncDuration * Math.random();
|
|
|
|
}
|
2017-07-01 14:12:00 +02:00
|
|
|
}
|
2017-07-02 12:34:07 +02:00
|
|
|
return execCommand(client, 'sync --random-failures', options);
|
2017-07-02 20:38:34 +02:00
|
|
|
}, 30],
|
2017-07-02 12:34:07 +02:00
|
|
|
[async () => { // UPDATE RANDOM ITEM
|
2017-07-03 20:58:01 +02:00
|
|
|
let items = await clientItems(client);
|
2017-07-11 20:17:23 +02:00
|
|
|
let item = randomNote(items);
|
2017-07-02 12:34:07 +02:00
|
|
|
if (!item) return;
|
|
|
|
|
|
|
|
return execCommand(client, 'set ' + item.id + ' title "' + randomWord() + '"');
|
|
|
|
}, 50],
|
2017-07-03 20:58:01 +02:00
|
|
|
[async () => { // ADD TAG
|
|
|
|
let items = await clientItems(client);
|
|
|
|
let note = randomNote(items);
|
|
|
|
if (!note) return;
|
|
|
|
|
|
|
|
let tag = randomTag(items);
|
|
|
|
let tagTitle = !tag || Math.random() >= 0.9 ? 'tag-' + randomWord() : tag.title;
|
|
|
|
|
2017-07-10 22:59:58 +02:00
|
|
|
return execCommand(client, 'tag add ' + tagTitle + ' ' + note.id);
|
2017-07-03 20:58:01 +02:00
|
|
|
}, 50],
|
2017-07-01 00:53:22 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
let cmd = null;
|
|
|
|
while (true) {
|
|
|
|
cmd = randomElement(possibleCommands);
|
|
|
|
let r = 1 + Math.floor(Math.random() * 100);
|
|
|
|
if (r <= cmd[1]) break;
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd = cmd[0];
|
|
|
|
|
|
|
|
if (typeof cmd === 'function') {
|
|
|
|
return cmd();
|
|
|
|
} else {
|
|
|
|
cmd = cmd.replace('{word}', randomWord());
|
|
|
|
return execCommand(client, cmd);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-01 14:12:00 +02:00
|
|
|
function averageSyncDuration() {
|
|
|
|
return lodash.mean(syncDurations);
|
|
|
|
}
|
|
|
|
|
2017-07-01 00:53:22 +02:00
|
|
|
function randomNextCheckTime() {
|
2017-07-02 12:34:07 +02:00
|
|
|
let output = time.unixMs() + 1000 + Math.random() * 1000 * 120;
|
2017-07-01 00:53:22 +02:00
|
|
|
logger.info('Next sync check: ' + time.unixMsToIso(output) + ' (' + (Math.round((output - time.unixMs()) / 1000)) + ' sec.)');
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2017-07-01 14:12:00 +02:00
|
|
|
function findItem(items, itemId) {
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
if (items[i].id == itemId) return items[i];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function compareItems(item1, item2) {
|
|
|
|
let output = [];
|
|
|
|
for (let n in item1) {
|
|
|
|
if (!item1.hasOwnProperty(n)) continue;
|
|
|
|
let p1 = item1[n];
|
|
|
|
let p2 = item2[n];
|
2017-07-03 20:58:01 +02:00
|
|
|
|
|
|
|
if (n == 'notes_') {
|
|
|
|
p1.sort();
|
|
|
|
p2.sort();
|
|
|
|
if (JSON.stringify(p1) !== JSON.stringify(p2)) {
|
|
|
|
output.push(n);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (p1 !== p2) output.push(n);
|
|
|
|
}
|
2017-07-01 14:12:00 +02:00
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
function findMissingItems_(items1, items2) {
|
|
|
|
let output = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < items1.length; i++) {
|
|
|
|
let item1 = items1[i];
|
|
|
|
let found = false;
|
|
|
|
for (let j = 0; j < items2.length; j++) {
|
|
|
|
let item2 = items2[j];
|
|
|
|
if (item1.id == item2.id) {
|
|
|
|
found = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!found) {
|
|
|
|
output.push(item1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
|
|
|
function findMissingItems(items1, items2) {
|
|
|
|
return [
|
|
|
|
findMissingItems_(items1, items2),
|
|
|
|
findMissingItems_(items2, items1),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2017-07-01 00:53:22 +02:00
|
|
|
async function compareClientItems(clientItems) {
|
|
|
|
let itemCounts = [];
|
|
|
|
for (let i = 0; i < clientItems.length; i++) {
|
|
|
|
let items = clientItems[i];
|
|
|
|
itemCounts.push(items.length);
|
|
|
|
}
|
|
|
|
logger.info('Item count: ' + itemCounts.join(', '));
|
2017-07-01 14:12:00 +02:00
|
|
|
|
|
|
|
let missingItems = findMissingItems(clientItems[0], clientItems[1]);
|
|
|
|
if (missingItems[0].length || missingItems[1].length) {
|
2017-07-01 17:07:17 +02:00
|
|
|
logger.error('Items are different');
|
2017-07-01 14:12:00 +02:00
|
|
|
logger.error(missingItems);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
let differences = [];
|
|
|
|
let items = clientItems[0];
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
let item1 = items[i];
|
|
|
|
for (let clientId = 1; clientId < clientItems.length; clientId++) {
|
|
|
|
let item2 = findItem(clientItems[clientId], item1.id);
|
|
|
|
if (!item2) {
|
|
|
|
logger.error('Item not found on client ' + clientId + ':');
|
|
|
|
logger.error(item1);
|
|
|
|
process.exit(1);
|
|
|
|
}
|
2017-07-01 00:53:22 +02:00
|
|
|
|
2017-07-01 14:12:00 +02:00
|
|
|
let diff = compareItems(item1, item2);
|
|
|
|
if (diff.length) {
|
|
|
|
differences.push({
|
2017-07-03 20:58:01 +02:00
|
|
|
item1: JSON.stringify(item1),
|
|
|
|
item2: JSON.stringify(item2),
|
2017-07-01 14:12:00 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (differences.length) {
|
|
|
|
logger.error('Found differences between items:');
|
|
|
|
logger.error(differences);
|
2017-07-01 00:53:22 +02:00
|
|
|
process.exit(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function main(argv) {
|
|
|
|
await fs.remove(syncDir);
|
2017-07-01 14:12:00 +02:00
|
|
|
|
2017-07-01 00:53:22 +02:00
|
|
|
let clients = await createClients();
|
|
|
|
let activeCommandCounts = [];
|
|
|
|
let clientId = 0;
|
|
|
|
|
|
|
|
for (let i = 0; i < clients.length; i++) {
|
|
|
|
clients[i].activeCommandCount = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
function handleCommand(clientId) {
|
|
|
|
if (clients[clientId].activeCommandCount >= 1) return;
|
|
|
|
|
|
|
|
clients[clientId].activeCommandCount++;
|
|
|
|
|
|
|
|
execRandomCommand(clients[clientId]).catch((error) => {
|
|
|
|
logger.info('Client ' + clientId + ':');
|
|
|
|
logger.error(error);
|
|
|
|
}).then((r) => {
|
|
|
|
if (r) {
|
2017-07-02 20:38:34 +02:00
|
|
|
logger.info('Client ' + clientId + ":\n" + r.trim())
|
2017-07-01 00:53:22 +02:00
|
|
|
}
|
|
|
|
clients[clientId].activeCommandCount--;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let nextSyncCheckTime = randomNextCheckTime();
|
|
|
|
let state = 'commands';
|
|
|
|
|
|
|
|
setInterval(async () => {
|
|
|
|
if (state == 'waitForSyncCheck') return;
|
|
|
|
|
|
|
|
if (state == 'syncCheck') {
|
|
|
|
state = 'waitForSyncCheck';
|
|
|
|
let clientItems = [];
|
2017-07-01 14:12:00 +02:00
|
|
|
// Up to 3 sync operations must be performed by each clients in order for them
|
|
|
|
// to be perfectly in sync - in order for each items to send their changes
|
|
|
|
// and get those from the other clients, and to also get changes that are
|
|
|
|
// made as a result of a sync operation (eg. renaming a folder that conflicts
|
|
|
|
// with another one).
|
|
|
|
for (let loopCount = 0; loopCount < 3; loopCount++) {
|
2017-07-01 00:53:22 +02:00
|
|
|
for (let i = 0; i < clients.length; i++) {
|
2017-07-01 14:12:00 +02:00
|
|
|
let beforeTime = time.unixMs();
|
2017-07-01 00:53:22 +02:00
|
|
|
await execCommand(clients[i], 'sync');
|
2017-07-01 14:12:00 +02:00
|
|
|
syncDurations.push(time.unixMs() - beforeTime);
|
|
|
|
if (syncDurations.length > 20) syncDurations.splice(0, 1);
|
|
|
|
if (loopCount === 2) {
|
2017-07-01 00:53:22 +02:00
|
|
|
let dump = await execCommand(clients[i], 'dump');
|
|
|
|
clientItems[i] = JSON.parse(dump);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await compareClientItems(clientItems);
|
|
|
|
|
|
|
|
nextSyncCheckTime = randomNextCheckTime();
|
|
|
|
state = 'commands';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state == 'waitForClients') {
|
|
|
|
for (let i = 0; i < clients.length; i++) {
|
|
|
|
if (clients[i].activeCommandCount > 0) return;
|
|
|
|
}
|
|
|
|
|
|
|
|
state = 'syncCheck';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (state == 'commands') {
|
|
|
|
if (nextSyncCheckTime <= time.unixMs()) {
|
|
|
|
state = 'waitForClients';
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCommand(clientId);
|
|
|
|
clientId++;
|
|
|
|
if (clientId >= clients.length) clientId = 0;
|
|
|
|
}
|
|
|
|
}, 100);
|
|
|
|
}
|
|
|
|
|
2017-07-01 14:12:00 +02:00
|
|
|
main(process.argv).catch((error) => {
|
|
|
|
logger.error(error);
|
|
|
|
});
|