Skip to content

Commit

Permalink
Print Derived Configuration Report (bcoe#517)
Browse files Browse the repository at this point in the history
feat: print derived config variables
feat: print derived config variables in json
test: 12 new unit tests to support features
test: 1 skipped unit test for discovered bug in yargs with reports param
  • Loading branch information
mcknasty committed Feb 10, 2024
1 parent bf3073b commit 20d6557
Show file tree
Hide file tree
Showing 11 changed files with 1,287 additions and 546 deletions.
19 changes: 18 additions & 1 deletion lib/parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ const { applyExtends } = require('yargs/helpers')
const parser = require('yargs-parser')
const { resolve } = require('path')

const { printConfig } = require('./print-config.js')

function buildYargs (withCommands = false) {
const yargs = Yargs([])
.usage('$0 [opts] [script] [opts]')
Expand Down Expand Up @@ -158,8 +160,19 @@ function buildYargs (withCommands = false) {
describe: 'supplying --merge-async will merge all v8 coverage reports asynchronously and incrementally. ' +
'This is to avoid OOM issues with Node.js runtime.'
})
.options('print-config', {
default: false,
type: 'boolean',
describe: 'Print the derived configuration between command line parameters and loaded configuration file'
})
// Todo: refactor. Use parse-args options.
.options('print-config-format', {
default: 'text',
type: 'string',
choices: ['text', 'json'],
describe: 'Format to print the configuration in. Accepted formats are either text or json'
})
.pkgConf('c8')
.demandCommand(1)
.check((argv) => {
if (!argv.tempDirectory) {
argv.tempDirectory = resolve(argv.reportsDir, 'tmp')
Expand All @@ -181,6 +194,10 @@ function buildYargs (withCommands = false) {
// }
// })

printConfig(yargs, hideInstrumenteeArgs)

yargs.demandCommand(1)

const checkCoverage = require('./commands/check-coverage')
const report = require('./commands/report')
if (withCommands) {
Expand Down
289 changes: 289 additions & 0 deletions lib/print-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
const parser = require('yargs-parser')

/**
* Function: printConfig
*
* @param {Object} yargs: instance of populated yargs object.
* @param {Function} hideInstrumenteeArgs: Callback defined in lib/parse-args.
* @returns {undefined}
*
* Entry point for print config logic from lib/parse-args.js file.
* Kills process at the end of execution.
*
*/
function printConfig (yargs, hideInstrumenteeArgs) {
const argv = process.argv.slice(2)
const checkArgs = parser(argv)

let shouldPrint = false

if (Object.keys(checkArgs).includes('print-config')) {
// checkArgs['print-config'] could contain a boolean or a string
// representing a boolean.
if (typeof checkArgs['print-config'] === 'boolean') {
shouldPrint = checkArgs['print-config']
} else if (typeof checkArgs['print-config'] === 'string') {
shouldPrint = JSON.parse(checkArgs['print-config'])
}
}

if (shouldPrint) {
const args = yargs.parse(hideInstrumenteeArgs())
const cmdExecuted = 'c8 ' + argv.join(' ')
const cleanArgs = cleanUpArgumentObject(args)

if (args.printConfigFormat === 'text') {
printConfigText(cleanArgs, cmdExecuted)
} else if (checkArgs.printConfigFormat === 'json') {
const jsonYargs = JSON.stringify(cleanArgs, 2)
console.log(jsonYargs)
}

process.exit()
}
}

/**
* Function: cleanUpArgumentObject
*
* @param {Object} args: key/value pairs of configuration options
* generated by yargs.parse().
* @returns {Object} - Clone of args with duplicated data removed.
*
* This function exclude duplicate values that have different keys.
* Additionally, scrubs convenience key values.
*
* For example: args['temp-directory'] and args['tempDirectory']
* are essentially the same variable.
*
*/
function cleanUpArgumentObject (args) {
const argsToPrint = {}

const keysToIterate = Object.keys(args).filter(v => {
return (!['_', '$0'].includes(v) && v.length > 1)
})

const camelCaseKeys = keysToIterate.filter(x => {
return [...x.matchAll(/([A-Z])/g)].length > 0
})

keysToIterate.forEach(v => {
if (camelCaseKeys.includes(v)) {
// Derive Kebab Case string from Camel Case Key string
const newKey = v.replace(/([A-Z])/g, '-$1').toLowerCase()
// If the Kebab Case key is not assigned a value
if (!args[newKey]) {
// Then assigned it the Camel Case Variable
argsToPrint[newKey] = args[v]
}
} else {
// Just keep the value. Either Kebab case or otherwise
argsToPrint[v] = args[v]
}
// Not sure if we will hit this scenario
// should we throw an error?
})

return argsToPrint
}

/**
* Function: printConfigText
*
* @param {Object} argsv: sanitized configuration option object.
* @param {String} cmdExecuted: the string representing the
* command for c8 that passed to the cli.
* @returns {undefined}
*
* Todo:
* 1. Address gap with a unit test
*
*/
function printConfigText (argsv, cmdExecuted) {
// Todo: gap in branch coverage
const configFilePath = argsv instanceof Object &&
Object.keys(argsv).includes('config')
? argsv.config
: ''

const banner = printConfigBanner(cmdExecuted, configFilePath)
const table = printConfigTable(argsv)

console.log(banner)
console.log(table)
}

/**
* Function: printConfigBanner
*
* @param {String} cmdExecuted: the string representing the
* command for c8 that passed to the cli.
* @param {String} configFilePath: the absolute path to
* the configuration file that was loaded.
* @returns {String} - the banner string to print.
*
* Todo: Should I center this using the process.stdout.columns
* variable?
*/
function printConfigBanner (cmdExecuted, configFilePath) {
return String.raw`
/* ________/\\\\\\\\\_ _____/\\\\\\\\\____ */
/* _____/\\\////////__ ___/\\\///////\\\__ */
/* ___/\\\/___________ __\/\\\_____\/\\\__ */
/* __/\\\_____________ __\///\\\\\\\\\/___ */
/* _\/\\\_____________ ___/\\\///////\\\__ */
/* _\//\\\____________ __/\\\______\//\\\_ */
/* __\///\\\__________ _\//\\\______/\\\__ */
/* ____\////\\\\\\\\\_ __\///\\\\\\\\\/___ */
/* _______\/////////__ ____\/////////_____ */
Command Issued: ${cmdExecuted}
Config File Loaded: ${configFilePath}
Derived Configuration from CLI options and configuration file
------------------------------------------------------------------------------
`.replace(/\n{1} +/g, '\n')
}

/**
* Function: printConfigTable
*
* @param {Object} args: An object of config params processed by
* cleanUpArgumentObject function.
* @returns {String} - A string representing the current configuration.
*
*/
function printConfigTable (args) {
let output = ''
const headerPadding = 10
const headerColWidth = tableCalcHeaderWidth(args) + headerPadding

Object.keys(args).forEach(v => {
const headerText = v
const value = args[v]
output += printConfigTableRow(headerText, value, headerColWidth)
})

return output
}

/**
* Function: tableCalcHeaderWidth
*
* @param {Object} args: An object of config params processed by
* cleanUpArgumentObject function.
* @returns {number} - An integer representing the max length of
* all keys assigned to the args object.
*
*/
function tableCalcHeaderWidth (args) {
return Object.keys(args)
.map(v => String(v).length)
.reduce((p, c) => {
return (p >= c) ? p : c
})
}

/**
* printConfigTableRow
*
* @param {String} header: a key in the arguments object.
* @param {any} value: a value in the arguments object
* @param {any} headerColWidth: max length of keys in
* arguments object plus padding.
* @returns {any} - A rendered row of config table
*
*/
function printConfigTableRow (header, value, headerColWidth) {
const { valueMargin, headerMargin } =
tableCalcRowMargin(headerColWidth, header)

const val = formatPrintVariable(value, valueMargin)
const output = String(header) + ':' + headerMargin + val + '\n'

return output
}

/**
* tableCalcRowMargin
*
* @param {number} headerWidth: The width of the header column.
* @param {String} headerText: The value of the header column.
* @returns {OBject} - An object containing whitespace string
* padding for the value and header columns
*
*/
function tableCalcRowMargin (headerWidth, headerText) {
const whiteSpaceString = (numOfSpace) => {
let space = ''
for (let i = 0; i < numOfSpace; i++) space += ' '
return space
}

const rowHeaderLength = headerWidth - headerText.length
const rowHeaderMargin = whiteSpaceString(rowHeaderLength)

const rowValueMargin = whiteSpaceString(headerWidth)

return {
valueMargin: rowValueMargin,
headerMargin: rowHeaderMargin
}
}

/**
* Function: formatPrintVariable
*
* @param {any} variable: the variable to format.
* @param {String} space: a string containing a variable
* amount of blank spaces.
* @returns {String} - string representation of the variable.
*
*
*/
function formatPrintVariable (variable, space) {
let value

if (Array.isArray(variable) && variable.length > 0) {
value = stringifyObject(variable, space, ']')
} else if (typeof variable === 'object' && Object.keys(variable).length > 0) {
value = stringifyObject(variable, space, '}')
} else if (typeof variable === 'string' && variable) {
value = "'" + variable + "'"
} else if (typeof variable === 'string' && !variable) {
value = "''"
} else {
value = variable
}

return value
}

/**
* Function: stringifyObject
*
* @param {any} variable: the variable to format.
* @param {String} space: string containing a variable
* amount of blank spaces.
* @param {String} closingChar: single string character
* either a ']' or a '}'.
* @returns {String} - string representation of the variable.
*
*/
function stringifyObject (variable, space, closingChar) {
const closeReg = new RegExp('\n' + closingChar, 'g')
const out = JSON.stringify(variable, null, '\t\t\t\t ')
.replace(closeReg, '\n' + space + ' ' + closingChar)

return out
}

module.exports = {
printConfig,
formatPrintVariable,
cleanUpArgumentObject
}
Loading

0 comments on commit 20d6557

Please sign in to comment.