From 53e2d806d313a40c03cde2fae9a176c54207e122 Mon Sep 17 00:00:00 2001 From: Red-Asuka Date: Fri, 15 Nov 2024 10:45:23 +0800 Subject: [PATCH 1/5] improve(cli): enhance handleLoadOptions to accept existing options --- cli/src/lib/conn.ts | 4 ++-- cli/src/lib/pub.ts | 6 +++--- cli/src/lib/sub.ts | 4 ++-- cli/src/types/global.d.ts | 9 +++++++++ cli/src/utils/options.ts | 34 +++++++++++++++++++++++++--------- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/cli/src/lib/conn.ts b/cli/src/lib/conn.ts index 9b95b3c1c..b60b07cfc 100644 --- a/cli/src/lib/conn.ts +++ b/cli/src/lib/conn.ts @@ -8,7 +8,7 @@ import * as Debug from 'debug' const conn = (options: ConnectOptions) => { const { debug, saveOptions, loadOptions } = options - loadOptions && (options = handleLoadOptions('conn', loadOptions)) + loadOptions && (options = handleLoadOptions('conn', loadOptions, options)) saveOptions && handleSaveOptions('conn', options) @@ -57,7 +57,7 @@ const conn = (options: ConnectOptions) => { const benchConn = async (options: BenchConnectOptions) => { const { saveOptions, loadOptions } = options - loadOptions && (options = handleLoadOptions('benchConn', loadOptions)) + loadOptions && (options = handleLoadOptions('benchConn', loadOptions, options)) saveOptions && handleSaveOptions('benchConn', options) diff --git a/cli/src/lib/pub.ts b/cli/src/lib/pub.ts index 82334ab19..1511a18c3 100644 --- a/cli/src/lib/pub.ts +++ b/cli/src/lib/pub.ts @@ -211,7 +211,7 @@ const handleFileRead = (filePath: string) => { const pub = (options: PublishOptions) => { const { debug, saveOptions, loadOptions } = options - loadOptions && (options = handleLoadOptions('pub', loadOptions)) + loadOptions && (options = handleLoadOptions('pub', loadOptions, options)) saveOptions && handleSaveOptions('pub', options) @@ -252,14 +252,14 @@ const multiPub = async (commandType: CommandType, options: BenchPublishOptions | let simulator: Simulator = {} as Simulator if (commandType === 'simulate') { - options = loadOptions ? handleLoadOptions('simulate', loadOptions) : options + options = loadOptions ? handleLoadOptions('simulate', loadOptions, options as SimulatePubOptions) : options saveOptions && handleSaveOptions('simulate', options) const simulateOptions = options as SimulatePubOptions checkScenarioExists(simulateOptions.scenario, simulateOptions.file) simulator = loadSimulator(simulateOptions.scenario, simulateOptions.file) } else { - options = loadOptions ? handleLoadOptions('benchPub', loadOptions) : options + options = loadOptions ? handleLoadOptions('benchPub', loadOptions, options as BenchPublishOptions) : options saveOptions && handleSaveOptions('benchPub', options) } diff --git a/cli/src/lib/sub.ts b/cli/src/lib/sub.ts index f740888b3..ee968c731 100644 --- a/cli/src/lib/sub.ts +++ b/cli/src/lib/sub.ts @@ -79,7 +79,7 @@ const handleDefaultBinaryFile = (format: FormatType | undefined, filePath?: stri const sub = (options: SubscribeOptions) => { const { loadOptions, saveOptions } = options - loadOptions && (options = handleLoadOptions('sub', loadOptions)) + loadOptions && (options = handleLoadOptions('sub', loadOptions, options)) saveOptions && handleSaveOptions('sub', options) @@ -232,7 +232,7 @@ const sub = (options: SubscribeOptions) => { const benchSub = async (options: BenchSubscribeOptions) => { const { saveOptions, loadOptions } = options - loadOptions && (options = handleLoadOptions('benchSub', loadOptions)) + loadOptions && (options = handleLoadOptions('benchSub', loadOptions, options)) saveOptions && handleSaveOptions('benchSub', options) diff --git a/cli/src/types/global.d.ts b/cli/src/types/global.d.ts index 7afb5db73..c657163cc 100644 --- a/cli/src/types/global.d.ts +++ b/cli/src/types/global.d.ts @@ -1,6 +1,15 @@ declare global { type CommandType = 'conn' | 'pub' | 'sub' | 'benchConn' | 'benchPub' | 'benchSub' | 'simulate' + type OptionsType = + | ConnectOptions + | PublishOptions + | SubscribeOptions + | BenchConnectOptions + | BenchPublishOptions + | BenchSubscribeOptions + | SimulatePubOptions + type MQTTVersion = 3 | 4 | 5 type Protocol = 'mqtt' | 'mqtts' | 'ws' | 'wss' diff --git a/cli/src/utils/options.ts b/cli/src/utils/options.ts index 43b3c446c..3f0356fb7 100644 --- a/cli/src/utils/options.ts +++ b/cli/src/utils/options.ts @@ -80,21 +80,37 @@ const handleSaveOptions = ( * @param savePath - The path to the configuration file. * @returns The options for the specified command type. */ -function handleLoadOptions(commandType: 'conn', savePath: boolean | string): ConnectOptions -function handleLoadOptions(commandType: 'pub', savePath: boolean | string): PublishOptions -function handleLoadOptions(commandType: 'sub', savePath: boolean | string): SubscribeOptions -function handleLoadOptions(commandType: 'benchConn', savePath: boolean | string): BenchConnectOptions -function handleLoadOptions(commandType: 'benchPub', savePath: boolean | string): BenchPublishOptions -function handleLoadOptions(commandType: 'benchSub', savePath: boolean | string): BenchSubscribeOptions -function handleLoadOptions(commandType: 'simulate', savePath: boolean | string): SimulatePubOptions -function handleLoadOptions(commandType: CommandType, savePath: boolean | string) { +function handleLoadOptions(commandType: 'conn', savePath: boolean | string, opts: ConnectOptions): ConnectOptions +function handleLoadOptions(commandType: 'pub', savePath: boolean | string, opts: PublishOptions): PublishOptions +function handleLoadOptions(commandType: 'sub', savePath: boolean | string, opts: SubscribeOptions): SubscribeOptions +function handleLoadOptions( + commandType: 'benchConn', + savePath: boolean | string, + opts: BenchConnectOptions, +): BenchConnectOptions +function handleLoadOptions( + commandType: 'benchPub', + savePath: boolean | string, + opts: BenchPublishOptions, +): BenchPublishOptions +function handleLoadOptions( + commandType: 'benchSub', + savePath: boolean | string, + opts: BenchSubscribeOptions, +): BenchSubscribeOptions +function handleLoadOptions( + commandType: 'simulate', + savePath: boolean | string, + opts: SimulatePubOptions, +): SimulatePubOptions +function handleLoadOptions(commandType: CommandType, savePath: boolean | string, opts: OptionsType) { try { const filePath = processPath(savePath, defaultPath) if (fileExists(filePath)) { const data = readFile(filePath).toString() const config = parseYamlOrJson(data, isYaml(filePath)) validateOptions(commandType, filePath, config) - return config[commandType] + return { ...config[commandType], ...opts } } else { logWrapper.fail(`Configuration file ${filePath} not found`) process.exit(1) From bb5fae013b569ac89fa06b727c947981c583c129 Mon Sep 17 00:00:00 2001 From: Red-Asuka Date: Fri, 15 Nov 2024 15:13:09 +0800 Subject: [PATCH 2/5] feat(cli): add preAction hook to commands and filter options based on source --- cli/src/index.ts | 30 ++++++++++++++++++++++++++++++ cli/src/types/global.d.ts | 4 ++++ cli/src/utils/options.ts | 18 ++++++++++++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/cli/src/index.ts b/cli/src/index.ts index 37cd25ba5..e0652682b 100755 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -43,6 +43,9 @@ export class Commander { .command('check') .description('Check for updates.') .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(async () => { await checkUpdate() }) @@ -51,6 +54,9 @@ export class Commander { .command('init') .description('Initialize the configuration file.') .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(initConfig) this.program @@ -128,6 +134,9 @@ export class Commander { ) .option('--debug', 'enable debug mode for MQTT.js', false) .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(conn) this.program @@ -252,6 +261,9 @@ export class Commander { ) .option('--debug', 'enable debug mode for MQTT.js', false) .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(pub) this.program @@ -383,6 +395,9 @@ export class Commander { ) .option('--debug', 'enable debug mode for MQTT.js', false) .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(sub) const benchCmd = this.program.command('bench').description('MQTT Benchmark in performance testing.') @@ -463,6 +478,9 @@ export class Commander { 'load the parameters from the local configuration file, which supports json and yaml format, default path is ./mqttx-cli-options.json', ) .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(benchConn) benchCmd @@ -584,6 +602,9 @@ export class Commander { 'split the input message in a single file by a specified character, default is "\\n"', ) .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(benchPub) benchCmd @@ -683,6 +704,9 @@ export class Commander { 'load the parameters from the local configuration file, which supports json and yaml format, default path is ./mqttx-cli-options.json', ) .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(benchSub) this.program @@ -796,6 +820,9 @@ export class Commander { 'load the parameters from the local configuration file, which supports json and yaml format, default path is ./mqttx-cli-options.json', ) .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(simulatePub) this.program @@ -803,6 +830,9 @@ export class Commander { .description('List information based on the provided options.') .option('-sc, --scenarios', 'List all built-in scenarios') .allowUnknownOption(false) + .hook('preAction', (thisCommand) => { + globalThis.command = thisCommand + }) .action(ls) } } diff --git a/cli/src/types/global.d.ts b/cli/src/types/global.d.ts index c657163cc..bd89c769d 100644 --- a/cli/src/types/global.d.ts +++ b/cli/src/types/global.d.ts @@ -1,4 +1,8 @@ +import { Command } from 'commander' + declare global { + var command: Command + type CommandType = 'conn' | 'pub' | 'sub' | 'benchConn' | 'benchPub' | 'benchSub' | 'simulate' type OptionsType = diff --git a/cli/src/utils/options.ts b/cli/src/utils/options.ts index 3f0356fb7..f85c35eb5 100644 --- a/cli/src/utils/options.ts +++ b/cli/src/utils/options.ts @@ -1,3 +1,4 @@ +import type { OptionValueSource } from 'commander' import { fileExists, writeFile, @@ -26,6 +27,18 @@ const removeUselessOptions = ( return rest } +/** + * Filters the options based on their source. + * @param source - The source of the option values. + * @param opts - The options to be filtered. + * @returns The filtered options. + */ +const filterOptions = (source: OptionValueSource, opts: T): Partial => { + return Object.fromEntries( + Object.entries(opts).filter(([key]) => globalThis.command.getOptionValueSource(key) === 'cli'), + ) as Partial +} + /** * Validates the options for a specific command type. * @param commandType - The type of command. @@ -75,9 +88,10 @@ const handleSaveOptions = ( } /** - * Handles loading options from a configuration file based on the command type. + * Load the configuration file according to the command type and save path, and merge the options of the current command. * @param commandType - The type of command. * @param savePath - The path to the configuration file. + * @param opts - The options of the current command. * @returns The options for the specified command type. */ function handleLoadOptions(commandType: 'conn', savePath: boolean | string, opts: ConnectOptions): ConnectOptions @@ -110,7 +124,7 @@ function handleLoadOptions(commandType: CommandType, savePath: boolean | string, const data = readFile(filePath).toString() const config = parseYamlOrJson(data, isYaml(filePath)) validateOptions(commandType, filePath, config) - return { ...config[commandType], ...opts } + return { ...config[commandType], ...filterOptions('cli', opts) } } else { logWrapper.fail(`Configuration file ${filePath} not found`) process.exit(1) From 83b5f6645b37f2c12daf5c815bdbf4c9cf93df13 Mon Sep 17 00:00:00 2001 From: Red-Asuka Date: Fri, 15 Nov 2024 15:16:03 +0800 Subject: [PATCH 3/5] feat(cli): add unit tests for save/load options --- cli/src/__tests__/utils/options.test.ts | 188 ++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 cli/src/__tests__/utils/options.test.ts diff --git a/cli/src/__tests__/utils/options.test.ts b/cli/src/__tests__/utils/options.test.ts new file mode 100644 index 000000000..986ba484b --- /dev/null +++ b/cli/src/__tests__/utils/options.test.ts @@ -0,0 +1,188 @@ +/// + +import { handleSaveOptions, handleLoadOptions } from '../../utils/options' +import { existsSync, unlinkSync } from 'fs' +import { join } from 'path' +import { expect, afterAll, describe, it } from '@jest/globals' + +const testFilePath = join(__dirname, 'test-options.json') +const defaultPath = join(process.cwd(), 'mqttx-cli-options.json') + +describe('options', () => { + afterAll(() => { + if (existsSync(testFilePath)) { + unlinkSync(testFilePath) + } + if (existsSync(defaultPath)) { + unlinkSync(defaultPath) + } + }) + + it('should save and load options for command type "conn" with default path', () => { + const options: ConnectOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + saveOptions: true, + } + + handleSaveOptions('conn', options) + const loadedOptions = handleLoadOptions('conn', true, {} as ConnectOptions) + expect(loadedOptions).toMatchObject(options as unknown as Record) + }) + + it('should save and load options for command type "pub" with custom path', () => { + const options: PublishOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + topic: 'test/topic', + message: 'Hello, MQTT!', + qos: 1, + saveOptions: testFilePath, + } + + handleSaveOptions('pub', options) + const loadedOptions = handleLoadOptions('pub', testFilePath, {} as PublishOptions) + expect(loadedOptions).toMatchObject(options as unknown as Record) + + const userOptions: PublishOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + topic: 'user/topic', + message: 'User message', + qos: 1, + saveOptions: testFilePath, + } + + const overriddenOptions = handleLoadOptions('pub', testFilePath, userOptions) + expect(overriddenOptions.topic).toEqual('user/topic') + expect(overriddenOptions.message).toEqual('User message') + }) + + it('should save and load options for command type "sub" with default path', () => { + const options: SubscribeOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + topic: ['test/topic'], + qos: [1], + saveOptions: true, + verbose: true, + } + + handleSaveOptions('sub', options) + const loadedOptions = handleLoadOptions('sub', true, {} as SubscribeOptions) + expect(loadedOptions).toMatchObject(options as unknown as Record) + }) + + it('should save and load options for command type "benchConn" with custom path', () => { + const options: BenchConnectOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + count: 100, + interval: 1000, + saveOptions: testFilePath, + } + + handleSaveOptions('benchConn', options) + const loadedOptions = handleLoadOptions('benchConn', testFilePath, {} as BenchConnectOptions) + expect(loadedOptions).toMatchObject(options as unknown as Record) + }) + + it('should save and load options for command type "benchPub" with default path', () => { + const options: BenchPublishOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + topic: 'test/topic', + message: 'Hello, MQTT!', + qos: 1, + count: 100, + interval: 1000, + messageInterval: 500, + limit: 1000, + verbose: true, + saveOptions: true, + } + + handleSaveOptions('benchPub', options) + const loadedOptions = handleLoadOptions('benchPub', true, {} as BenchPublishOptions) + expect(loadedOptions).toMatchObject(options as unknown as Record) + }) + + it('should save and load options for command type "benchSub" with custom path', () => { + const options: BenchSubscribeOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + topic: ['test/topic'], + qos: [1], + count: 100, + interval: 1000, + saveOptions: testFilePath, + verbose: true, + } + + handleSaveOptions('benchSub', options) + const loadedOptions = handleLoadOptions('benchSub', testFilePath, {} as BenchSubscribeOptions) + expect(loadedOptions).toMatchObject(options as unknown as Record) + }) + + it('should save and load options for command type "simulate" with default path', () => { + const options: SimulatePubOptions = { + mqttVersion: 5, + hostname: 'localhost', + clientId: 'testClient', + clean: true, + keepalive: 60, + reconnectPeriod: 1000, + maximumReconnectTimes: 10, + topic: 'test/topic', + message: 'Hello, MQTT!', + qos: 1, + count: 100, + interval: 1000, + messageInterval: 500, + limit: 1000, + verbose: true, + scenario: 'testScenario', + file: 'testFile', + saveOptions: true, + } + + handleSaveOptions('simulate', options) + const loadedOptions = handleLoadOptions('simulate', true, {} as SimulatePubOptions) + expect(loadedOptions).toMatchObject(options as unknown as Record) + }) +}) From 9b3e9cfbd26f5ad1ba4fc2abc4584a2e7d8ff20e Mon Sep 17 00:00:00 2001 From: Red-Asuka Date: Fri, 15 Nov 2024 15:27:36 +0800 Subject: [PATCH 4/5] fix(cli): update option tests to exclude save/load properties --- cli/src/__tests__/utils/options.test.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cli/src/__tests__/utils/options.test.ts b/cli/src/__tests__/utils/options.test.ts index 986ba484b..88175f4a7 100644 --- a/cli/src/__tests__/utils/options.test.ts +++ b/cli/src/__tests__/utils/options.test.ts @@ -32,7 +32,8 @@ describe('options', () => { handleSaveOptions('conn', options) const loadedOptions = handleLoadOptions('conn', true, {} as ConnectOptions) - expect(loadedOptions).toMatchObject(options as unknown as Record) + const { saveOptions, loadOptions, ...expectedOptions } = options + expect(loadedOptions).toMatchObject(expectedOptions as unknown as Record) }) it('should save and load options for command type "pub" with custom path', () => { @@ -52,7 +53,8 @@ describe('options', () => { handleSaveOptions('pub', options) const loadedOptions = handleLoadOptions('pub', testFilePath, {} as PublishOptions) - expect(loadedOptions).toMatchObject(options as unknown as Record) + const { saveOptions, loadOptions, ...expectedOptions } = options + expect(loadedOptions).toMatchObject(expectedOptions as unknown as Record) const userOptions: PublishOptions = { mqttVersion: 5, @@ -90,7 +92,8 @@ describe('options', () => { handleSaveOptions('sub', options) const loadedOptions = handleLoadOptions('sub', true, {} as SubscribeOptions) - expect(loadedOptions).toMatchObject(options as unknown as Record) + const { saveOptions, loadOptions, ...expectedOptions } = options + expect(loadedOptions).toMatchObject(expectedOptions as unknown as Record) }) it('should save and load options for command type "benchConn" with custom path', () => { @@ -109,7 +112,8 @@ describe('options', () => { handleSaveOptions('benchConn', options) const loadedOptions = handleLoadOptions('benchConn', testFilePath, {} as BenchConnectOptions) - expect(loadedOptions).toMatchObject(options as unknown as Record) + const { saveOptions, loadOptions, ...expectedOptions } = options + expect(loadedOptions).toMatchObject(expectedOptions as unknown as Record) }) it('should save and load options for command type "benchPub" with default path', () => { @@ -134,7 +138,8 @@ describe('options', () => { handleSaveOptions('benchPub', options) const loadedOptions = handleLoadOptions('benchPub', true, {} as BenchPublishOptions) - expect(loadedOptions).toMatchObject(options as unknown as Record) + const { saveOptions, loadOptions, ...expectedOptions } = options + expect(loadedOptions).toMatchObject(expectedOptions as unknown as Record) }) it('should save and load options for command type "benchSub" with custom path', () => { @@ -156,7 +161,8 @@ describe('options', () => { handleSaveOptions('benchSub', options) const loadedOptions = handleLoadOptions('benchSub', testFilePath, {} as BenchSubscribeOptions) - expect(loadedOptions).toMatchObject(options as unknown as Record) + const { saveOptions, loadOptions, ...expectedOptions } = options + expect(loadedOptions).toMatchObject(expectedOptions as unknown as Record) }) it('should save and load options for command type "simulate" with default path', () => { @@ -183,6 +189,7 @@ describe('options', () => { handleSaveOptions('simulate', options) const loadedOptions = handleLoadOptions('simulate', true, {} as SimulatePubOptions) - expect(loadedOptions).toMatchObject(options as unknown as Record) + const { saveOptions, loadOptions, ...expectedOptions } = options + expect(loadedOptions).toMatchObject(expectedOptions as unknown as Record) }) }) From 912e553b85bbf115946687d4a1f184b68e9c0e57 Mon Sep 17 00:00:00 2001 From: Red-Asuka Date: Fri, 15 Nov 2024 16:06:30 +0800 Subject: [PATCH 5/5] feat(cli): add global command mock for tests and enhance filterOptions to use dynamic source --- cli/src/__tests__/utils/options.test.ts | 8 +++++++- cli/src/utils/options.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cli/src/__tests__/utils/options.test.ts b/cli/src/__tests__/utils/options.test.ts index 88175f4a7..f980c64c9 100644 --- a/cli/src/__tests__/utils/options.test.ts +++ b/cli/src/__tests__/utils/options.test.ts @@ -3,12 +3,18 @@ import { handleSaveOptions, handleLoadOptions } from '../../utils/options' import { existsSync, unlinkSync } from 'fs' import { join } from 'path' -import { expect, afterAll, describe, it } from '@jest/globals' +import { expect, afterAll, describe, it, jest, beforeAll } from '@jest/globals' const testFilePath = join(__dirname, 'test-options.json') const defaultPath = join(process.cwd(), 'mqttx-cli-options.json') describe('options', () => { + beforeAll(() => { + global.command = { + getOptionValueSource: jest.fn().mockReturnValue('cli'), + } as any + }) + afterAll(() => { if (existsSync(testFilePath)) { unlinkSync(testFilePath) diff --git a/cli/src/utils/options.ts b/cli/src/utils/options.ts index f85c35eb5..996216975 100644 --- a/cli/src/utils/options.ts +++ b/cli/src/utils/options.ts @@ -35,7 +35,7 @@ const removeUselessOptions = ( */ const filterOptions = (source: OptionValueSource, opts: T): Partial => { return Object.fromEntries( - Object.entries(opts).filter(([key]) => globalThis.command.getOptionValueSource(key) === 'cli'), + Object.entries(opts).filter(([key]) => globalThis.command.getOptionValueSource(key) === source), ) as Partial }