Skip to content

Commit

Permalink
feat: add support for rewriting <file path="..."> attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
AriPerkkio committed Feb 4, 2024
1 parent d31deef commit 049c9b8
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 38 deletions.
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,14 @@ import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
reporters: 'vitest-sonar-reporter',
outputFile: 'sonar-report.xml',
reporters: [
['vitest-sonar-reporter', { outputFile: 'sonar-report.xml' }],
],
},
});
```

If you have multiple outputFile's defined, add one for `vitest-sonar-reporter`:
If you are using Vitest below version `^1.3.0` you can define file in `test.outputFile`:

```ts
test: {
Expand All @@ -64,16 +65,47 @@ sonar.testExecutionReportPaths=sonar-report.xml

### Options

You can pass additional options using `test.sonarReporterOptions` in `vite.config.ts`. Note that passing custom options to Vitest reporters is unconventional and may require you to use `@ts-ignore` when using TypeScript.
You can pass additional options to reporter. Note that this requires `vitest@^1.3.0`.

#### `silent`

Silence reporter's verbose logging.

```ts
test: {
reporters: 'vitest-sonar-reporter',
sonarReporterOptions: { silent: true }
reporters: [
['vitest-sonar-reporter', { silent: true }]
],
}
```

#### `onWritePath`

Rewrite `path` attribute of `<file>`. This can be useful when you need to change relative paths of the files.

```ts
test: {
reporters: [
['vitest-sonar-reporter', {
onWritePath(path: string) {
// Prefix all paths with root directory
// e.g. '<file path="test/math.ts">' to '<file path="frontend/test/math.ts">'
return `frontend/${path}`;
}
}]
],
}
```

#### `outputFile`

Location for the report.

```ts
test: {
reporters: [
['vitest-sonar-reporter', { outputFile: 'sonar-report.xml' }]
],
}
```

Expand Down
63 changes: 47 additions & 16 deletions src/sonar-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,64 @@ import type { Reporter, File, Vitest } from 'vitest';

import { generateXml } from './xml.js';

export interface SonarReporterOptions {
outputFile: string;
silent?: boolean;
onWritePath: (path: string) => string;
}

/**
* Reporter used by `vitest`
*/
export default class SonarReporter implements Reporter {
ctx!: Vitest;
outputFile!: string;
silent!: boolean;
options: SonarReporterOptions;

constructor(options?: Partial<SonarReporterOptions>) {
this.options = {
silent: options?.silent ?? false,
onWritePath: options?.onWritePath ?? defaultOnWritePath,

// @ts-expect-error -- Can also be initialized during onInit()
outputFile: options?.outputFile,
};
}

onInit(ctx: Vitest) {
this.ctx = ctx;

// @ts-expect-error -- untyped
this.silent = ctx.config.sonarReporterOptions?.silent === true;
this.options.silent =
this.options.silent ||
// TODO: Remove in v2.0.0
// @ts-expect-error -- untyped
ctx.config.sonarReporterOptions?.silent === true;

if (!this.ctx.config.outputFile) {
if (!this.ctx.config.outputFile && !this.options.outputFile) {
throw new Error(
'SonarReporter requires config.outputFile to be defined in vite config',
'SonarReporter requires outputFile to be defined in config',
);
}

this.outputFile = resolveOutputfile(this.ctx.config);
this.options.outputFile =
this.options.outputFile ?? resolveOutputfile(this.ctx.config);

if (existsSync(this.outputFile)) {
rmSync(this.outputFile);
if (existsSync(this.options.outputFile)) {
rmSync(this.options.outputFile);
}
}

onFinished(rawFiles?: File[]) {
const reportFile = resolve(this.ctx.config.root, this.outputFile);
const reportFile = resolve(
this.ctx.config.root,
this.options.outputFile,
);

// Map filepaths to be relative to root for workspace support
const files = rawFiles?.map((file) => ({
...file,
name: relative(process.cwd(), file.filepath),
name: this.options.onWritePath(
relative(process.cwd(), file.filepath),
),
}));

const outputDirectory = dirname(reportFile);
Expand All @@ -49,12 +73,16 @@ export default class SonarReporter implements Reporter {

writeFileSync(reportFile, generateXml(sorted), 'utf-8');

if (!this.silent) {
if (!this.options.silent) {
this.ctx.logger.log(`SonarQube report written to ${reportFile}`);
}
}
}

function defaultOnWritePath(path: string) {
return path;
}

function resolveOutputfile(config: Vitest['config']) {
if (typeof config.outputFile === 'string') {
return config.outputFile;
Expand All @@ -67,13 +95,16 @@ function resolveOutputfile(config: Vitest['config']) {
throw new Error(
[
'Unable to resolve outputFile for vitest-sonar-reporter.',
'Define outputFile as string or add entry for it:',
'Define outputFile in reporter options:',
JSON.stringify(
{
test: {
outputFile: {
'vitest-sonar-reporter': 'sonar-report.xml',
},
reporters: [
[
'vitest-sonar-reporter',
{ outputFile: 'sonar-report.xml' },
],
],
},
},
null,
Expand Down
82 changes: 77 additions & 5 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { startVitest } from 'vitest/node';
import { stabilizeReport } from './utils';

const outputFile = 'report-from-tests.xml';
const reporterPath = new URL('../src/index.ts', import.meta.url).href;

beforeEach(() => {
if (existsSync(outputFile)) {
Expand Down Expand Up @@ -67,6 +68,65 @@ test('writes a report', async () => {
`);
});

test('file path can be rewritten using options.onWritePath ', async () => {
await runVitest({
reporterOptions: {
onWritePath(path: string) {
return `custom-prefix/${path}`;
},
},
});

const contents = readFileSync(outputFile, 'utf-8');
const stable = stabilizeReport(contents);

expect(stable).toMatchInlineSnapshot(`
"<testExecutions version="1">
<file path="custom-prefix/test/fixtures/animals.test.ts">
<testCase name="animals - dogs say woof" duration="123" />
<testCase name="animals - figure out what rabbits say" duration="123">
<skipped message="figure out what rabbits say" />
</testCase>
<testCase name="animals - flying ones - cat can fly" duration="123">
<failure message="expected false to be true // Object.is equality">
<![CDATA[AssertionError: expected false to be true // Object.is equality
at <process-cwd>/test/fixtures/animals.test.js
<removed-stacktrace>
</failure>
</testCase>
<testCase name="animals - flying ones - bird can fly" duration="123" />
</file>
<file path="custom-prefix/test/fixtures/math.test.ts">
<testCase name="math - sum" duration="123" />
<testCase name="math - multiply" duration="123" />
<testCase name="math - slow calculation" duration="123" />
<testCase name="math - tricky calculation of &quot;16 / 4&quot;" duration="123">
<failure message="expected 4 to deeply equal 8">
<![CDATA[AssertionError: expected 4 to deeply equal 8
at <process-cwd>/test/fixtures/math.test.js
<removed-stacktrace>
</failure>
</testCase>
<testCase name="math - complex calculation" duration="123">
<error message="16.divideByTwo is not a function">
<![CDATA[TypeError: 16.divideByTwo is not a function
at <process-cwd>/test/fixtures/math.test.js
<removed-stacktrace>
</error>
</testCase>
<testCase name="math - random numbers are unstable" duration="123">
<skipped message="random numbers are unstable" />
</testCase>
<testCase name="math - learn square roots" duration="123">
<skipped message="learn square roots" />
</testCase>
<testCase name="math - divide - basic" duration="123" />
<testCase name="math - divide - by zero" duration="123" />
</file>
</testExecutions>"
`);
});

test('report location is logged', async () => {
const spy = vi.spyOn(console, 'log');
await runVitest();
Expand All @@ -81,21 +141,33 @@ test('report location is logged', async () => {
);
});

test('logging can be silenced', async () => {
test('logging can be silenced, legacy config', async () => {
const spy = vi.spyOn(console, 'log');
await runVitest({ config: { sonarReporterOptions: { silent: true } } });

expect(existsSync(outputFile)).toBe(true);
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});

test('logging can be silenced via options', async () => {
const spy = vi.spyOn(console, 'log');
await runVitest({ sonarReporterOptions: { silent: true } });
await runVitest({ reporterOptions: { silent: true } });

expect(existsSync(outputFile)).toBe(true);
expect(spy).not.toHaveBeenCalled();
spy.mockRestore();
});

async function runVitest(opts = {}) {
async function runVitest(options?: {
reporterOptions?: Record<string, unknown>;
config?: Record<string, unknown>;
}) {
await startVitest('test', [], {
watch: false,
reporters: new URL('../src/index.ts', import.meta.url).href,
reporters: [[reporterPath, options?.reporterOptions || {}]],
outputFile,
include: ['test/fixtures/*.test.ts'],
...opts,
...options?.config,
});
}
29 changes: 18 additions & 11 deletions test/sonar-reporter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ test('resolves outputFile from string', () => {

reporter.onInit(getConfig({ outputFile: 'test-report.xml' }));

expect(reporter.outputFile).toMatchInlineSnapshot('"test-report.xml"');
expect(reporter.options.outputFile).toMatchInlineSnapshot(
'"test-report.xml"',
);
});

test('resolves outputFile from object', () => {
Expand All @@ -21,7 +23,7 @@ test('resolves outputFile from object', () => {
}),
);

expect(reporter.outputFile).toMatchInlineSnapshot(
expect(reporter.options.outputFile).toMatchInlineSnapshot(
'"test-report-from-object.xml"',
);
});
Expand All @@ -32,7 +34,7 @@ test('throws when outputFile is missing', () => {
expect(() =>
reporter.onInit(getConfig({ outputFile: undefined })),
).toThrowErrorMatchingInlineSnapshot(
`[Error: SonarReporter requires config.outputFile to be defined in vite config]`,
`[Error: SonarReporter requires outputFile to be defined in config]`,
);
});

Expand All @@ -45,14 +47,19 @@ test('throws when outputFile object is missing entry', () => {
),
).toThrowErrorMatchingInlineSnapshot(`
[Error: Unable to resolve outputFile for vitest-sonar-reporter.
Define outputFile as string or add entry for it:
{
"test": {
"outputFile": {
"vitest-sonar-reporter": "sonar-report.xml"
}
}
}]
Define outputFile in reporter options:
{
"test": {
"reporters": [
[
"vitest-sonar-reporter",
{
"outputFile": "sonar-report.xml"
}
]
]
}
}]
`);
});

Expand Down

0 comments on commit 049c9b8

Please sign in to comment.