leetkick
Version:
A CLI tool for scaffolding LeetCode exercises with language-specific testing setups
305 lines (268 loc) • 10.8 kB
text/typescript
import test from 'node:test';
import assert from 'node:assert';
import {promises as fs} from 'fs';
import {join} from 'path';
import {tmpdir} from 'os';
// Mock the file operations module
const mockTemplateDir = join(tmpdir(), 'leetcode-cli-test-file-ops-templates');
const mockWorkingDir = join(tmpdir(), 'leetcode-cli-test-file-ops-workspace');
// Setup test fixtures
void test('file operations test suite', async t => {
// Setup test environment
await fs.mkdir(mockTemplateDir, {recursive: true});
await fs.mkdir(mockWorkingDir, {recursive: true});
await t.test('should extract function name from TypeScript code', () => {
const code =
'function twoSum(nums: number[], target: number): number[] {\n \n};';
// We would need to import the extractFunctionName function
// For now, test the regex pattern
const functionMatch = code.match(/function\s+(\w+)\s*\(/);
assert.strictEqual(functionMatch?.[1], 'twoSum');
});
await t.test('should extract function name from C++ code', () => {
const code =
'vector<int> twoSum(vector<int>& nums, int target) {\n return {};\n}';
// Test the C++ method regex pattern - need to handle templates like vector<int>
const cppMethodMatch = code.match(/[\w<>]+\s+(\w+)\s*\([^)]*\)\s*\{/);
assert.strictEqual(cppMethodMatch?.[1], 'twoSum');
});
await t.test('should extract function name from Kotlin code', () => {
const code =
'fun twoSum(nums: IntArray, target: Int): IntArray {\n return intArrayOf()\n}';
// Test the Kotlin function regex pattern
const kotlinFunMatch = code.match(/fun\s+(\w+)\s*\(/);
assert.strictEqual(kotlinFunMatch?.[1], 'twoSum');
});
await t.test('should extract function name from Java code', () => {
const code =
'public int[] twoSum(int[] nums, int target) {\n return new int[0];\n}';
// Test the Java method regex pattern
const javaMethodMatch = code.match(
/public\s+[\w<>[\]]+\s+(\w+)\s*\([^)]*\)\s*\{/,
);
assert.strictEqual(javaMethodMatch?.[1], 'twoSum');
});
await t.test('should extract function name from Rust code', () => {
const code =
'pub fn two_sum(nums: Vec<i32>, target: i32) -> Vec<i32> {\n vec![]\n}';
// Test the Rust function regex pattern
const rustFnMatch = code.match(/pub\s+fn\s+(\w+)\s*\(/);
assert.strictEqual(rustFnMatch?.[1], 'two_sum');
});
await t.test('should extract function name from Go code', () => {
const code =
'func twoSum(nums []int, target int) []int {\n return []int{}\n}';
// Test the Go function regex pattern
const goFuncMatch = code.match(/func\s+(\w+)\s*\(/);
assert.strictEqual(goFuncMatch?.[1], 'twoSum');
});
await t.test('should extract function name from Python code', () => {
const code =
'def twoSum(self, nums: List[int], target: int) -> List[int]:\n pass';
// Test the Python function regex pattern
const pythonFuncMatch = code.match(/def\s+(\w+)\s*\(/);
assert.strictEqual(pythonFuncMatch?.[1], 'twoSum');
});
await t.test('should extract function name from Python class method', () => {
const code =
'class Solution:\n def twoSum(self, nums: List[int], target: int) -> List[int]:\n pass';
// Test the Python method extraction
const pythonFuncMatch = code.match(/def\s+(\w+)\s*\(/);
assert.strictEqual(pythonFuncMatch?.[1], 'twoSum');
});
await t.test('should format problem name to camelCase', () => {
const title = 'Two Sum';
const formatted = title
.replace(/[^a-zA-Z0-9\s]/g, '')
.split(' ')
.map((word, index) =>
index === 0
? word.toLowerCase()
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join('');
assert.strictEqual(formatted, 'twoSum');
});
await t.test('should clean HTML description', () => {
const htmlContent =
'<p>Given an array of integers <code>nums</code> and an integer <code>target</code>, return indices.</p>';
const cleaned = htmlContent
.replace(/<[^>]*>/g, '')
.replace(/ /g, ' ')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/&/g, '&')
.trim();
assert.strictEqual(
cleaned,
'Given an array of integers nums and an integer target, return indices.',
);
});
await t.test('should decode HTML entities in descriptions', () => {
// Test the specific entities mentioned in the bug report
const htmlWithEntities =
'Given an m x n 2D binary grid which represents a map of '1's (land) and '0's (water). ' +
'Input: grid = ["1","1","0"] and grid[i][j] is '0' or '1'.';
const cleaned = htmlWithEntities
.replace(/<[^>]*>/g, '')
.replace(/ /g, ' ')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'")
.replace(/“/g, '"')
.replace(/”/g, '"')
.replace(/‘/g, "'")
.replace(/’/g, "'")
.replace(/&/g, '&')
.trim();
assert.strictEqual(
cleaned,
"Given an m x n 2D binary grid which represents a map of '1's (land) and '0's (water). " +
'Input: grid = ["1","1","0"] and grid[i][j] is \'0\' or \'1\'.',
);
});
await t.test('should handle various quote entities', () => {
const testCases = [
{input: '"hello"', expected: '"hello"'},
{input: ''world'', expected: "'world'"},
{input: ''test'', expected: "'test'"},
{input: '“fancy”', expected: '"fancy"'},
{input: '‘quote’', expected: "'quote'"},
];
testCases.forEach(({input, expected}) => {
const cleaned = input
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'")
.replace(/“/g, '"')
.replace(/”/g, '"')
.replace(/‘/g, "'")
.replace(/’/g, "'")
.replace(/&/g, '&');
assert.strictEqual(cleaned, expected);
});
});
await t.test('should create clean problem directory name', () => {
const problemId = '1';
const paddedId = problemId.padStart(4, '0');
const problemDirName = `problem_${paddedId}`;
assert.strictEqual(problemDirName, 'problem_0001');
});
await t.test('should format class names correctly', () => {
const testCases = [
{input: 'Two Sum', expected: 'TwoSum'},
{input: 'Roman to Integer', expected: 'RomanToInteger'},
{
input: 'Longest Substring Without Repeating Characters',
expected: 'LongestSubstringWithoutRepeatingCharacters',
},
];
testCases.forEach(({input, expected}) => {
const formatted = input
.replace(/[^a-zA-Z0-9\s]/g, '')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
assert.strictEqual(formatted, expected);
});
});
await t.test('should format snake case names correctly', () => {
const testCases = [
{input: 'Two Sum', expected: 'two_sum'},
{input: 'Roman to Integer', expected: 'roman_to_integer'},
{
input: 'Longest Substring Without Repeating Characters',
expected: 'longest_substring_without_repeating_characters',
},
];
testCases.forEach(({input, expected}) => {
const formatted = input
.replace(/[^a-zA-Z0-9\s]/g, '')
.toLowerCase()
.split(/\s+/)
.join('_');
assert.strictEqual(formatted, expected);
});
});
await t.test('should get correct file extension for language', () => {
const extMap: Record<string, string> = {
typescript: 'ts',
javascript: 'js',
python: 'py',
java: 'java',
cpp: 'cpp',
go: 'go',
rust: 'rs',
kotlin: 'kt',
};
assert.strictEqual(extMap.typescript, 'ts');
assert.strictEqual(extMap.javascript, 'js');
assert.strictEqual(extMap.python, 'py');
assert.strictEqual(extMap.java, 'java');
assert.strictEqual(extMap.kotlin, 'kt');
});
await t.test('should get correct language slug for LeetCode API', () => {
const slugMap: Record<string, string> = {
typescript: 'typescript',
javascript: 'javascript',
python: 'python3',
java: 'java',
cpp: 'cpp',
go: 'golang',
rust: 'rust',
kotlin: 'kotlin',
};
assert.strictEqual(slugMap.typescript, 'typescript');
assert.strictEqual(slugMap.javascript, 'javascript');
assert.strictEqual(slugMap.python, 'python3');
assert.strictEqual(slugMap.go, 'golang');
assert.strictEqual(slugMap.kotlin, 'kotlin');
});
await t.test('should generate correct test file names', () => {
const className = 'TwoSum';
const snakeCaseName = 'two_sum';
const testNameMap: Record<string, string> = {
typescript: `${className}.test.ts`,
javascript: `${className}.test.js`,
python: `test_${snakeCaseName}.py`,
java: `${className}Test.java`,
cpp: `${snakeCaseName}.test.cpp`,
kotlin: `${className}Test.kt`,
go: `${snakeCaseName}_test.go`,
rust: 'lib.rs', // Rust tests are in the same file
};
assert.strictEqual(testNameMap.typescript, 'TwoSum.test.ts');
assert.strictEqual(testNameMap.javascript, 'TwoSum.test.js');
assert.strictEqual(testNameMap.python, 'test_two_sum.py');
assert.strictEqual(testNameMap.java, 'TwoSumTest.java');
assert.strictEqual(testNameMap.cpp, 'two_sum.test.cpp');
assert.strictEqual(testNameMap.kotlin, 'TwoSumTest.kt');
assert.strictEqual(testNameMap.go, 'two_sum_test.go');
assert.strictEqual(testNameMap.rust, 'lib.rs');
});
await t.test('should generate correct exercise file names', () => {
const className = 'TwoSum';
const snakeCaseName = 'two_sum';
const exerciseNameMap: Record<string, string> = {
typescript: `${className}.ts`,
javascript: `${className}.js`,
python: `${snakeCaseName}.py`,
cpp: `${snakeCaseName}.cpp`,
kotlin: `${className}.kt`,
java: `${className}.java`,
go: `${snakeCaseName}.go`,
rust: 'lib.rs', // Rust always uses lib.rs
};
assert.strictEqual(exerciseNameMap.typescript, 'TwoSum.ts');
assert.strictEqual(exerciseNameMap.javascript, 'TwoSum.js');
assert.strictEqual(exerciseNameMap.python, 'two_sum.py');
assert.strictEqual(exerciseNameMap.cpp, 'two_sum.cpp');
assert.strictEqual(exerciseNameMap.go, 'two_sum.go');
assert.strictEqual(exerciseNameMap.rust, 'lib.rs');
});
// Cleanup
await fs.rm(mockTemplateDir, {recursive: true, force: true});
await fs.rm(mockWorkingDir, {recursive: true, force: true});
});