Improve line number detection in individual tests

This commit is contained in:
Pieter Vander Vennet 2022-04-04 03:38:25 +02:00
parent ad4157f6a1
commit c21c4e2b0b
6 changed files with 181 additions and 82 deletions

View file

@ -1,7 +1,7 @@
{
"$schema": "http://json.schemastore.org/package",
"name": "doctest-ts-improved",
"version": "0.8.4",
"version": "0.8.5",
"description": "doctest support for typescript with Mocha",
"main": "src/main.ts",
"bin": {
@ -9,7 +9,6 @@
},
"scripts": {
"build": "tsc && chmod 755 dist/main.js",
"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",
"test": "ts-node src/main.ts examples/ && ts-node src/main.ts src/ && mocha --require ts-node/register examples/*.ts src/*.ts",
"prettier": "rm -v -f {src,test}/*doctest.ts && prettier --list-different --write src/*ts* test/*ts*",
"publish": "npm run build && npm publish"

View file

@ -25,6 +25,9 @@ export default class ExtractComments {
this.traverse(ast, {filepath: filepath})
}
/**
* Gets the actual comments
*/
public getComments(): { comment: string, context: Context }[] {
return this.results
}

80
src/ScriptExtraction.ts Normal file
View file

@ -0,0 +1,80 @@
import * as ts from "typescript";
import {Equality, Script, Statement} from "./UnitTest";
export class ScriptExtraction {
/**
* ScriptExtraction.is_doctest('// => true') // => true
* ScriptExtraction.is_doctest('// true') // => false
*/
public static is_doctest(s: string): boolean {
return s.match(/\/\/[ \t]*=>/) != null
}
/**
* Extracts the expected value
*
* const m = ScriptExtraction.doctest_rhs('// => true') || []
* m[1] // => ' true'
*/
public static doctest_rhs(s: string) {
return s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m);
}
public static extractImports(docstring: string) {
return docstring.split("\n").filter(s => s.startsWith("import "));
}
public static extractScripts(docstring: string): { script: Script, name?: string, line: number }[] {
const out = [] as { script: Script, name?: string, line: number }[]
function lineIndexOf(part: string): number {
const index = docstring.indexOf(part)
const before = docstring.slice(0, index)
return before.split(/\r\n|\r|\n/).length - 1
}
for (const s of docstring.split(/\n\n+/m)) {
if (!ScriptExtraction.is_doctest(s)) {
continue;
}
const line = lineIndexOf(s)
const script = ScriptExtraction.extractScript(s, line)
let name = undefined
const match = s.match(/^[ \t]*\/\/([^\n]*)/)
if (match !== null) {
name = match[1].trim()
}
out.push({script, name, line})
}
return out
}
/**
* ScriptExtraction.extractScript('s', 0) // => [{tag: 'Statement', stmt: 's;'}]
* ScriptExtraction.extractScript('e // => 1', 0) // => [{tag: '==', lhs: 'e', rhs: '1', line: 0}]
* ScriptExtraction.extractScript('s; e // => 1', 0) // => [{tag: 'Statement', stmt: 's;'}, {tag: '==', lhs: 'e', rhs: '1', line: 1}]
*/
private static extractScript(s: string, linestart: number): 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 = ScriptExtraction.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, line: linestart + i}
}
}
return {tag: 'Statement', stmt: pwoc.printNode(ts.EmitHint.Unspecified, stmt, ast)}
})
}
}

View file

