1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-03 08:35:29 +02:00
joplin/CliClient/app/fuzzing.js

385 lines
27 KiB
JavaScript
Raw Normal View History

2017-07-03 20:58:01 +02:00
"use strict"
2017-07-01 00:53:22 +02:00
require('source-map-support').install();
require('babel-plugin-transform-runtime');
import { time } from 'lib/time-utils.js';
import { Logger } from 'lib/logger.js';
2017-07-06 00:29:03 +02:00
import { Resource } from 'lib/models/resource.js';
import { dirname } from 'lib/path-utils.js';
2017-07-06 00:29:03 +02:00
import { FsDriverNode } from './fs-driver-node.js';
2017-07-01 00:53:22 +02:00
import lodash from 'lodash';
const exec = require('child_process').exec
const fs = require('fs-extra');
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));
promises.push(execCommand(client, 'config sync.target local').then(() => { return execCommand(client, 'config sync.local.path ' + syncDir); }));
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-10 22:59:58 +02:00
let cmd = exePath + ' --update-geolocation-disabled --env dev --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
});
}
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) {
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);
});