Skip to content

Commit a95e269

Browse files
committed
timeout for python3 tests
1 parent 67daf65 commit a95e269

File tree

11 files changed

+174
-30
lines changed

11 files changed

+174
-30
lines changed

tmc-langs-framework/src/domain.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,21 @@ impl ExercisePackagingConfiguration {
128128
pub struct TmcProjectYml {
129129
#[serde(default)]
130130
pub extra_student_files: Vec<PathBuf>,
131+
131132
#[serde(default)]
132133
pub extra_exercise_files: Vec<PathBuf>,
134+
133135
#[serde(default)]
134136
pub force_update: Vec<PathBuf>,
137+
138+
#[serde(default)]
139+
pub tests_timeout_ms: Option<u64>,
140+
135141
#[serde(default)]
136142
#[serde(rename = "no-tests")]
137143
pub no_tests: Option<NoTests>,
138144

145+
#[serde(default)]
139146
pub fail_on_valgrind_error: Option<bool>,
140147
}
141148

tmc-langs-framework/src/lib.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ pub use policy::StudentFilePolicy;
1111
use domain::TmcProjectYml;
1212
use io::zip;
1313
use std::path::PathBuf;
14+
use std::process::{Command, Output};
15+
use std::thread;
16+
use std::time::{Duration, Instant};
1417
use thiserror::Error;
1518

1619
#[derive(Error, Debug)]
@@ -37,6 +40,11 @@ pub enum Error {
3740
#[error("Running command '{0}' failed")]
3841
CommandFailed(&'static str),
3942

43+
#[error("Failed to spawn command: {0}")]
44+
CommandSpawn(std::io::Error),
45+
#[error("Test timed out")]
46+
TestTimeout,
47+
4048
#[error("Error in plugin: {0}")]
4149
Plugin(Box<dyn std::error::Error + 'static + Send + Sync>),
4250

@@ -49,3 +57,60 @@ pub enum Error {
4957
}
5058

5159
pub type Result<T> = std::result::Result<T, Error>;
60+
61+
pub struct CommandWithTimeout<'a>(pub &'a mut Command);
62+
63+
impl CommandWithTimeout<'_> {
64+
pub fn wait_with_timeout(
65+
&mut self,
66+
name: &'static str,
67+
timeout: Option<Duration>,
68+
) -> Result<Output> {
69+
match timeout {
70+
Some(timeout) => {
71+
// spawn process and init timer
72+
let mut child = self.0.spawn().map_err(|e| Error::CommandSpawn(e))?;
73+
let timer = Instant::now();
74+
loop {
75+
match child.try_wait()? {
76+
Some(_exit_status) => {
77+
// done, get output
78+
return child
79+
.wait_with_output()
80+
.map_err(|_| Error::CommandFailed(name));
81+
}
82+
None => {
83+
// still running, check timeout
84+
if timer.elapsed() > timeout {
85+
child.kill()?;
86+
return Err(Error::TestTimeout);
87+
}
88+
89+
// TODO: gradually increase sleep duration?
90+
thread::sleep(Duration::from_millis(100));
91+
}
92+
}
93+
}
94+
}
95+
// no timeout, block forever
96+
None => self.0.output().map_err(|_| Error::CommandFailed(name)),
97+
}
98+
}
99+
}
100+
101+
#[cfg(test)]
102+
mod test {
103+
use super::*;
104+
105+
#[test]
106+
fn timeout() {
107+
let mut command = Command::new("sleep");
108+
let mut command = command.arg("1");
109+
let mut out = CommandWithTimeout(&mut command);
110+
let res = out.wait_with_timeout("sleep", Some(Duration::from_millis(100)));
111+
if let Err(Error::TestTimeout) = res {
112+
} else {
113+
panic!("unexpected result: {:?}", res);
114+
}
115+
}
116+
}

tmc-langs-framework/src/plugin.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use super::Result;
1010
use log::debug;
1111
use std::collections::HashSet;
1212
use std::path::{Path, PathBuf};
13+
use std::time::Duration;
1314
use walkdir::WalkDir;
1415

1516
/// The trait that each language plug-in must implement.
@@ -61,7 +62,17 @@ pub trait LanguagePlugin {
6162
fn scan_exercise(&self, path: &Path, exercise_name: String) -> Result<ExerciseDesc>;
6263

6364
/// Runs the tests for the exercise.
64-
fn run_tests(&self, path: &Path) -> Result<RunResult>;
65+
fn run_tests(&self, path: &Path) -> Result<RunResult> {
66+
let timeout = self
67+
.get_student_file_policy(path)
68+
.get_tmc_project_yml()
69+
.ok()
70+
.and_then(|t| t.tests_timeout_ms.map(Duration::from_millis));
71+
self.run_tests_with_timeout(path, timeout)
72+
}
73+
74+
/// Runs the tests for the exercise with the given timeout.
75+
fn run_tests_with_timeout(&self, path: &Path, timeout: Option<Duration>) -> Result<RunResult>;
6576

6677
/// Prepares a submission for processing in the sandbox.
6778
///
@@ -207,7 +218,11 @@ mod test {
207218
unimplemented!()
208219
}
209220

210-
fn run_tests(&self, _path: &Path) -> Result<RunResult> {
221+
fn run_tests_with_timeout(
222+
&self,
223+
_path: &Path,
224+
_timeout: Option<Duration>,
225+
) -> Result<RunResult> {
211226
unimplemented!()
212227
}
213228

tmc-langs-java/src/ant.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pub mod policy;
22

33
use super::{error::JavaError, plugin::JavaPlugin, CompileResult, TestRun, SEPARATOR};
4+
45
use isolang::Language;
56
use j4rs::Jvm;
67
use policy::AntStudentFilePolicy;
@@ -9,6 +10,7 @@ use std::fs::{self, File};
910
use std::io::Write;
1011
use std::path::Path;
1112
use std::process::Command;
13+
use std::time::Duration;
1214
use tmc_langs_abstraction::ValidationResult;
1315
use tmc_langs_framework::{
1416
domain::{ExerciseDesc, RunResult},
@@ -73,7 +75,11 @@ impl LanguagePlugin for AntPlugin {
7375
Ok(self.scan_exercise_with_compile_result(path, exercise_name, compile_result)?)
7476
}
7577

76-
fn run_tests(&self, project_root_path: &Path) -> Result<RunResult, Error> {
78+
fn run_tests_with_timeout(
79+
&self,
80+
project_root_path: &Path,
81+
_timeout: Option<Duration>,
82+
) -> Result<RunResult, Error> {
7783
Ok(self.run_java_tests(project_root_path)?)
7884
}
7985

tmc-langs-java/src/maven.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
pub mod policy;
22

33
use super::{error::JavaError, plugin::JavaPlugin, CompileResult, TestRun, SEPARATOR};
4+
45
use isolang::Language;
56
use j4rs::Jvm;
67
use policy::MavenStudentFilePolicy;
78
use std::fs;
89
use std::path::Path;
910
use std::process::Command;
11+
use std::time::Duration;
1012
use tmc_langs_abstraction::ValidationResult;
1113
use tmc_langs_framework::{
1214
domain::{ExerciseDesc, RunResult},
@@ -44,7 +46,11 @@ impl LanguagePlugin for MavenPlugin {
4446
Ok(self.scan_exercise_with_compile_result(path, exercise_name, compile_result)?)
4547
}
4648

47-
fn run_tests(&self, project_root_path: &Path) -> Result<RunResult, Error> {
49+
fn run_tests_with_timeout(
50+
&self,
51+
project_root_path: &Path,
52+
_timeout: Option<Duration>,
53+
) -> Result<RunResult, Error> {
4854
Ok(self.run_java_tests(project_root_path)?)
4955
}
5056

tmc-langs-make/src/plugin.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::check_log::CheckLog;
44
use crate::error::MakeError;
55
use crate::policy::MakeStudentFilePolicy;
66
use crate::valgrind_log::ValgrindLog;
7+
78
use lazy_static::lazy_static;
89
use regex::Regex;
910
use std::collections::HashMap;
@@ -12,6 +13,7 @@ use std::io;
1213
use std::io::{BufRead, BufReader};
1314
use std::path::Path;
1415
use std::process::Command;
16+
use std::time::Duration;
1517
use tmc_langs_framework::{
1618
domain::{ExerciseDesc, RunResult, RunStatus, TestDesc, TmcProjectYml},
1719
plugin::LanguagePlugin,
@@ -135,7 +137,11 @@ impl LanguagePlugin for MakePlugin {
135137
Ok(self.parse_exercise_desc(&available_points_path, exercise_name)?)
136138
}
137139

138-
fn run_tests(&self, path: &Path) -> Result<RunResult, Error> {
140+
fn run_tests_with_timeout(
141+
&self,
142+
path: &Path,
143+
_timeout: Option<Duration>,
144+
) -> Result<RunResult, Error> {
139145
if !self.builds(path)? {
140146
return Ok(RunResult {
141147
status: RunStatus::CompileFailed,

tmc-langs-notests/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@ use tmc_langs_framework::{
66

77
use std::collections::HashMap;
88
use std::path::Path;
9+
use std::time::Duration;
910

1011
pub struct NoTestsPlugin {}
1112

1213
impl NoTestsPlugin {
14+
pub fn new() -> Self {
15+
Self {}
16+
}
17+
1318
fn get_points(&self, path: &Path) -> Vec<String> {
1419
self.get_student_file_policy(path)
1520
.get_tmc_project_yml()
@@ -35,7 +40,11 @@ impl LanguagePlugin for NoTestsPlugin {
3540
})
3641
}
3742

38-
fn run_tests(&self, path: &Path) -> Result<RunResult, Error> {
43+
fn run_tests_with_timeout(
44+
&self,
45+
path: &Path,
46+
_timeout: Option<Duration>,
47+
) -> Result<RunResult, Error> {
3948
Ok(RunResult {
4049
status: RunStatus::Passed,
4150
test_results: vec![TestResult {

tmc-langs-python3/src/plugin.rs

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ use std::fs::{self, File};
77
use std::io::BufReader;
88
use std::path::Path;
99
use std::process::Command;
10+
use std::time::Duration;
1011
use tmc_langs_framework::{
1112
domain::{ExerciseDesc, RunResult, RunStatus, TestDesc, TestResult},
1213
plugin::LanguagePlugin,
1314
policy::StudentFilePolicy,
14-
Error,
15+
CommandWithTimeout, Error,
1516
};
1617
use walkdir::WalkDir;
1718

@@ -33,7 +34,7 @@ impl LanguagePlugin for Python3Plugin {
3334
}
3435

3536
fn scan_exercise(&self, path: &Path, exercise_name: String) -> Result<ExerciseDesc, Error> {
36-
let run_result = run_tmc_command(path, &["available_points"]);
37+
let run_result = run_tmc_command(path, &["available_points"], None);
3738

3839
if let Err(error) = run_result {
3940
log::error!("Failed to scan exercise. {}", error);
@@ -43,8 +44,12 @@ impl LanguagePlugin for Python3Plugin {
4344
Ok(ExerciseDesc::new(exercise_name, test_descs))
4445
}
4546

46-
fn run_tests(&self, path: &Path) -> Result<RunResult, Error> {
47-
let run_result = run_tmc_command(path, &[]);
47+
fn run_tests_with_timeout(
48+
&self,
49+
path: &Path,
50+
timeout: Option<Duration>,
51+
) -> Result<RunResult, Error> {
52+
let run_result = run_tmc_command(path, &[], timeout);
4853

4954
if let Err(error) = run_result {
5055
log::error!("Failed to parse exercise description. {}", error);
@@ -87,37 +92,45 @@ impl LanguagePlugin for Python3Plugin {
8792
}
8893
}
8994

90-
fn run_tmc_command(path: &Path, extra_args: &[&str]) -> Result<std::process::Output, PythonError> {
95+
fn run_tmc_command(
96+
path: &Path,
97+
extra_args: &[&str],
98+
timeout: Option<Duration>,
99+
) -> Result<std::process::Output, PythonError> {
91100
let path = path
92101
.canonicalize()
93102
.map_err(|e| PythonError::Path(path.to_path_buf(), e))?;
94103
log::debug!("running tmc command at {}", path.display());
95104
let common_args = ["-m", "tmc"];
96-
let result = match &*LOCAL_PY {
97-
LocalPy::Unix => Command::new("python3")
105+
106+
let (name, mut command) = match &*LOCAL_PY {
107+
LocalPy::Unix => ("python3", Command::new("python3")),
108+
LocalPy::Windows => ("py", Command::new("py")),
109+
//.map_err(|e| PythonError::Command("py", e))?,
110+
LocalPy::WindowsConda { conda_path } => ("conda", Command::new(conda_path)),
111+
};
112+
let command = match &*LOCAL_PY {
113+
LocalPy::Unix => command
98114
.args(&common_args)
99115
.args(extra_args)
100-
.current_dir(path)
101-
.output()
102-
.map_err(|e| PythonError::Command("python3", e))?,
103-
LocalPy::Windows => Command::new("py")
116+
.current_dir(path),
117+
LocalPy::Windows => command
104118
.args(&["-3"])
105119
.args(&common_args)
106120
.args(extra_args)
107-
.current_dir(path)
108-
.output()
109-
.map_err(|e| PythonError::Command("py", e))?,
110-
LocalPy::WindowsConda { conda_path } => Command::new(conda_path)
121+
.current_dir(path),
122+
LocalPy::WindowsConda { .. } => command
111123
.args(&common_args)
112124
.args(extra_args)
113-
.current_dir(path)
114-
.output()
115-
.map_err(|e| PythonError::Command(conda_path, e))?,
125+
.current_dir(path),
116126
};
127+
let output = CommandWithTimeout(command)
128+
.wait_with_timeout(name, timeout)
129+
.unwrap();
117130

118-
log::debug!("stdout: {}", String::from_utf8_lossy(&result.stdout));
119-
log::debug!("stderr: {}", String::from_utf8_lossy(&result.stderr));
120-
Ok(result)
131+
log::debug!("stdout: {}", String::from_utf8_lossy(&output.stdout));
132+
log::debug!("stderr: {}", String::from_utf8_lossy(&output.stderr));
133+
Ok(output)
121134
}
122135

123136
fn parse_exercise_description(path: &Path) -> Result<Vec<TestDesc>, PythonError> {

tmc-langs-r/src/plugin.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,16 @@ use std::collections::HashMap;
1212
use std::fs::{self, File};
1313
use std::path::Path;
1414
use std::process::Command;
15+
use std::time::Duration;
1516

1617
pub struct RPlugin {}
1718

19+
impl RPlugin {
20+
pub fn new() -> Self {
21+
Self {}
22+
}
23+
}
24+
1825
impl LanguagePlugin for RPlugin {
1926
fn get_plugin_name(&self) -> &str {
2027
"r"
@@ -54,7 +61,11 @@ impl LanguagePlugin for RPlugin {
5461
})
5562
}
5663

57-
fn run_tests(&self, path: &Path) -> Result<RunResult, Error> {
64+
fn run_tests_with_timeout(
65+
&self,
66+
path: &Path,
67+
_timeout: Option<Duration>,
68+
) -> Result<RunResult, Error> {
5869
// delete results json
5970
let results_path = path.join(".results.json");
6071
if results_path.exists() {

0 commit comments

Comments
 (0)