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