Skip to content

Commit 051ebe5

Browse files
committed
2026-03-17
1 parent 2e0180a commit 051ebe5

9 files changed

Lines changed: 459 additions & 49 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ node_modules/
1616
.venv/
1717
*.pyc
1818
*.o
19+
**/cptest

os-unix/bin/cptest.rs

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
use std::env;
2+
use std::fs;
3+
use std::io::Write;
4+
use std::os::unix::process::CommandExt;
5+
use std::path::Path;
6+
use std::process::{Command, Stdio, exit};
7+
8+
fn find_source_file() -> Result<String, String> {
9+
// Check for OCaml files.
10+
let ocaml_files: Vec<_> = fs::read_dir(".")
11+
.map_err(|e| e.to_string())?
12+
.filter_map(|entry| entry.ok())
13+
.map(|entry| entry.path())
14+
.filter(|path| path.extension().and_then(|s| s.to_str()) == Some("ml"))
15+
.collect();
16+
if !ocaml_files.is_empty() {
17+
if ocaml_files.len() > 1 {
18+
return Err("Error: Multiple OCaml files found".to_string());
19+
}
20+
return Ok(ocaml_files[0].to_string_lossy().to_string());
21+
}
22+
23+
// Check for C++ files.
24+
let cpp_files: Vec<_> = fs::read_dir(".")
25+
.map_err(|e| e.to_string())?
26+
.filter_map(|entry| entry.ok())
27+
.map(|entry| entry.path())
28+
.filter(|path| path.extension().and_then(|s| s.to_str()) == Some("cpp"))
29+
.collect();
30+
if !cpp_files.is_empty() {
31+
if cpp_files.len() > 1 {
32+
return Err("Error: Multiple C++ files found".to_string());
33+
}
34+
return Ok(cpp_files[0].to_string_lossy().to_string());
35+
}
36+
37+
// Check for Java files.
38+
let java_files: Vec<_> = fs::read_dir(".")
39+
.map_err(|e| e.to_string())?
40+
.filter_map(|entry| entry.ok())
41+
.map(|entry| entry.path())
42+
.filter(|path| path.extension().and_then(|s| s.to_str()) == Some("java"))
43+
.collect();
44+
if !java_files.is_empty() {
45+
if java_files.len() > 1 {
46+
return Err("Error: Multiple Java files found".to_string());
47+
}
48+
return Ok(java_files[0].to_string_lossy().to_string());
49+
}
50+
51+
// Check for Python files.
52+
let py_files: Vec<_> = fs::read_dir(".")
53+
.map_err(|e| e.to_string())?
54+
.filter_map(|entry| entry.ok())
55+
.map(|entry| entry.path())
56+
.filter(|path| path.extension().and_then(|s| s.to_str()) == Some("py"))
57+
.collect();
58+
if !py_files.is_empty() {
59+
if py_files.len() > 1 {
60+
return Err("Error: Multiple Python files found".to_string());
61+
}
62+
return Ok(py_files[0].to_string_lossy().to_string());
63+
}
64+
65+
Err("Error: No source file found".to_string())
66+
}
67+
68+
fn compile_file(filepath: &str, debug: bool) -> Result<(), String> {
69+
let path = Path::new(filepath);
70+
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
71+
let exe_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("a.out");
72+
73+
match ext {
74+
"ml" => {
75+
let mut args = vec!["-o", exe_name, filepath];
76+
if debug {
77+
args.insert(0, "-g");
78+
}
79+
let status = Command::new("ocamlopt")
80+
.args(&args)
81+
.status()
82+
.map_err(|e| format!("Failed to execute ocamlopt: {}", e))?;
83+
84+
if status.success() {
85+
Ok(())
86+
} else {
87+
Err("OCaml compilation failed".to_string())
88+
}
89+
}
90+
"cpp" => {
91+
let mut args = vec!["-x", "c++", "-O2", "-std=gnu++20", "-static"];
92+
if debug {
93+
args.push("-g");
94+
args.push("-fsanitize=address,undefined");
95+
}
96+
args.push(filepath);
97+
args.push("-o");
98+
args.push(exe_name);
99+
100+
let status = Command::new("g++")
101+
.args(&args)
102+
.status()
103+
.map_err(|e| format!("Failed to execute gcc: {}", e))?;
104+
105+
if status.success() {
106+
Ok(())
107+
} else {
108+
Err("C++ compilation failed".to_string())
109+
}
110+
}
111+
"java" => {
112+
let mut args = vec![
113+
"--source",
114+
"21",
115+
"-encoding",
116+
"UTF-8",
117+
"-sourcepath",
118+
".",
119+
"-d",
120+
".",
121+
];
122+
if debug {
123+
args.push("-g");
124+
}
125+
args.push(filepath);
126+
127+
let status = Command::new("javac")
128+
.args(&args)
129+
.status()
130+
.map_err(|e| format!("Failed to execute javac: {}", e))?;
131+
132+
if status.success() {
133+
Ok(())
134+
} else {
135+
Err("Java compilation failed".to_string())
136+
}
137+
}
138+
"py" => Ok(()),
139+
_ => Err(format!("Error: Unknown file type: .{}", ext)),
140+
}
141+
}
142+
143+
fn find_test_files() -> Vec<(String, String)> {
144+
let mut tests = Vec::new();
145+
146+
if let Ok(entries) = fs::read_dir(".") {
147+
let mut inputs: Vec<String> = entries
148+
.filter_map(|entry| entry.ok())
149+
.map(|entry| entry.file_name().to_string_lossy().to_string())
150+
.filter(|name| name.starts_with("~input") && name.ends_with(".txt"))
151+
.collect();
152+
153+
inputs.sort();
154+
155+
for input in inputs {
156+
let output = input.replace("~input", "~output");
157+
if Path::new(&output).exists() {
158+
tests.push((input, output));
159+
}
160+
}
161+
}
162+
163+
tests
164+
}
165+
166+
fn run_tests(filepath: &str) -> Result<(), String> {
167+
let path = Path::new(filepath);
168+
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
169+
let exe_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("a.out");
170+
let exe_path = format!("./{}", exe_name);
171+
172+
let tests = find_test_files();
173+
174+
if tests.is_empty() {
175+
println!("No test files found (looking for ~input*.txt and ~output*.txt)");
176+
return Ok(());
177+
}
178+
179+
println!(
180+
"Running {} {}\n",
181+
tests.len(),
182+
if tests.len() == 1 { "test" } else { "tests" }
183+
);
184+
for (test_num, (input_file, output_file)) in tests.iter().enumerate() {
185+
let input_data =
186+
fs::read(&input_file).map_err(|e| format!("Failed to read {}: {}", input_file, e))?;
187+
let expected_output = fs::read_to_string(&output_file)
188+
.map_err(|e| format!("Failed to read {}: {}", output_file, e))?;
189+
190+
let output = match ext {
191+
"ml" | "cpp" => {
192+
let mut child = Command::new(&exe_path)
193+
.stdin(Stdio::piped())
194+
.stdout(Stdio::piped())
195+
.spawn()
196+
.map_err(|e| format!("Failed to execute {}: {}", exe_name, e))?;
197+
198+
if let Some(mut stdin) = child.stdin.take() {
199+
stdin
200+
.write_all(&input_data)
201+
.map_err(|e| format!("Failed to write to stdin: {}", e))?;
202+
}
203+
204+
let output = child
205+
.wait_with_output()
206+
.map_err(|e| format!("Failed to wait for {}: {}", exe_name, e))?;
207+
208+
String::from_utf8_lossy(&output.stdout).to_string()
209+
}
210+
"java" => {
211+
let classname = path
212+
.file_stem()
213+
.and_then(|s| s.to_str())
214+
.ok_or("Invalid Java filename")?;
215+
216+
let mut child = Command::new("java")
217+
.args(&[
218+
"-Dfile.encoding=UTF-8",
219+
"-XX:+UseSerialGC",
220+
"-Xss64m",
221+
classname,
222+
])
223+
.stdin(Stdio::piped())
224+
.stdout(Stdio::piped())
225+
.spawn()
226+
.map_err(|e| format!("Failed to execute java: {}", e))?;
227+
228+
if let Some(mut stdin) = child.stdin.take() {
229+
stdin
230+
.write_all(&input_data)
231+
.map_err(|e| format!("Failed to write to stdin: {}", e))?;
232+
}
233+
234+
let output = child
235+
.wait_with_output()
236+
.map_err(|e| format!("Failed to wait for java: {}", e))?;
237+
238+
String::from_utf8_lossy(&output.stdout).to_string()
239+
}
240+
"py" => {
241+
let mut child = Command::new("pypy3")
242+
.arg(filepath)
243+
.stdin(Stdio::piped())
244+
.stdout(Stdio::piped())
245+
.spawn()
246+
.map_err(|e| format!("Failed to execute pypy3: {}", e))?;
247+
248+
if let Some(mut stdin) = child.stdin.take() {
249+
stdin
250+
.write_all(&input_data)
251+
.map_err(|e| format!("Failed to write to stdin: {}", e))?;
252+
}
253+
254+
let output = child
255+
.wait_with_output()
256+
.map_err(|e| format!("Failed to wait for pypy3: {}", e))?;
257+
258+
String::from_utf8_lossy(&output.stdout).to_string()
259+
}
260+
_ => return Err(format!("Error: Cannot execute file type: .{}", ext)),
261+
};
262+
263+
let actual_trimmed = output.trim_end();
264+
let expected_trimmed = expected_output.trim_end();
265+
266+
if actual_trimmed == expected_trimmed {
267+
println!("✓ PASS TEST {}", test_num + 1);
268+
println!("{}", actual_trimmed);
269+
} else {
270+
println!("✗ FAIL TEST {}", test_num + 1);
271+
println!("--- INPUT ---");
272+
println!("{}", String::from_utf8_lossy(&input_data).trim_end());
273+
println!("\n--- EXPECTED ---");
274+
println!("{}", expected_trimmed);
275+
println!("\n--- ACTUAL ---");
276+
println!("{}", actual_trimmed);
277+
}
278+
279+
if test_num != tests.len() - 1 {
280+
println!();
281+
}
282+
}
283+
284+
Ok(())
285+
}
286+
287+
fn main() {
288+
let args: Vec<String> = env::args().collect();
289+
let mut test = false;
290+
let mut execute = false;
291+
let mut debug = false;
292+
let mut filepath: Option<String> = None;
293+
for arg in args.iter().skip(1) {
294+
match arg.as_str() {
295+
"-h" | "--help" => {
296+
println!("Usage: test [OPTIONS] [FILE]");
297+
println!();
298+
println!(
299+
"Compile and optionally test or execute competitive programming solutions."
300+
);
301+
println!();
302+
println!("OPTIONS:");
303+
println!(
304+
" -d Compile with debug flags (-g -fsanitize=address,undefined)"
305+
);
306+
println!(" -t Run all tests (~input*.txt / ~output*.txt files)");
307+
println!(" -x Execute the compiled program (replaces current process)");
308+
println!(" -h, --help Show this help message");
309+
println!();
310+
println!("FILE:");
311+
println!(" Source file to compile (auto-detected if not specified)");
312+
println!(" Supports: .cpp, .ml, .java, .py");
313+
exit(0);
314+
}
315+
"-t" => test = true,
316+
"-x" => execute = true,
317+
"-d" => debug = true,
318+
s if !s.starts_with('-') => {
319+
if filepath.is_none() {
320+
filepath = Some(s.to_string());
321+
}
322+
}
323+
_ => {}
324+
}
325+
}
326+
327+
if test && execute {
328+
eprintln!("Error: Cannot use both -t and -x flags together");
329+
exit(1);
330+
}
331+
332+
let filepath = match filepath {
333+
Some(f) => f,
334+
None => match find_source_file() {
335+
Ok(f) => f,
336+
Err(e) => {
337+
eprintln!("{}", e);
338+
exit(1);
339+
}
340+
},
341+
};
342+
343+
if !Path::new(&filepath).exists() {
344+
eprintln!("Error: File not found: {}", filepath);
345+
exit(1);
346+
}
347+
348+
println!("Compiling {}", filepath);
349+
if let Err(e) = compile_file(&filepath, debug) {
350+
eprintln!("{}", e);
351+
exit(1);
352+
}
353+
354+
if test {
355+
if let Err(e) = run_tests(&filepath) {
356+
eprintln!("{}", e);
357+
exit(1);
358+
}
359+
}
360+
361+
if execute {
362+
println!("Executing {}", filepath);
363+
println!("\u{2E3B}");
364+
365+
let path = Path::new(&filepath);
366+
let ext = path.extension().and_then(|s| s.to_str()).unwrap_or("");
367+
let exe_name = path.file_stem().and_then(|s| s.to_str()).unwrap_or("a.out");
368+
let exe_path = format!("./{}", exe_name);
369+
370+
let err = match ext {
371+
"ml" | "cpp" => Command::new(&exe_path).exec(),
372+
"java" => {
373+
let classname = path.file_stem().and_then(|s| s.to_str()).unwrap_or("Main");
374+
375+
Command::new("java")
376+
.args(&[
377+
"-Dfile.encoding=UTF-8",
378+
"-XX:+UseSerialGC",
379+
"-Xss64m",
380+
classname,
381+
])
382+
.exec()
383+
}
384+
"py" => Command::new("pypy3").arg(&filepath).exec(),
385+
_ => {
386+
eprintln!("Error: Cannot execute file type: .{}", ext);
387+
exit(1);
388+
}
389+
};
390+
391+
eprintln!("Failed to exec: {}", err);
392+
exit(1);
393+
}
394+
}

0 commit comments

Comments
 (0)