Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 166 additions & 0 deletions .cursor/.dev/1-update-cookiecutter-test-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# Plan: Update CookieCutter Test Template to Use For Loop Instead of Parametrize

## Overview

Update the cookiecutter template for `test_solution.py` to use a for loop approach for test cases instead of the current `@pytest.mark.parametrize` decorator. This will make the test structure more explicit and easier to read.

## Current State Analysis

### Current Template Structure

- Uses `@pytest.mark.parametrize` decorator with `method.parametrize` and `method.test_cases`
- Test cases are stored as a single string in JSON: `"[([1, 2, 3], [1, 2, 3], True), ...]"`
- Generated test method signature includes all parameters: `(self, p_list: list[int | None], q_list: list[int | None], expected: bool)`

### Current JSON Structure

```json
"_test_methods": {
"list": [
{
"name": "test_is_same_tree",
"signature": "(self, p_list: list[int | None], q_list: list[int | None], expected: bool)",
"parametrize": "p_list, q_list, expected",
"test_cases": "[([1, 2, 3], [1, 2, 3], True), ([1, 2], [1, None, 2], False), ...]",
"body": "result = run_is_same_tree(Solution, p_list, q_list)\nassert_is_same_tree(result, expected)"
}
]
}
```

## Target State

### New Template Structure

- Keep `@pytest.mark.parametrize` decorator
- Use a for loop to iterate through individual test cases from the list
- Test method signature remains the same
- Only change: `test_cases` becomes a list instead of a string

### New JSON Structure

```json
"_test_methods": {
"list": [
{
"name": "test_is_same_tree",
"signature": "(self, p_list: list[int | None], q_list: list[int | None], expected: bool)",
"parametrize": "p_list, q_list, expected",
"test_cases": {
"list": [
"([1, 2, 3], [1, 2, 3], True)",
"([1, 2], [1, None, 2], False)",
"([1, 2, 1], [1, 1, 2], False)"
]
},
"body": "result = run_is_same_tree(Solution, p_list, q_list)\nassert_is_same_tree(result, expected)"
}
]
}
```

## Implementation Plan

### Phase 1: Update CookieCutter Template

1. **Update `test_solution.py` template**
- Change from `{{method.test_cases}}` to `{{method.test_cases.list | join(', ')}}`
- This follows the existing pattern used by other list fields in the template
- Template line 30: `@pytest.mark.parametrize("{{method.parametrize}}", [{{method.test_cases.list | join(', ')}}])`
- **✅ Validated:** Correctly follows existing "list" pattern used by `_tags`, `_readme_examples`, etc.

### Phase 2: Create JSON Migration Script

1. **Create `migrate_test_cases.py` script**
- Parse existing JSON files in `leetcode_py/cli/resources/leetcode/json/problems/`
- Convert `test_cases` string to `{"list": ["test_case1", "test_case2", ...]}` format
- Keep all other fields exactly the same
- Update only the `test_cases` field structure

### Phase 3: Update JSON Files

1. **Run migration script on all existing problem JSON files**
- Process all 107 JSON files in the problems directory
- Create backup of original files
- Validate migrated JSON structure

### Phase 4: Testing and Validation

1. **Test generated templates**
- Generate a test problem using updated template
- Verify test cases run correctly with parametrize approach
- Ensure JSON list format works with pytest parametrize

2. **Comprehensive validation with all problems**
- Copy additional LeetCode problems to `.cache/leetcode` (if not already present)
- Regenerate ALL problems from new design using updated template
- Copy existing solutions from cache to regenerated problems
- Run tests on all regenerated problems to ensure they still pass
- Verify no regressions in test functionality
- Document any issues found and fix them

### Phase 5: Update Documentation

1. **Update problem creation documentation**
- Update `.cursor/commands/problem-creation.md` to reflect new JSON structure
- Change `test_cases` format from string to `{"list": [...]}` in examples
- Update template examples to show new format
- Add note about migration for existing problems
2. **Update any other related documentation**
- Check for other docs that reference `test_cases` format
- Update examples in README or other guides if needed

## Benefits of New Approach

1. **Cleaner JSON Structure**: Test cases as `{"list": [...]}` object instead of single string
2. **Better Maintainability**: Easier to edit individual test cases in JSON files
3. **No Parsing Issues**: Avoids tuple conversion and complex JSON parsing
4. **Consistency**: Aligns with other JSON list fields in the structure
5. **Template Consistency**: Uses same pattern as other list fields (`| join(', ')`)

