Skip to content

Commit 67e117f

Browse files
committed
Fix duplicate keys in .tmcproject.yml
1 parent 7a790e3 commit 67e117f

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

crates/tmc-langs-framework/src/tmc_project_yml.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,8 @@ impl TmcProjectYml {
137137
/// Saves the TmcProjectYml to the given directory.
138138
pub fn save_to_dir(&self, dir: &Path) -> Result<(), TmcError> {
139139
let config_path = Self::path_in_dir(dir);
140-
let mut lock = Lock::file(&config_path, LockOptions::WriteCreate)?;
140+
// It is important to truncate the file here, when we save the merged tmcproject.yml files, the exercise folder can already contain a .tmcproject.yml file. If we don't truncate the file before writing, all the merged values will be appended to the file, and duplicate keys will make the file invalid YAML.
141+
let mut lock = Lock::file(&config_path, LockOptions::WriteTruncate)?;
141142
let mut guard = lock.lock()?;
142143
serde_yaml::to_writer(guard.get_file_mut(), &self)?;
143144
Ok(())
@@ -374,4 +375,39 @@ mod test {
374375
let tpy = TmcProjectYml::load(temp.path()).unwrap().unwrap();
375376
assert_eq!(tpy.tests_timeout_ms, Some(1234));
376377
}
378+
379+
#[test]
380+
fn saves_truncates_file_not_appends() {
381+
init();
382+
383+
let temp = tempfile::tempdir().unwrap();
384+
385+
// First save
386+
let first = TmcProjectYml {
387+
tests_timeout_ms: Some(1000),
388+
..Default::default()
389+
};
390+
first.save_to_dir(temp.path()).unwrap();
391+
392+
// Second save with a different value to ensure old contents are not kept
393+
let second = TmcProjectYml {
394+
tests_timeout_ms: Some(2000),
395+
..Default::default()
396+
};
397+
second.save_to_dir(temp.path()).unwrap();
398+
399+
// Read raw YAML and ensure tests_timeout_ms occurs only once
400+
let yaml_path = TmcProjectYml::path_in_dir(temp.path());
401+
let yaml = std::fs::read_to_string(&yaml_path).unwrap();
402+
let occurrences = yaml.matches("tests_timeout_ms").count();
403+
assert_eq!(
404+
occurrences, 1,
405+
"YAML should contain the key only once: {}",
406+
yaml
407+
);
408+
409+
// And the file is still valid YAML after the second save
410+
let parsed = TmcProjectYml::load(temp.path()).unwrap().unwrap();
411+
assert_eq!(parsed.tests_timeout_ms, Some(2000));
412+
}
377413
}

crates/tmc-langs/src/course_refresher.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,4 +712,41 @@ mod test {
712712
assert_eq!(tpyb.tests_timeout_ms, Some(1234));
713713
assert_eq!(tpyb.fail_on_valgrind_error, Some(false));
714714
}
715+
716+
#[test]
717+
fn merges_tmcproject_configs_exercise_overrides_root() {
718+
init();
719+
720+
let temp = tempfile::tempdir().unwrap();
721+
let exercise_path = PathBuf::from("exercise");
722+
let exercise_dir = temp.path().join(&exercise_path);
723+
file_util::create_dir(&exercise_dir).unwrap();
724+
725+
// Root config has tests_timeout_ms: 1000
726+
let root = TmcProjectYml {
727+
tests_timeout_ms: Some(1000),
728+
fail_on_valgrind_error: Some(true),
729+
..Default::default()
730+
};
731+
732+
// Exercise config has tests_timeout_ms: 2000 (should override root)
733+
let exercise_config = TmcProjectYml {
734+
tests_timeout_ms: Some(2000),
735+
..Default::default()
736+
};
737+
exercise_config.save_to_dir(&exercise_dir).unwrap();
738+
739+
let exercise_dirs = vec![exercise_path];
740+
741+
let dirs_configs =
742+
get_and_merge_tmcproject_configs(Some(root), temp.path(), exercise_dirs).unwrap();
743+
744+
let (_, merged_config) = &dirs_configs[0];
745+
let merged_config = merged_config.as_ref().unwrap();
746+
747+
// Exercise values should override root values when both are present
748+
assert_eq!(merged_config.tests_timeout_ms, Some(2000));
749+
// Root values should be inherited when exercise doesn't have that field
750+
assert_eq!(merged_config.fail_on_valgrind_error, Some(true));
751+
}
715752
}

0 commit comments

Comments
 (0)