Skip to content

Commit

Permalink
support doctests in CoffeeScript files
Browse files Browse the repository at this point in the history
  • Loading branch information
davidchambers committed Jun 3, 2012
1 parent 2785a79 commit 57b7bc9
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 76 deletions.
74 changes: 44 additions & 30 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -150,8 +163,8 @@ MyApp.utils.bar = function() {

### Dependencies

- [jQuery][2]
- [Underscore][3]
- [jQuery][3]
- [Underscore][4]

### Running the test suite

Expand All @@ -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/
33 changes: 22 additions & 11 deletions doctest.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

doctest = (urls...) -> _.each urls, fetch

doctest.version = '0.2.2'
doctest.version = '0.3.0'

doctest.queue = []

Expand Down Expand Up @@ -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
Expand Down
39 changes: 27 additions & 12 deletions doctest.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/server.js
Original file line number Diff line number Diff line change
@@ -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'));
});

Expand Down
98 changes: 98 additions & 0 deletions test/test.coffee
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 5 additions & 1 deletion test/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="utf-8" />
<title>doctest test suite</title>
<script src="/doctest.js"></script>
<script src="http://coffeescript.org/extras/coffee-script.js"></script>
<script src="https://raw.github.com/documentcloud/underscore/master/underscore.js"></script>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="http://code.jquery.com/qunit/git/qunit.js"></script>
Expand Down Expand Up @@ -80,9 +81,12 @@

start()
}
asyncTest('all tests', function() {
asyncTest('JavaScript doctests', function() {
doctest('./test.js')
})
asyncTest('CoffeeScript doctests', function() {
doctest('./test.coffee')
})

})
</script>
Expand Down
Loading

0 comments on commit 57b7bc9

Please sign in to comment.