1#[cfg(any(target_arch = "wasm32", test))]
4use crate::api::code_runner::{ExecResult, ExecStatus};
5#[cfg(any(target_arch = "wasm32", test))]
6use crate::api::error::ApiError;
7
8#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub(super) enum Phase {
12 Queued,
14 Preparing,
16 Running,
18}
19
20#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
22#[derive(Clone, Debug, PartialEq)]
23pub(super) enum Outcome {
24 Success {
25 duration_ms: u64,
26 },
27 RunError {
28 exit_code: Option<i64>,
29 duration_ms: u64,
30 },
31 Timeout {
32 duration_ms: u64,
33 },
34 OutOfMemory {
35 duration_ms: u64,
36 },
37 RateLimited(String),
39 SystemFailed(String),
41 Disconnected,
43 Example {
45 meta: String,
46 error: String,
47 },
48}
49
50#[derive(Clone, Debug, PartialEq)]
51pub(super) enum RunState {
52 Idle,
53 Busy(Phase),
54 Finished(Outcome),
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
59pub(super) enum Tone {
60 Neutral,
61 Warn,
62 Danger,
63}
64
65impl Tone {
66 pub fn as_str(self) -> &'static str {
67 match self {
68 Tone::Neutral => "neutral",
69 Tone::Warn => "warn",
70 Tone::Danger => "danger",
71 }
72 }
73}
74
75impl RunState {
76 pub fn is_running(&self) -> bool {
77 matches!(self, Self::Busy(_))
78 }
79
80 pub fn is_finished(&self) -> bool {
81 matches!(self, Self::Finished(_))
82 }
83
84 pub fn label(&self) -> &'static str {
85 match self {
86 Self::Idle => "未运行",
87 Self::Busy(Phase::Queued) => "排队中",
88 Self::Busy(Phase::Preparing) => "准备中",
89 Self::Busy(Phase::Running) => "运行中",
90 Self::Finished(outcome) => match outcome {
91 Outcome::Success { .. } => "成功",
92 Outcome::RunError { .. } => "运行错误",
93 Outcome::Timeout { .. } => "超时",
94 Outcome::OutOfMemory { .. } => "内存超限",
95 Outcome::RateLimited(_) => "请求过频",
96 Outcome::SystemFailed(_) => "系统失败",
97 Outcome::Disconnected => "连接中断",
98 Outcome::Example { .. } => "示例",
99 },
100 }
101 }
102
103 pub fn tone(&self) -> Tone {
104 match self {
105 Self::Finished(Outcome::Timeout { .. } | Outcome::OutOfMemory { .. })
106 | Self::Finished(Outcome::RateLimited(_)) => Tone::Warn,
107 Self::Finished(
108 Outcome::RunError { .. } | Outcome::SystemFailed(_) | Outcome::Disconnected,
109 ) => Tone::Danger,
110 Self::Finished(Outcome::Example { error, .. }) if !error.is_empty() => Tone::Danger,
111 _ => Tone::Neutral,
112 }
113 }
114
115 pub fn meta(&self) -> String {
117 match self {
118 Self::Finished(Outcome::Success { duration_ms }) => {
119 format!("退出码 0 · {}", seconds(*duration_ms))
120 }
121 Self::Finished(Outcome::RunError {
122 exit_code,
123 duration_ms,
124 }) => match exit_code {
125 Some(code) => format!("退出码 {code} · {}", seconds(*duration_ms)),
126 None => seconds(*duration_ms),
127 },
128 Self::Finished(
129 Outcome::Timeout { duration_ms } | Outcome::OutOfMemory { duration_ms },
130 ) => seconds(*duration_ms),
131 Self::Finished(Outcome::Example { meta, .. }) => meta.clone(),
132 _ => String::new(),
133 }
134 }
135
136 pub fn busy_hint(&self) -> Option<&'static str> {
138 match self {
139 Self::Busy(Phase::Queued) => Some("并发已满,空出槽位后自动开始"),
140 Self::Busy(Phase::Preparing) => Some("正在启动容器…"),
141 _ => None,
142 }
143 }
144
145 pub fn note(&self) -> Option<String> {
147 let text = match self {
148 Self::Finished(Outcome::Timeout { .. }) => "超过时间上限被终止;输出到终止前为止",
149 Self::Finished(Outcome::OutOfMemory { .. }) => "内存超过上限,进程被系统终止",
150 Self::Finished(Outcome::RateLimited(message)) => return Some(message.clone()),
151 Self::Finished(Outcome::SystemFailed(message)) if !message.is_empty() => {
152 return Some(message.clone())
153 }
154 Self::Finished(Outcome::SystemFailed(_)) => "运行环境出了问题,不是代码的错",
155 Self::Finished(Outcome::Disconnected) => "输出流断开,结果可能不完整",
156 Self::Finished(Outcome::Example { error, .. }) if !error.is_empty() => {
157 return Some(error.clone())
158 }
159 _ => return None,
160 };
161 Some(text.to_string())
162 }
163
164 pub fn retryable(&self) -> bool {
166 matches!(
167 self,
168 Self::Finished(
169 Outcome::RateLimited(_) | Outcome::SystemFailed(_) | Outcome::Disconnected
170 )
171 )
172 }
173
174 #[cfg(any(target_arch = "wasm32", test))]
176 pub fn rejected(error: &ApiError) -> Self {
177 let message = crate::utils::error::error_reason(error)
178 .unwrap_or_else(|| "运行服务暂时不可用,请稍后再试".to_string());
179 if message.contains("频繁") || message.contains("次数已达上限") {
180 Self::Finished(Outcome::RateLimited(message))
181 } else {
182 Self::Finished(Outcome::SystemFailed(message))
183 }
184 }
185
186 #[cfg(any(target_arch = "wasm32", test))]
188 pub fn result(result: &ExecResult) -> Self {
189 let duration_ms = result.duration_ms;
190 Self::Finished(match result.status {
191 ExecStatus::Success => Outcome::Success { duration_ms },
192 ExecStatus::Timeout => Outcome::Timeout { duration_ms },
193 ExecStatus::OomKilled => Outcome::OutOfMemory { duration_ms },
194 ExecStatus::Error => Outcome::RunError {
195 exit_code: result.exit_code,
196 duration_ms,
197 },
198 ExecStatus::RateLimited => Outcome::RateLimited("运行太频繁,稍后再试".into()),
199 ExecStatus::Failed | ExecStatus::Queued | ExecStatus::Running => {
200 Outcome::SystemFailed(result.stderr.trim().to_string())
201 }
202 })
203 }
204}
205
206#[cfg(any(target_arch = "wasm32", test))]
208#[derive(serde::Deserialize)]
209pub(super) struct DonePayload {
210 pub exit_code: Option<i64>,
211 pub oom_killed: bool,
212 pub timed_out: bool,
213 pub duration_ms: u64,
214 #[serde(default)]
215 pub error: Option<String>,
216}
217
218#[cfg(any(target_arch = "wasm32", test))]
219impl DonePayload {
220 pub fn outcome(self) -> RunState {
222 let duration_ms = self.duration_ms;
223 RunState::Finished(if self.timed_out {
224 Outcome::Timeout { duration_ms }
225 } else if self.oom_killed {
226 Outcome::OutOfMemory { duration_ms }
227 } else if let Some(error) = self.error {
228 Outcome::SystemFailed(error)
229 } else if self.exit_code == Some(0) {
230 Outcome::Success { duration_ms }
231 } else {
232 Outcome::RunError {
233 exit_code: self.exit_code,
234 duration_ms,
235 }
236 })
237 }
238}
239
240fn seconds(ms: u64) -> String {
242 let s = ms as f64 / 1000.0;
243 if s < 1.0 {
244 format!("{s:.2} s")
245 } else if s < 10.0 {
246 format!("{s:.1} s")
247 } else {
248 format!("{s:.0} s")
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 fn done(exit_code: Option<i64>, oom: bool, timed_out: bool, error: Option<&str>) -> RunState {
257 DonePayload {
258 exit_code,
259 oom_killed: oom,
260 timed_out,
261 duration_ms: 40,
262 error: error.map(str::to_string),
263 }
264 .outcome()
265 }
266
267 #[test]
268 fn done_payload_maps_to_each_outcome() {
269 assert_eq!(done(Some(0), false, false, None).label(), "成功");
270 assert_eq!(done(Some(1), false, false, None).label(), "运行错误");
271 assert_eq!(done(None, false, true, Some("执行超时")).label(), "超时");
272 assert_eq!(done(Some(137), true, false, None).label(), "内存超限");
273 assert_eq!(
274 done(None, false, false, Some("镜像未构建")).label(),
275 "系统失败"
276 );
277 }
278
279 #[test]
280 fn tones_follow_severity() {
281 assert_eq!(done(Some(0), false, false, None).tone(), Tone::Neutral);
282 assert_eq!(done(None, false, true, None).tone(), Tone::Warn);
283 assert_eq!(done(Some(1), false, false, None).tone(), Tone::Danger);
284 assert_eq!(RunState::Busy(Phase::Running).tone(), Tone::Neutral);
285 }
286
287 #[test]
288 fn meta_shows_exit_code_and_duration() {
289 assert_eq!(
290 done(Some(0), false, false, None).meta(),
291 "退出码 0 · 0.04 s"
292 );
293 assert_eq!(
294 done(Some(1), false, false, None).meta(),
295 "退出码 1 · 0.04 s"
296 );
297 assert_eq!(seconds(800), "0.80 s");
298 assert_eq!(seconds(2_340), "2.3 s");
299 assert_eq!(seconds(15_000), "15 s");
300 }
301
302 #[test]
303 fn rejected_submission_separates_rate_limits() {
304 let limited = RunState::rejected(&ApiError::BadRequest("请求过于频繁,请稍后再试".into()));
305 assert_eq!(limited.label(), "请求过频");
306 assert_eq!(limited.note().as_deref(), Some("请求过于频繁,请稍后再试"));
307 assert!(limited.retryable());
308 let oversized = RunState::rejected(&ApiError::BadRequest("源代码过大".into()));
309 assert_eq!(oversized.label(), "系统失败");
310 assert_eq!(oversized.note().as_deref(), Some("源代码过大"));
311 }
312
313 #[test]
314 fn busy_phases_never_add_a_note_row() {
315 for phase in [Phase::Queued, Phase::Preparing, Phase::Running] {
316 assert_eq!(RunState::Busy(phase).note(), None, "{phase:?} 不应占输出区");
317 }
318 assert!(RunState::Busy(Phase::Queued).busy_hint().is_some());
319 assert!(RunState::Busy(Phase::Preparing).busy_hint().is_some());
320 assert!(RunState::Busy(Phase::Running).busy_hint().is_none());
321 assert!(RunState::Finished(Outcome::Disconnected)
322 .busy_hint()
323 .is_none());
324 }
325
326 #[test]
327 fn only_environment_failures_offer_retry() {
328 assert!(RunState::Finished(Outcome::Disconnected).retryable());
329 assert!(!done(Some(1), false, false, None).retryable());
330 assert!(!done(None, false, true, None).retryable());
331 }
332
333 #[test]
334 fn poll_results_never_stay_running() {
335 for status in [
336 ExecStatus::Success,
337 ExecStatus::Error,
338 ExecStatus::Timeout,
339 ExecStatus::OomKilled,
340 ExecStatus::Failed,
341 ] {
342 let result = ExecResult {
343 status,
344 stdout: String::new(),
345 stderr: String::new(),
346 exit_code: None,
347 duration_ms: 12,
348 language: "python".into(),
349 };
350 let state = RunState::result(&result);
351 assert!(state.is_finished());
352 }
353 }
354}