diff --git a/codeflash/languages/base.py b/codeflash/languages/base.py index b5fd583c8..e699afd2b 100644 --- a/codeflash/languages/base.py +++ b/codeflash/languages/base.py @@ -50,6 +50,12 @@ class IndexResult: error: bool +@dataclass(frozen=True) +class SetupError: + message: str + should_abort: bool + + @dataclass class HelperFunction: """A helper function that is a dependency of the target function. @@ -725,8 +731,12 @@ def prepare_module( """Parse/validate a module before optimization.""" ... - def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> None: - """One-time project setup after language detection. Default: no-op.""" + def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> bool: + """One-time project setup after language detection. Default: no-op. + + Returns True if the project is valid for optimization, False otherwise. + """ + return True def adjust_test_config_for_discovery(self, test_cfg: TestConfig) -> None: """Adjust test config before test discovery. Default: no-op.""" diff --git a/codeflash/languages/java/support.py b/codeflash/languages/java/support.py index 9e6149e1b..ab3818348 100644 --- a/codeflash/languages/java/support.py +++ b/codeflash/languages/java/support.py @@ -403,11 +403,12 @@ def load_coverage( ) -> None: return None - def setup_test_config(self, test_cfg: Any, file_path: Path, current_worktree: Path | None = None) -> None: + def setup_test_config(self, test_cfg: Any, file_path: Path, current_worktree: Path | None = None) -> bool: """Detect test framework from project build config (pom.xml / build.gradle).""" config = detect_java_project(test_cfg.project_root_path) if config is not None: self._test_framework = config.test_framework + return True def adjust_test_config_for_discovery(self, test_cfg: Any) -> None: """Adjust test config before test discovery for Java. diff --git a/codeflash/languages/javascript/optimizer.py b/codeflash/languages/javascript/optimizer.py index bc88786b1..1fe382ed2 100644 --- a/codeflash/languages/javascript/optimizer.py +++ b/codeflash/languages/javascript/optimizer.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from codeflash.cli_cmds.console import logger +from codeflash.languages.base import SetupError from codeflash.models.models import ValidCode if TYPE_CHECKING: @@ -25,11 +26,13 @@ def prepare_javascript_module( return validated_original_code, None -def verify_js_requirements(test_cfg: TestConfig) -> None: +def verify_js_requirements(test_cfg: TestConfig) -> list[SetupError]: """Verify JavaScript/TypeScript requirements before optimization. Checks that Node.js, npm, and the test framework are available. Logs warnings if requirements are not met but does not abort. + + Returns: List of setup errors if requirements are not met, empty list otherwise. """ from codeflash.languages import get_language_support from codeflash.languages.base import Language @@ -37,7 +40,7 @@ def verify_js_requirements(test_cfg: TestConfig) -> None: js_project_root = test_cfg.js_project_root if not js_project_root: - return + return [SetupError("JavaScript project root not found", should_abort=True)] try: js_support = get_language_support(Language.JAVASCRIPT) @@ -47,6 +50,9 @@ def verify_js_requirements(test_cfg: TestConfig) -> None: if not success: logger.warning("JavaScript requirements check found issues:") for error in errors: - logger.warning(f" - {error}") + logger.warning(f" - {error.message}") + return errors + return [] except Exception as e: logger.debug(f"Failed to verify JS requirements: {e}") + return [SetupError(str(e), should_abort=True)] diff --git a/codeflash/languages/javascript/support.py b/codeflash/languages/javascript/support.py index db96c4df1..3f5c13b9e 100644 --- a/codeflash/languages/javascript/support.py +++ b/codeflash/languages/javascript/support.py @@ -14,7 +14,15 @@ from codeflash.code_utils.git_utils import git_root_dir, mirror_path from codeflash.discovery.functions_to_optimize import FunctionToOptimize -from codeflash.languages.base import CodeContext, FunctionFilterCriteria, HelperFunction, Language, TestInfo, TestResult +from codeflash.languages.base import ( + CodeContext, + FunctionFilterCriteria, + HelperFunction, + Language, + SetupError, + TestInfo, + TestResult, +) from codeflash.languages.javascript.treesitter import TreeSitterAnalyzer, TreeSitterLanguage, get_analyzer_for_file from codeflash.languages.registry import register_language from codeflash.models.models import FunctionParent @@ -1950,11 +1958,13 @@ def prepare_module( return prepare_javascript_module(module_code, module_path) - def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None) -> None: + def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None) -> bool: from codeflash.languages.javascript.optimizer import verify_js_requirements from codeflash.languages.javascript.test_runner import find_node_project_root test_cfg.js_project_root = find_node_project_root(file_path) + if test_cfg.js_project_root is None: + return False if current_worktree is not None: original_js_root = git_root_dir() worktree_node_modules = test_cfg.js_project_root / "node_modules" @@ -1970,7 +1980,11 @@ def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_workt original_root_node_modules = original_js_root / "node_modules" if original_root_node_modules.exists() and not worktree_root_node_modules.exists(): worktree_root_node_modules.symlink_to(original_root_node_modules) - verify_js_requirements(test_cfg) + setup_errors = verify_js_requirements(test_cfg) + if any(e.should_abort for e in setup_errors): + return False + + return True def adjust_test_config_for_discovery(self, test_cfg: TestConfig) -> None: test_cfg.tests_project_rootdir = test_cfg.tests_root @@ -2216,7 +2230,7 @@ def get_module_path(self, source_file: Path, project_root: Path, tests_root: Pat rel_path = source_file.relative_to(project_root) return "../" + rel_path.with_suffix("").as_posix() - def verify_requirements(self, project_root: Path, test_framework: str = "jest") -> tuple[bool, list[str]]: + def verify_requirements(self, project_root: Path, test_framework: str = "jest") -> tuple[bool, list[SetupError]]: """Verify that all JavaScript requirements are met. Checks for: @@ -2236,27 +2250,40 @@ def verify_requirements(self, project_root: Path, test_framework: str = "jest") Tuple of (success, list of error messages). """ - errors: list[str] = [] + errors: list[SetupError] = [] # Check Node.js try: result = subprocess.run(["node", "--version"], check=False, capture_output=True, text=True, timeout=10) if result.returncode != 0: - errors.append("Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/") + errors.append( + SetupError( + "Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/", + should_abort=True, + ) + ) except FileNotFoundError: - errors.append("Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/") + errors.append( + SetupError( + "Node.js is not installed. Please install Node.js 18+ from https://nodejs.org/", should_abort=True + ) + ) except Exception as e: - errors.append(f"Failed to check Node.js: {e}") + errors.append(SetupError(f"Failed to check Node.js: {e}", should_abort=True)) # Check npm try: result = subprocess.run(["npm", "--version"], check=False, capture_output=True, text=True, timeout=10) if result.returncode != 0: - errors.append("npm is not available. Please ensure npm is installed with Node.js.") + errors.append( + SetupError("npm is not available. Please ensure npm is installed with Node.js.", should_abort=True) + ) except FileNotFoundError: - errors.append("npm is not available. Please ensure npm is installed with Node.js.") + errors.append( + SetupError("npm is not available. Please ensure npm is installed with Node.js.", should_abort=True) + ) except Exception as e: - errors.append(f"Failed to check npm: {e}") + errors.append(SetupError(f"Failed to check npm: {e}", should_abort=True)) # Check test framework is installed (with monorepo support) # Uses find_node_modules_with_package which searches up the directory tree @@ -2270,12 +2297,17 @@ def verify_requirements(self, project_root: Path, test_framework: str = "jest") local_node_modules = project_root / "node_modules" if not local_node_modules.exists(): errors.append( - f"node_modules not found in {project_root}. Please run 'npm install' to install dependencies." + SetupError( + f"node_modules not found in {project_root}. Please run 'npm install' to install dependencies.", + should_abort=True, + ) ) else: errors.append( - f"{test_framework} is not installed. " - f"Please run 'npm install --save-dev {test_framework}' to install it." + SetupError( + f"{test_framework} is not installed. Please run 'npm install --save-dev {test_framework}' to install it.", + should_abort=True, + ) ) return len(errors) == 0, errors diff --git a/codeflash/languages/python/support.py b/codeflash/languages/python/support.py index 596073590..34f0527b2 100644 --- a/codeflash/languages/python/support.py +++ b/codeflash/languages/python/support.py @@ -1044,8 +1044,9 @@ def prepare_module( pytest_cmd: str = "pytest" - def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> None: + def setup_test_config(self, test_cfg: TestConfig, file_path: Path, current_worktree: Path | None = None) -> bool: self.pytest_cmd = test_cfg.pytest_cmd or "pytest" + return True def pytest_cmd_tokens(self, is_posix: bool) -> list[str]: import shlex diff --git a/codeflash/optimization/optimizer.py b/codeflash/optimization/optimizer.py index fef53a760..8c2f66db4 100644 --- a/codeflash/optimization/optimizer.py +++ b/codeflash/optimization/optimizer.py @@ -526,7 +526,11 @@ def run(self) -> None: if funcs and funcs[0].language: set_current_language(funcs[0].language) self.test_cfg.set_language(funcs[0].language) - current_language_support().setup_test_config(self.test_cfg, file_path, self.current_worktree) + if not current_language_support().setup_test_config( + self.test_cfg, file_path, self.current_worktree + ): + logger.error("Project setup failed — aborting optimization. Check warnings above for details.") + return break if self.args.all: diff --git a/tests/test_languages/test_javascript_requirements.py b/tests/test_languages/test_javascript_requirements.py index dc95d5584..efefda228 100644 --- a/tests/test_languages/test_javascript_requirements.py +++ b/tests/test_languages/test_javascript_requirements.py @@ -91,7 +91,7 @@ def test_verify_requirements_fails_without_node(self, js_support, project_with_j assert success is False assert len(errors) >= 1 - node_error_found = any("Node.js" in error for error in errors) + node_error_found = any("Node.js" in error.message for error in errors) assert node_error_found is True def test_verify_requirements_fails_without_npm(self, js_support, project_with_jest): @@ -108,7 +108,7 @@ def mock_run_side_effect(cmd, **kwargs): success, errors = js_support.verify_requirements(project_with_jest, "jest") assert success is False - npm_error_found = any("npm" in error for error in errors) + npm_error_found = any("npm" in error.message for error in errors) assert npm_error_found is True def test_verify_requirements_fails_without_node_modules(self, js_support, project_without_node_modules): @@ -120,11 +120,11 @@ def test_verify_requirements_fails_without_node_modules(self, js_support, projec assert success is False assert len(errors) == 1 - expected_error = ( + expected_message = ( f"node_modules not found in {project_without_node_modules}. " f"Please run 'npm install' to install dependencies." ) - assert errors[0] == expected_error + assert errors[0].message == expected_message def test_verify_requirements_fails_without_test_framework(self, js_support, project_without_jest): """Test verification fails when test framework is not installed.""" @@ -135,8 +135,8 @@ def test_verify_requirements_fails_without_test_framework(self, js_support, proj assert success is False assert len(errors) == 1 - expected_error = "jest is not installed. Please run 'npm install --save-dev jest' to install it." - assert errors[0] == expected_error + expected_message = "jest is not installed. Please run 'npm install --save-dev jest' to install it." + assert errors[0].message == expected_message def test_verify_requirements_returns_multiple_errors(self, js_support, project_without_node_modules): """Test that multiple errors can be returned.""" @@ -148,7 +148,7 @@ def test_verify_requirements_returns_multiple_errors(self, js_support, project_w assert success is False assert len(errors) >= 2 # Should have errors for Node.js, npm, and node_modules - error_text = " ".join(errors) + error_text = " ".join(e.message for e in errors) assert "Node.js" in error_text assert "npm" in error_text @@ -161,8 +161,8 @@ def test_verify_requirements_vitest_not_installed(self, js_support, project_with assert success is False assert len(errors) == 1 - expected_error = "vitest is not installed. Please run 'npm install --save-dev vitest' to install it." - assert errors[0] == expected_error + expected_message = "vitest is not installed. Please run 'npm install --save-dev vitest' to install it." + assert errors[0].message == expected_message def test_verify_requirements_jest_not_installed(self, js_support, project_with_vitest): """Test verification fails when Jest is requested but only Vitest is installed.""" @@ -173,8 +173,46 @@ def test_verify_requirements_jest_not_installed(self, js_support, project_with_v assert success is False assert len(errors) == 1 - expected_error = "jest is not installed. Please run 'npm install --save-dev jest' to install it." - assert errors[0] == expected_error + expected_message = "jest is not installed. Please run 'npm install --save-dev jest' to install it." + assert errors[0].message == expected_message + + +class TestSetupTestConfig: + """Tests for JavaScriptSupport.setup_test_config() early-exit behavior.""" + + @pytest.fixture + def js_support(self): + return JavaScriptSupport() + + def test_setup_test_config_returns_false_on_abort_error(self, js_support, tmp_path): + """setup_test_config returns False when verify_js_requirements reports a should_abort error.""" + from codeflash.languages.base import SetupError + + abort_error = SetupError("Node.js is not installed", should_abort=True) + with ( + patch("codeflash.languages.javascript.test_runner.find_node_project_root", return_value=tmp_path.resolve()), + patch("codeflash.languages.javascript.optimizer.verify_js_requirements", return_value=[abort_error]), + ): + test_cfg = MagicMock() + result = js_support.setup_test_config(test_cfg, tmp_path.resolve(), current_worktree=None) + assert result is False + + def test_setup_test_config_returns_true_on_no_errors(self, js_support, tmp_path): + """setup_test_config returns True when verify_js_requirements reports no errors.""" + with ( + patch("codeflash.languages.javascript.test_runner.find_node_project_root", return_value=tmp_path.resolve()), + patch("codeflash.languages.javascript.optimizer.verify_js_requirements", return_value=[]), + ): + test_cfg = MagicMock() + result = js_support.setup_test_config(test_cfg, tmp_path.resolve(), current_worktree=None) + assert result is True + + def test_setup_test_config_returns_false_when_project_root_is_none(self, js_support, tmp_path): + """setup_test_config returns False when find_node_project_root returns None.""" + with patch("codeflash.languages.javascript.test_runner.find_node_project_root", return_value=None): + test_cfg = MagicMock() + result = js_support.setup_test_config(test_cfg, tmp_path.resolve(), current_worktree=None) + assert result is False class TestVerifyRequirementsIntegration: