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",
|
||||
"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"
|
||||
|
|
|
@ -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
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 * 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)
|
||||
|
|
|
@ -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}?`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
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
Reference in a new issue