Skip to content

Commit e041a07

Browse files
authored
Merge pull request #8 from luke0408/feat/test-argument-parser
Implementation of `file-specific` unit testing capabilities
2 parents 5871b22 + a422f2c commit e041a07

File tree

5 files changed

+232
-22
lines changed

5 files changed

+232
-22
lines changed

package-lock.json

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"license": "ISC",
4949
"devDependencies": {
5050
"@samchon/shopping-api": "^0.14.1",
51+
"@types/inquirer": "^9.0.7",
5152
"@types/node": "^22.10.10",
5253
"prettier": "^3.4.2",
5354
"rimraf": "^6.0.1",

test/helpers/ArgumentParser.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import commander from 'commander';
2+
import * as inquirer from 'inquirer';
3+
4+
/**
5+
* A namespace for handling command-line argument parsing and interactive prompts.
6+
*/
7+
export namespace ArgumentParser {
8+
/**
9+
* A type representing an inquirer function that interacts with the user.
10+
*
11+
* @template T - The type of the options object to be returned.
12+
* @param command - The commander command instance.
13+
* @param prompt - Function to create a prompt module.
14+
* @param action - Function to execute the action.
15+
*/
16+
export type Inquirer<T> = (
17+
command: commander.Command,
18+
prompt: (opt?: inquirer.SeparatorOptions) => inquirer.PromptModule,
19+
action: (closure: (options: Partial<T>) => Promise<T>) => Promise<T>,
20+
) => Promise<T>;
21+
22+
/**
23+
* Interface defining the structure of the prompt utility functions.
24+
*/
25+
export interface Prompt {
26+
select: (name: string) => (message: string) => <Choice extends string>(choices: Choice[]) => Promise<Choice>;
27+
boolean: (name: string) => (message: string) => Promise<boolean>;
28+
number: (name: string) => (message: string, init?: number) => Promise<number>;
29+
}
30+
31+
/**
32+
* Parses command-line arguments and interacts with the user using prompts.
33+
*
34+
* @template T - The type of the options object to be returned.
35+
* @param inquiry - A function that defines the interaction logic.
36+
* @returns A promise resolving to the options object of type T.
37+
*/
38+
export const parse = async <T>(
39+
inquiry: (
40+
commad: commander.Command,
41+
prompt: Prompt,
42+
action: (closure: (options: Partial<T>) => Promise<T>) => Promise<T>,
43+
) => Promise<T>,
44+
): Promise<T> => {
45+
/**
46+
* Wraps the action logic in a promise to handle command execution.
47+
*
48+
* @param closure - A function that processes the options and returns a promise.
49+
* @returns A promise resolving to the options object of type T.
50+
*/
51+
const action = (closure: (options: Partial<T>) => Promise<T>) =>
52+
new Promise<T>((resolve, reject) => {
53+
commander.program.action(async (options) => {
54+
try {
55+
resolve(await closure(options));
56+
} catch (exp) {
57+
reject(exp);
58+
}
59+
});
60+
commander.program.parseAsync().catch(reject);
61+
});
62+
63+
/**
64+
* Creates a select prompt for choosing from a list of options.
65+
*
66+
* @param name - The name of the prompt.
67+
* @param message - The message to display to the user.
68+
* @returns A function that takes choices and returns a promise resolving to the selected choice.
69+
*/
70+
const select =
71+
(name: string) =>
72+
(message: string) =>
73+
async <Choice extends string>(choices: Choice[]) =>
74+
(
75+
await inquirer.createPromptModule()({
76+
type: 'list',
77+
name,
78+
message,
79+
choices,
80+
})
81+
)[name];
82+
83+
/**
84+
* Creates a boolean prompt for yes/no questions.
85+
*
86+
* @param name - The name of the prompt.
87+
* @returns A function that takes a message and returns a promise resolving to a boolean.
88+
*/
89+
const boolean = (name: string) => async (message: string) =>
90+
(
91+
await inquirer.createPromptModule()({
92+
type: 'confirm',
93+
name,
94+
message,
95+
})
96+
)[name] as boolean;
97+
98+
/**
99+
* Creates a number prompt for numeric input.
100+
*
101+
* @param name - The name of the prompt.
102+
* @returns A function that takes a message and an optional initial value, returning a promise resolving to a number.
103+
*/
104+
const number = (name: string) => async (message: string, init?: number) => {
105+
const value = Number(
106+
(
107+
await inquirer.createPromptModule()({
108+
type: 'number',
109+
name,
110+
message,
111+
})
112+
)[name],
113+
);
114+
return init !== undefined && isNaN(value) ? init : value;
115+
};
116+
117+
const output: T | Error = await (async () => {
118+
try {
119+
return await inquiry(commander.program, { select, boolean, number }, action);
120+
} catch (error) {
121+
return error as Error;
122+
}
123+
})();
124+
125+
if (output instanceof Error) throw output;
126+
return output;
127+
};
128+
}

test/helpers/TestFileLoader.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
export namespace TestFileLoader {
5+
export type FilterOption = { exclude?: string[]; include?: string[] };
6+
7+
const filter = (files: string[], options: FilterOption): string[] => {
8+
return files.filter((file) => {
9+
if (options.include) return isIncluded();
10+
if (options.exclude) return !isExcluded();
11+
return true;
12+
13+
function isExcluded() {
14+
return options.exclude?.some((exPath) => file.split(__dirname).at(0)?.includes(exPath));
15+
}
16+
17+
function isIncluded() {
18+
return options.include?.some((incPath) => file.split(__dirname).at(0)?.includes(incPath));
19+
}
20+
});
21+
};
22+
23+
const traverse = (dir: string, files: string[]) => {
24+
const entries = fs.readdirSync(dir, { withFileTypes: true });
25+
26+
for (const entry of entries) {
27+
if (isIgnoredEntry(entry)) continue;
28+
29+
const fullPath = path.join(dir, entry.name);
30+
31+
if (isTestDirectory(entry)) {
32+
traverse(fullPath, files);
33+
} else if (isTestFile(entry)) {
34+
files.push(fullPath);
35+
}
36+
37+
function isIgnoredEntry(entry: fs.Dirent): boolean {
38+
return entry.name.startsWith('.');
39+
}
40+
41+
function isTestFile(entry: fs.Dirent): boolean {
42+
return entry.isFile() && entry.name.endsWith('.js') && !entry.parentPath.endsWith('test');
43+
}
44+
45+
function isTestDirectory(entry: fs.Dirent): boolean {
46+
return entry.isDirectory() && entry.name !== 'helpers';
47+
}
48+
}
49+
};
50+
51+
export const load = (dir: string, options: FilterOption): string[] => {
52+
const files: string[] = [];
53+
const currentDir = dir;
54+
traverse(currentDir, files);
55+
return filter(files, options);
56+
};
57+
}

test/index.ts

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
1-
import fs from 'fs';
2-
import path from 'path';
1+
import { ArgumentParser } from './helpers/ArgumentParser';
2+
import { TestFileLoader } from './helpers/TestFileLoader';
33

4-
async function main() {
5-
/**
6-
* test code for types
7-
*/
8-
const types = fs.readdirSync(path.join(__dirname, './types'));
9-
for await (const filename of types) {
10-
if (filename.endsWith('.js')) {
11-
const filePath = path.join(__dirname, 'types', filename);
12-
await import(filePath);
13-
}
14-
}
4+
interface IOptions {
5+
include?: string[];
6+
exclude?: string[];
7+
}
8+
9+
const getOptions = () =>
10+
ArgumentParser.parse<IOptions>(async (command, prompt, action) => {
11+
command.option('--include <string...>', 'include feature files');
12+
command.option('--exclude <string...>', 'exclude feature files');
13+
14+
prompt;
15+
16+
return action(async (options) => {
17+
return options as IOptions;
18+
});
19+
});
20+
21+
async function main(): Promise<void> {
22+
const options: IOptions = await getOptions();
23+
const testSet = TestFileLoader.load(__dirname, options);
1524

16-
/**
17-
* test code for functions
18-
*/
19-
const functions = fs.readdirSync(path.join(__dirname, './functions'));
20-
for await (const filename of functions) {
21-
if (filename.endsWith('.js')) {
22-
const filePath = path.join(__dirname, 'functions', filename);
23-
await import(filePath);
24-
}
25+
for await (const test of testSet) {
26+
await import(test);
2527
}
2628
}
2729

0 commit comments

Comments
 (0)