|
| 1 | +// @ts-check |
| 2 | + |
| 3 | +import yargs from 'yargs/yargs' |
| 4 | +import path from 'path' |
| 5 | +import { pathToFileURL } from 'url' |
| 6 | +import * as assert from 'assert' |
| 7 | +import { commandMap } from '../bin/commandMap.js' |
| 8 | + |
| 9 | +/** |
| 10 | + * Build unique command module file list from command map values. |
| 11 | + * @returns {string[]} |
| 12 | + */ |
| 13 | +function getUniqueCommandFiles() { |
| 14 | + return [...new Set(Object.values(commandMap))] |
| 15 | +} |
| 16 | + |
| 17 | +/** |
| 18 | + * Extract command name token from yargs command export. |
| 19 | + * @param {string} commandSignature |
| 20 | + * @returns {string} |
| 21 | + */ |
| 22 | +function getCommandToken(commandSignature) { |
| 23 | + return String(commandSignature || '').trim().split(/\s+/)[0] |
| 24 | +} |
| 25 | + |
| 26 | +describe('@all alias conflict guardrails', function () { |
| 27 | + this.timeout(20000) |
| 28 | + |
| 29 | + const knownCommandAliasConflicts = new Set([ |
| 30 | + 'changes -> ./openChangeLog.js and ./changeLog.js', |
| 31 | + 'changeLog -> ./openChangeLog.js and ./changeLog.js', |
| 32 | + 'changelog -> ./openChangeLog.js and ./changeLog.js', |
| 33 | + 'c -> ./connect.js and ./connections.js', |
| 34 | + 'dataCompare -> ./compareData.js and ./dataDiff.js', |
| 35 | + 'docs -> ./generateDocs.js and ./viewDocs.js', |
| 36 | + 'documentation -> ./helpDocu.js and ./viewDocs.js', |
| 37 | + 'iniFiles -> ./iniContents.js and ./iniFiles.js', |
| 38 | + 'if -> ./iniContents.js and ./iniFiles.js', |
| 39 | + 'inifiles -> ./iniContents.js and ./iniFiles.js', |
| 40 | + 'ini -> ./iniContents.js and ./iniFiles.js', |
| 41 | + 'if -> ./iniContents.js and ./inspectFunction.js', |
| 42 | + 'mu -> ./massUpdate.js and ./massUsers.js', |
| 43 | + 'readme -> ./readMe.js and ./openReadMe.js', |
| 44 | + 'listschemas -> ./schemas.js and ./hanaCloudSchemaInstances.js', |
| 45 | + 'listschemasui -> ./schemasUI.js and ./hanaCloudSchemaInstancesUI.js', |
| 46 | + 's -> ./schemas.js and ./status.js' |
| 47 | + ]) |
| 48 | + |
| 49 | + const knownOptionAliasConflicts = new Set([ |
| 50 | + './auditLog.js: -a used by both admin and action', |
| 51 | + './auditLog.js: -d used by both debug and days', |
| 52 | + './adminHDI.js: -p used by both profile and password', |
| 53 | + './blocking.js: -d used by both debug and details', |
| 54 | + './callProcedure.js: -p used by both procedure and profile', |
| 55 | + './connections.js: -a used by both admin and application', |
| 56 | + './createXSAAdmin.js: -p used by both profile and password', |
| 57 | + './crashDumps.js: -d used by both debug and days', |
| 58 | + './encryptionStatus.js: -d used by both debug and details', |
| 59 | + './export.js: -d used by both debug and delimiter', |
| 60 | + './ftIndexes.js: -d used by both debug and details', |
| 61 | + './grantChains.js: -d used by both debug and depth', |
| 62 | + './inspectProcedure.js: -p used by both profile and procedure', |
| 63 | + './longRunning.js: -d used by both debug and duration', |
| 64 | + './massRename.js: -p used by both profile and prefix', |
| 65 | + './massUsers.js: -p used by both profile and password', |
| 66 | + './procedures.js: -p used by both procedure and profile', |
| 67 | + './pwdPolicy.js: -p used by both profile and policy', |
| 68 | + './pwdPolicy.js: -d used by both debug and details', |
| 69 | + './replicationStatus.js: -d used by both debug and detailed', |
| 70 | + './securityScan.js: -d used by both debug and detailed', |
| 71 | + './sdiTasks.js: -a used by both admin and action', |
| 72 | + './status.js: -p used by both profile and priv', |
| 73 | + './tableHotspots.js: -p used by both includePartitions and profile', |
| 74 | + './tableGroups.js: -a used by both admin and action', |
| 75 | + './xsaServices.js: -a used by both admin and action', |
| 76 | + './xsaServices.js: -d used by both debug and details', |
| 77 | + './workloadManagement.js: -p used by both profile and priority', |
| 78 | + './workloadManagement.js: -a used by both admin and activeOnly', |
| 79 | + './kafkaConnect.js: -a used by both admin and action', |
| 80 | + './timeSeriesTools.js: -a used by both admin and action' |
| 81 | + ]) |
| 82 | + |
| 83 | + it('does not introduce new duplicate command aliases across command modules', async function () { |
| 84 | + const files = getUniqueCommandFiles() |
| 85 | + /** @type {Map<string, string>} */ |
| 86 | + const ownerByAlias = new Map() |
| 87 | + /** @type {Array<string>} */ |
| 88 | + const conflicts = [] |
| 89 | + |
| 90 | + for (const relFile of files) { |
| 91 | + const absFile = path.resolve('bin', relFile.replace(/^\.\//, '')) |
| 92 | + const mod = await import(pathToFileURL(absFile).href) |
| 93 | + |
| 94 | + const commandToken = getCommandToken(mod.command) |
| 95 | + const aliases = Array.isArray(mod.aliases) ? mod.aliases : [] |
| 96 | + const allNames = [commandToken, ...aliases].filter(Boolean) |
| 97 | + |
| 98 | + for (const name of allNames) { |
| 99 | + const key = String(name).toLowerCase() |
| 100 | + const owner = ownerByAlias.get(key) |
| 101 | + if (owner && owner !== relFile) { |
| 102 | + conflicts.push(`${name} -> ${owner} and ${relFile}`) |
| 103 | + } else if (!owner) { |
| 104 | + ownerByAlias.set(key, relFile) |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + const unexpected = conflicts.filter((c) => !knownCommandAliasConflicts.has(c)) |
| 110 | + |
| 111 | + assert.strictEqual( |
| 112 | + unexpected.length, |
| 113 | + 0, |
| 114 | + `New duplicate command aliases introduced:\n${unexpected.join('\n')}` |
| 115 | + ) |
| 116 | + }) |
| 117 | + |
| 118 | + it('does not introduce new option alias collisions within command builders', async function () { |
| 119 | + const files = getUniqueCommandFiles() |
| 120 | + /** @type {Array<string>} */ |
| 121 | + const conflicts = [] |
| 122 | + |
| 123 | + for (const relFile of files) { |
| 124 | + const absFile = path.resolve('bin', relFile.replace(/^\.\//, '')) |
| 125 | + const mod = await import(pathToFileURL(absFile).href) |
| 126 | + |
| 127 | + if (typeof mod.builder !== 'function') { |
| 128 | + continue |
| 129 | + } |
| 130 | + |
| 131 | + const parser = mod.builder( |
| 132 | + yargs([]) |
| 133 | + .help(false) |
| 134 | + .version(false) |
| 135 | + .exitProcess(false) |
| 136 | + ) |
| 137 | + |
| 138 | + const aliasMap = parser.getOptions().alias || {} |
| 139 | + /** @type {Map<string, string>} */ |
| 140 | + const aliasOwner = new Map() |
| 141 | + |
| 142 | + for (const [optionName, aliasValues] of Object.entries(aliasMap)) { |
| 143 | + const aliases = Array.isArray(aliasValues) ? aliasValues : [aliasValues] |
| 144 | + |
| 145 | + for (const alias of aliases) { |
| 146 | + const aliasText = String(alias) |
| 147 | + if (!aliasText || aliasText === optionName) { |
| 148 | + continue |
| 149 | + } |
| 150 | + |
| 151 | + const owner = aliasOwner.get(aliasText) |
| 152 | + if (owner && owner !== optionName) { |
| 153 | + conflicts.push(`${relFile}: -${aliasText} used by both ${owner} and ${optionName}`) |
| 154 | + } else if (!owner) { |
| 155 | + aliasOwner.set(aliasText, String(optionName)) |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + const unexpected = conflicts.filter((c) => !knownOptionAliasConflicts.has(c)) |
| 162 | + |
| 163 | + assert.strictEqual( |
| 164 | + unexpected.length, |
| 165 | + 0, |
| 166 | + `New option alias collisions introduced:\n${unexpected.join('\n')}` |
| 167 | + ) |
| 168 | + }) |
| 169 | +}) |
0 commit comments