diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index 5d066dfd9..4f157ddfa 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -28,6 +28,219 @@ const AttributePool = require('../../../static/js/AttributePool'); const randInt = (maxValue) => Math.floor(Math.random() * maxValue); +const poolOrArray = (attribs) => { + if (attribs.getAttrib) { + return attribs; // it's already an attrib pool + } else { + // assume it's an array of attrib strings to be split and added + const p = new AttributePool(); + attribs.forEach((kv) => { + p.putAttrib(kv.split(',')); + }); + return p; + } +}; + +const randomInlineString = (len) => { + const assem = Changeset.stringAssembler(); + for (let i = 0; i < len; i++) { + assem.append(String.fromCharCode(randInt(26) + 97)); + } + return assem.toString(); +}; + +const randomMultiline = (approxMaxLines, approxMaxCols) => { + const numParts = randInt(approxMaxLines * 2) + 1; + const txt = Changeset.stringAssembler(); + txt.append(randInt(2) ? '\n' : ''); + for (let i = 0; i < numParts; i++) { + if ((i % 2) === 0) { + if (randInt(10)) { + txt.append(randomInlineString(randInt(approxMaxCols) + 1)); + } else { + txt.append('\n'); + } + } else { + txt.append('\n'); + } + } + return txt.toString(); +}; + +const randomStringOperation = (numCharsLeft) => { + let result; + switch (randInt(9)) { + case 0: + { + // insert char + result = { + insert: randomInlineString(1), + }; + break; + } + case 1: + { + // delete char + result = { + remove: 1, + }; + break; + } + case 2: + { + // skip char + result = { + skip: 1, + }; + break; + } + case 3: + { + // insert small + result = { + insert: randomInlineString(randInt(4) + 1), + }; + break; + } + case 4: + { + // delete small + result = { + remove: randInt(4) + 1, + }; + break; + } + case 5: + { + // skip small + result = { + skip: randInt(4) + 1, + }; + break; + } + case 6: + { + // insert multiline; + result = { + insert: randomMultiline(5, 20), + }; + break; + } + case 7: + { + // delete multiline + result = { + remove: Math.round(numCharsLeft * Math.random() * Math.random()), + }; + break; + } + case 8: + { + // skip multiline + result = { + skip: Math.round(numCharsLeft * Math.random() * Math.random()), + }; + break; + } + case 9: + { + // delete to end + result = { + remove: numCharsLeft, + }; + break; + } + case 10: + { + // skip to end + result = { + skip: numCharsLeft, + }; + break; + } + } + const maxOrig = numCharsLeft - 1; + if ('remove' in result) { + result.remove = Math.min(result.remove, maxOrig); + } else if ('skip' in result) { + result.skip = Math.min(result.skip, maxOrig); + } + return result; +}; + +const randomTwoPropAttribs = (opcode) => { + // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] + if (opcode === '-' || randInt(3)) { + return ''; + } else if (randInt(3)) { // eslint-disable-line no-dupe-else-if + if (opcode === '+' || randInt(2)) { + return `*${Changeset.numToString(randInt(2) * 2 + 1)}`; + } else { + return `*${Changeset.numToString(randInt(2) * 2)}`; + } + } else if (opcode === '+' || randInt(4) === 0) { + return '*1*3'; + } else { + return ['*0*2', '*0*3', '*1*2'][randInt(3)]; + } +}; + +const randomTestChangeset = (origText, withAttribs) => { + const charBank = Changeset.stringAssembler(); + let textLeft = origText; // always keep final newline + const outTextAssem = Changeset.stringAssembler(); + const opAssem = Changeset.smartOpAssembler(); + const oldLen = origText.length; + + const nextOp = new Changeset.Op(); + + const appendMultilineOp = (opcode, txt) => { + nextOp.opcode = opcode; + if (withAttribs) { + nextOp.attribs = randomTwoPropAttribs(opcode); + } + txt.replace(/\n|[^\n]+/g, (t) => { + if (t === '\n') { + nextOp.chars = 1; + nextOp.lines = 1; + opAssem.append(nextOp); + } else { + nextOp.chars = t.length; + nextOp.lines = 0; + opAssem.append(nextOp); + } + return ''; + }); + }; + + const doOp = () => { + const o = randomStringOperation(textLeft.length); + if (o.insert) { + const txt = o.insert; + charBank.append(txt); + outTextAssem.append(txt); + appendMultilineOp('+', txt); + } else if (o.skip) { + const txt = textLeft.substring(0, o.skip); + textLeft = textLeft.substring(o.skip); + outTextAssem.append(txt); + appendMultilineOp('=', txt); + } else if (o.remove) { + const txt = textLeft.substring(0, o.remove); + textLeft = textLeft.substring(o.remove); + appendMultilineOp('-', txt); + } + }; + + while (textLeft.length > 1) doOp(); + for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) + const outText = `${outTextAssem.toString()}\n`; + opAssem.endDocument(); + const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); + Changeset.checkRep(cs); + return [cs, outText]; +}; + describe('easysync', function () { it('throughIterator', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; @@ -176,19 +389,6 @@ describe('easysync', function () { ['skip', 1, 1, true], ], ['banana\n', 'cabbage\n', 'duffle\n']); - const poolOrArray = (attribs) => { - if (attribs.getAttrib) { - return attribs; // it's already an attrib pool - } else { - // assume it's an array of attrib strings to be split and added - const p = new AttributePool(); - attribs.forEach((kv) => { - p.putAttrib(kv.split(',')); - }); - return p; - } - }; - const runApplyToAttributionTest = (testId, attribs, cs, inAttr, outCorrect) => { it(`applyToAttribution#${testId}`, async function () { const p = poolOrArray(attribs); @@ -354,206 +554,6 @@ describe('easysync', function () { '|1+1', ]); - const randomInlineString = (len) => { - const assem = Changeset.stringAssembler(); - for (let i = 0; i < len; i++) { - assem.append(String.fromCharCode(randInt(26) + 97)); - } - return assem.toString(); - }; - - const randomMultiline = (approxMaxLines, approxMaxCols) => { - const numParts = randInt(approxMaxLines * 2) + 1; - const txt = Changeset.stringAssembler(); - txt.append(randInt(2) ? '\n' : ''); - for (let i = 0; i < numParts; i++) { - if ((i % 2) === 0) { - if (randInt(10)) { - txt.append(randomInlineString(randInt(approxMaxCols) + 1)); - } else { - txt.append('\n'); - } - } else { - txt.append('\n'); - } - } - return txt.toString(); - }; - - const randomStringOperation = (numCharsLeft) => { - let result; - switch (randInt(9)) { - case 0: - { - // insert char - result = { - insert: randomInlineString(1), - }; - break; - } - case 1: - { - // delete char - result = { - remove: 1, - }; - break; - } - case 2: - { - // skip char - result = { - skip: 1, - }; - break; - } - case 3: - { - // insert small - result = { - insert: randomInlineString(randInt(4) + 1), - }; - break; - } - case 4: - { - // delete small - result = { - remove: randInt(4) + 1, - }; - break; - } - case 5: - { - // skip small - result = { - skip: randInt(4) + 1, - }; - break; - } - case 6: - { - // insert multiline; - result = { - insert: randomMultiline(5, 20), - }; - break; - } - case 7: - { - // delete multiline - result = { - remove: Math.round(numCharsLeft * Math.random() * Math.random()), - }; - break; - } - case 8: - { - // skip multiline - result = { - skip: Math.round(numCharsLeft * Math.random() * Math.random()), - }; - break; - } - case 9: - { - // delete to end - result = { - remove: numCharsLeft, - }; - break; - } - case 10: - { - // skip to end - result = { - skip: numCharsLeft, - }; - break; - } - } - const maxOrig = numCharsLeft - 1; - if ('remove' in result) { - result.remove = Math.min(result.remove, maxOrig); - } else if ('skip' in result) { - result.skip = Math.min(result.skip, maxOrig); - } - return result; - }; - - const randomTwoPropAttribs = (opcode) => { - // assumes attrib pool like ['apple,','apple,true','banana,','banana,true'] - if (opcode === '-' || randInt(3)) { - return ''; - } else if (randInt(3)) { // eslint-disable-line no-dupe-else-if - if (opcode === '+' || randInt(2)) { - return `*${Changeset.numToString(randInt(2) * 2 + 1)}`; - } else { - return `*${Changeset.numToString(randInt(2) * 2)}`; - } - } else if (opcode === '+' || randInt(4) === 0) { - return '*1*3'; - } else { - return ['*0*2', '*0*3', '*1*2'][randInt(3)]; - } - }; - - const randomTestChangeset = (origText, withAttribs) => { - const charBank = Changeset.stringAssembler(); - let textLeft = origText; // always keep final newline - const outTextAssem = Changeset.stringAssembler(); - const opAssem = Changeset.smartOpAssembler(); - const oldLen = origText.length; - - const nextOp = new Changeset.Op(); - - const appendMultilineOp = (opcode, txt) => { - nextOp.opcode = opcode; - if (withAttribs) { - nextOp.attribs = randomTwoPropAttribs(opcode); - } - txt.replace(/\n|[^\n]+/g, (t) => { - if (t === '\n') { - nextOp.chars = 1; - nextOp.lines = 1; - opAssem.append(nextOp); - } else { - nextOp.chars = t.length; - nextOp.lines = 0; - opAssem.append(nextOp); - } - return ''; - }); - }; - - const doOp = () => { - const o = randomStringOperation(textLeft.length); - if (o.insert) { - const txt = o.insert; - charBank.append(txt); - outTextAssem.append(txt); - appendMultilineOp('+', txt); - } else if (o.skip) { - const txt = textLeft.substring(0, o.skip); - textLeft = textLeft.substring(o.skip); - outTextAssem.append(txt); - appendMultilineOp('=', txt); - } else if (o.remove) { - const txt = textLeft.substring(0, o.remove); - textLeft = textLeft.substring(o.remove); - appendMultilineOp('-', txt); - } - }; - - while (textLeft.length > 1) doOp(); - for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen) - const outText = `${outTextAssem.toString()}\n`; - opAssem.endDocument(); - const cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString()); - Changeset.checkRep(cs); - return [cs, outText]; - }; - const testCompose = (randomSeed) => { it(`testCompose#${randomSeed}`, async function () { const p = new AttributePool();