Skip to content
Open
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
43 changes: 41 additions & 2 deletions concore_cli/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,27 @@
"Windows": "winget install -e --id Docker.DockerDesktop",
},
},
"Java (javac)": {
"names": {
"posix": ["javac"],
"windows": ["javac"],
},
"version_flag": "-version",
"config_keys": ["JAVACEXE", "JAVACWIN", "JAVAEXE", "JAVAWIN"],
# On macOS /usr/bin/javac is a stub even without a JDK, so also
# confirm that the tool actually runs.
"verify_runs": True,
"install_hints": {
"Linux": "sudo apt install default-jdk",
"Darwin": "brew install --cask temurin",
"Windows": "winget install -e --id Microsoft.OpenJDK.21",
},
},
}

# Tools that are optional: a missing one is a warning, not an error.
OPTIONAL_TOOLS = ("MATLAB", "Verilog (iverilog)", "Docker", "Java (javac)")

REQUIRED_PACKAGES = [
"click",
"rich",
Expand Down Expand Up @@ -159,6 +178,23 @@ def _get_version(path, version_flag):
return None


def _tool_runs(path, version_flag):
"""Return True if the tool runs successfully with its version flag.

Some tools exist on PATH only as a stub (for example /usr/bin/javac on
macOS without a JDK), which prints an error and exits non-zero.
"""
try:
if isinstance(version_flag, list):
cmd = [path] + version_flag
else:
cmd = [path, version_flag]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return result.returncode == 0
except Exception:
return False


def _check_docker_daemon(docker_path):
"""Check if Docker daemon is running."""
try:
Expand Down Expand Up @@ -249,6 +285,9 @@ def doctor_check(console):
for tool_label, tool_def in TOOL_DEFINITIONS.items():
candidates = tool_def["names"].get(plat_key, [])
path, found_name = _detect_tool(candidates)
if path and tool_def.get("verify_runs"):
if not _tool_runs(path, tool_def["version_flag"]):
path, found_name = None, None

if path:
version = _get_version(path, tool_def["version_flag"])
Expand Down Expand Up @@ -277,8 +316,8 @@ def doctor_check(console):
else:
hint = tool_def["install_hints"].get(plat_name, "")
hint_str = f" (install: {hint})" if hint else ""
# Docker, MATLAB, Verilog are optional - show as warning
if tool_label in ("MATLAB", "Verilog (iverilog)", "Docker"):
# Docker, MATLAB, Verilog, Java are optional - show as warning
if tool_label in OPTIONAL_TOOLS:
console.print(
f" [yellow]![/yellow] {tool_label} -> Not found{hint_str}"
)
Expand Down
86 changes: 86 additions & 0 deletions tests/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,5 +209,91 @@ def test_doctor_with_concore_sudo(self, mock_path):
self.assertIsInstance(result, bool)


class TestDoctorJavaCheck(unittest.TestCase):
"""Tests for the optional Java (javac) check."""

def _run_doctor(self):
import io
from rich.console import Console

buffer = io.StringIO()
console = Console(file=buffer, no_color=True, width=200)
doctor_check(console)
return buffer.getvalue()

def _java_line(self, output):
for line in output.splitlines():
if "Java (javac)" in line:
return line.strip()
self.fail("No Java (javac) line in doctor output")

def _fake_which(self, javac_path):
real_which = shutil.which

def fake_which(name, *args, **kwargs):
if name == "javac":
return javac_path
return real_which(name, *args, **kwargs)

return fake_which

def test_java_is_defined(self):
"""Java should be a known tool on both platforms."""
from concore_cli.commands.doctor import TOOL_DEFINITIONS

java = TOOL_DEFINITIONS["Java (javac)"]
self.assertEqual(java["names"]["posix"], ["javac"])
self.assertEqual(java["names"]["windows"], ["javac"])

def test_java_config_keys_match_mkconcore(self):
"""Config keys should match the Java settings used by mkconcore.py."""
from concore_cli.commands.doctor import TOOL_DEFINITIONS

java = TOOL_DEFINITIONS["Java (javac)"]
self.assertEqual(
set(java["config_keys"]),
{"JAVACEXE", "JAVACWIN", "JAVAEXE", "JAVAWIN"},
)

def test_missing_java_is_a_warning(self):
"""A missing JDK should warn, not count as an error."""
with patch(
"concore_cli.commands.doctor.shutil.which",
side_effect=self._fake_which(None),
):
output = self._run_doctor()
line = self._java_line(output)
self.assertTrue(line.startswith("!"))
self.assertIn("Not found", line)

def test_failing_javac_stub_is_reported_missing(self):
"""A javac that exists but cannot run (macOS stub) counts as missing."""
with patch(
"concore_cli.commands.doctor.shutil.which",
side_effect=self._fake_which("/usr/bin/javac"),
):
with patch("concore_cli.commands.doctor._tool_runs", return_value=False):
output = self._run_doctor()
line = self._java_line(output)
self.assertTrue(line.startswith("!"))
self.assertIn("Not found", line)

def test_working_javac_is_reported_found(self):
"""A working javac should be shown as passed with its version."""
with patch(
"concore_cli.commands.doctor.shutil.which",
side_effect=self._fake_which("/usr/bin/javac"),
):
with patch("concore_cli.commands.doctor._tool_runs", return_value=True):
with patch(
"concore_cli.commands.doctor._get_version",
return_value="javac 21.0.1",
):
output = self._run_doctor()
line = self._java_line(output)
self.assertTrue(line.startswith("+"))
self.assertIn("javac 21.0.1", line)


if __name__ == "__main__":
unittest.main()
Loading