hana-cli
Version:
HANA Developer Command Line Interface
170 lines (147 loc) • 6.37 kB
JavaScript
// @ts-check
import yargs from 'yargs/yargs'
import path from 'path'
import { pathToFileURL } from 'url'
import * as assert from 'assert'
import { commandMap } from '../bin/commandMap.js'
/**
* Build unique command module file list from command map values.
* @returns {string[]}
*/
function getUniqueCommandFiles() {
return [...new Set(Object.values(commandMap))]
}
/**
* Extract command name token from yargs command export.
* @param {string} commandSignature
* @returns {string}
*/
function getCommandToken(commandSignature) {
return String(commandSignature || '').trim().split(/\s+/)[0]
}
describe('@all alias conflict guardrails', function () {
this.timeout(20000)
const knownCommandAliasConflicts = new Set([
'changes -> ./openChangeLog.js and ./changeLog.js',
'changeLog -> ./openChangeLog.js and ./changeLog.js',
'changelog -> ./openChangeLog.js and ./changeLog.js',
'c -> ./connect.js and ./connections.js',
'dataCompare -> ./compareData.js and ./dataDiff.js',
'docs -> ./generateDocs.js and ./viewDocs.js',
'documentation -> ./helpDocu.js and ./viewDocs.js',
'iniFiles -> ./iniContents.js and ./iniFiles.js',
'if -> ./iniContents.js and ./iniFiles.js',
'inifiles -> ./iniContents.js and ./iniFiles.js',
'ini -> ./iniContents.js and ./iniFiles.js',
'if -> ./iniContents.js and ./inspectFunction.js',
'mu -> ./massUpdate.js and ./massUsers.js',
'readme -> ./readMe.js and ./openReadMe.js',
'listschemas -> ./schemas.js and ./hanaCloudSchemaInstances.js',
'listschemasui -> ./schemasUI.js and ./hanaCloudSchemaInstancesUI.js',
's -> ./schemas.js and ./status.js'
])
const knownOptionAliasConflicts = new Set([
'./auditLog.js: -a used by both admin and action',
'./auditLog.js: -d used by both debug and days',
'./adminHDI.js: -p used by both profile and password',
'./blocking.js: -d used by both debug and details',
'./callProcedure.js: -p used by both procedure and profile',
'./connections.js: -a used by both admin and application',
'./createXSAAdmin.js: -p used by both profile and password',
'./crashDumps.js: -d used by both debug and days',
'./encryptionStatus.js: -d used by both debug and details',
'./export.js: -d used by both debug and delimiter',
'./ftIndexes.js: -d used by both debug and details',
'./grantChains.js: -d used by both debug and depth',
'./inspectProcedure.js: -p used by both profile and procedure',
'./longRunning.js: -d used by both debug and duration',
'./massRename.js: -p used by both profile and prefix',
'./massUsers.js: -p used by both profile and password',
'./procedures.js: -p used by both procedure and profile',
'./pwdPolicy.js: -p used by both profile and policy',
'./pwdPolicy.js: -d used by both debug and details',
'./replicationStatus.js: -d used by both debug and detailed',
'./securityScan.js: -d used by both debug and detailed',
'./sdiTasks.js: -a used by both admin and action',
'./status.js: -p used by both profile and priv',
'./tableHotspots.js: -p used by both includePartitions and profile',
'./tableGroups.js: -a used by both admin and action',
'./xsaServices.js: -a used by both admin and action',
'./xsaServices.js: -d used by both debug and details',
'./workloadManagement.js: -p used by both profile and priority',
'./workloadManagement.js: -a used by both admin and activeOnly',
'./kafkaConnect.js: -a used by both admin and action',
'./timeSeriesTools.js: -a used by both admin and action'
])
it('does not introduce new duplicate command aliases across command modules', async function () {
const files = getUniqueCommandFiles()
/** @type {Map<string, string>} */
const ownerByAlias = new Map()
/** @type {Array<string>} */
const conflicts = []
for (const relFile of files) {
const absFile = path.resolve('bin', relFile.replace(/^\.\//, ''))
const mod = await import(pathToFileURL(absFile).href)
const commandToken = getCommandToken(mod.command)
const aliases = Array.isArray(mod.aliases) ? mod.aliases : []
const allNames = [commandToken, ...aliases].filter(Boolean)
for (const name of allNames) {
const key = String(name).toLowerCase()
const owner = ownerByAlias.get(key)
if (owner && owner !== relFile) {
conflicts.push(`${name} -> ${owner} and ${relFile}`)
} else if (!owner) {
ownerByAlias.set(key, relFile)
}
}
}
const unexpected = conflicts.filter((c) => !knownCommandAliasConflicts.has(c))
assert.strictEqual(
unexpected.length,
0,
`New duplicate command aliases introduced:\n${unexpected.join('\n')}`
)
})
it('does not introduce new option alias collisions within command builders', async function () {
const files = getUniqueCommandFiles()
/** @type {Array<string>} */
const conflicts = []
for (const relFile of files) {
const absFile = path.resolve('bin', relFile.replace(/^\.\//, ''))
const mod = await import(pathToFileURL(absFile).href)
if (typeof mod.builder !== 'function') {
continue
}
const parser = mod.builder(
yargs([])
.help(false)
.version(false)
.exitProcess(false)
)
const aliasMap = parser.getOptions().alias || {}
/** @type {Map<string, string>} */
const aliasOwner = new Map()
for (const [optionName, aliasValues] of Object.entries(aliasMap)) {
const aliases = Array.isArray(aliasValues) ? aliasValues : [aliasValues]
for (const alias of aliases) {
const aliasText = String(alias)
if (!aliasText || aliasText === optionName) {
continue
}
const owner = aliasOwner.get(aliasText)
if (owner && owner !== optionName) {
conflicts.push(`${relFile}: -${aliasText} used by both ${owner} and ${optionName}`)
} else if (!owner) {
aliasOwner.set(aliasText, String(optionName))
}
}
}
}
const unexpected = conflicts.filter((c) => !knownOptionAliasConflicts.has(c))
assert.strictEqual(
unexpected.length,
0,
`New option alias collisions introduced:\n${unexpected.join('\n')}`
)
})
})