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: prevent crashing when directory of outputFile does not exist #986

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 7 additions & 2 deletions packages/vitest/src/node/reporters/json.ts
@@ -1,5 +1,5 @@
import { promises as fs } from 'fs'
import { resolve } from 'pathe'
import { existsSync, promises as fs } from 'fs'
import { dirname, resolve } from 'pathe'
import type { Vitest } from '../../node'
import type { File, Reporter } from '../../types'
import { getSuites, getTests } from '../../utils'
Expand Down Expand Up @@ -90,6 +90,11 @@ export class JsonReporter implements Reporter {
async writeReport(report: string) {
if (this.ctx.config.outputFile) {
const reportFile = resolve(this.ctx.config.root, this.ctx.config.outputFile)

const outputDirectory = dirname(reportFile)
if (!existsSync(outputDirectory))
await fs.mkdir(outputDirectory, { recursive: true })

await fs.writeFile(reportFile, report, 'utf-8')
this.ctx.log(`JSON report written to ${reportFile}`)
}
Expand Down
9 changes: 7 additions & 2 deletions packages/vitest/src/node/reporters/junit.ts
@@ -1,6 +1,6 @@
import { promises as fs } from 'fs'
import { existsSync, promises as fs } from 'fs'
import { hostname } from 'os'
import { relative, resolve } from 'pathe'
import { dirname, relative, resolve } from 'pathe'

import type { Vitest } from '../../node'
import type { ErrorWithDiff, Reporter, Task } from '../../types'
Expand Down Expand Up @@ -46,6 +46,11 @@ export class JUnitReporter implements Reporter {

if (this.ctx.config.outputFile) {
this.reportFile = resolve(this.ctx.config.root, this.ctx.config.outputFile)

const outputDirectory = dirname(this.reportFile)
if (!existsSync(outputDirectory))
await fs.mkdir(outputDirectory, { recursive: true })

const fileFd = await fs.open(this.reportFile, 'w+')

this.baseLog = async(text: string) => await fs.writeFile(fileFd, `${text}\n`)
Expand Down
128 changes: 128 additions & 0 deletions test/reporters/tests/__snapshots__/reporters.spec.ts.snap
Expand Up @@ -65,6 +65,41 @@ AssertionError: expected 2.23606797749979 to equal 2
"
`;

exports[`JUnit reporter with outputFile in non-existing directory 1`] = `
"JUNIT report written to <process-cwd>/junitReportDirectory/deeply/nested/report.xml
"
`;

exports[`JUnit reporter with outputFile in non-existing directory 2`] = `
"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\" ?>
<testsuites>
<testsuite name=\\"test/core/test/basic.test.ts\\" timestamp=\\"2022-01-19T10:10:01.759Z\\" hostname=\\"hostname\\" tests=\\"8\\" failures=\\"1\\" errors=\\"0\\" skipped=\\"1\\" time=\\"0.1459928420\\">
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; inner suite &gt; Math.sqrt()\\" time=\\"0.0014422860\\">
<failure message=\\"expected 2.23606797749979 to equal 2\\" type=\\"AssertionError\\">
AssertionError: expected 2.23606797749979 to equal 2
❯ vitest/test/core/test/basic.test.ts:8:32
</failure>
</testcase>
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; JSON\\" time=\\"0.0010237110\\">
</testcase>
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; async with timeout\\">
<skipped/>
</testcase>
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; timeout\\" time=\\"0.1005059841\\">
</testcase>
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; callback setup success \\" time=\\"0.0201848750\\">
</testcase>
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; callback test success \\" time=\\"0.0003324542\\">
</testcase>
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; callback setup success done(false)\\" time=\\"0.0197386060\\">
</testcase>
<testcase classname=\\"test/core/test/basic.test.ts\\" name=\\"suite &gt; callback test success done(false)\\" time=\\"0.0001923509\\">
</testcase>
</testsuite>
</testsuites>
"
`;

exports[`json reporter 1`] = `
{
"numFailedTestSuites": 0,
Expand Down Expand Up @@ -246,6 +281,99 @@ exports[`json reporter with outputFile 2`] = `
}"
`;

exports[`json reporter with outputFile in non-existing directory 1`] = `
"JSON report written to <process-cwd>/jsonReportDirectory/deeply/nested/report.json
"
`;

exports[`json reporter with outputFile in non-existing directory 2`] = `
"{
\\"numTotalTestSuites\\": 3,
\\"numPassedTestSuites\\": 3,
\\"numFailedTestSuites\\": 0,
\\"numPendingTestSuites\\": 0,
\\"numTotalTests\\": 8,
\\"numPassedTests\\": 7,
\\"numFailedTests\\": 1,
\\"numPendingTests\\": 0,
\\"numTodoTests\\": 0,
\\"startTime\\": 1642587001759,
\\"success\\": false,
\\"testResults\\": [
{
\\"perfStats\\": {
\\"runtime\\": 1.4422860145568848
},
\\"displayName\\": \\"Math.sqrt()\\",
\\"failureMessage\\": \\"expected 2.23606797749979 to equal 2\\",
\\"skipped\\": false,
\\"status\\": \\"fail\\",
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
},
{
\\"perfStats\\": {
\\"runtime\\": 1.0237109661102295
},
\\"displayName\\": \\"JSON\\",
\\"skipped\\": false,
\\"status\\": \\"pass\\",
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
},
{
\\"perfStats\\": {},
\\"displayName\\": \\"async with timeout\\",
\\"skipped\\": true,
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
},
{
\\"perfStats\\": {
\\"runtime\\": 100.50598406791687
},
\\"displayName\\": \\"timeout\\",
\\"skipped\\": false,
\\"status\\": \\"pass\\",
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
},
{
\\"perfStats\\": {
\\"runtime\\": 20.184875011444092
},
\\"displayName\\": \\"callback setup success \\",
\\"skipped\\": false,
\\"status\\": \\"pass\\",
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
},
{
\\"perfStats\\": {
\\"runtime\\": 0.33245420455932617
},
\\"displayName\\": \\"callback test success \\",
\\"skipped\\": false,
\\"status\\": \\"pass\\",
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
},
{
\\"perfStats\\": {
\\"runtime\\": 19.738605976104736
},
\\"displayName\\": \\"callback setup success done(false)\\",
\\"skipped\\": false,
\\"status\\": \\"pass\\",
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
},
{
\\"perfStats\\": {
\\"runtime\\": 0.1923508644104004
},
\\"displayName\\": \\"callback test success done(false)\\",
\\"skipped\\": false,
\\"status\\": \\"pass\\",
\\"testFilePath\\": \\"/vitest/test/core/test/basic.test.ts\\"
}
]
}"
`;

exports[`tap reporter 1`] = `
"TAP version 13
1..1
Expand Down
65 changes: 60 additions & 5 deletions test/reporters/tests/reporters.spec.ts
@@ -1,4 +1,4 @@
import { existsSync, readFileSync, rmSync } from 'fs'
import { existsSync, readFileSync, rmSync, rmdirSync } from 'fs'
import { afterEach, expect, test, vi } from 'vitest'
import { normalize, resolve } from 'pathe'
import { JsonReporter } from '../../../packages/vitest/src/node/reporters/json'
Expand Down Expand Up @@ -75,15 +75,41 @@ test('JUnit reporter with outputFile', async() => {
await reporter.onFinished(files)

// Assert
const output = context.output.replace(normalize(process.cwd()), '<process-cwd>')
expect(output).toMatchSnapshot()
expect(normalizeCwd(context.output)).toMatchSnapshot()
expect(existsSync(outputFile)).toBe(true)
expect(readFileSync(outputFile, 'utf8')).toMatchSnapshot()

// Cleanup
rmSync(outputFile)
})

test('JUnit reporter with outputFile in non-existing directory', async() => {
// Arrange
const reporter = new JUnitReporter()
const rootDirectory = resolve('junitReportDirectory')
const outputFile = `${rootDirectory}/deeply/nested/report.xml`
const context = getContext()
context.vitest.config.outputFile = outputFile

vi.mock('os', () => ({
hostname: () => 'hostname',
}))

vi.setSystemTime(1642587001759)

// Act
await reporter.onInit(context.vitest)
await reporter.onFinished(files)

// Assert
expect(normalizeCwd(context.output)).toMatchSnapshot()
expect(existsSync(outputFile)).toBe(true)
expect(readFileSync(outputFile, 'utf8')).toMatchSnapshot()

// Cleanup
rmdirSync(rootDirectory, { recursive: true })
})

test('json reporter', async() => {
// Arrange
const reporter = new JsonReporter()
Expand Down Expand Up @@ -113,11 +139,40 @@ test('json reporter with outputFile', async() => {
await reporter.onFinished(files)

// Assert
const output = context.output.replace(normalize(process.cwd()), '<process-cwd>')
expect(output).toMatchSnapshot()
expect(normalizeCwd(context.output)).toMatchSnapshot()
expect(existsSync(outputFile)).toBe(true)
expect(readFileSync(outputFile, 'utf8')).toMatchSnapshot()

// Cleanup
rmSync(outputFile)
})

test('json reporter with outputFile in non-existing directory', async() => {
// Arrange
const reporter = new JsonReporter()
const rootDirectory = resolve('jsonReportDirectory')
const outputFile = `${rootDirectory}/deeply/nested/report.json`
const context = getContext()
context.vitest.config.outputFile = outputFile

vi.setSystemTime(1642587001759)

// Act
reporter.onInit(context.vitest)
await reporter.onFinished(files)

// Assert
expect(normalizeCwd(context.output)).toMatchSnapshot()
expect(existsSync(outputFile)).toBe(true)
expect(readFileSync(outputFile, 'utf8')).toMatchSnapshot()

// Cleanup
rmdirSync(rootDirectory, { recursive: true })
})

/**
* Ensure environment and OS specific paths are consistent in snapshots
*/
function normalizeCwd(text: string) {
return text.replace(normalize(process.cwd()), '<process-cwd>')
}