use crate::client::{ClientError, UhuntSubmission, UvaClient};
use crate::parser::Verdict;
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
const DEFAULT_MAX_DURATION: u64 = 120;
const SPINNER_TICK_MS: u64 = 100;
pub async fn wait_for_verdict(
client: &UvaClient,
max_duration_secs: u64,
) -> Result<Verdict, ClientError> {
let timeout = if max_duration_secs == 0 {
DEFAULT_MAX_DURATION
} else {
max_duration_secs
};
let pb = ProgressBar::new_spinner();
pb.set_style(
ProgressStyle::default_spinner()
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"])
.template("{spinner:.cyan} {msg}")
.expect("valid spinner template"),
);
pb.set_message(format!("Waiting for verdict (timeout: {}s)...", timeout));
pb.enable_steady_tick(std::time::Duration::from_millis(SPINNER_TICK_MS));
let result = client.poll_verdict(timeout).await;
pb.finish_and_clear();
result
}
#[allow(dead_code)]
pub fn format_verdict_table(verdict: &Verdict) -> String {
let label_width = 14;
let value_width = 28;
let rows = [
("Run ID", &verdict.run_id),
(
"Problem",
&format!("{} - {}", verdict.problem_id, verdict.problem_name),
),
("Verdict", &verdict.verdict),
("Language", &verdict.language),
("Runtime", &format!("{}s", verdict.runtime)),
("Submitted", &verdict.date),
];
let top = format!(
"┌{}┬{}┐",
"─".repeat(label_width + 2),
"─".repeat(value_width + 2),
);
let bottom = format!(
"└{}┴{}┘",
"─".repeat(label_width + 2),
"─".repeat(value_width + 2),
);
let mut lines = vec![top];
for (label, value) in &rows {
let display_value = if value.len() > value_width {
format!("{}…", &value[..value_width - 1])
} else {
value.to_string()
};
lines.push(format!(
"│ {:<label_width$} │ {:<value_width$} │",
label,
display_value,
label_width = label_width,
value_width = value_width,
));
}
lines.push(bottom);
lines.join("\n")
}
#[allow(dead_code)]
pub fn verdict_color(verdict: &str) -> &'static str {
match verdict {
"Accepted" => "green",
"Wrong Answer" | "Runtime Error" | "Time limit exceeded" | "Memory limit exceeded" => "red",
"Compile Error" | "Presentation Error" => "yellow",
"In judge queue" => "cyan",
_ => "white",
}
}
fn verdict_code_to_string(code: u32) -> String {
match code {
10 => "In judge queue".into(),
15 => "Compile error".into(),
20 => "Restricted function".into(),
30 => "Runtime error".into(),
35 => "Output limit exceeded".into(),
40 | 80 => "Time limit exceeded".into(),
45 => "Nothing to do".into(),
50 | 90 => "Accepted".into(),
60 => "Presentation error".into(),
70 => "Wrong answer".into(),
95 => "Internal error".into(),
100 => "Input format error".into(),
_ => format!("Unknown ({code})"),
}
}
fn colorize_verdict(verdict: &str) -> String {
match verdict {
"Accepted" => verdict.green().bold().to_string(),
"Wrong answer"
| "Compile error"
| "Runtime error"
| "Time limit exceeded"
| "Output limit exceeded" => verdict.red().bold().to_string(),
"In judge queue" => verdict.yellow().to_string(),
"Presentation error" => verdict.yellow().bold().to_string(),
_ => verdict.normal().to_string(),
}
}
pub fn colorize_final_verdict(verdict: &Verdict) -> String {
let colored = colorize_verdict(&verdict.verdict);
format!(
"Run ID: {}\nProblem: {} ({})\nVerdict: {}\nLanguage: {}\nRuntime: {}\nDate: {}",
verdict.run_id,
verdict.problem_id,
verdict.problem_name,
colored,
verdict.language,
verdict.runtime,
verdict.date,
)
}
pub fn format_status_table(submissions: &[UhuntSubmission]) -> String {
if submissions.is_empty() {
return "No submissions found.".to_string();
}
let mut lines: Vec<String> = Vec::new();
lines.push(format!(
"{:<10} {:<8} {:<25} {:<10} {:<8} {}",
"Run ID", "Problem", "Verdict", "Language", "Runtime", "Date"
));
lines.push("-".repeat(80));
for sub in submissions {
let verdict_text = verdict_code_to_string(sub.verdict_code);
let runtime = if sub.runtime == 0 {
"-".to_string()
} else {
format!("{:.3}s", sub.runtime as f64 / 1000.0)
};
let verdict_colored = colorize_verdict(&verdict_text);
lines.push(format!(
"{:<10} {:<8} {:<25} {:<10} {:<8} {}",
sub.run_id, sub.problem_id, verdict_colored, sub.language, runtime, sub.date
));
}
lines.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_accepted_is_green() {
assert_eq!(verdict_color("Accepted"), "green");
}
#[test]
fn color_wrong_answer_is_red() {
assert_eq!(verdict_color("Wrong Answer"), "red");
}
#[test]
fn color_runtime_error_is_red() {
assert_eq!(verdict_color("Runtime Error"), "red");
}
#[test]
fn color_time_limit_exceeded_is_red() {
assert_eq!(verdict_color("Time limit exceeded"), "red");
}
#[test]
fn color_memory_limit_exceeded_is_red() {
assert_eq!(verdict_color("Memory limit exceeded"), "red");
}
#[test]
fn color_compile_error_is_yellow() {
assert_eq!(verdict_color("Compile Error"), "yellow");
}
#[test]
fn color_presentation_error_is_yellow() {
assert_eq!(verdict_color("Presentation Error"), "yellow");
}
#[test]
fn color_in_judge_queue_is_cyan() {
assert_eq!(verdict_color("In judge queue"), "cyan");
}
#[test]
fn color_unknown_verdict_is_white() {
assert_eq!(verdict_color("Something Unknown"), "white");
}
#[test]
fn color_empty_string_is_white() {
assert_eq!(verdict_color(""), "white");
}
fn sample_verdict() -> Verdict {
Verdict {
run_id: "29964933".into(),
problem_id: "100".into(),
problem_name: "The 3n + 1 Problem".into(),
verdict: "Accepted".into(),
language: "C++11".into(),
runtime: "0.140".into(),
date: "2024-11-15 07:56:25".into(),
}
}
#[test]
fn table_contains_run_id() {
let table = format_verdict_table(&sample_verdict());
assert!(table.contains("29964933"));
}
#[test]
fn table_contains_problem_id_and_name() {
let table = format_verdict_table(&sample_verdict());
assert!(table.contains("100 - The 3n + 1 Problem"));
}
#[test]
fn table_contains_verdict() {
let table = format_verdict_table(&sample_verdict());
assert!(table.contains("Accepted"));
}
#[test]
fn table_contains_language() {
let table = format_verdict_table(&sample_verdict());
assert!(table.contains("C++11"));
}
#[test]
fn table_contains_runtime_with_suffix() {
let table = format_verdict_table(&sample_verdict());
assert!(table.contains("0.140s"));
}
#[test]
fn table_contains_date() {
let table = format_verdict_table(&sample_verdict());
assert!(table.contains("2024-11-15 07:56:25"));
}
#[test]
fn table_has_border_characters() {
let table = format_verdict_table(&sample_verdict());
assert!(table.starts_with('┌'));
assert!(table.ends_with('┘'));
}
#[test]
fn table_has_six_data_rows() {
let table = format_verdict_table(&sample_verdict());
let line_count = table.lines().count();
assert_eq!(line_count, 8);
}
#[test]
fn table_truncates_long_problem_name() {
let verdict = Verdict {
run_id: "12345".into(),
problem_id: "9999".into(),
problem_name: "A Very Long Problem Name That Should Be Truncated".into(),
verdict: "Accepted".into(),
language: "Python 3".into(),
runtime: "0.001".into(),
date: "2024-01-01 00:00:00".into(),
};
let table = format_verdict_table(&verdict);
assert!(table.starts_with('┌'));
assert!(table.ends_with('┘'));
}
#[test]
fn table_with_wrong_answer_verdict() {
let verdict = Verdict {
run_id: "99999".into(),
problem_id: "500".into(),
problem_name: "World Cup Fever".into(),
verdict: "Wrong Answer".into(),
language: "Java".into(),
runtime: "0.000".into(),
date: "2025-06-01 12:00:00".into(),
};
let table = format_verdict_table(&verdict);
assert!(table.contains("Wrong Answer"));
assert!(table.contains("500 - World Cup Fever"));
assert!(table.contains("Java"));
}
#[test]
fn table_with_queue_verdict() {
let verdict = Verdict {
run_id: "11111".into(),
problem_id: "100".into(),
problem_name: "The 3n + 1 Problem".into(),
verdict: "In judge queue".into(),
language: "C++11".into(),
runtime: "-".into(),
date: "2024-11-15 08:00:00".into(),
};
let table = format_verdict_table(&verdict);
assert!(table.contains("In judge queue"));
assert!(table.contains("-s"));
}
}