Skip to content

Commit

Permalink
Use cwd-relative pathname to load config file (#3829)
Browse files Browse the repository at this point in the history
  • Loading branch information
plroebuck authored and juergba committed Apr 7, 2019
1 parent b079d24 commit aaf2b72
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 13 deletions.
35 changes: 28 additions & 7 deletions lib/cli/config.js
Expand Up @@ -9,9 +9,9 @@
*/

const fs = require('fs');
const findUp = require('find-up');
const path = require('path');
const debug = require('debug')('mocha:cli:config');
const findUp = require('find-up');

/**
* These are the valid config files, in order of precedence;
Expand All @@ -28,14 +28,31 @@ exports.CONFIG_FILES = [
'.mocharc.json'
];

const isModuleNotFoundError = err =>
err.code !== 'MODULE_NOT_FOUND' ||
err.message.indexOf('Cannot find module') !== -1;

/**
* Parsers for various config filetypes. Each accepts a filepath and
* Parsers for various config filetypes. Each accepts a filepath and
* returns an object (but could throw)
*/
const parsers = (exports.parsers = {
yaml: filepath =>
require('js-yaml').safeLoad(fs.readFileSync(filepath, 'utf8')),
js: filepath => require(filepath),
js: filepath => {
const cwdFilepath = path.resolve(filepath);
try {
debug(`parsers: load using cwd-relative path: "${cwdFilepath}"`);
return require(cwdFilepath);
} catch (err) {
if (isModuleNotFoundError(err)) {
debug(`parsers: retry load as module-relative path: "${filepath}"`);
return require(filepath);
} else {
throw err; // rethrow
}
}
},
json: filepath =>
JSON.parse(
require('strip-json-comments')(fs.readFileSync(filepath, 'utf8'))
Expand All @@ -45,36 +62,40 @@ const parsers = (exports.parsers = {
/**
* Loads and parses, based on file extension, a config file.
* "JSON" files may have comments.
*
* @private
* @param {string} filepath - Config file path to load
* @returns {Object} Parsed config object
* @private
*/
exports.loadConfig = filepath => {
let config = {};
debug(`loadConfig: "${filepath}"`);

const ext = path.extname(filepath);
try {
if (/\.ya?ml/.test(ext)) {
if (ext === '.yml' || ext === '.yaml') {
config = parsers.yaml(filepath);
} else if (ext === '.js') {
config = parsers.js(filepath);
} else {
config = parsers.json(filepath);
}
} catch (err) {
throw new Error(`failed to parse ${filepath}: ${err}`);
throw new Error(`failed to parse config "${filepath}": ${err}`);
}
return config;
};

/**
* Find ("find up") config file starting at `cwd`
*
* @param {string} [cwd] - Current working directory
* @returns {string|null} Filepath to config, if found
*/
exports.findConfig = (cwd = process.cwd()) => {
const filepath = findUp.sync(exports.CONFIG_FILES, {cwd});
if (filepath) {
debug(`found config at ${filepath}`);
debug(`findConfig: found "${filepath}"`);
}
return filepath;
};
86 changes: 83 additions & 3 deletions test/integration/config.spec.js
@@ -1,10 +1,11 @@
'use strict';

// this is not a "functional" test; we aren't invoking the mocha executable.
// instead we just avoid test doubles.
// This is not a "functional" test; we aren't invoking the mocha executable.
// Instead we just avoid test doubles.

var loadConfig = require('../../lib/cli/config').loadConfig;
var fs = require('fs');
var path = require('path');
var loadConfig = require('../../lib/cli/config').loadConfig;

describe('config', function() {
it('should return the same values for all supported config types', function() {
Expand All @@ -15,4 +16,83 @@ describe('config', function() {
expect(js, 'to equal', json);
expect(json, 'to equal', yaml);
});

describe('when configuring Mocha via a ".js" file', function() {
var projRootDir = path.join(__dirname, '..', '..');
var configDir = path.join(__dirname, 'fixtures', 'config');
var json = loadConfig(path.join(configDir, 'mocharc.json'));

it('should load configuration given absolute path', function() {
var js;

function _loadConfig() {
js = loadConfig(path.join(configDir, 'mocharc.js'));
}

expect(_loadConfig, 'not to throw');
expect(js, 'to equal', json);
});

it('should load configuration given cwd-relative path', function() {
var relConfigDir = configDir.substring(projRootDir.length + 1);
var js;

function _loadConfig() {
js = loadConfig(path.join('.', relConfigDir, 'mocharc.js'));
}

expect(_loadConfig, 'not to throw');
expect(js, 'to equal', json);
});

// In other words, path does not begin with '/', './', or '../'
describe('when path is neither absolute or relative', function() {
var nodeModulesDir = path.join(projRootDir, 'node_modules');
var pkgName = 'mocha-config';
var installedLocally = false;
var symlinkedPkg = false;

before(function() {
try {
var srcPath = path.join(configDir, pkgName);
var targetPath = path.join(nodeModulesDir, pkgName);
fs.symlinkSync(srcPath, targetPath, 'dir');
symlinkedPkg = true;
installedLocally = true;
} catch (err) {
if (err.code === 'EEXIST') {
console.log('setup:', 'package already exists in "node_modules"');
installedLocally = true;
} else {
console.error('setup failed:', err);
}
}
});

it('should load configuration given module-relative path', function() {
var js;

if (!installedLocally) {
return this.skip();
}

function _loadConfig() {
js = loadConfig(path.join(pkgName, 'index.js'));
}

expect(_loadConfig, 'not to throw');
expect(js, 'to equal', json);
});

after(function() {
if (symlinkedPkg) {
try {
fs.unlinkSync(path.join(nodeModulesDir, pkgName));
} catch (err) {
console.error('teardown failed:', err);
}
}
});
});
});
});
9 changes: 9 additions & 0 deletions test/integration/fixtures/config/mocha-config/index.js
@@ -0,0 +1,9 @@
'use strict';

// a comment
module.exports = {
require: ['foo', 'bar'],
bail: true,
reporter: 'dot',
slow: 60
};
14 changes: 14 additions & 0 deletions test/integration/fixtures/config/mocha-config/package.json
@@ -0,0 +1,14 @@
{
"name": "mocha-config",
"version": "1.0.0",
"description": "Configure Mocha via package",
"main": "index.js",
"peerDependencies": {
"mocha": "^7.0.0"
},
"keywords": [
"mocha",
"config"
],
"license": "CC0"
}
4 changes: 1 addition & 3 deletions test/node-unit/cli/config.spec.js
Expand Up @@ -82,12 +82,10 @@ describe('cli/config', function() {

describe('when supplied a filepath with unsupported extension', function() {
beforeEach(function() {
sandbox.stub(parsers, 'yaml').returns(config);
sandbox.stub(parsers, 'json').returns(config);
sandbox.stub(parsers, 'js').returns(config);
});

it('should assume JSON', function() {
it('should use the JSON parser', function() {
loadConfig('foo.bar');
expect(parsers.json, 'was called');
});
Expand Down

0 comments on commit aaf2b72

Please sign in to comment.