use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::client::{ClientError, UvaClient};
use crate::language::LanguageInfo;
const MIN_SUBMISSION_INTERVAL: Duration = Duration::from_secs(5);
static LAST_SUBMISSION: Mutex<Option<Instant>> = Mutex::new(None);
async fn rate_limit_wait() {
let wait_duration = {
let lock = LAST_SUBMISSION.lock().expect("rate limiter poisoned");
match *lock {
Some(last) => {
let elapsed = last.elapsed();
if elapsed < MIN_SUBMISSION_INTERVAL {
MIN_SUBMISSION_INTERVAL - elapsed
} else {
Duration::ZERO
}
}
None => Duration::ZERO,
}
};
if !wait_duration.is_zero() {
tokio::time::sleep(wait_duration).await;
}
let mut lock = LAST_SUBMISSION.lock().expect("rate limiter poisoned");
*lock = Some(Instant::now());
}
#[cfg(test)]
pub(crate) fn reset_rate_limiter() {
let mut lock = LAST_SUBMISSION.lock().expect("rate limiter poisoned");
*lock = None;
}
#[derive(Debug, Clone, PartialEq)]
pub struct SubmissionRequest {
pub problem_id: String,
pub language: LanguageInfo,
pub source_code: String,
pub file_path: String,
}
#[derive(Debug, Clone)]
pub struct SubmissionOutcome {
pub run_id: String,
pub initial_verdict: String,
}
pub async fn submit_solution(
client: &mut UvaClient,
request: &SubmissionRequest,
) -> Result<SubmissionOutcome, ClientError> {
rate_limit_wait().await;
let result = client
.submit(
&request.problem_id,
request.language.code,
&request.source_code,
)
.await?;
Ok(SubmissionOutcome {
run_id: result.run_id,
initial_verdict: result.initial_verdict,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;
static TEST_MUTEX: StdMutex<()> = StdMutex::new(());
#[test]
fn submission_request_fields() {
let req = SubmissionRequest {
problem_id: "100".into(),
language: LanguageInfo {
code: 5,
name: "C++11",
},
source_code: "#include <iostream>".into(),
file_path: "100.cpp".into(),
};
assert_eq!(req.problem_id, "100");
assert_eq!(req.language.code, 5);
assert_eq!(req.source_code, "#include <iostream>");
assert_eq!(req.file_path, "100.cpp");
}
#[test]
fn submission_outcome_fields() {
let outcome = SubmissionOutcome {
run_id: "29964933".into(),
initial_verdict: "In judge queue".into(),
};
assert_eq!(outcome.run_id, "29964933");
assert_eq!(outcome.initial_verdict, "In judge queue");
}
#[test]
fn rate_limit_reset_clears_timer() {
let _guard = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
{
let mut lock = LAST_SUBMISSION.lock().unwrap();
*lock = Some(Instant::now());
}
reset_rate_limiter();
let lock = LAST_SUBMISSION.lock().unwrap();
assert!(lock.is_none(), "rate limiter should be cleared after reset");
}
#[test]
fn rate_limit_records_submission_time() {
let _guard = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
reset_rate_limiter();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(rate_limit_wait());
let lock = LAST_SUBMISSION.lock().unwrap();
let last = lock.expect("timer should be set after rate_limit_wait");
assert!(last.elapsed() < Duration::from_secs(1));
}
#[test]
fn rate_limit_second_call_waits() {
let _guard = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
reset_rate_limiter();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async { rate_limit_wait().await });
let elapsed = rt.block_on(async {
let start = Instant::now();
rate_limit_wait().await;
start.elapsed()
});
assert!(elapsed >= Duration::from_secs(4));
}
#[test]
fn rate_limit_after_interval_allows_immediate() {
let _guard = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
reset_rate_limiter();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
{
let mut lock = LAST_SUBMISSION.lock().unwrap();
*lock = Some(Instant::now() - Duration::from_secs(10));
}
let elapsed = rt.block_on(async {
let start = Instant::now();
rate_limit_wait().await;
start.elapsed()
});
assert!(
elapsed < Duration::from_secs(2),
"expected immediate return but waited {elapsed:?}"
);
}
#[test]
fn submission_request_clone() {
let req = SubmissionRequest {
problem_id: "500".into(),
language: LanguageInfo {
code: 6,
name: "Python 3",
},
source_code: "print('hello')".into(),
file_path: "problems/500.py".into(),
};
let cloned = req.clone();
assert_eq!(req, cloned);
}
#[test]
fn submission_outcome_clone() {
let outcome = SubmissionOutcome {
run_id: "12345".into(),
initial_verdict: "Accepted".into(),
};
let cloned = outcome.clone();
assert_eq!(outcome.run_id, cloned.run_id);
assert_eq!(outcome.initial_verdict, cloned.initial_verdict);
}
}