Skip to content
This repository has been archived by the owner on Feb 19, 2021. It is now read-only.

feat(read-karma-config): Use karma configuration #10

Merged
merged 6 commits into from
Dec 29, 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: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"license": "Apache-2.0",
"peerDependencies": {
"karma": "^0.13.0 || ~1.0.0 || ^1.1.1",
"stryker-api": "^0.4.0"
"stryker-api": "^0.4.2-rc0"
},
"devDependencies": {
"@types/chai": "^3.4.32",
Expand Down Expand Up @@ -55,7 +55,7 @@
"mocha": "^3.0.2",
"sinon": "^1.17.5",
"sinon-chai": "^2.8.0",
"stryker-api": "^0.4.1",
"stryker-api": "^0.4.2-rc0",
"tslint": "^4.0.2",
"typescript": "^2.1.4"
},
Expand Down
82 changes: 82 additions & 0 deletions src/KarmaConfigReader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as log4js from 'log4js';
import * as path from 'path';
import * as karma from 'karma';
import { requireModule } from './utils';

const karmaConfigReaderLocation = 'karma/lib/config';

export default class KarmaConfigReader {

private readonly log: log4js.Logger;

constructor(private karmaConfigFile: string) {
if (this.karmaConfigFile) {
this.karmaConfigFile = path.resolve(this.karmaConfigFile);
}
this.log = log4js.getLogger('KarmaConfigReader');
}

read(): karma.ConfigOptions | null {
if (this.karmaConfigFile && typeof this.karmaConfigFile === 'string') {
this.log.info('Importing config from "%s"', this.karmaConfigFile);
try {
this.validateConfig();
return this.readConfig();
} catch (error) {
this.log.error(`Could not read karma configuration from ${this.karmaConfigFile}.`, error);
}
}
return null;
}

private validateConfig() {
// Deligate validation of configuration to karma config lib, as it has nice and recognizable error handling
this.parseNativeKarmaConfig();
}

private readConfig(): karma.ConfigOptions {
// We cannot deligate config reading to karma's config reader, because we cannot serialize the result to child processes
// It results in: TypeError: Serializing native function: bound configure
const configModule = requireModule(this.karmaConfigFile);
const config = new Config();
configModule(config);

// Use native functionality of parsing the files, so we ensure that those are correctly resolved
const karmaOptions = this.parseNativeKarmaConfig();
if (karmaOptions && karmaOptions.files) {
config['files'] = karmaOptions.files;
config['exclude'] = karmaOptions.exclude;
}
return config;
}

private parseNativeKarmaConfig(): karma.ConfigOptions | null {
let cfg: any;
try {
cfg = require(karmaConfigReaderLocation);
} catch (e) {
this.log.warn(`Could not find karma config reader at "%s"`, karmaConfigReaderLocation);
}
if (cfg) {
return cfg.parseConfig(this.karmaConfigFile, {});
} else {
return null;
}
}
}

class Config implements karma.ConfigOptions {
[key: string]: any

readonly LOG_DISABLE = 'OFF';
readonly LOG_ERROR = 'ERROR';
readonly LOG_WARN = 'WARN';
readonly LOG_INFO = 'INFO';
readonly LOG_DEBUG = 'DEBUG';

set(obj: any) {
for (let i in obj) {
this[i] = obj[i];
}
}
}
64 changes: 64 additions & 0 deletions src/KarmaConfigWriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as log4js from 'log4js';
import * as karma from 'karma';
import { InputFileDescriptor } from 'stryker-api/core';
import { ConfigWriter, Config as StrykerConfig } from 'stryker-api/config';
import KarmaConfigReader from './KarmaConfigReader';

const log = log4js.getLogger('KarmaConfigWriter');

