Add support for a title, add support for tags in docstrings, expose private methods in doctests to make them testable
This commit is contained in:
parent
1812b4e0e8
commit
e8ddc329e8
3 changed files with 75 additions and 13 deletions
18
examples/example0.ts
Normal file
18
examples/example0.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
|
||||||
|
export default class Example0 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the field doubled
|
||||||
|
* @example xyz
|
||||||
|
*
|
||||||
|
* // Should equal 42
|
||||||
|
* Example0.get() // => 42
|
||||||
|
*
|
||||||
|
* Example0.get() + 1 // => 43
|
||||||
|
*/
|
||||||
|
private static get(){
|
||||||
|
return 42
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://json.schemastore.org/package",
|
"$schema": "http://json.schemastore.org/package",
|
||||||
"name": "doctest-ts",
|
"name": "doctest-ts",
|
||||||
"version": "0.5.0",
|
"version": "0.6.0",
|
||||||
"description": "doctest support for typescript",
|
"description": "doctest support for typescript",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
"build": "tsc && chmod 755 dist/src/main.js",
|
"build": "tsc && chmod 755 dist/src/main.js",
|
||||||
"test": "ts-node src/main.ts --tape src/*ts test/*ts && ts-node node_modules/.bin/tape test/*.ts src/*doctest*.ts | tap-diff",
|
"test": "ts-node src/main.ts --tape src/*ts test/*ts && ts-node node_modules/.bin/tape test/*.ts src/*doctest*.ts | tap-diff",
|
||||||
"doctest:watch": "ts-node src/main.ts --tape --watch {src,test}/*.ts | while read file; do echo tape $file; ts-node $file | tap-diff; done",
|
"doctest:watch": "ts-node src/main.ts --tape --watch {src,test}/*.ts | while read file; do echo tape $file; ts-node $file | tap-diff; done",
|
||||||
|
"debug": "ts-node src/main.ts --mocha examples/example0.ts",
|
||||||
"prettier": "rm -v -f {src,test}/*doctest.ts && prettier --list-different --write src/*ts* test/*ts*"
|
"prettier": "rm -v -f {src,test}/*doctest.ts && prettier --list-different --write src/*ts* test/*ts*"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as ts from 'typescript'
|
import * as ts from 'typescript'
|
||||||
|
import {SyntaxKind} from 'typescript'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
|
@ -30,7 +31,6 @@ export interface Comment {
|
||||||
|
|
||||||
export function Comments(s: string): Comment[] {
|
export function Comments(s: string): Comment[] {
|
||||||
const out: Comment[] = []
|
const out: Comment[] = []
|
||||||
function add_comment(c: string, context: string | null) {}
|
|
||||||
|
|
||||||
function traverse(node: ts.Node) {
|
function traverse(node: ts.Node) {
|
||||||
const jsdocs = (node as any).jsDoc || []
|
const jsdocs = (node as any).jsDoc || []
|
||||||
|
@ -50,8 +50,22 @@ export function Comments(s: string): Comment[] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jsdocs.forEach((doc: ts.JSDoc) => {
|
jsdocs.forEach((doc: ts.JSDoc) => {
|
||||||
out.push({comment: doc.comment || '', context})
|
out.push({comment: doc.comment || '', context});
|
||||||
|
|
||||||
|
// A part of the comment might be in the tags; we simply add those too and figure out later if they contain doctests
|
||||||
|
const tags = doc.tags;
|
||||||
|
if(tags !== undefined){
|
||||||
|
tags.forEach(tag => {
|
||||||
|
out.push({comment: tag.comment || '', context});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*(doc.tags || []).forEach(tag => {
|
||||||
|
console.log(tag)
|
||||||
|
})*/
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
ts.forEachChild(node, traverse)
|
ts.forEachChild(node, traverse)
|
||||||
}
|
}
|
||||||
|
@ -110,11 +124,18 @@ export function extractScript(s: string): Script {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractScripts(docstring: string): Script[] {
|
export function extractScripts(docstring: string): {script: Script, name?: string}[] {
|
||||||
const out = [] as Script[]
|
const out = [] as {script: Script, name?: string}[]
|
||||||
docstring.split(/\n\n+/m).forEach(s => {
|
docstring.split(/\n\n+/m).forEach(s => {
|
||||||
if (is_doctest(s)) {
|
if (is_doctest(s)) {
|
||||||
out.push(extractScript(s))
|
const script = extractScript(s)
|
||||||
|
let name = undefined
|
||||||
|
const match = s.match(/^[ \t]*\/\/([^\n]*)/)
|
||||||
|
if(match !== null)
|
||||||
|
{
|
||||||
|
name = match[1].trim()
|
||||||
|
}
|
||||||
|
out.push({script, name})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
|
@ -124,7 +145,7 @@ export function extractScripts(docstring: string): Script[] {
|
||||||
// Showing test scripts
|
// Showing test scripts
|
||||||
export interface ShowScript {
|
export interface ShowScript {
|
||||||
showImports: string
|
showImports: string
|
||||||
showScript(script: Script, c: Context): string
|
showScript(script: Script, c: Context, name?: string): string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** show("hello") // => '"hello"' */
|
/** show("hello") // => '"hello"' */
|
||||||
|
@ -136,7 +157,7 @@ export function showContext(c: Context) {
|
||||||
return show(c || 'doctest')
|
return show(c || 'doctest')
|
||||||
}
|
}
|
||||||
|
|
||||||
function tapeOrAVA(script: Script, c: Context, before_end = (t: string) => '') {
|
function tapeOrAVA(script: Script, c: Context, name: string | undefined, before_end = (t: string) => '') {
|
||||||
const t = `t`
|
const t = `t`
|
||||||
const body = script
|
const body = script
|
||||||
.map(s => {
|
.map(s => {
|
||||||
|
@ -155,7 +176,7 @@ function tapeOrAVA(script: Script, c: Context, before_end = (t: string) => '') {
|
||||||
})`
|
})`
|
||||||
}
|
}
|
||||||
|
|
||||||
const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c) => {
|
const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c, name) => {
|
||||||
const body = script
|
const body = script
|
||||||
.map(s => {
|
.map(s => {
|
||||||
if (s.tag == 'Statement') {
|
if (s.tag == 'Statement') {
|
||||||
|
@ -169,7 +190,7 @@ const mochaOrJest = (deepEqual: string): typeof tapeOrAVA => (script, c) => {
|
||||||
|
|
||||||
return `
|
return `
|
||||||
describe(${showContext(c)}, () => {
|
describe(${showContext(c)}, () => {
|
||||||
it(${showContext(c)}, () => {${body}})
|
it(${show(name) || showContext(c)}, () => {${body}})
|
||||||
})
|
})
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
@ -182,7 +203,7 @@ export const showScriptInstances: Record<string, ShowScript> = {
|
||||||
|
|
||||||
tape: {
|
tape: {
|
||||||
showImports: 'import * as __test from "tape"',
|
showImports: 'import * as __test from "tape"',
|
||||||
showScript: (s, c) => tapeOrAVA(s, c, t => `\n;${t}.end()`),
|
showScript: (s, c, name) => tapeOrAVA(s, c, name, t => `\n;${t}.end()`),
|
||||||
},
|
},
|
||||||
|
|
||||||
mocha: {
|
mocha: {
|
||||||
|
@ -196,12 +217,34 @@ export const showScriptInstances: Record<string, ShowScript> = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exposePrivates(s: string): string {
|
||||||
|
const ast = ts.createSourceFile('_.ts', s, ts.ScriptTarget.Latest) as ts.SourceFile
|
||||||
|
|
||||||
|
const transformer = <T extends ts.Node>(context: ts.TransformationContext) =>
|
||||||
|
(rootNode: T) => {
|
||||||
|
function visit(node: ts.Node): ts.Node {
|
||||||
|
if (node.kind === ts.SyntaxKind.PrivateKeyword) {
|
||||||
|
return ts.createModifier(ts.SyntaxKind.PublicKeyword)
|
||||||
|
}
|
||||||
|
return ts.visitEachChild(node, visit, context);
|
||||||
|
}
|
||||||
|
return ts.visitNode(rootNode, visit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformed = ts.transform(ast, [transformer]).transformed[0]
|
||||||
|
|
||||||
|
const pwoc = ts.createPrinter({removeComments: true})
|
||||||
|
return pwoc.printNode(ts.EmitHint.Unspecified, transformed, ast)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export function instrument(d: ShowScript, file: string, mode?: 'watch'): void {
|
export function instrument(d: ShowScript, file: string, mode?: 'watch'): void {
|
||||||
const {base, ext, ...u} = path.parse(file)
|
const {base, ext, ...u} = path.parse(file)
|
||||||
if (base.includes('doctest')) {
|
if (base.includes('doctest')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const buffer = fs.readFileSync(file, {encoding: 'utf8'})
|
const buffer = fs.readFileSync(file, {encoding: 'utf8'})
|
||||||
|
const withoutPrivates = exposePrivates(buffer)
|
||||||
const tests = Doctests(d, buffer)
|
const tests = Doctests(d, buffer)
|
||||||
const outfile = path.format({...u, ext: '.doctest' + ext})
|
const outfile = path.format({...u, ext: '.doctest' + ext})
|
||||||
if (tests.length == 0) {
|
if (tests.length == 0) {
|
||||||
|
@ -211,7 +254,7 @@ export function instrument(d: ShowScript, file: string, mode?: 'watch'): void {
|
||||||
if (mode == 'watch') {
|
if (mode == 'watch') {
|
||||||
console.log(outfile)
|
console.log(outfile)
|
||||||
}
|
}
|
||||||
fs.writeFileSync(outfile, buffer + '\n' + d.showImports + '\n' + tests.join('\n'))
|
fs.writeFileSync(outfile, withoutPrivates + '\n' + d.showImports + '\n' + tests.join('\n'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +262,7 @@ function Doctests(d: ShowScript, buffer: string): string[] {
|
||||||
const out: string[] = []
|
const out: string[] = []
|
||||||
for (const c of Comments(buffer)) {
|
for (const c of Comments(buffer)) {
|
||||||
for (const script of extractScripts(c.comment)) {
|
for (const script of extractScripts(c.comment)) {
|
||||||
out.push(d.showScript(script, c.context))
|
out.push(d.showScript(script.script, c.context, script.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue