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