## Files to Modify

### Template Files

- `leetcode_py/cli/resources/leetcode/{{cookiecutter.problem_name}}/test_solution.py`

### Migration Script

- `migrate_test_cases.py` (new file)

### JSON Files

- All files in `leetcode_py/cli/resources/leetcode/json/problems/` (107 files)

### Generation Code

- **No changes needed** - The `test_cases` field is created manually, not by automated code
- The existing "list" pattern is already established and working correctly
- Phase 4 removed after investigation showed no automated generation code exists

## Implementation Steps

1. ✅ Create this plan document
2. ✅ Update cookiecutter template
3. ✅ Create migration script
4. ✅ Run migration on all JSON files
5. ✅ Test updated template generation
6. ✅ Validate all tests still work
7. 🔄 Update documentation (separate step - do not combine with implementation)

## Risk Mitigation

1. **Backup Strategy**: Create backups of all JSON files before migration
2. **Incremental Testing**: Test migration on a few files first
3. **Rollback Plan**: Keep original template and migration script for rollback
4. **Validation**: Ensure all existing tests still pass after migration

## Success Criteria

1. All existing JSON files successfully migrated to new list format
2. Generated test templates work with JSON array format for test_cases
3. All existing tests continue to pass with parametrize approach
4. JSON structure is cleaner and more maintainable
5. Template generation works correctly for new problems
6. **All regenerated problems pass their tests** (comprehensive validation)
7. No regressions in test functionality across the entire problem set
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"problem_title": "Accounts Merge",
"difficulty": "Medium",
"topics": "Array, Hash Table, String, Depth-First Search, Breadth-First Search, Union Find, Sorting",
"_tags": { "list": ["grind-75"] },
"_tags": {
"list": ["grind-75"]
},
"readme_description": "Given a list of `accounts` where each element `accounts[i]` is a list of strings, where the first element `accounts[i][0]` is a name, and the rest of the elements are emails representing emails of the account.\n\nNow, we would like to merge these accounts. Two accounts definitely belong to the same person if there is some common email to both accounts. Note that even if two accounts have the same name, they may belong to different people as people could have the same name. A person can have any number of accounts initially, but all of their accounts definitely have the same name.\n\nAfter merging the accounts, return the accounts in the following format: the first element of each account is the name, and the rest of the elements are emails in sorted order. The accounts themselves can be returned in any order.",
"_readme_examples": {
"list": [
Expand Down Expand Up @@ -44,15 +46,36 @@
]
},
"_test_helper_methods": {
"list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }]
"list": [
{
"name": "setup_method",
"parameters": "",
"body": "self.solution = Solution()"
}
]
},
"_test_methods": {
"list": [
{
"name": "test_accounts_merge",
"signature": "(self, accounts: list[list[str]], expected: list[list[str]])",
"parametrize": "accounts, expected",
"test_cases": "[([['John', 'johnsmith@mail.com', 'john_newyork@mail.com'], ['John', 'johnsmith@mail.com', 'john00@mail.com'], ['Mary', 'mary@mail.com'], ['John', 'johnnybravo@mail.com']], [['John', 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'], ['Mary', 'mary@mail.com'], ['John', 'johnnybravo@mail.com']]), ([['Gabe', 'Gabe0@m.co', 'Gabe3@m.co', 'Gabe1@m.co'], ['Kevin', 'Kevin3@m.co', 'Kevin5@m.co', 'Kevin0@m.co'], ['Ethan', 'Ethan5@m.co', 'Ethan4@m.co', 'Ethan0@m.co'], ['Hanzo', 'Hanzo3@m.co', 'Hanzo1@m.co', 'Hanzo0@m.co'], ['Fern', 'Fern5@m.co', 'Fern1@m.co', 'Fern0@m.co']], [['Ethan', 'Ethan0@m.co', 'Ethan4@m.co', 'Ethan5@m.co'], ['Gabe', 'Gabe0@m.co', 'Gabe1@m.co', 'Gabe3@m.co'], ['Hanzo', 'Hanzo0@m.co', 'Hanzo1@m.co', 'Hanzo3@m.co'], ['Kevin', 'Kevin0@m.co', 'Kevin3@m.co', 'Kevin5@m.co'], ['Fern', 'Fern0@m.co', 'Fern1@m.co', 'Fern5@m.co']]), ([['Alice', 'alice@mail.com']], [['Alice', 'alice@mail.com']]), ([['Bob', 'bob1@mail.com'], ['Bob', 'bob2@mail.com']], [['Bob', 'bob1@mail.com'], ['Bob', 'bob2@mail.com']]), ([['Alice', 'alice@mail.com', 'alice2@mail.com'], ['Alice', 'alice2@mail.com', 'alice3@mail.com']], [['Alice', 'alice2@mail.com', 'alice3@mail.com', 'alice@mail.com']]), ([['A', 'a@mail.com', 'b@mail.com'], ['B', 'b@mail.com', 'c@mail.com'], ['C', 'c@mail.com', 'd@mail.com']], [['A', 'a@mail.com', 'b@mail.com', 'c@mail.com', 'd@mail.com']]), ([['David', 'david@mail.com'], ['David', 'david@mail.com']], [['David', 'david@mail.com']]), ([['Alex', 'alex1@mail.com'], ['Bob', 'bob1@mail.com'], ['Charlie', 'charlie1@mail.com']], [['Alex', 'alex1@mail.com'], ['Bob', 'bob1@mail.com'], ['Charlie', 'charlie1@mail.com']]), ([['John', 'john1@mail.com', 'john2@mail.com'], ['John', 'john3@mail.com'], ['Jane', 'jane1@mail.com']], [['John', 'john1@mail.com', 'john2@mail.com'], ['John', 'john3@mail.com'], ['Jane', 'jane1@mail.com']]), ([['User', 'user@mail.com', 'user1@mail.com'], ['User', 'user2@mail.com', 'user@mail.com'], ['User', 'user3@mail.com', 'user1@mail.com']], [['User', 'user1@mail.com', 'user2@mail.com', 'user3@mail.com', 'user@mail.com']]), ([['Test', 'test1@mail.com'], ['Test', 'test2@mail.com'], ['Test', 'test1@mail.com', 'test3@mail.com']], [['Test', 'test2@mail.com'], ['Test', 'test1@mail.com', 'test3@mail.com']]), ([['Name', 'a@mail.com', 'b@mail.com', 'c@mail.com'], ['Name', 'd@mail.com', 'e@mail.com'], ['Name', 'c@mail.com', 'f@mail.com']], [['Name', 'd@mail.com', 'e@mail.com'], ['Name', 'a@mail.com', 'b@mail.com', 'c@mail.com', 'f@mail.com']])]",
"test_cases": {
"list": [
"([['John', 'johnsmith@mail.com', 'john_newyork@mail.com'], ['John', 'johnsmith@mail.com', 'john00@mail.com'], ['Mary', 'mary@mail.com'], ['John', 'johnnybravo@mail.com']], [['John', 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'], ['Mary', 'mary@mail.com'], ['John', 'johnnybravo@mail.com']])",
"([['Gabe', 'Gabe0@m.co', 'Gabe3@m.co', 'Gabe1@m.co'], ['Kevin', 'Kevin3@m.co', 'Kevin5@m.co', 'Kevin0@m.co'], ['Ethan', 'Ethan5@m.co', 'Ethan4@m.co', 'Ethan0@m.co'], ['Hanzo', 'Hanzo3@m.co', 'Hanzo1@m.co', 'Hanzo0@m.co'], ['Fern', 'Fern5@m.co', 'Fern1@m.co', 'Fern0@m.co']], [['Ethan', 'Ethan0@m.co', 'Ethan4@m.co', 'Ethan5@m.co'], ['Gabe', 'Gabe0@m.co', 'Gabe1@m.co', 'Gabe3@m.co'], ['Hanzo', 'Hanzo0@m.co', 'Hanzo1@m.co', 'Hanzo3@m.co'], ['Kevin', 'Kevin0@m.co', 'Kevin3@m.co', 'Kevin5@m.co'], ['Fern', 'Fern0@m.co', 'Fern1@m.co', 'Fern5@m.co']])",
"([['Alice', 'alice@mail.com']], [['Alice', 'alice@mail.com']])",
"([['Bob', 'bob1@mail.com'], ['Bob', 'bob2@mail.com']], [['Bob', 'bob1@mail.com'], ['Bob', 'bob2@mail.com']])",
"([['Alice', 'alice@mail.com', 'alice2@mail.com'], ['Alice', 'alice2@mail.com', 'alice3@mail.com']], [['Alice', 'alice2@mail.com', 'alice3@mail.com', 'alice@mail.com']])",
"([['A', 'a@mail.com', 'b@mail.com'], ['B', 'b@mail.com', 'c@mail.com'], ['C', 'c@mail.com', 'd@mail.com']], [['A', 'a@mail.com', 'b@mail.com', 'c@mail.com', 'd@mail.com']])",
"([['David', 'david@mail.com'], ['David', 'david@mail.com']], [['David', 'david@mail.com']])",
"([['Alex', 'alex1@mail.com'], ['Bob', 'bob1@mail.com'], ['Charlie', 'charlie1@mail.com']], [['Alex', 'alex1@mail.com'], ['Bob', 'bob1@mail.com'], ['Charlie', 'charlie1@mail.com']])",
"([['John', 'john1@mail.com', 'john2@mail.com'], ['John', 'john3@mail.com'], ['Jane', 'jane1@mail.com']], [['John', 'john1@mail.com', 'john2@mail.com'], ['John', 'john3@mail.com'], ['Jane', 'jane1@mail.com']])",
"([['User', 'user@mail.com', 'user1@mail.com'], ['User', 'user2@mail.com', 'user@mail.com'], ['User', 'user3@mail.com', 'user1@mail.com']], [['User', 'user1@mail.com', 'user2@mail.com', 'user3@mail.com', 'user@mail.com']])",
"([['Test', 'test1@mail.com'], ['Test', 'test2@mail.com'], ['Test', 'test1@mail.com', 'test3@mail.com']], [['Test', 'test2@mail.com'], ['Test', 'test1@mail.com', 'test3@mail.com']])",
"([['Name', 'a@mail.com', 'b@mail.com', 'c@mail.com'], ['Name', 'd@mail.com', 'e@mail.com'], ['Name', 'c@mail.com', 'f@mail.com']], [['Name', 'd@mail.com', 'e@mail.com'], ['Name', 'a@mail.com', 'b@mail.com', 'c@mail.com', 'f@mail.com']])"
]
},
"body": " result = run_accounts_merge(Solution, accounts)\n assert_accounts_merge(result, expected)"
}
]
Expand Down
36 changes: 31 additions & 5 deletions leetcode_py/cli/resources/leetcode/json/problems/add_binary.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
"problem_title": "Add Binary",
"difficulty": "Easy",
"topics": "Math, String, Bit Manipulation, Simulation",
"_tags": { "list": ["grind-75"] },
"_tags": {
"list": ["grind-75"]
},
"readme_description": "Given two binary strings `a` and `b`, return *their sum as a binary string*.",
"_readme_examples": {
"list": [
{ "content": "```\nInput: a = \"11\", b = \"1\"\nOutput: \"100\"\n```" },
{ "content": "```\nInput: a = \"1010\", b = \"1011\"\nOutput: \"10101\"\n```" }
{
"content": "```\nInput: a = \"11\", b = \"1\"\nOutput: \"100\"\n```"
},
{
"content": "```\nInput: a = \"1010\", b = \"1011\"\nOutput: \"10101\"\n```"
}
]
},
"readme_constraints": "- `1 <= a.length, b.length <= 10^4`\n- `a` and `b` consist only of `'0'` or `'1'` characters.\n- Each string does not contain leading zeros except for the zero itself.",
Expand Down Expand Up @@ -40,15 +46,35 @@
]
},
"_test_helper_methods": {
"list": [{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }]
"list": [
{
"name": "setup_method",
"parameters": "",
"body": "self.solution = Solution()"
}
]
},
"_test_methods": {
"list": [
{
"name": "test_add_binary",
"signature": "(self, a: str, b: str, expected: str)",
"parametrize": "a, b, expected",
"test_cases": "[('11', '1', '100'), ('1010', '1011', '10101'), ('0', '0', '0'), ('1', '1', '10'), ('1111', '1111', '11110'), ('1', '0', '1'), ('0', '1', '1'), ('1', '111', '1000'), ('111', '1', '1000'), ('1010', '1', '1011'), ('1111', '1', '10000')]",
"test_cases": {
"list": [
"('11', '1', '100')",
"('1010', '1011', '10101')",
"('0', '0', '0')",
"('1', '1', '10')",
"('1111', '1111', '11110')",
"('1', '0', '1')",
"('0', '1', '1')",
"('1', '111', '1000')",
"('111', '1', '1000')",
"('1010', '1', '1011')",
"('1111', '1', '10000')"
]
},
"body": " result = run_add_binary(Solution, a, b)\n assert_add_binary(result, expected)"
}
]
Expand Down
Loading