|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +Script to standardize mock_response usage in test files. |
| 4 | +
|
| 5 | +This script finds test files that use direct mock_response manipulation and |
| 6 | +converts them to use mock_response_factory instead. |
| 7 | +""" |
| 8 | + |
| 9 | +# Standard library imports |
| 10 | +import os |
| 11 | +from pathlib import Path |
| 12 | +import re |
| 13 | +import sys |
| 14 | +from typing import List |
| 15 | +from typing import Tuple |
| 16 | + |
| 17 | + |
| 18 | +def find_pattern_in_file(file_path: str, pattern: str) -> List[str]: |
| 19 | + """Find all matches of pattern in the given file.""" |
| 20 | + with open(file_path, "r") as f: |
| 21 | + content = f.read() |
| 22 | + return re.findall(pattern, content, re.MULTILINE) |
| 23 | + |
| 24 | + |
| 25 | +def transform_file(file_path: str, dry_run: bool = False) -> bool: |
| 26 | + """Transform a file to use mock_response_factory.""" |
| 27 | + with open(file_path, "r") as f: |
| 28 | + content = f.read() |
| 29 | + |
| 30 | + # Backup original content |
| 31 | + original_content = content |
| 32 | + |
| 33 | + # Pattern 1: def test_*(mock_response) -> def test_*(mock_response_factory) |
| 34 | + # Find all test function definitions |
| 35 | + test_pattern = r"(def\s+test_\w+\([^)]*)(,\s*mock_response)(\s*[,)].*)" |
| 36 | + |
| 37 | + def param_replacement(match): |
| 38 | + before = match.group(1) |
| 39 | + param = match.group(2).replace("mock_response", "mock_response_factory") |
| 40 | + after = match.group(3) |
| 41 | + return f"{before}{param}{after}" |
| 42 | + |
| 43 | + content = re.sub(test_pattern, param_replacement, content) |
| 44 | + |
| 45 | + # Pattern 2: mock_response.json.return_value = {...} |
| 46 | + pattern2 = r"([ \t]*)mock_response\.json\.return_value\s*=\s*({[^;]*}|\[[^;]*\])" |
| 47 | + |
| 48 | + def replacement2(match): |
| 49 | + indent = match.group(1) |
| 50 | + data = match.group(2).strip() |
| 51 | + # Ensure data has balanced braces |
| 52 | + open_braces = data.count("{") - data.count("}") |
| 53 | + open_brackets = data.count("[") - data.count("]") |
| 54 | + |
| 55 | + if open_braces != 0 or open_brackets != 0: |
| 56 | + # Skip this match as it has unbalanced braces or brackets |
| 57 | + return match.group(0) |
| 58 | + |
| 59 | + return f"{indent}mock_response = mock_response_factory(\n{indent} 200, \n{indent} {data}\n{indent})" |
| 60 | + |
| 61 | + content = re.sub(pattern2, replacement2, content) |
| 62 | + |
| 63 | + # Pattern 3: mock_response.status_code = 204 |
| 64 | + pattern3 = r"([ \t]*)mock_response\.status_code\s*=\s*(\d+)" |
| 65 | + |
| 66 | + def replacement3(match): |
| 67 | + indent = match.group(1) |
| 68 | + status_code = match.group(2) |
| 69 | + return f"{indent}mock_response = mock_response_factory({status_code})" |
| 70 | + |
| 71 | + content = re.sub(pattern3, replacement3, content) |
| 72 | + |
| 73 | + # Pattern 4: We're disabling this pattern for now as it was causing issues |
| 74 | + # The goal was to change order of assignments (response assignment should come before oauth assignment) |
| 75 | + # but it was causing syntax errors |
| 76 | + """ |
| 77 | + pattern4 = r'([ \t]*)(.*?)\.oauth(?:\.|\w+\.)*request\.return_value\s*=\s*mock_response\n([ \t]*)mock_response\s*=\s*mock_response_factory' |
| 78 | + |
| 79 | + def replacement4(match): |
| 80 | + indent1 = match.group(1) |
| 81 | + obj = match.group(2) |
| 82 | + indent2 = match.group(3) |
| 83 | + return f"{indent2}mock_response = mock_response_factory\n{indent1}{obj}.oauth.request.return_value = mock_response" |
| 84 | + |
| 85 | + content = re.sub(pattern4, replacement4, content) |
| 86 | + """ |
| 87 | + |
| 88 | + # Pattern 5: Fix cases where test was updated to use mock_response_factory as parameter |
| 89 | + # but still uses mock_response in the body |
| 90 | + pattern5 = r"def\s+test_\w+\([^)]*mock_response_factory[^)]*\).*?(?=\n\s*def|\Z)" |
| 91 | + |
| 92 | + def fix_mock_response_usage(match): |
| 93 | + test_func = match.group(0) |
| 94 | + # If the function uses mock_response_factory but also has direct mock_response usage |
| 95 | + if ( |
| 96 | + "mock_response.json.return_value =" in test_func |
| 97 | + or "mock_response.status_code =" in test_func |
| 98 | + ): |
| 99 | + # Add a mock_response declaration at the beginning of the function body |
| 100 | + # Find the first indented line after the function def |
| 101 | + lines = test_func.split("\n") |
| 102 | + for i, line in enumerate(lines): |
| 103 | + if i > 0 and line.strip() and not line.strip().startswith("#"): |
| 104 | + indent = re.match(r"(\s*)", line).group(1) |
| 105 | + # Insert the mock_response assignment after docstring (if any) |
| 106 | + for j in range(i, len(lines)): |
| 107 | + if not lines[j].strip().startswith('"""') and not lines[ |
| 108 | + j |
| 109 | + ].strip().startswith("'''"): |
| 110 | + lines.insert(j, f"{indent}mock_response = mock_response_factory(200)") |
| 111 | + break |
| 112 | + break |
| 113 | + return "\n".join(lines) |
| 114 | + return test_func |
| 115 | + |
| 116 | + content = re.sub(pattern5, fix_mock_response_usage, content, flags=re.DOTALL) |
| 117 | + |
| 118 | + # If no changes were made, return False |
| 119 | + if content == original_content: |
| 120 | + return False |
| 121 | + |
| 122 | + # Write the changes back to the file |
| 123 | + if not dry_run: |
| 124 | + with open(file_path, "w") as f: |
| 125 | + f.write(content) |
| 126 | + |
| 127 | + return True |
| 128 | + |
| 129 | + |
| 130 | +def find_files_to_transform() -> List[Tuple[str, bool]]: |
| 131 | + """Find all test files that need to be transformed.""" |
| 132 | + test_dir = Path("tests") |
| 133 | + result = [] |
| 134 | + |
| 135 | + for root, _, files in os.walk(test_dir): |
| 136 | + for file in files: |
| 137 | + if file.endswith(".py") and file.startswith("test_"): |
| 138 | + file_path = os.path.join(root, file) |
| 139 | + |
| 140 | + # Check if file uses mock_response directly |
| 141 | + uses_mock_response = bool( |
| 142 | + find_pattern_in_file( |
| 143 | + file_path, r"def\s+test_\w+\([^)]*,\s*mock_response\s*[,)]" |
| 144 | + ) |
| 145 | + ) |
| 146 | + |
| 147 | + # Check if file directly manipulates mock_response |
| 148 | + manipulates_mock_response = bool( |
| 149 | + find_pattern_in_file( |
| 150 | + file_path, r"mock_response\.(json\.return_value|status_code)\s*=" |
| 151 | + ) |
| 152 | + ) |
| 153 | + |
| 154 | + # Check if file uses mock_response_factory |
| 155 | + uses_factory = bool( |
| 156 | + find_pattern_in_file(file_path, r"mock_response\s*=\s*mock_response_factory") |
| 157 | + ) |
| 158 | + |
| 159 | + # Determine if file needs transformation |
| 160 | + needs_transform = ( |
| 161 | + uses_mock_response or manipulates_mock_response |
| 162 | + ) and not uses_factory |
| 163 | + |
| 164 | + if needs_transform: |
| 165 | + result.append((file_path, needs_transform)) |
| 166 | + |
| 167 | + return result |
| 168 | + |
| 169 | + |
| 170 | +def main(): |
| 171 | + """Main entry point.""" |
| 172 | + dry_run = "--dry-run" in sys.argv |
| 173 | + files = find_files_to_transform() |
| 174 | + |
| 175 | + print(f"Found {len(files)} files that need to be transformed.") |
| 176 | + if dry_run: |
| 177 | + print("Running in dry-run mode. No files will be modified.") |
| 178 | + |
| 179 | + for file_path, _ in files: |
| 180 | + print(f"Transforming {file_path}...", end="") |
| 181 | + transformed = transform_file(file_path, dry_run) |
| 182 | + print(" TRANSFORMED" if transformed else " SKIPPED (no changes)") |
| 183 | + |
| 184 | + print("\nDone!") |
| 185 | + print(f"Transformed {len(files)} files.") |
| 186 | + |
| 187 | + if dry_run: |
| 188 | + print("\nRun without --dry-run to apply changes.") |
| 189 | + |
| 190 | + |
| 191 | +if __name__ == "__main__": |
| 192 | + main() |
0 commit comments