diff --git a/deps.ts b/deps.ts index a44f82a..9540ceb 100644 --- a/deps.ts +++ b/deps.ts @@ -33,11 +33,13 @@ export { export { blue, + brightBlack, gray, green, red, yellow, } from "https://deno.land/std@0.116.0/fmt/colors.ts"; +export { fromFileUrl } from "https://deno.land/std@0.116.0/path/mod.ts"; // third party diff --git a/report/README.md b/report/README.md new file mode 100644 index 0000000..afcaef7 --- /dev/null +++ b/report/README.md @@ -0,0 +1,3 @@ +# report + +assert result reporter diff --git a/report/assertion.ts b/report/assertion.ts new file mode 100644 index 0000000..259b316 --- /dev/null +++ b/report/assertion.ts @@ -0,0 +1,35 @@ +// Copyright 2021-Present the Unitest authors. All rights reserved. MIT license. +import { brightBlack, red } from "../deps.ts"; + +type CodeInfo = { + code: string; + lineNumber: number; + column: number; +}; + +// /** insert something to beginning of line */ +// function BOL(text: string, symbol: string): string { +// return `${symbol}${text.replaceAll("\n", `\n${symbol}`)}`; +// } + +function prettyCode({ code, lineNumber }: CodeInfo): string { + const codes = code.split("\n"); + + const sliced = codes.slice( + lineNumber - 2, + lineNumber + 2, + ); + + const numberedCodes = sliced.map((fragment, i) => { + const number = i + lineNumber - 1; + const isWrongLine = number === lineNumber; + + return `${isWrongLine ? red(">") : " "} ${brightBlack(String(number))} ${ + brightBlack("|") + } ${isWrongLine ? fragment : brightBlack(fragment)}`; + }); + + return numberedCodes.join("\n"); +} + +export { prettyCode }; diff --git a/report/stack_trace.ts b/report/stack_trace.ts new file mode 100644 index 0000000..77cea2c --- /dev/null +++ b/report/stack_trace.ts @@ -0,0 +1,33 @@ +// Copyright 2021-Present the Unitest authors. All rights reserved. MIT license. +type StackTrace = { + file: string; + methodName: string; + lineNumber: number; + column: number; +}; + +function parseStackTraceLine(line: string): StackTrace | null { + const re = + /^\s*at (?\S+) \(?(?\S+):(?\d+):(?\d+)\)?.*$/i; + + const result = re.exec(line); + if (!result || !result.groups) return null; + const { file, methodName, lineNumber, column } = result.groups; + + return { + file, + methodName, + lineNumber: Number(lineNumber), + column: Number(column), + }; +} + +function parseStackTrace(stack: string): StackTrace[] { + const lines = stack.split("\n"); + + const traces = lines.map((line) => parseStackTraceLine(line)); + + return traces.filter(Boolean) as StackTrace[]; +} + +export { parseStackTrace, parseStackTraceLine }; diff --git a/report/stack_trace_test.ts b/report/stack_trace_test.ts new file mode 100644 index 0000000..78158ac --- /dev/null +++ b/report/stack_trace_test.ts @@ -0,0 +1,86 @@ +// Copyright 2021-Present the Unitest authors. All rights reserved. MIT license. +import { assertEquals } from "../dev_deps.ts"; +import { parseStackTrace, parseStackTraceLine } from "./stack_trace.ts"; + +Deno.test({ + name: "parseStackTraceLine", + fn: () => { + const table: [ + ...Parameters, + ReturnType, + ][] = [ + [ + ` at main (file:///path/to/file.ts:23:11)`, + { + file: "file:///path/to/file.ts", + methodName: "main", + lineNumber: 23, + column: 11, + }, + ], + [ + ` at assertEquals (https://deno.land/std@0.115.1/testing/asserts.ts:266:9)`, + { + file: "https://deno.land/std@0.115.1/testing/asserts.ts", + methodName: "assertEquals", + lineNumber: 266, + column: 9, + }, + ], + + [ + ` at assertEquals https://deno.land/std@0.115.1/testing/asserts.ts:266:9`, + { + file: "https://deno.land/std@0.115.1/testing/asserts.ts", + methodName: "assertEquals", + lineNumber: 266, + column: 9, + }, + ], + + [ + ` at Array.forEach ()`, + null, + ], + ]; + + table.forEach(([value, result]) => { + assertEquals(parseStackTraceLine(value), result); + }); + }, +}); + +Deno.test({ + name: "trace", + fn: () => { + const table: [ + ...Parameters, + ReturnType, + ][] = [ + [ + `ReferenceError: FAIL is not defined + at Constraint.execute (deltablue.js:525:2) + at Constraint.execute anonymous + at Constraint.recalculate (deltablue.js:424:21)`, + [ + { + file: "deltablue.js", + methodName: "Constraint.execute", + lineNumber: 525, + column: 2, + }, + { + file: "deltablue.js", + methodName: "Constraint.recalculate", + lineNumber: 424, + column: 21, + }, + ], + ], + ]; + + table.forEach(([value, result]) => { + assertEquals(parseStackTrace(value), result); + }); + }, +}); diff --git a/test/mod.ts b/test/mod.ts index ce93afc..21e41c9 100644 --- a/test/mod.ts +++ b/test/mod.ts @@ -1,7 +1,9 @@ // Copyright 2021-Present the Unitest authors. All rights reserved. MIT license. import { AnyFn } from "../_types.ts"; -import { isString } from "../deps.ts"; +import { AssertionError, fromFileUrl, isString } from "../deps.ts"; import { each } from "./each.ts"; +import { parseStackTrace } from "../report/stack_trace.ts"; +import { prettyCode } from "../report/assertion.ts"; type Test> = { setup?: () => Context | void; @@ -41,6 +43,37 @@ function _test>( try { await fn({ ...context, ...denoContext }); + } catch (e) { + if (e instanceof Error) { + if (e.stack) { + const { state } = await Deno.permissions.query({ + name: "read", + }); + + if (state !== "granted") { + throw e; + } + + const stackTraces = parseStackTrace(e.stack); + const testFileStack = stackTraces.find(({ file }) => { + return /file:\/\/\S+_test\.[j|t]sx?$/.test(file); + }); + + if (!testFileStack) { + throw e; + } + + const { lineNumber, column, file } = testFileStack; + const code = await Deno.readTextFile( + fromFileUrl(file), + ); + + const pretty = prettyCode({ code, lineNumber, column }); + throw new AssertionError(`${e.message}\n${pretty}\n`); + } + } else { + throw Error("panic: unexpected error"); + } } finally { teardown?.(context); }