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