Add --jest and --mocha

This commit is contained in:
Dan Rosén 2018-03-05 17:15:31 +01:00
parent ae932d9335
commit 72e11e644a
5 changed files with 274 additions and 221 deletions

View file

@ -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
View 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
}

View file

@ -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()

View file

@ -9,6 +9,5 @@
*/
function hasFoo(s: string): boolean {
return null != s.match(/foo/)
return null != s.match(/foo/i)
}

View file

@ -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