UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

237 lines 13.9 kB
import fs from 'fs'; import { syncBuiltinESMExports } from 'module'; import { pathToFileURL } from 'url'; import path from 'path'; import { testInjector } from '@stryker-mutator/test-helpers'; import sinon from 'sinon'; import { expect } from 'chai'; import { ConfigReader, OptionsValidator } from '../../../src/config/index.js'; import { ConfigError } from '../../../src/errors.js'; import { coreTokens } from '../../../src/di/index.js'; import { fileUtils } from '../../../src/utils/file-utils.js'; describe(ConfigReader.name, () => { let sut; let existsStub; let readFileStub; let importModuleStub; let optionsValidatorMock; beforeEach(() => { existsStub = sinon.stub(fileUtils, 'exists'); optionsValidatorMock = sinon.createStubInstance(OptionsValidator); readFileStub = sinon.stub(fs.promises, 'readFile'); importModuleStub = sinon.stub(fileUtils, 'importModule'); syncBuiltinESMExports(); sut = testInjector.injector.provideValue(coreTokens.optionsValidator, optionsValidatorMock).injectClass(ConfigReader); }); it('should be able to read config from a custom JSON file', async () => { // Arrange const expectedOptions = { testRunner: 'my-runner', configFile: 'file.json' }; existsStub.withArgs('file.json').resolves(true); readFileStub.resolves(JSON.stringify(expectedOptions)); // Act const result = await sut.readConfig({ configFile: 'file.json' }); // Assert sinon.assert.calledWithExactly(optionsValidatorMock.validate, expectedOptions); sinon.assert.calledWithExactly(readFileStub, 'file.json', 'utf-8'); expect(result).deep.eq(expectedOptions); expect(testInjector.logger.warn).not.called; }); it('should be able to import from a custom JS file', async () => { // Arrange const expectedOptions = { testRunner: 'my-runner', configFile: 'file.js' }; existsStub.withArgs('file.js').resolves(true); readFileStub.resolves(JSON.stringify(expectedOptions)); importModuleStub.withArgs(pathToFileURL(path.resolve('file.js')).toString()).resolves({ default: expectedOptions }); // Act const result = await sut.readConfig({ configFile: 'file.js' }); // Assert sinon.assert.calledWithExactly(optionsValidatorMock.validate, expectedOptions); expect(result).deep.eq(expectedOptions); expect(testInjector.logger.warn).not.called; }); ['stryker.conf', '.stryker.conf', 'stryker.config', '.stryker.config'].forEach((base) => { it(`should load ${base}.json file by default`, async () => { // Arrange const expectedOptions = { testRunner: 'my-runner' }; existsStub.withArgs(`${base}.json`).resolves(true); readFileStub.resolves(JSON.stringify(expectedOptions)); // Act const result = await sut.readConfig({}); // Assert sinon.assert.calledWithExactly(optionsValidatorMock.validate, expectedOptions); expect(result).deep.eq(expectedOptions); }); ['js', 'mjs', 'cjs'].forEach((ext) => { it(`should load ${base}.${ext} by default`, async () => { // Arrange const strykerConfFile = `${base}.${ext}`; const expectedOptions = { testRunner: 'my-runner' }; existsStub.withArgs(strykerConfFile).resolves(true); readFileStub.resolves(JSON.stringify(expectedOptions)); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves({ default: expectedOptions }); // Act const result = await sut.readConfig({}); // Assert sinon.assert.calledWithExactly(optionsValidatorMock.validate, expectedOptions); expect(result).deep.eq(expectedOptions); }); }); }); it('should use cli options if no config file is available', async () => { // Arrange const expectedOptions = { testRunner: 'my-runner' }; existsStub.resolves(false); // Act const result = await sut.readConfig(expectedOptions); // Assert expect(importModuleStub).not.called; expect(readFileStub).not.called; sinon.assert.calledWithExactly(testInjector.logger.info, 'No config file specified. Running with command line arguments.'); sinon.assert.calledWithExactly(testInjector.logger.info, 'Use `stryker init` command to generate your config file.'); sinon.assert.calledWithExactly(optionsValidatorMock.validate, expectedOptions); expect(result).deep.eq(expectedOptions); }); it('should override loaded config with cli options (cli takes precedence)', async () => { // Arrange existsStub.withArgs('stryker.conf.json').resolves(true); readFileStub.resolves(JSON.stringify({ testRunner: 'my-runner', concurrency: 3 })); // Act const result = await sut.readConfig({ concurrency: 2 }); // Assert const expectedOptions = { testRunner: 'my-runner', concurrency: 2 }; sinon.assert.calledWithExactly(optionsValidatorMock.validate, expectedOptions); expect(result).deep.eq(expectedOptions); }); it('should throw a config error and log details when the loaded configuration is not an object', async () => { // Arrange const strykerConfFile = 'stryker.conf.js'; existsStub.withArgs(strykerConfFile).resolves(true); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves({ default: 42 }); // Act & assert const error = await expect(sut.readConfig({})).rejectedWith('Invalid config file "stryker.conf.js". Default export of config file must be an object!'); expect(error).instanceOf(ConfigError); sinon.assert.calledWithMatch(testInjector.logger.fatal, sinon.match('Invalid config file. It must export an object, found a "number"!')); sinon.assert.calledWithMatch(testInjector.logger.fatal, sinon.match('Example of how a config file should look:')); }); it('should throw when the .json config could not be read', async () => { // Arrange existsStub.withArgs('stryker.conf.json').resolves(true); const expectedError = new Error('Cannot read file.'); readFileStub.rejects(expectedError); // Act & assert await expect(sut.readConfig({})).rejectedWith(expectedError); }); it("should throw when the explicit config file doesn't exist", async () => { existsStub.resolves(false); const error = await expect(sut.readConfig({ configFile: 'foo.conf.js' })).rejectedWith('Invalid config file "foo.conf.js". File does not exist!'); expect(error).instanceOf(ConfigError); }); it('should throw when the imported file contains invalid js', async () => { const syntaxError = new Error('SyntaxError'); existsStub.withArgs('stryker.conf.js').resolves(true); importModuleStub.rejects(syntaxError); const error = await expect(sut.readConfig({})).rejectedWith('Invalid config file "stryker.conf.js". Error during import.'); expect(error).instanceOf(ConfigError); expect(error.innerError).eq(syntaxError); }); describe('logging', () => { it('should log a specific deprecation error when the loaded configuration is a function', async () => { // Arrange const strykerConfFile = 'stryker.conf.js'; existsStub.withArgs(strykerConfFile).resolves(true); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves({ default: () => { /* idle */ }, }); // Act & assert const error = await expect(sut.readConfig({})).rejectedWith('Invalid config file "stryker.conf.js". Default export of config file must be an object!'); expect(error).instanceOf(ConfigError); sinon.assert.calledWithMatch(testInjector.logger.fatal, sinon.match('Invalid config file. Exporting a function is no longer supported. Please export an object with your configuration instead, or use a "stryker.conf.json" file.')); }); it('should log a specific error when there is no default export in the loaded configuration module', async () => { // Arrange const strykerConfFile = 'stryker.conf.js'; existsStub.withArgs(strykerConfFile).resolves(true); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves({ pi: 3.14, foo: 'bar' }); // Act & assert const error = await expect(sut.readConfig({})).rejectedWith('Invalid config file "stryker.conf.js". Config file must have a default export!'); expect(error).instanceOf(ConfigError); sinon.assert.calledWithMatch(testInjector.logger.fatal, sinon.match('Invalid config file. It is missing a default export. Found named export(s): "pi", "foo".')); }); it('should log a specific error when nothing is exported from the configuration module', async () => { // Arrange const strykerConfFile = 'stryker.conf.js'; existsStub.withArgs(strykerConfFile).resolves(true); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves({}); // Act & assert await expect(sut.readConfig({})).rejectedWith('Invalid config file "stryker.conf.js". Config file must have a default export!'); sinon.assert.calledWithMatch(testInjector.logger.fatal, sinon.match("Invalid config file. It is missing a default export. In fact, it didn't export anything")); }); it('should log a specific error when imported module configuration module is a number somehow', async () => { // Arrange const strykerConfFile = 'stryker.conf.js'; existsStub.withArgs(strykerConfFile).resolves(true); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves(42); // Act & assert await expect(sut.readConfig({})).rejected; sinon.assert.calledWithMatch(testInjector.logger.fatal, sinon.match("Invalid config file. It is missing a default export. In fact, it didn't export anything")); }); it("should log an error when the .json config couldn't be parsed", async () => { // Arrange existsStub.withArgs('stryker.conf.json').resolves(true); readFileStub.resolves('{ not: "json" }'); // Act & assert const error = await expect(sut.readConfig({})).rejectedWith('Invalid config file "stryker.conf.json". File contains invalid JSON'); // Assert expect(error).instanceOf(ConfigError); expect(error.innerError.message).eq('Unexpected token n in JSON at position 2'); }); it('should the final configuration to debug', async () => { // Arrange testInjector.logger.isDebugEnabled.returns(true); existsStub.withArgs('stryker.conf.json').resolves(true); readFileStub.resolves(JSON.stringify({ testRunner: 'my-runner', concurrency: 3 })); // Act await sut.readConfig({ concurrency: 2 }); // Assert const expectedOptions = { testRunner: 'my-runner', concurrency: 2 }; sinon.assert.calledWithExactly(testInjector.logger.debug, `Loaded config: ${JSON.stringify(expectedOptions, null, 2)}`); }); it('should not log final configuration to debug when debug logging is not enabled', async () => { // Arrange testInjector.logger.isDebugEnabled.returns(false); // Act await sut.readConfig({ concurrency: 2 }); // Assert sinon.assert.neverCalledWithMatch(testInjector.logger.debug, sinon.match('Loaded config:')); }); it('should log the file it is reading to debug', async () => { existsStub.withArgs('stryker.conf.json').resolves(true); readFileStub.resolves('{}'); await sut.readConfig({ concurrency: 2 }); sinon.assert.calledWithExactly(testInjector.logger.debug, 'Loading config from stryker.conf.json'); }); it('should log a warning when the loaded config was empty', async () => { // Arrange const strykerConfFile = 'foo.conf.js'; existsStub.withArgs(strykerConfFile).resolves(true); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves({ default: {} }); // Act await sut.readConfig({ configFile: 'foo.conf.js' }); // Assert sinon.assert.calledWithMatch(testInjector.logger.warn, sinon.match('Stryker options were empty. Did you forget to export options from foo.conf.js?')); }); it('should log a warning when the loaded config was nullish', async () => { // Arrange const strykerConfFile = 'foo.conf.js'; existsStub.withArgs(strykerConfFile).resolves(true); importModuleStub.withArgs(pathToFileURL(path.resolve(strykerConfFile)).toString()).resolves({ default: null }); // Act await sut.readConfig({ configFile: 'foo.conf.js' }); // Assert sinon.assert.calledWithMatch(testInjector.logger.warn, sinon.match('Stryker options were empty. Did you forget to export options from foo.conf.js?')); }); }); }); //# sourceMappingURL=config-reader.spec.js.map