export default class KarmaConfigWriter implements ConfigWriter {
write(strykerConfig: StrykerConfig) {
const karmaConfig = new KarmaConfigReader(strykerConfig['karmaConfigFile']).read();
if (karmaConfig) {
KarmaConfigWriter.importFiles(strykerConfig, karmaConfig);
KarmaConfigWriter.importDefaultKarmaConfig(strykerConfig, karmaConfig);
}
}

private static importFiles(strykerConfig: StrykerConfig, karmaConfig: karma.ConfigOptions) {
if (!strykerConfig.files) {
strykerConfig.files = [];
}
const files: (karma.FilePattern | string)[] = karmaConfig.files;
const exclude: string[] = karmaConfig.exclude;
if (files && Array.isArray(files)) {
const karmaFiles = files.map(KarmaConfigWriter.toInputFileDescriptor);
log.debug(`Importing following files from karma.conf file to stryker: ${JSON.stringify(karmaFiles)}`);
strykerConfig.files = strykerConfig.files.concat(karmaFiles);
}
if (exclude && Array.isArray(exclude)) {
const ignores = exclude.map(fileToIgnore => `!${fileToIgnore}`);
log.debug(`Importing following "exclude" files from karma configuration: ${JSON.stringify(ignores)}`);
strykerConfig.files = strykerConfig.files.concat(ignores);
}
}

private static importDefaultKarmaConfig(strykerConfig: StrykerConfig, karmaConfig: karma.ConfigOptions) {
if (strykerConfig['karmaConfig']) {
const target = strykerConfig['karmaConfig'];
for (let i in karmaConfig) {
if (!target[i]) {
target[i] = (<any>karmaConfig)[i];
}
}
} else {
strykerConfig['karmaConfig'] = karmaConfig;
}
}

private static toInputFileDescriptor(karmaPattern: karma.FilePattern | string): InputFileDescriptor {
if (typeof karmaPattern === 'string') {
return {
pattern: karmaPattern,
included: true,
mutated: false
};
} else {
return {
pattern: karmaPattern.pattern,
included: karmaPattern.included || false,
mutated: (karmaPattern as any)['mutated'] || false
};
}
}
}
11 changes: 7 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import KarmaTestRunner from './KarmaTestRunner';
import {TestRunnerFactory} from 'stryker-api/test_runner';

TestRunnerFactory.instance().register('karma', KarmaTestRunner);
import { TestRunnerFactory } from 'stryker-api/test_runner';
import { ConfigWriterFactory } from 'stryker-api/config';
import KarmaTestRunner from './KarmaTestRunner';
import KarmaConfigWriter from './KarmaConfigWriter';

TestRunnerFactory.instance().register('karma', KarmaTestRunner);
ConfigWriterFactory.instance().register('karma', KarmaConfigWriter);
3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function requireModule(name: string): any {
return require(name);
}
15 changes: 15 additions & 0 deletions test/helpers/LoggerStub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as sinon from 'sinon';

export default class LoggerStub {
error: sinon.SinonStub;
warn: sinon.SinonStub;
info: sinon.SinonStub;
debug: sinon.SinonStub;

constructor() {
this.error = sinon.stub();
this.warn = sinon.stub();
this.info = sinon.stub();
this.debug = sinon.stub();
}
}
10 changes: 5 additions & 5 deletions test/helpers/chaiSetup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as chai from 'chai';
import * as sinonChai from 'sinon-chai';
import * as chaiAsPromised from 'chai-as-promised';
chai.use(sinonChai);
chai.use(chaiAsPromised);
import * as chai from 'chai';
import * as sinonChai from 'sinon-chai';
import * as chaiAsPromised from 'chai-as-promised';
chai.use(sinonChai);
chai.use(chaiAsPromised);
72 changes: 72 additions & 0 deletions test/integration/KarmaConfigWriter.it.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { expect } from 'chai';
import * as path from 'path';
import { Config } from 'stryker-api/config';
import KarmaConfigWriter from '../../src/KarmaConfigWriter';

function resolve(filePath: string) {
return path.resolve(`testResources/configs/${filePath}`).replace(/\\/g, '/');
}

function strykerConfig(karmaConfigPath: string) {
const config = new Config();
config['karmaConfigFile'] = karmaConfigPath;
return config;
}

