Skip to content

Commit 031e95e

Browse files
author
William Yang
committed
feat(check): add java.py module for Java language support
- Add java.compile() for compiling Java source files - Add java.run() for running compiled Java classes - Add java.version() for getting Java version info - Add java.clean() for removing .class files - Add unit tests for all functions - Export java module from check package
1 parent 9e1c80e commit 031e95e

File tree

3 files changed

+383
-1
lines changed

3 files changed

+383
-1
lines changed

bootcs/check/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ def _(s):
2727

2828
from . import regex
2929
from . import c
30+
from . import java
3031
from . import internal
3132
from .runner import check
3233
from pexpect import EOF
3334

3435
__all__ = ["import_checks", "data", "exists", "hash", "include", "regex",
3536
"run", "log", "Failure", "Mismatch", "Missing", "check", "EOF",
36-
"c", "internal", "hidden"]
37+
"c", "java", "internal", "hidden"]

bootcs/check/java.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
"""
2+
Java language support for bootcs check.
3+
4+
Provides compile() and run() functions for Java programs.
5+
"""
6+
7+
import os
8+
import re
9+
import shutil
10+
from pathlib import Path
11+
12+
from ._api import run as _run, log, Failure, exists
13+
14+
15+
def _(s):
16+
"""Translation function - returns string as-is for now."""
17+
return s
18+
19+
20+
#: Default Java compiler
21+
JAVAC = "javac"
22+
23+
#: Default Java runtime
24+
JAVA = "java"
25+
26+
#: Default compiler options
27+
JAVAC_OPTIONS = {
28+
"encoding": "UTF-8",
29+
}
30+
31+
32+
def _check_java_installed():
33+
"""Check if Java is installed and available."""
34+
if not shutil.which(JAVAC):
35+
raise Failure(_("javac not found. Make sure Java JDK is installed."))
36+
if not shutil.which(JAVA):
37+
raise Failure(_("java not found. Make sure Java JRE is installed."))
38+
39+
40+
def compile(*files, javac=JAVAC, classpath=None, **options):
41+
"""
42+
Compile Java source files.
43+
44+
:param files: Java source files to compile
45+
:param javac: Java compiler to use (default: javac)
46+
:param classpath: classpath for compilation (optional)
47+
:param options: additional compiler options
48+
:raises Failure: if compilation fails
49+
50+
Example usage::
51+
52+
import bootcs.check as check
53+
from bootcs.check import java
54+
55+
@check.check()
56+
def compiles():
57+
java.compile("Hello.java")
58+
59+
@check.check()
60+
def compiles_multiple():
61+
java.compile("Main.java", "Helper.java")
62+
"""
63+
_check_java_installed()
64+
65+
# Ensure all files exist
66+
for file in files:
67+
exists(file)
68+
69+
# Merge default options with provided options
70+
opts = {**JAVAC_OPTIONS, **options}
71+
72+
# Build command
73+
cmd_parts = [javac]
74+
75+
# Add classpath if provided
76+
if classpath:
77+
cmd_parts.extend(["-cp", classpath])
78+
79+
# Add options
80+
for key, value in opts.items():
81+
if value is True:
82+
cmd_parts.append(f"-{key}")
83+
elif value is not False and value is not None:
84+
cmd_parts.extend([f"-{key}", str(value)])
85+
86+
# Add source files
87+
cmd_parts.extend(files)
88+
89+
cmd = " ".join(cmd_parts)
90+
log(_("compiling {} with {}...").format(", ".join(files), javac))
91+
92+
# Run compilation and wait for it to complete
93+
proc = _run(cmd)
94+
proc._wait(timeout=60) # Wait for compilation to finish
95+
96+
# Check for compilation errors
97+
if proc.exitcode != 0:
98+
# Log compilation errors
99+
output = proc.process.before if proc.process.before else ""
100+
if output:
101+
log(_("compilation errors:"))
102+
for line in output.splitlines()[:20]: # Limit error output
103+
log(f" {line}")
104+
105+
raise Failure(_("code failed to compile. See log for details."))
106+
107+
return proc
108+
109+
110+
def run(classname, *args, java=JAVA, classpath=None):
111+
"""
112+
Run a compiled Java class.
113+
114+
:param classname: Name of the class to run (without .class extension)
115+
:param args: Command-line arguments to pass to the program
116+
:param java: Java runtime to use (default: java)
117+
:param classpath: classpath for execution (optional)
118+
:return: Process object for chaining (stdin, stdout, exit, etc.)
119+
120+
Example usage::
121+
122+
import bootcs.check as check
123+
from bootcs.check import java
124+
125+
@check.check("compiles")
126+
def prints_hello():
127+
java.run("Hello").stdout("hello, world")
128+
129+
@check.check("compiles")
130+
def accepts_input():
131+
java.run("Hello").stdin("David").stdout("hello, David")
132+
133+
@check.check("compiles")
134+
def with_args():
135+
java.run("Main", "arg1", "arg2").exit(0)
136+
"""
137+
_check_java_installed()
138+
139+
# Build command
140+
cmd_parts = [java]
141+
142+
# Add classpath if provided
143+
if classpath:
144+
cmd_parts.extend(["-cp", classpath])
145+
146+
# Add classname
147+
cmd_parts.append(classname)
148+
149+
# Add arguments
150+
cmd_parts.extend(str(arg) for arg in args)
151+
152+
cmd = " ".join(cmd_parts)
153+
log(_("running {}...").format(classname))
154+
155+
return _run(cmd)
156+
157+
158+
def version():
159+
"""
160+
Get Java version information.
161+
162+
:return: tuple of (java_version, javac_version)
163+
:raises Failure: if Java is not installed
164+
165+
Example usage::
166+
167+
java_ver, javac_ver = java.version()
168+
print(f"Java: {java_ver}, Javac: {javac_ver}")
169+
"""
170+
_check_java_installed()
171+
172+
# Get java version (java -version outputs to stderr)
173+
import subprocess
174+
try:
175+
result = subprocess.run([JAVA, "-version"], capture_output=True, text=True)
176+
java_version = result.stderr.splitlines()[0] if result.stderr else "unknown"
177+
except Exception:
178+
java_version = "unknown"
179+
180+
# Get javac version
181+
try:
182+
result = subprocess.run([JAVAC, "-version"], capture_output=True, text=True)
183+
javac_version = result.stdout.strip() or result.stderr.strip() or "unknown"
184+
except Exception:
185+
javac_version = "unknown"
186+
187+
return java_version, javac_version
188+
189+
190+
def clean(*patterns):
191+
"""
192+
Remove compiled .class files.
193+
194+
:param patterns: file patterns to clean (default: all .class files in current directory)
195+
196+
Example usage::
197+
198+
java.clean() # Remove all .class files
199+
java.clean("*.class", "bin/*.class") # Remove specific patterns
200+
"""
201+
if not patterns:
202+
patterns = ["*.class"]
203+
204+
for pattern in patterns:
205+
for path in Path(".").glob(pattern):
206+
if path.is_file():
207+
log(_("removing {}...").format(path))
208+
path.unlink()

