Skip to content

Commit

Permalink
fix(reporter): use default error formatter for JUnit (#5629)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa committed Apr 29, 2024
1 parent eeaebff commit 200609c
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 390 deletions.
23 changes: 22 additions & 1 deletion packages/vitest/src/node/error.ts
@@ -1,5 +1,6 @@
/* eslint-disable prefer-template */
import { existsSync, readFileSync } from 'node:fs'
import { Writable } from 'node:stream'
import { normalize, relative } from 'pathe'
import c from 'picocolors'
import cliTruncate from 'cli-truncate'
Expand All @@ -13,7 +14,7 @@ import { TypeCheckError } from '../typecheck/typechecker'
import { isPrimitive } from '../utils'
import type { Vitest } from './core'
import { divider } from './reporters/renderers/utils'
import type { Logger } from './logger'
import { Logger } from './logger'
import type { WorkspaceProject } from './workspace'

interface PrintErrorOptions {
Expand All @@ -27,6 +28,26 @@ interface PrintErrorResult {
nearest?: ParsedStack
}

// use Logger with custom Console to capture entire error printing
export async function captuerPrintError(
error: unknown,
ctx: Vitest,
project: WorkspaceProject,
) {
let output = ''
const writable = new Writable({
write(chunk, _encoding, callback) {
output += String(chunk)
callback()
},
})
const result = await printError(error, project, {
showCodeFrame: false,
logger: new Logger(ctx, writable, writable),
})
return { nearest: result?.nearest, output }
}

export async function printError(error: unknown, project: WorkspaceProject | undefined, options: PrintErrorOptions): Promise<PrintErrorResult | undefined> {
const { showCodeFrame = true, fullStack = false, type } = options
const logger = options.logger
Expand Down
23 changes: 2 additions & 21 deletions packages/vitest/src/node/reporters/github-actions.ts
@@ -1,10 +1,8 @@
import { Writable } from 'node:stream'
import { getTasks } from '@vitest/runner/utils'
import stripAnsi from 'strip-ansi'
import type { File, Reporter, Vitest } from '../../types'
import { getFullName } from '../../utils'
import { printError } from '../error'
import { Logger } from '../logger'
import { captuerPrintError } from '../error'
import type { WorkspaceProject } from '../workspace'

export class GithubActionsReporter implements Reporter {
Expand Down Expand Up @@ -44,7 +42,7 @@ export class GithubActionsReporter implements Reporter {

// format errors via `printError`
for (const { project, title, error } of projectErrors) {
const result = await printErrorWrapper(error, this.ctx, project)
const result = await captuerPrintError(error, this.ctx, project)
const stack = result?.nearest
if (!stack)
continue
Expand All @@ -63,23 +61,6 @@ export class GithubActionsReporter implements Reporter {
}
}

// use Logger with custom Console to extract messgage from `processError` util
// TODO: maybe refactor `processError` to require single function `(message: string) => void` instead of full Logger?
async function printErrorWrapper(error: unknown, ctx: Vitest, project: WorkspaceProject) {
let output = ''
const writable = new Writable({
write(chunk, _encoding, callback) {
output += String(chunk)
callback()
},
})
const result = await printError(error, project, {
showCodeFrame: false,
logger: new Logger(ctx, writable, writable),
})
return { nearest: result?.nearest, output }
}

// workflow command formatting based on
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-error-message
// https://github.com/actions/toolkit/blob/f1d9b4b985e6f0f728b4b766db73498403fd5ca3/packages/core/src/command.ts#L80-L85
Expand Down
37 changes: 8 additions & 29 deletions packages/vitest/src/node/reporters/junit.ts
Expand Up @@ -3,13 +3,12 @@ import { hostname } from 'node:os'
import { dirname, relative, resolve } from 'pathe'

import type { Task } from '@vitest/runner'
import type { ErrorWithDiff } from '@vitest/utils'
import { getSuites } from '@vitest/runner/utils'
import stripAnsi from 'strip-ansi'
import type { Vitest } from '../../node'
import type { Reporter } from '../../types/reporter'
import { parseErrorStacktrace } from '../../utils/source-map'
import { F_POINTER } from '../../utils/figures'
import { getOutputFile } from '../../utils/config-helpers'
import { captuerPrintError } from '../error'
import { IndentedLogger } from './renderers/indented-logger'

export interface JUnitOptions {
Expand Down Expand Up @@ -140,31 +139,6 @@ export class JUnitReporter implements Reporter {
await this.logger.log(`</${name}>`)
}

async writeErrorDetails(task: Task, error: ErrorWithDiff): Promise<void> {
const errorName = error.name ?? error.nameStr ?? 'Unknown Error'
const errorDetails = `${errorName}: ${error.message}`

// Be sure to escape any XML in the error Details
await this.baseLog(escapeXML(errorDetails))

const project = this.ctx.getProjectByTaskId(task.id)
const stack = parseErrorStacktrace(error, {
getSourceMap: file => project.getBrowserSourceMapModuleById(file),
frameFilter: this.ctx.config.onStackTrace,
})

// TODO: This is same as printStack but without colors. Find a way to reuse code.
for (const frame of stack) {
const path = relative(this.ctx.config.root, frame.file)

await this.baseLog(escapeXML(` ${F_POINTER} ${[frame.method, `${path}:${frame.line}:${frame.column}`].filter(Boolean).join(' ')}`))

// reached at test file, skip the follow stack
if (frame.file in this.ctx.state.filesMap)
break
}
}

async writeLogs(task: Task, type: 'err' | 'out'): Promise<void> {
if (task.logs == null || task.logs.length === 0)
return
Expand Down Expand Up @@ -205,7 +179,12 @@ export class JUnitReporter implements Reporter {
if (!error)
return

await this.writeErrorDetails(task, error)
const result = await captuerPrintError(
error,
this.ctx,
this.ctx.getProjectByTaskId(task.id),
)
await this.baseLog(escapeXML(stripAnsi(result.output.trim())))
})
}
}
Expand Down
49 changes: 49 additions & 0 deletions test/reporters/fixtures/error.test.ts
@@ -0,0 +1,49 @@
import { afterAll, it, expect } from "vitest";

afterAll(() => {
throwSuite()
})

it('stack', () => {
throwDeep()
})

it('diff', () => {
expect({ hello: 'x' }).toEqual({ hello: 'y' })
})

it('unhandled', () => {
(async () => throwSimple())()
})

it('no name object', () => {
throw { noName: 'hi' };
});

it('string', () => {
throw "hi";
});

it('number', () => {
throw 1234;
});

it('number name object', () => {
throw { name: 1234 };
});

it('xml', () => {
throw new Error('error message that has XML in it <div><input/></div>');
})

function throwDeep() {
throwSimple()
}

function throwSimple() {
throw new Error('throwSimple')
}

function throwSuite() {
throw new Error('throwSuite')
}
63 changes: 0 additions & 63 deletions test/reporters/src/data-for-junit.ts

This file was deleted.

80 changes: 80 additions & 0 deletions test/reporters/tests/__snapshots__/junit.test.ts.snap
Expand Up @@ -34,3 +34,83 @@ Error: afterAll error
</testsuites>
"
`;
exports[`format error 1`] = `
"<?xml version="1.0" encoding="UTF-8" ?>
<testsuites name="vitest tests" tests="9" failures="8" errors="0" time="...">
<testsuite name="error.test.ts" timestamp="..." hostname="..." tests="9" failures="8" errors="0" skipped="0" time="...">
<testcase classname="error.test.ts" name="stack" time="...">
<failure message="throwSimple" type="Error">
Error: throwSimple
❯ throwSimple error.test.ts:44:9
❯ throwDeep error.test.ts:40:3
❯ error.test.ts:8:3
</failure>
</testcase>
<testcase classname="error.test.ts" name="diff" time="...">
<failure message="expected { hello: &apos;x&apos; } to deeply equal { hello: &apos;y&apos; }" type="AssertionError">
AssertionError: expected { hello: &apos;x&apos; } to deeply equal { hello: &apos;y&apos; }
- Expected
+ Received
Object {
- &quot;hello&quot;: &quot;y&quot;,
+ &quot;hello&quot;: &quot;x&quot;,
}
❯ error.test.ts:12:26
</failure>
</testcase>
<testcase classname="error.test.ts" name="unhandled" time="...">
</testcase>
<testcase classname="error.test.ts" name="no name object" time="...">
<failure>
{
noName: &apos;hi&apos;,
expected: &apos;undefined&apos;,
actual: &apos;undefined&apos;,
stacks: []
}
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Serialized Error: { noName: &apos;hi&apos; }
</failure>
</testcase>
<testcase classname="error.test.ts" name="string" time="...">
<failure message="hi">
Unknown Error: hi
</failure>
</testcase>
<testcase classname="error.test.ts" name="number" time="...">
<failure message="1234">
Unknown Error: 1234
</failure>
</testcase>
<testcase classname="error.test.ts" name="number name object" time="...">
<failure type="1234">
{
name: 1234,
nameStr: &apos;1234&apos;,
expected: &apos;undefined&apos;,
actual: &apos;undefined&apos;,
stacks: []
}
</failure>
</testcase>
<testcase classname="error.test.ts" name="xml" time="...">
<failure message="error message that has XML in it &lt;div&gt;&lt;input/&gt;&lt;/div&gt;" type="Error">
Error: error message that has XML in it &lt;div&gt;&lt;input/&gt;&lt;/div&gt;
❯ error.test.ts:36:9
</failure>
</testcase>
<testcase classname="error.test.ts" name="error.test.ts" time="...">
<failure message="throwSuite" type="Error">
Error: throwSuite
❯ throwSuite error.test.ts:48:9
❯ error.test.ts:4:3
</failure>
</testcase>
</testsuite>
</testsuites>
"
`;

0 comments on commit 200609c

Please sign in to comment.