├── .babelrc ├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── Makefile ├── README.md ├── bin └── mocha-doctest ├── decls └── utils.js ├── index.js ├── package.json ├── register.js ├── runtime.js └── src ├── compile.js ├── index.js ├── register.js ├── runtime.js └── testutils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "ast-literal" 4 | ], 5 | "presets": [ 6 | "prometheusresearch" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/esformatter/.* 3 | .*/node_modules/fbjs/.*.flow 4 | .*/node_modules/.store/fbjs@.*/.*.flow 5 | .*/node_modules/.store/gh-pages@.*/.* 6 | .*/node_modules/.store/browserify-zlib@.*/_/package.json 7 | .*/test/.* 8 | 9 | [include] 10 | 11 | [libs] 12 | decls 13 | 14 | [options] 15 | esproposal.class_static_fields=enable 16 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | examples/webpack/bundle 4 | site/bundle 5 | npm-debug.log 6 | .testcase.testmd 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | npm-debug.log 3 | .nyc_output/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DELETE_ON_ERROR: 2 | 3 | BIN = ./node_modules/.bin 4 | TESTS = $(shell find src -path '*/__tests__/*-test.js') 5 | SRC = $(filter-out $(TESTS) $(FIXTURES), $(shell find src -name '*.js')) 6 | LIB = $(SRC:src/%=lib/%) 7 | MOCHA_OPTS = -R dot --require babel-core/register --timeout 4000 8 | 9 | build:: 10 | @$(MAKE) -j 8 $(LIB) 11 | 12 | build-silent:: 13 | @$(MAKE) -s -j 8 $(LIB) 14 | 15 | lint:: 16 | @$(BIN)/eslint src 17 | 18 | check:: 19 | @$(BIN)/flow --show-all-errors src 20 | 21 | test:: build-silent 22 | @$(BIN)/mocha $(MOCHA_OPTS) --compilers md:./lib/register ./README.md 23 | 24 | ci:: 25 | @$(BIN)/mocha $(MOCHA_OPTS) --compilers md:./lib/register -w ./README.md 26 | 27 | sloc:: 28 | @$(BIN)/sloc -e __tests__ src 29 | 30 | version-major version-minor version-patch:: lint test 31 | @npm version $(@:version-%=%) 32 | 33 | publish:: build 34 | @git push --tags origin HEAD:master 35 | @npm publish 36 | 37 | clean:: 38 | @rm -rf lib 39 | 40 | lib/%.js: src/%.js 41 | @echo "Building $<" 42 | @mkdir -p $(@D) 43 | @$(BIN)/babel $(BABEL_OPTIONS) -o $@ $< 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mocha-doctest 2 | 3 | [![Travis build status](https://img.shields.io/travis/andreypopp/mocha-doctest/master.svg)](https://travis-ci.org/andreypopp/mocha-doctest) 4 | 5 | Test your documentation. 6 | 7 | Snippets like this: 8 | 9 | Ok, let's test addition: 10 | 11 | ```js+test 12 | 2 + 2 13 | // => 4 14 | ``` 15 | 16 | are coverted into runnable Mocha test suites which can be tested with the 17 | following command: 18 | 19 | ``` 20 | % mocha --compilers md:mocha-doctest ./README.md 21 | ``` 22 | 23 | ## Installation & Usage 24 | 25 | Install with: 26 | 27 | ``` 28 | % npm install mocha-doctest mocha 29 | ``` 30 | 31 | Run with: 32 | 33 | ``` 34 | % mocha --compilers md:mocha-doctest ./README.md 35 | ``` 36 | 37 | ## Assertions against text representation 38 | 39 | Trailing comments which starts with `=>` are treated as assertions against 40 | textual representation of an expression which goes before: 41 | 42 | ```js+test 43 | 2 + 2 44 | // => 4 45 | ``` 46 | 47 | ## Regular assertions 48 | 49 | Also `assert` Node.js module is available so you can use it directly: 50 | 51 | ```js+test 52 | assert(2 + 2 === 4) 53 | ``` 54 | 55 | ## Assertions for errors 56 | 57 | If trailing comment is detected and start with `Error:` (actually any error name 58 | which ends with `Error` suffix) line then it is treated as an assertion against 59 | an error being thrown: 60 | 61 | ```js+test 62 | let maybeFunction = undefined; 63 | 64 | undefined() 65 | // TypeError: undefined is not a function 66 | 67 | null() 68 | // TypeError: ... 69 | ``` 70 | 71 | ## Testing async code (with Promise based API) 72 | 73 | There's a possibility to test async code based on Promise API: 74 | 75 | ```js+test 76 | function sleep(ms) { 77 | let promise = new Promise(resolve => setTimeout(resolve, ms)); 78 | promise = promise.then(value => { 79 | value; 80 | // => undefined 81 | return value; 82 | }); 83 | return promise; 84 | } 85 | 86 | await sleep(50).then(_ => 42); 87 | // => 42 88 | ``` 89 | 90 | ## Using wildcard pattern 91 | 92 | You can use `...` specify a wildcards: 93 | 94 | ```js+test 95 | let a = [1, 2, Math.random(), 4]; 96 | 97 | a 98 | // => [ 1, 2, ..., 4 ] 99 | ``` 100 | 101 | ## Babel 102 | 103 | Project babel configuration will be used (either `.babelrc` or `"babel"` key in 104 | `package.json`) but currently at least `es2015` preset must be used as 105 | mocha-doctest emits ES2015 code. That might change is the future. 106 | 107 | # Tests 108 | 109 | Import required harness: 110 | 111 | ```js+test 112 | import {runTest} from 'mocha-doctest/lib/testutils' 113 | ``` 114 | 115 | Simple assertions: 116 | 117 | ```js+test 118 | let {passes, failures} = runTest(` 119 | 1 120 | // => 1 121 | `) 122 | 123 | passes.length 124 | // => 1 125 | 126 | failures.length 127 | // => 0 128 | ``` 129 | 130 | Assertions against an error: 131 | 132 | ```js+test 133 | let {passes, failures} = runTest(` 134 | undefined() 135 | // TypeError: undefined is not a function 136 | `) 137 | 138 | passes.length 139 | // => 1 140 | 141 | failures.length 142 | // => 0 143 | ``` 144 | 145 | 146 | Simple assertion with failure: 147 | 148 | ```js+test 149 | let {passes, failures} = runTest(` 150 | 2 + 2 151 | // => 5 152 | `) 153 | 154 | passes.length 155 | // => 0 156 | 157 | failures.length 158 | // => 1 159 | 160 | failures[0].err.expected 161 | // => '5' 162 | failures[0].err.actual 163 | // => '4' 164 | ``` 165 | 166 | More complex assertions: 167 | 168 | ```js+test 169 | let {passes, failures} = runTest(` 170 | [1, 2, 3] 171 | // => [ 1, 2, 3 ] 172 | `) 173 | 174 | passes.length 175 | // => 1 176 | 177 | failures.length 178 | // => 0 179 | ``` 180 | 181 | Assertions which use `...` wildcard: 182 | 183 | ```js+test 184 | let {passes, failures} = runTest(` 185 | [1, Math.random(), 3] 186 | // => [ 1, ..., 3 ] 187 | `) 188 | 189 | passes.length 190 | // => 1 191 | 192 | failures.length 193 | // => 0 194 | ``` 195 | 196 | Assertions against an error with `...` wildcard: 197 | 198 | ```js+test 199 | let {passes, failures} = runTest(` 200 | undefined() 201 | // TypeError: ... 202 | `) 203 | 204 | passes.length 205 | // => 1 206 | 207 | failures.length 208 | // => 0 209 | ``` 210 | 211 | Assertions which use `...` wildcard (failure): 212 | 213 | ```js+test 214 | let {passes, failures} = runTest(` 215 | let array = [1, Math.random(), 3] 216 | 217 | array 218 | // => [ 1, ..., 4 ] 219 | `) 220 | 221 | passes.length 222 | // => 0 223 | 224 | failures.length 225 | // => 1 226 | 227 | failures[0].err.expected 228 | // => '[ 1, ..., 4 ]' 229 | ``` 230 | 231 | Assertions which async code: 232 | 233 | ```js+test 234 | let {passes, failures} = runTest(` 235 | await 1 236 | // => 1 237 | 238 | await Promise.resolve(42) 239 | // => 42 240 | `) 241 | 242 | passes.length 243 | // => 1 244 | 245 | failures.length 246 | // => 0 247 | ``` 248 | 249 | Assertions which async code (failures) 250 | 251 | ```js+test 252 | let {passes, failures} = runTest(` 253 | await Promise.resolve(42) 254 | // => 43 255 | `) 256 | 257 | passes.length 258 | // => 0 259 | 260 | failures.length 261 | // => 1 262 | 263 | failures[0].err.expected 264 | // => '43' 265 | failures[0].err.actual 266 | // => '42' 267 | ``` 268 | 269 | Assertions within async callbacks: 270 | 271 | ```js+test 272 | let {passes, failures} = runTest(` 273 | await Promise.resolve(42).then(async value => { 274 | value; 275 | // => 42 276 | await Promise.resolve(43) 277 | // => 43 278 | }) 279 | `) 280 | 281 | passes.length 282 | // => 1 283 | 284 | failures.length 285 | // => 0 286 | ``` 287 | 288 | Assertions which async code errors: 289 | 290 | ```js+test 291 | let {passes, failures} = runTest(` 292 | await Promise.reject(new Error('oops')) 293 | // Error: oops 294 | `) 295 | 296 | passes.length 297 | // => 1 298 | 299 | failures.length 300 | // => 0 301 | ``` 302 | 303 | Assertions which async code errors (failures): 304 | 305 | ```js+test 306 | let {passes, failures} = runTest(` 307 | await Promise.reject(new Error('oops')) 308 | // SomeError: nope 309 | `) 310 | 311 | passes.length 312 | // => 0 313 | 314 | failures.length 315 | // => 1 316 | 317 | failures[0].err.expected 318 | // => 'SomeError: nope' 319 | failures[0].err.actual 320 | // => 'Error: oops' 321 | ``` 322 | -------------------------------------------------------------------------------- /bin/mocha-doctest: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | MOCHA=`node -p 'require.resolve("mocha/bin/mocha")'` 3 | MOCHA_DOCTEST=`node -p 'require.resolve("mocha-doctest")'` 4 | exec $MOCHA --compilers md:mocha-doctest $* 5 | -------------------------------------------------------------------------------- /decls/utils.js: -------------------------------------------------------------------------------- 1 | declare var expr; 2 | declare var stmt; 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mocha-doctest", 3 | "version": "0.3.4", 4 | "description": "Test your documentation", 5 | "main": "./index.js", 6 | "bin": { 7 | "mocha-doctest": "bin/mocha-doctest" 8 | }, 9 | "scripts": { 10 | "test": "make lint test" 11 | }, 12 | "babel": {}, 13 | "eslintConfig": { 14 | "extends": "prometheusresearch", 15 | "env": { 16 | "mocha": true 17 | }, 18 | "globals": { 19 | "expr": true, 20 | "stmt": true, 21 | "$Shape": true 22 | } 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/andreypopp/mocha-doctest.git" 27 | }, 28 | "keywords": [ 29 | "doctest", 30 | "markdown" 31 | ], 32 | "author": "Andrey Popp <8mayday@gmail.com>", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/andreypopp/mocha-doctest/issues" 36 | }, 37 | "homepage": "https://github.com/andreypopp/mocha-doctest#readme", 38 | "dependencies": { 39 | "babel-core": "^6.7.7", 40 | "babel-generator": "^6.7.7", 41 | "babel-polyfill": "^6.7.4", 42 | "babel-preset-es2015": "^6.6.0", 43 | "babel-preset-stage-1": "^6.5.0", 44 | "babel-traverse": "^6.7.6", 45 | "babel-types": "^6.7.7", 46 | "babylon": "^6.7.0", 47 | "find-package-json": "^1.0.0", 48 | "mdast-util-to-string": "^1.0.1", 49 | "remark": "^4.2.2", 50 | "unist-util-visit": "^1.1.0" 51 | }, 52 | "devDependencies": { 53 | "babel-cli": "^6.7.7", 54 | "babel-plugin-ast-literal": "^0.4.0", 55 | "babel-preset-prometheusresearch": "^0.1.0", 56 | "eslint": "^2.8.0", 57 | "eslint-config-prometheusresearch": "^0.2.0", 58 | "flow-bin": "^0.23.1", 59 | "mocha": "^2.4.5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /register.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/register'); 2 | -------------------------------------------------------------------------------- /runtime.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/runtime'); 2 | -------------------------------------------------------------------------------- /src/compile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2016-present, Andrey Popp <8mayday@gmail.com> 3 | * @flow 4 | */ 5 | 6 | import path from 'path'; 7 | import generate from 'babel-generator'; 8 | import * as BabelCore from 'babel-core'; 9 | import * as types from 'babel-types'; 10 | import * as Remark from 'remark'; 11 | import traverse from 'babel-traverse'; 12 | import * as Babylon from 'babylon'; 13 | import mdastVisitNode from 'unist-util-visit'; 14 | import mdastToString from 'mdast-util-to-string'; 15 | import findPackageJSON from 'find-package-json'; 16 | 17 | type JSASTComment = { 18 | value: string; 19 | }; 20 | type JSAST = { 21 | trailingComments: ?Array; 22 | }; 23 | 24 | type Options = $Shape<{ 25 | name: string, 26 | filename: string 27 | }>; 28 | 29 | const REPR_ASSERTION_RE = /^\s*=> /; 30 | const ERR_ASSSERTION_RE = /^\s*([a-zA-Z]*Error): /; 31 | 32 | const TESTDOC_SEEN = '__TESTDOC_SEEN'; 33 | const RUNTIME = require.resolve('./runtime'); 34 | const POLYFILL = require.resolve('babel-polyfill'); 35 | 36 | const SUPPORTED_LANG = { 37 | 'js+test': true, 38 | 'javasctipt+test': true, 39 | }; 40 | 41 | export function compile(source: string, options: Options = {}) { 42 | 43 | // Find all code blocks in markdown 44 | let node = Remark.parse(source); 45 | let testCaseList = []; 46 | mdastVisitNode(node, 'code', (node, index, parent) => { 47 | let prev = parent.children[index - 1]; 48 | let title = null; 49 | if (prev && prev.type === 'paragraph') { 50 | title = mdastToString(prev); 51 | } 52 | if (title && title[title.length - 1] === ':') { 53 | title = title.slice(0, title.length - 1); 54 | } 55 | if (SUPPORTED_LANG[node.lang]) { 56 | testCaseList.push({ 57 | title: title, 58 | lang: node.lang, 59 | value: node.value 60 | }); 61 | } 62 | }); 63 | 64 | // Find all assertions within code blocks and generate case and import nodes 65 | // from them. 66 | let importList = []; 67 | let caseList = []; 68 | testCaseList.forEach(testCase => { 69 | let src = testCase.value; 70 | src = `async function testcase() { 71 | ${src} 72 | }`; 73 | let node = Babylon.parse(src, { 74 | allowImportExportEverywhere: true, 75 | plugins: ['flow', 'objectRestSpread', 'asyncFunctions'], 76 | }); 77 | traverse(node, { 78 | 79 | enter(path) { 80 | let assertion = findAssertion(path.node); 81 | if (!path.node[TESTDOC_SEEN] && assertion) { 82 | let nodes; 83 | if (assertion === 'repr') { 84 | let {repr} = parseExpectationFromNode(path.node, assertion); 85 | nodes = stmt` 86 | __MochaDoctestRuntime.assertRepr( 87 | ${path.node.expression}, 88 | "${repr}" 89 | ); 90 | `; 91 | } else if (assertion === 'error') { 92 | let {name, message} = parseExpectationFromNode(path.node, assertion); 93 | nodes = stmt` 94 | async function dummy() { 95 | await __MochaDoctestRuntime.assertError( 96 | async () => ${path.node.expression}, 97 | "${name}", "${message}" 98 | ); 99 | } 100 | `; 101 | nodes = nodes.body.body[0]; 102 | } 103 | // $FlowIssue: .. 104 | nodes[TESTDOC_SEEN] = true; 105 | path.replaceWithMultiple(nodes); 106 | } else if (types.isImportDeclaration(path.node)) { 107 | importList.push(path.node); 108 | path.remove(); 109 | } 110 | } 111 | 112 | }); 113 | 114 | caseList = caseList.concat(stmt` 115 | it( 116 | "${types.stringLiteral(testCase.title || 'works')}", 117 | ${caseExpression(node.program.body[0].body.body, {async: true})} 118 | ); 119 | `); 120 | }); 121 | 122 | // generate program 123 | let program = []; 124 | program = program.concat( 125 | stmt` 126 | import '${types.stringLiteral(POLYFILL)}'; 127 | import * as __MochaDoctestRuntime from '${types.stringLiteral(RUNTIME)}'; 128 | import assert from 'assert'; 129 | `, 130 | importList, 131 | stmt` 132 | describe( 133 | "${types.stringLiteral(options.name || options.filename || 'Suite')}", 134 | ${caseExpression(caseList)} 135 | ); 136 | ` 137 | ); 138 | 139 | let packageJSONCache = {}; 140 | 141 | // apply babel transformations 142 | program = types.program(program); 143 | program = BabelCore.transformFromAst(program, undefined, { 144 | babelrc: true, 145 | filename: options.filename, 146 | resolveModuleSource(source, filename) { 147 | let dirname = path.dirname(filename); 148 | if (packageJSONCache[dirname] === undefined) { 149 | packageJSONCache[dirname] = findPackageJSON(dirname).next().value || null; 150 | } 151 | if (packageJSONCache[dirname] != null) { 152 | let packageJSON = packageJSONCache[dirname]; 153 | if (source === packageJSON.name) { 154 | let packageDirname = path.relative( 155 | dirname, 156 | path.dirname(packageJSON.__path) 157 | ) || '.'; 158 | return packageDirname + '/'; 159 | } 160 | if (source.indexOf(packageJSON.name + '/') === 0) { 161 | let packageDirname = path.relative( 162 | dirname, 163 | path.dirname(packageJSON.__path) 164 | ) || '.'; 165 | return packageDirname + source.slice(packageJSON.name.length); 166 | } 167 | } 168 | return source; 169 | } 170 | }).ast; 171 | 172 | return generate(program).code; 173 | } 174 | 175 | function caseExpression(body, options = {async: false}) { 176 | return types.functionExpression(null, [], types.blockStatement(body), false, options.async); 177 | } 178 | 179 | function parseExpectationFromNode(node, assertion) { 180 | let firstLine = node.trailingComments[0].value; 181 | let restLines = node.trailingComments.slice(1).map(comment => comment.value.slice(1)); 182 | if (assertion === 'repr') { 183 | firstLine = firstLine.replace(REPR_ASSERTION_RE, ''); 184 | let repr = [firstLine].concat(restLines).join('\n'); 185 | return { 186 | repr: types.stringLiteral(repr) 187 | }; 188 | } else if (assertion === 'error') { 189 | let name = ERR_ASSSERTION_RE.exec(firstLine)[1]; 190 | let message = firstLine.replace(ERR_ASSSERTION_RE, ''); 191 | message = [message].concat(restLines).join('\n'); 192 | return { 193 | name: types.stringLiteral(name), 194 | message: types.stringLiteral(message), 195 | }; 196 | } 197 | } 198 | 199 | function findAssertion(node: JSAST): false | 'repr' | 'error' { 200 | if ( 201 | types.isExpressionStatement(node) && 202 | node.trailingComments && 203 | node.trailingComments.length > 0 204 | ) { 205 | let firstLine = node.trailingComments[0].value; 206 | if (REPR_ASSERTION_RE.exec(firstLine)) { 207 | return 'repr'; 208 | } else if (ERR_ASSSERTION_RE.exec(firstLine)) { 209 | return 'error'; 210 | } else { 211 | return false; 212 | } 213 | } else { 214 | return false; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2016-present, Andrey Popp <8mayday@gmail.com> 3 | */ 4 | 5 | import './register'; 6 | -------------------------------------------------------------------------------- /src/register.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2016-present, Andrey Popp <8mayday@gmail.com> 3 | */ 4 | 5 | import fs from 'fs'; 6 | import {compile} from './compile'; 7 | 8 | const EXTENSION = '.' + (process.env.MOCHA_DOCTEST_EXT || 'md'); 9 | 10 | function loadTestdoc(module, filename) { 11 | let content = fs.readFileSync(filename, 'utf8'); 12 | let source = compile(content, {filename: filename}); 13 | return module._compile(source, filename); 14 | } 15 | 16 | require.extensions[EXTENSION] = loadTestdoc; 17 | -------------------------------------------------------------------------------- /src/runtime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Testdoc runtime. 3 | * 4 | * Some of the code is based on doctest.js by Ian Bicking. 5 | * 6 | * @copyright 2016-present, Andrey Popp <8mayday@gmail.com> 7 | * @copyright 2013, Ian Bicking and Contributors 8 | */ 9 | 10 | import {inspect} from 'util'; 11 | import assert from 'assert'; 12 | 13 | export function assertRepr(expression, repr) { 14 | let pattern = compilePattern(repr); 15 | let r = inspect(expression); 16 | if (!pattern.exec(r)) { 17 | assert.equal(r, repr); 18 | } 19 | } 20 | 21 | export async function assertError(expression, name, message) { 22 | try { 23 | await expression(); 24 | } catch(err) { 25 | let repr = `${name}: ${message}`; 26 | let pattern = compilePattern(repr); 27 | let r = `${err.name}: ${err.message}`; 28 | if (!pattern.exec(r)) { 29 | assert.equal(r, repr); 30 | } 31 | return; 32 | } 33 | assert(false, 'Missing exception'); 34 | } 35 | 36 | /** 37 | * Compile expectation pattern to regexp. 38 | * 39 | * Based on code found in doctest.js by Ian Bicking. 40 | */ 41 | export function compilePattern(pattern) { 42 | let re = regexpEscape(pattern); 43 | re = '^' + re + '$'; 44 | re = re.replace(/\\\.\\\.\\\./g, '[\\S\\s\\r\\n]*'); 45 | re = re.replace(/\\\?/g, '[a-zA-Z0-9_.\\?]+'); 46 | re = re.replace(/[ \t]+/g, ' +'); 47 | re = re.replace(/["']/g, '[\'"]'); 48 | return new RegExp(re); 49 | } 50 | 51 | const REGEXP_SPECIALS = [ 52 | '/', '.', '*', '+', '?', '|', '$', 53 | '(', ')', '[', ']', '{', '}', '\\' 54 | ]; 55 | const REGEXP_SPECIALS_RE = new RegExp('(\\' + REGEXP_SPECIALS.join('|\\') + ')', 'g'); 56 | 57 | /** 58 | * Escape text for use as a regexp. 59 | * 60 | * Based on code found in doctest.js by Ian Bicking. 61 | */ 62 | function regexpEscape(text) { 63 | return text.replace(REGEXP_SPECIALS_RE, '\\$1'); 64 | } 65 | -------------------------------------------------------------------------------- /src/testutils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @copyright 2016-present, Andrey Popp <8mayday@gmail.com> 3 | */ 4 | 5 | import {spawnSync} from 'child_process'; 6 | import fs from 'fs'; 7 | import path from 'path'; 8 | 9 | const REGISTER = require.resolve('../register'); 10 | const CMD = require.resolve('mocha/bin/mocha'); 11 | const OPTIONS = `-R json --compilers testmd:${REGISTER}`.split(' '); 12 | 13 | export function runTest(source) { 14 | source = '```js+test\n' + source + '\n```'; 15 | 16 | let filename = path.join(__dirname, '..', '.testcase.testmd'); 17 | try { 18 | fs.writeFileSync(filename, source); 19 | let info = spawnSync(CMD, OPTIONS.concat(filename), { 20 | env: { 21 | ...process.env, 22 | MOCHA_DOCTEST_EXT: 'testmd' 23 | } 24 | }); 25 | let stdout = info.stdout.toString(); 26 | return JSON.parse(stdout); 27 | } finally { 28 | fs.unlinkSync(filename); 29 | } 30 | } 31 | --------------------------------------------------------------------------------