SuitestAutomation/suitest-js-api

View on GitHub
lib/testLauncher/composeConfig.js

Summary

Maintainability
B
5 hrs
Test Coverage
B
83%
// TODO: add test coverage for file

const {pickNonNil} = require('../utils/common');
const {hideOwnArgs} = require('./processArgs');
const suitest = require('../../index');
const fs = require('fs');
const SuitestError = require('../utils/SuitestError');
const {
    invalidUserConfig,
    circularDependencyError,
} = require('../texts');
const yaml = require('js-yaml');
const JSON5 = require('json5');
const ini = require('ini');
const path = require('path');

const PRESETS = 'presets';
const APP_NAME = 'suitest';
const ETC_DIR = '/etc';
const IS_WINDOWS = process.platform === 'win32';
const HOME_DIR = IS_WINDOWS
    ? process.env.USERPROFILE
    : process.env.HOME;

const CONFIG_FORMATS = [
    '.js',
    '.json',
    '.yaml',
    '.yml',
    '.json5',
    '.ini',
];

/**
 * @description default directories to search. Logic the same as in rc
 * @type {
    * {
    * path: string,
    * filename: string,
    * deepSearch: boolean,
    * isGeneral: boolean
    * } []
 * }
 */
const DEFAULT_PATHS = [
    {path: process.cwd(), filename: `.${APP_NAME}rc`, deepSearch: true, isGeneral: true},
    {path: path.join(HOME_DIR, '.config', APP_NAME), filename: 'config', deepSearch: false, isGeneral: true},
    {path: path.join(HOME_DIR, '.config'), filename: APP_NAME, deepSearch: false, isGeneral: true},
    {path: path.join(HOME_DIR, `.${APP_NAME}`), filename: 'config', deepSearch: false, isGeneral: true},
    {path: HOME_DIR, filename: `.${APP_NAME}rc`, deepSearch: false, isGeneral: true},

    {path: path.join(ETC_DIR, APP_NAME), filename: 'config', deepSearch: false, isGeneral: false},
    {path: ETC_DIR, filename: `${APP_NAME}rc`, deepSearch: false, isGeneral: false},
];

/**
 * @description Reads file from path and returns file as an object. Supports json, yaml, json5 and ini formats.
 * @param {String} filePath path to file
 */
function readConfigFile(filePath) {
    const extension = path.extname(filePath);
    if (extension === '.js') {
        if (!path.isAbsolute(filePath)) {
            // ensure correct handling of relative paths, we want to prevent searching in __dirname
            filePath = path.join(process.cwd(), filePath);
        }
        return require(filePath);
    }
    const fileContent = fs.readFileSync(filePath).toString('utf-8');

    switch (extension) {
        case '.yaml':
        case '.yml':
            return yaml.load(fileContent);
        case '.json5':
            return JSON5.parse(fileContent);
        case '.ini':
            return ini.parse(fileContent);
        case '.json':
            return JSON.parse(fileContent);
        default:
            try {
                return JSON.parse(fileContent);
            } catch (error) {
                return ini.parse(fileContent);
            }
    }
}

/**
 * @description Recursive search and merging for Extends configs
 * @param {object} defaultConfigObject main configuration file object
 * @param {String} extendPath extend path of other configuration file to merge with
 * @param {String} filePath currently in read configuration file path
 * @param {Array} foundPaths array of extends paths. Used to verify circular dependency
 */
function findExtendConfigs(defaultConfigObject, extendPath, filePath, foundPaths) {
    const pathToConfig = path.join(path.dirname(filePath), extendPath);

    if (foundPaths.includes(pathToConfig)) {
        throw new SuitestError(circularDependencyError(pathToConfig));
    }

    const additionalConfigFile = readConfigFile(pathToConfig);
    const mainConfigFile = {...additionalConfigFile};

    Object.keys(defaultConfigObject).forEach((objKey) => {
        if (objKey === PRESETS) {
            mainConfigFile.presets = {...mainConfigFile[objKey], ...defaultConfigObject[objKey]};
        } else {
            mainConfigFile[objKey] = defaultConfigObject[objKey];
        }
    });

    if ('extends' in additionalConfigFile) {
        return findExtendConfigs(
            mainConfigFile,
            additionalConfigFile.extends,
            pathToConfig,
            [...foundPaths, pathToConfig],
        );
    }

    return mainConfigFile;
}

/**
 * @description Search for configuration files.
 * @param {String} pathToSearch path to directory to search
 * @param {String} filename base filename without extension
 */
