test.js 10.2 KB
var _ = require('lodash');
var seedrandom = require('seedrandom');
var diff = require('./diff.js');

var ITERATIONS = 10000;
var ALPHABET = 'GATTACA';
var LENGTH = 100;
var EMOJI_MAX_LENGTH = 50;

var seed = Math.floor(Math.random() * 10000);
var random = seedrandom(seed);

console.log('Running regression tests...');
[
  ['GAATAAAAAAAGATTAACAT', 'AAAAACTTGTAATTAACAAC'],
  ['🔘🤘🔗🔗', '🔗🤗🤗__🤗🤘🤘🤗🔗🤘🔗'],
  ['🔗🤗🤗__🤗🤘🤘🤗🔗🤘🔗', '🤗🤘🔘'],
  ['🤘🤘🔘🔘_🔘🔗🤘🤗🤗__🔗🤘', '🤘🔘🤘🔗🤘🤘🔗🤗🤘🔘🔘'],
  ['🤗🤘🤗🔘🤘🔘🤗_🤗🔗🤘🤗_🤘🔗🤗🤘🔗🤘🤘🤘🔗🤗🔗🔗🔗🤗_🤘🔗🤗🤗🔘🤗🤗🤘🤗',
   '_🤗🤘_🤘🤘🔘🤗🔘🤘_🔘🤗🔗🔘🔗🤘🔗🤘🤗🔗🔗🔗🤘🔘_🤗🤘🤘🤘__🤘_🔘🤘🤘_🔗🤘🔘'],
  ['🔗🤘🤗🔘🔘🤗', '🤘🤘🤘🤗🔘🔗🔗'],
  ['🔘_🔗🔗🔗🤗🔗', '🤘🤗🔗🤗_🤘🔘_'],
].forEach(function (data) {
  var result = diff(data[0], data[1]);
  applyDiff(result, data[0], data[1]);
});

console.log('Running computing ' + ITERATIONS + ' diffs with seed ' + seed + '...');

console.log('Generating strings...');
var strings = [];
for (var i = 0; i <= ITERATIONS; ++i) {
  var chars = [];
  for (var l = 0; l < LENGTH; ++l) {
    var letter = ALPHABET.substr(Math.floor(random() * ALPHABET.length), 1);
    chars.push(letter);
  }
  strings.push(chars.join(''));
}

console.log('Running fuzz tests *without* cursor information...');
for (var i = 0; i < ITERATIONS; ++i) {
  var result = diff(strings[i], strings[i + 1]);
  applyDiff(result, strings[i], strings[i + 1]);
}

console.log('Running fuzz tests *with* cursor information');
for (var i = 0; i < ITERATIONS; ++i) {
  var cursor_pos = Math.floor(random() * strings[i].length + 1);
  var diffs = diff(strings[i], strings[i + 1], cursor_pos);
  applyDiff(diffs, strings[i], strings[i + 1]);
}

function parseDiff(str) {
  if (!str) {
    return [];
  }
  return str.split(/(?=[+\-=])/).map(function (piece) {
    var symbol = piece.charAt(0);
    var text = piece.slice(1);
    return [
      symbol === '+' ? diff.INSERT : symbol === '-' ? diff.DELETE : diff.EQUAL,
      text
    ]
  });
}

