Skip to content

Commit

Permalink
test: use oclif/test v4 and improve test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed May 20, 2024
1 parent d062173 commit 3340c3a
Show file tree
Hide file tree
Showing 27 changed files with 428 additions and 256 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@oclif/plugin-help": "^6",
"@oclif/plugin-plugins": "^5",
"@oclif/prettier-config": "^0.2.1",
"@oclif/test": "^4.0.1-beta.4",
"@types/benchmark": "^2.1.5",
"@types/chai": "^4.3.11",
"@types/chai-as-promised": "^7.1.8",
Expand Down
2 changes: 1 addition & 1 deletion src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default class Cache extends Map<keyof CacheContents, ValueOf<CacheContent
try {
return {
name: '@oclif/core',
version: JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')),
version: JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version,
}
} catch {
return {name: '@oclif/core', version: 'unknown'}
Expand Down
3 changes: 3 additions & 0 deletions src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ export abstract class Command {
}

const config = await Config.load(opts || require.main?.filename || __dirname)
const cache = Cache.getInstance()
if (!cache.has('config')) cache.set('config', config)

const cmd = new this(argv, config)
if (!cmd.id) {
const id = cmd.constructor.name.toLowerCase()
Expand Down
71 changes: 10 additions & 61 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {makeDebug as loggerMakeDebug, setLogger} from '../logger'
import {loadWithData} from '../module-loader'
import {OCLIF_MARKER_OWNER, Performance} from '../performance'
import {settings} from '../settings'
import {determinePriority} from '../util/determine-priority'
import {safeReadJson} from '../util/fs'
import {getHomeDir, getPlatform} from '../util/os'
import {compact, isProd} from '../util/util'
Expand Down Expand Up @@ -702,64 +703,6 @@ export class Config implements IConfig {
}
}

/**
* This method is responsible for locating the correct plugin to use for a named command id
* It searches the {Config} registered commands to match either the raw command id or the command alias
* It is possible that more than one command will be found. This is due the ability of two distinct plugins to
* create the same command or command alias.
*
* In the case of more than one found command, the function will select the command based on the order in which
* the plugin is included in the package.json `oclif.plugins` list. The command that occurs first in the list
* is selected as the command to run.
*
* Commands can also be present from either an install or a link. When a command is one of these and a core plugin
* is present, this function defers to the core plugin.
*
* If there is not a core plugin command present, this function will return the first
* plugin as discovered (will not change the order)
*
* @param commands commands to determine the priority of
* @returns command instance {Command.Loadable} or undefined
*/
private determinePriority(commands: Command.Loadable[]): Command.Loadable {
const oclifPlugins = this.pjson.oclif?.plugins ?? []
const commandPlugins = commands.sort((a, b) => {
const pluginAliasA = a.pluginAlias ?? 'A-Cannot-Find-This'
const pluginAliasB = b.pluginAlias ?? 'B-Cannot-Find-This'
const aIndex = oclifPlugins.indexOf(pluginAliasA)
const bIndex = oclifPlugins.indexOf(pluginAliasB)
// When both plugin types are 'core' plugins sort based on index
if (a.pluginType === 'core' && b.pluginType === 'core') {
// If b appears first in the pjson.plugins sort it first
return aIndex - bIndex
}

// if b is a core plugin and a is not sort b first
if (b.pluginType === 'core' && a.pluginType !== 'core') {
return 1
}

// if a is a core plugin and b is not sort a first
if (a.pluginType === 'core' && b.pluginType !== 'core') {
return -1
}

// if a is a jit plugin and b is not sort b first
if (a.pluginType === 'jit' && b.pluginType !== 'jit') {
return 1
}

// if b is a jit plugin and a is not sort a first
if (b.pluginType === 'jit' && a.pluginType !== 'jit') {
return -1
}

// neither plugin is core, so do not change the order
return 0
})
return commandPlugins[0]
}

private getCmdLookupId(id: string): string {
if (this._commands.has(id)) return id
if (this.commandPermutations.hasValid(id)) return this.commandPermutations.getValid(id)!
Expand All @@ -786,7 +729,7 @@ export class Config implements IConfig {
this.plugins.set(plugin.name, plugin)

// Delete all commands from the legacy plugin so that we can re-add them.
// This is necessary because this.determinePriority will pick the initial
// This is necessary because determinePriority will pick the initial
// command that was added, which won't have been converted by PluginLegacy yet.
for (const cmd of plugin.commands ?? []) {
this._commands.delete(cmd.id)
Expand All @@ -812,7 +755,10 @@ export class Config implements IConfig {
for (const command of plugin.commands) {
// set canonical command id
if (this._commands.has(command.id)) {
const prioritizedCommand = this.determinePriority([this._commands.get(command.id)!, command])
const prioritizedCommand = determinePriority(this.pjson.oclif.plugins ?? [], [
this._commands.get(command.id)!,
command,
])
this._commands.set(prioritizedCommand.id, prioritizedCommand)
} else {
this._commands.set(command.id, command)
Expand All @@ -831,7 +777,10 @@ export class Config implements IConfig {

const handleAlias = (alias: string, hidden = false) => {
if (this._commands.has(alias)) {
const prioritizedCommand = this.determinePriority([this._commands.get(alias)!, command])
const prioritizedCommand = determinePriority(this.pjson.oclif.plugins ?? [], [
this._commands.get(alias)!,
command,
])
this._commands.set(alias, {...prioritizedCommand, id: alias})
} else {
this._commands.set(alias, {...command, hidden, id: alias})
Expand Down
58 changes: 58 additions & 0 deletions src/util/determine-priority.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {Command} from '../command'

/**
* This function is responsible for locating the correct plugin to use for a named command id
* It searches the {Config} registered commands to match either the raw command id or the command alias
* It is possible that more than one command will be found. This is due the ability of two distinct plugins to
* create the same command or command alias.
*
* In the case of more than one found command, the function will select the command based on the order in which
* the plugin is included in the package.json `oclif.plugins` list. The command that occurs first in the list
* is selected as the command to run.
*
* Commands can also be present from either an install or a link. When a command is one of these and a core plugin
* is present, this function defers to the core plugin.
*
* If there is not a core plugin command present, this function will return the first
* plugin as discovered (will not change the order)
*
* @param commands commands to determine the priority of
* @returns command instance {Command.Loadable} or undefined
*/
export function determinePriority(plugins: string[], commands: Command.Loadable[]): Command.Loadable {
const commandPlugins = commands.sort((a, b) => {
const pluginAliasA = a.pluginAlias ?? 'A-Cannot-Find-This'
const pluginAliasB = b.pluginAlias ?? 'B-Cannot-Find-This'
const aIndex = plugins.indexOf(pluginAliasA)
const bIndex = plugins.indexOf(pluginAliasB)
// When both plugin types are 'core' plugins sort based on index
if (a.pluginType === 'core' && b.pluginType === 'core') {
// If b appears first in the pjson.plugins sort it first
return aIndex - bIndex
}

// if b is a core plugin and a is not sort b first
if (b.pluginType === 'core' && a.pluginType !== 'core') {
return 1
}

// if a is a core plugin and b is not sort a first
if (a.pluginType === 'core' && b.pluginType !== 'core') {
return -1
}

// if a is a jit plugin and b is not sort b first
if (a.pluginType === 'jit' && b.pluginType !== 'jit') {
return 1
}

// if b is a jit plugin and a is not sort a first
if (b.pluginType === 'jit' && a.pluginType !== 'jit') {
return -1
}

// neither plugin is core, so do not change the order
return 0
})
return commandPlugins[0]
}
2 changes: 1 addition & 1 deletion src/ux/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function colorize(color: string | StandardAnsi | undefined, text: string)
return text
}

export function parseTheme(theme: Record<string, string>): Theme {
export function parseTheme(theme: Record<string, string | Record<string, string>>): Theme {
return Object.fromEntries(
Object.entries(theme)
.map(([key, value]) => [key, typeof value === 'string' ? isValid(value) : parseTheme(value)])
Expand Down
2 changes: 1 addition & 1 deletion test/command/command.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {captureOutput} from '@oclif/test'
import {expect} from 'chai'

import {Command as Base, Flags} from '../../src'
import {captureOutput} from '../test'

class Command extends Base {
static description = 'test command'
Expand Down
3 changes: 1 addition & 2 deletions test/command/explicit-command-strategy.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {runCommand} from '@oclif/test'
import {expect} from 'chai'
import {resolve} from 'node:path'

import {runCommand} from '../test'

const root = resolve(__dirname, 'fixtures/bundled-cli/package.json')

describe('explicit command discovery strategy', () => {
Expand Down
3 changes: 1 addition & 2 deletions test/command/main-esm.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {runCommand} from '@oclif/test'
import {expect} from 'chai'
import {resolve} from 'node:path'
import {pathToFileURL} from 'node:url'

import {runCommand} from '../test'

// This tests file URL / import.meta.url simulation.
const convertToFileURL = (filepath: string) => pathToFileURL(filepath).toString()

Expand Down
3 changes: 1 addition & 2 deletions test/command/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {runCommand} from '@oclif/test'
import {expect} from 'chai'
import {readFileSync} from 'node:fs'
import {join, resolve} from 'node:path'

import {runCommand} from '../test'

const pjson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf8'))
const version = `@oclif/core/${pjson.version} ${process.platform}-${process.arch} node-${process.version}`

Expand Down
3 changes: 1 addition & 2 deletions test/command/single-command-cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {runCommand} from '@oclif/test'
import {expect} from 'chai'
import {resolve} from 'node:path'

import {runCommand} from '../test'

describe('single command cli', () => {
it('should show help for commands', async () => {
const {stdout} = await runCommand(['--help'], {root: resolve(__dirname, 'fixtures/single-cmd-cli/package.json')})
Expand Down
2 changes: 1 addition & 1 deletion test/config/esm.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {runCommand, runHook} from '@oclif/test'
import {expect} from 'chai'
import {join, resolve} from 'node:path'
import url from 'node:url'

import {Config} from '../../src/config'
import {runCommand, runHook} from '../test'

const root = resolve(__dirname, 'fixtures/esm')

Expand Down
2 changes: 1 addition & 1 deletion test/config/mixed-cjs-esm.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {runCommand, runHook} from '@oclif/test'
import {expect} from 'chai'
import {join, resolve} from 'node:path'

import {Config} from '../../src/config'
import {runCommand, runHook} from '../test'

const root = resolve(__dirname, 'fixtures/mixed-cjs-esm')

Expand Down
2 changes: 1 addition & 1 deletion test/config/mixed-esm-cjs.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {runCommand, runHook} from '@oclif/test'
import {expect} from 'chai'
import {join, resolve} from 'node:path'

import {Config} from '../../src/config'
import {runCommand, runHook} from '../test'

const root = resolve(__dirname, 'fixtures/mixed-esm-cjs')

Expand Down
2 changes: 1 addition & 1 deletion test/config/typescript.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {runCommand, runHook} from '@oclif/test'
import {expect} from 'chai'
import {join, resolve} from 'node:path'

import {Config} from '../../src/config'
import {runCommand, runHook} from '../test'

const root = resolve(__dirname, 'fixtures/typescript')

Expand Down
3 changes: 1 addition & 2 deletions test/config/wildcard-plugins.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {runCommand} from '@oclif/test'
import {expect} from 'chai'
import {resolve} from 'node:path'

import {runCommand} from '../test'

describe('plugins defined as patterns in package.json', () => {
it('should load all core plugins in dependencies that match pattern', async () => {
const {result} = await runCommand<Array<{name: string; type: string}>>(['plugins', '--core'], {
Expand Down
2 changes: 1 addition & 1 deletion test/errors/error.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {captureOutput} from '@oclif/test'
import {expect} from 'chai'

import {error} from '../../src/errors'
import {PrettyPrintableError} from '../../src/interfaces/errors'
import {captureOutput} from '../test'

function isPrettyPrintableError(error: any): error is PrettyPrintableError {
return error.code !== undefined && error.ref !== undefined && error.suggestions !== undefined
Expand Down
38 changes: 36 additions & 2 deletions test/errors/handle.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {captureOutput} from '@oclif/test'
import {expect} from 'chai'
import process from 'node:process'
import {SinonSandbox, SinonStub, createSandbox} from 'sinon'
import {SinonSandbox, SinonStub, createSandbox, createStubInstance} from 'sinon'

import {Command, Flags} from '../../src'
// import Cache from '../../src/cache'
import {CLIError, ExitError, exit as exitErrorThrower} from '../../src/errors'
import {Exit, handle} from '../../src/errors/handle'
import {captureOutput} from '../test'
import * as Help from '../../src/help'
// import {NonExistentFlagsError} from '../../src/parser/errors'

const x = process.platform === 'win32' ? '»' : '›'

Expand Down Expand Up @@ -83,6 +87,36 @@ describe('handle', () => {
expect(exitStub.firstCall.firstArg).to.equal(9999)
})

it('should print help', async () => {
class MyCommand extends Command {
static flags = {
foo: Flags.string(),
bar: Flags.string(),
}

async run() {
await this.parse(MyCommand)
}
}

const classStubbedInstance = createStubInstance(Help.Help)
const constructorStub = sandbox.stub(Help, 'Help').returns(classStubbedInstance)
await captureOutput(async () => {
try {
await MyCommand.run(['--DOES_NOT_EXIST'])
} catch (error: any) {
await handle(error)
}
})

expect(constructorStub.calledOnce).to.be.true
const [, options] = constructorStub.firstCall.args
expect(options).to.deep.equal({
sections: ['flags', 'usage', 'arguments'],
sendToStderr: true,
})
})

describe('exit', () => {
it('exits without displaying anything', async () => {
const {stdout, stderr} = await captureOutput(async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/errors/warn.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {captureOutput} from '@oclif/test'
import {expect} from 'chai'

import {warn} from '../../src/errors'
import {captureOutput} from '../test'

describe('warn', () => {
it('warns', async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/help/help-test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ function cleanOutput(output: string) {
.join('\n')
}

export async function makeLoadable(command: Command.Class): Promise<Command.Loadable> {
export async function makeLoadable(command: Command.Class, plugin?: Interfaces.Plugin): Promise<Command.Loadable> {
return {
...(await cacheCommand(command)),
...(await cacheCommand(command, plugin)),
load: async () => command,
}
}
Expand Down
Loading

0 comments on commit 3340c3a

Please sign in to comment.