Skip to content

Commit 078d0e2

Browse files
authored
Merge pull request #218 from rage/duplicate-yaml-keys
Fix duplicate keys in .tmcproject.yml after merging
2 parents 7a790e3 + d937c43 commit 078d0e2

File tree

4 files changed

+90
-17
lines changed

4 files changed

+90
-17
lines changed

Cargo.lock

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ authors = [
2222
edition = "2024"
2323
license = "MIT OR Apache-2.0"
2424
rust-version = "1.85.0"
25-
version = "0.39.1"
25+
version = "0.39.2"
2626

2727
[workspace.dependencies]
2828
mooc-langs-api = { git = "https://github.com/rage/secret-project-331.git", rev = "24179d597e5f4120649be50b903a9a4e544ea77c" }

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)