console.log('Running cursor tests');
[
  ['', 0, '', null, ''],

  ['', 0, 'a', null, '+a'],
  ['a', 0, 'aa', null, '+a=a'],
  ['a', 1, 'aa', null, '=a+a'],
  ['aa', 0, 'aaa', null, '+a=aa'],
  ['aa', 1, 'aaa', null, '=a+a=a'],
  ['aa', 2, 'aaa', null, '=aa+a'],
  ['aaa', 0, 'aaaa', null, '+a=aaa'],
  ['aaa', 1, 'aaaa', null, '=a+a=aa'],
  ['aaa', 2, 'aaaa', null, '=aa+a=a'],
  ['aaa', 3, 'aaaa', null, '=aaa+a'],

  ['a', 0, '', null, '-a'],
  ['a', 1, '', null, '-a'],
  ['aa', 0, 'a', null, '-a=a'],
  ['aa', 1, 'a', null, '-a=a'],
  ['aa', 2, 'a', null, '=a-a'],
  ['aaa', 0, 'aa', null, '-a=aa'],
  ['aaa', 1, 'aa', null, '-a=aa'],
  ['aaa', 2, 'aa', null, '=a-a=a'],
  ['aaa', 3, 'aa', null, '=aa-a'],

  ['', 0, '', 0, ''],

  ['', 0, 'a', 1, '+a'],
  ['a', 0, 'aa', 1, '+a=a'],
  ['a', 1, 'aa', 2, '=a+a'],
  ['aa', 0, 'aaa', 1, '+a=aa'],
  ['aa', 1, 'aaa', 2, '=a+a=a'],
  ['aa', 2, 'aaa', 3, '=aa+a'],
  ['aaa', 0, 'aaaa', 1, '+a=aaa'],
  ['aaa', 1, 'aaaa', 2, '=a+a=aa'],
  ['aaa', 2, 'aaaa', 3, '=aa+a=a'],
  ['aaa', 3, 'aaaa', 4, '=aaa+a'],

  ['a', 1, '', 0, '-a'],
  ['aa', 1, 'a', 0, '-a=a'],
  ['aa', 2, 'a', 1, '=a-a'],
  ['aaa', 1, 'aa', 0, '-a=aa'],
  ['aaa', 2, 'aa', 1, '=a-a=a'],
  ['aaa', 3, 'aa', 2, '=aa-a'],

  ['a', 1, '', 0, '-a'],
  ['aa', 1, 'a', 0, '-a=a'],
  ['aa', 2, 'a', 1, '=a-a'],
  ['aaa', 1, 'aa', 0, '-a=aa'],
  ['aaa', 2, 'aa', 1, '=a-a=a'],
  ['aaa', 3, 'aa', 2, '=aa-a'],

  // forward-delete
  ['a', 0, '', 0, '-a'],
  ['aa', 0, 'a', 0, '-a=a'],
  ['aa', 1, 'a', 1, '=a-a'],
  ['aaa', 0, 'aa', 0, '-a=aa'],
  ['aaa', 1, 'aa', 1, '=a-a=a'],
  ['aaa', 2, 'aa', 2, '=aa-a'],

  ['bob', 0, 'bobob', null, '+bo=bob'],
  ['bob', 1, 'bobob', null, '=b+ob=ob'],
  ['bob', 2, 'bobob', null, '=bo+bo=b'],
  ['bob', 3, 'bobob', null, '=bob+ob'],
  ['bob', 0, 'bobob', 2, '+bo=bob'],
  ['bob', 1, 'bobob', 3, '=b+ob=ob'],
  ['bob', 2, 'bobob', 4, '=bo+bo=b'],
  ['bob', 3, 'bobob', 5, '=bob+ob'],
  ['bobob', 2, 'bob', null, '-bo=bob'],
  ['bobob', 3, 'bob', null, '=b-ob=ob'],
  ['bobob', 4, 'bob', null, '=bo-bo=b'],
  ['bobob', 5, 'bob', null, '=bob-ob'],
  ['bobob', 2, 'bob', 0, '-bo=bob'],
  ['bobob', 3, 'bob', 1, '=b-ob=ob'],
  ['bobob', 4, 'bob', 2, '=bo-bo=b'],
  ['bobob', 5, 'bob', 3, '=bob-ob'],

  ['bob', 1, 'b', null, '=b-ob'],

  ['hello', [0, 5], 'h', 1, '-hello+h'],
  ['yay', [0, 3], 'y', 1, '-yay+y'],
  ['bobob', [1, 4], 'bob', 2, '=b-obo+o=b'],

].forEach(function (data) {
  var oldText = data[0];
  var newText = data[2];
  var oldRange = typeof data[1] === 'number' ?
    { index: data[1], length: 0 } :
    { index: data[1][0], length: data[1][1] - data[1][0] };
  var newRange = typeof data[3] === 'number' ?
    { index: data[3], length: 0 } :
    data[3] === null ? null : { index: data[3][0], length: data[3][1] - data[3][0] };
  var expected = parseDiff(data[4]);
  if (newRange === null && typeof data[1] !== 'number') {
    throw new Error('invalid test case');
  }
  var cursorInfo = newRange === null ? data[1] : {
    oldRange: oldRange,
    newRange: newRange,
  };
  doCursorTest(oldText, newText, cursorInfo, expected);
  doCursorTest('x' + oldText, 'x' + newText, shiftCursorInfo(cursorInfo, 1), diffPrepend(expected, 'x'));
  doCursorTest(oldText + 'x', newText + 'x', cursorInfo, diffAppend(expected, 'x'));
});

function diffPrepend(tuples, text) {
  if (tuples.length > 0 && tuples[0][0] === diff.EQUAL) {
    return [[diff.EQUAL, text + tuples[0][1]]].concat(tuples.slice(1));
  } else {
    return [[diff.EQUAL, text]].concat(tuples);
  }
}

function diffAppend(tuples, text) {
  var lastTuple = tuples[tuples.length - 1];
  if (lastTuple && lastTuple[0] === diff.EQUAL) {
    return tuples.slice(0, -1).concat([[diff.EQUAL, lastTuple[1] + text]]);
  } else {
    return tuples.concat([[diff.EQUAL, text]]);
  }
}

