tmc_langs_framework/
policy.rs

1//! Contains StudentFilePolicy.
2
3use crate::{TmcError, TmcProjectYml};
4use std::path::Path;
5
6/// Specifies which files are student files. A single StudentFilePolicy is only valid for a single project as it uses a config file to determine its output.
7///
8/// Student files are any files that are expected to be modified and/or created by the student.
9/// That is, any files that should not be overwritten when when updating an already downloaded
10/// exercise and any files that should be submitted to the server.
11pub trait StudentFilePolicy {
12    /// This constructor should store the project config in the implementing struct.
13    fn new_with_project_config(project_config: TmcProjectYml) -> Self
14    where
15        Self: Sized;
16
17    /// Parses a project config and calls the helper constructor. Implementing types should only be constructed using this function.
18    fn new(project_dir: &Path) -> Result<Self, TmcError>
19    where
20        Self: Sized,
21    {
22        let project_config = TmcProjectYml::load_or_default(project_dir)?;
23        Ok(Self::new_with_project_config(project_config))
24    }
25
26    /// The policy should contain a TmcProjectYml parsed from the project this policy was created for.
27    fn get_project_config(&self) -> &TmcProjectYml;
28
29    /// Determines whether a file is a student source file.
30    ///
31    /// A file should be considered a student source file if it resides in a location the student
32    /// is expected to create his or her own source files in the general case. Any special cases
33    /// are specified as ExtraStudentFiles in a separate configuration.
34    ///
35    /// For example in a Java project that uses Apache Ant, should return `true` for any files in the `src` directory.
36    ///
37    /// The file_path should be relative to the project root path.
38    fn is_student_file(&self, file_path: &Path) -> bool {
39        // .tmcproject.yml should never be considered a student file
40        if file_path == Path::new(".tmcproject.yml") {
41            return false;
42        }
43
44        // check extra student files
45        let config = self.get_project_config();
46        let is_extra_student_file = config
47            .extra_student_files
48            .iter()
49            .any(|f| file_path.starts_with(f));
50
51        let is_extra_exercise_file = config
52            .extra_exercise_files
53            .iter()
54            .any(|f| file_path.starts_with(f));
55
56        // extra student files take precedence, otherwise check if it's a non-extra student file and not an extra exercise file
57        is_extra_student_file
58            || (self.is_non_extra_student_file(file_path) && !is_extra_exercise_file)
59    }
60
61    /// Used by is_student_file.
62    /// Defines a language plugin policy's rules for determining whether a file is a student file or not.
63    fn is_non_extra_student_file(&self, file_path: &Path) -> bool;
64
65    /// Used to check for files which should always be overwritten.
66    ///
67    /// The file_path should be relative, starting from the project root.
68    fn is_updating_forced(&self, path: &Path) -> Result<bool, TmcError> {
69        for force_update_path in &self.get_project_config().force_update {
70            if path.starts_with(force_update_path) {
71                return Ok(true);
72            }
73        }
74        Ok(false)
75    }
76}
77
78/// Mock policy that ignores the config file and returns false for all files.
79// TODO: remove
80pub struct NothingIsStudentFilePolicy {
81    project_config: TmcProjectYml,
82}
83
84impl StudentFilePolicy for NothingIsStudentFilePolicy {
85    fn new(_project_dir: &Path) -> Result<Self, TmcError>
86    where
87        Self: Sized,
88    {
89        let project_config = TmcProjectYml {
90            ..Default::default()
91        };
92        Ok(Self { project_config })
93    }
94
95    fn new_with_project_config(_project_config: TmcProjectYml) -> Self
96    where
97        Self: Sized,
98    {
99        let project_config = TmcProjectYml {
100            ..Default::default()
101        };
102        Self { project_config }
103    }
104
105    fn get_project_config(&self) -> &TmcProjectYml {
106        &self.project_config
107    }
108
109    fn is_non_extra_student_file(&self, _path: &Path) -> bool {
110        false
111    }
112}
113
114/// Mock policy that ignores the config file and returns true for all files.
115// TODO: remove
116#[derive(Default)]
117pub struct EverythingIsStudentFilePolicy {
118    project_config: TmcProjectYml,
119}
120
121impl StudentFilePolicy for EverythingIsStudentFilePolicy {
122    fn new(_project_dir: &Path) -> Result<Self, TmcError>
123    where
124        Self: Sized,
125    {
126        let project_config = TmcProjectYml {
127            ..Default::default()
128        };
129        Ok(Self { project_config })
130    }
131
132    fn new_with_project_config(_project_config: TmcProjectYml) -> Self
133    where
134        Self: Sized,
135    {
136        let project_config = TmcProjectYml {
137            ..Default::default()
138        };
139        Self { project_config }
140    }
141
142    fn get_project_config(&self) -> &TmcProjectYml {
143        &self.project_config
144    }
145
146    fn is_non_extra_student_file(&self, _path: &Path) -> bool {
147        true
148    }
149}
150
151#[cfg(test)]
152#[allow(clippy::unwrap_used)]
153mod test {
154    use super::*;
155    use std::path::{Path, PathBuf};
156
157    fn init() {
158        use log::*;
159        use simple_logger::*;
160        let _ = SimpleLogger::new().with_level(LevelFilter::Debug).init();
161    }
162
163    fn file_to(
164        target_dir: impl AsRef<std::path::Path>,
165        target_relative: impl AsRef<std::path::Path>,
166        contents: impl AsRef<[u8]>,
167    ) -> PathBuf {
168        let target = target_dir.as_ref().join(target_relative);
169        if let Some(parent) = target.parent() {
170            std::fs::create_dir_all(parent).unwrap();
171        }
172        std::fs::write(&target, contents.as_ref()).unwrap();
173        target
174    }
175
176    struct MockPolicy {
177        project_config: TmcProjectYml,
178    }
179
180    impl StudentFilePolicy for MockPolicy {
181        fn new_with_project_config(project_config: TmcProjectYml) -> Self
182        where
183            Self: Sized,
184        {
185            Self { project_config }
186        }
187
188        fn get_project_config(&self) -> &TmcProjectYml {
189            &self.project_config
190        }
191
192        fn is_non_extra_student_file(&self, file_path: &Path) -> bool {
193            file_path
194                .components()
195                .any(|c| c.as_os_str() == "student_file")
196        }
197    }
198
199    #[test]
200    fn considers_student_source_files() {
201        init();
202
203        let temp = tempfile::tempdir().unwrap();
204        file_to(&temp, "dir/student_file/some file", "");
205        file_to(&temp, "other dir/student_file", "");
206        file_to(&temp, "other dir/other file", "");
207        file_to(&temp, "other file", "");
208
209        let project_config = TmcProjectYml::default();
210        let policy = MockPolicy { project_config };
211        assert!(policy.is_student_file(Path::new("dir/student_file/some file")));
212        assert!(policy.is_student_file(Path::new("other dir/student_file")));
213        assert!(!policy.is_student_file(Path::new("other dir/other file")));
214        assert!(!policy.is_student_file(Path::new("other file")));
215    }
216
217    #[test]
218    fn considers_extra_student_files() {
219        init();
220
221        let temp = tempfile::tempdir().unwrap();
222        file_to(&temp, "sdir/some file", "");
223        file_to(&temp, "other dir/sfile", "");
224        file_to(&temp, "other dir/other file", "");
225        file_to(&temp, "other file", "");
226
227        let project_config = TmcProjectYml {
228            extra_student_files: vec![PathBuf::from("sdir"), PathBuf::from("other dir/sfile")],
229            ..Default::default()
230        };
231        let policy = MockPolicy { project_config };
232        assert!(policy.is_student_file(Path::new("sdir/some file")));
233        assert!(policy.is_student_file(Path::new("other dir/sfile")));
234        assert!(!policy.is_student_file(Path::new("other dir/other file")));
235        assert!(!policy.is_student_file(Path::new("other file")));
236    }
237
238    #[test]
239    fn considers_force_uodate_paths() {
240        init();
241
242        let temp = tempfile::tempdir().unwrap();
243        file_to(&temp, "sdir/some file", "");
244        file_to(&temp, "other dir/sfile", "");
245        file_to(&temp, "other dir/other file", "");
246        file_to(&temp, "other file", "");
247
248        let project_config = TmcProjectYml {
249            force_update: vec![PathBuf::from("sdir"), PathBuf::from("other dir/sfile")],
250            ..Default::default()
251        };
252        let policy = MockPolicy { project_config };
253        assert!(
254            policy
255                .is_updating_forced(Path::new("sdir/some file"))
256                .unwrap()
257        );
258        assert!(
259            policy
260                .is_updating_forced(Path::new("other dir/sfile"))
261                .unwrap()
262        );
263        assert!(
264            !policy
265                .is_updating_forced(Path::new("other dir/other file"))
266                .unwrap()
267        );
268        assert!(!policy.is_updating_forced(Path::new("other file")).unwrap());
269    }
270
271    #[test]
272    fn is_object_safe() {
273        // this will fail to compile if the trait is not object safe
274        fn _f(_: Box<dyn StudentFilePolicy>) {}
275    }
276}