From 5eac4ca63ad8ee84651249c6bcb427e3216f1ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Sat, 19 Mar 2022 17:37:20 +0100 Subject: [PATCH] fix: prevent crashing when directory of `outputFile` does not exist (#986) --- packages/vitest/src/node/reporters/json.ts | 9 +- packages/vitest/src/node/reporters/junit.ts | 9 +- .../__snapshots__/reporters.spec.ts.snap | 128 ++++++++++++++++++ test/reporters/tests/reporters.spec.ts | 65 ++++++++- 4 files changed, 202 insertions(+), 9 deletions(-) diff --git a/packages/vitest/src/node/reporters/json.ts b/packages/vitest/src/node/reporters/json.ts index 8d102e7ee4cb..34fab62a57e9 100644 --- a/packages/vitest/src/node/reporters/json.ts +++ b/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' @@ -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}`) } diff --git a/packages/vitest/src/node/reporters/junit.ts b/packages/vitest/src/node/reporters/junit.ts index 9efc094706a6..6b99b9f23c58 100644 --- a/packages/vitest/src/node/reporters/junit.ts +++ b/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' @@ -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`) diff --git a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap index ca8e4287c1c6..4d6aa52003c5 100644 --- a/test/reporters/tests/__snapshots__/reporters.spec.ts.snap +++ b/test/reporters/tests/__snapshots__/reporters.spec.ts.snap @@ -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 /junitReportDirectory/deeply/nested/report.xml +" +`; + +exports[`JUnit reporter with outputFile in non-existing directory 2`] = ` +" + + + + +AssertionError: expected 2.23606797749979 to equal 2 + ❯ vitest/test/core/test/basic.test.ts:8:32 + + + + + + + + + + + + + + + + + + + +" +`; + exports[`json reporter 1`] = ` { "numFailedTestSuites": 0, @@ -246,6 +281,99 @@ exports[`json reporter with outputFile 2`] = ` }" `; +exports[`json reporter with outputFile in non-existing directory 1`] = ` +"JSON report written to /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 diff --git a/test/reporters/tests/reporters.spec.ts b/test/reporters/tests/reporters.spec.ts index 5bc45af01e3c..8c94a9bd0619 100644 --- a/test/reporters/tests/reporters.spec.ts +++ b/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' @@ -75,8 +75,7 @@ test('JUnit reporter with outputFile', async() => { await reporter.onFinished(files) // Assert - const output = context.output.replace(normalize(process.cwd()), '') - expect(output).toMatchSnapshot() + expect(normalizeCwd(context.output)).toMatchSnapshot() expect(existsSync(outputFile)).toBe(true) expect(readFileSync(outputFile, 'utf8')).toMatchSnapshot() @@ -84,6 +83,33 @@ test('JUnit reporter with outputFile', async() => { 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() @@ -113,11 +139,40 @@ test('json reporter with outputFile', async() => { await reporter.onFinished(files) // Assert - const output = context.output.replace(normalize(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()), '') +}