function shiftCursorInfo(cursorInfo, amount) {
  if (typeof cursorInfo === 'number') {
    return cursorInfo + amount;
  } else {
    return {
      oldRange: {
        index: cursorInfo.oldRange.index + amount,
        length: cursorInfo.oldRange.length,
      },
      newRange: {
        index: cursorInfo.newRange.index + amount,
        length: cursorInfo.newRange.length,
      },
    }
  }
}

function doCursorTest(oldText, newText, cursorInfo, expected) {
  var result = diff(oldText, newText, cursorInfo);
  if (!_.isEqual(result, expected)) {
    console.log([oldText, newText, cursorInfo]);
    console.log(result, '!==', expected);
    throw new Error('cursor test failed');
  }
}

console.log('Running emoji tests');
[
  ['🐶', '🐯', '-🐶+🐯'],
  ['👨🏽', '👩🏽', '-👨+👩=🏽'],
  ['👩🏼', '👩🏽', '=👩-🏼+🏽'],

  ['🍏🍎', '🍎', '-🍏=🍎'],
  ['🍎', '🍏🍎', '+🍏=🍎'],

].forEach(function (data) {
  var oldText = data[0];
  var newText = data[1];
  var expected = parseDiff(data[2]);
  doEmojiTest(oldText, newText, expected);
  doEmojiTest('x' + oldText, 'x' + newText, diffPrepend(expected, 'x'));
  doEmojiTest(oldText + 'x', newText + 'x', diffAppend(expected, 'x'));
});

function doEmojiTest(oldText, newText, expected) {
  var result = diff(oldText, newText);
  if (!_.isEqual(result, expected)) {
    console.log(oldText, newText, expected);
    console.log(result, '!==', expected);
    throw new Error('Emoji simple test case failed');
  }
}

// emojis chosen to share high and low surrogates!
var EMOJI_ALPHABET = ['_', '🤗', '🔗', '🤘', '🔘'];

console.log('Generating emoji strings...');
var emoji_strings = [];
for (var i = 0; i <= ITERATIONS; ++i) {
  var letters = [];
  var len = Math.floor(random() * EMOJI_MAX_LENGTH);
  for (var l = 0; l < len; ++l) {
    var letter = EMOJI_ALPHABET[Math.floor(random() * EMOJI_ALPHABET.length)];
    letters.push(letter);
  }
  emoji_strings.push(letters.join(''));
}

console.log('Running emoji fuzz tests...');
for (var i = 0; i < ITERATIONS; ++i) {
  var oldText = emoji_strings[i];
  var newText = emoji_strings[i + 1];
  var result = diff(oldText, newText);
  applyDiff(result, oldText, newText);
}

// Applies a diff to text, throwing an error if diff is invalid or incorrect
function applyDiff(diffs, text, expectedResult) {
  var pos = 0;
  function throwError(message) {
    console.log(diffs, text, expectedResult);
    throw new Error(message);
  }
  function expect(expected) {
    var found = text.substr(pos, expected.length);
    if (found !== expected) {
      throwError('Expected "' + expected + '", found "' + found + '"');
    }
  }
  var result = '';
  var inserts_since_last_equality = 0;
  var deletes_since_last_equality = 0;
  for (var i = 0; i < diffs.length; i++) {
    var d = diffs[i];
    if (!d[1]) {
      throwError('Empty tuple in diff')
    }
    var firstCharCode = d[1].charCodeAt(0);
    var lastCharCode = d[1].slice(-1).charCodeAt(0);
    if (firstCharCode >= 0xDC00 && firstCharCode <= 0xDFFF ||
      lastCharCode >= 0xD800 && lastCharCode <= 0xDBFF) {
        throwError('Bad unicode diff tuple')
    }
    switch (d[0]) {
      case diff.EQUAL:
        if (i !== 0 && !inserts_since_last_equality && !deletes_since_last_equality) {
          throwError('two consecutive equalities in diff');
        }
        inserts_since_last_equality = 0;
        deletes_since_last_equality = 0;
        expect(d[1]);
        result += d[1];
        pos += d[1].length;
        break;
      case diff.DELETE:
        if (deletes_since_last_equality) {
          throwError('multiple deletes between equalities')
        }
        if (inserts_since_last_equality) {
          throwError('delete following insert in diff')
        }
        deletes_since_last_equality++;
        expect(d[1]);
        pos += d[1].length;
        break
      case diff.INSERT:
        if (inserts_since_last_equality) {
          throwError('multiple inserts between equalities')
        }
        inserts_since_last_equality++;
        result += d[1];
        break;
    }
  }
  if (pos !== text.length) {
    throwError('Diff did not consume entire input text');
  }
  if (result !== expectedResult) {
    console.log(diffs, text, expectedResult, result);
    throw new Error('Diff not correct')
  }
  return result;
}

console.log("Success!");