From 7bd59fa52d5a15c9526140e391cf4305e67fdef1 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Fri, 12 Dec 2025 17:12:45 +0900 Subject: [PATCH] Fix fix_test.py --- scripts/fix_test.py | 165 ++++++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 69 deletions(-) diff --git a/scripts/fix_test.py b/scripts/fix_test.py index a5663e3eee3..9716bd0b008 100644 --- a/scripts/fix_test.py +++ b/scripts/fix_test.py @@ -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 -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(): 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) 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")