-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Fix fix_test.py #6415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix fix_test.py #6415
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,16 +5,20 @@ | |
|
|
||
| How to use: | ||
| 1. Copy a specific test from the CPython repository to the RustPython repository. | ||
| 2. Remove all unexpected failures from the test and skip the tests that hang | ||
| 3. Run python ./scripts/fix_test.py --test test_venv --path ./Lib/test/test_venv.py or equivalent for the test from the project root. | ||
| 4. Ensure that there are no unexpected successes in the test. | ||
| 5. Actually fix the test. | ||
| 2. Remove all unexpected failures from the test and skip the tests that hang. | ||
| 3. Build RustPython: cargo build --release | ||
| 4. Run from the project root: | ||
| - For single-file tests: python ./scripts/fix_test.py --path ./Lib/test/test_venv.py | ||
| - For package tests: python ./scripts/fix_test.py --path ./Lib/test/test_inspect/test_inspect.py | ||
| 5. Verify: cargo run --release -- -m test test_venv (should pass with expected failures) | ||
| 6. Actually fix the tests marked with # TODO: RUSTPYTHON | ||
| """ | ||
|
|
||
| import argparse | ||
| import ast | ||
| import itertools | ||
| import platform | ||
| import sys | ||
| from pathlib import Path | ||
|
|
||
|
|
||
|
|
@@ -58,85 +62,87 @@ def parse_results(result): | |
| in_test_results = True | ||
| elif line.startswith("-----------"): | ||
| in_test_results = False | ||
| if ( | ||
| in_test_results | ||
| and not line.startswith("tests") | ||
| and not line.startswith("[") | ||
| ): | ||
| line = line.split(" ") | ||
| if line != [] and len(line) > 3: | ||
| test = Test() | ||
| test.name = line[0] | ||
| test.path = line[1].strip("(").strip(")") | ||
| test.result = " ".join(line[3:]).lower() | ||
| test_results.tests.append(test) | ||
| else: | ||
| if "== Tests result: " in line: | ||
| res = line.split("== Tests result: ")[1] | ||
| res = res.split(" ")[0] | ||
| test_results.tests_result = res | ||
| if in_test_results and " ... " in line: | ||
| line = line.strip() | ||
| # Skip lines that don't look like test results | ||
| if line.startswith("tests") or line.startswith("["): | ||
| continue | ||
| # Parse: "test_name (path) [subtest] ... RESULT" | ||
| parts = line.split(" ... ") | ||
| if len(parts) >= 2: | ||
| test_info = parts[0] | ||
| result_str = parts[-1].lower() | ||
| # Only process FAIL or ERROR | ||
| if result_str not in ("fail", "error"): | ||
| continue | ||
| # Extract test name (first word) | ||
| first_space = test_info.find(" ") | ||
| if first_space > 0: | ||
| test = Test() | ||
| test.name = test_info[:first_space] | ||
| # Extract path from (path) | ||
| rest = test_info[first_space:].strip() | ||
| if rest.startswith("("): | ||
| end_paren = rest.find(")") | ||
| if end_paren > 0: | ||
| test.path = rest[1:end_paren] | ||
| test.result = result_str | ||
| test_results.tests.append(test) | ||
| elif "== Tests result: " in line: | ||
| res = line.split("== Tests result: ")[1] | ||
| res = res.split(" ")[0] | ||
| test_results.tests_result = res | ||
| return test_results | ||
|
|
||
|
|
||
| def path_to_test(path) -> list[str]: | ||
| return path.split(".")[2:] | ||
| # path format: test.module_name[.submodule].ClassName.test_method | ||
| # We need [ClassName, test_method] - always the last 2 elements | ||
| parts = path.split(".") | ||
| return parts[-2:] # Get class name and method name | ||
|
|
||
|
Comment on lines
98
to
103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, let's look at the structure of the repository and locate the file
find . -name "fix_test.py" -type fRepository: RustPython/RustPython Length of output: 86 🏁 Script executed: # Get the file size to see if we can read it entirely
wc -l scripts/fix_test.pyRepository: RustPython/RustPython Length of output: 88 🏁 Script executed: # Read the file around the mentioned lines 98-103 and 169-182
cat -n scripts/fix_test.py | head -200Repository: RustPython/RustPython Length of output: 8340 🏁 Script executed: # Search for all calls to path_to_test in the file
rg "path_to_test" scripts/fix_test.py -nRepository: RustPython/RustPython Length of output: 157 🏁 Script executed: # Let's also verify the unittest output format assumption by checking if there are any tests or documentation
find . -name "*.py" -type f -exec grep -l "Run tests sequentially" {} \; 2>/dev/null | head -5Repository: RustPython/RustPython Length of output: 86 Bug confirmed: The function discards The suggested fix—passing both 🤖 Prompt for AI Agents |
||
|
|
||
| def modify_test(file: str, test: list[str], for_platform: bool = False) -> str: | ||
| def find_test_lineno(file: str, test: list[str]) -> tuple[int, int] | None: | ||
| """Find the line number and column offset of a test function. | ||
| Returns (lineno, col_offset) or None if not found. | ||
| """ | ||
| a = ast.parse(file) | ||
| lines = file.splitlines() | ||
| fixture = "@unittest.expectedFailure" | ||
| for node in ast.walk(a): | ||
| if isinstance(node, ast.FunctionDef): | ||
| if node.name == test[-1]: | ||
| assert not for_platform | ||
| indent = " " * node.col_offset | ||
| lines.insert(node.lineno - 1, indent + fixture) | ||
| lines.insert(node.lineno - 1, indent + "# TODO: RUSTPYTHON") | ||
| break | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def modify_test_v2(file: str, test: list[str], for_platform: bool = False) -> str: | ||
| a = ast.parse(file) | ||
| lines = file.splitlines() | ||
| fixture = "@unittest.expectedFailure" | ||
| for key, node in ast.iter_fields(a): | ||
| if key == "body": | ||
| for i, n in enumerate(node): | ||
| for n in node: | ||
| match n: | ||
| case ast.ClassDef(): | ||
| if len(test) == 2 and test[0] == n.name: | ||
| # look through body for function def | ||
| for i, fn in enumerate(n.body): | ||
| for fn in n.body: | ||
| match fn: | ||
| case ast.FunctionDef(): | ||
| case ast.FunctionDef() | ast.AsyncFunctionDef(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should consider to make this script to generate the json payload
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea |
||
| if fn.name == test[-1]: | ||
| assert not for_platform | ||
| indent = " " * fn.col_offset | ||
| lines.insert( | ||
| fn.lineno - 1, indent + fixture | ||
| ) | ||
| lines.insert( | ||
| fn.lineno - 1, | ||
| indent + "# TODO: RUSTPYTHON", | ||
| ) | ||
| break | ||
| case ast.FunctionDef(): | ||
| return (fn.lineno, fn.col_offset) | ||
| case ast.FunctionDef() | ast.AsyncFunctionDef(): | ||
| if n.name == test[0] and len(test) == 1: | ||
| assert not for_platform | ||
| indent = " " * n.col_offset | ||
| lines.insert(n.lineno - 1, indent + fixture) | ||
| lines.insert(n.lineno - 1, indent + "# TODO: RUSTPYTHON") | ||
| break | ||
| if i > 500: | ||
| exit() | ||
| return (n.lineno, n.col_offset) | ||
| return None | ||
|
|
||
|
|
||
| def apply_modifications(file: str, modifications: list[tuple[int, int]]) -> str: | ||
| """Apply all modifications in reverse order to avoid line number offset issues.""" | ||
| lines = file.splitlines() | ||
| fixture = "@unittest.expectedFailure" | ||
| # Sort by line number in descending order | ||
| modifications.sort(key=lambda x: x[0], reverse=True) | ||
| for lineno, col_offset in modifications: | ||
| indent = " " * col_offset | ||
| lines.insert(lineno - 1, indent + fixture) | ||
| lines.insert(lineno - 1, indent + "# TODO: RUSTPYTHON") | ||
| return "\n".join(lines) | ||
|
Comment on lines
+127
to
137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
def apply_modifications(file: str, modifications: list[tuple[int, int]]) -> str:
@@
for lineno, col_offset in modifications:
indent = " " * col_offset
- lines.insert(lineno - 1, indent + fixture)
- lines.insert(lineno - 1, indent + "# TODO: RUSTPYTHON")
+ insert_at = lineno - 1
+ prev = lines[max(0, insert_at - 3):insert_at]
+ if any(l.strip() == fixture for l in prev):
+ continue
+ lines.insert(insert_at, indent + fixture)
+ lines.insert(insert_at, indent + "# TODO: RUSTPYTHON")
return "\n".join(lines)🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def run_test(test_name): | ||
| print(f"Running test: {test_name}") | ||
| rustpython_location = "./target/release/rustpython" | ||
| if sys.platform == "win32": | ||
| rustpython_location += ".exe" | ||
|
|
||
| import subprocess | ||
|
|
||
| result = subprocess.run( | ||
|
|
@@ -149,13 +155,34 @@ def run_test(test_name): | |
|
|
||
| if __name__ == "__main__": | ||
| args = parse_args() | ||
| test_name = args.path.stem | ||
| test_path = args.path.resolve() | ||
| if not test_path.exists(): | ||
| print(f"Error: File not found: {test_path}") | ||
| sys.exit(1) | ||
| test_name = test_path.stem | ||
| tests = run_test(test_name) | ||
| f = open(args.path).read() | ||
| f = test_path.read_text(encoding="utf-8") | ||
|
|
||
| # Collect all modifications first (with deduplication for subtests) | ||
| modifications = [] | ||
| seen_tests = set() # Track (class_name, method_name) to avoid duplicates | ||
| for test in tests.tests: | ||
| if test.result == "fail" or test.result == "error": | ||
| print("Modifying test:", test.name) | ||
| f = modify_test_v2(f, path_to_test(test.path), args.platform) | ||
| with open(args.path, "w") as file: | ||
| # TODO: Find validation method, and make --force override it | ||
| file.write(f) | ||
| test_parts = path_to_test(test.path) | ||
| test_key = tuple(test_parts) | ||
| if test_key in seen_tests: | ||
| continue # Skip duplicate (same test, different subtest) | ||
| seen_tests.add(test_key) | ||
| location = find_test_lineno(f, test_parts) | ||
| if location: | ||
| print(f"Modifying test: {test.name} at line {location[0]}") | ||
| modifications.append(location) | ||
| else: | ||
| print(f"Warning: Could not find test: {test.name} ({test_parts})") | ||
|
|
||
| # Apply all modifications in reverse order | ||
| if modifications: | ||
| f = apply_modifications(f, modifications) | ||
| test_path.write_text(f, encoding="utf-8") | ||
|
|
||
| print(f"Modified {len(modifications)} tests") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused imports (flake8 F401) to keep the script clean.
itertools(Line 19) andplatform(Line 20) are unused per static analysis;sys(Line 21) is used.📝 Committable suggestion
🧰 Tools
🪛 Flake8 (7.3.0)
[error] 19-19: 'itertools' imported but unused
(F401)
[error] 20-20: 'platform' imported but unused
(F401)
🤖 Prompt for AI Agents