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

feature (reporter): issue #1814 Add json reporter #2601

Closed
wants to merge 1 commit into from
Closed
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
16 changes: 16 additions & 0 deletions packages/api/schema/stryker-core.json
Expand Up @@ -133,6 +133,18 @@
}
}
},
"jsonReporterOptions": {
"title": "JsonReporterOptions",
"additionalProperties": false,
"type": "object",
"properties": {
"baseDir": {
"description": "The output folder for the json report.",
"type": "string",
"default": "reports/mutation/json"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the JSON report will be a single file, I think it makes sense to default to reports/mutation here. So

}
}
},
"mutationScoreThresholds": {
"title": "MutationScoreThresholds",
"additionalProperties": false,
Expand Down Expand Up @@ -349,6 +361,10 @@
"description": "The options for the html reporter",
"$ref": "#/definitions/htmlReporterOptions"
},
"jsonReporter": {
"description": "The options for the json reporter",
"$ref": "#/definitions/jsonReporterOptions"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fill the default here with {}. That way, you don't need to check if options.jsonReporter` is null later, or duplicate the default value of "jsonReporterOptions.baseDir".

},
"disableTypeChecks": {
"description": "Configure a pattern that matches the files of which type checking has to be disabled. This is needed because Stryker will create (typescript) type errors when inserting the mutants in your code. Stryker disables type checking by inserting `// @ts-nocheck` atop those files and removing other `// @ts-xxx` directives (so they won't interfere with `@ts-nocheck`). The default setting allows these directives to be stripped from all JavaScript and friend files in `lib`, `src` and `test` directories. You can specify a different glob expression or set it to `false` to completely disable this behavior.",
"anyOf": [
Expand Down
12 changes: 9 additions & 3 deletions packages/core/README.md
Expand Up @@ -301,17 +301,23 @@ you can consult [npm](https://www.npmjs.com/search?q=stryker-plugin) or
### `reporters` [`string[]`]

Default: `['clear-text', 'progress', 'html']`
Command line: `--reporters clear-text,progress,dots,dashboard,html`
Config file: `reporters: ['clear-text', 'progress', 'dots', 'dashboard', 'html']`
Command line: `--reporters clear-text,progress,dots,dashboard,html,json`
Config file: `reporters: ['clear-text', 'progress', 'dots', 'dashboard', 'html', 'json']`

With `reporters`, you can set the reporters for stryker to use.
These reporters can be used out of the box: `html`, `progress`, `clear-text`, `dots`, `dashboard` and `event-recorder`.
These reporters can be used out of the box: `html`, `json`, `progress`, `clear-text`, `dots`, `dashboard` and `event-recorder`.
By default, `clear-text`, `progress`, `html` are active if no reporters are configured.
You can load additional plugins to get more reporters. See [stryker-mutator.io](https://stryker-mutator.io)
for an up-to-date list of supported reporter plugins and a description on each reporter.

The `html` reporter allows you to specify an output folder. This defaults to `reports/mutation/html`. The config for your config file is: `htmlReporter: { baseDir: 'mypath/reports/stryker' }`

The `json` reporter supports the following options:
* `baseDir` allows you to specify an output folder. This defaults to `reports/mutation/json`.


The config for your config file is: `jsonReporter: { baseDir: 'mypath/reports/stryker' }`

The `clear-text` reporter supports three additional config options:
* `allowColor` to use cyan and yellow in printing source file names and positions. This defaults to `true`, so specify as `clearTextReporter: { allowColor: false },` to disable if you must.
* `logTests` to log the names of unit tests that were run to allow mutants. By default, only the first three are logged. The config for your config file is: `clearTextReporter: { logTests: true },`
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/reporters/index.ts
Expand Up @@ -7,6 +7,7 @@ import EventRecorderReporter from './event-recorder-reporter';
import ProgressAppendOnlyReporter from './progress-append-only-reporter';
import ProgressReporter from './progress-reporter';
import HtmlReporter from './html/html-reporter';
import JsonReporter from './json/json-reporter';

export const strykerPlugins = [
declareClassPlugin(PluginKind.Reporter, 'clear-text', ClearTextReporter),
Expand All @@ -15,5 +16,6 @@ export const strykerPlugins = [
declareClassPlugin(PluginKind.Reporter, 'dots', DotsReporter),
declareClassPlugin(PluginKind.Reporter, 'event-recorder', EventRecorderReporter),
declareClassPlugin(PluginKind.Reporter, 'html', HtmlReporter),
declareClassPlugin(PluginKind.Reporter, 'json', JsonReporter),
declareFactoryPlugin(PluginKind.Reporter, 'dashboard', dashboardReporterFactory),
];
19 changes: 19 additions & 0 deletions packages/core/src/reporters/json/json-reporter-util.ts
@@ -0,0 +1,19 @@
import * as path from 'path';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a duplication of the code in html-reporter-util.ts. Can we rename that file to reporter-util and reuse the writeFile implementation from that file?

import { promisify } from 'util';
import { promises as fs } from 'fs';

import mkdirp = require('mkdirp');
import * as rimraf from 'rimraf';
import { throwError } from 'rxjs';

export const deleteDir = promisify(rimraf);
export const mkdir = mkdirp;

export async function writeFile(fileName: string, content: string) {
try {
await mkdirp(path.dirname(fileName));
await fs.writeFile(fileName, content, 'utf8');
} catch (error) {
throwError(error);
}
}
58 changes: 58 additions & 0 deletions packages/core/src/reporters/json/json-reporter.ts
@@ -0,0 +1,58 @@
import * as path from 'path';

import fileUrl = require('file-url');

import { Logger } from '@stryker-mutator/api/logging';
import { mutationTestReportSchema, Reporter } from '@stryker-mutator/api/report';
import { StrykerOptions } from '@stryker-mutator/api/core';
import { commonTokens, tokens } from '@stryker-mutator/api/plugin';

import { mkdir, deleteDir, writeFile } from './json-reporter-util';

const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/json');

export default class JsonReporter implements Reporter {
private _baseDir!: string;
private mainPromise: Promise<void> | undefined;

constructor(private readonly options: StrykerOptions, private readonly log: Logger) {}

public static readonly inject = tokens(commonTokens.options, commonTokens.logger);

public onMutationTestReportReady(report: mutationTestReportSchema.MutationTestResult) {
this.mainPromise = this.generateReport(report);
}

public wrapUp() {
return this.mainPromise;
}

public async generateReport(report: mutationTestReportSchema.MutationTestResult) {
const indexFileName = path.resolve(this.baseDir, 'report.json');

await this.cleanBaseFolder();
await writeFile(indexFileName, JSON.stringify(report));

this.log.info(`Your report can be found at: ${fileUrl(indexFileName)}`);
}

private get baseDir(): string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As stated earlier, this null check is no longer needed if you use "default": {} for the jsonReporter option. The base dir inside the StrykerOptions will always be filled with a string.

if (!this._baseDir) {
if (this.options.jsonReporter && this.options.jsonReporter.baseDir) {
this._baseDir = this.options.jsonReporter.baseDir;
this.log.debug(`Using configured output folder ${this._baseDir}`);
} else {
this.log.debug(
`No base folder configuration found (using configuration: jsonReporter: { baseDir: 'output/folder' }), using default ${DEFAULT_BASE_FOLDER}`
);
this._baseDir = DEFAULT_BASE_FOLDER;
}
}
return this._baseDir;
}

private async cleanBaseFolder(): Promise<void> {
await deleteDir(this.baseDir);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to delete the baseDir, since it is a single file report. We will just override this file whenever onMutationTestingReportReady is called.

await mkdir(this.baseDir);
}
}
7 changes: 7 additions & 0 deletions packages/core/test/unit/config/options-validator.spec.ts
Expand Up @@ -245,6 +245,13 @@ describe(markUnknownOptions.name, () => {
expect(testInjector.logger.warn).not.called;
});

it('should not warn when there are no unknown properties for jsonReporter', () => {
testInjector.options.jsonReporter = {
baseDir: 'test',
};
expect(testInjector.logger.warn).not.called;
});

it('should return the options, no matter what', () => {
testInjector.options['this key does not exist'] = 'foo';
const output = markUnknownOptions(testInjector.options, strykerCoreSchema, testInjector.logger);
Expand Down
97 changes: 97 additions & 0 deletions packages/core/test/unit/reporters/json/json-reporter.spec.ts
@@ -0,0 +1,97 @@
import * as path from 'path';

import { mutationTestReportSchema } from '@stryker-mutator/api/report';
import { testInjector } from '@stryker-mutator/test-helpers';
import { expect } from 'chai';
import * as sinon from 'sinon';

import JsonReporter from '../../../../src/reporters/json/json-reporter';
import * as JsonReporterUtil from '../../../../src/reporters/json/json-reporter-util';

describe(JsonReporter.name, () => {
let writeFileStub: sinon.SinonStub;
let mkdirStub: sinon.SinonStub;
let deleteDirStub: sinon.SinonStub;
let sut: JsonReporter;

beforeEach(() => {
writeFileStub = sinon.stub(JsonReporterUtil, 'writeFile');
deleteDirStub = sinon.stub(JsonReporterUtil, 'deleteDir');
mkdirStub = sinon.stub(JsonReporterUtil, 'mkdir');
sut = testInjector.injector.injectClass(JsonReporter);
});

describe('onMutationTestReportReady', () => {
it('should use configured base directory', async () => {
testInjector.options.jsonReporter = { baseDir: 'foo/bar' };
actReportReady();
await sut.wrapUp();
expect(testInjector.logger.debug).calledWith('Using configured output folder foo/bar');
expect(deleteDirStub).calledWith('foo/bar');
});

it('should use default base directory when no override is configured', async () => {
const expectedBaseDir = path.normalize('reports/mutation/json');
actReportReady();
await sut.wrapUp();
expect(testInjector.logger.debug).calledWith(
`No base folder configuration found (using configuration: jsonReporter: { baseDir: 'output/folder' }), using default ${expectedBaseDir}`
);
expect(deleteDirStub).calledWith(expectedBaseDir);
});

it('should clean the base directory', async () => {
actReportReady();
await sut.wrapUp();
expect(deleteDirStub).calledWith(path.normalize('reports/mutation/json'));
expect(mkdirStub).calledWith(path.normalize('reports/mutation/json'));
expect(deleteDirStub).calledBefore(mkdirStub);
});

it('should write the mutation report to disk', async () => {
const report: mutationTestReportSchema.MutationTestResult = {
files: {},
schemaVersion: '1.0',
thresholds: {
high: 80,
low: 60,
},
};
sut.onMutationTestReportReady(report);
await sut.wrapUp();
expect(writeFileStub).calledWith(path.resolve('reports', 'mutation', 'json', 'report.json'), JSON.stringify(report));
});
});

describe('wrapUp', () => {
it('should resolve when everything is OK', () => {
actReportReady();
return expect(sut.wrapUp()).eventually.undefined;
});

it('should reject when "deleteDir" rejects', () => {
const expectedError = new Error('delete dir');
deleteDirStub.rejects(expectedError);
actReportReady();
return expect(sut.wrapUp()).rejectedWith(expectedError);
});

it('should reject when "mkdir" rejects', () => {
const expectedError = new Error('mkdir');
mkdirStub.rejects(expectedError);
actReportReady();
return expect(sut.wrapUp()).rejectedWith(expectedError);
});

it('should reject when "writeFile" rejects', () => {
const expectedError = new Error('writeFile');
writeFileStub.rejects(expectedError);
actReportReady();
return expect(sut.wrapUp()).rejectedWith(expectedError);
});
});

function actReportReady() {
sut.onMutationTestReportReady({ files: {}, schemaVersion: '', thresholds: { high: 0, low: 0 } });
}
});
3 changes: 2 additions & 1 deletion stryker.parent.conf.json
Expand Up @@ -5,7 +5,8 @@
"reporters": [
"progress",
"html",
"dashboard"
"dashboard",
"json"
],
"plugins": [
"../mocha-runner",
Expand Down
3 changes: 2 additions & 1 deletion workspace.code-workspace
Expand Up @@ -102,7 +102,8 @@
"preprocessors",
"serializable",
"surrialize"
]
],
"compile-hero.disable-compile-files-on-did-save-code": false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That went in by mistake. It's a markdown plugin I have.

},
"extensions": {
"recommendations": [
Expand Down