test.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. var _ = require('lodash');
  2. var seedrandom = require('seedrandom');
  3. var diff = require('./diff.js');
  4. var ITERATIONS = 10000;
  5. var ALPHABET = 'GATTACA';
  6. var LENGTH = 100;
  7. var EMOJI_MAX_LENGTH = 50;
  8. var seed = Math.floor(Math.random() * 10000);
  9. var random = seedrandom(seed);
  10. console.log('Running regression tests...');
  11. [
  12. ['GAATAAAAAAAGATTAACAT', 'AAAAACTTGTAATTAACAAC'],
  13. ['๐Ÿ”˜๐Ÿค˜๐Ÿ”—๐Ÿ”—', '๐Ÿ”—๐Ÿค—๐Ÿค—__๐Ÿค—๐Ÿค˜๐Ÿค˜๐Ÿค—๐Ÿ”—๐Ÿค˜๐Ÿ”—'],
  14. ['๐Ÿ”—๐Ÿค—๐Ÿค—__๐Ÿค—๐Ÿค˜๐Ÿค˜๐Ÿค—๐Ÿ”—๐Ÿค˜๐Ÿ”—', '๐Ÿค—๐Ÿค˜๐Ÿ”˜'],
  15. ['๐Ÿค˜๐Ÿค˜๐Ÿ”˜๐Ÿ”˜_๐Ÿ”˜๐Ÿ”—๐Ÿค˜๐Ÿค—๐Ÿค—__๐Ÿ”—๐Ÿค˜', '๐Ÿค˜๐Ÿ”˜๐Ÿค˜๐Ÿ”—๐Ÿค˜๐Ÿค˜๐Ÿ”—๐Ÿค—๐Ÿค˜๐Ÿ”˜๐Ÿ”˜'],
  16. ['๐Ÿค—๐Ÿค˜๐Ÿค—๐Ÿ”˜๐Ÿค˜๐Ÿ”˜๐Ÿค—_๐Ÿค—๐Ÿ”—๐Ÿค˜๐Ÿค—_๐Ÿค˜๐Ÿ”—๐Ÿค—๐Ÿค˜๐Ÿ”—๐Ÿค˜๐Ÿค˜๐Ÿค˜๐Ÿ”—๐Ÿค—๐Ÿ”—๐Ÿ”—๐Ÿ”—๐Ÿค—_๐Ÿค˜๐Ÿ”—๐Ÿค—๐Ÿค—๐Ÿ”˜๐Ÿค—๐Ÿค—๐Ÿค˜๐Ÿค—',
  17. '_๐Ÿค—๐Ÿค˜_๐Ÿค˜๐Ÿค˜๐Ÿ”˜๐Ÿค—๐Ÿ”˜๐Ÿค˜_๐Ÿ”˜๐Ÿค—๐Ÿ”—๐Ÿ”˜๐Ÿ”—๐Ÿค˜๐Ÿ”—๐Ÿค˜๐Ÿค—๐Ÿ”—๐Ÿ”—๐Ÿ”—๐Ÿค˜๐Ÿ”˜_๐Ÿค—๐Ÿค˜๐Ÿค˜๐Ÿค˜__๐Ÿค˜_๐Ÿ”˜๐Ÿค˜๐Ÿค˜_๐Ÿ”—๐Ÿค˜๐Ÿ”˜'],
  18. ['๐Ÿ”—๐Ÿค˜๐Ÿค—๐Ÿ”˜๐Ÿ”˜๐Ÿค—', '๐Ÿค˜๐Ÿค˜๐Ÿค˜๐Ÿค—๐Ÿ”˜๐Ÿ”—๐Ÿ”—'],
  19. ['๐Ÿ”˜_๐Ÿ”—๐Ÿ”—๐Ÿ”—๐Ÿค—๐Ÿ”—', '๐Ÿค˜๐Ÿค—๐Ÿ”—๐Ÿค—_๐Ÿค˜๐Ÿ”˜_'],
  20. ].forEach(function (data) {
  21. var result = diff(data[0], data[1]);
  22. applyDiff(result, data[0], data[1]);
  23. });
  24. console.log('Running computing ' + ITERATIONS + ' diffs with seed ' + seed + '...');
  25. console.log('Generating strings...');
  26. var strings = [];
  27. for (var i = 0; i <= ITERATIONS; ++i) {
  28. var chars = [];
  29. for (var l = 0; l < LENGTH; ++l) {
  30. var letter = ALPHABET.substr(Math.floor(random() * ALPHABET.length), 1);
  31. chars.push(letter);
  32. }
  33. strings.push(chars.join(''));
  34. }
  35. console.log('Running fuzz tests *without* cursor information...');
  36. for (var i = 0; i < ITERATIONS; ++i) {
  37. var result = diff(strings[i], strings[i + 1]);
  38. applyDiff(result, strings[i], strings[i + 1]);
  39. }
  40. console.log('Running fuzz tests *with* cursor information');
  41. for (var i = 0; i < ITERATIONS; ++i) {
  42. var cursor_pos = Math.floor(random() * strings[i].length + 1);
  43. var diffs = diff(strings[i], strings[i + 1], cursor_pos);
  44. applyDiff(diffs, strings[i], strings[i + 1]);
  45. }
  46. function parseDiff(str) {
  47. if (!str) {
  48. return [];
  49. }
  50. return str.split(/(?=[+\-=])/).map(function (piece) {
  51. var symbol = piece.charAt(0);
  52. var text = piece.slice(1);
  53. return [
  54. symbol === '+' ? diff.INSERT : symbol === '-' ? diff.DELETE : diff.EQUAL,
  55. text
  56. ]
  57. });
  58. }
  59. console.log('Running cursor tests');
  60. [
  61. ['', 0, '', null, ''],
  62. ['', 0, 'a', null, '+a'],
  63. ['a', 0, 'aa', null, '+a=a'],
  64. ['a', 1, 'aa', null, '=a+a'],
  65. ['aa', 0, 'aaa', null, '+a=aa'],
  66. ['aa', 1, 'aaa', null, '=a+a=a'],
  67. ['aa', 2, 'aaa', null, '=aa+a'],
  68. ['aaa', 0, 'aaaa', null, '+a=aaa'],
  69. ['aaa', 1, 'aaaa', null, '=a+a=aa'],
  70. ['aaa', 2, 'aaaa', null, '=aa+a=a'],
  71. ['aaa', 3, 'aaaa', null, '=aaa+a'],
  72. ['a', 0, '', null, '-a'],
  73. ['a', 1, '', null, '-a'],
  74. ['aa', 0, 'a', null, '-a=a'],
  75. ['aa', 1, 'a', null, '-a=a'],
  76. ['aa', 2, 'a', null, '=a-a'],
  77. ['aaa', 0, 'aa', null, '-a=aa'],
  78. ['aaa', 1, 'aa', null, '-a=aa'],
  79. ['aaa', 2, 'aa', null, '=a-a=a'],
  80. ['aaa', 3, 'aa', null, '=aa-a'],
  81. ['', 0, '', 0, ''],
  82. ['', 0, 'a', 1, '+a'],
  83. ['a', 0, 'aa', 1, '+a=a'],
  84. ['a', 1, 'aa', 2, '=a+a'],
  85. ['aa', 0, 'aaa', 1, '+a=aa'],
  86. ['aa', 1, 'aaa', 2, '=a+a=a'],
  87. ['aa', 2, 'aaa', 3, '=aa+a'],
  88. ['aaa', 0, 'aaaa', 1, '+a=aaa'],
  89. ['aaa', 1, 'aaaa', 2, '=a+a=aa'],
  90. ['aaa', 2, 'aaaa', 3, '=aa+a=a'],
  91. ['aaa', 3, 'aaaa', 4, '=aaa+a'],
  92. ['a', 1, '', 0, '-a'],
  93. ['aa', 1, 'a', 0, '-a=a'],
  94. ['aa', 2, 'a', 1, '=a-a'],
  95. ['aaa', 1, 'aa', 0, '-a=aa'],
  96. ['aaa', 2, 'aa', 1, '=a-a=a'],
  97. ['aaa', 3, 'aa', 2, '=aa-a'],
  98. ['a', 1, '', 0, '-a'],
  99. ['aa', 1, 'a', 0, '-a=a'],
  100. ['aa', 2, 'a', 1, '=a-a'],
  101. ['aaa', 1, 'aa', 0, '-a=aa'],
  102. ['aaa', 2, 'aa', 1, '=a-a=a'],
  103. ['aaa', 3, 'aa', 2, '=aa-a'],
  104. // forward-delete
  105. ['a', 0, '', 0, '-a'],
  106. ['aa', 0, 'a', 0, '-a=a'],
  107. ['aa', 1, 'a', 1, '=a-a'],
  108. ['aaa', 0, 'aa', 0, '-a=aa'],
  109. ['aaa', 1, 'aa', 1, '=a-a=a'],
  110. ['aaa', 2, 'aa', 2, '=aa-a'],
  111. ['bob', 0, 'bobob', null, '+bo=bob'],
  112. ['bob', 1, 'bobob', null, '=b+ob=ob'],
  113. ['bob', 2, 'bobob', null, '=bo+bo=b'],
  114. ['bob', 3, 'bobob', null, '=bob+ob'],
  115. ['bob', 0, 'bobob', 2, '+bo=bob'],
  116. ['bob', 1, 'bobob', 3, '=b+ob=ob'],
  117. ['bob', 2, 'bobob', 4, '=bo+bo=b'],
  118. ['bob', 3, 'bobob', 5, '=bob+ob'],
  119. ['bobob', 2, 'bob', null, '-bo=bob'],
  120. ['bobob', 3, 'bob', null, '=b-ob=ob'],
  121. ['bobob', 4, 'bob', null, '=bo-bo=b'],
  122. ['bobob', 5, 'bob', null, '=bob-ob'],
  123. ['bobob', 2, 'bob', 0, '-bo=bob'],
  124. ['bobob', 3, 'bob', 1, '=b-ob=ob'],
  125. ['bobob', 4, 'bob', 2, '=bo-bo=b'],
  126. ['bobob', 5, 'bob', 3, '=bob-ob'],
  127. ['bob', 1, 'b', null, '=b-ob'],
  128. ['hello', [0, 5], 'h', 1, '-hello+h'],
  129. ['yay', [0, 3], 'y', 1, '-yay+y'],
  130. ['bobob', [1, 4], 'bob', 2, '=b-obo+o=b'],
  131. ].forEach(function (data) {
  132. var oldText = data[0];
  133. var newText = data[2];
  134. var oldRange = typeof data[1] === 'number' ?
  135. { index: data[1], length: 0 } :
  136. { index: data[1][0], length: data[1][1] - data[1][0] };
  137. var newRange = typeof data[3] === 'number' ?
  138. { index: data[3], length: 0 } :
  139. data[3] === null ? null : { index: data[3][0], length: data[3][1] - data[3][0] };
  140. var expected = parseDiff(data[4]);
  141. if (newRange === null && typeof data[1] !== 'number') {
  142. throw new Error('invalid test case');
  143. }
  144. var cursorInfo = newRange === null ? data[1] : {
  145. oldRange: oldRange,
  146. newRange: newRange,
  147. };
  148. doCursorTest(oldText, newText, cursorInfo, expected);
  149. doCursorTest('x' + oldText, 'x' + newText, shiftCursorInfo(cursorInfo, 1), diffPrepend(expected, 'x'));
  150. doCursorTest(oldText + 'x', newText + 'x', cursorInfo, diffAppend(expected, 'x'));
  151. });
  152. function diffPrepend(tuples, text) {
  153. if (tuples.length > 0 && tuples[0][0] === diff.EQUAL) {
  154. return [[diff.EQUAL, text + tuples[0][1]]].concat(tuples.slice(1));
  155. } else {
  156. return [[diff.EQUAL, text]].concat(tuples);
  157. }
  158. }
  159. function diffAppend(tuples, text) {
  160. var lastTuple = tuples[tuples.length - 1];
  161. if (lastTuple && lastTuple[0] === diff.EQUAL) {
  162. return tuples.slice(0, -1).concat([[diff.EQUAL, lastTuple[1] + text]]);
  163. } else {
  164. return tuples.concat([[diff.EQUAL, text]]);
  165. }
  166. }
  167. function shiftCursorInfo(cursorInfo, amount) {
  168. if (typeof cursorInfo === 'number') {
  169. return cursorInfo + amount;
  170. } else {
  171. return {
  172. oldRange: {
  173. index: cursorInfo.oldRange.index + amount,
  174. length: cursorInfo.oldRange.length,
  175. },
  176. newRange: {
  177. index: cursorInfo.newRange.index + amount,
  178. length: cursorInfo.newRange.length,
  179. },
  180. }
  181. }
  182. }
  183. function doCursorTest(oldText, newText, cursorInfo, expected) {
  184. var result = diff(oldText, newText, cursorInfo);
  185. if (!_.isEqual(result, expected)) {
  186. console.log([oldText, newText, cursorInfo]);
  187. console.log(result, '!==', expected);
  188. throw new Error('cursor test failed');
  189. }
  190. }
  191. console.log('Running emoji tests');
  192. [
  193. ['๐Ÿถ', '๐Ÿฏ', '-๐Ÿถ+๐Ÿฏ'],
  194. ['๐Ÿ‘จ๐Ÿฝ', '๐Ÿ‘ฉ๐Ÿฝ', '-๐Ÿ‘จ+๐Ÿ‘ฉ=๐Ÿฝ'],
  195. ['๐Ÿ‘ฉ๐Ÿผ', '๐Ÿ‘ฉ๐Ÿฝ', '=๐Ÿ‘ฉ-๐Ÿผ+๐Ÿฝ'],
  196. ['๐Ÿ๐ŸŽ', '๐ŸŽ', '-๐Ÿ=๐ŸŽ'],
  197. ['๐ŸŽ', '๐Ÿ๐ŸŽ', '+๐Ÿ=๐ŸŽ'],
  198. ].forEach(function (data) {
  199. var oldText = data[0];
  200. var newText = data[1];
  201. var expected = parseDiff(data[2]);
  202. doEmojiTest(oldText, newText, expected);
  203. doEmojiTest('x' + oldText, 'x' + newText, diffPrepend(expected, 'x'));
  204. doEmojiTest(oldText + 'x', newText + 'x', diffAppend(expected, 'x'));
  205. });
  206. function doEmojiTest(oldText, newText, expected) {
  207. var result = diff(oldText, newText);
  208. if (!_.isEqual(result, expected)) {
  209. console.log(oldText, newText, expected);
  210. console.log(result, '!==', expected);
  211. throw new Error('Emoji simple test case failed');
  212. }
  213. }
  214. // emojis chosen to share high and low surrogates!
  215. var EMOJI_ALPHABET = ['_', '๐Ÿค—', '๐Ÿ”—', '๐Ÿค˜', '๐Ÿ”˜'];
  216. console.log('Generating emoji strings...');
  217. var emoji_strings = [];
  218. for (var i = 0; i <= ITERATIONS; ++i) {
  219. var letters = [];
  220. var len = Math.floor(random() * EMOJI_MAX_LENGTH);
  221. for (var l = 0; l < len; ++l) {
  222. var letter = EMOJI_ALPHABET[Math.floor(random() * EMOJI_ALPHABET.length)];
  223. letters.push(letter);
  224. }
  225. emoji_strings.push(letters.join(''));
  226. }
  227. console.log('Running emoji fuzz tests...');
  228. for (var i = 0; i < ITERATIONS; ++i) {
  229. var oldText = emoji_strings[i];
  230. var newText = emoji_strings[i + 1];
  231. var result = diff(oldText, newText);
  232. applyDiff(result, oldText, newText);
  233. }
  234. // Applies a diff to text, throwing an error if diff is invalid or incorrect
  235. function applyDiff(diffs, text, expectedResult) {
  236. var pos = 0;
  237. function throwError(message) {
  238. console.log(diffs, text, expectedResult);
  239. throw new Error(message);
  240. }
  241. function expect(expected) {
  242. var found = text.substr(pos, expected.length);
  243. if (found !== expected) {
  244. throwError('Expected "' + expected + '", found "' + found + '"');
  245. }
  246. }
  247. var result = '';
  248. var inserts_since_last_equality = 0;
  249. var deletes_since_last_equality = 0;
  250. for (var i = 0; i < diffs.length; i++) {
  251. var d = diffs[i];
  252. if (!d[1]) {
  253. throwError('Empty tuple in diff')
  254. }
  255. var firstCharCode = d[1].charCodeAt(0);
  256. var lastCharCode = d[1].slice(-1).charCodeAt(0);
  257. if (firstCharCode >= 0xDC00 && firstCharCode <= 0xDFFF ||
  258. lastCharCode >= 0xD800 && lastCharCode <= 0xDBFF) {
  259. throwError('Bad unicode diff tuple')
  260. }
  261. switch (d[0]) {
  262. case diff.EQUAL:
  263. if (i !== 0 && !inserts_since_last_equality && !deletes_since_last_equality) {
  264. throwError('two consecutive equalities in diff');
  265. }
  266. inserts_since_last_equality = 0;
  267. deletes_since_last_equality = 0;
  268. expect(d[1]);
  269. result += d[1];
  270. pos += d[1].length;
  271. break;
  272. case diff.DELETE:
  273. if (deletes_since_last_equality) {
  274. throwError('multiple deletes between equalities')
  275. }
  276. if (inserts_since_last_equality) {
  277. throwError('delete following insert in diff')
  278. }
  279. deletes_since_last_equality++;
  280. expect(d[1]);
  281. pos += d[1].length;
  282. break
  283. case diff.INSERT:
  284. if (inserts_since_last_equality) {
  285. throwError('multiple inserts between equalities')
  286. }
  287. inserts_since_last_equality++;
  288. result += d[1];
  289. break;
  290. }
  291. }
  292. if (pos !== text.length) {
  293. throwError('Diff did not consume entire input text');
  294. }
  295. if (result !== expectedResult) {
  296. console.log(diffs, text, expectedResult, result);
  297. throw new Error('Diff not correct')
  298. }
  299. return result;
  300. }
  301. console.log("Success!");