tests/unit/test_java.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""
2+
Unit tests for java module.
3+
"""
4+
5+
import os
6+
import shutil
7+
import tempfile
8+
import pytest
9+
from pathlib import Path
10+
11+
# Skip all tests if Java is not installed
12+
pytestmark = pytest.mark.skipif(
13+
not shutil.which("javac") or not shutil.which("java"),
14+
reason="Java JDK not installed"
15+
)
16+
17+
18+
class TestJavaCompile:
19+
"""Test java.compile() function."""
20+
21+
def setup_method(self):
22+
"""Create a temporary directory for each test."""
23+
self.test_dir = tempfile.mkdtemp()
24+
self.original_dir = os.getcwd()
25+
os.chdir(self.test_dir)
26+
27+
def teardown_method(self):
28+
"""Clean up after each test."""
29+
os.chdir(self.original_dir)
30+
shutil.rmtree(self.test_dir, ignore_errors=True)
31+
32+
def test_compile_simple(self):
33+
"""Test compiling a simple Java file."""
34+
from bootcs.check import java
35+
36+
# Create a simple Java file
37+
with open("Hello.java", "w") as f:
38+
f.write("""
39+
public class Hello {
40+
public static void main(String[] args) {
41+
System.out.println("hello, world");
42+
}
43+
}
44+
""")
45+
46+
# Should compile without error
47+
java.compile("Hello.java")
48+
49+
# Check .class file was created
50+
assert os.path.exists("Hello.class")
51+
52+
def test_compile_error(self):
53+
"""Test compilation failure."""
54+
from bootcs.check import java
55+
from bootcs.check._api import Failure
56+
57+
# Create a Java file with syntax error
58+
with open("Bad.java", "w") as f:
59+
f.write("""
60+
public class Bad {
61+
public static void main(String[] args) {
62+
System.out.println("missing semicolon")
63+
}
64+
}
65+
""")
66+
67+
# Should raise Failure
68+
with pytest.raises(Failure):
69+
java.compile("Bad.java")
70+
71+
def test_compile_file_not_exists(self):
72+
"""Test compilation of non-existent file."""
73+
from bootcs.check import java
74+
from bootcs.check._api import Failure
75+
76+
with pytest.raises(Failure):
77+
java.compile("NotExist.java")
78+
79+
80+
class TestJavaRun:
81+
"""Test java.run() function."""
82+
83+
def setup_method(self):
84+
"""Create a temporary directory for each test."""
85+
self.test_dir = tempfile.mkdtemp()
86+
self.original_dir = os.getcwd()
87+
os.chdir(self.test_dir)
88+
89+
# Create and compile a test Java file
90+
with open("Hello.java", "w") as f:
91+
f.write("""
92+
import java.util.Scanner;
93+
94+
public class Hello {
95+
public static void main(String[] args) {
96+
if (args.length > 0) {
97+
System.out.println("hello, " + args[0]);
98+
} else {
99+
Scanner scanner = new Scanner(System.in);
100+
System.out.print("What is your name? ");
101+
String name = scanner.nextLine();
102+
System.out.println("hello, " + name);
103+
scanner.close();
104+
}
105+
}
106+
}
107+
""")
108+
os.system("javac Hello.java")
109+
110+
def teardown_method(self):
111+
"""Clean up after each test."""
112+
os.chdir(self.original_dir)
113+
shutil.rmtree(self.test_dir, ignore_errors=True)
114+
115+
def test_run_simple(self):
116+
"""Test running a simple Java program."""
117+
from bootcs.check import java
118+
119+
proc = java.run("Hello", "World")
120+
proc.stdout("hello, World")
121+
122+
def test_run_with_stdin(self):
123+
"""Test running Java program with stdin."""
124+
from bootcs.check import java
125+
126+
proc = java.run("Hello")
127+
proc.stdin("David")
128+
proc.stdout("hello, David")
129+
130+
131+
class TestJavaClean:
132+
"""Test java.clean() function."""
133+
134+
def setup_method(self):
135+
"""Create a temporary directory for each test."""
136+
self.test_dir = tempfile.mkdtemp()
137+
self.original_dir = os.getcwd()
138+
os.chdir(self.test_dir)
139+
140+
def teardown_method(self):
141+
"""Clean up after each test."""
142+
os.chdir(self.original_dir)
143+
shutil.rmtree(self.test_dir, ignore_errors=True)
144+
145+
def test_clean_class_files(self):
146+
"""Test cleaning .class files."""
147+
from bootcs.check import java
148+
149+
# Create some .class files
150+
Path("Hello.class").touch()
151+
Path("World.class").touch()
152+
153+
assert os.path.exists("Hello.class")
154+
assert os.path.exists("World.class")
155+
156+
java.clean()
157+
158+
assert not os.path.exists("Hello.class")
159+
assert not os.path.exists("World.class")
160+
161+
162+
class TestJavaVersion:
163+
"""Test java.version() function."""
164+
165+
def test_version(self):
166+
"""Test getting Java version."""
167+
from bootcs.check import java
168+
169+
java_ver, javac_ver = java.version()
170+
171+
# Should return non-empty strings
172+
assert java_ver
173+
assert javac_ver

0 commit comments

Comments
 (0)