Use diff to visualize results

This commit is contained in:
Konstantin Käfer 2011-01-21 12:59:09 -05:00
parent 41ed66e962
commit 40f7cfbc7a
3 changed files with 207 additions and 11 deletions

View File

@ -45,8 +45,7 @@ helper.files('specificity', 'mss', function(filename) {
assert.deepEqual(mss, json);
} catch (e) {
console.log(helper.stylize("Failure", 'red') + ': ' + helper.stylize(file, 'underline') + ' differs from expected result.');
console.log(helper.stylize('actual:', 'bold') + '\n' + formatJSON(e.actual));
console.log(helper.stylize('expected:', 'bold') + '\n' + formatJSON(e.expected));
helper.showDifferences(e);
throw '';
}
});
@ -55,10 +54,3 @@ helper.files('specificity', 'mss', function(filename) {
});
}
});
function formatJSON(arr) {
return '[\n ' + arr.map(function(t) {
return JSON.stringify(t);
}).join(',\n ') + '\n]';
}

185
test/support/diff.js Normal file
View File

@ -0,0 +1,185 @@
/**
* Fragment used to represent a string fragment in the diff.
*/
var Fragment = function (string) {
this.content = string;
this.equiv = false;
};
/**
* Wrap in given tag or return the clean value.
*/
Fragment.prototype.toString = function (tag) {
if (this.equiv || !tag) {
return this.content;
}
else {
return '<' + tag + '>' + this.content + '</' + tag + '>';
}
};
var moveToEnd = function (a, i, k) {
if (!a.equiv && (!k[i-1] || k[i-1].equiv)) {
// Find next item equiv item.
for (var j = i+1; k[j] && !k[j].equiv; j++);
if (k[j] && k[j].content === a.content) {
k[i] = k[j];
k[j] = a;
}
}
};
var aggregate = function (a, i, k) {
if (!a.equiv && k[i+1] && !k[i+1].equiv) {
k[i+1].content = a.content + k[i+1].content;
delete k[i];
}
};
var join = function (what, t) {
return what.map(function (a) {
if (a) return a.toString(t);
}).join('');
};
var clone = function(source) {
if (typeof source === 'object' && source !== null) {
var target = Array.isArray(source) ? [] : {};
for (var key in source) target[key] = clone(source[key]);
return target;
}
return source;
};
var WordDiff = {
nonWord: /(&.+?;|[\u0000-\u0040\u005B-\u0060\u007B-\u00A9\u00AB-\u00B4\u00B6-\u00B9\u00BB-\u00BF\u00D7\u00F7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u036F\u0375\u037E\u0384\u0385\u0387\u03F6\u0482-\u0489\u055A-\u055F\u0589\u058A\u0591-\u05C7\u05F3\u05F4\u0600-\u0603\u0606-\u061B\u061E\u061F\u064B-\u065E\u0660-\u066D\u0670\u06D4\u06D6-\u06E4\u06EA-\u06ED\u06F0-\u06F9\u06FD\u06FE\u0700-\u070D\u070F\u0711\u0730-\u074A\u07A6-\u07B0\u07C0-\u07C9\u07EB-\u07F3\u07F6-\u07F9\u0901-\u0903\u093C\u093E-\u094D\u0951-\u0954\u09E2\u0962-\u0970\u06E7-\u06E9\u0981-\u0983\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09E3\u09E6-\u09EF\u09F2-\u09FA\u0A01-\u0A03\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A66-\u0A71\u0A75\u0A81-\u0A83\u0ABC\u0ABE-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE2\u0AE3\u0AE6-\u0AEF\u0AF1\u0B01-\u0B03\u0B3C\u0B3E-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B62\u0B63\u0B66-\u0B70\u0B82\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE6-\u0BFA\u0C01-\u0C03\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C62\u0C63\u0C66-\u0C6F\u0C78-\u0C7F\u0C82\u0C83\u0CBC\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CE2\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D02\u0D03\u0D3E-\u0D44\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D62\u0D63\u0D66-\u0D75\u0D79\u0D82\u0D83\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF4\u0E31\u0E34-\u0E3A\u0E3F\u0E47-\u0E5B\u0EB1\u0EB4-\u0EB9\u0EBB\u0EBC\u0EC8-\u0ECD\u0ED0-\u0ED9\u0F01-\u0F3F\u0F71-\u0F87\u0F90-\u0F97\u0F99-\u0FBC\u0FBE-\u0FCC\u0FCE-\u0FD4\u102B-\u103E\u1040-\u104F\u1056-\u1059\u105E-\u1060\u1062-\u1064\u1067-\u106D\u1071-\u1074\u1082-\u108D\u108F-\u1099\u109E\u109F\u10FB\u135F-\u137C\u1390-\u1399\u166D\u166E\u1680\u169B\u169C\u16EB-\u16F0\u1712-\u1714\u1732-\u1736\u1752\u1753\u1772\u1773\u17B4-\u17D6\u17D8-\u17DB\u17DD\u17E0-\u17E9\u17F0-\u17F9\u1800-\u180E\u1810-\u1819\u18A9\u1920-\u192B\u1930-\u193B\u1940\u1944-\u194F\u19B0-\u19C0\u19C8\u19C9\u19D0-\u19D9\u19DE-\u19FF\u1A17-\u1A1B\u1A1E\u1A1F\u1B00-\u1B04\u1B34-\u1B44\u1B50-\u1B7C\u1B80-\u1B82\u1BA1-\u1BAA\u1BB0-\u1BB9\u1C24-\u1C37\u1C3B-\u1C49\u1C50-\u1C59\u1C7E\u1C7F\u1DC0-\u1DE6\u1DFE\u1DFF\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2000-\u2064\u206A-\u2070\u2074-\u207E\u2080-\u208E\u20A0-\u20B5\u20D0-\u20F0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u2153-\u2182\u2185-\u2188\u2190-\u23E7\u2400-\u2426\u2440-\u244A\u2460-\u269D\u26A0-\u26BC\u26C0-\u26C3\u2701-\u2704\u2706-\u2709\u270C-\u2727\u2729-\u274B\u274D\u274F-\u2752\u2756\u2758-\u275E\u2761-\u2794\u2798-\u27AF\u27B1-\u27BE\u27C0-\u27CA\u27CC\u27D0-\u2B4C\u2B50-\u2B54\u2CE5-\u2CEA\u2CF9-\u2CFF\u2DE0-\u2E2E\u2E30\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3000-\u3004\u3007-\u3030\u3036-\u303A\u303D-\u303F\u3099-\u309C\u30A0\u30FB\u3190-\u319F\u31C0-\u31E3\u3200-\u321E\u3220-\u3243\u3250-\u32FE\u3300-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA60D-\uA60F\uA620-\uA629\uA66F-\uA673\uA67C-\uA67E\uA700-\uA716\uA720\uA721\uA789\uA78A\uA802\uA806\uA80B\uA823-\uA82B\uA874-\uA877\uA880\uA881\uA8B4-\uA8C4\uA8CE-\uA8D9\uA900-\uA909\uA926-\uA92F\uA947-\uA953\uA95F\uAA29-\uAA36\uAA43\uAA4C\uAA4D\uAA50-\uAA59\uAA5C-\uAA5F\uD800\uDB7F\uDB80\uDBFF\uDC00\uDFFF\uE000\uF8FF\uFB1E\uFB29\uFD3E\uFD3F\uFDFC\uFDFD\uFE00-\uFE19\uFE20-\uFE26\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFEFF\uFF01-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFF9-\uFFFD])/,
tokenize: function (args) {
// Split on non-word characters.
for (var type in args) {
args[type] = args[type].split(WordDiff.nonWord).filter(function (s) {
return s.length;
});
}
// Calculate the indexes and offsets for common suffixes and prefixes.
var i = -1, j = args.del.length, k = args.ins.length;
while (args.del[++i] === args.ins[i] && i <= j);
while (j >= i && k >= i && args.del[--j] === args.ins[--k]);
args.prefix = args.del.slice(0, i).join('');
args.suffix = args.del.slice(j + 1).join('');
args.del = args.del.slice(i, ++j);
args.ins = args.ins.slice(i, ++k);
},
lcs: function (args) {
var matrix = [];
for (var i = 0; i < args.del.length; i++) {
matrix[i] = [];
for (var j = 0; j < args.ins.length; j++) {
if (args.del[i] === args.ins[j]) {
matrix[i][j] = (matrix[i - 1] && matrix[i - 1][j - 1] || 0) + args.del[i].length;
}
else {
matrix[i][j] = Math.max(matrix[i][j - 1] || 0, matrix[i - 1] && matrix[i - 1][j] || 0);
}
}
}
return matrix;
},
changeset: function (args, matrix) {
var result = {};
['del', 'ins'].forEach(function (type) {
result[type] = args[type].map(function (a) { return new Fragment(a); });
});
// Backtrack through the matrix.
for (var i = result.del.length - 1, j = result.ins.length - 1; i >= 0; i--, j--) {
if (j < 0 || result.del[i].content !== result.ins[j].content) {
if (j < 0 || (j > 0 && matrix[i - 1] && (matrix[i][j - 1] < matrix[i - 1][j]))) {
j++;
}
else {
i++;
}
}
else {
result.del[i] = result.ins[j];
result.del[i].equiv = true;
}
}
// Fill up gaps.
for (var i = 0; i < result.del.length; i++) {
if (result.del[i].equiv && result.del[i].content.length < 3) {
var j = result.ins.indexOf(result.del[i]);
if (result.del[i-1] && result.del[i+1] && result.ins[j-1] && result.ins[j+1] && !result.del[i-1].equiv && !result.del[i+1].equiv && !result.ins[j-1].equiv && !result.ins[j+1].equiv){
result.del[i].equiv = false;
result.ins[j] = clone(result.del[i]);
}
}
}
['del', 'ins'].forEach(function (type) {
// Try to move changes to the end.
for (var i = 0; i < result[type].length; i++)
moveToEnd(result[type][i], i, result[type]);
// Aggregate subsequent changes to minimize ins/del tags.
for (var i = 0; i < result[type].length; i++)
aggregate(result[type][i], i, result[type]);
});
return result;
},
htmlRender: function (args, result) {
var diff = {
del: args.prefix + join(result.del, 'del') + args.suffix,
ins: args.prefix + join(result.ins, 'ins') + args.suffix
};
return diff;
},
htmlDiff: function (del, ins) {
var args = { 'del': del, 'ins': ins };
WordDiff.tokenize(args);
var matrix = WordDiff.lcs(args);
var result = WordDiff.changeset(args, matrix);
return WordDiff.htmlRender(args, result);
},
render: function (args, result) {
var join = function (what, type) {
return what.map(function (a) {
if (!a) return;
if (a.equiv) return a.content;
if (type == 'del') return '\033[31;4m' + a.content + '\033[0m';
if (type == 'ins') return '\033[32;4m' + a.content + '\033[0m';
}).join('');
};
return {
del: args.prefix + join(result.del, 'del') + args.suffix,
ins: args.prefix + join(result.ins, 'ins') + args.suffix
};
},
diff: function(del, ins) {
var args = { 'del': del, 'ins': ins };
WordDiff.tokenize(args);
var matrix = WordDiff.lcs(args);
var result = WordDiff.changeset(args, matrix);
return WordDiff.render(args, result);
}
};
module.exports = WordDiff;

View File

@ -1,6 +1,8 @@
var path = require('path'),
fs = require('fs');
fs = require('fs'),
diff = require('./diff').diff;
var helper = exports;
exports.files = function(dir, extension, callback) {
var dir = path.join(__dirname, '..', dir);
@ -17,7 +19,24 @@ exports.json = function(file, callback) {
if (err) throw err;
callback(JSON.parse(content));
});
}
};
exports.showDifferences = function(e) {
var changes = diff(
helper.formatJSON(e.actual),
helper.formatJSON(e.expected)
);
console.log(helper.stylize('actual:', 'bold') + '\n' + changes.del);
console.log(helper.stylize('expected:', 'bold') + '\n' + changes.ins);
};
exports.formatJSON = function(arr) {
return '[\n ' + arr.map(function(t) {
return JSON.stringify(t);
}).join(',\n ') + '\n]';
};