Skip to content

Commit

Permalink
feat(html reporter): allow choice of fileName. (#3438)
Browse files Browse the repository at this point in the history
Allow a user to configure a custom `htmlReporter.fileName`. This _replaces_ the previous ability to use `htmlReporter.baseDir`. This also prevents the directory from being cleared.

Before:

```
{
  "htmlReporter": {
    "baseDir": "my-custom/dir"
  }
}
```

New:
```
{
  "htmlReporter": {
    "fileName": "my-custom/dir/index.html"
  }
}
```

BREAKING CHANGE: Configuration option `htmlReporter.baseDir` is deprecated and will be removed in a later version. Please use `htmlReporter.fileName` instead.
  • Loading branch information
nicojs committed Feb 18, 2022
1 parent 7fc3433 commit d197319
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 232 deletions.
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ These reporters can be used out of the box: `html`, `json`, `progress`, `clear-t
By default, `clear-text`, `progress`, `html` are active if no reporters are configured. See [reporter plugins](./plugins.md#reporters)
for a full description of 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 `html` reporter allows you to specify an output folder. This defaults to `reports/mutation/html`. The config for your config file is: `htmlReporter: { fileName: 'mypath/reports/stryker.html' }` (since Stryker v6).

The `json` reporter allows specifying an output file name (may also contain a path). The config for your config file is: `jsonReporter: { fileName: 'mypath/reports/mutation.json' }`

Expand Down
2 changes: 1 addition & 1 deletion e2e/test/reporters-e2e/verify/verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('Verify stryker has ran correctly', () => {
}

it('should report the html file', () => {
expectExists('reports/mutation/html/index.html');
expectExists('reports/mutation/mutation.html');
});

it('should have a clear text report', () => {
Expand Down
9 changes: 5 additions & 4 deletions packages/api/schema/stryker-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@
"additionalProperties": false,
"type": "object",
"properties": {
"baseDir": {
"description": "The output folder for the html report.",
"fileName": {
"description": "The `fileName` of the html report.",
"type": "string",
"default": "reports/mutation/html"
"default": "reports/mutation/mutation.html"
}
}
},
Expand Down Expand Up @@ -383,7 +383,8 @@
},
"htmlReporter": {
"description": "The options for the html reporter",
"$ref": "#/definitions/htmlReporterOptions"
"$ref": "#/definitions/htmlReporterOptions",
"default": {}
},
"jsonReporter": {
"description": "The options for the json reporter",
Expand Down
28 changes: 23 additions & 5 deletions packages/core/src/config/options-validator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os from 'os';
import path from 'path';

import glob from 'glob';
import Ajv, { ValidateFunction } from 'ajv';
Expand Down Expand Up @@ -98,13 +99,30 @@ export class OptionsValidator {
// @ts-expect-error jest.enableBail
if (rawOptions.jest?.enableBail !== undefined) {
this.log.warn(
'DEPRECATED. Use of "jest.enableBail" inside deprecated, please use "disableBail" instead. See https://stryker-mutator.io/docs/stryker-js/configuration#disablebail-boolean'
'DEPRECATED. Use of "jest.enableBail" is deprecated, please use "disableBail" instead. See https://stryker-mutator.io/docs/stryker-js/configuration#disablebail-boolean'
);
// @ts-expect-error jest.enableBail
rawOptions.disableBail = !rawOptions.jest?.enableBail;
// @ts-expect-error jest.enableBail
delete rawOptions.jest.enableBail;
}

// @ts-expect-error htmlReporter.baseDir
if (rawOptions.htmlReporter?.baseDir) {
this.log.warn(
`DEPRECATED. Use of "htmlReporter.baseDir" is deprecated, please use "${optionsPath(
'htmlReporter',
'fileName'
)}" instead. See https://stryker-mutator.io/docs/stryker-js/configuration/#reporters-string`
);
// @ts-expect-error htmlReporter.baseDir
if (!rawOptions.htmlReporter.fileName) {
// @ts-expect-error htmlReporter.baseDir
rawOptions.htmlReporter.fileName = path.join(String(rawOptions.htmlReporter.baseDir), 'index.html');
}
// @ts-expect-error htmlReporter.baseDir
delete rawOptions.htmlReporter.baseDir;
}
}

