      1 /**
      2  * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS-IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16
     17 var Changeset = require('./Changeset');
     18 var AttributePoolFactory = require("./AttributePoolFactory");
     19
     20 function random() {
     21   this.nextInt = function (maxValue) {
     22     return Math.floor(Math.random() * maxValue);
     23   }
     24
     25   this.nextDouble = function (maxValue) {
     26     return Math.random();
     27   }
     28 }
     29
     30 function runTests() {
     31
     32   function print(str) {
     33     console.log(str);
     34   }
     35
     36   function assert(code, optMsg) {
     37     if (!eval(code)) throw new Error("FALSE: " + (optMsg || code));
     38   }
     39
     40   function literal(v) {
     41     if ((typeof v) == "string") {
     42       return '"' + v.replace(/[\\\"]/g, '\\$1').replace(/\n/g, '\\n') + '"';
     43     } else
     44     return JSON.stringify(v);
     45   }
     46
     47   function assertEqualArrays(a, b) {
     48     assert("JSON.stringify(" + literal(a) + ") == JSON.stringify(" + literal(b) + ")");
     49   }
     50
     51   function assertEqualStrings(a, b) {
     52     assert(literal(a) + " == " + literal(b));
     53   }
     54
     55   function throughIterator(opsStr) {
     56     var iter = Changeset.opIterator(opsStr);
     57     var assem = Changeset.opAssembler();
     58     while (iter.hasNext()) {
     59       assem.append(iter.next());
     60     }
     61     return assem.toString();
     62   }
     63
     64   function throughSmartAssembler(opsStr) {
     65     var iter = Changeset.opIterator(opsStr);
     66     var assem = Changeset.smartOpAssembler();
     67     while (iter.hasNext()) {
     68       assem.append(iter.next());
     69     }
     70     assem.endDocument();
     71     return assem.toString();
     72   }
     73
     74   (function () {
     75     print("> throughIterator");
     76     var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
     77     assert("throughIterator(" + literal(x) + ") == " + literal(x));
     78   })();
     79
     80   (function () {
     81     print("> throughSmartAssembler");
     82     var x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
     83     assert("throughSmartAssembler(" + literal(x) + ") == " + literal(x));
     84   })();
     85
     86   function applyMutations(mu, arrayOfArrays) {
     87     arrayOfArrays.forEach(function (a) {
     88       var result = mu[a[0]].apply(mu, a.slice(1));
     89       if (a[0] == 'remove' && a[3]) {
     90         assertEqualStrings(a[3], result);
     91       }
     92     });
     93   }
     94
     95   function mutationsToChangeset(oldLen, arrayOfArrays) {
     96     var assem = Changeset.smartOpAssembler();
     97     var op = Changeset.newOp();
     98     var bank = Changeset.stringAssembler();
     99     var oldPos = 0;
    100     var newLen = 0;
    101     arrayOfArrays.forEach(function (a) {
    102       if (a[0] == 'skip') {
    103         op.opcode = '=';
    104         op.chars = a[1];
    105         op.lines = (a[2] || 0);
    106         assem.append(op);
    107         oldPos += op.chars;
    108         newLen += op.chars;
    109       } else if (a[0] == 'remove') {
    110         op.opcode = '-';
    111         op.chars = a[1];
    112         op.lines = (a[2] || 0);
    113         assem.append(op);
    114         oldPos += op.chars;
    115       } else if (a[0] == 'insert') {
    116         op.opcode = '+';
    117         bank.append(a[1]);
    118         op.chars = a[1].length;
    119         op.lines = (a[2] || 0);
    120         assem.append(op);
    121         newLen += op.chars;
    122       }
    123     });
    124     newLen += oldLen - oldPos;
    125     assem.endDocument();
    126     return Changeset.pack(oldLen, newLen, assem.toString(), bank.toString());
    127   }
    128
    129   function runMutationTest(testId, origLines, muts, correct) {
    130     print("> runMutationTest#" + testId);
    131     var lines = origLines.slice();
    132     var mu = Changeset.textLinesMutator(lines);
    133     applyMutations(mu, muts);
    134     mu.close();
    135     assertEqualArrays(correct, lines);
    136
    137     var inText = origLines.join('');
    138     var cs = mutationsToChangeset(inText.length, muts);
    139     lines = origLines.slice();
    140     Changeset.mutateTextLines(cs, lines);
    141     assertEqualArrays(correct, lines);
    142
    143     var correctText = correct.join('');
    144     //print(literal(cs));
    145     var outText = Changeset.applyToText(cs, inText);
    146     assertEqualStrings(correctText, outText);
    147   }
    148
    149   runMutationTest(1, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
    150     ['remove', 1, 0, "a"],
    151     ['insert', "tu"],
    152     ['remove', 1, 0, "p"],
    153     ['skip', 4, 1],
    154     ['skip', 7, 1],
    155     ['insert', "cream\npie\n", 2],
    156     ['skip', 2],
    157     ['insert', "bot"],
    158     ['insert', "\n", 1],
    159     ['insert', "bu"],
    160     ['skip', 3],
    161     ['remove', 3, 1, "ge\n"],
    162     ['remove', 6, 0, "duffle"]
    163   ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
    164
    165   runMutationTest(2, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
    166     ['remove', 1, 0, "a"],
    167     ['remove', 1, 0, "p"],
    168     ['insert', "tu"],
    169     ['skip', 11, 2],
    170     ['insert', "cream\npie\n", 2],
    171     ['skip', 2],
    172     ['insert', "bot"],
    173     ['insert', "\n", 1],
    174     ['insert', "bu"],
    175     ['skip', 3],
    176     ['remove', 3, 1, "ge\n"],
    177     ['remove', 6, 0, "duffle"]
    178   ], ["tuple\n", "banana\n", "cream\n", "pie\n", "cabot\n", "bubba\n", "eggplant\n"]);
    179
    180   runMutationTest(3, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
    181     ['remove', 6, 1, "apple\n"],
    182     ['skip', 15, 2],
    183     ['skip', 6],
    184     ['remove', 1, 1, "\n"],
    185     ['remove', 8, 0, "eggplant"],
    186     ['skip', 1, 1]
    187   ], ["banana\n", "cabbage\n", "duffle\n"]);
    188
    189   runMutationTest(4, ["15\n"], [
    190     ['skip', 1],
    191     ['insert', "\n2\n3\n4\n", 4],
    192     ['skip', 2, 1]
    193   ], ["1\n", "2\n", "3\n", "4\n", "5\n"]);
    194
    195   runMutationTest(5, ["1\n", "2\n", "3\n", "4\n", "5\n"], [
    196     ['skip', 1],
    197     ['remove', 7, 4, "\n2\n3\n4\n"],
    198     ['skip', 2, 1]
    199   ], ["15\n"]);
    200
    201   runMutationTest(6, ["123\n", "abc\n", "def\n", "ghi\n", "xyz\n"], [
    202     ['insert', "0"],
    203     ['skip', 4, 1],
    204     ['skip', 4, 1],
    205     ['remove', 8, 2, "def\nghi\n"],
    206     ['skip', 4, 1]
    207   ], ["0123\n", "abc\n", "xyz\n"]);
    208
    209   runMutationTest(7, ["apple\n", "banana\n", "cabbage\n", "duffle\n", "eggplant\n"], [
    210     ['remove', 6, 1, "apple\n"],
    211     ['skip', 15, 2, true],
    212     ['skip', 6, 0, true],
    213     ['remove', 1, 1, "\n"],
    214     ['remove', 8, 0, "eggplant"],
    215     ['skip', 1, 1, true]
    216   ], ["banana\n", "cabbage\n", "duffle\n"]);
    217
    218   function poolOrArray(attribs) {
    219     if (attribs.getAttrib) {
    220       return attribs; // it's already an attrib pool
    221     } else {
    222       // assume it's an array of attrib strings to be split and added
    223       var p = AttributePoolFactory.createAttributePool();
    224       attribs.forEach(function (kv) {
    225         p.putAttrib(kv.split(','));
    226       });
    227       return p;
    228     }
    229   }
    230
    231   function runApplyToAttributionTest(testId, attribs, cs, inAttr, outCorrect) {
    232     print("> applyToAttribution#" + testId);
    233     var p = poolOrArray(attribs);
    234     var result = Changeset.applyToAttribution(
    235     Changeset.checkRep(cs), inAttr, p);
    236     assertEqualStrings(outCorrect, result);
    237   }
    238
    239   // turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n
    240   runApplyToAttributionTest(1, ['bold,', 'bold,true'], "Z:7>3-1*0=1*1=1=3+4$abcd", "+1*1+1|1+5", "+1*1+1|1+8");
    241
    242   // turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n"
    243   runApplyToAttributionTest(2, ['bold,', 'bold,true'], "Z:g<4*1|1=6*1=5-4$", "|2+g", "*1|1+6*1+5|1+1");
    244
    245   (function () {
    246     print("> mutatorHasMore");
    247     var lines = ["1\n", "2\n", "3\n", "4\n"];
    248     var mu;
    249
    250     mu = Changeset.textLinesMutator(lines);
    251     assert(mu.hasMore() + ' == true');
    252     mu.skip(8, 4);
    253     assert(mu.hasMore() + ' == false');
    254     mu.close();
    255     assert(mu.hasMore() + ' == false');
    256
    257     // still 1,2,3,4
    258     mu = Changeset.textLinesMutator(lines);
    259     assert(mu.hasMore() + ' == true');
    260     mu.remove(2, 1);
    261     assert(mu.hasMore() + ' == true');
    262     mu.skip(2, 1);
    263     assert(mu.hasMore() + ' == true');
    264     mu.skip(2, 1);
    265     assert(mu.hasMore() + ' == true');
    266     mu.skip(2, 1);
    267     assert(mu.hasMore() + ' == false');
    268     mu.insert("5\n", 1);
    269     assert(mu.hasMore() + ' == false');
    270     mu.close();
    271     assert(mu.hasMore() + ' == false');
    272
    273     // 2,3,4,5 now
    274     mu = Changeset.textLinesMutator(lines);
    275     assert(mu.hasMore() + ' == true');
    276     mu.remove(6, 3);
    277     assert(mu.hasMore() + ' == true');
    278     mu.remove(2, 1);
    279     assert(mu.hasMore() + ' == false');
    280     mu.insert("hello\n", 1);
    281     assert(mu.hasMore() + ' == false');
    282     mu.close();
    283     assert(mu.hasMore() + ' == false');
    284
    285   })();
    286
    287   function runMutateAttributionTest(testId, attribs, cs, alines, outCorrect) {
    288     print("> runMutateAttributionTest#" + testId);
    289     var p = poolOrArray(attribs);
    290     var alines2 = Array.prototype.slice.call(alines);
    291     var result = Changeset.mutateAttributionLines(
    292     Changeset.checkRep(cs), alines2, p);
    293     assertEqualArrays(outCorrect, alines2);
    294
    295     print("> runMutateAttributionTest#" + testId + ".applyToAttribution");
    296
    297     function removeQuestionMarks(a) {
    298       return a.replace(/\?/g, '');
    299     }
    300     var inMerged = Changeset.joinAttributionLines(alines.map(removeQuestionMarks));
    301     var correctMerged = Changeset.joinAttributionLines(outCorrect.map(removeQuestionMarks));
    302     var mergedResult = Changeset.applyToAttribution(cs, inMerged, p);
    303     assertEqualStrings(correctMerged, mergedResult);
    304   }
    305
    306   // turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n
    307   runMutateAttributionTest(1, ["bold,true"], "Z:c>0|1=4=1*0=1$", ["|1+4", "|1+4", "|1+4"], ["|1+4", "+1*0+1|1+2", "|1+4"]);
    308
    309   // make a document bold
    310   runMutateAttributionTest(2, ["bold,true"], "Z:c>0*0|3=c$", ["|1+4", "|1+4", "|1+4"], ["*0|1+4", "*0|1+4", "*0|1+4"]);
    311
    312   // clear bold on document
    313   runMutateAttributionTest(3, ["bold,", "bold,true"], "Z:c>0*0|3=c$", ["*1+1+1*1+1|1+1", "+1*1+1|1+2", "*1+1+1*1+1|1+1"], ["|1+4", "|1+4", "|1+4"]);
    314
    315   // add a character on line 3 of a document with 5 blank lines, and make sure
    316   // the optimization that skips purely-kept lines is working; if any attribution string
    317   // with a '?' is parsed it will cause an error.
    318   runMutateAttributionTest(4, ['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'], "Z:5>1|2=2+1$x", ["?*1|1+1", "?*2|1+1", "*3|1+1", "?*4|1+1", "?*5|1+1"], ["?*1|1+1", "?*2|1+1", "+1*3|1+1", "?*4|1+1", "?*5|1+1"]);
    319
    320   var testPoolWithChars = (function () {
    321     var p = AttributePoolFactory.createAttributePool();
    322     p.putAttrib(['char', 'newline']);
    323     for (var i = 1; i < 36; i++) {
    324       p.putAttrib(['char', Changeset.numToString(i)]);
    325     }
    326     p.putAttrib(['char', '']);
    327     return p;
    328   })();
    329
    330   // based on runMutationTest#1
    331   runMutateAttributionTest(5, testPoolWithChars, "Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$" + "tucream\npie\nbot\nbu", ["*a+1*p+2*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1", "*d+1*u+1*f+2*l+1*e+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"], ["*t+1*u+1*p+1*l+1*e+1*0|1+1", "*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1", "|1+6", "|1+4", "*c+1*a+1*b+1*o+1*t+1*0|1+1", "*b+1*u+1*b+2*a+1*0|1+1", "*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1"]);
    332
    333   // based on runMutationTest#3
    334   runMutateAttributionTest(6, testPoolWithChars, "Z:11<f|1-6|2=f=6|1-1-8$", ["*a|1+6", "*b|1+7", "*c|1+8", "*d|1+7", "*e|1+9"], ["*b|1+7", "*c|1+8", "*d+6*e|1+1"]);
    335
    336   // based on runMutationTest#4
    337   runMutateAttributionTest(7, testPoolWithChars, "Z:3>7=1|4+7$\n2\n3\n4\n", ["*1+1*5|1+2"], ["*1+1|1+1", "|1+2", "|1+2", "|1+2", "*5|1+2"]);
    338
    339   // based on runMutationTest#5
    340   runMutateAttributionTest(8, testPoolWithChars, "Z:a<7=1|4-7$", ["*1|1+2", "*2|1+2", "*3|1+2", "*4|1+2", "*5|1+2"], ["*1+1*5|1+2"]);
    341
    342   // based on runMutationTest#6
    343   runMutateAttributionTest(9, testPoolWithChars, "Z:k<7*0+1*10|2=8|2-8$0", ["*1+1*2+1*3+1|1+1", "*a+1*b+1*c+1|1+1", "*d+1*e+1*f+1|1+1", "*g+1*h+1*i+1|1+1", "?*x+1*y+1*z+1|1+1"], ["*0+1|1+4", "|1+4", "?*x+1*y+1*z+1|1+1"]);
    344
    345   runMutateAttributionTest(10, testPoolWithChars, "Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd", ["|1+3", "|1+3"], ["|1+5", "+2*0+1|1+2"]);
    346
    347
    348   runMutateAttributionTest(11, testPoolWithChars, "Z:s>1|1=4=6|1+1$\n", ["*0|1+4", "*0|1+8", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"], ["*0|1+4", "*0+6|1+1", "*0|1+2", "*0+5|1+1", "*0|1+1", "*0|1+5", "*0|1+1", "*0|1+1", "*0|1+1", "|1+1"]);
    349
    350   function randomInlineString(len, rand) {
    351     var assem = Changeset.stringAssembler();
    352     for (var i = 0; i < len; i++) {
    353       assem.append(String.fromCharCode(rand.nextInt(26) + 97));
    354     }
    355     return assem.toString();
    356   }
    357
    358   function randomMultiline(approxMaxLines, approxMaxCols, rand) {
    359     var numParts = rand.nextInt(approxMaxLines * 2) + 1;
    360     var txt = Changeset.stringAssembler();
    361     txt.append(rand.nextInt(2) ? '\n' : '');
    362     for (var i = 0; i < numParts; i++) {
    363       if ((i % 2) == 0) {
    364         if (rand.nextInt(10)) {
    365           txt.append(randomInlineString(rand.nextInt(approxMaxCols) + 1, rand));
    366         } else {
    367           txt.append('\n');
    368         }
    369       } else {
    370         txt.append('\n');
    371       }
    372     }
    373     return txt.toString();
    374   }
    375
    376   function randomStringOperation(numCharsLeft, rand) {
    377     var result;
    378     switch (rand.nextInt(9)) {
    379     case 0:
    380       {
    381         // insert char
    382         result = {
    383           insert: randomInlineString(1, rand)
    384         };
    385         break;
    386       }
    387     case 1:
    388       {
    389         // delete char
    390         result = {
    391           remove: 1
    392         };
    393         break;
    394       }
    395     case 2:
    396       {
    397         // skip char
    398         result = {
    399           skip: 1
    400         };
    401         break;
    402       }
    403     case 3:
    404       {
    405         // insert small
    406         result = {
    407           insert: randomInlineString(rand.nextInt(4) + 1, rand)
    408         };
    409         break;
    410       }
    411     case 4:
    412       {
    413         // delete small
    414         result = {
    415           remove: rand.nextInt(4) + 1
    416         };
    417         break;
    418       }
    419     case 5:
    420       {
    421         // skip small
    422         result = {
    423           skip: rand.nextInt(4) + 1
    424         };
    425         break;
    426       }
    427     case 6:
    428       {
    429         // insert multiline;
    430         result = {
    431           insert: randomMultiline(5, 20, rand)
    432         };
    433         break;
    434       }
    435     case 7:
    436       {
    437         // delete multiline
    438         result = {
    439           remove: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
    440         };
    441         break;
    442       }
    443     case 8:
    444       {
    445         // skip multiline
    446         result = {
    447           skip: Math.round(numCharsLeft * rand.nextDouble() * rand.nextDouble())
    448         };
    449         break;
    450       }
    451     case 9:
    452       {
    453         // delete to end
    454         result = {
    455           remove: numCharsLeft
    456         };
    457         break;
    458       }
    459     case 10:
    460       {
    461         // skip to end
    462         result = {
    463           skip: numCharsLeft
    464         };
    465         break;
    466       }
    467     }
    468     var maxOrig = numCharsLeft - 1;
    469     if ('remove' in result) {
    470       result.remove = Math.min(result.remove, maxOrig);
    471     } else if ('skip' in result) {
    472       result.skip = Math.min(result.skip, maxOrig);
    473     }
    474     return result;
    475   }
    476
    477   function randomTwoPropAttribs(opcode, rand) {
    478     // assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
    479     if (opcode == '-' || rand.nextInt(3)) {
    480       return '';
    481     } else if (rand.nextInt(3)) {
    482       if (opcode == '+' || rand.nextInt(2)) {
    483         return '*' + Changeset.numToString(rand.nextInt(2) * 2 + 1);
    484       } else {
    485         return '*' + Changeset.numToString(rand.nextInt(2) * 2);
    486       }
    487     } else {
    488       if (opcode == '+' || rand.nextInt(4) == 0) {
    489         return '*1*3';
    490       } else {
    491         return ['*0*2', '*0*3', '*1*2'][rand.nextInt(3)];
    492       }
    493     }
    494   }
    495
    496   function randomTestChangeset(origText, rand, withAttribs) {
    497     var charBank = Changeset.stringAssembler();
    498     var textLeft = origText; // always keep final newline
    499     var outTextAssem = Changeset.stringAssembler();
    500     var opAssem = Changeset.smartOpAssembler();
    501     var oldLen = origText.length;
    502
    503     var nextOp = Changeset.newOp();
    504
    505     function appendMultilineOp(opcode, txt) {
    506       nextOp.opcode = opcode;
    507       if (withAttribs) {
    508         nextOp.attribs = randomTwoPropAttribs(opcode, rand);
    509       }
    510       txt.replace(/\n|[^\n]+/g, function (t) {
    511         if (t == '\n') {
    512           nextOp.chars = 1;
    513           nextOp.lines = 1;
    514           opAssem.append(nextOp);
    515         } else {
    516           nextOp.chars = t.length;
    517           nextOp.lines = 0;
    518           opAssem.append(nextOp);
    519         }
    520         return '';
    521       });
    522     }
    523
    524     function doOp() {
    525       var o = randomStringOperation(textLeft.length, rand);
    526       if (o.insert) {
    527         var txt = o.insert;
    528         charBank.append(txt);
    529         outTextAssem.append(txt);
    530         appendMultilineOp('+', txt);
    531       } else if (o.skip) {
    532         var txt = textLeft.substring(0, o.skip);
    533         textLeft = textLeft.substring(o.skip);
    534         outTextAssem.append(txt);
    535         appendMultilineOp('=', txt);
    536       } else if (o.remove) {
    537         var txt = textLeft.substring(0, o.remove);
    538         textLeft = textLeft.substring(o.remove);
    539         appendMultilineOp('-', txt);
    540       }
    541     }
    542
    543     while (textLeft.length > 1) doOp();
    544     for (var i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
    545     var outText = outTextAssem.toString() + '\n';
    546     opAssem.endDocument();
    547     var cs = Changeset.pack(oldLen, outText.length, opAssem.toString(), charBank.toString());
    548     Changeset.checkRep(cs);
    549     return [cs, outText];
    550   }
    551
    552   function testCompose(randomSeed) {
    553     var rand = new random();
    554     print("> testCompose#" + randomSeed);
    555
    556     var p = AttributePoolFactory.createAttributePool();
    557
    558     var startText = randomMultiline(10, 20, rand) + '\n';
    559
    560     var x1 = randomTestChangeset(startText, rand);
    561     var change1 = x1[0];
    562     var text1 = x1[1];
    563
    564     var x2 = randomTestChangeset(text1, rand);
    565     var change2 = x2[0];
    566     var text2 = x2[1];
    567
    568     var x3 = randomTestChangeset(text2, rand);
    569     var change3 = x3[0];
    570     var text3 = x3[1];
    571
    572     //print(literal(Changeset.toBaseTen(startText)));
    573     //print(literal(Changeset.toBaseTen(change1)));
    574     //print(literal(Changeset.toBaseTen(change2)));
    575     var change12 = Changeset.checkRep(Changeset.compose(change1, change2, p));
    576     var change23 = Changeset.checkRep(Changeset.compose(change2, change3, p));
    577     var change123 = Changeset.checkRep(Changeset.compose(change12, change3, p));
    578     var change123a = Changeset.checkRep(Changeset.compose(change1, change23, p));
    579     assertEqualStrings(change123, change123a);
    580
    581     assertEqualStrings(text2, Changeset.applyToText(change12, startText));
    582     assertEqualStrings(text3, Changeset.applyToText(change23, text1));
    583     assertEqualStrings(text3, Changeset.applyToText(change123, startText));
    584   }
    585
    586   for (var i = 0; i < 30; i++) testCompose(i);
    587
    588   (function simpleComposeAttributesTest() {
    589     print("> simpleComposeAttributesTest");
    590     var p = AttributePoolFactory.createAttributePool();
    591     p.putAttrib(['bold', '']);
    592     p.putAttrib(['bold', 'true']);
    593     var cs1 = Changeset.checkRep("Z:2>1*1+1*1=1$x");
    594     var cs2 = Changeset.checkRep("Z:3>0*0|1=3$");
    595     var cs12 = Changeset.checkRep(Changeset.compose(cs1, cs2, p));
    596     assertEqualStrings("Z:2>1+1*0|1=2$x", cs12);
    597   })();
    598
    599   (function followAttributesTest() {
    600     var p = AttributePoolFactory.createAttributePool();
    601     p.putAttrib(['x', '']);
    602     p.putAttrib(['x', 'abc']);
    603     p.putAttrib(['x', 'def']);
    604     p.putAttrib(['y', '']);
    605     p.putAttrib(['y', 'abc']);
    606     p.putAttrib(['y', 'def']);
    607
    608     function testFollow(a, b, afb, bfa, merge) {
    609       assertEqualStrings(afb, Changeset.followAttributes(a, b, p));
    610       assertEqualStrings(bfa, Changeset.followAttributes(b, a, p));
    611       assertEqualStrings(merge, Changeset.composeAttributes(a, afb, true, p));
    612       assertEqualStrings(merge, Changeset.composeAttributes(b, bfa, true, p));
    613     }
    614
    615     testFollow('', '', '', '', '');
    616     testFollow('*0', '', '', '*0', '*0');
    617     testFollow('*0', '*0', '', '', '*0');
    618     testFollow('*0', '*1', '', '*0', '*0');
    619     testFollow('*1', '*2', '', '*1', '*1');
    620     testFollow('*0*1', '', '', '*0*1', '*0*1');
    621     testFollow('*0*4', '*2*3', '*3', '*0', '*0*3');
    622     testFollow('*0*4', '*2', '', '*0*4', '*0*4');
    623   })();
    624
    625   function testFollow(randomSeed) {
    626     var rand = new random();
    627     print("> testFollow#" + randomSeed);
    628
    629     var p = AttributePoolFactory.createAttributePool();
    630
    631     var startText = randomMultiline(10, 20, rand) + '\n';
    632
    633     var cs1 = randomTestChangeset(startText, rand)[0];
    634     var cs2 = randomTestChangeset(startText, rand)[0];
    635
    636     var afb = Changeset.checkRep(Changeset.follow(cs1, cs2, false, p));
    637     var bfa = Changeset.checkRep(Changeset.follow(cs2, cs1, true, p));
    638
    639     var merge1 = Changeset.checkRep(Changeset.compose(cs1, afb));
    640     var merge2 = Changeset.checkRep(Changeset.compose(cs2, bfa));
    641
    642     assertEqualStrings(merge1, merge2);
    643   }
    644
    645   for (var i = 0; i < 30; i++) testFollow(i);
    646
    647   function testSplitJoinAttributionLines(randomSeed) {
    648     var rand = new random();
    649     print("> testSplitJoinAttributionLines#" + randomSeed);
    650
    651     var doc = randomMultiline(10, 20, rand) + '\n';
    652
    653     function stringToOps(str) {
    654       var assem = Changeset.mergingOpAssembler();
    655       var o = Changeset.newOp('+');
    656       o.chars = 1;
    657       for (var i = 0; i < str.length; i++) {
    658         var c = str.charAt(i);
    659         o.lines = (c == '\n' ? 1 : 0);
    660         o.attribs = (c == 'a' || c == 'b' ? '*' + c : '');
    661         assem.append(o);
    662       }
    663       return assem.toString();
    664     }
    665
    666     var theJoined = stringToOps(doc);
    667     var theSplit = doc.match(/[^\n]*\n/g).map(stringToOps);
    668
    669     assertEqualArrays(theSplit, Changeset.splitAttributionLines(theJoined, doc));
    670     assertEqualStrings(theJoined, Changeset.joinAttributionLines(theSplit));
    671   }
    672
    673   for (var i = 0; i < 10; i++) testSplitJoinAttributionLines(i);
    674
    675   (function testMoveOpsToNewPool() {
    676     print("> testMoveOpsToNewPool");
    677
    678     var pool1 = AttributePoolFactory.createAttributePool();
    679     var pool2 = AttributePoolFactory.createAttributePool();
    680
    681     pool1.putAttrib(['baz', 'qux']);
    682     pool1.putAttrib(['foo', 'bar']);
    683
    684     pool2.putAttrib(['foo', 'bar']);
    685
    686     assertEqualStrings(Changeset.moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2), 'Z:1>2*0+1*1+1$ab');
    687     assertEqualStrings(Changeset.moveOpsToNewPool('*1+1*0+1', pool1, pool2), '*0+1*1+1');
    688   })();
    689
    690
    691   (function testMakeSplice() {
    692     print("> testMakeSplice");
    693
    694     var t = "a\nb\nc\n";
    695     var t2 = Changeset.applyToText(Changeset.makeSplice(t, 5, 0, "def"), t);
    696     assertEqualStrings("a\nb\ncdef\n", t2);
    697
    698   })();
    699
    700   (function testToSplices() {
    701     print("> testToSplices");
    702
    703     var cs = Changeset.checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk');
    704     var correctSplices = [
    705       [5, 8, "123456789"],
    706       [9, 17, "abcdefghijk"]
    707     ];
    708     assertEqualArrays(correctSplices, Changeset.toSplices(cs));
    709   })();
    710
    711   function testCharacterRangeFollow(testId, cs, oldRange, insertionsAfter, correctNewRange) {
    712     print("> testCharacterRangeFollow#" + testId);
    713
    714     var cs = Changeset.checkRep(cs);
    715     assertEqualArrays(correctNewRange, Changeset.characterRangeFollow(cs, oldRange[0], oldRange[1], insertionsAfter));
    716
    717   }
    718
    719   testCharacterRangeFollow(1, 'Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk', [7, 10], false, [14, 15]);
    720   testCharacterRangeFollow(2, "Z:bc<6|x=b4|2-6$", [400, 407], false, [400, 401]);
    721   testCharacterRangeFollow(3, "Z:4>0-3+3$abc", [0, 3], false, [3, 3]);
    722   testCharacterRangeFollow(4, "Z:4>0-3+3$abc", [0, 3], true, [0, 0]);
    723   testCharacterRangeFollow(5, "Z:5>1+1=1-3+3$abcd", [1, 4], false, [5, 5]);
    724   testCharacterRangeFollow(6, "Z:5>1+1=1-3+3$abcd", [1, 4], true, [2, 2]);
    725   testCharacterRangeFollow(7, "Z:5>1+1=1-3+3$abcd", [0, 6], false, [1, 7]);
    726   testCharacterRangeFollow(8, "Z:5>1+1=1-3+3$abcd", [0, 3], false, [1, 2]);
    727   testCharacterRangeFollow(9, "Z:5>1+1=1-3+3$abcd", [2, 5], false, [5, 6]);
    728   testCharacterRangeFollow(10, "Z:2>1+1$a", [0, 0], false, [1, 1]);
    729   testCharacterRangeFollow(11, "Z:2>1+1$a", [0, 0], true, [0, 0]);
    730
    731   (function testOpAttributeValue() {
    732     print("> testOpAttributeValue");
    733
    734     var p = AttributePoolFactory.createAttributePool();
    735     p.putAttrib(['name', 'david']);
    736     p.putAttrib(['color', 'green']);
    737
    738     assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'name', p));
    739     assertEqualStrings("david", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'name', p));
    740     assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'name', p));
    741     assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'name', p));
    742     assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*0*1+1'), 'color', p));
    743     assertEqualStrings("green", Changeset.opAttributeValue(Changeset.stringOp('*1+1'), 'color', p));
    744     assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('*0+1'), 'color', p));
    745     assertEqualStrings("", Changeset.opAttributeValue(Changeset.stringOp('+1'), 'color', p));
    746   })();
    747
    748   function testAppendATextToAssembler(testId, atext, correctOps) {
    749     print("> testAppendATextToAssembler#" + testId);
    750
    751     var assem = Changeset.smartOpAssembler();
    752     Changeset.appendATextToAssembler(atext, assem);
    753     assertEqualStrings(correctOps, assem.toString());
    754   }
    755
    756   testAppendATextToAssembler(1, {
    757     text: "\n",
    758     attribs: "|1+1"
    759   }, "");
    760   testAppendATextToAssembler(2, {
    761     text: "\n\n",
    762     attribs: "|2+2"
    763   }, "|1+1");
    764   testAppendATextToAssembler(3, {
    765     text: "\n\n",
    766     attribs: "*x|2+2"
    767   }, "*x|1+1");
    768   testAppendATextToAssembler(4, {
    769     text: "\n\n",
    770     attribs: "*x|1+1|1+1"
    771   }, "*x|1+1");
    772   testAppendATextToAssembler(5, {
    773     text: "foo\n",
    774     attribs: "|1+4"
    775   }, "+3");
    776   testAppendATextToAssembler(6, {
    777     text: "\nfoo\n",
    778     attribs: "|2+5"
    779   }, "|1+1+3");
    780   testAppendATextToAssembler(7, {
    781     text: "\nfoo\n",
    782     attribs: "*x|2+5"
    783   }, "*x|1+1*x+3");
    784   testAppendATextToAssembler(8, {
    785     text: "\n\n\nfoo\n",
    786     attribs: "|2+2*x|2+5"
    787   }, "|2+2*x|1+1*x+3");
    788
    789   function testMakeAttribsString(testId, pool, opcode, attribs, correctString) {
    790     print("> testMakeAttribsString#" + testId);
    791
    792     var p = poolOrArray(pool);
    793     var str = Changeset.makeAttribsString(opcode, attribs, p);
    794     assertEqualStrings(correctString, str);
    795   }
    796
    797   testMakeAttribsString(1, ['bold,'], '+', [
    798     ['bold', '']
    799   ], '');
    800   testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [
    801     ['bold', '']
    802   ], '*1');
    803   testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [
    804     ['abc', 'def'],
    805     ['bold', 'true']
    806   ], '*0*1');
    807   testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [
    808     ['bold', 'true'],
    809     ['abc', 'def']
    810   ], '*0*1');
    811
    812   function testSubattribution(testId, astr, start, end, correctOutput) {
    813     print("> testSubattribution#" + testId);
    814
    815     var str = Changeset.subattribution(astr, start, end);
    816     assertEqualStrings(correctOutput, str);
    817   }
    818
    819   testSubattribution(1, "+1", 0, 0, "");
    820   testSubattribution(2, "+1", 0, 1, "+1");
    821   testSubattribution(3, "+1", 0, undefined, "+1");
    822   testSubattribution(4, "|1+1", 0, 0, "");
    823   testSubattribution(5, "|1+1", 0, 1, "|1+1");
    824   testSubattribution(6, "|1+1", 0, undefined, "|1+1");
    825   testSubattribution(7, "*0+1", 0, 0, "");
    826   testSubattribution(8, "*0+1", 0, 1, "*0+1");
    827   testSubattribution(9, "*0+1", 0, undefined, "*0+1");
    828   testSubattribution(10, "*0|1+1", 0, 0, "");
    829   testSubattribution(11, "*0|1+1", 0, 1, "*0|1+1");
    830   testSubattribution(12, "*0|1+1", 0, undefined, "*0|1+1");
    831   testSubattribution(13, "*0+2+1*1+3", 0, 1, "*0+1");
    832   testSubattribution(14, "*0+2+1*1+3", 0, 2, "*0+2");
    833   testSubattribution(15, "*0+2+1*1+3", 0, 3, "*0+2+1");
    834   testSubattribution(16, "*0+2+1*1+3", 0, 4, "*0+2+1*1+1");
    835   testSubattribution(17, "*0+2+1*1+3", 0, 5, "*0+2+1*1+2");
    836   testSubattribution(18, "*0+2+1*1+3", 0, 6, "*0+2+1*1+3");
    837   testSubattribution(19, "*0+2+1*1+3", 0, 7, "*0+2+1*1+3");
    838   testSubattribution(20, "*0+2+1*1+3", 0, undefined, "*0+2+1*1+3");
    839   testSubattribution(21, "*0+2+1*1+3", 1, undefined, "*0+1+1*1+3");
    840   testSubattribution(22, "*0+2+1*1+3", 2, undefined, "+1*1+3");
    841   testSubattribution(23, "*0+2+1*1+3", 3, undefined, "*1+3");
    842   testSubattribution(24, "*0+2+1*1+3", 4, undefined, "*1+2");
    843   testSubattribution(25, "*0+2+1*1+3", 5, undefined, "*1+1");
    844   testSubattribution(26, "*0+2+1*1+3", 6, undefined, "");
    845   testSubattribution(27, "*0+2+1*1|1+3", 0, 1, "*0+1");
    846   testSubattribution(28, "*0+2+1*1|1+3", 0, 2, "*0+2");
    847   testSubattribution(29, "*0+2+1*1|1+3", 0, 3, "*0+2+1");
    848   testSubattribution(30, "*0+2+1*1|1+3", 0, 4, "*0+2+1*1+1");
    849   testSubattribution(31, "*0+2+1*1|1+3", 0, 5, "*0+2+1*1+2");
    850   testSubattribution(32, "*0+2+1*1|1+3", 0, 6, "*0+2+1*1|1+3");
    851   testSubattribution(33, "*0+2+1*1|1+3", 0, 7, "*0+2+1*1|1+3");
    852   testSubattribution(34, "*0+2+1*1|1+3", 0, undefined, "*0+2+1*1|1+3");
    853   testSubattribution(35, "*0+2+1*1|1+3", 1, undefined, "*0+1+1*1|1+3");
    854   testSubattribution(36, "*0+2+1*1|1+3", 2, undefined, "+1*1|1+3");
    855   testSubattribution(37, "*0+2+1*1|1+3", 3, undefined, "*1|1+3");
    856   testSubattribution(38, "*0+2+1*1|1+3", 4, undefined, "*1|1+2");
    857   testSubattribution(39, "*0+2+1*1|1+3", 5, undefined, "*1|1+1");
    858   testSubattribution(40, "*0+2+1*1|1+3", 1, 5, "*0+1+1*1+2");
    859   testSubattribution(41, "*0+2+1*1|1+3", 2, 6, "+1*1|1+3");
    860   testSubattribution(42, "*0+2+1*1+3", 2, 6, "+1*1+3");
    861
    862   function testFilterAttribNumbers(testId, cs, filter, correctOutput) {
    863     print("> testFilterAttribNumbers#" + testId);
    864
    865     var str = Changeset.filterAttribNumbers(cs, filter);
    866     assertEqualStrings(correctOutput, str);
    867   }
    868
    869   testFilterAttribNumbers(1, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
    870     return (n % 2) == 0;
    871   }, "*0+1+2+3+4*2+5*0*2*c+6");
    872   testFilterAttribNumbers(2, "*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6", function (n) {
    873     return (n % 2) == 1;
    874   }, "*1+1+2+3*1+4+5*1*b+6");
    875
    876   function testInverse(testId, cs, lines, alines, pool, correctOutput) {
    877     print("> testInverse#" + testId);
    878
    879     pool = poolOrArray(pool);
    880     var str = Changeset.inverse(Changeset.checkRep(cs), lines, alines, pool);
    881     assertEqualStrings(correctOutput, str);
    882   }
    883
    884   // take "FFFFTTTTT" and apply "-FT--FFTT", the inverse of which is "--F--TT--"
    885   testInverse(1, "Z:9>0=1*0=1*1=1=2*0=2*1|1=2$", null, ["+4*1+5"], ['bold,', 'bold,true'], "Z:9>0=2*0=1=2*1=2$");
    886
    887   function testMutateTextLines(testId, cs, lines, correctLines) {
    888     print("> testMutateTextLines#" + testId);
    889
    890     var a = lines.slice();
    891     Changeset.mutateTextLines(cs, a);
    892     assertEqualArrays(correctLines, a);
    893   }
    894
    895   testMutateTextLines(1, "Z:4<1|1-2-1|1+1+1$\nc", ["a\n", "b\n"], ["\n", "c\n"]);
    896   testMutateTextLines(2, "Z:4>0|1-2-1|2+3$\nc\n", ["a\n", "b\n"], ["\n", "c\n", "\n"]);
    897
    898   function testInverseRandom(randomSeed) {
    899     var rand = new random();
    900     print("> testInverseRandom#" + randomSeed);
    901
    902     var p = poolOrArray(['apple,', 'apple,true', 'banana,', 'banana,true']);
    903
    904     var startText = randomMultiline(10, 20, rand) + '\n';
    905     var alines = Changeset.splitAttributionLines(Changeset.makeAttribution(startText), startText);
    906     var lines = startText.slice(0, -1).split('\n').map(function (s) {
    907       return s + '\n';
    908     });
    909
    910     var stylifier = randomTestChangeset(startText, rand, true)[0];
    911
    912     //print(alines.join('\n'));
    913     Changeset.mutateAttributionLines(stylifier, alines, p);
    914     //print(stylifier);
    915     //print(alines.join('\n'));
    916     Changeset.mutateTextLines(stylifier, lines);
    917
    918     var changeset = randomTestChangeset(lines.join(''), rand, true)[0];
    919     var inverseChangeset = Changeset.inverse(changeset, lines, alines, p);
    920
    921     var origLines = lines.slice();
    922     var origALines = alines.slice();
    923
    924     Changeset.mutateTextLines(changeset, lines);
    925     Changeset.mutateAttributionLines(changeset, alines, p);
    926     //print(origALines.join('\n'));
    927     //print(changeset);
    928     //print(inverseChangeset);
    929     //print(origLines.map(function(s) { return '1: '+s.slice(0,-1); }).join('\n'));
    930     //print(lines.map(function(s) { return '2: '+s.slice(0,-1); }).join('\n'));
    931     //print(alines.join('\n'));
    932     Changeset.mutateTextLines(inverseChangeset, lines);
    933     Changeset.mutateAttributionLines(inverseChangeset, alines, p);
    934     //print(lines.map(function(s) { return '3: '+s.slice(0,-1); }).join('\n'));
    935     assertEqualArrays(origLines, lines);
    936     assertEqualArrays(origALines, alines);
    937   }
    938
    939   for (var i = 0; i < 30; i++) testInverseRandom(i);
    940 }
    941
    942 runTests();
    943