diff --git a/Cargo.lock b/Cargo.lock index 644df9eb20d..d5dafe8bafa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2485,7 +2485,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tmc-langs" -version = "0.39.1" +version = "0.39.2" dependencies = [ "base64 0.22.1", "blake3", @@ -2528,7 +2528,7 @@ dependencies = [ [[package]] name = "tmc-langs-cli" -version = "0.39.1" +version = "0.39.2" dependencies = [ "anyhow", "base64 0.22.1", @@ -2555,7 +2555,7 @@ dependencies = [ [[package]] name = "tmc-langs-csharp" -version = "0.39.1" +version = "0.39.2" dependencies = [ "dirs", "log", @@ -2573,7 +2573,7 @@ dependencies = [ [[package]] name = "tmc-langs-framework" -version = "0.39.1" +version = "0.39.2" dependencies = [ "blake3", "fd-lock", @@ -2600,7 +2600,7 @@ dependencies = [ [[package]] name = "tmc-langs-java" -version = "0.39.1" +version = "0.39.2" dependencies = [ "dirs", "flate2", @@ -2621,7 +2621,7 @@ dependencies = [ [[package]] name = "tmc-langs-make" -version = "0.39.1" +version = "0.39.2" dependencies = [ "log", "once_cell", @@ -2640,7 +2640,7 @@ dependencies = [ [[package]] name = "tmc-langs-node" -version = "0.39.1" +version = "0.39.2" dependencies = [ "base64 0.22.1", "env_logger", @@ -2658,7 +2658,7 @@ dependencies = [ [[package]] name = "tmc-langs-notests" -version = "0.39.1" +version = "0.39.2" dependencies = [ "log", "simple_logger", @@ -2670,7 +2670,7 @@ dependencies = [ [[package]] name = "tmc-langs-plugins" -version = "0.39.1" +version = "0.39.2" dependencies = [ "blake3", "log", @@ -2693,7 +2693,7 @@ dependencies = [ [[package]] name = "tmc-langs-python3" -version = "0.39.1" +version = "0.39.2" dependencies = [ "dunce", "hex", @@ -2715,7 +2715,7 @@ dependencies = [ [[package]] name = "tmc-langs-r" -version = "0.39.1" +version = "0.39.2" dependencies = [ "log", "serde", @@ -2731,7 +2731,7 @@ dependencies = [ [[package]] name = "tmc-langs-util" -version = "0.39.1" +version = "0.39.2" dependencies = [ "dunce", "fd-lock", @@ -2756,7 +2756,7 @@ dependencies = [ [[package]] name = "tmc-mooc-client" -version = "0.39.1" +version = "0.39.2" dependencies = [ "bytes", "chrono", @@ -2779,7 +2779,7 @@ dependencies = [ [[package]] name = "tmc-server-mock" -version = "0.39.1" +version = "0.39.2" dependencies = [ "mockito", "serde_json", @@ -2787,7 +2787,7 @@ dependencies = [ [[package]] name = "tmc-testmycode-client" -version = "0.39.1" +version = "0.39.2" dependencies = [ "chrono", "dirs", diff --git a/Cargo.toml b/Cargo.toml index 4e3aa0bcb1f..8db5f38b5a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ authors = [ edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.85.0" -version = "0.39.1" +version = "0.39.2" [workspace.dependencies] mooc-langs-api = { git = "https://github.com/rage/secret-project-331.git", rev = "24179d597e5f4120649be50b903a9a4e544ea77c" } diff --git a/crates/tmc-langs-framework/src/tmc_project_yml.rs b/crates/tmc-langs-framework/src/tmc_project_yml.rs index f3ceccbf811..ccc2b8c4794 100644 --- a/crates/tmc-langs-framework/src/tmc_project_yml.rs +++ b/crates/tmc-langs-framework/src/tmc_project_yml.rs @@ -137,7 +137,8 @@ impl TmcProjectYml { /// Saves the TmcProjectYml to the given directory. pub fn save_to_dir(&self, dir: &Path) -> Result<(), TmcError> { let config_path = Self::path_in_dir(dir); - let mut lock = Lock::file(&config_path, LockOptions::WriteCreate)?; + // 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. + let mut lock = Lock::file(&config_path, LockOptions::WriteTruncate)?; let mut guard = lock.lock()?; serde_yaml::to_writer(guard.get_file_mut(), &self)?; Ok(()) @@ -374,4 +375,39 @@ mod test { let tpy = TmcProjectYml::load(temp.path()).unwrap().unwrap(); assert_eq!(tpy.tests_timeout_ms, Some(1234)); } + + #[test] + fn saves_truncates_file_not_appends() { + init(); + + let temp = tempfile::tempdir().unwrap(); + + // First save + let first = TmcProjectYml { + tests_timeout_ms: Some(1000), + ..Default::default() + }; + first.save_to_dir(temp.path()).unwrap(); + + // Second save with a different value to ensure old contents are not kept + let second = TmcProjectYml { + tests_timeout_ms: Some(2000), + ..Default::default() + }; + second.save_to_dir(temp.path()).unwrap(); + + // Read raw YAML and ensure tests_timeout_ms occurs only once + let yaml_path = TmcProjectYml::path_in_dir(temp.path()); + let yaml = std::fs::read_to_string(&yaml_path).unwrap(); + let occurrences = yaml.matches("tests_timeout_ms").count(); + assert_eq!( + occurrences, 1, + "YAML should contain the key only once: {}", + yaml + ); + + // And the file is still valid YAML after the second save + let parsed = TmcProjectYml::load(temp.path()).unwrap().unwrap(); + assert_eq!(parsed.tests_timeout_ms, Some(2000)); + } } diff --git a/crates/tmc-langs/src/course_refresher.rs b/crates/tmc-langs/src/course_refresher.rs index 669ac9923d9..3a55f132036 100644 --- a/crates/tmc-langs/src/course_refresher.rs +++ b/crates/tmc-langs/src/course_refresher.rs @@ -712,4 +712,41 @@ mod test { assert_eq!(tpyb.tests_timeout_ms, Some(1234)); assert_eq!(tpyb.fail_on_valgrind_error, Some(false)); } + + #[test] + fn merges_tmcproject_configs_exercise_overrides_root() { + init(); + + let temp = tempfile::tempdir().unwrap(); + let exercise_path = PathBuf::from("exercise"); + let exercise_dir = temp.path().join(&exercise_path); + file_util::create_dir(&exercise_dir).unwrap(); + + // Root config has tests_timeout_ms: 1000 + let root = TmcProjectYml { + tests_timeout_ms: Some(1000), + fail_on_valgrind_error: Some(true), + ..Default::default() + }; + + // Exercise config has tests_timeout_ms: 2000 (should override root) + let exercise_config = TmcProjectYml { + tests_timeout_ms: Some(2000), + ..Default::default() + }; + exercise_config.save_to_dir(&exercise_dir).unwrap(); + + let exercise_dirs = vec![exercise_path]; + + let dirs_configs = + get_and_merge_tmcproject_configs(Some(root), temp.path(), exercise_dirs).unwrap(); + + let (_, merged_config) = &dirs_configs[0]; + let merged_config = merged_config.as_ref().unwrap(); + + // Exercise values should override root values when both are present + assert_eq!(merged_config.tests_timeout_ms, Some(2000)); + // Root values should be inherited when exercise doesn't have that field + assert_eq!(merged_config.fail_on_valgrind_error, Some(true)); + } }