describe('KarmaConfigWriter', () => {

it('should override the stryker `files` property when no `files` were present', () => {
const config = strykerConfig('testResources/configs/files-karma.conf.js');
new KarmaConfigWriter().write(config);
expect(config.files).to.deep.eq([
{ included: true, mutated: false, pattern: resolve('src/**/*.js') },
{ included: false, mutated: false, pattern: resolve('resources/**/*.js') },
'!' + resolve('**/index.js'),
'!' + resolve('+(Error|InfiniteAdd).js'),
'!' + resolve('files-karma.conf.js')]);
});

it('should add to the stryker `files` property when `files` were already present', () => {
const config = strykerConfig('testResources/configs/files-karma.conf.js');
config.files = ['some file'];
new KarmaConfigWriter().write(config);
expect(config.files).to.have.length(6);
expect(config.files[0]).to.be.eq('some file');
});

it('should fill the "karmaConfig" object if no "karmaConfig" object was present', () => {
const config = strykerConfig(resolve('example-karma.conf.js'));
new KarmaConfigWriter().write(config);

const expectedConfig: any = {
basePath: '',
frameworks: ['jasmine', 'requirejs'],
files: [
],
exclude: [
resolve('example-karma.conf.js')
],
preprocessors: {
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: 'INFO',
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
concurrency: Infinity,
};
const actualConfig = config['karmaConfig'];
for (let i in expectedConfig) {
expect(actualConfig).to.have.property(i).with.deep.eq(expectedConfig[i], `Expected property ${i} with value ${JSON.stringify(actualConfig[i])} to eq ${JSON.stringify(expectedConfig[i])}`);
}
});

it('should not do anything if no "karmaConfigFile" property is present', () => {
const config = new Config();
new KarmaConfigWriter().write(config);
expect(config.files).to.not.be.ok;
expect(config['karmaConfig']).to.not.be.ok;
});
});
54 changes: 54 additions & 0 deletions test/unit/KarmaConfigReaderSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as sinon from 'sinon';
import { expect } from 'chai';
import * as log4js from 'log4js';
const cfg: { parseConfig: sinon.SinonStub } = require('karma/lib/config');
import KarmaConfigReader from '../../src/KarmaConfigReader';
import LoggerStub from '../helpers/LoggerStub';
import * as path from 'path';
import * as utils from '../../src/utils';

describe('KarmaConfigReader', () => {
let sandbox: sinon.SinonSandbox;
let sut: KarmaConfigReader;
let log: LoggerStub;
let karmaConfigModule: sinon.SinonStub;

beforeEach(() => {
log = new LoggerStub();
sandbox = sinon.sandbox.create();
karmaConfigModule = sandbox.stub();
sandbox.stub(utils, 'requireModule').returns(karmaConfigModule);
sandbox.stub(cfg, 'parseConfig');
sandbox.stub(log4js, 'getLogger').returns(log);
});

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

describe('read', () => {

beforeEach(() => sut = new KarmaConfigReader('someLocation'));

it('should log an error when the config has validation errors', () => {
const expectedConfigFileLocation = path.resolve('someLocation');
cfg.parseConfig.throws(new Error('Config error'));
sut.read();
expect(log.error).to.have.been.calledWith(sinon.match(`Could not read karma configuration from ${expectedConfigFileLocation}.`), sinon.match.has('message', 'Config error'));
expect(cfg.parseConfig).to.have.been.calledWith(expectedConfigFileLocation);
});

it('should load the config', () => {
karmaConfigModule.returns('expectedConfig');
const acualConfig = sut.read();
expect(karmaConfigModule).to.have.been.calledWith(sinon.match((obj: any) => typeof obj.set === 'function'));
expect(acualConfig).to.be.ok;
});
});

describe('read without karmaConfigFile', () => {
beforeEach(() => sut = new KarmaConfigReader(''));

it('should not read anything', () => {
expect(sut.read()).to.be.null;
});
});
});
Loading