Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: align json output with jest's #1034

Merged
merged 7 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
133 changes: 102 additions & 31 deletions packages/vitest/src/node/reporters/json.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,62 @@
import { existsSync, promises as fs } from 'fs'
import { dirname, resolve } from 'pathe'
import type { Vitest } from '../../node'
import type { File, Reporter } from '../../types'
import type { File, Reporter, Suite, TaskState, Test } from '../../types'
import { getSuites, getTests } from '../../utils'

// for compatibility reasons, the reporter produces a JSON similar to the one produced by the Jest JSON reporter
// the following types are extracted from the Jest repository (and simplified)
// the commented-out fields are the missing ones

type Status = 'passed' | 'failed' | 'skipped' | 'pending' | 'todo' | 'disabled'
type Milliseconds = number
interface TestResult {
displayName?: string
failureMessage?: string | null
skipped: boolean
status?: string
testFilePath?: string
perfStats: {
end?: Milliseconds
runtime?: Milliseconds
start?: Milliseconds
}
const StatusMap: Record<TaskState, Status> = {
fail: 'failed',
only: 'pending',
pass: 'passed',
run: 'pending',
skip: 'skipped',
todo: 'todo',
}

interface FormattedAssertionResult {
ancestorTitles: Array<string>
fullName: string
status: Status
title: string
duration?: Milliseconds | null
failureMessages: Array<string>
// location?: Callsite | null
}

interface FormattedTestResult {
message: string
name: string
status: 'failed' | 'passed'
startTime: number
endTime: number
assertionResults: Array<FormattedAssertionResult>
// summary: string
// coverage: unknown
}

interface AggregatedResult {
interface FormattedTestResults {
numFailedTests: number
numFailedTestSuites: number
numPassedTests: number
numPassedTestSuites: number
numPendingTests: number
numTodoTests: number
numPendingTestSuites: number
numTodoTests: number
numTotalTests: number
numTotalTestSuites: number
startTime: number
success: boolean
testResults: Array<TestResult>
testResults: Array<FormattedTestResult>
// coverageMap?: CoverageMap | null | undefined
// numRuntimeErrorTestSuites: number
// snapshot: SnapshotSummary
// wasInterrupted: boolean
}

export class JsonReporter implements Reporter {
Expand All @@ -47,33 +71,80 @@ export class JsonReporter implements Reporter {
protected async logTasks(files: File[]) {
const suites = getSuites(files)
const numTotalTestSuites = suites.length
const tests = getTests(files)
const numTotalTests = tests.length
const fileToTestCases = new Map<File, Test[]>()

for (const file of files) {
const relatedTests = getTests([file])
fileToTestCases.set(file, relatedTests)
}

const tests = [...fileToTestCases.values()].flat()
zxch3n marked this conversation as resolved.
Show resolved Hide resolved
const numTotalTests = tests.length
const numFailedTestSuites = suites.filter(s => s.result?.error).length
const numPassedTestSuites = numTotalTestSuites - numFailedTestSuites
const numPendingTestSuites = suites.filter(s => s.result?.state === 'run').length
const numFailedTests = tests.filter(t => t.result?.state === 'fail').length
const numPassedTests = numTotalTests - numFailedTests
const numPendingTests = tests.filter(t => t.result?.state === 'run').length
const numTodoTests = tests.filter(t => t.mode === 'todo').length
const testResults: Array<FormattedTestResult> = []

const success = numFailedTestSuites === 0 && numFailedTests === 0

const testResults: Array<TestResult> = tests.map(t => ({
perfStats: {
runtime: t.result?.duration,
start: t.result?.startTime,
end: t.result?.duration && t.result?.startTime && t.result.duration + t.result.startTime,
},
displayName: t.name,
failureMessage: t.result?.error?.message,
skipped: t.mode === 'skip',
status: t.result?.state,
testFilePath: t.file?.filepath,
}))

const result: AggregatedResult = { numTotalTestSuites, numPassedTestSuites, numFailedTestSuites, numPendingTestSuites, numTotalTests, numPassedTests, numFailedTests, numPendingTests, numTodoTests, startTime: this.start, success, testResults }
for (const [file, tests] of fileToTestCases) {
let startTime = tests.reduce((prev, next) => Math.min(prev, next.result?.startTime ?? Infinity), Infinity)
if (startTime === Infinity)
startTime = this.start

const endTime = tests.reduce((prev, next) => Math.max(prev, (next.result?.startTime ?? 0) + (next.result?.duration ?? 0)), startTime)
const assertionResults = tests.map((t) => {
const ancestorTitles = [] as string[]
let iter: Suite | undefined = t.suite
while (iter) {
ancestorTitles.push(iter.name)
iter = iter.suite
}
ancestorTitles.reverse()

return {
ancestorTitles,
fullName: ancestorTitles.length > 0 ? `${ancestorTitles.join(' ')} ${t.name}` : t.name,
status: t.result != null ? StatusMap[t.result.state] : 'skipped',
title: t.name,
duration: t.result?.duration,
failureMessages: t.result?.error?.message == null ? [] : [t.result.error.message],
} as FormattedAssertionResult
})

testResults.push({
assertionResults,
startTime,
endTime,
status: tests.every(t =>
t.result?.state === 'pass'
|| t.result?.state === 'skip'
|| t.result?.state === 'todo')
? 'passed'
: 'failed',
message: file.result?.error?.message ?? '',
name: file.filepath,
})
}

const result: FormattedTestResults = {
numTotalTestSuites,
numPassedTestSuites,
numFailedTestSuites,
numPendingTestSuites,
numTotalTests,
numPassedTests,
numFailedTests,
numPendingTests,
numTodoTests,
startTime: this.start,
success,
testResults,
}

await this.writeReport(JSON.stringify(result, null, 2))
}
Expand Down