private customValidation(options: StrykerOptions) {
Expand Down Expand Up @@ -211,11 +229,11 @@ export class OptionsValidator {
if (objectUtils.isWarningEnabled('unserializableOptions', options.warnings)) {
const unserializables = findUnserializables(options);
if (unserializables) {
unserializables.forEach(({ reason, path }) =>
unserializables.forEach((unserializable) =>
this.log.warn(
`Config option "${path.join(
'.'
)}" is not (fully) serializable. ${reason}. Any test runner or checker worker processes might not receive this value as intended.`
`Config option "${unserializable.path.join('.')}" is not (fully) serializable. ${
unserializable.reason
}. Any test runner or checker worker processes might not receive this value as intended.`
)
);
this.log.warn(`(disable ${optionsPath('warnings', 'unserializableOptions')} to ignore this warning)`);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
import { readFile } from 'fs';
import { promisify } from 'util';
import path from 'path';
import fs from 'fs';

import { createRequire } from 'module';

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

import { reporterUtil } from './reporter-util.js';

export class HtmlReporter implements Reporter {
private mainPromise: Promise<void> | undefined;

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

const promisedReadFile = promisify(readFile);
public static readonly inject = tokens(commonTokens.options, commonTokens.logger);

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

public wrapUp(): Promise<void> | undefined {
return this.mainPromise;
}

private async generateReport(report: schema.MutationTestResult) {
this.log.debug(`Using file "${this.options.htmlReporter.fileName}"`);
const html = await createReportHtml(report);
await reporterUtil.writeFile(this.options.htmlReporter.fileName, html);
this.log.info(`Your report can be found at: ${fileUrl(path.resolve(this.options.htmlReporter.fileName))}`);
}
}

export async function reportTemplate(report: schema.MutationTestResult): Promise<string> {
async function createReportHtml(report: schema.MutationTestResult): Promise<string> {
const require = createRequire(import.meta.url);
const scriptContent = await promisedReadFile(require.resolve('mutation-testing-elements/dist/mutation-test-elements.js'), 'utf-8');
const scriptContent = await fs.promises.readFile(require.resolve('mutation-testing-elements/dist/mutation-test-elements.js'), 'utf-8');

return `<!DOCTYPE html>
<html>
Expand Down Expand Up @@ -41,5 +69,6 @@ export async function reportTemplate(report: schema.MutationTestResult): Promise
* Escapes the HTML tags inside strings in a JSON input by breaking them apart.
*/
function escapeHtmlTags(json: string) {
return json.replace(/</g, '<" + "');
const j = json.replace(/</g, '<"+"');
return j;
}
59 changes: 0 additions & 59 deletions packages/core/src/reporters/html/html-reporter.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/core/src/reporters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DotsReporter } from './dots-reporter.js';
import { EventRecorderReporter } from './event-recorder-reporter.js';
import { ProgressAppendOnlyReporter } from './progress-append-only-reporter.js';
import { ProgressBarReporter } from './progress-reporter.js';
import { HtmlReporter } from './html/html-reporter.js';
import { HtmlReporter } from './html-reporter.js';
import { JsonReporter } from './json-reporter.js';

export { BroadcastReporter } from './broadcast-reporter.js';
Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/reporters/reporter-util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import path from 'path';
import { promisify } from 'util';
import { createReadStream, createWriteStream, promises as fs } from 'fs';

import mkdirp from 'mkdirp';
import rimraf from 'rimraf';

export const reporterUtil = {
copyFile(fromFilename: string, toFilename: string): Promise<void> {
Expand All @@ -17,7 +15,6 @@ export const reporterUtil = {
});
},

deleteDir: promisify(rimraf),
mkdir: mkdirp,

async writeFile(fileName: string, content: string): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,20 @@ import fs from 'fs';
import { testInjector } from '@stryker-mutator/test-helpers';
import { expect } from 'chai';

import { HtmlReporter } from '../../../../src/reporters/html/html-reporter.js';
import { HtmlReporter } from '../../../../src/reporters/html-reporter.js';
import { fileUtils } from '../../../../src/utils/file-utils.js';

import { simpleReport } from './simple-report.js';

describe('HtmlReporter with example math project', () => {
let sut: HtmlReporter;
const baseDir = 'reports/mutation/math';

beforeEach(async () => {
testInjector.options.htmlReporter = { baseDir };
sut = testInjector.injector.injectClass(HtmlReporter);
it('should have created the "index.html" file in the configured directory', async () => {
const fileName = 'reports/mutation/math.html';
testInjector.options.htmlReporter = { fileName };
const sut = testInjector.injector.injectClass(HtmlReporter);
sut.onMutationTestReportReady(simpleReport);
await sut.wrapUp();
});

it('should have created the "index.html" file in the configured directory', () => {
expectFileExists(`${baseDir}/index.html`, true);
const bindMutationTestReportContent = fs.readFileSync(`${baseDir}/index.html`, 'utf8');
expect(await fileUtils.exists(fileName), `file ${fileName} does not exist`).true;
const bindMutationTestReportContent = await fs.promises.readFile(fileName, 'utf8');
expect(bindMutationTestReportContent).include(JSON.stringify(simpleReport));
});
});

function expectFileExists(file: string, expected: boolean) {
try {
fs.readFileSync(file);
expect(expected, `file ${file} does not exist`).to.be.true;
} catch (err) {
expect(expected, `file ${file} exists`).to.be.false;
}
}
29 changes: 25 additions & 4 deletions packages/core/test/unit/config/options-validator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os from 'os';
import path from 'path';

import sinon from 'sinon';
import { LogLevel, ReportType, strykerCoreSchema, StrykerOptions } from '@stryker-mutator/api/core';
Expand Down Expand Up @@ -58,6 +59,9 @@ describe(OptionsValidator.name, () => {
jsonReporter: {
fileName: 'reports/mutation/mutation.json',
},
htmlReporter: {
fileName: 'reports/mutation/mutation.html',
},
logLevel: LogLevel.Information,
maxConcurrentTestRunners: 9007199254740991,
maxTestRunnerReuse: 0,
Expand Down Expand Up @@ -169,11 +173,28 @@ describe(OptionsValidator.name, () => {
testInjector.options.jest = { enableBail: false };
sut.validate(testInjector.options);
expect(testInjector.logger.warn).calledWith(
'DEPRECATED. Use of "jest.enableBail" inside deprecated, please use "disableBail" instead. See https://stryker-mutator.io/docs/stryker-js/configuration#disablebail-boolean'
'DEPRECATED. Use of "jest.enableBail" is deprecated, please use "disableBail" instead. See https://stryker-mutator.io/docs/stryker-js/configuration#disablebail-boolean'
);
expect(testInjector.options.disableBail).true;
});

describe('htmlReporter.baseDir', () => {
it('should report a deprecation warning and set fileName', () => {
breakConfig('htmlReporter', { baseDir: 'some/base/dir' }, false);
sut.validate(testInjector.options);
expect(testInjector.logger.warn).calledWith(
'DEPRECATED. Use of "htmlReporter.baseDir" is deprecated, please use "htmlReporter.fileName" instead. See https://stryker-mutator.io/docs/stryker-js/configuration/#reporters-string'
);
expect(testInjector.options.htmlReporter.fileName).eq(path.join('some', 'base', 'dir', 'index.html'));
});

it('should not override the fileName if a fileName is already set', () => {
breakConfig('htmlReporter', { baseDir: 'some/base/dir', fileName: 'some-other.file.html' });
sut.validate(testInjector.options);
expect(testInjector.options.htmlReporter.fileName).eq('some-other.file.html');
});
});

describe('plugins', () => {
it('should be invalid with non-array plugins', () => {
breakConfig('plugins', '@stryker-mutator/typescript');
Expand Down Expand Up @@ -362,7 +383,7 @@ describe(OptionsValidator.name, () => {
describe('unknown options', () => {
it('should not warn when there are no unknown properties', () => {
testInjector.options.htmlReporter = {
baseDir: 'test',
fileName: 'test.html',
};
sut.validate(testInjector.options, true);
expect(testInjector.logger.warn).not.called;
Expand Down Expand Up @@ -454,9 +475,9 @@ describe(OptionsValidator.name, () => {
expect(testInjector.logger.warn).not.called;
}

function breakConfig(key: keyof StrykerOptions, value: any): void {
function breakConfig(key: keyof StrykerOptions, value: any, mergeObjects = true): void {
const original = testInjector.options[key];
if (typeof original === 'object' && !Array.isArray(original)) {
if (typeof original === 'object' && !Array.isArray(original) && mergeObjects) {
testInjector.options[key] = { ...original, ...value };
} else {
testInjector.options[key] = value;
Expand Down
Loading

0 comments on commit d197319

Please sign in to comment.