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); | |
| } | |