tmc_langs_notests/
plugin.rs1use crate::NoTestsStudentFilePolicy;
4use std::{
5 collections::HashMap,
6 io::{Read, Seek},
7 ops::ControlFlow::{Break, Continue},
8 path::{Path, PathBuf},
9 time::Duration,
10};
11use tmc_langs_framework::{
12 Archive, ExerciseDesc, LanguagePlugin, RunResult, RunStatus, StudentFilePolicy, TestDesc,
13 TestResult, TmcError, TmcProjectYml,
14 nom::{self, IResult},
15 nom_language::error::VerboseError,
16};
17use tmc_langs_util::{deserialize, path_util};
18
19#[derive(Default)]
20pub struct NoTestsPlugin {}
21
22impl NoTestsPlugin {
23 pub fn new() -> Self {
24 Self {}
25 }
26
27 fn get_points(path: &Path) -> Vec<String> {
29 <Self as LanguagePlugin>::StudentFilePolicy::new(path)
30 .ok()
31 .as_ref()
32 .map(|p| p.get_project_config())
33 .and_then(|c| c.no_tests.as_ref().map(|n| n.points.clone()))
34 .unwrap_or_default()
35 }
36}
37
38impl LanguagePlugin for NoTestsPlugin {
41 const PLUGIN_NAME: &'static str = "No-Tests";
42 const DEFAULT_SANDBOX_IMAGE: &'static str = "eu.gcr.io/moocfi-public/tmc-sandbox-python:latest"; const LINE_COMMENT: &'static str = "//";
44 const BLOCK_COMMENT: Option<(&'static str, &'static str)> = None;
45 type StudentFilePolicy = NoTestsStudentFilePolicy;
46
47 fn scan_exercise(&self, path: &Path, exercise_name: String) -> Result<ExerciseDesc, TmcError> {
48 let test_name = format!("{exercise_name}Test");
49 Ok(ExerciseDesc {
50 name: exercise_name,
51 tests: vec![TestDesc {
52 name: test_name,
53 points: Self::get_points(path),
54 }],
55 })
56 }
57
58 fn run_tests_with_timeout(
59 &self,
60 path: &Path,
61 _timeout: Option<Duration>,
62 ) -> Result<RunResult, TmcError> {
63 Ok(RunResult {
64 status: RunStatus::Passed,
65 test_results: vec![TestResult {
66 name: "Default test".to_string(),
67 successful: true,
68 points: Self::get_points(path),
69 message: "".to_string(),
70 exception: vec![],
71 }],
72 logs: HashMap::new(),
73 })
74 }
75
76 fn is_exercise_type_correct(path: &Path) -> bool {
77 Self::StudentFilePolicy::new(path)
78 .ok()
79 .as_ref()
80 .map(|p| p.get_project_config())
81 .and_then(|c| c.no_tests.as_ref())
82 .map(|nt| nt.flag)
83 .unwrap_or(false)
84 }
85
86 fn find_project_dir_in_archive<R: Read + Seek>(
87 archive: &mut Archive<R>,
88 ) -> Result<PathBuf, TmcError> {
89 let mut iter = archive.iter()?;
90
91 let project_dir = loop {
92 let next = iter.with_next(|file| {
93 let file_path = file.path()?;
94
95 if file.is_file() {
96 if let Some(parent) =
98 path_util::get_parent_of_named(&file_path, ".tmcproject.yml")
99 {
100 let tmc_project_yml: TmcProjectYml = deserialize::yaml_from_reader(file)
101 .map_err(|e| TmcError::YamlDeserialize(file_path, e))?;
102 if tmc_project_yml
104 .no_tests
105 .map(|nt| nt.flag)
106 .unwrap_or_default()
107 {
108 return Ok(Break(Some(parent)));
109 }
110 }
111 }
112 Ok(Continue(()))
113 });
114 match next? {
115 Continue(_) => continue,
116 Break(project_dir) => break project_dir,
117 }
118 };
119 if let Some(project_dir) = project_dir {
120 Ok(project_dir)
121 } else {
122 Err(TmcError::NoProjectDirInArchive)
123 }
124 }
125
126 fn clean(&self, _path: &Path) -> Result<(), TmcError> {
127 Ok(())
128 }
129
130 fn get_default_student_file_paths() -> Vec<PathBuf> {
131 vec![PathBuf::from("src")]
132 }
133
134 fn get_default_exercise_file_paths() -> Vec<PathBuf> {
135 vec![PathBuf::from("test")]
136 }
137
138 fn points_parser(_: &str) -> IResult<&str, Vec<&str>, VerboseError<&str>> {
139 Err(nom::Err::Error(VerboseError { errors: vec![] }))
141 }
142}
143
144#[cfg(test)]
145#[allow(clippy::unwrap_used)]
146mod test {
147 use super::*;
148
149 fn init() {
150 use log::*;
151 use simple_logger::*;
152 let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init();
153 }
154
155 fn file_to(
156 target_dir: impl AsRef<std::path::Path>,
157 target_relative: impl AsRef<std::path::Path>,
158 contents: impl AsRef<[u8]>,
159 ) -> PathBuf {
160 let target = target_dir.as_ref().join(target_relative);
161 if let Some(parent) = target.parent() {
162 std::fs::create_dir_all(parent).unwrap();
163 }
164 std::fs::write(&target, contents.as_ref()).unwrap();
165 target
166 }
167
168 #[test]
169 fn gets_points() {
170 init();
171
172 let temp_dir = tempfile::tempdir().unwrap();
173 file_to(
174 &temp_dir,
175 ".tmcproject.yml",
176 r#"
177no-tests:
178 points:
179 - point1
180 - point2
181 - 3
182 - 4
183"#,
184 );
185
186 let points = NoTestsPlugin::get_points(temp_dir.path());
187 assert_eq!(points.len(), 4)
188 }
189
190 #[test]
191 fn scans_exercise() {
192 init();
193
194 let plugin = NoTestsPlugin::new();
195 let _exercise_desc = plugin
196 .scan_exercise(Path::new("/nonexistent path"), "ex".to_string())
197 .unwrap();
198 }
199
200 #[test]
201 fn runs_tests_ignores_timeout() {
202 init();
203
204 let plugin = NoTestsPlugin::new();
205 let run_result = plugin
206 .run_tests_with_timeout(
207 Path::new("/nonexistent"),
208 Some(std::time::Duration::from_nanos(1)),
209 )
210 .unwrap();
211 assert_eq!(run_result.status, RunStatus::Passed);
212 }
213
214 #[test]
215 fn exercise_type_is_correct() {
216 init();
217
218 let temp_dir = tempfile::tempdir().unwrap();
219 file_to(
220 &temp_dir,
221 ".tmcproject.yml",
222 r#"
223no-tests:
224 points: [point1]
225"#,
226 );
227 assert!(NoTestsPlugin::is_exercise_type_correct(temp_dir.path()));
228
229 let temp_dir = tempfile::tempdir().unwrap();
230 file_to(
231 &temp_dir,
232 ".tmcproject.yml",
233 r#"
234no-tests: true
235"#,
236 );
237 assert!(NoTestsPlugin::is_exercise_type_correct(temp_dir.path()));
238 }
239
240 #[test]
241 fn exercise_type_is_not_correct() {
242 init();
243
244 let temp_dir = tempfile::tempdir().unwrap();
245 assert!(!NoTestsPlugin::is_exercise_type_correct(temp_dir.path()));
246
247 let temp_dir = tempfile::tempdir().unwrap();
248 file_to(&temp_dir, ".tmcproject.yml", r#""#);
249 assert!(!NoTestsPlugin::is_exercise_type_correct(temp_dir.path()));
250
251 let temp_dir = tempfile::tempdir().unwrap();
252 file_to(
253 &temp_dir,
254 ".tmcproject.yml",
255 r#"
256no-tests: false
257"#,
258 );
259 assert!(!NoTestsPlugin::is_exercise_type_correct(temp_dir.path()));
260 }
261
262 #[test]
263 fn parses_empty() {
264 init();
265
266 let temp = tempfile::tempdir().unwrap();
267 file_to(&temp, "test/.keep", r#""#);
268
269 let points = NoTestsPlugin::get_available_points(temp.path()).unwrap();
270 assert!(points.is_empty());
271
272 let temp = tempfile::tempdir().unwrap();
273 file_to(
274 &temp,
275 "test/.keep",
276 r#"
277"#,
278 );
279
280 let points = NoTestsPlugin::get_available_points(temp.path()).unwrap();
281 assert!(points.is_empty());
282 }
283}