From f26cc26e3f9b81a5ce99a6c32520fb04572febbc Mon Sep 17 00:00:00 2001 From: yadavchiragg Date: Wed, 11 Nov 2026 16:54:07 +0530 Subject: [PATCH] feat(doctor): check for Java (javac) as an optional tool --- concore_cli/commands/doctor.py | 43 ++++++++++++++++- tests/test_doctor.py | 86 ++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) diff --git a/concore_cli/commands/doctor.py b/concore_cli/commands/doctor.py index 254edfd0..49cdda51 100644 --- a/concore_cli/commands/doctor.py +++ b/concore_cli/commands/doctor.py @@ -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", @@ -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: @@ -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"]) @@ -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}" ) diff --git a/tests/test_doctor.py b/tests/test_doctor.py index e9ec5846..711557d1 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -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()