function findConfig(pathToSearch, filename) {
    if (!fs.existsSync(pathToSearch)) {
        return;
    }
    const files = fs.readdirSync(pathToSearch);
    const file = files.find(file => {
        const {name, ext} = path.parse(file);

        return name === filename && (CONFIG_FORMATS.includes(ext) || !ext);
    });

    return file ? path.join(pathToSearch, file) : undefined;
}

/**
 * @description Search for configuration files up to the root.
 * @param {String} pathToSearch path to directory to search
 * @param {String} filename base filename without extension
 */
function findConfigUpToRoot(pathToSearch, filename) {
    const foundConfigFile = findConfig(pathToSearch, filename);

    if (foundConfigFile || path.parse(pathToSearch).root === pathToSearch) {
        return foundConfigFile;
    }

    return findConfigUpToRoot(path.join(pathToSearch, '../'), filename);
}

/**
 * Read `.suitestrc` launcher config file.
 * Also, searches for default RC paths.
 * If file not found, return empty object.
 * Supports json, json5, js, yaml, yml, ini formats.
 * Searches for 'extends' property for other config file and if presents merge them.
 * cli arguments are not parsed.
 * If file found, but json invalid, throw error.
 * @param {string} [pathToConfig] expicit path to configuration file
 * @returns {Object}
 */
function readRcConfig(pathToConfig) {
    let mainConfigFilePath = '';

    if (pathToConfig) {
        mainConfigFilePath = pathToConfig;
    } else {
        const defaultConfigurations = DEFAULT_PATHS
            .filter(defaultConfig => IS_WINDOWS ? defaultConfig.isGeneral : true);

        for (const defaultConfig of defaultConfigurations) {
            if (
                fs.existsSync(defaultConfig.path) &&
                fs.lstatSync(defaultConfig.path).isDirectory()
            ) {
                mainConfigFilePath = (
                    defaultConfig.deepSearch ?
                        findConfigUpToRoot :
                        findConfig)(defaultConfig.path, defaultConfig.filename);
                if (mainConfigFilePath) {
                    break;
                }
            }
        }
    }

    if (!mainConfigFilePath) {
        return {};
    }

    const configFile = readConfigFile(mainConfigFilePath);

    if ('extends' in configFile) {
        return {
            ...findExtendConfigs(
                configFile,
                configFile.extends,
                mainConfigFilePath,
                [mainConfigFilePath],
            ),
            config: mainConfigFilePath,
        };
    }

    return {
        ...configFile,
        config: mainConfigFilePath,
    };
}

/**
 * Read config file provided by user.
 * @param {string} path - path to config file
 * @throws {SuitestError}
 * @returns {Object} - parsed config file
 */
function readUserConfig(path) {
    try {
        return readConfigFile(path);
    } catch (error) {
        throw new SuitestError(invalidUserConfig(path, error.message), error.code);
    }
}

/**
 * Compose config from rc file, user config file and cli args
 * @param {Object} argv - yargs cli args object
 * @returns {{ownArgs: Object, userCommandArgs: string[]}}
 */
const composeConfig = (argv) => {
    if (argv.configFile) {
        console.warn('Warning: You are using deprecated argument --config-file. Please use --override-config-file or --base-config-file instead.');
        if (argv.baseConfigFile || argv.overrideConfigFile) {
            throw new SuitestError('Combination of deprecated --config-file with either --base-config-file or --override-config-file is not allowed');
        }
    }
    const rcConfig = readRcConfig(argv.baseConfigFile || argv.configFile);
    if (rcConfig.configFile) {
        console.warn('Warning: You are using deprecated option configFile. Please use overrideConfigFile instead.');
    }
    if ((argv.configFile || rcConfig.configFile) && (argv.baseConfigFile || argv.overrideConfigFile || rcConfig.overrideConfigFile)) {
        throw new SuitestError('Combination of deprecated configFile with --base-config-file or --override-config-file or overrideConfigFile is not allowed');
    }
    const configFilePath = argv.overrideConfigFile || rcConfig.overrideConfigFile || argv.configFile || rcConfig.configFile;
    const userConfig = configFilePath ? readUserConfig(configFilePath) : {};

    const ownArgs = {
        ...pickNonNil(suitest.configuration.overridableFields, rcConfig),
        ...pickNonNil(suitest.configuration.overridableFields, userConfig),
        ...pickNonNil(suitest.configuration.overridableFields, argv),
    };
    const userCommandArgs = hideOwnArgs();

    return {
        ownArgs,
        userCommandArgs,
    };
};

module.exports = {
    readConfigFile,
    findExtendConfigs,
    composeConfig,
    readRcConfig,
    readUserConfig,
};