Spaces:
Sleeping
Sleeping
/* | |
Javascript doctest runner | |
Copyright 2006-2010 Ian Bicking | |
This program is free software; you can redistribute it and/or modify it under | |
the terms of the MIT License. | |
*/ | |
function doctest(verbosity/*default=0*/, elements/*optional*/, | |
outputId/*optional*/) { | |
var output = document.getElementById(outputId || 'doctestOutput'); | |
var reporter = new doctest.Reporter(output, verbosity || 0); | |
if (elements) { | |
if (typeof elements == 'string') { | |
// Treat it as an id | |
elements = [document.getElementById(elementId)]; | |
} | |
if (! elements.length) { | |
throw('No elements'); | |
} | |
var suite = new doctest.TestSuite(elements, reporter); | |
} else { | |
var els = doctest.getElementsByTagAndClassName('pre', 'doctest'); | |
var suite = new doctest.TestSuite(els, reporter); | |
} | |
suite.run(); | |
} | |
doctest.runDoctest = function (el, reporter) { | |
logDebug('Testing element', el); | |
reporter.startElement(el); | |
if (el === null) { | |
throw('runDoctest() with a null element'); | |
} | |
var parsed = new doctest.Parser(el); | |
var runner = new doctest.JSRunner(reporter); | |
runner.runParsed(parsed); | |
}; | |
doctest.TestSuite = function (els, reporter) { | |
if (this === window) { | |
throw('you forgot new!'); | |
} | |
this.els = els; | |
this.parsers = []; | |
for (var i=0; i<els.length; i++) { | |
this.parsers.push(new doctest.Parser(els[i])); | |
} | |
this.reporter = reporter; | |
}; | |
doctest.TestSuite.prototype.run = function (ctx) { | |
if (! ctx) { | |
ctx = new doctest.Context(this); | |
} | |
if (! ctx.runner ) { | |
ctx.runner = new doctest.JSRunner(this.reporter); | |
} | |
return ctx.run(); | |
}; | |
// FIXME: should this just be part of TestSuite? | |
doctest.Context = function (testSuite) { | |
if (this === window) { | |
throw('You forgot new!'); | |
} | |
this.testSuite = testSuite; | |
this.runner = null; | |
}; | |
doctest.Context.prototype.run = function (parserIndex) { | |
var self = this; | |
parserIndex = parserIndex || 0; | |
if (parserIndex >= this.testSuite.parsers.length) { | |
logInfo('All examples from all sections tested'); | |
this.runner.reporter.finish(); | |
return; | |
} | |
logInfo('Testing example ' + (parserIndex+1) + ' of ' | |
+ this.testSuite.parsers.length); | |
var runNext = function () { | |
self.run(parserIndex+1); | |
}; | |
this.runner.runParsed(this.testSuite.parsers[parserIndex], 0, runNext); | |
}; | |
doctest.Parser = function (el) { | |
if (this === window) { | |
throw('you forgot new!'); | |
} | |
if (! el) { | |
throw('Bad call to doctest.Parser'); | |
} | |
if (el.getAttribute('parsed-id')) { | |
var examplesID = el.getAttribute('parsed-id'); | |
if (doctest._allExamples[examplesID]) { | |
this.examples = doctest._allExamples[examplesID]; | |
return; | |
} | |
} | |
var newHTML = document.createElement('span'); | |
newHTML.className = 'doctest-example-set'; | |
var examplesID = doctest.genID('example-set'); | |
newHTML.setAttribute('id', examplesID); | |
el.setAttribute('parsed-id', examplesID); | |
var text = doctest.getText(el); | |
var lines = text.split(/(?:\r\n|\r|\n)/); | |
this.examples = []; | |
var example_lines = []; | |
var output_lines = []; | |
for (var i=0; i<lines.length; i++) { | |
var line = lines[i]; | |
if (/^[$]/.test(line)) { | |
if (example_lines.length) { | |
var ex = new doctest.Example(example_lines, output_lines); | |
this.examples.push(ex); | |
newHTML.appendChild(ex.createSpan()); | |
} | |
example_lines = []; | |
output_lines = []; | |
line = line.substr(1).replace(/ *$/, '').replace(/^ /, ''); | |
example_lines.push(line); | |
} else if (/^>/.test(line)) { | |
if (! example_lines.length) { | |
throw('Bad example: '+doctest.repr(line)+'\n' | |
+'> line not preceded by $'); | |
} | |
line = line.substr(1).replace(/ *$/, '').replace(/^ /, ''); | |
example_lines.push(line); | |
} else { | |
output_lines.push(line); | |
} | |
} | |
if (example_lines.length) { | |
var ex = new doctest.Example(example_lines, output_lines); | |
this.examples.push(ex); | |
newHTML.appendChild(ex.createSpan()); | |
} | |
el.innerHTML = ''; | |
el.appendChild(newHTML); | |
doctest._allExamples[examplesID] = this.examples; | |
}; | |
doctest._allExamples = {}; | |
doctest.Example = function (example, output) { | |
if (this === window) { | |
throw('you forgot new!'); | |
} | |
this.example = example.join('\n'); | |
this.output = output.join('\n'); | |
this.htmlID = null; | |
this.detailID = null; | |
}; | |
doctest.Example.prototype.createSpan = function () { | |
var id = doctest.genID('example'); | |
var span = document.createElement('span'); | |
span.className = 'doctest-example'; | |
span.setAttribute('id', id); | |
this.htmlID = id; | |
var exampleSpan = document.createElement('span'); | |
exampleSpan.className = 'doctest-example-code'; | |
var exampleLines = this.example.split(/\n/); | |
for (var i=0; i<exampleLines.length; i++) { | |
var promptSpan = document.createElement('span'); | |
promptSpan.className = 'doctest-example-prompt'; | |
promptSpan.innerHTML = i == 0 ? '$ ' : '> '; | |
exampleSpan.appendChild(promptSpan); | |
var lineSpan = document.createElement('span'); | |
lineSpan.className = 'doctest-example-code-line'; | |
lineSpan.appendChild(document.createTextNode(doctest.rstrip(exampleLines[i]))); | |
exampleSpan.appendChild(lineSpan); | |
exampleSpan.appendChild(document.createTextNode('\n')); | |
} | |
span.appendChild(exampleSpan); | |
var outputSpan = document.createElement('span'); | |
outputSpan.className = 'doctest-example-output'; | |
outputSpan.appendChild(document.createTextNode(this.output)); | |
span.appendChild(outputSpan); | |
span.appendChild(document.createTextNode('\n')); | |
return span; | |
}; | |
doctest.Example.prototype.markExample = function (name, detail) { | |
if (! this.htmlID) { | |
return; | |
} | |
if (this.detailID) { | |
var el = document.getElementById(this.detailID); | |
el.parentNode.removeChild(el); | |
this.detailID = null; | |
} | |
var span = document.getElementById(this.htmlID); | |
span.className = span.className.replace(/ doctest-failure/, '') | |
.replace(/ doctest-success/, '') | |
+ ' ' + name; | |
if (detail) { | |
this.detailID = doctest.genID('doctest-example-detail'); | |
var detailSpan = document.createElement('span'); | |
detailSpan.className = 'doctest-example-detail'; | |
detailSpan.setAttribute('id', this.detailID); | |
detailSpan.appendChild(document.createTextNode(detail)); | |
span.appendChild(detailSpan); | |
} | |
}; | |
doctest.Reporter = function (container, verbosity) { | |
if (this === window) { | |
throw('you forgot new!'); | |
} | |
if (! container) { | |
throw('No container passed to doctest.Reporter'); | |
} | |
this.container = container; | |
this.verbosity = verbosity; | |
this.success = 0; | |
this.failure = 0; | |
this.elements = 0; | |
}; | |
doctest.Reporter.prototype.startElement = function (el) { | |
this.elements += 1; | |
logDebug('Adding element', el); | |
}; | |
doctest.Reporter.prototype.reportSuccess = function (example, output) { | |
if (this.verbosity > 0) { | |
if (this.verbosity > 1) { | |
this.write('Trying:\n'); | |
this.write(this.formatOutput(example.example)); | |
this.write('Expecting:\n'); | |
this.write(this.formatOutput(example.output)); | |
this.write('ok\n'); | |
} else { | |
this.writeln(example.example + ' ... passed!'); | |
} | |
} | |
this.success += 1; | |
if ((example.output.indexOf('...') >= 0 | |
|| example.output.indexOf('?') >= 0) | |
&& output) { | |
example.markExample('doctest-success', 'Output:\n' + output); | |
} else { | |
example.markExample('doctest-success'); | |
} | |
}; | |
doctest.Reporter.prototype.reportFailure = function (example, output) { | |
this.write('Failed example:\n'); | |
this.write('<span style="color: #00f"><a href="#' | |
+ example.htmlID | |
+ '" class="doctest-failure-link" title="Go to example">' | |
+ this.formatOutput(example.example) | |
+'</a></span>'); | |
this.write('Expected:\n'); | |
this.write(this.formatOutput(example.output)); | |
this.write('Got:\n'); | |
this.write(this.formatOutput(output)); | |
this.failure += 1; | |
example.markExample('doctest-failure', 'Actual output:\n' + output); | |
}; | |
doctest.Reporter.prototype.finish = function () { | |
this.writeln((this.success+this.failure) | |
+ ' tests in ' + this.elements + ' items.'); | |
if (this.failure) { | |
var color = '#f00'; | |
} else { | |
var color = '#0f0'; | |
} | |
this.writeln('<span class="passed">' + this.success + '</span> tests of ' | |
+ '<span class="total">' + (this.success+this.failure) + '</span> passed, ' | |
+ '<span class="failed" style="color: '+color+'">' | |
+ this.failure + '</span> failed.'); | |
}; | |
doctest.Reporter.prototype.writeln = function (text) { | |
this.write(text + '\n'); | |
}; | |
doctest.Reporter.prototype.write = function (text) { | |
var leading = /^[ ]*/.exec(text)[0]; | |
text = text.substr(leading.length); | |
for (var i=0; i<leading.length; i++) { | |
text = String.fromCharCode(160)+text; | |
} | |
text = text.replace(/\n/g, '<br>'); | |
this.container.innerHTML += text; | |
}; | |
doctest.Reporter.prototype.formatOutput = function (text) { | |
if (! text) { | |
return ' <span style="color: #999">(nothing)</span>\n'; | |
} | |
var lines = text.split(/\n/); | |
var output = ''; | |
for (var i=0; i<lines.length; i++) { | |
output += ' '+doctest.escapeSpaces(doctest.escapeHTML(lines[i]))+'\n'; | |
} | |
return output; | |
}; | |
doctest.JSRunner = function (reporter) { | |
if (this === window) { | |
throw('you forgot new!'); | |
} | |
this.reporter = reporter; | |
}; | |
doctest.JSRunner.prototype.runParsed = function (parsed, index, finishedCallback) { | |
var self = this; | |
index = index || 0; | |
if (index >= parsed.examples.length) { | |
if (finishedCallback) { | |
finishedCallback(); | |
} | |
return; | |
} | |
var example = parsed.examples[index]; | |
if (typeof example == 'undefined') { | |
throw('Undefined example (' + (index+1) + ' of ' + parsed.examples.length + ')'); | |
} | |
doctest._waitCond = null; | |
this.run(example); | |
var finishThisRun = function () { | |
self.finishRun(example); | |
if (doctest._AbortCalled) { | |
// FIXME: I need to find a way to make this more visible: | |
logWarn('Abort() called'); | |
return; | |
} | |
self.runParsed(parsed, index+1, finishedCallback); | |
}; | |
if (doctest._waitCond !== null) { | |
if (typeof doctest._waitCond == 'number') { | |
var condition = null; | |
var time = doctest._waitCond; | |
var maxTime = null; | |
} else { | |
var condition = doctest._waitCond; | |
// FIXME: shouldn't be hard-coded | |
var time = 100; | |
var maxTime = doctest._waitTimeout || doctest.defaultTimeout; | |
} | |
var start = (new Date()).getTime(); | |
var timeoutFunc = function () { | |
if (condition === null | |
|| condition()) { | |
finishThisRun(); | |
} else { | |
// Condition not met, try again soon... | |
if ((new Date()).getTime() - start > maxTime) { | |
// Time has run out | |
var msg = 'Error: wait(' + repr(condition) + ') has timed out'; | |
writeln(msg); | |
logDebug(msg); | |
logDebug('Timeout after ' + ((new Date()).getTime() - start) | |
+ ' milliseconds'); | |
finishThisRun(); | |
return; | |
} | |
setTimeout(timeoutFunc, time); | |
} | |
}; | |
setTimeout(timeoutFunc, time); | |
} else { | |
finishThisRun(); | |
} | |
}; | |
doctest.formatTraceback = function (e, skipFrames) { | |
skipFrames = skipFrames || 0; | |
var lines = []; | |
if (typeof e == 'undefined' || !e) { | |
var caughtErr = null; | |
try { | |
(null).foo; | |
} catch (caughtErr) { | |
e = caughtErr; | |
} | |
skipFrames++; | |
} | |
if (e.stack) { | |
var stack = e.stack.split('\n'); | |
for (var i=skipFrames; i<stack.length; i++) { | |
if (stack[i] == '@:0' || ! stack[i]) { | |
continue; | |
} | |
if (stack[i].indexOf('@') == -1) { | |
lines.push(stack[i]); | |
continue; | |
} | |
var parts = stack[i].split('@'); | |
var context = parts[0]; | |
parts = parts[1].split(':'); | |
var filename = parts[parts.length-2].split('/'); | |
filename = filename[filename.length-1]; | |
var lineno = parts[parts.length-1]; | |
context = context.replace('\\n', '\n'); | |
if (context != '' && filename != 'doctest.js') { | |
lines.push(' ' + context + ' -> ' + filename + ':' + lineno); | |
} | |
} | |
} | |
if (lines.length) { | |
return lines; | |
} else { | |
return null; | |
} | |
}; | |
doctest.logTraceback = function (e, skipFrames) { | |
var tracebackLines = doctest.formatTraceback(e, skipFrames); | |
if (! tracebackLines) { | |
return; | |
} | |
for (var i=0; i<tracebackLines.length; i++) { | |
logDebug(tracebackLines[i]); | |
} | |
}; | |
doctest.JSRunner.prototype.run = function (example) { | |
this.capturer = new doctest.OutputCapturer(); | |
this.capturer.capture(); | |
try { | |
var result = doctest.eval(example.example); | |
} catch (e) { | |
var tracebackLines = doctest.formatTraceback(e); | |
writeln('Error: ' + (e.message || e)); | |
var result = null; | |
logWarn('Error in expression: ' + example.example); | |
logDebug('Traceback for error', e); | |
if (tracebackLines) { | |
for (var i=0; i<tracebackLines.length; i++) { | |
logDebug(tracebackLines[i]); | |
} | |
} | |
if (e instanceof Abort) { | |
throw e; | |
} | |
} | |
if (typeof result != 'undefined' | |
&& result !== null | |
&& example.output) { | |
writeln(doctest.repr(result)); | |
} | |
}; | |
doctest._AbortCalled = false; | |
doctest.Abort = function (message) { | |
if (this === window) { | |
return new Abort(message); | |
} | |
this.message = message; | |
// We register this so Abort can be raised in an async call: | |
doctest._AbortCalled = true; | |
}; | |
doctest.Abort.prototype.toString = function () { | |
return this.message; | |
}; | |
if (typeof Abort == 'undefined') { | |
Abort = doctest.Abort; | |
} | |
doctest.JSRunner.prototype.finishRun = function(example) { | |
this.capturer.stopCapture(); | |
var success = this.checkResult(this.capturer.output, example.output); | |
if (success) { | |
this.reporter.reportSuccess(example, this.capturer.output); | |
} else { | |
this.reporter.reportFailure(example, this.capturer.output); | |
logDebug('Failure: '+doctest.repr(example.output) | |
+' != '+doctest.repr(this.capturer.output)); | |
if (location.href.search(/abort/) != -1) { | |
doctest.Abort('abort on first failure'); | |
} | |
} | |
}; | |
doctest.JSRunner.prototype.checkResult = function (got, expected) { | |
// Make sure trailing whitespace doesn't matter: | |
got = got.replace(/ +\n/, '\n'); | |
expected = expected.replace(/ +\n/, '\n'); | |
got = got.replace(/[ \n\r]*$/, '') + '\n'; | |
expected = expected.replace(/[ \n\r]*$/, '') + '\n'; | |
if (expected == '...\n') { | |
return true; | |
} | |
expected = RegExp.escape(expected); | |
// Note: .* doesn't match newlines, [^] doesn't work on IE | |
expected = '^' + expected.replace(/\\\.\\\.\\\./g, "[\\S\\s\\r\\n]*") + '$'; | |
expected = expected.replace(/\\\?/g, "[a-zA-Z0-9_.]+"); | |
expected = expected.replace(/[ \t]+/g, " +"); | |
expected = expected.replace(/\n/g, '\\n'); | |
var re = new RegExp(expected); | |
var result = got.search(re) != -1; | |
if (! result) { | |
if (doctest.strip(got).split('\n').length > 1) { | |
// If it's only one line it's not worth showing this | |
var check = this.showCheckDifference(got, expected); | |
logWarn('Mismatch of output (line-by-line comparison follows)'); | |
for (var i=0; i<check.length; i++) { | |
logDebug(check[i]); | |
} | |
} | |
} | |
return result; | |
}; | |
doctest.JSRunner.prototype.showCheckDifference = function (got, expectedRegex) { | |
if (expectedRegex.charAt(0) != '^') { | |
throw 'Unexpected regex, no leading ^'; | |
} | |
if (expectedRegex.charAt(expectedRegex.length-1) != '$') { | |
throw 'Unexpected regex, no trailing $'; | |
} | |
expectedRegex = expectedRegex.substr(1, expectedRegex.length-2); | |
// Technically this might not be right, but this is all a heuristic: | |
var expectedRegex = expectedRegex.replace(/\(\?:\.\|\[\\r\\n\]\)\*/g, '...'); | |
var expectedLines = expectedRegex.split('\\n'); | |
for (var i=0; i<expectedLines.length; i++) { | |
expectedLines[i] = expectedLines[i].replace(/\.\.\./g, '(?:.|[\r\n])*'); | |
} | |
var gotLines = got.split('\n'); | |
var result = []; | |
var totalLines = expectedLines.length > gotLines.length ? | |
expectedLines.length : gotLines.length; | |
function displayExpectedLine(line) { | |
return line; | |
line = line.replace(/\[a-zA-Z0-9_.\]\+/g, '?'); | |
line = line.replace(/ \+/g, ' '); | |
line = line.replace(/\(\?:\.\|\[\\r\\n\]\)\*/g, '...'); | |
// FIXME: also unescape values? e.g., * became \* | |
return line; | |
} | |
for (var i=0; i<totalLines; i++) { | |
if (i >= expectedLines.length) { | |
result.push('got extra line: ' + repr(gotLines[i])); | |
continue; | |
} else if (i >= gotLines.length) { | |
result.push('expected extra line: ' + displayExpectedLine(expectedLines[i])); | |
continue; | |
} | |
var gotLine = gotLines[i]; | |
try { | |
var expectRE = new RegExp('^' + expectedLines[i] + '$'); | |
} catch (e) { | |
result.push('regex match failed: ' + repr(gotLine) + ' (' | |
+ expectedLines[i] + ')'); | |
continue; | |
} | |
if (gotLine.search(expectRE) != -1) { | |
result.push('match: ' + repr(gotLine)); | |
} else { | |
result.push('no match: ' + repr(gotLine) + ' (' | |
+ displayExpectedLine(expectedLines[i]) + ')'); | |
} | |
} | |
return result; | |
}; | |
// Should I really be setting this on RegExp? | |
RegExp.escape = function (text) { | |
if (!arguments.callee.sRE) { | |
var specials = [ | |
'/', '.', '*', '+', '?', '|', | |
'(', ')', '[', ']', '{', '}', '\\' | |
]; | |
arguments.callee.sRE = new RegExp( | |
'(\\' + specials.join('|\\') + ')', 'g' | |
); | |
} | |
return text.replace(arguments.callee.sRE, '\\$1'); | |
}; | |
doctest.OutputCapturer = function () { | |
if (this === window) { | |
throw('you forgot new!'); | |
} | |
this.output = ''; | |
}; | |
doctest._output = null; | |
doctest.OutputCapturer.prototype.capture = function () { | |
doctest._output = this; | |
}; | |
doctest.OutputCapturer.prototype.stopCapture = function () { | |
doctest._output = null; | |
}; | |
doctest.OutputCapturer.prototype.write = function (text) { | |
if (typeof text == 'string') { | |
this.output += text; | |
} else { | |
this.output += repr(text); | |
} | |
}; | |
// Used to create unique IDs: | |
doctest._idGen = 0; | |
doctest.genID = function (prefix) { | |
prefix = prefix || 'generic-doctest'; | |
var id = doctest._idGen++; | |
return prefix + '-' + doctest._idGen; | |
}; | |
doctest.writeln = function () { | |
for (var i=0; i<arguments.length; i++) { | |
write(arguments[i]); | |
if (i) { | |
write(' '); | |
} | |
} | |
write('\n'); | |
}; | |
if (typeof writeln == 'undefined') { | |
writeln = doctest.writeln; | |
} | |
doctest.write = function (text) { | |
if (doctest._output !== null) { | |
doctest._output.write(text); | |
} else { | |
log(text); | |
} | |
}; | |
if (typeof write == 'undefined') { | |
write = doctest.write; | |
} | |
doctest._waitCond = null; | |
function wait(conditionOrTime, hardTimeout) { | |
// FIXME: should support a timeout even with a condition | |
if (typeof conditionOrTime == 'undefined' | |
|| conditionOrTime === null) { | |
// same as wait-some-small-amount-of-time | |
conditionOrTime = 0; | |
} | |
doctest._waitCond = conditionOrTime; | |
doctest._waitTimeout = hardTimeout; | |
}; | |
doctest.wait = wait; | |
doctest.assert = function (expr, statement) { | |
if (typeof expr == 'string') { | |
if (! statement) { | |
statement = expr; | |
} | |
expr = doctest.eval(expr); | |
} | |
if (! expr) { | |
throw('AssertionError: '+statement); | |
} | |
}; | |
if (typeof assert == 'undefined') { | |
assert = doctest.assert; | |
} | |
doctest.getText = function (el) { | |
if (! el) { | |
throw('You must pass in an element'); | |
} | |
var text = ''; | |
for (var i=0; i<el.childNodes.length; i++) { | |
var sub = el.childNodes[i]; | |
if (sub.nodeType == 3) { | |
// TEXT_NODE | |
text += sub.nodeValue; | |
} else if (sub.childNodes) { | |
text += doctest.getText(sub); | |
} | |
} | |
return text; | |
}; | |
doctest.reload = function (button/*optional*/) { | |
if (button) { | |
button.innerHTML = 'reloading...'; | |
button.disabled = true; | |
} | |
location.reload(); | |
}; | |
/* Taken from MochiKit, with an addition to print objects */ | |
doctest.repr = function (o, indentString, maxLen) { | |
indentString = indentString || ''; | |
if (doctest._reprTracker === null) { | |
var iAmTheTop = true; | |
doctest._reprTracker = []; | |
} else { | |
var iAmTheTop = false; | |
} | |
try { | |
if (doctest._reprTrackObj(o)) { | |
return '..recursive..'; | |
} | |
if (maxLen === undefined) { | |
maxLen = 120; | |
} | |
if (typeof o == 'undefined') { | |
return 'undefined'; | |
} else if (o === null) { | |
return "null"; | |
} | |
try { | |
if (typeof(o.__repr__) == 'function') { | |
return o.__repr__(indentString, maxLen); | |
} else if (typeof(o.repr) == 'function' && o.repr != arguments.callee) { | |
return o.repr(indentString, maxLen); | |
} | |
for (var i=0; i<doctest.repr.registry.length; i++) { | |
var item = doctest.repr.registry[i]; | |
if (item[0](o)) { | |
return item[1](o, indentString, maxLen); | |
} | |
} | |
} catch (e) { | |
if (typeof(o.NAME) == 'string' && ( | |
o.toString == Function.prototype.toString || | |
o.toString == Object.prototype.toString)) { | |
return o.NAME; | |
} | |
} | |
try { | |
var ostring = (o + ""); | |
if (ostring == '[object Object]' || ostring == '[object]') { | |
ostring = doctest.objRepr(o, indentString, maxLen); | |
} | |
} catch (e) { | |
return "[" + typeof(o) + "]"; | |
} | |
if (typeof(o) == "function") { | |
var ostring = ostring.replace(/^\s+/, "").replace(/\s+/g, " "); | |
var idx = ostring.indexOf("{"); | |
if (idx != -1) { | |
ostring = ostring.substr(o, idx) + "{...}"; | |
} | |
} | |
return ostring; | |
} finally { | |
if (iAmTheTop) { | |
doctest._reprTracker = null; | |
} | |
} | |
}; | |
doctest._reprTracker = null; | |
doctest._reprTrackObj = function (obj) { | |
if (typeof obj != 'object') { | |
return false; | |
} | |
for (var i=0; i<doctest._reprTracker.length; i++) { | |
if (doctest._reprTracker[i] === obj) { | |
return true; | |
} | |
} | |
doctest._reprTracker.push(obj); | |
return false; | |
}; | |
doctest._reprTrackSave = function () { | |
return doctest._reprTracker.length-1; | |
}; | |
doctest._reprTrackRestore = function (point) { | |
doctest._reprTracker.splice(point, doctest._reprTracker.length - point); | |
}; | |
doctest._sortedKeys = function (obj) { | |
var keys = []; | |
for (var i in obj) { | |
// FIXME: should I use hasOwnProperty? | |
if (typeof obj.prototype == 'undefined' | |
|| obj[i] !== obj.prototype[i]) { | |
keys.push(i); | |
} | |
} | |
keys.sort(); | |
return keys; | |
}; | |
doctest.objRepr = function (obj, indentString, maxLen) { | |
var restorer = doctest._reprTrackSave(); | |
var ostring = '{'; | |
var keys = doctest._sortedKeys(obj); | |
for (var i=0; i<keys.length; i++) { | |
if (ostring != '{') { | |
ostring += ', '; | |
} | |
ostring += keys[i] + ': ' + doctest.repr(obj[keys[i]], indentString, maxLen); | |
} | |
ostring += '}'; | |
if (ostring.length > (maxLen - indentString.length)) { | |
doctest._reprTrackRestore(restorer); | |
return doctest.multilineObjRepr(obj, indentString, maxLen); | |
} | |
return ostring; | |
}; | |
doctest.multilineObjRepr = function (obj, indentString, maxLen) { | |
var keys = doctest._sortedKeys(obj); | |
var ostring = '{\n'; | |
for (var i=0; i<keys.length; i++) { | |
ostring += indentString + ' ' + keys[i] + ': '; | |
ostring += doctest.repr(obj[keys[i]], indentString+' ', maxLen); | |
if (i != keys.length - 1) { | |
ostring += ','; | |
} | |
ostring += '\n'; | |
} | |
ostring += indentString + '}'; | |
return ostring; | |
}; | |
doctest.arrayRepr = function (obj, indentString, maxLen) { | |
var restorer = doctest._reprTrackSave(); | |
var s = "["; | |
for (var i=0; i<obj.length; i++) { | |
s += doctest.repr(obj[i], indentString, maxLen); | |
if (i != obj.length-1) { | |
s += ", "; | |
} | |
} | |
s += "]"; | |
if (s.length > (maxLen + indentString.length)) { | |
doctest._reprTrackRestore(restorer); | |
return doctest.multilineArrayRepr(obj, indentString, maxLen); | |
} | |
return s; | |
}; | |
doctest.multilineArrayRepr = function (obj, indentString, maxLen) { | |
var s = "[\n"; | |
for (var i=0; i<obj.length; i++) { | |
s += indentString + ' ' + doctest.repr(obj[i], indentString+' ', maxLen); | |
if (i != obj.length - 1) { | |
s += ','; | |
} | |
s += '\n'; | |
} | |
s += indentString + ']'; | |
return s; | |
}; | |
doctest.xmlRepr = function (doc, indentString) { | |
var i; | |
if (doc.nodeType == doc.DOCUMENT_NODE) { | |
return doctest.xmlRepr(doc.childNodes[0], indentString); | |
} | |
indentString = indentString || ''; | |
var s = indentString + '<' + doc.tagName; | |
var attrs = []; | |
if (doc.attributes && doc.attributes.length) { | |
for (i=0; i<doc.attributes.length; i++) { | |
attrs.push(doc.attributes[i].nodeName); | |
} | |
attrs.sort(); | |
for (i=0; i<attrs.length; i++) { | |
s += ' ' + attrs[i] + '="'; | |
var value = doc.getAttribute(attrs[i]); | |
value = value.replace('&', '&'); | |
value = value.replace('"', '"'); | |
s += value; | |
s += '"'; | |
} | |
} | |
if (! doc.childNodes.length) { | |
s += ' />'; | |
return s; | |
} else { | |
s += '>'; | |
} | |
var hasNewline = false; | |
for (i=0; i<doc.childNodes.length; i++) { | |
var el = doc.childNodes[i]; | |
if (el.nodeType == doc.TEXT_NODE) { | |
s += doctest.strip(el.textContent); | |
} else { | |
if (! hasNewline) { | |
s += '\n'; | |
hasNewline = true; | |
} | |
s += doctest.xmlRepr(el, indentString + ' '); | |
s += '\n'; | |
} | |
} | |
if (hasNewline) { | |
s += indentString; | |
} | |
s += '</' + doc.tagName + '>'; | |
return s; | |
}; | |
doctest.repr.registry = [ | |
[function (o) { | |
return typeof o == 'string';}, | |
function (o) { | |
o = '"' + o.replace(/([\"\\])/g, '\\$1') + '"'; | |
o = o.replace(/[\f]/g, "\\f") | |
.replace(/[\b]/g, "\\b") | |
.replace(/[\n]/g, "\\n") | |
.replace(/[\t]/g, "\\t") | |
.replace(/[\r]/g, "\\r"); | |
return o; | |
}], | |
[function (o) { | |
return typeof o == 'number';}, | |
function (o) { | |
return o + ""; | |
}], | |
[function (o) { | |
return (typeof o == 'object' && o.xmlVersion); | |
}, | |
doctest.xmlRepr], | |
[function (o) { | |
var typ = typeof o; | |
if ((typ != 'object' && ! (type == 'function' && typeof o.item == 'function')) || | |
o === null || | |
typeof o.length != 'number' || | |
o.nodeType === 3) { | |
return false; | |
} | |
return true; | |
}, | |
doctest.arrayRepr | |
]]; | |
doctest.objDiff = function (orig, current) { | |
var result = { | |
added: {}, | |
removed: {}, | |
changed: {}, | |
same: {} | |
}; | |
for (var i in orig) { | |
if (! (i in current)) { | |
result.removed[i] = orig[i]; | |
} else if (orig[i] !== current[i]) { | |
result.changed[i] = [orig[i], current[i]]; | |
} else { | |
result.same[i] = orig[i]; | |
} | |
} | |
for (i in current) { | |
if (! (i in orig)) { | |
result.added[i] = current[i]; | |
} | |
} | |
return result; | |
}; | |
doctest.writeDiff = function (orig, current, indentString) { | |
if (typeof orig != 'object' || typeof current != 'object') { | |
writeln(indentString + repr(orig, indentString) + ' -> ' + repr(current, indentString)); | |
return; | |
} | |
indentString = indentString || ''; | |
var diff = doctest.objDiff(orig, current); | |
var i, keys; | |
var any = false; | |
keys = doctest._sortedKeys(diff.added); | |
for (i=0; i<keys.length; i++) { | |
any = true; | |
writeln(indentString + '+' + keys[i] + ': ' | |
+ repr(diff.added[keys[i]], indentString)); | |
} | |
keys = doctest._sortedKeys(diff.removed); | |
for (i=0; i<keys.length; i++) { | |
any = true; | |
writeln(indentString + '-' + keys[i] + ': ' | |
+ repr(diff.removed[keys[i]], indentString)); | |
} | |
keys = doctest._sortedKeys(diff.changed); | |
for (i=0; i<keys.length; i++) { | |
any = true; | |
writeln(indentString + keys[i] + ': ' | |
+ repr(diff.changed[keys[i]][0], indentString) | |
+ ' -> ' | |
+ repr(diff.changed[keys[i]][1], indentString)); | |
} | |
if (! any) { | |
writeln(indentString + '(no changes)'); | |
} | |
}; | |
doctest.objectsEqual = function (ob1, ob2) { | |
var i; | |
if (typeof ob1 != 'object' || typeof ob2 != 'object') { | |
return ob1 === ob2; | |
} | |
for (i in ob1) { | |
if (ob1[i] !== ob2[i]) { | |
return false; | |
} | |
} | |
for (i in ob2) { | |
if (! (i in ob1)) { | |
return false; | |
} | |
} | |
return true; | |
}; | |
doctest.getElementsByTagAndClassName = function (tagName, className, parent/*optional*/) { | |
parent = parent || document; | |
var els = parent.getElementsByTagName(tagName); | |
var result = []; | |
var regexes = []; | |
if (typeof className == 'string') { | |
className = [className]; | |
} | |
for (var i=0; i<className.length; i++) { | |
regexes.push(new RegExp("\\b" + className[i] + "\\b")); | |
} | |
for (i=0; i<els.length; i++) { | |
var el = els[i]; | |
if (el.className) { | |
var passed = true; | |
for (var j=0; j<regexes.length; j++) { | |
if (el.className.search(regexes[j]) == -1) { | |
passed = false; | |
break; | |
} | |
} | |
if (passed) { | |
result.push(el); | |
} | |
} | |
} | |
return result; | |
}; | |
doctest.strip = function (str) { | |
str = str + ""; | |
return str.replace(/\s+$/, "").replace(/^\s+/, ""); | |
}; | |
doctest.rstrip = function (str) { | |
str = str + ""; | |
return str.replace(/\s+$/, ""); | |
}; | |
doctest.escapeHTML = function (s) { | |
return s.replace(/&/g, '&') | |
.replace(/\"/g, """) | |
.replace(/</g, '<') | |
.replace(/>/g, '>'); | |
}; | |
doctest.escapeSpaces = function (s) { | |
return s.replace(/ /g, ' '); | |
}; | |
doctest.extend = function (obj, extendWith) { | |
for (i in extendWith) { | |
obj[i] = extendWith[i]; | |
} | |
return obj; | |
}; | |
doctest.extendDefault = function (obj, extendWith) { | |
for (i in extendWith) { | |
if (typeof obj[i] == 'undefined') { | |
obj[i] = extendWith[i]; | |
} | |
} | |
return obj; | |
}; | |
if (typeof repr == 'undefined') { | |
repr = doctest.repr; | |
} | |
doctest._consoleFunc = function (attr) { | |
if (typeof window.console != 'undefined' | |
&& typeof window.console[attr] != 'undefined') { | |
if (typeof console[attr].apply === 'function') { | |
result = function() { | |
console[attr].apply(console, arguments); | |
}; | |
} else { | |
result = console[attr]; | |
} | |
} else { | |
result = function () { | |
// FIXME: do something | |
}; | |
} | |
return result; | |
}; | |
if (typeof log == 'undefined') { | |
log = doctest._consoleFunc('log'); | |
} | |
if (typeof logDebug == 'undefined') { | |
logDebug = doctest._consoleFunc('log'); | |
} | |
if (typeof logInfo == 'undefined') { | |
logInfo = doctest._consoleFunc('info'); | |
} | |
if (typeof logWarn == 'undefined') { | |
logWarn = doctest._consoleFunc('warn'); | |
} | |
doctest.eval = function () { | |
return window.eval.apply(window, arguments); | |
}; | |
doctest.useCoffeeScript = function (options) { | |
options = options || {}; | |
options.bare = true; | |
options.globals = true; | |
if (! options.fileName) { | |
options.fileName = 'repl'; | |
} | |
if (typeof CoffeeScript == 'undefined') { | |
doctest.logWarn('coffee-script.js is not included'); | |
throw 'coffee-script.js is not included'; | |
} | |
doctest.eval = function (code) { | |
var src = CoffeeScript.compile(code, options); | |
logDebug('Compiled code to:', src); | |
return window.eval(src); | |
}; | |
}; | |
doctest.autoSetup = function (parent) { | |
var tags = doctest.getElementsByTagAndClassName('div', 'test', parent); | |
// First we'll make sure everything has an ID | |
var tagsById = {}; | |
for (var i=0; i<tags.length; i++) { | |
var tagId = tags[i].getAttribute('id'); | |
if (! tagId) { | |
tagId = 'test-' + (++doctest.autoSetup._idCount); | |
tags[i].setAttribute('id', tagId); | |
} | |
// FIXME: test uniqueness here, warn | |
tagsById[tagId] = tags[i]; | |
} | |
// Then fill in the labels | |
for (i=0; i<tags.length; i++) { | |
var el = document.createElement('span'); | |
el.className = 'test-id'; | |
var anchor = document.createElement('a'); | |
anchor.setAttribute('href', '#' + tags[i].getAttribute('id')); | |
anchor.appendChild(document.createTextNode(tags[i].getAttribute('id'))); | |
var button = document.createElement('button'); | |
button.innerHTML = 'test'; | |
button.setAttribute('type', 'button'); | |
button.setAttribute('test-id', tags[i].getAttribute('id')); | |
button.onclick = function () { | |
location.hash = '#' + this.getAttribute('test-id'); | |
location.reload(); | |
}; | |
el.appendChild(anchor); | |
el.appendChild(button); | |
tags[i].insertBefore(el, tags[i].childNodes[0]); | |
} | |
// Lastly, create output areas in each section | |
for (i=0; i<tags.length; i++) { | |
var outEl = doctest.getElementsByTagAndClassName('pre', 'output', tags[i]); | |
if (! outEl.length) { | |
outEl = document.createElement('pre'); | |
outEl.className = 'output'; | |
outEl.setAttribute('id', tags[i].getAttribute('id') + '-output'); | |
} | |
} | |
if (location.hash.length > 1) { | |
// This makes the :target CSS work, since if the hash points to an | |
// element whose id has just been added, it won't be noticed | |
location.hash = location.hash; | |
} | |
var output = document.getElementById('doctestOutput'); | |
if (! tags.length) { | |
tags = document.getElementsByTagName('body'); | |
} | |
if (! output) { | |
output = document.createElement('pre'); | |
output.setAttribute('id', 'doctestOutput'); | |
output.className = 'output'; | |
tags[0].parentNode.insertBefore(output, tags[0]); | |
} | |
var reloader = document.getElementById('doctestReload'); | |
if (! reloader) { | |
reloader = document.createElement('button'); | |
reloader.setAttribute('type', 'button'); | |
reloader.setAttribute('id', 'doctest-testall'); | |
reloader.innerHTML = 'test all'; | |
reloader.onclick = function () { | |
location.hash = '#doctest-testall'; | |
location.reload(); | |
}; | |
output.parentNode.insertBefore(reloader, output); | |
} | |
}; | |
doctest.autoSetup._idCount = 0; | |
doctest.Spy = function (name, options, extraOptions) { | |
var self; | |
if (doctest.spies[name]) { | |
self = doctest.spies[name]; | |
if (! options && ! extraOptions) { | |
return self; | |
} | |
} else { | |
self = function () { | |
return self.func.apply(this, arguments); | |
}; | |
} | |
name = name || 'spy'; | |
options = options || {}; | |
if (typeof options == 'function') { | |
options = {applies: options}; | |
} | |
if (extraOptions) { | |
doctest.extendDefault(options, extraOptions); | |
} | |
doctest.extendDefault(options, doctest.defaultSpyOptions); | |
self._name = name; | |
self.options = options; | |
self.called = false; | |
self.calledWait = false; | |
self.args = null; | |
self.self = null; | |
self.argList = []; | |
self.selfList = []; | |
self.writes = options.writes || false; | |
self.returns = options.returns || null; | |
self.applies = options.applies || null; | |
self.binds = options.binds || null; | |
self.throwError = options.throwError || null; | |
self.ignoreThis = options.ignoreThis || false; | |
self.wrapArgs = options.wrapArgs || false; | |
self.func = function () { | |
self.called = true; | |
self.calledWait = true; | |
self.args = doctest._argsToArray(arguments); | |
self.self = this; | |
self.argList.push(self.args); | |
self.selfList.push(this); | |
// It might be possible to get the caller? | |
if (self.writes) { | |
writeln(self.formatCall()); | |
} | |
if (self.throwError) { | |
throw self.throwError; | |
} | |
if (self.applies) { | |
return self.applies.apply(this, arguments); | |
} | |
return self.returns; | |
}; | |
self.func.toString = function () { | |
return "Spy('" + self._name + "').func"; | |
}; | |
// Method definitions: | |
self.formatCall = function () { | |
var s = ''; | |
if ((! self.ignoreThis) && self.self !== window && self.self !== self) { | |
s += doctest.repr(self.self) + '.'; | |
} | |
s += self._name; | |
if (self.args === null) { | |
return s + ':never called'; | |
} | |
s += '('; | |
for (var i=0; i<self.args.length; i++) { | |
if (i) { | |
s += ', '; | |
} | |
if (self.wrapArgs) { | |
var maxLen = 10; | |
} else { | |
var maxLen = undefined; | |
} | |
s += doctest.repr(self.args[i], '', maxLen); | |
} | |
s += ')'; | |
return s; | |
}; | |
self.method = function (name, options, extraOptions) { | |
var desc = self._name + '.' + name; | |
var newSpy = Spy(desc, options, extraOptions); | |
self[name] = self.func[name] = newSpy.func; | |
return newSpy; | |
}; | |
self.methods = function (props) { | |
for (var i in props) { | |
if (props[i] === props.prototype[i]) { | |
continue; | |
} | |
self.method(i, props[i]); | |
} | |
return self; | |
}; | |
self.wait = function (timeout) { | |
var func = function () { | |
var value = self.calledWait; | |
if (value) { | |
self.calledWait = false; | |
} | |
return value; | |
}; | |
func.repr = function () { | |
return 'called:'+repr(self); | |
}; | |
doctest.wait(func, timeout); | |
}; | |
self.repr = function () { | |
return "Spy('" + self._name + "')"; | |
}; | |
if (options.methods) { | |
self.methods(options.methods); | |
} | |
doctest.spies[name] = self; | |
if (options.wait) { | |
self.wait(); | |
} | |
return self; | |
}; | |
doctest._argsToArray = function (args) { | |
var array = []; | |
for (var i=0; i<args.length; i++) { | |
array.push(args[i]); | |
} | |
return array; | |
}; | |
Spy = doctest.Spy; | |
doctest.spies = {}; | |
doctest.defaultTimeout = 2000; | |
doctest.defaultSpyOptions = {writes: true}; | |
var docTestOnLoad = function () { | |
var auto = false; | |
if (/\bautodoctest\b/.exec(document.body.className)) { | |
doctest.autoSetup(); | |
auto = true; | |
} else { | |
logDebug('No autodoctest class on <body>'); | |
} | |
var loc = window.location.search.substring(1); | |
if (auto || (/doctestRun/).exec(loc)) { | |
var elements = null; | |
// FIXME: we need to put the output near the specific test being tested: | |
if (location.hash) { | |
var el = document.getElementById(location.hash.substr(1)); | |
if (el) { | |
if (/\btest\b/.exec(el.className)) { | |
var testEls = doctest.getElementsByTagAndClassName('pre', 'doctest', el); | |
elements = doctest.getElementsByTagAndClassName('pre', ['doctest', 'setup']); | |
for (var i=0; i<testEls.length; i++) { | |
elements.push(testEls[i]); | |
} | |
} | |
} | |
} | |
doctest(0, elements); | |
} | |
}; | |
if (window.addEventListener) { | |
window.addEventListener('load', docTestOnLoad, false); | |
} else if(window.attachEvent) { | |
window.attachEvent('onload', docTestOnLoad); | |
} | |