diff --git a/README.markdown b/README.markdown index 6d5ec3bb..e22cd80f 100644 --- a/README.markdown +++ b/README.markdown @@ -1,41 +1,54 @@ # doctest -A quick and (very) dirty implementation of JavaScript [doctests][1] (which are -occasionally very useful). - -```javascript -// > Math.product(3, 4, 5) -// 60 -// > Math.product(2, "ten") -// NaN -// > Math.product(6) -// undefined -Math.product = function() { - var idx = arguments.length - if (idx) { - var product = arguments[0] - while (--idx) product *= arguments[idx] - return product - } -} +[Doctests][1] are executable usage examples sometimes found in "docstrings". +JavaScript doesn't have docstrings, but inline documentation can be included +in code comments. doctest executes usage examples in JavaScript/CoffeeScript +comments to verify that one's code and inline documentation are in agreement. + +```coffeescript +# ### Math.product +# +# Return the product of two or more numeric values: +# +# > Math.product -2, 2.5, "3" +# -15 +# +# `NaN` is returned if the values cannot all be coerced to numbers: +# +# > Math.product 2, "ten" +# NaN +# +# `undefined` is returned if fewer than two values are provided: +# +# > Math.product 100 +# undefined +# +Math.product = (numbers...) -> + return unless numbers.length + product = 1 + product *= number for number in numbers + product ``` +Doctests needn't be indented, though there's no harm in being +[Docco-friendly][2]. + To run doctests, pass `doctest` paths to one or more "modules" to be tested. Each path should be one of the following: - - an absolute URL; e.g. "http://example.com/scripts/math-extensions.js" - - a root-relative URL; e.g. "/scripts/math-extensions.js" - - a path relative to doctest.js; e.g. "./math-extensions.js" + - an absolute URL; e.g. "http://example.com/scripts/some-module.js" + - a root-relative URL; e.g. "/scripts/some-module.js" + - a path relative to doctest.js; e.g. "./some-module.js" This can easily be done from a browser console: - > doctest("./math-extensions.js") - retrieving /scripts/./math-extensions.js... - running doctests in math-extensions.js... + > doctest("../src/math-extensions.coffee") + retrieving /scripts/lib/../src/math-extensions.coffee... + running doctests in math-extensions.coffee... ..x - expected undefined on line 7 (got 6) + expected undefined on line 16 (got 100) -Oops. Looks like we have a bug. +Oops. Looks like we have a disagreement. ### Errors @@ -150,8 +163,8 @@ MyApp.utils.bar = function() { ### Dependencies - - [jQuery][2] - - [Underscore][3] + - [jQuery][3] + - [Underscore][4] ### Running the test suite @@ -162,5 +175,6 @@ Visit [localhost:3000](http://localhost:3000/). [1]: http://docs.python.org/library/doctest.html -[2]: http://jquery.com/ -[3]: http://documentcloud.github.com/underscore/ +[2]: http://bit.ly/LaeTsw +[3]: http://jquery.com/ +[4]: http://documentcloud.github.com/underscore/ diff --git a/doctest.coffee b/doctest.coffee index 38057eec..7ee5ff89 100644 --- a/doctest.coffee +++ b/doctest.coffee @@ -12,7 +12,7 @@ doctest = (urls...) -> _.each urls, fetch -doctest.version = '0.2.2' +doctest.version = '0.3.0' doctest.queue = [] @@ -52,29 +52,40 @@ fetch = (url) -> console.log "retrieving #{url}..." jQuery.ajax url, dataType: 'text', success: (text) -> - console.log "running doctests in #{/[^/]+$/.exec url}..." + [name, type] = /[^/]+[.](coffee|js)$/.exec url + console.log "running doctests in #{name}..." + source = rewrite text, type + source = CoffeeScript.compile source if type is 'coffee' # Functions created via `Function` are always run in the `window` # context, which ensures that doctests can't access variables in # _this_ context. A doctest which assigns to or references `text` # sets/gets `window.text`, not this function's `text` parameter. - do Function rewrite text + do Function source doctest.run() -rewrite = (text) -> - f = (expr) -> "function() {\n return #{expr}\n}" +rewrite = (text, type) -> + f = (expr) -> + switch type + when 'coffee' then "->\n#{indent} #{expr}\n#{indent}" + when 'js' then "function() {\n#{indent} return #{expr}\n#{indent}}" + + comments = + coffee: /^([ \t]*)#[ \t]*(.+)/ + js: /^([ \t]*)\/\/[ \t]*(.+)/ + lines = []; expr = '' for line, idx in text.split /\r?\n|\r/ - if match = /^[ \t]*\/\/[ \t]*(.+)/.exec line - comment = match[1] + if match = comments[type].exec line + [match, indent, comment] = match if match = /^>(.*)/.exec comment - lines.push "doctest.input(#{f expr});" if expr + lines.push "#{indent}doctest.input(#{f expr});" if expr expr = match[1] else if match = /^[.](.*)/.exec comment - expr += '\n' + match[1] + expr += "\n#{indent} #{match[1]}" else if expr - lines.push "doctest.input(#{f expr});" - lines.push "doctest.output(#{idx + 1}, #{f comment});" + lines.push "#{indent}doctest.input(#{f expr});" + lines.push "#{indent}doctest.output(#{idx + 1}, #{f comment});" expr = '' else lines.push line diff --git a/doctest.js b/doctest.js index 6b317f4e..ce6c34dc 100644 --- a/doctest.js +++ b/doctest.js @@ -22,7 +22,7 @@ return _.each(urls, fetch); }; - doctest.version = '0.2.2'; + doctest.version = '0.3.0'; doctest.queue = []; @@ -100,35 +100,50 @@ return jQuery.ajax(url, { dataType: 'text', success: function(text) { - console.log("running doctests in " + (/[^/]+$/.exec(url)) + "..."); - Function(rewrite(text))(); + var name, source, type, _ref; + _ref = /[^/]+[.](coffee|js)$/.exec(url), name = _ref[0], type = _ref[1]; + console.log("running doctests in " + name + "..."); + source = rewrite(text, type); + if (type === 'coffee') { + source = CoffeeScript.compile(source); + } + Function(source)(); return doctest.run(); } }); }; - rewrite = function(text) { - var comment, expr, f, idx, line, lines, match, _i, _len, _ref; + rewrite = function(text, type) { + var comment, comments, expr, f, idx, indent, line, lines, match, _i, _len, _ref, _ref1; f = function(expr) { - return "function() {\n return " + expr + "\n}"; + switch (type) { + case 'coffee': + return "->\n" + indent + " " + expr + "\n" + indent; + case 'js': + return "function() {\n" + indent + " return " + expr + "\n" + indent + "}"; + } + }; + comments = { + coffee: /^([ \t]*)#[ \t]*(.+)/, + js: /^([ \t]*)\/\/[ \t]*(.+)/ }; lines = []; expr = ''; _ref = text.split(/\r?\n|\r/); for (idx = _i = 0, _len = _ref.length; _i < _len; idx = ++_i) { line = _ref[idx]; - if (match = /^[ \t]*\/\/[ \t]*(.+)/.exec(line)) { - comment = match[1]; + if (match = comments[type].exec(line)) { + _ref1 = match, match = _ref1[0], indent = _ref1[1], comment = _ref1[2]; if (match = /^>(.*)/.exec(comment)) { if (expr) { - lines.push("doctest.input(" + (f(expr)) + ");"); + lines.push("" + indent + "doctest.input(" + (f(expr)) + ");"); } expr = match[1]; } else if (match = /^[.](.*)/.exec(comment)) { - expr += '\n' + match[1]; + expr += "\n" + indent + " " + match[1]; } else if (expr) { - lines.push("doctest.input(" + (f(expr)) + ");"); - lines.push("doctest.output(" + (idx + 1) + ", " + (f(comment)) + ");"); + lines.push("" + indent + "doctest.input(" + (f(expr)) + ");"); + lines.push("" + indent + "doctest.output(" + (idx + 1) + ", " + (f(comment)) + ");"); expr = ''; } } else { diff --git a/test/server.js b/test/server.js index 83b9be2c..d020d694 100644 --- a/test/server.js +++ b/test/server.js @@ -1,6 +1,6 @@ var app = require('express').createServer(); -app.get(/^\/((doc)?test[.]js)?$/, function (req, res) { +app.get(/^\/((doc)?test[.](coffee|js))?$/, function (req, res) { res.sendfile(__dirname + '/' + (req.route.params[0] || 'test.html')); }); diff --git a/test/test.coffee b/test/test.coffee new file mode 100644 index 00000000..bce76ee9 --- /dev/null +++ b/test/test.coffee @@ -0,0 +1,98 @@ +1: 'global variable accessible in outer scope' +# > global +# "global" +global = 'global' + +do -> + + 2: 'global variable accessible in inner scope' + # > global + # "global" + do (global = 'shadowed') -> + 3: 'local variable referenced, not shadowed global' + # > global + # "shadowed" + + + + 4: 'local variable accessible before declaration' + # > one * two + # 2 + one = 1 + two = 2 + 5: 'assignment is an expression' + # > @three = one + two + # 3 + 6: 'variable declared in doctest remains accessible' + # > [one, two, three] + # [1, 2, 3] + 7: 'arithmetic error reported' + # > two + two + # 5 + + 8: 'TypeError captured and reported' + # > null.length + # TypeError + 9: 'TypeError expected but not reported' + # > [].length + # TypeError + + 10: 'function accessible before declaration' + # > double(6) + # 12 + 11: 'NaN can be used as expected result' + # > double() + # NaN + double = (n) -> + # doctests should only be included in contexts where they'll be + # invoked immediately (i.e. at the top level or within an IIFE) + 2 * n + + 12: 'function accessible after declaration' + # > double.call(null, 2) + # 4 + + triple = (n) -> + # > this.doctest.should.never.be.executed + # ( blow.up.if.for.some.reason.it.is ) + 3 * n + + + 13: 'multiline input' + # > [1,2,3, + # . 4,5,6, + # . 7,8,9] + # [1,2,3,4,5,6,7,8,9] + 14: 'multiline assignment' + # > @text = "input " + + # . "may span many " + + # . "lines" + # > text + # "input may span many lines" + + 15: 'spaces following "//" and ">" are optional' + #>"no spaces" + #"no spaces" + 16: 'indented doctest' + # > "Docco-compatible whitespace" + # "Docco-compatible whitespace" + 17: '">" in doctest' + # > 2 > 1 + # true + + 18: 'comment on input line' + # > "foo" + "bar" # comment + # "foobar" + 19: 'comment on output line' + # > 5 * 5 + # 25 + + 20: 'variable in creation context is not accessible' + # > @text = "The quick brown fox jumps over the lazy dog" + # > window.text + # "The quick brown fox jumps over the lazy dog" + + 21: 'the rewriter should not rely on automatic semicolon insertion' + # > "the rewriter should not rely" + # "on automatic semicolon insertion" + (4 + 4) diff --git a/test/test.html b/test/test.html index 45633762..ebc91bee 100644 --- a/test/test.html +++ b/test/test.html @@ -4,6 +4,7 @@