Improve line number detection in individual tests
This commit is contained in:
parent
ad4157f6a1
commit
c21c4e2b0b
6 changed files with 181 additions and 82 deletions
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"$schema": "http://json.schemastore.org/package",
|
"$schema": "http://json.schemastore.org/package",
|
||||||
"name": "doctest-ts-improved",
|
"name": "doctest-ts-improved",
|
||||||
"version": "0.8.4",
|
"version": "0.8.5",
|
||||||
"description": "doctest support for typescript with Mocha",
|
"description": "doctest support for typescript with Mocha",
|
||||||
"main": "src/main.ts",
|
"main": "src/main.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
@ -9,7 +9,6 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && chmod 755 dist/main.js",
|
"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",
|
"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*",
|
"prettier": "rm -v -f {src,test}/*doctest.ts && prettier --list-different --write src/*ts* test/*ts*",
|
||||||
"publish": "npm run build && npm publish"
|
"publish": "npm run build && npm publish"
|
||||||
|
|
|
@ -25,6 +25,9 @@ export default class ExtractComments {
|
||||||
this.traverse(ast, {filepath: filepath})
|
this.traverse(ast, {filepath: filepath})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the actual comments
|
||||||
|
*/
|
||||||
public getComments(): { comment: string, context: Context }[] {
|
public getComments(): { comment: string, context: Context }[] {
|
||||||
return this.results
|
return this.results
|
||||||
}
|
}
|
||||||
|
|
80
src/ScriptExtraction.ts
Normal file
80
src/ScriptExtraction.ts
Normal 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)}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,91 +1,19 @@
|
||||||
import {Context} from "./ExtractComments";
|
import {Context} from "./ExtractComments";
|
||||||
import * as ts from "typescript";
|
import {ScriptExtraction} from "./ScriptExtraction";
|
||||||
|
|
||||||
type Script = (Statement | Equality)[]
|
export type Script = (Statement | Equality)[]
|
||||||
interface Equality {
|
export interface Equality {
|
||||||
tag: '=='
|
tag: '=='
|
||||||
lhs: string
|
lhs: string
|
||||||
rhs: string
|
rhs: string,
|
||||||
|
line: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Statement {
|
export interface Statement {
|
||||||
tag: 'Statement'
|
tag: 'Statement'
|
||||||
stmt: string
|
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.
|
* Represents a single unit test somewhere in a file.
|
||||||
*/
|
*/
|
||||||
|
@ -134,7 +62,7 @@ export default class UnitTest {
|
||||||
if (s.tag == 'Statement') {
|
if (s.tag == 'Statement') {
|
||||||
return s.stmt
|
return s.stmt
|
||||||
} else {
|
} 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)
|
.map(x => '\n ' + x)
|
||||||
|
|
|
@ -50,7 +50,9 @@ function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (noTests.length > 0) {
|
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}?`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
87
tests/ExtractScript.spec.ts
Normal file
87
tests/ExtractScript.spec.ts
Normal 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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Add table
Add a link
Reference in a new issue