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

feat(event-recorder): Add event recorder reporter #116

Merged
merged 3 commits into from
Jul 15, 2016
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/ReporterOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {StrykerOptions} from 'stryker-api/core';
import {Reporter, ReporterFactory} from 'stryker-api/report';
import ClearTextReporter from './reporters/ClearTextReporter';
import ProgressReporter from './reporters/ProgressReporter';
import EventRecorderReporter from './reporters/EventRecorderReporter';
import BroadcastReporter, {NamedReporter} from './reporters/BroadcastReporter';
import * as log4js from 'log4js';

Expand Down Expand Up @@ -44,5 +45,6 @@ export default class ReporterOrchestrator {
private registerDefaultReporters() {
ReporterFactory.instance().register('progress', ProgressReporter);
ReporterFactory.instance().register('clear-text', ClearTextReporter);
}
ReporterFactory.instance().register('event-recorder', EventRecorderReporter);
}
}
4 changes: 3 additions & 1 deletion src/reporters/BroadcastReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ export interface NamedReporter {
reporter: Reporter
}

export const ALL_EVENT_METHOD_NAMES = ['onSourceFileRead', 'onAllSourceFilesRead', 'onMutantTested', 'onAllMutantsTested', 'onConfigRead'];

export default class BroadcastReporter implements Reporter {

constructor(private reporters: NamedReporter[]) {
['onSourceFileRead', 'onAllSourceFilesRead', 'onMutantTested', 'onAllMutantsTested', 'onConfigRead', 'wrapUp'].forEach(method => {
ALL_EVENT_METHOD_NAMES.concat('wrapUp').forEach(method => {
(<any>this)[method] = (arg: any) => {
return this.broadcast(method, arg);
}
Expand Down
60 changes: 60 additions & 0 deletions src/reporters/EventRecorderReporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {Reporter} from 'stryker-api/report';
import {StrykerOptions} from 'stryker-api/core';
import {ALL_EVENT_METHOD_NAMES} from './BroadcastReporter';
import * as fileUtils from '../utils/fileUtils';
import * as log4js from 'log4js';
import * as path from 'path';

const log = log4js.getLogger('EventRecorderReporter');
const DEFAULT_BASE_FOLDER = 'reports/mutation/events';

export default class EventRecorderReporter implements Reporter {

private allWork: Promise<any>[] = [];
private createBaseFolderTask: Promise<any>;
private _baseFolder: string;

constructor(private options: StrykerOptions) {
let index = 0;
this.createBaseFolderTask = fileUtils.cleanFolder(this.baseFolder);
ALL_EVENT_METHOD_NAMES.forEach(method => {
(<any>this)[method] = (data: any) => {
this.allWork.push(this.createBaseFolderTask.then(() => this.writeToFile(index++, method, data)));
}
});
}

private get baseFolder() {
if (!this._baseFolder) {
if (this.options['eventReporter'] && this.options['eventReporter']['baseDir']) {
this._baseFolder = this.options['eventReporter']['baseDir'];
log.debug(`Using configured output folder ${this._baseFolder}`)
} else {
log.debug(`No base folder configuration found (using configuration: eventReporter: { baseDir: 'output/folder' }), using default ${DEFAULT_BASE_FOLDER}`);
this._baseFolder = DEFAULT_BASE_FOLDER;
}
}
return this._baseFolder;
}


private writeToFile(index: number, methodName: string, data: any) {
let filename = path.join(this.baseFolder, `${this.format(index)}-${methodName}.json`);
log.debug(`Writing event ${methodName} to file ${filename}`);
return fileUtils.writeFile(filename, JSON.stringify(data));
}

private format(input: number) {
let str = input.toString();
for (let i = 10000; i > 1; i = i / 10) {
if(i > input){
str = '0' + str;
}
}
return str;
}

wrapUp(): Promise<any> {
return this.createBaseFolderTask.then( () => Promise.all(this.allWork));
}
}
108 changes: 107 additions & 1 deletion src/utils/fileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import fs = require('fs');
import os = require('os');
import path = require('path');
import * as nodeGlob from 'glob';
import * as mkdirp from 'mkdirp';

/**
* Checks if a file or folder exists.
* @function
* @param path - The path to the file or folder.
* @returns True if the file exists.
*/
export function fileOrFolderExists(path: string): boolean {
export function fileOrFolderExistsSync(path: string): boolean {
try {
var stats = fs.lstatSync(path);
return true;
Expand All @@ -21,6 +22,15 @@ export function fileOrFolderExists(path: string): boolean {
}
};

export function fileOrFolderExists(path: string): Promise<boolean> {
return new Promise(resolve => {
fs.lstat(path, (error, stats) => {
resolve(!error);
});
})
}


/**
* Reads a file.
* @function
Expand Down Expand Up @@ -55,6 +65,102 @@ export function glob(expression: string): Promise<string[]> {
});
}


export function readdir(path: string): Promise<string[]> {
return new Promise<string[]>((resolve, reject) => {
fs.readdir(path, (error, files) => {
if (error) {
reject(error);
} else {
resolve(files);
}
});
});
}

function stats(path: string): Promise<fs.Stats> {
return new Promise((resolve, reject) => {
fs.stat(path, (error, stats) => {
if (error) {
reject(error);
} else {
resolve(stats);
}
});
});
}


function rmFile(path: string) {
return new Promise<void>((fileResolve, fileReject) => {
fs.unlink(path, error => {
if (error) {
fileReject(error);
} else {
fileResolve();
}
});
});
}

export function deleteDir(dirToDelete: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
fileOrFolderExists(dirToDelete).then(exists => {
if (exists) {
readdir(dirToDelete).then(files => {
let promisses: Promise<void>[] = [];
files.forEach(file => {
let currentPath = path.join(dirToDelete, file);
promisses.push(stats(currentPath).then(stats => {
if (stats.isDirectory()) {
// recurse
return deleteDir(currentPath);
} else {
// delete file
return rmFile(currentPath);
}
}));
});
Promise.all(promisses).then(() => resolve());
});
} else {
resolve();
}
});
});
}

export function cleanFolder(folderName: string) {
return fileOrFolderExists(folderName)
.then(exists => {
if (exists) {
return deleteDir(folderName)
.then(() => mkdirRecursive(folderName));
}else{
return mkdirRecursive(folderName);
}
});
}

export function writeFile(fileName: string, content: string) {
return new Promise<void>((resolve, reject) => {
fs.writeFile(fileName, content, err => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}

export function mkdirRecursive(folderName: string) {
if (!fileOrFolderExistsSync(folderName)) {
mkdirp.sync(folderName);
}
}


/**
* Wrapper around the 'require' function (for testability)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ describe('fileUtils', function() {
});

it('should indicate that an existing file exists', function() {
var exists = fileUtils.fileOrFolderExists('src/Stryker.ts');
var exists = fileUtils.fileOrFolderExistsSync('src/Stryker.ts');

expect(exists).to.equal(true);
});

it('should indicate that an non-existing file does not exists', function() {
var exists = fileUtils.fileOrFolderExists('src/Strykerfaefeafe.js');
var exists = fileUtils.fileOrFolderExistsSync('src/Strykerfaefeafe.js');

expect(exists).to.equal(false);
});
Expand Down
92 changes: 92 additions & 0 deletions test/unit/reporters/EventRecorderReporterSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import EventRecorderReporter from '../../../src/reporters/EventRecorderReporter';
import * as fileUtils from '../../../src/utils/fileUtils';
import {Reporter, MutantStatus, MutantResult} from 'stryker-api/report';
import * as sinon from 'sinon';
import log from '../../helpers/log4jsMock';
import {expect} from 'chai';
import {ALL_EVENT_METHOD_NAMES} from '../../../src/reporters/BroadcastReporter';


describe('EventRecorderReporter', () => {

let sut: Reporter;
let sandbox: sinon.SinonSandbox;
let cleanFolderStub: sinon.SinonStub;
let writeFileStub: sinon.SinonStub;

beforeEach(() => {
sandbox = sinon.sandbox.create();
cleanFolderStub = sandbox.stub(fileUtils, 'cleanFolder');
writeFileStub = sandbox.stub(fileUtils, 'writeFile');
});

describe('when constructed with empty options', () => {

describe('and cleanFolder resolves correctly', () => {
beforeEach(() => {
cleanFolderStub.returns(Promise.resolve());
sut = new EventRecorderReporter({});
});

it('should log about the default baseFolder', () => {
expect(log.debug).to.have.been.calledWith(`No base folder configuration found (using configuration: eventReporter: { baseDir: 'output/folder' }), using default reports/mutation/events`);
});

it('should clean the baseFolder', () => {
expect(fileUtils.cleanFolder).to.have.been.calledWith('reports/mutation/events');
});

let arrangeActAssertEvent = (eventName: string) => {
describe(`${eventName} event`, () => {

let writeFileRejection: any;
const expected: any = { some: 'eventData' };

let arrange = () => beforeEach(() => {
writeFileRejection = undefined;
(<any>sut)[eventName](expected);
return (<Promise<any>>sut.wrapUp()).then(() => void 0, (error) => writeFileRejection = error);
});

describe('when writeFile results in a rejection', () => {
beforeEach(() => writeFileStub.returns(Promise.reject('some error')));
arrange();

it('should reject `wrapUp`', () => expect(writeFileRejection).to.be.eq('some error'));
});

describe('when writeFile is successful', () => {
arrange();
it('should writeFile', () => expect(fileUtils.writeFile).to.have.been.calledWith(sinon.match(RegExp(`.*0000\\d-${eventName}\\.json`)), JSON.stringify(expected)));
});
});
};

ALL_EVENT_METHOD_NAMES.forEach(arrangeActAssertEvent);
});

describe('and cleanFolder results in a rejection', () => {
beforeEach(() => {
cleanFolderStub.returns(Promise.reject('Some error'));
sut = new EventRecorderReporter({});
});

describe('and `wrapUp()` is called', () => {
let result: any;

beforeEach(() => {
let promise = <Promise<any>>sut.wrapUp();
return promise.then(() => result = true, (error: any) => result = error);
});

it('should reject', () => expect(result).to.be.eq('Some error'));
})

});

});

afterEach(() => {
sandbox.restore();
});
});