@ -1,91 +1,19 @@
import {Context} from "./ExtractComments";
import * as ts from "typescript";
import {ScriptExtraction} from "./ScriptExtraction";
type Script = (Statement | Equality)[]
interface Equality {
export type Script = (Statement | Equality)[]
export interface Equality {
tag: '=='
lhs: string
rhs: string
rhs: string,
line: number
}
interface Statement {
export interface Statement {
tag: 'Statement'
stmt: string
}
class ScriptExtraction {
/**
* ScriptExtraction.is_doctest('// => true') // => true
* ScriptExtraction.is_doctest('// true') // => false
*/
public static is_doctest(s: string): boolean {
return s.match(/\/\/[ \t]*=>/) != null
}
/**
* Extracts the expected value
*
* const m = ScriptExtraction.doctest_rhs('// => true') || []
* m[1] // => ' true'
*/
public static doctest_rhs(s: string) {
return s.match(/^\s*\/\/[ \t]*=>([^\n]*)/m);
}
public static extractImports(docstring: string){
return docstring.split("\n").filter(s => s.startsWith("import "));
}
public static extractScripts(docstring: string): { script: Script, name?: string, line: number }[] {
const out = [] as { script: Script, name?: string, line: number }[]
let line = 0;
for (const s of docstring.split(/\n\n+/m)) {
const p: number = line;
line += s.split(/\r\n|\r|\n/).length
if (!ScriptExtraction.is_doctest(s)) {
continue;
}
const script = ScriptExtraction.extractScript(s)
let name = undefined
const match = s.match(/^[ \t]*\/\/([^\n]*)/)
if (match !== null) {
name = match[1].trim()
}
out.push({script, name, line: p})
}
return out
}
/**
* ScriptExtraction.extractScript('s') // => [{tag: 'Statement', stmt: 's;'}]
* ScriptExtraction.extractScript('e // => 1') // => [{tag: '==', lhs: 'e', rhs: '1'}]
* ScriptExtraction.extractScript('s; e // => 1') // => [{tag: 'Statement', stmt: 's;'}, {tag: '==', lhs: 'e', rhs: '1'}]
*/
private static 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 = ScriptExtraction.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)}
})
}
}
/**
* Represents a single unit test somewhere in a file.
*/
@ -134,7 +62,7 @@ export default class UnitTest {
if (s.tag == 'Statement') {
return s.stmt
} else {
return `__expect(${s.lhs}, "failed at ${this.context.functionname} (${this.context.filepath}:${this.context.linenumber}:1)").to.deep.equal(${s.rhs})`
return `__expect(${s.lhs}, "failed at ${this.context.functionname} (${this.context.filepath}:${s.line}:1)").to.deep.equal(${s.rhs})`
}
})
.map(x => '\n ' + x)

View file

@ -50,7 +50,9 @@ function main() {
}
}
if (noTests.length > 0) {
console.log(`No tests found in ${noTests.length} files: ${noTests.join(", ")}`)
const i = Math.round(Math.random() * noTests.length)
const randomFile = noTests[i]
console.log(`No tests found in ${noTests.length} files. Why not add a test to ${randomFile}?`)
}
}

View file

@ -0,0 +1,87 @@
import {describe} from 'mocha'
import {expect} from 'chai'
import ExtractComments from "../src/ExtractComments";
import {ScriptExtraction} from "../src/ScriptExtraction";
describe("ExtractComments", () => {
describe("getComments", () => {
it("should return correct line numbers", () => {
const code = `
class A {
/**
* some actual comment about x()
*
* A.x() // => 42
*
* A.x() + 1 // => 43
* A.x() - 1 // => 41
*
* // should equal 2 * 21
* A.x() // => 21 * 2
*/
static x() {
return 42
}
}
`
const comments = new ExtractComments("testfile.ts", code).getComments()
expect(comments.length).to.eq(1)
expect(comments[0].context).to.deep.eq(
{
filepath: "testfile.ts",
classname: "A",
functionname: "x",
linenumber: 3
}
)
})
})
})
describe("ScriptExtraction", () => {
describe("extractScripts", () => {
it("Should have correct line numbers", () => {
const comment = `some actual comment about x()
A.x() // => 42
A.x() + 1 // => 43
A.x() - 1 // => 41
// should equal 2 * 21
A.x() // => 21 * 2`
const scripts = ScriptExtraction.extractScripts(comment)
expect(scripts.length).eq(3)
const [doctest0, doctest1, doctest2shouldEqual] = scripts;
expect(doctest0).to.deep.eq({
script: [{tag: '==', lhs: 'A.x()', rhs: "42", line: 2}],
name: undefined,
line: 2
})
expect(doctest1).to.deep.eq({
script: [{tag: '==', lhs: 'A.x() + 1', rhs: "43", line: 4}, {
tag: '==',
lhs: 'A.x() - 1',
rhs: "41",
line: 5
}],
name: undefined,
line: 4
})
expect(doctest2shouldEqual).to.deep.eq({
script: [{tag: '==', lhs: 'A.x()', rhs: "21 * 2", line: 7}],
name: "should equal 2 * 21",
line: 7
})
})
})
})