Add --jest and --mocha
This commit is contained in:
parent
ae932d9335
commit
72e11e644a
5 changed files with 274 additions and 221 deletions
11
README.md
11
README.md
|
@ -79,7 +79,16 @@ passed: 6 failed: 0 of 6 tests (171ms)
|
|||
All of 6 tests passed!
|
||||
```
|
||||
|
||||
The default is to use tape output but it can also create ava output. Pull requests for other test runners are welcome.
|
||||
There are four different outputs available:
|
||||
|
||||
```
|
||||
* tape
|
||||
* AVA
|
||||
* jest
|
||||
* mocha (using chai)
|
||||
```
|
||||
|
||||
Pull requests for other test runners are welcome.
|
||||
|
||||
## Watching file changes
|
||||
|
||||
|
|
226
src/internal.ts
Normal file
226
src/internal.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
import * as ts from 'typescript'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Types
|
||||
|
||||
export interface Equality {
|
||||
tag: '=='
|
||||
lhs: string
|
||||
rhs: string
|
||||
}
|
||||
|
||||
export interface Statement {
|
||||
tag: 'Statement'
|
||||
stmt: string
|
||||
}
|
||||
|
||||
export type Script = (Statement | Equality)[]
|
||||
|
||||
export type Context = string | null
|
||||
|
||||
export interface Comment {
|
||||
comment: string
|
||||
context: Context
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Extracting docstrings from program
|
||||
|
||||
export function Comments(s: string): Comment[] {
|
||||
const out: Comment[] = []
|
||||
function add_comment(c: string, context: string | null) {}
|
||||
|
||||
function traverse(node: ts.Node) {
|
||||
const jsdocs = (node as any).jsDoc || []
|
||||
if (jsdocs.length > 0) {
|
||||
let context: string | null = null
|
||||
try {
|
||||
context = (node as any).name.escapedText || null
|
||||
} catch (e) {
|
||||
try {
|
||||
const decls = (node as any).declarationList.declarations
|
||||
if (decls.length == 1) {
|
||||
context = decls[0].name.escapedText || null
|
||||
}
|
||||
} catch (e) {
|
||||
// console.dir(node)
|
||||
context = ts.isConstructorDeclaration(node) ? 'constructor' : null
|
||||
}
|
||||
}
|
||||
jsdocs.forEach((doc: ts.JSDoc) => {
|
||||
out.push({comment: doc.comment || '', context})
|
||||
})
|
||||
}
|
||||
ts.forEachChild(node, traverse)
|
||||
}
|
||||
|
||||
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest)
|
||||
traverse(ast)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Extracting test scripts from docstrings
|
||||
|
||||
/**
|
||||
|
||||
is_doctest('// => true') // => true
|
||||
is_doctest('// true') // => false
|
||||
|
||||
*/
|
||||
const is_doctest = (s: string) => s.match(/\/\/[ \t]*=>/) != null
|
||||
|
||||
/**
|
||||
|
||||
const m = doctest_rhs('// => true') || []
|
||||
m[1] // => ' true'
|
||||
|
||||
*/
|
||||
const doctest_rhs = (s: string) => s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m)
|
||||
|
||||
/**
|
||||
|
||||
extractScript('s') // => [{tag: 'Statement', stmt: 's;'}]
|
||||
|
||||
extractScript('e // => 1') // => [{tag: '==', lhs: 'e', rhs: '1'}]
|
||||
|
||||
extractScript('s; e // => 1') // => [{tag: 'Statement', stmt: 's;'}, {tag: '==', lhs: 'e', rhs: '1'}]
|
||||
|
||||
*/
|
||||
export function extractScript(s: string): Script {
|
||||
const pwoc = ts.createPrinter({removeComments: true})
|
||||
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest)
|
||||
return ast.statements.map((stmt, i): Statement | Equality => {
|
||||
if (ts.isExpressionStatement(stmt)) {
|
||||
const next = ast.statements[i + 1] // zip with next
|
||||
const [a, z] = next ? [next.pos, next.end] : [stmt.end, ast.end]
|
||||
const after = ast.text.slice(a, z)
|
||||
const m = doctest_rhs(after)
|
||||
if (m && m[1]) {
|
||||
const lhs = pwoc.printNode(ts.EmitHint.Expression, stmt.expression, ast)
|
||||
const rhs = m[1].trim()
|
||||
return {tag: '==', lhs, rhs}
|
||||
}
|
||||
}
|
||||
|
||||
return {tag: 'Statement', stmt: pwoc.printNode(ts.EmitHint.Unspecified, stmt, ast)}
|
||||
})
|
||||
}
|
||||
|
||||
export function extractScripts(docstring: string): Script[] {
|
||||
const out = [] as Script[]
|
||||
docstring.split(/\n\n+/m).forEach(s => {
|
||||
if (is_doctest(s)) {
|
||||
out.push(extractScript(s))
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Showing test scripts
|
||||
export interface ShowScript {
|
||||
showImports: string
|
||||
showScript(script: Script, c: Context): string
|
||||
}
|
||||
|
||||
/** show("hello") // => '"hello"' */
|
||||
export function show(s: any) {
|
||||
return JSON.stringify(s)
|
||||
}
|
||||
|
||||
export function showContext(c: Context) {
|
||||
return show(c || 'doctest')
|
||||
}
|
||||
|
||||
function tapeOrAVA(script: Script, c: Context, before_end = (t: string) => '') {
|
||||
const t = `t`
|
||||
const body = script
|
||||
.map(s => {
|
||||
if (s.tag == 'Statement') {
|
||||
return s.stmt
|
||||
} else {
|
||||
return `${t}.deepEqual(${s.lhs}, ${s.rhs}, ${show(s.rhs)})`
|
||||
}
|
||||
})
|
||||
.map(x => '\n ' + x)
|
||||
.join('')
|
||||
return `
|
||||
__test(${showContext(c)}, ${t} => {
|
||||
${body}
|
||||
${before_end(t)}
|
||||
})`
|
||||
}
|
||||
|
||||
const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c) => {
|
||||
const body = script
|
||||
.map(s => {
|
||||
if (s.tag == 'Statement') {
|
||||
return s.stmt
|
||||
} else {
|
||||
return `__expect(${s.lhs}).${deepEqual}(${s.rhs})`
|
||||
}
|
||||
})
|
||||
.map(x => '\n ' + x)
|
||||
.join('')
|
||||
|
||||
return `
|
||||
describe(${showContext(c)}, () => {
|
||||
it(${showContext(c)}, () => {${body}})
|
||||
})
|
||||
`
|
||||
}
|
||||
|
||||
export const showScriptInstances: Record<string, ShowScript> = {
|
||||
ava: {
|
||||
showImports: 'import {test as __test} from "ava"',
|
||||
showScript: tapeOrAVA,
|
||||
},
|
||||
|
||||
tape: {
|
||||
showImports: 'import * as __test from "tape"',
|
||||
showScript: (s, c) => tapeOrAVA(s, c, t => `\n;${t}.end()`),
|
||||
},
|
||||
|
||||
mocha: {
|
||||
showImports: 'import "mocha"\nimport {expect as __expect} from "chai"',
|
||||
showScript: mochaOrJest(`to.deep.equal`),
|
||||
},
|
||||
|
||||
jest: {
|
||||
showImports: 'import "jest"\nconst __expect: jest.Expect = expect',
|
||||
showScript: mochaOrJest(`toEqual`),
|
||||
},
|
||||
}
|
||||
|
||||
export function instrument(d: ShowScript, file: string, mode?: 'watch'): void {
|
||||
const {base, ext, ...u} = path.parse(file)
|
||||
if (base.includes('doctest')) {
|
||||
return
|
||||
}
|
||||
const buffer = fs.readFileSync(file, {encoding: 'utf8'})
|
||||
const tests = Doctests(d, buffer)
|
||||
const outfile = path.format({...u, ext: '.doctest' + ext})
|
||||
if (tests.length == 0) {
|
||||
console.error('No doctests found in', file)
|
||||
} else {
|
||||
console.error('Writing', outfile)
|
||||
if (mode == 'watch') {
|
||||
console.log(outfile)
|
||||
}
|
||||
fs.writeFileSync(outfile, buffer + '\n' + d.showImports + '\n' + tests.join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
function Doctests(d: ShowScript, buffer: string): string[] {
|
||||
const out: string[] = []
|
||||
for (const c of Comments(buffer)) {
|
||||
for (const script of extractScripts(c.comment)) {
|
||||
out.push(d.showScript(script, c.context))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
231
src/main.ts
231
src/main.ts
|
@ -1,211 +1,34 @@
|
|||
#!/usr/bin/env node
|
||||
import * as fs from 'fs'
|
||||
import * as chokidar from 'chokidar'
|
||||
import * as minimist from 'minimist'
|
||||
import * as ts from 'typescript'
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Types
|
||||
|
||||
export interface Equality {
|
||||
tag: '=='
|
||||
lhs: string
|
||||
rhs: string
|
||||
}
|
||||
|
||||
export interface Statement {
|
||||
tag: 'Statement'
|
||||
stmt: string
|
||||
}
|
||||
|
||||
export type Script = (Statement | Equality)[]
|
||||
|
||||
export type Context = string | null
|
||||
|
||||
export interface Comment {
|
||||
comment: string
|
||||
context: Context
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Extracting docstrings from program
|
||||
|
||||
export function Comments(s: string): Comment[] {
|
||||
const out: Comment[] = []
|
||||
function add_comment(c: string, context: string | null) {}
|
||||
|
||||
function traverse(node: ts.Node) {
|
||||
const jsdocs = (node as any).jsDoc || []
|
||||
if (jsdocs.length > 0) {
|
||||
let context: string | null = null
|
||||
try {
|
||||
context = (node as any).name.escapedText || null
|
||||
} catch (e) {
|
||||
try {
|
||||
const decls = (node as any).declarationList.declarations
|
||||
if (decls.length == 1) {
|
||||
context = decls[0].name.escapedText || null
|
||||
}
|
||||
} catch (e) {
|
||||
// console.dir(node)
|
||||
context = ts.isConstructorDeclaration(node) ? 'constructor' : null
|
||||
}
|
||||
}
|
||||
jsdocs.forEach((doc: ts.JSDoc) => {
|
||||
out.push({comment: doc.comment || '', context})
|
||||
})
|
||||
}
|
||||
ts.forEachChild(node, traverse)
|
||||
}
|
||||
|
||||
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest)
|
||||
traverse(ast)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Extracting test scripts from docstrings
|
||||
|
||||
/**
|
||||
|
||||
is_doctest('// => true') // => true
|
||||
is_doctest('// true') // => false
|
||||
|
||||
*/
|
||||
const is_doctest = (s: string) => s.match(/\/\/[ \t]*=>/) != null
|
||||
|
||||
/**
|
||||
|
||||
const m = doctest_rhs('// => true') || []
|
||||
m[1] // => ' true'
|
||||
|
||||
*/
|
||||
const doctest_rhs = (s: string) => s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m)
|
||||
|
||||
/**
|
||||
|
||||
extractScript('s') // => [{tag: 'Statement', stmt: 's;'}]
|
||||
|
||||
extractScript('e // => 1') // => [{tag: '==', lhs: 'e', rhs: '1'}]
|
||||
|
||||
extractScript('s; e // => 1') // => [{tag: 'Statement', stmt: 's;'}, {tag: '==', lhs: 'e', rhs: '1'}]
|
||||
|
||||
*/
|
||||
export function extractScript(s: string): Script {
|
||||
const pwoc = ts.createPrinter({removeComments: true})
|
||||
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest)
|
||||
return ast.statements.map((stmt, i): Statement | Equality => {
|
||||
if (ts.isExpressionStatement(stmt)) {
|
||||
const next = ast.statements[i + 1] // zip with next
|
||||
const [a, z] = next ? [next.pos, next.end] : [stmt.end, ast.end]
|
||||
const after = ast.text.slice(a, z)
|
||||
const m = doctest_rhs(after)
|
||||
if (m && m[1]) {
|
||||
const lhs = pwoc.printNode(ts.EmitHint.Expression, stmt.expression, ast)
|
||||
const rhs = m[1].trim()
|
||||
return {tag: '==', lhs, rhs}
|
||||
}
|
||||
}
|
||||
|
||||
return {tag: 'Statement', stmt: pwoc.printNode(ts.EmitHint.Unspecified, stmt, ast)}
|
||||
})
|
||||
}
|
||||
|
||||
export function extractScripts(docstring: string): Script[] {
|
||||
const out = [] as Script[]
|
||||
docstring.split(/\n\n+/m).forEach(s => {
|
||||
if (is_doctest(s)) {
|
||||
out.push(extractScript(s))
|
||||
}
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// Showing test scripts
|
||||
export interface ShowScript {
|
||||
showImports: string
|
||||
showScript(script: Script, c: Context): string
|
||||
}
|
||||
|
||||
/** show("hello") // => '"hello"' */
|
||||
export function show(s: any) {
|
||||
return JSON.stringify(s)
|
||||
}
|
||||
|
||||
export function showContext(c: Context) {
|
||||
return show(c || 'doctest')
|
||||
}
|
||||
|
||||
function showScript(script: Script, c: Context, before_end = (t: string) => '') {
|
||||
const t = `t`
|
||||
const body = script
|
||||
.map(s => {
|
||||
if (s.tag == 'Statement') {
|
||||
return s.stmt
|
||||
} else {
|
||||
return `${t}.deepEqual(${s.lhs}, ${s.rhs}, ${show(s.rhs)})`
|
||||
}
|
||||
})
|
||||
.join('\n')
|
||||
return `__test(${showContext(c)}, ${t} => {${body}${before_end(t)}})`
|
||||
}
|
||||
|
||||
const ava: ShowScript = {
|
||||
showImports: 'import {test as __test} from "ava"',
|
||||
showScript: showScript,
|
||||
}
|
||||
|
||||
const tape: ShowScript = {
|
||||
showImports: 'import * as __test from "tape"',
|
||||
showScript: (s, c) => showScript(s, c, t => `\n;${t}.end()`),
|
||||
}
|
||||
|
||||
const showScriptInstances = {ava, tape}
|
||||
|
||||
import * as path from 'path'
|
||||
|
||||
function instrument(d: ShowScript, file: string, mode?: 'watch'): void {
|
||||
const {base, ext, ...u} = path.parse(file)
|
||||
if (base.includes('doctest')) {
|
||||
return
|
||||
}
|
||||
const buffer = fs.readFileSync(file, {encoding: 'utf8'})
|
||||
const tests = Doctests(d, buffer)
|
||||
const outfile = path.format({...u, ext: '.doctest' + ext})
|
||||
if (tests.length == 0) {
|
||||
console.error('No doctests found in', file)
|
||||
} else {
|
||||
console.error('Writing', outfile)
|
||||
if (mode == 'watch') {
|
||||
console.log(outfile)
|
||||
}
|
||||
fs.writeFileSync(outfile, buffer + '\n' + d.showImports + '\n' + tests)
|
||||
}
|
||||
}
|
||||
|
||||
function Doctests(d: ShowScript, buffer: string): string[] {
|
||||
const out: string[] = []
|
||||
for (const c of Comments(buffer)) {
|
||||
for (const script of extractScripts(c.comment)) {
|
||||
out.push(d.showScript(script, c.context))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
import {instrument, showScriptInstances} from './internal'
|
||||
function main() {
|
||||
const opts = minimist(process.argv.slice(2), {boolean: ['tape', 'ava', 'watch']})
|
||||
const d = showScriptInstances[opts.ava == true ? 'ava' : 'tape']
|
||||
const outputs = Object.keys(showScriptInstances)
|
||||
const flags = outputs.map(f => '--' + f)
|
||||
const boolean = ['watch'].concat(outputs)
|
||||
const opts = minimist(process.argv.slice(2), {boolean})
|
||||
let output: string | null = null
|
||||
let error: string | null = null
|
||||
outputs.forEach(k => {
|
||||
if (opts[k]) {
|
||||
if (output != null) {
|
||||
error = `Cannot output both ${output} and ${k}`
|
||||
}
|
||||
output = k
|
||||
}
|
||||
})
|
||||
if (output == null) {
|
||||
error = `Choose an output from ${flags.join(' ')}`
|
||||
}
|
||||
const files = opts._
|
||||
if (files.length == 0) {
|
||||
if (files.length == 0 || output == null) {
|
||||
console.error(
|
||||
`No files specified!
|
||||
`
|
||||
Error: ${error || `No files specified!`}
|
||||
|
||||
Usage:
|
||||
|
||||
[-w|--watch] [--ava|--tape] files globs...
|
||||
${flags.join('|')} [-w|--watch] files globs...
|
||||
|
||||
Your options were:`,
|
||||
opts,
|
||||
|
@ -213,8 +36,9 @@ function main() {
|
|||
From:`,
|
||||
process.argv
|
||||
)
|
||||
process.exit(1)
|
||||
return
|
||||
}
|
||||
const d = showScriptInstances[output]
|
||||
files.forEach(file => instrument(d, file))
|
||||
if (opts.w == true || opts.watch == true) {
|
||||
const watcher = chokidar.watch(files, {ignored: '*.doctest.*'})
|
||||
|
@ -222,9 +46,4 @@ function main() {
|
|||
}
|
||||
}
|
||||
|
||||
// if we are checking the doctests of this very file we don't need to run main
|
||||
if (~process.argv[1].indexOf('.doctest.')) {
|
||||
console.debug(`Not running main since argv[1] contains '.doctest.':`, process.argv)
|
||||
} else {
|
||||
main()
|
||||
}
|
||||
main()
|
||||
|
|
|
@ -9,6 +9,5 @@
|
|||
|
||||
*/
|
||||
function hasFoo(s: string): boolean {
|
||||
return null != s.match(/foo/)
|
||||
return null != s.match(/foo/i)
|
||||
}
|
||||
|
||||
|
|
24
test/test.ts
24
test/test.ts
|
@ -1,10 +1,10 @@
|
|||
import * as main from '../src/main'
|
||||
import * as internal from '../src/internal'
|
||||
import * as test from 'tape'
|
||||
|
||||
test('tests', t => {
|
||||
t.plan(1)
|
||||
t.deepEqual(
|
||||
main.extractScripts(`*
|
||||
internal.extractScripts(`*
|
||||
|
||||
foo // => 1
|
||||
|
||||
|
@ -16,7 +16,7 @@ test('tests', t => {
|
|||
test('tests', t => {
|
||||
t.plan(1)
|
||||
t.deepEqual(
|
||||
main.extractScripts(`*
|
||||
internal.extractScripts(`*
|
||||
|
||||
a
|
||||
b // => 1 + 2 + 3
|
||||
|
@ -39,7 +39,7 @@ const c = (comment: string, context: string | null) => ({comment, context})
|
|||
|
||||
test('modules and namespace', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** m */
|
||||
namespace m {}
|
||||
|
||||
|
@ -51,7 +51,7 @@ test('modules and namespace', t => {
|
|||
|
||||
test('const', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** u */
|
||||
const u = 1
|
||||
`)
|
||||
|
@ -60,7 +60,7 @@ test('const', t => {
|
|||
|
||||
test('const object', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** k */
|
||||
const k = {
|
||||
/** a */
|
||||
|
@ -74,7 +74,7 @@ test('const object', t => {
|
|||
|
||||
test('object deconstruction', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** hello */
|
||||
const {u, v} = {u: 1, v: 2}
|
||||
`)
|
||||
|
@ -83,7 +83,7 @@ test('object deconstruction', t => {
|
|||
|
||||
test('function', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** v */
|
||||
function v(s: string): number {
|
||||
return s.length + 1
|
||||
|
@ -94,7 +94,7 @@ test('function', t => {
|
|||
|
||||
test('class', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** C */
|
||||
class C<A> {
|
||||
/** constructor */
|
||||
|
@ -111,7 +111,7 @@ test('class', t => {
|
|||
|
||||
test('interface', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** I */
|
||||
interface I<A> {
|
||||
/** i */
|
||||
|
@ -125,7 +125,7 @@ test('interface', t => {
|
|||
|
||||
test('type', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
/** T */
|
||||
type T = number
|
||||
`)
|
||||
|
@ -134,7 +134,7 @@ test('type', t => {
|
|||
|
||||
test('anywhere', t => {
|
||||
t.plan(1)
|
||||
const cs = main.Comments(`
|
||||
const cs = internal.Comments(`
|
||||
const $ = () => {
|
||||
/** test1 */
|
||||
const w = 1
|
||||
|
|
Loading…
Add table
Reference in a new issue