33use crate :: TmcError ;
44use serde:: {
55 de:: { Error , Visitor } ,
6- Deserialize , Deserializer ,
6+ Deserialize , Deserializer , Serialize ,
77} ;
88use std:: fmt;
9+ use std:: ops:: Deref ;
910use std:: path:: { Path , PathBuf } ;
10- use tmc_langs_util:: file_util;
11+ use tmc_langs_util:: { file_util, FileError } ;
1112
1213/// Extra data from a `.tmcproject.yml` file.
13- #[ derive( Debug , Deserialize , Default ) ]
14+ // NOTE: when adding fields, remember to update the merge function as well
15+ #[ derive( Debug , Serialize , Deserialize , Default , Clone ) ]
1416pub struct TmcProjectYml {
1517 #[ serde( default ) ]
1618 pub extra_student_files : Vec < PathBuf > ,
@@ -36,9 +38,12 @@ pub struct TmcProjectYml {
3638}
3739
3840impl TmcProjectYml {
41+ fn path_in_dir ( dir : & Path ) -> PathBuf {
42+ dir. join ( ".tmcproject.yml" )
43+ }
44+
3945 pub fn from ( project_dir : & Path ) -> Result < Self , TmcError > {
40- let mut config_path = project_dir. to_owned ( ) ;
41- config_path. push ( ".tmcproject.yml" ) ;
46+ let config_path = Self :: path_in_dir ( project_dir) ;
4247
4348 if !config_path. exists ( ) {
4449 log:: trace!( "no config found at {}" , config_path. display( ) ) ;
@@ -50,11 +55,47 @@ impl TmcProjectYml {
5055 log:: trace!( "read {:#?}" , config) ;
5156 Ok ( config)
5257 }
58+
59+ /// Merges the contents of `with` with `self`.
60+ /// Empty or missing values in self are replaced with those from with. Other values are left unchanged.
61+ pub fn merge ( & mut self , with : Self ) {
62+ if self . extra_student_files . is_empty ( ) {
63+ self . extra_student_files = with. extra_student_files ;
64+ }
65+ if self . extra_exercise_files . is_empty ( ) {
66+ self . extra_exercise_files = with. extra_exercise_files ;
67+ }
68+ if self . force_update . is_empty ( ) {
69+ self . force_update = with. force_update ;
70+ }
71+ if self . tests_timeout_ms . is_none ( ) {
72+ self . tests_timeout_ms = with. tests_timeout_ms ;
73+ }
74+ if self . no_tests . is_none ( ) {
75+ self . no_tests = with. no_tests ;
76+ }
77+ if self . fail_on_valgrind_error . is_none ( ) {
78+ self . fail_on_valgrind_error = with. fail_on_valgrind_error ;
79+ }
80+ if self . minimum_python_version . is_none ( ) {
81+ self . minimum_python_version = with. minimum_python_version ;
82+ }
83+ }
84+
85+ pub fn save_to_dir ( & self , dir : & Path ) -> Result < ( ) , TmcError > {
86+ let config_path = Self :: path_in_dir ( dir) ;
87+ let mut file = file_util:: create_file_lock ( & config_path) ?;
88+ let guard = file
89+ . lock ( )
90+ . map_err ( |e| FileError :: FdLock ( config_path. clone ( ) , e) ) ?;
91+ serde_yaml:: to_writer ( guard. deref ( ) , & self ) ?;
92+ Ok ( ( ) )
93+ }
5394}
5495
5596/// Minimum Python version requirement.
5697/// TODO: if patch is Some minor is also guaranteed to be Some etc. encode this in the type system
57- #[ derive( Debug , Default ) ]
98+ #[ derive( Debug , Default , Clone , Copy , Serialize ) ]
5899pub struct PythonVer {
59100 pub major : Option < usize > ,
60101 pub minor : Option < usize > ,
@@ -112,7 +153,7 @@ impl<'de> Deserialize<'de> for PythonVer {
112153}
113154
114155/// Contents of the no-tests field.
115- #[ derive( Debug , Deserialize ) ]
156+ #[ derive( Debug , Serialize , Deserialize , Clone ) ]
116157#[ serde( from = "NoTestsWrapper" ) ]
117158pub struct NoTests {
118159 pub flag : bool ,
@@ -167,8 +208,16 @@ pub enum IntOrString {
167208mod test {
168209 use super :: * ;
169210
211+ fn init ( ) {
212+ use log:: * ;
213+ use simple_logger:: * ;
214+ let _ = SimpleLogger :: new ( ) . with_level ( LevelFilter :: Debug ) . init ( ) ;
215+ }
216+
170217 #[ test]
171218 fn deserialize_no_tests ( ) {
219+ init ( ) ;
220+
172221 let no_tests_yml = r#"no-tests:
173222 points:
174223 - 1
@@ -183,6 +232,8 @@ mod test {
183232
184233 #[ test]
185234 fn deserialize_python_ver ( ) {
235+ init ( ) ;
236+
186237 let python_ver: PythonVer = serde_yaml:: from_str ( "1.2.3" ) . unwrap ( ) ;
187238 assert_eq ! ( python_ver. major, Some ( 1 ) ) ;
188239 assert_eq ! ( python_ver. minor, Some ( 2 ) ) ;
@@ -200,4 +251,42 @@ mod test {
200251
201252 assert ! ( serde_yaml:: from_str:: <PythonVer >( "asd" ) . is_err( ) )
202253 }
254+
255+ #[ test]
256+ fn merges ( ) {
257+ init ( ) ;
258+
259+ let tpy_root = TmcProjectYml {
260+ tests_timeout_ms : Some ( 123 ) ,
261+ fail_on_valgrind_error : Some ( true ) ,
262+ ..Default :: default ( )
263+ } ;
264+ let mut tpy_exercise = TmcProjectYml {
265+ tests_timeout_ms : Some ( 234 ) ,
266+ ..Default :: default ( )
267+ } ;
268+ tpy_exercise. merge ( tpy_root) ;
269+ assert_eq ! ( tpy_exercise. tests_timeout_ms, Some ( 234 ) ) ;
270+ assert_eq ! ( tpy_exercise. fail_on_valgrind_error, Some ( true ) ) ;
271+ }
272+
273+ #[ test]
274+ fn saves_to_dir ( ) {
275+ init ( ) ;
276+
277+ let temp = tempfile:: tempdir ( ) . unwrap ( ) ;
278+ let path = TmcProjectYml :: path_in_dir ( temp. path ( ) ) ;
279+
280+ assert ! ( !path. exists( ) ) ;
281+
282+ let tpy = TmcProjectYml {
283+ tests_timeout_ms : Some ( 1234 ) ,
284+ ..Default :: default ( )
285+ } ;
286+ tpy. save_to_dir ( temp. path ( ) ) . unwrap ( ) ;
287+
288+ assert ! ( path. exists( ) ) ;
289+ let tpy = TmcProjectYml :: from ( temp. path ( ) ) . unwrap ( ) ;
290+ assert_eq ! ( tpy. tests_timeout_ms, Some ( 1234 ) ) ;
291+ }
203292}
0 commit comments