├── .config ├── .eslintignore ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── doctest └── package.json ├── lib ├── Comparison.js ├── Effect.js ├── Line.js ├── command.js ├── doctest.js ├── program.js └── require.js ├── package.json ├── scripts ├── doctest └── test └── test ├── bin ├── executable └── results.js ├── commonjs ├── __dirname │ ├── index.coffee │ ├── index.js │ ├── results.coffee.js │ └── results.js ├── __doctest.require │ ├── index.coffee │ ├── index.js │ └── results.js ├── __filename │ ├── index.coffee │ ├── index.js │ ├── results.coffee.js │ └── results.js ├── exports │ ├── index.js │ └── results.js ├── module.exports │ ├── index.js │ └── results.js ├── require │ ├── index.js │ └── results.js └── strict │ ├── index.js │ └── results.js ├── contiguity ├── index.coffee ├── index.js └── results.js ├── es2015 ├── index.js └── results.js ├── es2018 ├── index.js └── results.js ├── es2020 ├── index.js └── results.js ├── esm ├── dependencies.js ├── globals │ ├── index.js │ └── results.js ├── incorrect.js └── index.js ├── exceptions ├── index.js └── results.js ├── fantasy-land ├── index.js └── results.js ├── index.js ├── line-endings ├── CR+LF.coffee ├── CR+LF.js ├── CR.coffee ├── CR.js ├── LF.coffee ├── LF.js └── results.js ├── shared ├── index.coffee ├── index.js ├── results.coffee.js └── results.js ├── statements ├── index.js └── results.js └── transcribe ├── index.coffee ├── index.js └── results.js /.config: -------------------------------------------------------------------------------- 1 | repo-owner = davidchambers 2 | repo-name = doctest 3 | author-name = David Chambers 4 | source-files = bin/doctest lib/**/*.js 5 | readme-source-files = 6 | test-files = test/index.js 7 | version-tag-prefix = 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /test/commonjs/exports/index.js 2 | /test/commonjs/module.exports/index.js 3 | /test/commonjs/require/index.js 4 | /test/es2015/index.js 5 | /test/exceptions/index.js 6 | /test/fantasy-land/index.js 7 | /test/line-endings/CR+LF.js 8 | /test/line-endings/CR.js 9 | /test/shared/index.js 10 | /test/transcribe/index.js 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["./node_modules/sanctuary-style/eslint.json"], 4 | "parserOptions": {"ecmaVersion": 2020, "sourceType": "module"}, 5 | "env": {"node": true}, 6 | "rules": { 7 | "comma-dangle": ["error", {"arrays": "always-multiline", "objects": "always-multiline", "imports": "always-multiline", "exports": "always-multiline", "functions": "never"}] 8 | }, 9 | "overrides": [ 10 | { 11 | "files": ["bin/doctest"], 12 | "parserOptions": {"sourceType": "script"} 13 | }, 14 | { 15 | "files": ["lib/doctest.js"], 16 | "rules": { 17 | "no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 0}], 18 | "spaced-comment": ["error", "always", {"markers": ["/"]}] 19 | } 20 | }, 21 | { 22 | "files": ["test/**/*.js"], 23 | "rules": { 24 | "max-len": ["off"] 25 | } 26 | }, 27 | { 28 | "files": ["test/commonjs/**/index.js"], 29 | "parserOptions": {"sourceType": "script"}, 30 | "globals": {"__doctest": "readonly"} 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [davidchambers] 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | node-version: [16.2.0, 18, 20] 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - run: npm install 20 | - run: npm test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Update local main branch: 4 | 5 | $ git checkout main 6 | $ git pull upstream main 7 | 8 | 2. Create feature branch: 9 | 10 | $ git checkout -b feature-x 11 | 12 | 3. Make one or more atomic commits, and ensure that each commit has a 13 | descriptive commit message. Commit messages should be line wrapped 14 | at 72 characters. 15 | 16 | 4. Run `npm test`, and address any errors. Preferably, fix commits in place 17 | using `git rebase` or `git commit --amend` to make the changes easier to 18 | review. 19 | 20 | 5. Push: 21 | 22 | $ git push origin feature-x 23 | 24 | 6. Open a pull request. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 3 | Version 2, December 2004 4 | 5 | Copyright (c) 2023 David Chambers 6 | 7 | Everyone is permitted to copy and distribute verbatim or modified 8 | copies of this license document, and changing it is allowed as long 9 | as the name is changed. 10 | 11 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 12 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 13 | 14 | 0. You just DO WHAT THE FUCK YOU WANT TO. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doctest 2 | 3 | [Doctests][1] are executable usage examples sometimes found in "docstrings". 4 | JavaScript doesn't have docstrings, but inline documentation can be included 5 | in code comments. doctest finds and evaluates usage examples in code comments 6 | and reports any inaccuracies. doctest works with JavaScript and CoffeeScript 7 | modules. 8 | 9 | ### Example 10 | 11 | ```javascript 12 | // toFahrenheit :: Number -> Number 13 | // 14 | // Convert degrees Celsius to degrees Fahrenheit. 15 | // 16 | // > toFahrenheit(0) 17 | // 32 18 | // > toFahrenheit(100) 19 | // 212 20 | function toFahrenheit(degreesCelsius) { 21 | return degreesCelsius * 9 / 5 + 32; 22 | } 23 | ``` 24 | 25 | Doctest will execute `toFahrenheit(0)` and verify that its output is `32`. 26 | 27 | ### Installation 28 | 29 | ```console 30 | $ npm install doctest 31 | ``` 32 | 33 | ### Running doctests 34 | 35 | Test a module via JavaScript API: 36 | 37 | ```javascript 38 | > doctest ({}) ('lib/temperature.js') 39 | ``` 40 | 41 | Test a module via command-line interface: 42 | 43 | ```console 44 | $ doctest lib/temperature.js 45 | ``` 46 | 47 | The exit code is 0 if all tests pass, 1 otherwise. 48 | 49 | ### Supported module systems 50 | 51 | | Module system | Option | Node.js | Dependencies | 52 | | --------------------- | ------------- |:-------------:|:-------------:| 53 | | CommonJS | `commonjs` | ✔︎ | ✔︎ | 54 | | ECMAScript modules | `esm` | ✔︎ | ✔︎ | 55 | 56 | Specify module system via JavaScript API: 57 | 58 | ```javascript 59 | > doctest ({module: 'esm'}) ('path/to/esm/module.js') 60 | ``` 61 | 62 | Specify module system via command-line interface: 63 | 64 | ```console 65 | $ doctest --module commonjs path/to/commonjs/module.js 66 | ``` 67 | 68 | ### Line wrapping 69 | 70 | Input lines may be wrapped by beginning each continuation with FULL STOP (`.`): 71 | 72 | ```javascript 73 | // > reverse([ 74 | // . 'foo', 75 | // . 'bar', 76 | // . 'baz', 77 | // . ]) 78 | // ['baz', 'bar', 'foo'] 79 | ``` 80 | 81 | Output lines may be wrapped in the same way: 82 | 83 | ```javascript 84 | // > reverse([ 85 | // . 'foo', 86 | // . 'bar', 87 | // . 'baz', 88 | // . ]) 89 | // [ 'baz', 90 | // . 'bar', 91 | // . 'foo' ] 92 | ``` 93 | 94 | ### Exceptions 95 | 96 | An output line beginning with EXCLAMATION MARK (`!`) indicates that the 97 | preceding expression is expected to throw. The exclamation mark *must* be 98 | followed by SPACE ( ) and the name of an Error constructor. 99 | For example: 100 | 101 | ```javascript 102 | // > null.length 103 | // ! TypeError 104 | ``` 105 | 106 | The constructor name *may* be followed by COLON (`:`), SPACE ( ), 107 | and the expected error message. For example: 108 | 109 | ```javascript 110 | // > null.length 111 | // ! TypeError: Cannot read property 'length' of null 112 | ``` 113 | 114 | ### Scoping 115 | 116 | Each doctest has access to variables in its scope chain. 117 | 118 | ### Integrations 119 | 120 | - [Grunt](https://gruntjs.com/): 121 | [paolodm/grunt-doctest](https://github.com/paolodm/grunt-doctest) 122 | 123 | ### Running the test suite 124 | 125 | ```console 126 | $ npm install 127 | $ npm test 128 | ``` 129 | 130 | 131 | [1]: https://docs.python.org/library/doctest.html 132 | -------------------------------------------------------------------------------- /bin/doctest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const {spawn} = require ('node:child_process'); 6 | const path = require ('node:path'); 7 | const process = require ('node:process'); 8 | 9 | const command = process.execPath; 10 | 11 | const args = [ 12 | ...process.execArgv, 13 | '--experimental-import-meta-resolve', 14 | '--experimental-vm-modules', 15 | '--', 16 | path.resolve (__dirname, '..', 'lib', 'command.js'), 17 | ...(process.argv.slice (2)), 18 | ]; 19 | 20 | const options = { 21 | cwd: process.cwd (), 22 | env: process.env, 23 | stdio: [0, 1, 2], 24 | }; 25 | 26 | spawn (command, args, options) 27 | .on ('exit', process.exit); 28 | -------------------------------------------------------------------------------- /bin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /lib/Comparison.js: -------------------------------------------------------------------------------- 1 | // Incorrect :: Array Effect -> Array Effect -> Comparison 2 | const Incorrect = actual => expected => ({ 3 | tag: 'Incorrect', 4 | actual, 5 | expected, 6 | }); 7 | 8 | // Correct :: Array Effect -> Comparison 9 | const Correct = actual => ({ 10 | tag: 'Correct', 11 | actual, 12 | }); 13 | 14 | // comparison :: (Array Effect -> Array Effect -> a) 15 | // -> (Array Effect -> a) 16 | // -> Comparison 17 | // -> a 18 | const comparison = incorrect => correct => comparison => { 19 | switch (comparison.tag) { 20 | case 'Incorrect': 21 | return incorrect (comparison.actual) (comparison.expected); 22 | case 'Correct': 23 | return correct (comparison.actual); 24 | } 25 | }; 26 | 27 | 28 | export {Incorrect, Correct, comparison}; 29 | -------------------------------------------------------------------------------- /lib/Effect.js: -------------------------------------------------------------------------------- 1 | // Failure :: Any -> Effect 2 | const Failure = exception => ({ 3 | tag: 'Failure', 4 | exception, 5 | }); 6 | 7 | // Success :: Any -> Effect 8 | const Success = value => ({ 9 | tag: 'Success', 10 | value, 11 | }); 12 | 13 | // effect :: (Any -> a) -> (Any -> a) -> Effect -> a 14 | const effect = failure => success => effect => { 15 | switch (effect.tag) { 16 | case 'Failure': return failure (effect.exception); 17 | case 'Success': return success (effect.value); 18 | } 19 | }; 20 | 21 | // encase :: AnyFunction -> ...Any -> Effect 22 | const encase = f => (...args) => { 23 | try { 24 | return Success (f (...args)); 25 | } catch (exception) { 26 | return Failure (exception); 27 | } 28 | }; 29 | 30 | 31 | export {Failure, Success, effect, encase}; 32 | -------------------------------------------------------------------------------- /lib/Line.js: -------------------------------------------------------------------------------- 1 | // Line :: Integer -> String -> Line 2 | const Line = number => text => ({number, text}); 3 | 4 | 5 | export {Line}; 6 | -------------------------------------------------------------------------------- /lib/command.js: -------------------------------------------------------------------------------- 1 | import {comparison} from './Comparison.js'; 2 | import doctest from './doctest.js'; 3 | import program from './program.js'; 4 | 5 | 6 | Promise.all ( 7 | program.args.map (path => 8 | doctest (program) (path) 9 | .then (tests => tests.reduce ( 10 | (status, test) => comparison (_ => _ => 1) 11 | (_ => status) 12 | (test.comparison), 13 | 0 14 | )) 15 | ) 16 | ) 17 | .then ( 18 | statuses => { 19 | process.exit (statuses.every (s => s === 0) ? 0 : 1); 20 | }, 21 | err => { 22 | process.stderr.write (`${err}\n`); 23 | process.exit (1); 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /lib/doctest.js: -------------------------------------------------------------------------------- 1 | 2 | /// >>> 3 | /// >>> >>> >>> 4 | /// >>>>>>>> >>>>>>> >>>>>>> >>>>> >>>>>>> >>>>>> >>>>> 5 | /// >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> 6 | /// >>> >>> >>> >>> >>> >>> >>>>>>>>> >>>>>>>> >>> 7 | /// >>> >>> >>> >>> >>> >>> >>> >>> >>> >>> 8 | /// >>>>>>>> >>>>>>> >>>>>>> >>>> >>>>>>> >>>>>> >>>> 9 | /// .....................x.......xx.x................................. 10 | 11 | import fs from 'node:fs/promises'; 12 | import {dirname, resolve} from 'node:path'; 13 | import url from 'node:url'; 14 | import vm from 'node:vm'; 15 | 16 | import * as acorn from 'acorn'; 17 | import CoffeeScript from 'coffeescript'; 18 | import show from 'sanctuary-show'; 19 | import Z from 'sanctuary-type-classes'; 20 | 21 | import {Incorrect, Correct, comparison} from './Comparison.js'; 22 | import {Failure, Success, effect, encase} from './Effect.js'; 23 | import {Line} from './Line.js'; 24 | import require from './require.js'; 25 | 26 | 27 | // formatLines :: String -> NonEmpty (Array Line) -> String 28 | const formatLines = indent => lines => ( 29 | lines 30 | .map (line => `{number: ${show (line.number)}, text: ${show (line.text)}},`) 31 | .join (`\n${indent}`) 32 | ); 33 | 34 | // formatInput :: String -> NonEmpty (Array Line) -> String 35 | const formatInput = indent => lines => ( 36 | lines 37 | .map (line => line.text.replace (/^\s*([>]|[.]+)[ ]?/, '')) 38 | .join (`\n${indent}`) 39 | ); 40 | 41 | // formatOutput :: String -> NonEmpty (Array Line) -> String 42 | const formatOutput = indent => lines => { 43 | const [head, ...tail] = lines.map (line => line.text.replace (/^\s*/, '')); 44 | const match = /^![ ]?([^:]*)(?::[ ]?(.*))?$/.exec (head); 45 | return [ 46 | `${head.startsWith ('!') ? 'throw' : 'return'} (`, 47 | ` ${match == null ? head : `new ${match[1]}(${show (match[2] ?? '')})`}`, 48 | ...(tail.map (text => ' ' + text.replace (/^[.]+[ ]?/, ''))), 49 | ')', 50 | ].join (`\n${indent}`); 51 | }; 52 | 53 | const wrapJs = sourceType => ({input, output}) => { 54 | const source = formatInput ('') (input.lines); 55 | const ast = acorn.parse ( 56 | source.startsWith ('{') && source.endsWith ('}') ? `(${source})` : source, 57 | {ecmaVersion: 2023, sourceType} 58 | ); 59 | const {type} = ast.body[0]; 60 | if (type !== 'ExpressionStatement') return source; 61 | 62 | return ` 63 | __doctest.enqueue({ 64 | input: { 65 | lines: [ 66 | ${formatLines (' ') (input.lines)} 67 | ], 68 | thunk: () => { 69 | return ( 70 | ${formatInput (' ') (input.lines)} 71 | ); 72 | }, 73 | }, 74 | output: ${output && `{ 75 | lines: [ 76 | ${formatLines (' ') (output.lines)} 77 | ], 78 | thunk: () => { 79 | ${formatOutput (' ') (output.lines)}; 80 | }, 81 | }`}, 82 | });`; 83 | }; 84 | 85 | const wrapCoffee = ({indent, input, output}) => ` 86 | ${indent}__doctest.enqueue { 87 | ${indent} input: { 88 | ${indent} lines: [ 89 | ${indent} ${formatLines (`${indent} `) (input.lines)} 90 | ${indent} ] 91 | ${indent} thunk: -> 92 | ${indent} ${formatInput (`${indent} `) (input.lines)} 93 | ${indent} } 94 | ${indent} output: ${output && `{ 95 | ${indent} lines: [ 96 | ${indent} ${formatLines (`${indent} `) (output.lines)} 97 | ${indent} ] 98 | ${indent} thunk: -> 99 | ${indent} ${formatOutput (`${indent} `) (output.lines)} 100 | ${indent} }`} 101 | ${indent}} 102 | `; 103 | 104 | // contiguous :: Line -> NonEmpty (Array Line) -> Boolean 105 | const contiguous = line => lines => ( 106 | line.number === lines[lines.length - 1].number + 1 107 | ); 108 | 109 | const rewriteJs = sourceType => ({ 110 | prefix, 111 | openingDelimiter, 112 | closingDelimiter, 113 | }) => input => { 114 | // 1: Parse source text to extract comments 115 | const comments = []; 116 | acorn.parse (input, { 117 | ecmaVersion: 2023, 118 | sourceType, 119 | locations: true, 120 | onComment: comments, 121 | }); 122 | 123 | // 2: Preserve source text between comments 124 | const chunks = []; 125 | { 126 | let offset = 0; 127 | for (const {start, end} of comments) { 128 | chunks.push ([offset, input.slice (offset, start)]); 129 | offset = end; 130 | } 131 | chunks.push ([offset, input.slice (offset)]); 132 | } 133 | 134 | // 3: Extract prefixed comment lines 135 | const lines = []; 136 | { 137 | const maybePushLine = (text, offset, number) => { 138 | if (text.startsWith (prefix)) { 139 | const unprefixed = (text.slice (prefix.length)).trimStart (); 140 | lines.push ([offset, Line (number) (unprefixed)]); 141 | } 142 | }; 143 | for (const {type, value, start, loc} of comments) { 144 | if (type === 'Line') { 145 | maybePushLine (value, start, loc.start.line); 146 | } else { 147 | for (let from = 0, number = loc.start.line; ; number += 1) { 148 | const to = value.indexOf ('\n', from); 149 | const text = to < 0 ? value.slice (from) : value.slice (from, to); 150 | maybePushLine (text.replace (/^\s*[*]/, ''), start + from, number); 151 | if (to < 0) break; 152 | from = to + '\n'.length; 153 | } 154 | } 155 | } 156 | } 157 | 158 | // 4: Coalesce related input and output lines 159 | const tests = []; 160 | { 161 | let test; 162 | let state = openingDelimiter == null ? 'open' : 'closed'; 163 | for (const [offset, line] of lines) { 164 | if (state === 'closed') { 165 | if (line.text === openingDelimiter) state = 'open'; 166 | } else if (line.text === closingDelimiter) { 167 | state = 'closed'; 168 | } else if (line.text.startsWith ('>')) { 169 | tests.push ([offset, test = {input: {lines: [line]}}]); 170 | state = 'input'; 171 | } else if (line.text.startsWith ('.')) { 172 | test[state].lines.push (line); 173 | } else if (state === 'input') { 174 | // A comment immediately following an input line is an output 175 | // line if and only if it contains non-whitespace characters. 176 | if (contiguous (line) (test.input.lines) && line.text !== '') { 177 | test.output = {lines: [line]}; 178 | state = 'output'; 179 | } else { 180 | state = 'open'; 181 | } 182 | } 183 | } 184 | } 185 | 186 | // 5: Convert doctests to source text 187 | for (const [offset, test] of tests) { 188 | chunks.push ([offset, wrapJs (sourceType) (test)]); 189 | } 190 | 191 | // 6: Sort verbatim and generated source text by original offsets 192 | chunks.sort (([a], [b]) => a - b); 193 | 194 | // 7: Concatenate source text 195 | let sourceText = ''; 196 | for (const [, text] of chunks) sourceText += text; 197 | return sourceText; 198 | }; 199 | 200 | const rewriteCoffee = ({ 201 | prefix, 202 | openingDelimiter, 203 | closingDelimiter, 204 | }) => input => { 205 | // 1: Lex source text to extract comments 206 | const tokens = CoffeeScript.tokens (input); 207 | const comments = CoffeeScript.helpers.extractAllCommentTokens (tokens); 208 | 209 | // 2: Preserve source text between comments 210 | const chunks = []; 211 | { 212 | let offset = 0; 213 | for (const {locationData: {range: [start, end]}} of comments) { 214 | chunks.push ([offset, input.slice (offset, start)]); 215 | offset = end; 216 | } 217 | chunks.push ([offset, input.slice (offset)]); 218 | } 219 | 220 | // 3: Extract prefixed comment lines 221 | const lines = []; 222 | for (const {content, locationData} of comments) { 223 | const indent = ' '.repeat (locationData.first_column); 224 | const offset = locationData.range[0]; 225 | let number = locationData.first_line + 1; 226 | if (locationData.last_line > locationData.first_line) { 227 | for (let from = 0; ; number += 1) { 228 | const to = content.indexOf ('\n', from); 229 | const text = to < 0 ? content.slice (from) : content.slice (from, to); 230 | const line = Line (number) (text.trimStart ()); 231 | lines.push ([offset + from, indent, line]); 232 | if (to < 0) break; 233 | from = to + '\n'.length; 234 | } 235 | } else { 236 | const text = content.trimStart (); 237 | if (text.startsWith (prefix)) { 238 | const unprefixed = (text.slice (prefix.length)).trimStart (); 239 | lines.push ([offset, indent, Line (number) (unprefixed)]); 240 | } 241 | } 242 | } 243 | 244 | // 4: Coalesce related input and output lines 245 | const tests = []; 246 | { 247 | let test; 248 | let state = openingDelimiter == null ? 'open' : 'closed'; 249 | for (const [offset, indent, line] of lines) { 250 | if (state === 'closed') { 251 | if (line.text === openingDelimiter) state = 'open'; 252 | } else if (line.text === closingDelimiter) { 253 | state = 'closed'; 254 | } else if (line.text.startsWith ('>')) { 255 | tests.push ([offset, test = {indent, input: {lines: [line]}}]); 256 | state = 'input'; 257 | } else if (line.text.startsWith ('.')) { 258 | test[state].lines.push (line); 259 | } else if (state === 'input') { 260 | // A comment immediately following an input line is an output 261 | // line if and only if it contains non-whitespace characters. 262 | if (contiguous (line) (test.input.lines) && line.text !== '') { 263 | test.output = {lines: [line]}; 264 | state = 'output'; 265 | } else { 266 | state = 'open'; 267 | } 268 | } 269 | } 270 | } 271 | 272 | // 5: Convert doctests to source text 273 | for (const [offset, test] of tests) { 274 | chunks.push ([offset, wrapCoffee (test)]); 275 | } 276 | 277 | // 6: Sort verbatim and generated source text by original offsets 278 | chunks.sort (([a], [b]) => a - b); 279 | 280 | // 7: Concatenate source text 281 | let sourceText = ''; 282 | for (const [, text] of chunks) sourceText += text; 283 | return CoffeeScript.compile (sourceText); 284 | }; 285 | 286 | const run = queue => 287 | queue.flatMap (({input, output}) => { 288 | const i = encase (input.thunk) (); 289 | if (output == null) return []; 290 | const o = encase (output.thunk) (); 291 | const comparison = ( 292 | effect (o => effect (i => i.name === o.name && 293 | i.message === (o.message || i.message) ? 294 | Correct (Failure (i)) : 295 | Incorrect (Failure (i)) (Failure (o))) 296 | (i => Incorrect (Success (i)) (Failure (o)))) 297 | (o => effect (i => Incorrect (Failure (i)) (Success (o))) 298 | (i => Z.equals (i, o) ? 299 | Correct (Success (i)) : 300 | Incorrect (Success (i)) (Success (o)))) 301 | (o) 302 | (i) 303 | ); 304 | return [{lines: {input: input.lines, output: output.lines}, comparison}]; 305 | }); 306 | 307 | const evaluateModule = moduleUrl => async source => { 308 | const queue = []; 309 | const enqueue = io => { queue.push (io); }; 310 | const __doctest = {enqueue}; 311 | const context = vm.createContext ({...global, __doctest}); 312 | const module = new vm.SourceTextModule (source, {context}); 313 | await module.link (async (specifier, referencingModule) => { 314 | // import.meta.resolve returned a promise prior to Node.js v20.0.0. 315 | const importUrl = await import.meta.resolve (specifier, moduleUrl); 316 | const entries = Object.entries (await import (importUrl)); 317 | const module = new vm.SyntheticModule ( 318 | entries.map (([name]) => name), 319 | () => { 320 | for (const [name, value] of entries) { 321 | module.setExport (name, value); 322 | } 323 | }, 324 | {identifier: specifier, context: referencingModule.context} 325 | ); 326 | return module; 327 | }); 328 | await module.evaluate (); 329 | return run (queue); 330 | }; 331 | 332 | const evaluateScript = context => async source => { 333 | const queue = []; 334 | const enqueue = io => { queue.push (io); }; 335 | const __doctest = {enqueue, require}; 336 | vm.runInNewContext (source, {...global, ...context, __doctest}); 337 | return run (queue); 338 | }; 339 | 340 | const log = tests => { 341 | console.log ( 342 | tests 343 | .map (test => comparison (_ => _ => 'x') (_ => '.') (test.comparison)) 344 | .join ('') 345 | ); 346 | for (const test of tests) { 347 | comparison 348 | (actual => expected => { 349 | console.log (`FAIL: expected ${ 350 | effect (x => `! ${x}`) (show) (expected) 351 | } on line ${ 352 | test.lines.output[test.lines.output.length - 1].number 353 | } (got ${ 354 | effect (x => `! ${x}`) (show) (actual) 355 | })`); 356 | }) 357 | (_ => {}) 358 | (test.comparison); 359 | } 360 | }; 361 | 362 | const test = options => path => rewrite => async evaluate => { 363 | const originalSource = await fs.readFile (path, 'utf8'); 364 | const modifiedSource = ( 365 | rewrite ({prefix: options.prefix ?? '', 366 | openingDelimiter: options.openingDelimiter, 367 | closingDelimiter: options.closingDelimiter}) 368 | (originalSource 369 | .replace (/\r\n?/g, '\n') 370 | .replace (/^#!.*/, '')) 371 | ); 372 | if (options.print) { 373 | console.log (modifiedSource.replace (/\n$/, '')); 374 | return []; 375 | } else { 376 | const results = await evaluate (modifiedSource); 377 | if (!options.silent) { 378 | console.log (`running doctests in ${path}...`); 379 | log (results); 380 | } 381 | return results; 382 | } 383 | }; 384 | 385 | export default options => async path => { 386 | const __filename = resolve (process.cwd (), path); 387 | let context = {}; 388 | switch (options.module) { 389 | case 'esm': { 390 | return test (options) 391 | (path) 392 | (rewriteJs ('module')) 393 | (evaluateModule (url.pathToFileURL (__filename))); 394 | } 395 | case 'commonjs': { 396 | const exports = {}; 397 | const module = {exports}; 398 | const __dirname = dirname (__filename); 399 | context = {process, exports, module, require, __dirname, __filename}; 400 | } // fall through 401 | case undefined: { 402 | return test (options) 403 | (path) 404 | (options.coffee ? rewriteCoffee : rewriteJs ('script')) 405 | (evaluateScript (context)); 406 | } 407 | default: { 408 | throw new Error (`Invalid module ${show (options.module)}`); 409 | } 410 | } 411 | /* c8 ignore next */ 412 | }; 413 | -------------------------------------------------------------------------------- /lib/program.js: -------------------------------------------------------------------------------- 1 | import program from 'commander'; 2 | 3 | import require from './require.js'; 4 | 5 | 6 | const pkg = require ('../package.json'); 7 | 8 | program 9 | .version (pkg.version) 10 | .usage ('[options] path/to/js/or/coffee/module') 11 | .option ('-m, --module ', 12 | 'specify module system ("commonjs" or "esm")') 13 | .option (' --coffee', 14 | 'parse CoffeeScript files') 15 | .option (' --prefix ', 16 | 'specify Transcribe-style prefix (e.g. ".")') 17 | .option (' --opening-delimiter ', 18 | 'specify line preceding doctest block (e.g. "```javascript")') 19 | .option (' --closing-delimiter ', 20 | 'specify line following doctest block (e.g. "```")') 21 | .option ('-p, --print', 22 | 'output the rewritten source without running tests') 23 | .option ('-s, --silent', 24 | 'suppress output') 25 | .parse (process.argv); 26 | 27 | export default program; 28 | -------------------------------------------------------------------------------- /lib/require.js: -------------------------------------------------------------------------------- 1 | import module from 'node:module'; 2 | import url from 'node:url'; 3 | 4 | export default module.createRequire (url.fileURLToPath (import.meta.url)); 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doctest", 3 | "version": "0.21.0", 4 | "description": "Doctests for JavaScript and CoffeeScript", 5 | "license": "WTFPL", 6 | "contributors": [ 7 | "Aldwin Vlasblom ", 8 | "David Chambers ", 9 | "Francesco Occhipinti " 10 | ], 11 | "keywords": [ 12 | "doctests", 13 | "test" 14 | ], 15 | "type": "module", 16 | "main": "./lib/doctest.js", 17 | "bin": "./bin/doctest", 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/davidchambers/doctest.git" 21 | }, 22 | "engines": { 23 | "node": ">=16.2.0" 24 | }, 25 | "dependencies": { 26 | "acorn": "8.11.x", 27 | "coffeescript": "2.7.x", 28 | "commander": "2.20.x", 29 | "sanctuary-show": "3.0.x", 30 | "sanctuary-type-classes": "13.0.x" 31 | }, 32 | "devDependencies": { 33 | "sanctuary-scripts": "7.0.x" 34 | }, 35 | "scripts": { 36 | "doctest": "sanctuary-doctest", 37 | "lint": "sanctuary-lint", 38 | "release": "sanctuary-release", 39 | "test": "npm run lint && sanctuary-test && npm run doctest" 40 | }, 41 | "files": [ 42 | "/bin/", 43 | "/lib/", 44 | "/LICENSE", 45 | "/README.md", 46 | "/package.json" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /scripts/doctest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchambers/doctest/b6d1bcb29623d7bceb400a8292f1831c45b71cd8/scripts/doctest -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euf -o pipefail 3 | 4 | source "$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}")")")/node_modules/sanctuary-scripts/functions" 5 | 6 | branches="$(get min-branch-coverage)" 7 | 8 | set +f ; shopt -s globstar nullglob 9 | # shellcheck disable=SC2207 10 | source_files=($(get source-files)) 11 | # shellcheck disable=SC2207 12 | test_files=($(get test-files)) 13 | set -f ; shopt -u globstar nullglob 14 | 15 | args=( 16 | --check-coverage 17 | --branches "$branches" 18 | --functions 0 19 | --lines 0 20 | --statements 0 21 | ) 22 | for name in "${source_files[@]}" ; do 23 | args+=(--include "$name") 24 | done 25 | node_modules/.bin/c8 "${args[@]}" -- node --experimental-vm-modules -- node_modules/.bin/oletus -- "${test_files[@]}" 26 | -------------------------------------------------------------------------------- /test/bin/executable: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // > identity(42) 4 | // 42 5 | var identity = function(x) { 6 | return x; 7 | }; 8 | -------------------------------------------------------------------------------- /test/bin/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('executable without file extension') 4 | ([Line (3) ('> identity(42)')]) 5 | ([Line (4) ('42')]) 6 | (Correct (Success (42))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/commonjs/__dirname/index.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | # > typeof __dirname 4 | # 'string' 5 | 6 | # > @path = require 'path' 7 | 8 | # > path.isAbsolute __dirname 9 | # true 10 | 11 | # > path.relative process.cwd(), __dirname 12 | # 'test/commonjs/__dirname' 13 | -------------------------------------------------------------------------------- /test/commonjs/__dirname/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // > typeof __dirname 4 | // 'string' 5 | // 6 | // > const path = require ('path') 7 | // 8 | // > path.isAbsolute (__dirname) 9 | // true 10 | // 11 | // > path.relative (process.cwd (), __dirname) 12 | // 'test/commonjs/__dirname' 13 | -------------------------------------------------------------------------------- /test/commonjs/__dirname/results.coffee.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('__dirname is defined') 4 | ([Line (3) ('> typeof __dirname')]) 5 | ([Line (4) ("'string'")]) 6 | (Correct (Success ('string'))), 7 | 8 | Test ('__dirname is absolute') 9 | ([Line (8) ('> path.isAbsolute __dirname')]) 10 | ([Line (9) ('true')]) 11 | (Correct (Success (true))), 12 | 13 | Test ('__dirname is correct') 14 | ([Line (11) ('> path.relative process.cwd(), __dirname')]) 15 | ([Line (12) ("'test/commonjs/__dirname'")]) 16 | (Correct (Success ('test/commonjs/__dirname'))), 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /test/commonjs/__dirname/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('__dirname is defined') 4 | ([Line (3) ('> typeof __dirname')]) 5 | ([Line (4) ("'string'")]) 6 | (Correct (Success ('string'))), 7 | 8 | Test ('__dirname is absolute') 9 | ([Line (8) ('> path.isAbsolute (__dirname)')]) 10 | ([Line (9) ('true')]) 11 | (Correct (Success (true))), 12 | 13 | Test ('__dirname is correct') 14 | ([Line (11) ('> path.relative (process.cwd (), __dirname)')]) 15 | ([Line (12) ("'test/commonjs/__dirname'")]) 16 | (Correct (Success ('test/commonjs/__dirname'))), 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /test/commonjs/__doctest.require/index.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | url = if typeof __doctest is 'undefined' then {} else __doctest.require 'url' 4 | 5 | # > (new url.URL ('https://sanctuary.js.org/')).hostname 6 | # 'sanctuary.js.org' 7 | -------------------------------------------------------------------------------- /test/commonjs/__doctest.require/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const url = typeof __doctest === 'undefined' ? {} : __doctest.require ('url'); 4 | 5 | // > (new url.URL ('https://sanctuary.js.org/')).hostname 6 | // 'sanctuary.js.org' 7 | 8 | url; 9 | -------------------------------------------------------------------------------- /test/commonjs/__doctest.require/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('__doctest.require') 4 | ([Line (5) ("> (new url.URL ('https://sanctuary.js.org/')).hostname")]) 5 | ([Line (6) ("'sanctuary.js.org'")]) 6 | (Correct (Success ('sanctuary.js.org'))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/commonjs/__filename/index.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | # > typeof __filename 4 | # 'string' 5 | 6 | # > @path = require 'path' 7 | 8 | # > path.isAbsolute __filename 9 | # true 10 | 11 | # > path.relative process.cwd(), __filename 12 | # 'test/commonjs/__filename/index.coffee' 13 | -------------------------------------------------------------------------------- /test/commonjs/__filename/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // > typeof __filename 4 | // 'string' 5 | // 6 | // > const path = require ('path') 7 | // 8 | // > path.isAbsolute (__filename) 9 | // true 10 | // 11 | // > path.relative (process.cwd (), __filename) 12 | // 'test/commonjs/__filename/index.js' 13 | -------------------------------------------------------------------------------- /test/commonjs/__filename/results.coffee.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('__filename is defined') 4 | ([Line (3) ('> typeof __filename')]) 5 | ([Line (4) ("'string'")]) 6 | (Correct (Success ('string'))), 7 | 8 | Test ('__filename is absolute') 9 | ([Line (8) ('> path.isAbsolute __filename')]) 10 | ([Line (9) ('true')]) 11 | (Correct (Success (true))), 12 | 13 | Test ('__filename is correct') 14 | ([Line (11) ('> path.relative process.cwd(), __filename')]) 15 | ([Line (12) ("'test/commonjs/__filename/index.coffee'")]) 16 | (Correct (Success ('test/commonjs/__filename/index.coffee'))), 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /test/commonjs/__filename/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('__filename is defined') 4 | ([Line (3) ('> typeof __filename')]) 5 | ([Line (4) ("'string'")]) 6 | (Correct (Success ('string'))), 7 | 8 | Test ('__filename is absolute') 9 | ([Line (8) ('> path.isAbsolute (__filename)')]) 10 | ([Line (9) ('true')]) 11 | (Correct (Success (true))), 12 | 13 | Test ('__filename is correct') 14 | ([Line (11) ('> path.relative (process.cwd (), __filename)')]) 15 | ([Line (12) ("'test/commonjs/__filename/index.js'")]) 16 | (Correct (Success ('test/commonjs/__filename/index.js'))), 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /test/commonjs/exports/index.js: -------------------------------------------------------------------------------- 1 | // > exports.identity(42) 2 | // 42 3 | exports.identity = function(x) { 4 | return x; 5 | }; 6 | -------------------------------------------------------------------------------- /test/commonjs/exports/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('exports') 4 | ([Line (1) ('> exports.identity(42)')]) 5 | ([Line (2) ('42')]) 6 | (Correct (Success (42))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/commonjs/module.exports/index.js: -------------------------------------------------------------------------------- 1 | // > module.exports(42) 2 | // 42 3 | module.exports = function(x) { 4 | return x; 5 | }; 6 | -------------------------------------------------------------------------------- /test/commonjs/module.exports/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('module.exports') 4 | ([Line (1) ('> module.exports(42)')]) 5 | ([Line (2) ('42')]) 6 | (Correct (Success (42))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/commonjs/require/index.js: -------------------------------------------------------------------------------- 1 | // > typeof $require("assert") 2 | // "function" 3 | var $require = function(name) { 4 | return require(name); 5 | }; 6 | -------------------------------------------------------------------------------- /test/commonjs/require/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('require another CommonJS module') 4 | ([Line (1) ('> typeof $require("assert")')]) 5 | ([Line (2) ('"function"')]) 6 | (Correct (Success ('function'))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/commonjs/strict/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // > (function() { return this; }()) 4 | // undefined 5 | -------------------------------------------------------------------------------- /test/commonjs/strict/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ("preserves 'use strict' directive") 4 | ([Line (3) ('> (function() { return this; }())')]) 5 | ([Line (4) ('undefined')]) 6 | (Correct (Success (undefined))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/contiguity/index.coffee: -------------------------------------------------------------------------------- 1 | # inc :: Number -> Number 2 | # 3 | # > inc (0) 4 | inc = (x) -> x + 1 5 | 6 | # dec :: Number -> Number 7 | # 8 | # > dec (0) 9 | # 10 | # This is not an output line as it does not immediately follow an input line. 11 | dec = (x) -> x - 1 12 | 13 | # zero :: Integer -> Integer 14 | # 15 | # > zero (42) 16 | # 0 17 | zero = (x) -> 18 | switch true 19 | when x < 0 20 | zero inc x 21 | when x > 0 22 | zero dec x 23 | else 24 | 0 25 | -------------------------------------------------------------------------------- /test/contiguity/index.js: -------------------------------------------------------------------------------- 1 | // inc :: Number -> Number 2 | // 3 | // > inc (0) 4 | const inc = x => x + 1; 5 | 6 | // dec :: Number -> Number 7 | // 8 | // > dec (0) 9 | // 10 | // This is not an output line as it does not immediately follow an input line. 11 | const dec = x => x - 1; 12 | 13 | // zero :: Integer -> Integer 14 | // 15 | // > zero (42) 16 | // 0 17 | function zero(x) { 18 | return x < 0 ? zero (inc (x)) : 19 | x > 0 ? zero (dec (x)) : 20 | 0; 21 | } 22 | 23 | zero (0); 24 | -------------------------------------------------------------------------------- /test/contiguity/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('output line immediately following input line') 4 | ([Line (15) ('> zero (42)')]) 5 | ([Line (16) ('0')]) 6 | (Correct (Success (0))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/es2015/index.js: -------------------------------------------------------------------------------- 1 | function* fibonacci() { 2 | var prev = 0; 3 | var curr = 1; 4 | while (true) { 5 | yield curr; 6 | var next = prev + curr; 7 | prev = curr; 8 | curr = next; 9 | } 10 | } 11 | 12 | // > seq.next().value 13 | // 1 14 | // > seq.next().value 15 | // 1 16 | // > seq.next().value 17 | // 2 18 | // > seq.next().value 19 | // 3 20 | // > seq.next().value 21 | // 5 22 | var seq = fibonacci(); 23 | -------------------------------------------------------------------------------- /test/es2015/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('seq.next().value') 4 | ([Line (12) ('> seq.next().value')]) 5 | ([Line (13) ('1')]) 6 | (Correct (Success (1))), 7 | 8 | Test ('seq.next().value') 9 | ([Line (14) ('> seq.next().value')]) 10 | ([Line (15) ('1')]) 11 | (Correct (Success (1))), 12 | 13 | Test ('seq.next().value') 14 | ([Line (16) ('> seq.next().value')]) 15 | ([Line (17) ('2')]) 16 | (Correct (Success (2))), 17 | 18 | Test ('seq.next().value') 19 | ([Line (18) ('> seq.next().value')]) 20 | ([Line (19) ('3')]) 21 | (Correct (Success (3))), 22 | 23 | Test ('seq.next().value') 24 | ([Line (20) ('> seq.next().value')]) 25 | ([Line (21) ('5')]) 26 | (Correct (Success (5))), 27 | 28 | ]; 29 | -------------------------------------------------------------------------------- /test/es2018/index.js: -------------------------------------------------------------------------------- 1 | // > {x: 0, ...{x: 1, y: 2, z: 3}, z: 4} 2 | // {x: 1, y: 2, z: 4} 3 | -------------------------------------------------------------------------------- /test/es2018/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('object spread syntax') 4 | ([Line (1) ('> {x: 0, ...{x: 1, y: 2, z: 3}, z: 4}')]) 5 | ([Line (2) ('{x: 1, y: 2, z: 4}')]) 6 | (Correct (Success ({x: 1, y: 2, z: 4}))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/es2020/index.js: -------------------------------------------------------------------------------- 1 | // > null ?? 'default' 2 | // 'default' 3 | -------------------------------------------------------------------------------- /test/es2020/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('nullish coalescing operator') 4 | ([Line (1) ("> null ?? 'default'")]) 5 | ([Line (2) ("'default'")]) 6 | (Correct (Success ('default'))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/esm/dependencies.js: -------------------------------------------------------------------------------- 1 | // Convert degrees Celsius to degrees Fahrenheit. 2 | // 3 | // > import util from 'node:util' 4 | // > util.inspect (toFahrenheit (0)) 5 | // '32' 6 | export function toFahrenheit(degreesCelsius) { 7 | return degreesCelsius * 9 / 5 + 32; 8 | } 9 | -------------------------------------------------------------------------------- /test/esm/globals/index.js: -------------------------------------------------------------------------------- 1 | // > typeof setTimeout 2 | // 'function' 3 | -------------------------------------------------------------------------------- /test/esm/globals/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('setTimeout is defined') 4 | ([Line (1) ('> typeof setTimeout')]) 5 | ([Line (2) ("'function'")]) 6 | (Correct (Success ('function'))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/esm/incorrect.js: -------------------------------------------------------------------------------- 1 | // Convert degrees Celsius to degrees Fahrenheit. 2 | // 3 | // > toFahrenheit (0) 4 | // 32 5 | export function toFahrenheit(degreesCelsius) { 6 | return `${degreesCelsius}°F`; 7 | } 8 | -------------------------------------------------------------------------------- /test/esm/index.js: -------------------------------------------------------------------------------- 1 | // Convert degrees Celsius to degrees Fahrenheit. 2 | // 3 | // > toFahrenheit (0) 4 | // 32 5 | export function toFahrenheit(degreesCelsius) { 6 | return degreesCelsius * 9 / 5 + 32; 7 | } 8 | -------------------------------------------------------------------------------- /test/exceptions/index.js: -------------------------------------------------------------------------------- 1 | 2 | // > new Error('Invalid value') 3 | // new Error('Invalid value') 4 | 5 | // > new Error() 6 | // new Error('Invalid value') 7 | 8 | // > new Error('Invalid value') 9 | // new Error() 10 | 11 | // > new Error('Invalid value') 12 | // new Error('XXX') 13 | 14 | // > new Error('Invalid value') 15 | // ! Error: Invalid value 16 | 17 | // > sqrt(-1) 18 | // new Error('Invalid value') 19 | 20 | // > 0..toString(1) 21 | // ! RangeError 22 | 23 | // > 0..toString(1) 24 | // ! Error 25 | 26 | // > sqrt(-1) 27 | // ! Error: Invalid value 28 | 29 | // > sqrt(-1) 30 | // ! Error: XXX 31 | 32 | // > 'foo' + 'bar' 33 | // foobar 34 | 35 | var sqrt = function(n) { 36 | if (n >= 0) { 37 | return Math.sqrt(n); 38 | } else { 39 | throw new Error('Invalid value'); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /test/exceptions/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Incorrect, Correct, Failure, Success}) => [ 2 | 3 | Test ('input evaluates to Error with expected message') 4 | ([Line (2) ("> new Error('Invalid value')")]) 5 | ([Line (3) ("new Error('Invalid value')")]) 6 | (Correct (Success (new Error ('Invalid value')))), 7 | 8 | Test ('input evaluates to Error without expected message') 9 | ([Line (5) ('> new Error()')]) 10 | ([Line (6) ("new Error('Invalid value')")]) 11 | (Incorrect (Success (new Error ())) 12 | (Success (new Error ('Invalid value')))), 13 | 14 | Test ('input evaluates to Error with unexpected message') 15 | ([Line (8) ("> new Error('Invalid value')")]) 16 | ([Line (9) ('new Error()')]) 17 | (Incorrect (Success (new Error ('Invalid value'))) 18 | (Success (new Error ()))), 19 | 20 | Test ('input evaluates to Error with unexpected message') 21 | ([Line (11) ("> new Error('Invalid value')")]) 22 | ([Line (12) ("new Error('XXX')")]) 23 | (Incorrect (Success (new Error ('Invalid value'))) 24 | (Success (new Error ('XXX')))), 25 | 26 | Test ('evaluating input does not throw expected exception') 27 | ([Line (14) ("> new Error('Invalid value')")]) 28 | ([Line (15) ('! Error: Invalid value')]) 29 | (Incorrect (Success (new Error ('Invalid value'))) 30 | (Failure (new Error ('Invalid value')))), 31 | 32 | Test ('evaluating input throws unexpected exception') 33 | ([Line (17) ('> sqrt(-1)')]) 34 | ([Line (18) ("new Error('Invalid value')")]) 35 | (Incorrect (Failure (new Error ('Invalid value'))) 36 | (Success (new Error ('Invalid value')))), 37 | 38 | Test ('evaluating input throws exception as expected, of expected type') 39 | ([Line (20) ('> 0..toString(1)')]) 40 | ([Line (21) ('! RangeError')]) 41 | (Correct (Failure (new RangeError ('toString() radix argument must be between 2 and 36')))), 42 | 43 | Test ('evaluating input throws exception as expected, of unexpected type') 44 | ([Line (23) ('> 0..toString(1)')]) 45 | ([Line (24) ('! Error')]) 46 | (Incorrect (Failure (new RangeError ('toString() radix argument must be between 2 and 36'))) 47 | (Failure (new Error ()))), 48 | 49 | Test ('evaluating input throws exception as expected, with expected message') 50 | ([Line (26) ('> sqrt(-1)')]) 51 | ([Line (27) ('! Error: Invalid value')]) 52 | (Correct (Failure (new Error ('Invalid value')))), 53 | 54 | Test ('evaluating input throws exception as expected, with unexpected message') 55 | ([Line (29) ('> sqrt(-1)')]) 56 | ([Line (30) ('! Error: XXX')]) 57 | (Incorrect (Failure (new Error ('Invalid value'))) 58 | (Failure (new Error ('XXX')))), 59 | 60 | Test ('evaluating output throws unexpected exception') 61 | ([Line (32) ("> 'foo' + 'bar'")]) 62 | ([Line (33) ('foobar')]) 63 | (Incorrect (Success ('foobar')) 64 | (Failure (new ReferenceError ('foobar is not defined')))), 65 | 66 | ]; 67 | -------------------------------------------------------------------------------- /test/fantasy-land/index.js: -------------------------------------------------------------------------------- 1 | // > Absolute(-1) 2 | // Absolute(1) 3 | function Absolute(n) { 4 | if (!(this instanceof Absolute)) return new Absolute(n); 5 | this.value = n; 6 | } 7 | 8 | Absolute['@@type'] = 'doctest/Absolute'; 9 | 10 | Absolute.prototype['@@show'] = function() { 11 | return 'Absolute (' + this.value + ')'; 12 | }; 13 | 14 | Absolute.prototype['fantasy-land/equals'] = function(other) { 15 | return Math.abs(this.value) === Math.abs(other.value); 16 | }; 17 | 18 | export default Absolute; 19 | -------------------------------------------------------------------------------- /test/fantasy-land/results.js: -------------------------------------------------------------------------------- 1 | import Absolute from './index.js'; 2 | 3 | 4 | export default ({Test, Line, Correct, Success}) => [ 5 | 6 | Test ('uses Z.equals for equality checks') 7 | ([Line (1) ('> Absolute(-1)')]) 8 | ([Line (2) ('Absolute(1)')]) 9 | (Correct (Success (Absolute (-1)))), 10 | 11 | ]; 12 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import {strictEqual} from 'node:assert'; 2 | import {exec} from 'node:child_process'; 3 | import {promisify} from 'node:util'; 4 | 5 | import test from 'oletus'; 6 | import show from 'sanctuary-show'; 7 | import Z from 'sanctuary-type-classes'; 8 | 9 | import {Incorrect, Correct} from '../lib/Comparison.js'; 10 | import {Failure, Success} from '../lib/Effect.js'; 11 | import {Line} from '../lib/Line.js'; 12 | import doctest from '../lib/doctest.js'; 13 | 14 | import resultsBin from './bin/results.js'; 15 | import resultsCommonJsDirnameCoffee from './commonjs/__dirname/results.coffee.js'; 16 | import resultsCommonJsDirnameJs from './commonjs/__dirname/results.js'; 17 | import resultsCommonJsDoctestRequire from './commonjs/__doctest.require/results.js'; 18 | import resultsCommonJsFilenameCoffee from './commonjs/__filename/results.coffee.js'; 19 | import resultsCommonJsFilenameJs from './commonjs/__filename/results.js'; 20 | import resultsCommonJsExports from './commonjs/exports/results.js'; 21 | import resultsCommonJsModuleExports from './commonjs/module.exports/results.js'; 22 | import resultsCommonJsRequire from './commonjs/require/results.js'; 23 | import resultsCommonJsStrict from './commonjs/strict/results.js'; 24 | import resultsContiguity from './contiguity/results.js'; 25 | import resultsEs2015 from './es2015/results.js'; 26 | import resultsEs2018 from './es2018/results.js'; 27 | import resultsEs2020 from './es2020/results.js'; 28 | import resultsEsmGlobals from './esm/globals/results.js'; 29 | import resultsExceptions from './exceptions/results.js'; 30 | import resultsFantasyLand from './fantasy-land/results.js'; 31 | import resultsLineEndings from './line-endings/results.js'; 32 | import resultsSharedCoffee from './shared/results.coffee.js'; 33 | import resultsSharedJs from './shared/results.js'; 34 | import resultsStatements from './statements/results.js'; 35 | import resultsTranscribe from './transcribe/results.js'; 36 | 37 | 38 | const eq = actual => expected => { 39 | strictEqual (show (actual), show (expected)); 40 | strictEqual (Z.equals (actual, expected), true); 41 | }; 42 | 43 | const Test = description => input => output => comparison => ({ 44 | description, 45 | expected: { 46 | lines: { 47 | input, 48 | output, 49 | }, 50 | comparison, 51 | }, 52 | }); 53 | 54 | const dependencies = { 55 | Test, 56 | Line, 57 | Incorrect, 58 | Correct, 59 | Failure, 60 | Success, 61 | }; 62 | 63 | const testModule = (module, path, options) => { 64 | const expecteds = module (dependencies); 65 | 66 | let promise = null; 67 | const run = () => (promise ?? (promise = doctest (options) (path))); 68 | 69 | const b = '\u001B[1m'; 70 | const x = '\u001B[22m'; 71 | const prefix = ( 72 | x + 'doctest (' + show (options) + ') (' + b + show (path) + x + ') › ' + b 73 | ); 74 | 75 | test (prefix + '.length', async () => { 76 | const actuals = await run (); 77 | eq (actuals.length) (expecteds.length); 78 | }); 79 | 80 | expecteds.forEach (({description, expected}, idx) => { 81 | test (prefix + description, async () => { 82 | const actuals = await run (); 83 | eq (actuals[idx]) (expected); 84 | }); 85 | }); 86 | }; 87 | 88 | const testCommand = (command, expected) => { 89 | test (command, () => 90 | promisify (exec) (command) 91 | .then ( 92 | actual => { 93 | eq (0) (expected.status); 94 | eq (actual.stdout) (expected.stdout); 95 | eq (actual.stderr) (expected.stderr); 96 | }, 97 | actual => { 98 | eq (actual.code) (expected.status); 99 | eq (actual.stdout) (expected.stdout); 100 | eq (actual.stderr) (expected.stderr); 101 | } 102 | ) 103 | ); 104 | }; 105 | 106 | testModule (resultsSharedJs, 'test/shared/index.js', { 107 | silent: true, 108 | }); 109 | 110 | testModule (resultsSharedCoffee, 'test/shared/index.coffee', { 111 | coffee: true, 112 | silent: true, 113 | }); 114 | 115 | testModule (resultsLineEndings, 'test/line-endings/CR.js', { 116 | silent: true, 117 | }); 118 | 119 | testModule (resultsLineEndings, 'test/line-endings/CR.coffee', { 120 | coffee: true, 121 | silent: true, 122 | }); 123 | 124 | testModule (resultsLineEndings, 'test/line-endings/CR+LF.js', { 125 | silent: true, 126 | }); 127 | 128 | testModule (resultsLineEndings, 'test/line-endings/CR+LF.coffee', { 129 | coffee: true, 130 | silent: true, 131 | }); 132 | 133 | testModule (resultsLineEndings, 'test/line-endings/LF.js', { 134 | silent: true, 135 | }); 136 | 137 | testModule (resultsLineEndings, 'test/line-endings/LF.coffee', { 138 | coffee: true, 139 | silent: true, 140 | }); 141 | 142 | testModule (resultsExceptions, 'test/exceptions/index.js', { 143 | silent: true, 144 | }); 145 | 146 | testModule (resultsStatements, 'test/statements/index.js', { 147 | silent: true, 148 | }); 149 | 150 | testModule (resultsFantasyLand, 'test/fantasy-land/index.js', { 151 | module: 'esm', 152 | silent: true, 153 | }); 154 | 155 | testModule (resultsTranscribe, 'test/transcribe/index.js', { 156 | prefix: '.', 157 | openingDelimiter: '```javascript', 158 | closingDelimiter: '```', 159 | silent: true, 160 | }); 161 | 162 | testModule (resultsTranscribe, 'test/transcribe/index.coffee', { 163 | coffee: true, 164 | prefix: '.', 165 | openingDelimiter: '```coffee', 166 | closingDelimiter: '```', 167 | silent: true, 168 | }); 169 | 170 | testModule (resultsCommonJsRequire, 'test/commonjs/require/index.js', { 171 | module: 'commonjs', 172 | silent: true, 173 | }); 174 | 175 | testModule (resultsCommonJsExports, 'test/commonjs/exports/index.js', { 176 | module: 'commonjs', 177 | silent: true, 178 | }); 179 | 180 | testModule (resultsCommonJsModuleExports, 'test/commonjs/module.exports/index.js', { 181 | module: 'commonjs', 182 | silent: true, 183 | }); 184 | 185 | testModule (resultsCommonJsStrict, 'test/commonjs/strict/index.js', { 186 | module: 'commonjs', 187 | silent: true, 188 | }); 189 | 190 | testModule (resultsCommonJsDirnameJs, 'test/commonjs/__dirname/index.js', { 191 | module: 'commonjs', 192 | silent: true, 193 | }); 194 | 195 | testModule (resultsCommonJsDirnameCoffee, 'test/commonjs/__dirname/index.coffee', { 196 | module: 'commonjs', 197 | coffee: true, 198 | silent: true, 199 | }); 200 | 201 | testModule (resultsCommonJsFilenameJs, 'test/commonjs/__filename/index.js', { 202 | module: 'commonjs', 203 | silent: true, 204 | }); 205 | 206 | testModule (resultsCommonJsFilenameCoffee, 'test/commonjs/__filename/index.coffee', { 207 | module: 'commonjs', 208 | coffee: true, 209 | silent: true, 210 | }); 211 | 212 | testModule (resultsCommonJsDoctestRequire, 'test/commonjs/__doctest.require/index.js', { 213 | module: 'commonjs', 214 | silent: true, 215 | }); 216 | 217 | testModule (resultsBin, 'test/bin/executable', { 218 | silent: true, 219 | }); 220 | 221 | testModule (resultsEs2015, 'test/es2015/index.js', { 222 | silent: true, 223 | }); 224 | 225 | testModule (resultsEs2018, 'test/es2018/index.js', { 226 | silent: true, 227 | }); 228 | 229 | testModule (resultsEs2020, 'test/es2020/index.js', { 230 | silent: true, 231 | }); 232 | 233 | testModule (resultsEsmGlobals, 'test/esm/globals/index.js', { 234 | module: 'esm', 235 | silent: true, 236 | }); 237 | 238 | testModule (resultsContiguity, 'test/contiguity/index.js', { 239 | silent: true, 240 | }); 241 | 242 | testModule (resultsContiguity, 'test/contiguity/index.coffee', { 243 | coffee: true, 244 | silent: true, 245 | }); 246 | 247 | testCommand ('bin/doctest', { 248 | status: 0, 249 | stdout: '', 250 | stderr: '', 251 | }); 252 | 253 | testCommand ('bin/doctest --xxx', { 254 | status: 1, 255 | stdout: '', 256 | stderr: `error: unknown option \`--xxx' 257 | `, 258 | }); 259 | 260 | testCommand ('bin/doctest test/shared/index.js', { 261 | status: 1, 262 | stdout: `running doctests in test/shared/index.js... 263 | ......x.x...........x........x 264 | FAIL: expected 5 on line 31 (got 4) 265 | FAIL: expected ! TypeError on line 38 (got 0) 266 | FAIL: expected 9.5 on line 97 (got 5) 267 | FAIL: expected "on automatic semicolon insertion" on line 155 (got "the rewriter should not rely") 268 | `, 269 | stderr: '', 270 | }); 271 | 272 | testCommand ('bin/doctest --coffee test/shared/index.coffee', { 273 | status: 1, 274 | stdout: `running doctests in test/shared/index.coffee... 275 | ......x.x...........x..... 276 | FAIL: expected 5 on line 31 (got 4) 277 | FAIL: expected ! TypeError on line 38 (got 0) 278 | FAIL: expected 9.5 on line 97 (got 5) 279 | `, 280 | stderr: '', 281 | }); 282 | 283 | testCommand ('bin/doctest --silent test/shared/index.js', { 284 | status: 1, 285 | stdout: '', 286 | stderr: '', 287 | }); 288 | 289 | testCommand ('bin/doctest test/bin/executable', { 290 | status: 0, 291 | stdout: `running doctests in test/bin/executable... 292 | . 293 | `, 294 | stderr: '', 295 | }); 296 | 297 | testCommand ('bin/doctest --module xxx file.js', { 298 | status: 1, 299 | stdout: '', 300 | stderr: `Error: Invalid module "xxx" 301 | `, 302 | }); 303 | 304 | testCommand ('bin/doctest --module esm lib/doctest.js', { 305 | status: 0, 306 | stdout: `running doctests in lib/doctest.js... 307 | 308 | `, 309 | stderr: '', 310 | }); 311 | 312 | testCommand ('bin/doctest --module esm test/esm/index.js', { 313 | status: 0, 314 | stdout: `running doctests in test/esm/index.js... 315 | . 316 | `, 317 | stderr: '', 318 | }); 319 | 320 | testCommand ('bin/doctest --module esm test/esm/dependencies.js', { 321 | status: 0, 322 | stdout: `running doctests in test/esm/dependencies.js... 323 | . 324 | `, 325 | stderr: '', 326 | }); 327 | 328 | testCommand ('bin/doctest --module esm test/esm/incorrect.js', { 329 | status: 1, 330 | stdout: `running doctests in test/esm/incorrect.js... 331 | x 332 | FAIL: expected 32 on line 4 (got "0°F") 333 | `, 334 | stderr: '', 335 | }); 336 | 337 | testCommand ('bin/doctest --print test/commonjs/exports/index.js', { 338 | status: 0, 339 | stdout: ` 340 | __doctest.enqueue({ 341 | input: { 342 | lines: [ 343 | {number: 1, text: "> exports.identity(42)"}, 344 | ], 345 | thunk: () => { 346 | return ( 347 | exports.identity(42) 348 | ); 349 | }, 350 | }, 351 | output: { 352 | lines: [ 353 | {number: 2, text: "42"}, 354 | ], 355 | thunk: () => { 356 | return ( 357 | 42 358 | ); 359 | }, 360 | }, 361 | }); 362 | 363 | exports.identity = function(x) { 364 | return x; 365 | }; 366 | `, 367 | stderr: '', 368 | }); 369 | 370 | testCommand ('bin/doctest --print --module commonjs test/commonjs/exports/index.js', { 371 | status: 0, 372 | stdout: ` 373 | __doctest.enqueue({ 374 | input: { 375 | lines: [ 376 | {number: 1, text: "> exports.identity(42)"}, 377 | ], 378 | thunk: () => { 379 | return ( 380 | exports.identity(42) 381 | ); 382 | }, 383 | }, 384 | output: { 385 | lines: [ 386 | {number: 2, text: "42"}, 387 | ], 388 | thunk: () => { 389 | return ( 390 | 42 391 | ); 392 | }, 393 | }, 394 | }); 395 | 396 | exports.identity = function(x) { 397 | return x; 398 | }; 399 | `, 400 | stderr: '', 401 | }); 402 | -------------------------------------------------------------------------------- /test/line-endings/CR+LF.coffee: -------------------------------------------------------------------------------- 1 | # > 2 * 3 * 7 2 | # 42 3 | -------------------------------------------------------------------------------- /test/line-endings/CR+LF.js: -------------------------------------------------------------------------------- 1 | // > 2 * 3 * 7 2 | // 42 3 | -------------------------------------------------------------------------------- /test/line-endings/CR.coffee: -------------------------------------------------------------------------------- 1 | # > 2 * 3 * 7 # 42 -------------------------------------------------------------------------------- /test/line-endings/CR.js: -------------------------------------------------------------------------------- 1 | // > 2 * 3 * 7 // 42 -------------------------------------------------------------------------------- /test/line-endings/LF.coffee: -------------------------------------------------------------------------------- 1 | # > 2 * 3 * 7 2 | # 42 3 | -------------------------------------------------------------------------------- /test/line-endings/LF.js: -------------------------------------------------------------------------------- 1 | // > 2 * 3 * 7 2 | // 42 3 | -------------------------------------------------------------------------------- /test/line-endings/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('correct line number reported irrespective of line endings') 4 | ([Line (1) ('> 2 * 3 * 7')]) 5 | ([Line (2) ('42')]) 6 | (Correct (Success (42))), 7 | 8 | ]; 9 | -------------------------------------------------------------------------------- /test/shared/index.coffee: -------------------------------------------------------------------------------- 1 | 1: 'global variable accessible in outer scope' 2 | # > global 3 | # "global" 4 | global = 'global' 5 | 6 | do -> 7 | 8 | 2: 'global variable accessible in inner scope' 9 | # > global 10 | # "global" 11 | do (global = 'shadowed') -> 12 | 3: 'local variable referenced, not shadowed global' 13 | # > global 14 | # "shadowed" 15 | 16 | 17 | 18 | 4: 'local variable accessible before declaration' 19 | # > one * two 20 | # 2 21 | one = 1 22 | two = 2 23 | 5: 'assignment is an expression' 24 | # > @three = one + two 25 | # 3 26 | 6: 'variable declared in doctest remains accessible' 27 | # > [one, two, three] 28 | # [1, 2, 3] 29 | 7: 'arithmetic error reported' 30 | # > two + two 31 | # 5 32 | 33 | 8: 'RangeError captured and reported' 34 | # > 0.toString 1 35 | # ! RangeError 36 | 9: 'TypeError expected but not reported' 37 | # > [].length 38 | # ! TypeError 39 | 40 | 10: 'function accessible before declaration' 41 | # > double(6) 42 | # 12 43 | 11: 'NaN can be used as expected result' 44 | # > double() 45 | # NaN 46 | double = (n) -> 47 | # doctests should only be included in contexts where they'll be 48 | # invoked immediately (i.e. at the top level or within an IIFE) 49 | 2 * n 50 | 51 | 12: 'function accessible after declaration' 52 | # > double.call(null, 2) 53 | # 4 54 | 55 | triple = (n) -> 56 | # > this.doctest.should.never.be.executed 57 | # ( blow.up.if.for.some.reason.it.is ) 58 | 3 * n 59 | 60 | 61 | 13: 'multiline input' 62 | # > [1,2,3, 63 | # . 4,5,6, 64 | # . 7,8,9] 65 | # [1,2,3,4,5,6,7,8,9] 66 | 14: 'multiline assignment' 67 | # > @string = "input " + 68 | # . "may span many " + 69 | # . "lines" 70 | # > string 71 | # "input may span many lines" 72 | 73 | 15: 'spaces following "#" and ">" are optional' 74 | #>"no spaces" 75 | #"no spaces" 76 | 16: 'indented doctest' 77 | # > "Docco-compatible whitespace" 78 | # "Docco-compatible whitespace" 79 | 17: '">" in doctest' 80 | # > 2 > 1 81 | # true 82 | 83 | 18: 'comment on input line' 84 | # > "foo" + "bar" # comment 85 | # "foobar" 86 | 19: 'comment on output line' 87 | # > 5 * 5 88 | # 25 # comment 89 | 90 | 20: 'variable in creation context is not accessible' 91 | # > typeof text 92 | # "undefined" 93 | 94 | 21: '"." should not follow leading "." in multiline expressions' 95 | # >10 - 96 | # ..5 97 | # 9.5 98 | 99 | 22: 'wrapped lines may begin with more than one "."' 100 | # > 1000 + 101 | # .. 200 + 102 | # ... 30 + 103 | # .... 4 + 104 | # ..... .5 105 | # 1234.5 106 | 107 | 23: 'multiline comment' 108 | ### 109 | > 3 ** 3 - 2 ** 2 110 | 23 111 | ### 112 | 113 | 24: 'multiline comment with wrapped input' 114 | ### 115 | > (["foo", "bar", "baz"] 116 | . .slice(0, -1) 117 | . .join(" ") 118 | . .toUpperCase()) 119 | "FOO BAR" 120 | ### 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 27: 'multiline output' 139 | # > ["foo", "bar", "baz"] 140 | # [ "foo" 141 | # . "bar" 142 | # . "baz" ] 143 | 144 | 28: 'multiline input with multiline output' 145 | # > ["foo", "bar", "baz"] 146 | # . .join(",") 147 | # . .toUpperCase() 148 | # . .split(",") 149 | # [ "FOO" 150 | # . "BAR" 151 | # . "BAZ" ] 152 | -------------------------------------------------------------------------------- /test/shared/index.js: -------------------------------------------------------------------------------- 1 | 1, 'global variable accessible in outer scope' 2 | // > global 3 | // "global" 4 | global = 'global' 5 | 6 | !function() { 7 | 8 | 2, 'global variable accessible in inner scope' 9 | // > global 10 | // "global" 11 | !function() { 12 | 3, 'local variable referenced, not shadowed global' 13 | // > global 14 | // "shadowed" 15 | var global = 'shadowed' 16 | }() 17 | 18 | 4, 'local variable accessible before declaration' 19 | // > one * two 20 | // 2 21 | var one = 1 22 | var two = 2 23 | 5, 'assignment is an expression' 24 | // > three = one + two 25 | // 3 26 | 6, 'variable declared in doctest remains accessible' 27 | // > [one, two, three] 28 | // [1, 2, 3] 29 | 7, 'arithmetic error reported' 30 | // > two + two 31 | // 5 32 | 33 | 8, 'RangeError captured and reported' 34 | // > 0..toString(1) 35 | // ! RangeError 36 | 9, 'TypeError expected but not reported' 37 | // > [].length 38 | // ! TypeError 39 | 40 | 10, 'function accessible before declaration' 41 | // > double(6) 42 | // 12 43 | 11, 'NaN can be used as expected result' 44 | // > double() 45 | // NaN 46 | var double = function(n) { 47 | // doctests should only be included in contexts where they'll be 48 | // invoked immediately (i.e. at the top level or within an IIFE) 49 | return 2 * n 50 | } 51 | 12, 'function accessible after declaration' 52 | // > double.call(null, 2) 53 | // 4 54 | 55 | var triple = function(n) { 56 | // > this.doctest.should.never.be.executed 57 | // ( blow.up.if.for.some.reason.it.is ) 58 | return 3 * n 59 | } 60 | 61 | 13, 'multiline input' 62 | // > [1,2,3, 63 | // . 4,5,6, 64 | // . 7,8,9] 65 | // [1,2,3,4,5,6,7,8,9] 66 | 14, 'multiline assignment' 67 | // > string = "input " + 68 | // . "may span many " + 69 | // . "lines" 70 | // > string 71 | // "input may span many lines" 72 | 73 | 15, 'spaces following "//" and ">" are optional' 74 | //>"no spaces" 75 | //"no spaces" 76 | 16, 'indented doctest' 77 | // > "Docco-compatible whitespace" 78 | // "Docco-compatible whitespace" 79 | 17, '">" in doctest' 80 | // > 2 > 1 81 | // true 82 | 83 | 18, 'comment on input line' 84 | // > "foo" + "bar" // comment 85 | // "foobar" 86 | 19, 'comment on output line' 87 | // > 5 * 5 88 | // 25 // comment 89 | 90 | 20, 'variable in creation context is not accessible' 91 | // > typeof text 92 | // "undefined" 93 | 94 | 21, '"." should not follow leading "." in multiline expressions' 95 | // >10 - 96 | // ..5 97 | // 9.5 98 | 99 | 22, 'wrapped lines may begin with more than one "."' 100 | // > 1000 + 101 | // .. 200 + 102 | // ... 30 + 103 | // .... 4 + 104 | // ..... .5 105 | // 1234.5 106 | 107 | 23, 'multiline comment' 108 | /* 109 | > Math.pow(3, 3) - Math.pow(2, 2) 110 | 23 111 | */ 112 | 113 | 24, 'multiline comment with wrapped input' 114 | /* 115 | > ["foo", "bar", "baz"] 116 | . .slice(0, -1) 117 | . .join(" ") 118 | . .toUpperCase() 119 | "FOO BAR" 120 | */ 121 | 122 | 25, 'multiline comment with leading asterisks' 123 | /* 124 | * > 1 + 2 * 3 * 4 125 | * 25 126 | * > 1 * 2 + 3 + 4 * 5 127 | * 25 128 | */ 129 | 130 | 26, 'multiline comment with leading asterisks and wrapped input' 131 | /* 132 | * > (function fib(n) { 133 | * . return n == 0 || n == 1 ? n : fib(n - 2) + fib(n - 1); 134 | * . })(10) 135 | * 55 136 | */ 137 | 138 | 27, 'multiline output' 139 | // > ["foo", "bar", "baz"] 140 | // [ "foo", 141 | // . "bar", 142 | // . "baz" ] 143 | 144 | 28, 'multiline input with multiline output' 145 | // > ["foo", "bar", "baz"] 146 | // . .join(",") 147 | // . .toUpperCase() 148 | // . .split(",") 149 | // [ "FOO", 150 | // . "BAR", 151 | // . "BAZ" ] 152 | 153 | 29, 'the rewriter should not rely on automatic semicolon insertion' 154 | // > "the rewriter should not rely" 155 | // "on automatic semicolon insertion" 156 | (4 + 4) 157 | 158 | }() 159 | -------------------------------------------------------------------------------- /test/shared/results.coffee.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Incorrect, Correct, Failure, Success}) => [ 2 | 3 | Test ('global variable accessible in outer scope') 4 | ([Line (2) ('> global')]) 5 | ([Line (3) ('"global"')]) 6 | (Correct (Success ('global'))), 7 | 8 | Test ('global variable accessible in inner scope') 9 | ([Line (9) ('> global')]) 10 | ([Line (10) ('"global"')]) 11 | (Correct (Success ('global'))), 12 | 13 | Test ('local variable referenced, not shadowed global') 14 | ([Line (13) ('> global')]) 15 | ([Line (14) ('"shadowed"')]) 16 | (Correct (Success ('shadowed'))), 17 | 18 | Test ('local variable accessible before declaration') 19 | ([Line (19) ('> one * two')]) 20 | ([Line (20) ('2')]) 21 | (Correct (Success (2))), 22 | 23 | Test ('assignment is an expression') 24 | ([Line (24) ('> @three = one + two')]) 25 | ([Line (25) ('3')]) 26 | (Correct (Success (3))), 27 | 28 | Test ('variable declared in doctest remains accessible') 29 | ([Line (27) ('> [one, two, three]')]) 30 | ([Line (28) ('[1, 2, 3]')]) 31 | (Correct (Success ([1, 2, 3]))), 32 | 33 | Test ('arithmetic error reported') 34 | ([Line (30) ('> two + two')]) 35 | ([Line (31) ('5')]) 36 | (Incorrect (Success (4)) 37 | (Success (5))), 38 | 39 | Test ('RangeError captured and reported') 40 | ([Line (34) ('> 0.toString 1')]) 41 | ([Line (35) ('! RangeError')]) 42 | (Correct (Failure (new RangeError ('toString() radix argument must be between 2 and 36')))), 43 | 44 | Test ('TypeError expected but not reported') 45 | ([Line (37) ('> [].length')]) 46 | ([Line (38) ('! TypeError')]) 47 | (Incorrect (Success (0)) 48 | (Failure (new TypeError ()))), 49 | 50 | Test ('function accessible before declaration') 51 | ([Line (41) ('> double(6)')]) 52 | ([Line (42) ('12')]) 53 | (Correct (Success (12))), 54 | 55 | Test ('NaN can be used as expected result') 56 | ([Line (44) ('> double()')]) 57 | ([Line (45) ('NaN')]) 58 | (Correct (Success (NaN))), 59 | 60 | Test ('function accessible after declaration') 61 | ([Line (52) ('> double.call(null, 2)')]) 62 | ([Line (53) ('4')]) 63 | (Correct (Success (4))), 64 | 65 | Test ('multiline input') 66 | ([Line (62) ('> [1,2,3,'), 67 | Line (63) ('. 4,5,6,'), 68 | Line (64) ('. 7,8,9]')]) 69 | ([Line (65) ('[1,2,3,4,5,6,7,8,9]')]) 70 | (Correct (Success ([1, 2, 3, 4, 5, 6, 7, 8, 9]))), 71 | 72 | Test ('multiline assignment') 73 | ([Line (70) ('> string')]) 74 | ([Line (71) ('"input may span many lines"')]) 75 | (Correct (Success ('input may span many lines'))), 76 | 77 | Test ("spaces following '#' and '>' are optional") 78 | ([Line (74) ('>"no spaces"')]) 79 | ([Line (75) ('"no spaces"')]) 80 | (Correct (Success ('no spaces'))), 81 | 82 | Test ('indented doctest') 83 | ([Line (77) ('> "Docco-compatible whitespace"')]) 84 | ([Line (78) ('"Docco-compatible whitespace"')]) 85 | (Correct (Success ('Docco-compatible whitespace'))), 86 | 87 | Test ("'>' in doctest") 88 | ([Line (80) ('> 2 > 1')]) 89 | ([Line (81) ('true')]) 90 | (Correct (Success (true))), 91 | 92 | Test ('comment on input line') 93 | ([Line (84) ('> "foo" + "bar" # comment')]) 94 | ([Line (85) ('"foobar"')]) 95 | (Correct (Success ('foobar'))), 96 | 97 | Test ('comment on output line') 98 | ([Line (87) ('> 5 * 5')]) 99 | ([Line (88) ('25 # comment')]) 100 | (Correct (Success (25))), 101 | 102 | Test ('variable in creation context is not accessible') 103 | ([Line (91) ('> typeof text')]) 104 | ([Line (92) ('"undefined"')]) 105 | (Correct (Success ('undefined'))), 106 | 107 | Test ("'.' should not follow leading '.' in multiline expressions") 108 | ([Line (95) ('>10 -'), 109 | Line (96) ('..5')]) 110 | ([Line (97) ('9.5')]) 111 | (Incorrect (Success (5)) 112 | (Success (9.5))), 113 | 114 | Test ("wrapped lines may begin with more than one '.'") 115 | ([Line (100) ('> 1000 +'), 116 | Line (101) ('.. 200 +'), 117 | Line (102) ('... 30 +'), 118 | Line (103) ('.... 4 +'), 119 | Line (104) ('..... .5')]) 120 | ([Line (105) ('1234.5')]) 121 | (Correct (Success (1234.5))), 122 | 123 | Test ('multiline comment') 124 | ([Line (109) ('> 3 ** 3 - 2 ** 2')]) 125 | ([Line (110) ('23')]) 126 | (Correct (Success (23))), 127 | 128 | Test ('multiline comment with wrapped input') 129 | ([Line (115) ('> (["foo", "bar", "baz"]'), 130 | Line (116) ('. .slice(0, -1)'), 131 | Line (117) ('. .join(" ")'), 132 | Line (118) ('. .toUpperCase())')]) 133 | ([Line (119) ('"FOO BAR"')]) 134 | (Correct (Success ('FOO BAR'))), 135 | 136 | Test ('multiline output') 137 | ([Line (139) ('> ["foo", "bar", "baz"]')]) 138 | ([Line (140) ('[ "foo"'), 139 | Line (141) ('. "bar"'), 140 | Line (142) ('. "baz" ]')]) 141 | (Correct (Success (['foo', 'bar', 'baz']))), 142 | 143 | Test ('multiline input with multiline output') 144 | ([Line (145) ('> ["foo", "bar", "baz"]'), 145 | Line (146) ('. .join(",")'), 146 | Line (147) ('. .toUpperCase()'), 147 | Line (148) ('. .split(",")')]) 148 | ([Line (149) ('[ "FOO"'), 149 | Line (150) ('. "BAR"'), 150 | Line (151) ('. "BAZ" ]')]) 151 | (Correct (Success (['FOO', 'BAR', 'BAZ']))), 152 | 153 | ]; 154 | -------------------------------------------------------------------------------- /test/shared/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Incorrect, Correct, Failure, Success}) => [ 2 | 3 | Test ('global variable accessible in outer scope') 4 | ([Line (2) ('> global')]) 5 | ([Line (3) ('"global"')]) 6 | (Correct (Success ('global'))), 7 | 8 | Test ('global variable accessible in inner scope') 9 | ([Line (9) ('> global')]) 10 | ([Line (10) ('"global"')]) 11 | (Correct (Success ('global'))), 12 | 13 | Test ('local variable referenced, not shadowed global') 14 | ([Line (13) ('> global')]) 15 | ([Line (14) ('"shadowed"')]) 16 | (Correct (Success ('shadowed'))), 17 | 18 | Test ('local variable accessible before declaration') 19 | ([Line (19) ('> one * two')]) 20 | ([Line (20) ('2')]) 21 | (Correct (Success (2))), 22 | 23 | Test ('assignment is an expression') 24 | ([Line (24) ('> three = one + two')]) 25 | ([Line (25) ('3')]) 26 | (Correct (Success (3))), 27 | 28 | Test ('variable declared in doctest remains accessible') 29 | ([Line (27) ('> [one, two, three]')]) 30 | ([Line (28) ('[1, 2, 3]')]) 31 | (Correct (Success ([1, 2, 3]))), 32 | 33 | Test ('arithmetic error reported') 34 | ([Line (30) ('> two + two')]) 35 | ([Line (31) ('5')]) 36 | (Incorrect (Success (4)) 37 | (Success (5))), 38 | 39 | Test ('RangeError captured and reported') 40 | ([Line (34) ('> 0..toString(1)')]) 41 | ([Line (35) ('! RangeError')]) 42 | (Correct (Failure (new RangeError ('toString() radix argument must be between 2 and 36')))), 43 | 44 | Test ('TypeError expected but not reported') 45 | ([Line (37) ('> [].length')]) 46 | ([Line (38) ('! TypeError')]) 47 | (Incorrect (Success (0)) 48 | (Failure (new TypeError ()))), 49 | 50 | Test ('function accessible before declaration') 51 | ([Line (41) ('> double(6)')]) 52 | ([Line (42) ('12')]) 53 | (Correct (Success (12))), 54 | 55 | Test ('NaN can be used as expected result') 56 | ([Line (44) ('> double()')]) 57 | ([Line (45) ('NaN')]) 58 | (Correct (Success (NaN))), 59 | 60 | Test ('function accessible after declaration') 61 | ([Line (52) ('> double.call(null, 2)')]) 62 | ([Line (53) ('4')]) 63 | (Correct (Success (4))), 64 | 65 | Test ('multiline input') 66 | ([Line (62) ('> [1,2,3,'), 67 | Line (63) ('. 4,5,6,'), 68 | Line (64) ('. 7,8,9]')]) 69 | ([Line (65) ('[1,2,3,4,5,6,7,8,9]')]) 70 | (Correct (Success ([1, 2, 3, 4, 5, 6, 7, 8, 9]))), 71 | 72 | Test ('multiline assignment') 73 | ([Line (70) ('> string')]) 74 | ([Line (71) ('"input may span many lines"')]) 75 | (Correct (Success ('input may span many lines'))), 76 | 77 | Test ("spaces following '//' and '>' are optional") 78 | ([Line (74) ('>"no spaces"')]) 79 | ([Line (75) ('"no spaces"')]) 80 | (Correct (Success ('no spaces'))), 81 | 82 | Test ('indented doctest') 83 | ([Line (77) ('> "Docco-compatible whitespace"')]) 84 | ([Line (78) ('"Docco-compatible whitespace"')]) 85 | (Correct (Success ('Docco-compatible whitespace'))), 86 | 87 | Test ("'>' in doctest") 88 | ([Line (80) ('> 2 > 1')]) 89 | ([Line (81) ('true')]) 90 | (Correct (Success (true))), 91 | 92 | Test ('comment on input line') 93 | ([Line (84) ('> "foo" + "bar" // comment')]) 94 | ([Line (85) ('"foobar"')]) 95 | (Correct (Success ('foobar'))), 96 | 97 | Test ('comment on output line') 98 | ([Line (87) ('> 5 * 5')]) 99 | ([Line (88) ('25 // comment')]) 100 | (Correct (Success (25))), 101 | 102 | Test ('variable in creation context is not accessible') 103 | ([Line (91) ('> typeof text')]) 104 | ([Line (92) ('"undefined"')]) 105 | (Correct (Success ('undefined'))), 106 | 107 | Test ("'.' should not follow leading '.' in multiline expressions") 108 | ([Line (95) ('>10 -'), 109 | Line (96) ('..5')]) 110 | ([Line (97) ('9.5')]) 111 | (Incorrect (Success (5)) 112 | (Success (9.5))), 113 | 114 | Test ("wrapped lines may begin with more than one '.'") 115 | ([Line (100) ('> 1000 +'), 116 | Line (101) ('.. 200 +'), 117 | Line (102) ('... 30 +'), 118 | Line (103) ('.... 4 +'), 119 | Line (104) ('..... .5')]) 120 | ([Line (105) ('1234.5')]) 121 | (Correct (Success (1234.5))), 122 | 123 | Test ('multiline comment') 124 | ([Line (109) ('> Math.pow(3, 3) - Math.pow(2, 2)')]) 125 | ([Line (110) ('23')]) 126 | (Correct (Success (23))), 127 | 128 | Test ('multiline comment with wrapped input') 129 | ([Line (115) ('> ["foo", "bar", "baz"]'), 130 | Line (116) ('. .slice(0, -1)'), 131 | Line (117) ('. .join(" ")'), 132 | Line (118) ('. .toUpperCase()')]) 133 | ([Line (119) ('"FOO BAR"')]) 134 | (Correct (Success ('FOO BAR'))), 135 | 136 | Test ('multiline comment with leading asterisks') 137 | ([Line (124) ('> 1 + 2 * 3 * 4')]) 138 | ([Line (125) ('25')]) 139 | (Correct (Success (25))), 140 | 141 | Test ('multiline comment with leading asterisks') 142 | ([Line (126) ('> 1 * 2 + 3 + 4 * 5')]) 143 | ([Line (127) ('25')]) 144 | (Correct (Success (25))), 145 | 146 | Test ('multiline comment with leading asterisks and wrapped input') 147 | ([Line (132) ('> (function fib(n) {'), 148 | Line (133) ('. return n == 0 || n == 1 ? n : fib(n - 2) + fib(n - 1);'), 149 | Line (134) ('. })(10)')]) 150 | ([Line (135) ('55')]) 151 | (Correct (Success (55))), 152 | 153 | Test ('multiline output') 154 | ([Line (139) ('> ["foo", "bar", "baz"]')]) 155 | ([Line (140) ('[ "foo",'), 156 | Line (141) ('. "bar",'), 157 | Line (142) ('. "baz" ]')]) 158 | (Correct (Success (['foo', 'bar', 'baz']))), 159 | 160 | Test ('multiline input with multiline output') 161 | ([Line (145) ('> ["foo", "bar", "baz"]'), 162 | Line (146) ('. .join(",")'), 163 | Line (147) ('. .toUpperCase()'), 164 | Line (148) ('. .split(",")')]) 165 | ([Line (149) ('[ "FOO",'), 166 | Line (150) ('. "BAR",'), 167 | Line (151) ('. "BAZ" ]')]) 168 | (Correct (Success (['FOO', 'BAR', 'BAZ']))), 169 | 170 | Test ('the rewriter should not rely on automatic semicolon insertion') 171 | ([Line (154) ('> "the rewriter should not rely"')]) 172 | ([Line (155) ('"on automatic semicolon insertion"')]) 173 | (Incorrect (Success ('the rewriter should not rely')) 174 | (Success ('on automatic semicolon insertion'))), 175 | 176 | ]; 177 | -------------------------------------------------------------------------------- /test/statements/index.js: -------------------------------------------------------------------------------- 1 | // > var x = 64 2 | // 3 | // > Math.sqrt(x) 4 | // 8 5 | 6 | // > let y = -1 7 | // 8 | // > Math.abs(y) 9 | // 1 10 | 11 | // > function fib(n) { return n <= 1 ? n : fib(n - 2) + fib(n - 1); } 12 | // 13 | // > fib(10) 14 | // 55 15 | -------------------------------------------------------------------------------- /test/statements/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('var') 4 | ([Line (3) ('> Math.sqrt(x)')]) 5 | ([Line (4) ('8')]) 6 | (Correct (Success (8))), 7 | 8 | Test ('let') 9 | ([Line (8) ('> Math.abs(y)')]) 10 | ([Line (9) ('1')]) 11 | (Correct (Success (1))), 12 | 13 | Test ('function declaration') 14 | ([Line (13) ('> fib(10)')]) 15 | ([Line (14) ('55')]) 16 | (Correct (Success (55))), 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /test/transcribe/index.coffee: -------------------------------------------------------------------------------- 1 | #% map :: (a -> b) -> Array a -> Array b 2 | #. 3 | #. Transforms a list of elements of type `a` into a list of elements 4 | #. of type `b` using the provided function of type `a -> b`. 5 | #. 6 | #. > This is a Markdown `
` element. If the `--opening-delimiter` 7 | #. > and `--closing-delimiter` options are set to ```coffee and 8 | #. > ``` respectively, these lines will not be evaluated. 9 | #. 10 | #. ```coffee 11 | #. > map(Math.sqrt)([1, 4, 9]) 12 | #. [1, 2, 3] 13 | #. ``` 14 | map = (f) -> (xs) -> f x for x in xs 15 | -------------------------------------------------------------------------------- /test/transcribe/index.js: -------------------------------------------------------------------------------- 1 | //# map :: (a -> b) -> Array a -> Array b 2 | //. 3 | //. Transforms a list of elements of type `a` into a list of elements 4 | //. of type `b` using the provided function of type `a -> b`. 5 | //. 6 | //. > This is a Markdown `
` element. If the `--opening-delimiter` 7 | //. > and `--closing-delimiter` options are set to ```javascript 8 | //. > and ``` respectively, these lines will not be evaluated. 9 | //. 10 | //. ```javascript 11 | //. > map(Math.sqrt)([1, 4, 9]) 12 | //. [1, 2, 3] 13 | //. ``` 14 | var map = function(f) { 15 | return function(xs) { 16 | var output = []; 17 | for (var idx = 0; idx < xs.length; idx += 1) { 18 | output.push(f(xs[idx])); 19 | } 20 | return output; 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /test/transcribe/results.js: -------------------------------------------------------------------------------- 1 | export default ({Test, Line, Correct, Success}) => [ 2 | 3 | Test ('accepts Transcribe-style prefix') 4 | ([Line (11) ('> map(Math.sqrt)([1, 4, 9])')]) 5 | ([Line (12) ('[1, 2, 3]')]) 6 | (Correct (Success ([1, 2, 3]))), 7 | 8 | ]; 9 | --------------------------------------------------------------------------------