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
14 changes: 12 additions & 2 deletions codeflash/languages/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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."""
Expand Down
3 changes: 2 additions & 1 deletion codeflash/languages/java/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 9 additions & 3 deletions codeflash/languages/javascript/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -25,19 +26,21 @@ 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
from codeflash.languages.test_framework import get_js_test_framework_or_default

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)
Expand All @@ -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)]
60 changes: 46 additions & 14 deletions codeflash/languages/javascript/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion codeflash/languages/python/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion codeflash/optimization/optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
60 changes: 49 additions & 11 deletions tests/test_languages/test_javascript_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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."""
Expand All @@ -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."""
Expand All @@ -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

Expand All @@ -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."""
Expand All @@ -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:
Expand Down
Loading