From 48f9778e3692ab11c4c56864b7db249d33f68a48 Mon Sep 17 00:00:00 2001 From: Heliozoa Date: Tue, 7 Oct 2025 12:42:05 +0300 Subject: [PATCH 1/3] Simplify student file policies --- .gitignore | 1 + Cargo.lock | 2 + crates/bindings/tmc-langs-node/src/lib.rs | 2 +- crates/plugins/csharp/src/policy.rs | 26 +- crates/plugins/java/src/ant_policy.rs | 9 +- crates/plugins/java/src/maven_plugin.rs | 13 +- crates/plugins/java/src/maven_policy.rs | 4 +- crates/plugins/make/src/policy.rs | 5 +- crates/plugins/python3/src/policy.rs | 31 +- crates/plugins/r/src/policy.rs | 6 +- crates/tmc-langs-cli/src/lib.rs | 2 +- ..._project_tar@csharp__failing-exercise.snap | 2 +- ...ct_tar@csharp__non-compiling-exercise.snap | 2 +- ...project_tar@csharp__partially-passing.snap | 2 +- ..._project_tar@csharp__passing-exercise.snap | 2 +- ...mpress_project_tar@java__ant-exercise.snap | 2 +- ...ress_project_tar@java__maven-exercise.snap | 2 +- ..._project_tar@make__failing-exercise-2.snap | 3 - ...ss_project_tar@make__failing-exercise.snap | 2 +- ..._project_tar@make__passing-exercise-2.snap | 3 - ...ss_project_tar@make__passing-exercise.snap | 2 +- ...tar@make__valgrind-failing-exercise-2.snap | 3 - ...t_tar@make__valgrind-failing-exercise.snap | 2 +- ...ompress_project_tar@python3__exercise.snap | 2 +- ...ion__compress_project_tar@r__exercise.snap | 2 +- ..._project_zip@csharp__failing-exercise.snap | 2 +- ...ct_zip@csharp__non-compiling-exercise.snap | 2 +- ...project_zip@csharp__partially-passing.snap | 2 +- ..._project_zip@csharp__passing-exercise.snap | 2 +- ...ress_project_zip@java__ant-exercise-2.snap | 1 - ...mpress_project_zip@java__ant-exercise.snap | 2 +- ...ress_project_zip@java__maven-exercise.snap | 2 +- ..._project_zip@make__failing-exercise-2.snap | 3 - ...ss_project_zip@make__failing-exercise.snap | 2 +- ..._project_zip@make__passing-exercise-2.snap | 3 - ...ss_project_zip@make__passing-exercise.snap | 2 +- ...zip@make__valgrind-failing-exercise-2.snap | 3 - ...t_zip@make__valgrind-failing-exercise.snap | 2 +- ...ompress_project_zip@python3__exercise.snap | 2 +- ...ion__compress_project_zip@r__exercise.snap | 2 +- ...project_zstd@csharp__failing-exercise.snap | 2 +- ...t_zstd@csharp__non-compiling-exercise.snap | 2 +- ...roject_zstd@csharp__partially-passing.snap | 2 +- ...project_zstd@csharp__passing-exercise.snap | 2 +- ...ess_project_zstd@java__ant-exercise-2.snap | 1 - ...press_project_zstd@java__ant-exercise.snap | 2 +- ...ess_project_zstd@java__maven-exercise.snap | 2 +- ...project_zstd@make__failing-exercise-2.snap | 3 - ...s_project_zstd@make__failing-exercise.snap | 2 +- ...project_zstd@make__passing-exercise-2.snap | 3 - ...s_project_zstd@make__passing-exercise.snap | 2 +- ...std@make__valgrind-failing-exercise-2.snap | 3 - ..._zstd@make__valgrind-failing-exercise.snap | 2 +- ...mpress_project_zstd@python3__exercise.snap | 2 +- ...on__compress_project_zstd@r__exercise.snap | 2 +- crates/tmc-langs-framework/src/archive.rs | 293 +++++++++++++----- crates/tmc-langs-framework/src/error.rs | 11 +- crates/tmc-langs-framework/src/lib.rs | 2 +- .../src/tmc_project_yml.rs | 20 +- crates/tmc-langs-plugins/src/archive.rs | 122 -------- crates/tmc-langs-plugins/src/compression.rs | 50 ++- crates/tmc-langs-plugins/src/lib.rs | 1 - crates/tmc-langs/Cargo.toml | 2 + crates/tmc-langs/src/lib.rs | 6 +- crates/tmc-langs/tests/integration.rs | 234 ++++++++++++++ crates/tmc-testmycode-client/src/client.rs | 19 +- .../src/client/api_v8.rs | 8 +- crates/tmc-testmycode-client/src/lib.rs | 2 +- .../tests/api_integration.rs | 4 +- docker/Dockerfile | 2 +- docs/tmcproject.md | 4 +- docker.sh => scripts/docker.sh | 4 + scripts/tmc-batch-test.bash | 9 + 73 files changed, 598 insertions(+), 391 deletions(-) delete mode 100644 crates/tmc-langs-plugins/src/archive.rs create mode 100644 crates/tmc-langs/tests/integration.rs rename docker.sh => scripts/docker.sh (67%) create mode 100644 scripts/tmc-batch-test.bash diff --git a/.gitignore b/.gitignore index c97713135d4..ab74f1b09cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk .vscode +/test-cache diff --git a/Cargo.lock b/Cargo.lock index cf453aab494..b49cc333d1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2489,6 +2489,7 @@ dependencies = [ "blake3", "chrono", "dirs", + "env_logger", "hmac", "jwt", "log", @@ -2498,6 +2499,7 @@ dependencies = [ "oauth2", "once_cell", "regex", + "rpassword", "schemars", "serde", "serde_json", diff --git a/crates/bindings/tmc-langs-node/src/lib.rs b/crates/bindings/tmc-langs-node/src/lib.rs index 345ca85ba44..b04f1f0b835 100644 --- a/crates/bindings/tmc-langs-node/src/lib.rs +++ b/crates/bindings/tmc-langs-node/src/lib.rs @@ -547,7 +547,7 @@ fn login(mut cx: FunctionContext) -> JsResult { password }; let token = with_client(&client_name, client_version, |client| { - tmc_langs::login_with_password(client, &client_name, email, decoded) + tmc_langs::login_with_password(client, email, decoded) }) .map_err(|e| convert_err(&mut cx, e))?; diff --git a/crates/plugins/csharp/src/policy.rs b/crates/plugins/csharp/src/policy.rs index a6bf1ef59a0..bb460618761 100644 --- a/crates/plugins/csharp/src/policy.rs +++ b/crates/plugins/csharp/src/policy.rs @@ -1,27 +1,12 @@ //! Student file policy for the C# plugin. -use std::path::Path; +use std::{ffi::OsStr, path::Path}; use tmc_langs_framework::{StudentFilePolicy, TmcProjectYml}; pub struct CSharpStudentFilePolicy { project_config: TmcProjectYml, } -impl CSharpStudentFilePolicy { - /// Goes up directories until a bin or obj directory is found, either indicating that the path is in a binary directory. - fn is_child_of_binary_dir(path: &Path) -> bool { - // checks each parent directory for bin or obj - for ancestor in path.ancestors().skip(1) { - if let Some(file_name) = ancestor.file_name() { - if file_name == "bin" || file_name == "obj" { - return true; - } - } - } - false - } -} - impl StudentFilePolicy for CSharpStudentFilePolicy { fn new_with_project_config(project_config: TmcProjectYml) -> Self where @@ -34,14 +19,9 @@ impl StudentFilePolicy for CSharpStudentFilePolicy { &self.project_config } - // false for .csproj files and files in bin or obj directories - // true for files in src except for .csproj files + // .cs files in src fn is_non_extra_student_file(&self, path: &Path) -> bool { - path.starts_with("src") - // exclude files in bin - && !Self::is_child_of_binary_dir(path) - // exclude .csproj files - && !path.extension().map(|ext| ext == "csproj").unwrap_or_default() + path.starts_with("src") && path.extension() == Some(OsStr::new("cs")) } } diff --git a/crates/plugins/java/src/ant_policy.rs b/crates/plugins/java/src/ant_policy.rs index 2abc8f1b11d..a0d3d461dbd 100644 --- a/crates/plugins/java/src/ant_policy.rs +++ b/crates/plugins/java/src/ant_policy.rs @@ -1,6 +1,6 @@ //! Ant student file policy -use std::path::Path; +use std::{ffi::OsStr, path::Path}; use tmc_langs_framework::{StudentFilePolicy, TmcProjectYml}; pub struct AntStudentFilePolicy { @@ -20,7 +20,7 @@ impl StudentFilePolicy for AntStudentFilePolicy { } fn is_non_extra_student_file(&self, path: &Path) -> bool { - path.starts_with("src") + path.starts_with("src") && path.extension() == Some(OsStr::new("java")) } } @@ -31,13 +31,14 @@ mod test { #[test] fn is_student_file() { let policy = AntStudentFilePolicy::new(Path::new(".")).unwrap(); - assert!(policy.is_student_file(Path::new("src/file"))); - assert!(policy.is_student_file(Path::new("src/dir/file"))); + assert!(policy.is_student_file(Path::new("src/file.java"))); + assert!(policy.is_student_file(Path::new("src/dir/file.java"))); } #[test] fn is_not_student_source_file() { let policy = AntStudentFilePolicy::new(Path::new(".")).unwrap(); + assert!(policy.is_student_file(Path::new("src/file"))); assert!(!policy.is_student_file(Path::new("file"))); assert!(!policy.is_student_file(Path::new("dir/src/file"))); assert!(!policy.is_student_file(Path::new("srca/file"))); diff --git a/crates/plugins/java/src/maven_plugin.rs b/crates/plugins/java/src/maven_plugin.rs index 4754e0411e8..0bb01f56bdd 100644 --- a/crates/plugins/java/src/maven_plugin.rs +++ b/crates/plugins/java/src/maven_plugin.rs @@ -6,7 +6,7 @@ use crate::{ }; use flate2::read::GzDecoder; use std::{ - ffi::OsString, + ffi::{OsStr, OsString}, io::{Cursor, Read, Seek}, ops::ControlFlow::{Break, Continue}, path::{Path, PathBuf}, @@ -132,14 +132,17 @@ impl LanguagePlugin for MavenPlugin { archive: &mut Archive, ) -> Result { let mut iter = archive.iter()?; + let project_dir = loop { let next = iter.with_next(|file| { let file_path = file.path()?; - if file.is_file() { - // check for pom.xml - if let Some(parent) = path_util::get_parent_of_named(&file_path, "pom.xml") { - return Ok(Break(Some(parent))); + if file.is_file() && file_path.extension() == Some(OsStr::new("java")) { + // check if java file has src as ancestor + for ancestor in file_path.ancestors() { + if let Some(src_parent) = path_util::get_parent_of_named(ancestor, "src") { + return Ok(Break(Some(src_parent))); + } } } Ok(Continue(())) diff --git a/crates/plugins/java/src/maven_policy.rs b/crates/plugins/java/src/maven_policy.rs index 7d686acf1b1..1a665f6f37c 100644 --- a/crates/plugins/java/src/maven_policy.rs +++ b/crates/plugins/java/src/maven_policy.rs @@ -1,6 +1,6 @@ //! Maven student file policy -use std::path::Path; +use std::{ffi::OsStr, path::Path}; use tmc_langs_framework::{StudentFilePolicy, TmcProjectYml}; pub struct MavenStudentFilePolicy { @@ -20,7 +20,7 @@ impl StudentFilePolicy for MavenStudentFilePolicy { } fn is_non_extra_student_file(&self, path: &Path) -> bool { - path.starts_with("src/main") + path.starts_with("src/main") && path.extension() == Some(OsStr::new("java")) } } diff --git a/crates/plugins/make/src/policy.rs b/crates/plugins/make/src/policy.rs index 42c3298b6bc..c03c4b1f606 100644 --- a/crates/plugins/make/src/policy.rs +++ b/crates/plugins/make/src/policy.rs @@ -1,6 +1,6 @@ //! Contains the language policy for the plugin. -use std::path::Path; +use std::{ffi::OsStr, path::Path}; use tmc_langs_framework::{StudentFilePolicy, TmcProjectYml}; pub struct MakeStudentFilePolicy { @@ -20,7 +20,8 @@ impl StudentFilePolicy for MakeStudentFilePolicy { } fn is_non_extra_student_file(&self, path: &Path) -> bool { - path.starts_with("src") + let ext = path.extension(); + path.starts_with("src") && (ext == Some(OsStr::new("c")) || ext == Some(OsStr::new("h"))) } } diff --git a/crates/plugins/python3/src/policy.rs b/crates/plugins/python3/src/policy.rs index 5b555c06e3a..d6367eb7b24 100644 --- a/crates/plugins/python3/src/policy.rs +++ b/crates/plugins/python3/src/policy.rs @@ -20,33 +20,20 @@ impl StudentFilePolicy for Python3StudentFilePolicy { } fn is_non_extra_student_file(&self, path: &Path) -> bool { - // no files in tmc, test and venv subdirectories are considered student files - let is_cache_file = path.extension() == Some(OsStr::new("pyc")) - || path - .components() - .any(|c| c.as_os_str() == OsStr::new("__pycache__")); - let is_in_exercise_subdir = path.starts_with("test") || path.starts_with("tmc"); - let is_in_venv = path.starts_with(".venv") || path.starts_with("venv"); - if is_cache_file || is_in_exercise_subdir || is_in_venv { + // never include pyc files + let is_pyc = path.extension() == Some(OsStr::new("pyc")); + if is_pyc { return false; } - // all non-pyc or __pycache__ files in src are student source files let in_src = path.starts_with("src"); - // .py files in exercise root are student source files - let is_in_project_root = match path.parent() { + let in_exercise_root = match path.parent() { Some(s) => s.as_os_str().is_empty(), None => true, }; - let is_py_file = path.extension() == Some(OsStr::new("py")); + let is_py = path.extension() == Some(OsStr::new("py")); let is_ipynb = path.extension() == Some(OsStr::new("ipynb")); - - // all in all, excluding cache files and the exception subdirs, - // we take non-cache files in src, py files in root, everything not in the root and not in src, and all ipynb files - in_src - || is_in_project_root && is_py_file - || !is_in_exercise_subdir && !is_in_project_root - || is_ipynb + (in_src || in_exercise_root) && (is_py || is_ipynb) } } @@ -83,10 +70,10 @@ mod test { } #[test] - fn subdirs_are_student_files() { + fn subdirs_are_not_student_files() { let policy = Python3StudentFilePolicy::new(Path::new(".")).unwrap(); - assert!(policy.is_student_file(Path::new("subdir/something"))); - assert!(policy.is_student_file(Path::new("another/mid/else"))); + assert!(!policy.is_student_file(Path::new("subdir/something"))); + assert!(!policy.is_student_file(Path::new("another/mid/else"))); } #[test] diff --git a/crates/plugins/r/src/policy.rs b/crates/plugins/r/src/policy.rs index 51fb395e9f5..5bddef4e070 100644 --- a/crates/plugins/r/src/policy.rs +++ b/crates/plugins/r/src/policy.rs @@ -1,6 +1,6 @@ //! Contains the R student file policy -use std::path::Path; +use std::{ffi::OsStr, path::Path}; use tmc_langs_framework::{StudentFilePolicy, TmcProjectYml}; pub struct RStudentFilePolicy { @@ -20,7 +20,7 @@ impl StudentFilePolicy for RStudentFilePolicy { } fn is_non_extra_student_file(&self, path: &Path) -> bool { - path.starts_with("R") + path.starts_with("R") && path.extension() == Some(OsStr::new("R")) } } @@ -40,7 +40,7 @@ mod test { let policy = RStudentFilePolicy::new(Path::new(".")).unwrap(); assert!(policy.is_student_file(Path::new("R"))); - assert!(policy.is_student_file(Path::new("R/file"))); + assert!(policy.is_student_file(Path::new("R/file.R"))); } #[test] diff --git a/crates/tmc-langs-cli/src/lib.rs b/crates/tmc-langs-cli/src/lib.rs index 62dacfe8b22..b481dcdc50e 100644 --- a/crates/tmc-langs-cli/src/lib.rs +++ b/crates/tmc-langs-cli/src/lib.rs @@ -847,7 +847,7 @@ fn run_tmc_inner( } else { password }; - tmc_langs::login_with_password(client, client_name, email, decoded)? + tmc_langs::login_with_password(client, email, decoded)? } else { unreachable!("validation error"); }; diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__failing-exercise.snap index 021ebdde0a1..6e5f088d7a6 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: b1221f4d32193235543d57c173c4b7ebe75586f4cc56c455df67da761cfc5af8 + output-data: 0fd4baebce75eed92c41c7213c3e052308c2c0f6e91ebef9b5fa8ccae93cb607 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__non-compiling-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__non-compiling-exercise.snap index 06626673758..5cb340a1266 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__non-compiling-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__non-compiling-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 34856483dead601201bdf0e75c26481e97fdf36af401b3729b410711d57c13f5 + output-data: 65e4df36150280d62c526097d328c38b677ea0ebb6c4a88f7a52597c5a719dcd diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__partially-passing.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__partially-passing.snap index ceecd836b68..958bda37a27 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__partially-passing.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__partially-passing.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 753a9a1d15a783ca6b874ca9d4d38f647eabbf9897b2c91102dcbb284cfde7c9 + output-data: f9887325928a81c8b84c7a7fd457742606969f1504b47a8a8d7d562cc218c071 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__passing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__passing-exercise.snap index 807a56517f7..e2b8f2db25a 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__passing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@csharp__passing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: cd89870f4c9a5c09b1a13fd7119fa1a8e29ab8b06faf125e3f8dc30d3cee519e + output-data: dd4d587c1c25524f92321216bc2a32e75890fbe75e055155f48452789070be71 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__ant-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__ant-exercise.snap index 34f4a5c5ae7..9d521616287 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__ant-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__ant-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: dac5a9a56c89053fbcbf5d2c14618247b62584dfb1af4aa9602510a2f048b779 + output-data: b319b9e0a36dbc40a63e4f84ae67e86f8bf97b8963ecfd70d610c36c2cfa9def diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__maven-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__maven-exercise.snap index c66fcf7325f..57b2536dde8 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__maven-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@java__maven-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: beed439db1934bfe9210893772a4e1ba312ed323b4f8c9b1e4b30f850e283b5e + output-data: 4a6e0ea2c5af8a57dcf350922414034206ee1459a393b79e671312a5275f2cc6 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise-2.snap index b082b394c9c..ca42a5d7d45 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/failing-exercise --- - failing-exercise - failing-exercise/src -- failing-exercise/src/Makefile -- failing-exercise/src/main - failing-exercise/src/main.c - failing-exercise/src/source.c - failing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise.snap index b12dbf50193..7044099ac71 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: d8473af22ec3f6f558a8dfe030e5d80eeeb4b3a2bdb9cae495b5089083440966 + output-data: 42dd190e70d9e3cdaebf2302adc4ecacca771c2d3df55343e78bf48a9ca494d5 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise-2.snap index 368a7d156fa..6c771d17938 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/passing-exercise --- - passing-exercise - passing-exercise/src -- passing-exercise/src/Makefile -- passing-exercise/src/main - passing-exercise/src/main.c - passing-exercise/src/source.c - passing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise.snap index b4b880d16c9..0bfa9a1fff5 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__passing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 221fb1596de1d57e178f176f4cc30913dcf9bc0c11f18371612aee43e7923a21 + output-data: 7b5f432a372380e755df8c3204f8f1330a93ceeb662817c5ef33fbe1550a49a3 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise-2.snap index 8d6e6be06f3..950a16dc54f 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/valgrind-failing-exercise --- - valgrind-failing-exercise - valgrind-failing-exercise/src -- valgrind-failing-exercise/src/Makefile -- valgrind-failing-exercise/src/main - valgrind-failing-exercise/src/main.c - valgrind-failing-exercise/src/source.c - valgrind-failing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap index 21332164de4..8a51821d93d 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 4bcabff21d80c5401599d3984737be9a8b116ad41df3c8f5231a69600a37c926 + output-data: f22f8d2f4502dc8c14e98c9b89f8413b92b219e9ad1aaf6dfc429d1cc177745d diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@python3__exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@python3__exercise.snap index 9369a675106..831c00cabe2 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@python3__exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@python3__exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 6b484ee20469ba185feaed8b1b25e43fa659ba4a23fb52745a7f01cf96ff25c6 + output-data: 62a048c52fcbe87303d91fabf94bab1accf7b9b165100274633504873cd66609 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@r__exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@r__exercise.snap index b14d67600d2..b8efb5b7fd2 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@r__exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@r__exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 5a3286878deecd58c910721cbf0fa50640d4ef860e027ecd83802d03c1ac9789 + output-data: a2b82a0a89d27443bdcd2d480e53123991fa1fe3725e3071ade687399a81b280 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__failing-exercise.snap index 021ebdde0a1..6e5f088d7a6 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: b1221f4d32193235543d57c173c4b7ebe75586f4cc56c455df67da761cfc5af8 + output-data: 0fd4baebce75eed92c41c7213c3e052308c2c0f6e91ebef9b5fa8ccae93cb607 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__non-compiling-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__non-compiling-exercise.snap index 06626673758..5cb340a1266 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__non-compiling-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__non-compiling-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 34856483dead601201bdf0e75c26481e97fdf36af401b3729b410711d57c13f5 + output-data: 65e4df36150280d62c526097d328c38b677ea0ebb6c4a88f7a52597c5a719dcd diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__partially-passing.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__partially-passing.snap index ceecd836b68..958bda37a27 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__partially-passing.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__partially-passing.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 753a9a1d15a783ca6b874ca9d4d38f647eabbf9897b2c91102dcbb284cfde7c9 + output-data: f9887325928a81c8b84c7a7fd457742606969f1504b47a8a8d7d562cc218c071 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__passing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__passing-exercise.snap index 807a56517f7..e2b8f2db25a 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__passing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@csharp__passing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: cd89870f4c9a5c09b1a13fd7119fa1a8e29ab8b06faf125e3f8dc30d3cee519e + output-data: dd4d587c1c25524f92321216bc2a32e75890fbe75e055155f48452789070be71 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise-2.snap index 1b329298699..86f11a42f61 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise-2.snap @@ -6,4 +6,3 @@ input_file: sample_exercises/java/ant-exercise - ant-exercise - ant-exercise/src - ant-exercise/src/Arith.java - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise.snap index 34f4a5c5ae7..9d521616287 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__ant-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: dac5a9a56c89053fbcbf5d2c14618247b62584dfb1af4aa9602510a2f048b779 + output-data: b319b9e0a36dbc40a63e4f84ae67e86f8bf97b8963ecfd70d610c36c2cfa9def diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__maven-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__maven-exercise.snap index c66fcf7325f..57b2536dde8 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__maven-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@java__maven-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: beed439db1934bfe9210893772a4e1ba312ed323b4f8c9b1e4b30f850e283b5e + output-data: 4a6e0ea2c5af8a57dcf350922414034206ee1459a393b79e671312a5275f2cc6 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise-2.snap index b082b394c9c..ca42a5d7d45 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/failing-exercise --- - failing-exercise - failing-exercise/src -- failing-exercise/src/Makefile -- failing-exercise/src/main - failing-exercise/src/main.c - failing-exercise/src/source.c - failing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise.snap index b12dbf50193..7044099ac71 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: d8473af22ec3f6f558a8dfe030e5d80eeeb4b3a2bdb9cae495b5089083440966 + output-data: 42dd190e70d9e3cdaebf2302adc4ecacca771c2d3df55343e78bf48a9ca494d5 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise-2.snap index 368a7d156fa..6c771d17938 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/passing-exercise --- - passing-exercise - passing-exercise/src -- passing-exercise/src/Makefile -- passing-exercise/src/main - passing-exercise/src/main.c - passing-exercise/src/source.c - passing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise.snap index b4b880d16c9..0bfa9a1fff5 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__passing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 221fb1596de1d57e178f176f4cc30913dcf9bc0c11f18371612aee43e7923a21 + output-data: 7b5f432a372380e755df8c3204f8f1330a93ceeb662817c5ef33fbe1550a49a3 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise-2.snap index 8d6e6be06f3..950a16dc54f 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/valgrind-failing-exercise --- - valgrind-failing-exercise - valgrind-failing-exercise/src -- valgrind-failing-exercise/src/Makefile -- valgrind-failing-exercise/src/main - valgrind-failing-exercise/src/main.c - valgrind-failing-exercise/src/source.c - valgrind-failing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap index 21332164de4..8a51821d93d 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 4bcabff21d80c5401599d3984737be9a8b116ad41df3c8f5231a69600a37c926 + output-data: f22f8d2f4502dc8c14e98c9b89f8413b92b219e9ad1aaf6dfc429d1cc177745d diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@python3__exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@python3__exercise.snap index 9369a675106..831c00cabe2 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@python3__exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@python3__exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 6b484ee20469ba185feaed8b1b25e43fa659ba4a23fb52745a7f01cf96ff25c6 + output-data: 62a048c52fcbe87303d91fabf94bab1accf7b9b165100274633504873cd66609 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@r__exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@r__exercise.snap index b14d67600d2..b8efb5b7fd2 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@r__exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@r__exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 5a3286878deecd58c910721cbf0fa50640d4ef860e027ecd83802d03c1ac9789 + output-data: a2b82a0a89d27443bdcd2d480e53123991fa1fe3725e3071ade687399a81b280 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__failing-exercise.snap index 021ebdde0a1..6e5f088d7a6 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: b1221f4d32193235543d57c173c4b7ebe75586f4cc56c455df67da761cfc5af8 + output-data: 0fd4baebce75eed92c41c7213c3e052308c2c0f6e91ebef9b5fa8ccae93cb607 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__non-compiling-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__non-compiling-exercise.snap index 06626673758..5cb340a1266 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__non-compiling-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__non-compiling-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 34856483dead601201bdf0e75c26481e97fdf36af401b3729b410711d57c13f5 + output-data: 65e4df36150280d62c526097d328c38b677ea0ebb6c4a88f7a52597c5a719dcd diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__partially-passing.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__partially-passing.snap index ceecd836b68..958bda37a27 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__partially-passing.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__partially-passing.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 753a9a1d15a783ca6b874ca9d4d38f647eabbf9897b2c91102dcbb284cfde7c9 + output-data: f9887325928a81c8b84c7a7fd457742606969f1504b47a8a8d7d562cc218c071 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__passing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__passing-exercise.snap index 807a56517f7..e2b8f2db25a 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__passing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@csharp__passing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: cd89870f4c9a5c09b1a13fd7119fa1a8e29ab8b06faf125e3f8dc30d3cee519e + output-data: dd4d587c1c25524f92321216bc2a32e75890fbe75e055155f48452789070be71 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise-2.snap index 1b329298699..86f11a42f61 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise-2.snap @@ -6,4 +6,3 @@ input_file: sample_exercises/java/ant-exercise - ant-exercise - ant-exercise/src - ant-exercise/src/Arith.java - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise.snap index 34f4a5c5ae7..9d521616287 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__ant-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: dac5a9a56c89053fbcbf5d2c14618247b62584dfb1af4aa9602510a2f048b779 + output-data: b319b9e0a36dbc40a63e4f84ae67e86f8bf97b8963ecfd70d610c36c2cfa9def diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__maven-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__maven-exercise.snap index c66fcf7325f..57b2536dde8 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__maven-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@java__maven-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: beed439db1934bfe9210893772a4e1ba312ed323b4f8c9b1e4b30f850e283b5e + output-data: 4a6e0ea2c5af8a57dcf350922414034206ee1459a393b79e671312a5275f2cc6 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise-2.snap index b082b394c9c..ca42a5d7d45 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/failing-exercise --- - failing-exercise - failing-exercise/src -- failing-exercise/src/Makefile -- failing-exercise/src/main - failing-exercise/src/main.c - failing-exercise/src/source.c - failing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise.snap index b12dbf50193..7044099ac71 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: d8473af22ec3f6f558a8dfe030e5d80eeeb4b3a2bdb9cae495b5089083440966 + output-data: 42dd190e70d9e3cdaebf2302adc4ecacca771c2d3df55343e78bf48a9ca494d5 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise-2.snap index 368a7d156fa..6c771d17938 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/passing-exercise --- - passing-exercise - passing-exercise/src -- passing-exercise/src/Makefile -- passing-exercise/src/main - passing-exercise/src/main.c - passing-exercise/src/source.c - passing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise.snap index b4b880d16c9..0bfa9a1fff5 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__passing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 221fb1596de1d57e178f176f4cc30913dcf9bc0c11f18371612aee43e7923a21 + output-data: 7b5f432a372380e755df8c3204f8f1330a93ceeb662817c5ef33fbe1550a49a3 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise-2.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise-2.snap index 8d6e6be06f3..950a16dc54f 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise-2.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise-2.snap @@ -5,9 +5,6 @@ input_file: sample_exercises/make/valgrind-failing-exercise --- - valgrind-failing-exercise - valgrind-failing-exercise/src -- valgrind-failing-exercise/src/Makefile -- valgrind-failing-exercise/src/main - valgrind-failing-exercise/src/main.c - valgrind-failing-exercise/src/source.c - valgrind-failing-exercise/src/source.h - diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap index 21332164de4..8a51821d93d 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 4bcabff21d80c5401599d3984737be9a8b116ad41df3c8f5231a69600a37c926 + output-data: f22f8d2f4502dc8c14e98c9b89f8413b92b219e9ad1aaf6dfc429d1cc177745d diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@python3__exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@python3__exercise.snap index 9369a675106..831c00cabe2 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@python3__exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@python3__exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 6b484ee20469ba185feaed8b1b25e43fa659ba4a23fb52745a7f01cf96ff25c6 + output-data: 62a048c52fcbe87303d91fabf94bab1accf7b9b165100274633504873cd66609 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@r__exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@r__exercise.snap index b14d67600d2..b8efb5b7fd2 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@r__exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@r__exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: 5a3286878deecd58c910721cbf0fa50640d4ef860e027ecd83802d03c1ac9789 + output-data: a2b82a0a89d27443bdcd2d480e53123991fa1fe3725e3071ade687399a81b280 diff --git a/crates/tmc-langs-framework/src/archive.rs b/crates/tmc-langs-framework/src/archive.rs index c65664fcc22..0f67889ec96 100644 --- a/crates/tmc-langs-framework/src/archive.rs +++ b/crates/tmc-langs-framework/src/archive.rs @@ -9,10 +9,12 @@ use std::{ ops::ControlFlow::{self, Break}, path::{Path, PathBuf}, str::FromStr, + usize, }; +use tar::Builder; use tmc_langs_util::file_util; use walkdir::WalkDir; -use zip::write::SimpleFileOptions; +use zip::{DateTime, ZipWriter, write::SimpleFileOptions}; /// Wrapper unifying the API of all the different compression formats supported by langs. /// Unfortunately the API is more complicated due to tar only supporting iterating through the files one by one, @@ -299,99 +301,34 @@ impl Compression { hash: bool, size_limit_mb: u32, ) -> Result<(Vec, Option), TmcError> { - let mut hasher = if hash { Some(Hasher::new()) } else { None }; - let buf = match self { - Self::Tar => { - let buf = Cursor::new(Vec::new()); - let mut builder = tar::Builder::new(buf); - walk_dir_for_compression(path, |entry, relative_path| { - if entry.path().is_dir() { - hasher.as_mut().map(|h| h.update(relative_path.as_bytes())); - builder - .append_dir(relative_path, entry.path()) - .map_err(TmcError::TarWrite)?; - } else if entry.path().is_file() { - if let Some(h) = &mut hasher { - let file = file_util::read_file(entry.path())?; - h.update(&file); - } - builder - .append_path_with_name(entry.path(), relative_path) - .map_err(TmcError::TarWrite)?; - } - Ok(()) - })?; - builder - .into_inner() - .map_err(TmcError::TarWrite)? - .into_inner() - } - Self::Zip => { - let buf = Cursor::new(Vec::new()); - let mut writer = zip::ZipWriter::new(buf); - walk_dir_for_compression(path, |entry, relative_path| { - if entry.path().is_dir() { - hasher.as_mut().map(|h| h.update(relative_path.as_bytes())); - writer.add_directory(relative_path, SimpleFileOptions::default())?; - } else if entry.path().is_file() { - let contents = file_util::read_file(entry.path())?; - hasher.as_mut().map(|h| h.update(&contents)); - writer.start_file(relative_path, SimpleFileOptions::default())?; - writer - .write_all(&contents) - .map_err(|err| TmcError::ZipWrite(path.to_path_buf(), err))?; - } - Ok(()) - })?; - writer.finish()?.into_inner() - } - Self::TarZstd => { - let tar_buf = vec![]; - let mut builder = tar::Builder::new(tar_buf); - walk_dir_for_compression(path, |entry, relative_path| { - if entry.path().is_dir() { - hasher.as_mut().map(|h| h.update(relative_path.as_bytes())); - builder - .append_dir(relative_path, entry.path()) - .map_err(TmcError::TarWrite)?; - } else if entry.path().is_file() { - if let Some(h) = &mut hasher { - let file = file_util::read_file(entry.path())?; - h.update(&file); - } - builder - .append_path_with_name(entry.path(), relative_path) - .map_err(TmcError::TarWrite)?; - } - Ok(()) - })?; - let tar_buf = builder.into_inner().map_err(TmcError::TarWrite)?; - zstd::stream::encode_all(tar_buf.as_slice(), 0).map_err(TmcError::ZstdWrite)? - } - }; - let hash = hasher.map(|h| h.finalize()); - Self::enforce_archive_size_limit(&buf, size_limit_mb)?; - Ok((buf, hash)) - } - - pub fn enforce_archive_size_limit(data: &[u8], size_limit_mb: u32) -> Result<(), TmcError> { - if let Ok(data_len_b) = u64::try_from(data.len()) { - let size_limit_b = u64::from(size_limit_mb) * 1000 * 1000; - if data_len_b <= size_limit_b { - return Ok(()); + let mut builder = + ArchiveBuilder::new(Cursor::new(Vec::new()), self, size_limit_mb, true, hash); + walk_dir_for_compression(path, size_limit_mb, |entry, relative_path| { + if entry.path().is_dir() { + builder.add_directory(entry.path(), relative_path)?; + } else if entry.path().is_file() { + builder.add_file(entry.path(), relative_path)?; } + Ok(()) + })?; + let (cursor, hash) = builder.finish()?; + if u32::try_from(cursor.get_ref().len()).unwrap_or(u32::MAX) > size_limit_mb { + return Err(TmcError::ArchiveSizeLimitExceeded { + limit: size_limit_mb, + }); } - Err(TmcError::ArchiveSizeLimitExceeded { - limit: size_limit_mb, - actual: data.len(), - }) + Ok((cursor.into_inner(), hash)) } } fn walk_dir_for_compression( root: &Path, + size_limit_mb: u32, mut f: impl FnMut(&walkdir::DirEntry, &str) -> Result<(), TmcError>, ) -> Result<(), TmcError> { + let size_limit_b = u64::from(size_limit_mb).saturating_mul(1000 * 1000); + let mut size_total_b = 0; + let parent = root.parent().map(PathBuf::from).unwrap_or_default(); for entry in WalkDir::new(root) .sort_by_file_name() @@ -400,6 +337,13 @@ fn walk_dir_for_compression( .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME) { let entry = entry?; + let metadata = entry.metadata()?; + size_total_b += metadata.len(); + if size_total_b > size_limit_b { + return Err(TmcError::ArchiveSizeLimitExceeded { + limit: size_limit_mb, + }); + } let stripped = entry .path() .strip_prefix(&parent) @@ -435,3 +379,182 @@ impl FromStr for Compression { Ok(format) } } + +pub struct ArchiveBuilder { + size_limit_b: usize, + size_limit_mb: u32, + size_total_b: usize, + hasher: Option, + kind: Kind, +} + +enum Kind { + Tar { + builder: Builder, + }, + TarZstd { + writer: W, + builder: Builder>>, + }, + Zip { + builder: Box>, + deterministic: bool, + }, +} + +impl ArchiveBuilder { + pub fn new( + writer: W, + compression: Compression, + size_limit_mb: u32, + deterministic: bool, + hash: bool, + ) -> Self { + let size_limit_b = usize::try_from(size_limit_mb) + .unwrap_or(usize::MAX) + .saturating_mul(1000 * 1000); + let hasher = if hash { Some(Hasher::new()) } else { None }; + let kind = match compression { + Compression::Tar => { + let mut builder = Builder::new(writer); + if deterministic { + builder.mode(tar::HeaderMode::Deterministic); + } + Kind::Tar { builder } + } + Compression::TarZstd => { + let mut builder = Builder::new(Cursor::new(vec![])); + if deterministic { + builder.mode(tar::HeaderMode::Deterministic); + } + Kind::TarZstd { writer, builder } + } + Compression::Zip => Kind::Zip { + builder: Box::new(ZipWriter::new(writer)), + deterministic, + }, + }; + Self { + size_limit_b, + size_limit_mb, + size_total_b: 0, + hasher, + kind, + } + } + + /// Does not include any files within the directory. + pub fn add_directory(&mut self, source: &Path, path_in_archive: &str) -> Result<(), TmcError> { + log::trace!("adding directory {path_in_archive}"); + self.hash(path_in_archive.as_bytes()); + match &mut self.kind { + Kind::Tar { builder } => { + builder + .append_dir(path_in_archive, source) + .map_err(TmcError::TarWrite)?; + } + Kind::TarZstd { builder, .. } => { + builder + .append_dir(path_in_archive, source) + .map_err(TmcError::TarWrite)?; + } + Kind::Zip { + builder, + deterministic, + } => builder.add_directory(path_in_archive, zip_file_options(*deterministic))?, + } + Ok(()) + } + + pub fn add_file(&mut self, source: &Path, path_in_archive: &str) -> Result<(), TmcError> { + log::trace!("writing file {} as {}", source.display(), path_in_archive); + self.hash(path_in_archive.as_bytes()); + let bytes = file_util::read_file(source)?; + self.size_total_b += bytes.len(); + if self.size_total_b > self.size_limit_b { + return Err(TmcError::ArchiveSizeLimitExceeded { + limit: self.size_limit_mb, + }); + } + self.hash(&bytes); + match &mut self.kind { + Kind::Tar { builder } => builder + .append_path_with_name(source, path_in_archive) + .map_err(TmcError::TarWrite)?, + Kind::TarZstd { builder, .. } => builder + .append_path_with_name(source, path_in_archive) + .map_err(TmcError::TarWrite)?, + Kind::Zip { + builder, + deterministic, + } => { + builder.start_file(path_in_archive, zip_file_options(*deterministic))?; + builder + .write_all(&bytes) + .map_err(|e| TmcError::ZipWrite(source.into(), e))?; + } + } + Ok(()) + } + + pub fn finish(self) -> Result<(W, Option), TmcError> { + let res = match self.kind { + Kind::Tar { builder } => builder.into_inner().map_err(TmcError::TarWrite)?, + Kind::TarZstd { + mut writer, + builder, + } => { + let tar_data = builder.into_inner().map_err(TmcError::TarWrite)?; + zstd::stream::copy_encode(tar_data.get_ref().as_slice(), &mut writer, 0) + .map_err(TmcError::ZstdWrite)?; + writer + } + Kind::Zip { builder, .. } => builder.finish()?, + }; + let hash = self.hasher.map(|h| h.finalize()); + Ok((res, hash)) + } + + fn hash(&mut self, input: &[u8]) { + self.hasher.as_mut().map(|h| h.update(input)); + } +} + +fn zip_file_options(deterministic: bool) -> SimpleFileOptions { + let file_options = SimpleFileOptions::default().unix_permissions(0o755); + if deterministic { + file_options.last_modified_time( + DateTime::from_date_and_time(2023, 1, 1, 0, 0, 0).expect("known to work"), + ) + } else { + file_options + } +} + +#[cfg(test)] +mod test { + use super::*; + use tempfile::NamedTempFile; + + #[test] + fn exceeding_file_limit_causes_error() { + let mut builder = + ArchiveBuilder::new(Cursor::new(Vec::new()), Compression::Tar, 1, true, true); + + // write exactly 1MB, OK + let mut temp = NamedTempFile::new().unwrap(); + temp.write_all("a".as_bytes().repeat(1000 * 1000).as_slice()) + .unwrap(); + builder + .add_file(temp.path(), "file") + .expect("should not be over size limit"); + + // write one byte more, error + let mut temp = NamedTempFile::new().unwrap(); + temp.write_all("a".as_bytes()).unwrap(); + assert!( + builder.add_file(temp.path(), "file").is_err(), + "should be over size limit" + ); + } +} diff --git a/crates/tmc-langs-framework/src/error.rs b/crates/tmc-langs-framework/src/error.rs index 960afc02b0f..6d6a1035857 100644 --- a/crates/tmc-langs-framework/src/error.rs +++ b/crates/tmc-langs-framework/src/error.rs @@ -13,6 +13,9 @@ pub enum TmcError { #[error("Failed to seek archive")] Seek(#[source] std::io::Error), + #[error("Failed to hash file")] + Hash(#[source] std::io::Error), + #[error("Failed to read zip file")] ZipRead(#[source] std::io::Error), #[error("Failed to read file inside zip archive with path {0}")] @@ -29,16 +32,16 @@ pub enum TmcError { ZstdRead(#[source] std::io::Error), #[error("Failed to write zstd archive")] ZstdWrite(#[source] std::io::Error), - #[error( - "Archive size limit exceeded when compressing proejct (limit: {limit} MB, archive: {actual} MB)" - )] - ArchiveSizeLimitExceeded { limit: u32, actual: usize }, + #[error("Archive size limit exceeded when compressing project (limit: {limit} MB)")] + ArchiveSizeLimitExceeded { limit: u32 }, #[error("Path {0} is not valid UTF-8")] InvalidUtf8(PathBuf), #[error("Failed to read line")] ReadLine(#[source] std::io::Error), + #[error("Failed to get file metadata")] + FileMetadata(#[source] std::io::Error), #[error("Failed to canonicalize path {0}")] Canonicalize(PathBuf, #[source] std::io::Error), #[error("File {0} not in given project root {1}")] diff --git a/crates/tmc-langs-framework/src/lib.rs b/crates/tmc-langs-framework/src/lib.rs index ab7e2bfe6c3..c74c287c9d7 100644 --- a/crates/tmc-langs-framework/src/lib.rs +++ b/crates/tmc-langs-framework/src/lib.rs @@ -12,7 +12,7 @@ mod policy; mod tmc_project_yml; pub use self::{ - archive::{Archive, Compression}, + archive::{Archive, ArchiveBuilder, Compression}, command::{ExitStatus, Output, TmcCommand}, domain::{ ExerciseDesc, ExercisePackagingConfiguration, RunResult, RunStatus, StyleValidationError, diff --git a/crates/tmc-langs-framework/src/tmc_project_yml.rs b/crates/tmc-langs-framework/src/tmc_project_yml.rs index 9a020d9a8da..f3ceccbf811 100644 --- a/crates/tmc-langs-framework/src/tmc_project_yml.rs +++ b/crates/tmc-langs-framework/src/tmc_project_yml.rs @@ -14,7 +14,7 @@ use tmc_langs_util::{ file_util::{self, Lock, LockOptions}, }; -const DEFAULT_SUBMISSION_SIZE_LIMIT_MB: u32 = 500; +const DEFAULT_SUBMISSION_SIZE_LIMIT_MB: u32 = 1; /// Extra data from a `.tmcproject.yml` file. #[derive(Debug, Serialize, Deserialize, Default, Clone)] @@ -315,24 +315,6 @@ mod test { assert_eq!(no_tests.points, &["1", "notests"]); } - #[test] - fn deserialize_submission_size_limit_mb() { - init(); - - let submission_size_limit_mb = r#"submission_size_limit_mb: 100 -"#; - - let cfg: TmcProjectYml = deserialize::yaml_from_str(submission_size_limit_mb).unwrap(); - let submission_size_limit_mb = cfg.submission_size_limit_mb.unwrap(); - assert_eq!(submission_size_limit_mb, 100); - let submission_size_limit_mb = cfg.get_submission_size_limit_mb(); - assert_eq!(submission_size_limit_mb, 100); - - let cfg: TmcProjectYml = deserialize::yaml_from_str("").unwrap(); - let submission_size_limit_mb = cfg.get_submission_size_limit_mb(); - assert_eq!(submission_size_limit_mb, DEFAULT_SUBMISSION_SIZE_LIMIT_MB); - } - #[test] fn deserialize_python_ver() { init(); diff --git a/crates/tmc-langs-plugins/src/archive.rs b/crates/tmc-langs-plugins/src/archive.rs deleted file mode 100644 index c3c6648e74a..00000000000 --- a/crates/tmc-langs-plugins/src/archive.rs +++ /dev/null @@ -1,122 +0,0 @@ -//! Contains types that abstract over the various archive formats. - -use std::{ - io::{Cursor, Seek, Write}, - path::Path, -}; -use tar::Builder; -use tmc_langs_framework::{Compression, TmcError}; -use tmc_langs_util::file_util; -use zip::{DateTime, ZipWriter, write::SimpleFileOptions}; - -pub enum ArchiveBuilder { - Tar { - builder: Builder, - }, - TarZstd { - writer: W, - builder: Builder>>, - }, - Zip { - builder: Box>, - deterministic: bool, - }, -} - -impl ArchiveBuilder { - pub fn new(writer: W, compression: Compression, deterministic: bool) -> Self { - match compression { - Compression::Tar => { - let mut builder = Builder::new(writer); - if deterministic { - builder.mode(tar::HeaderMode::Deterministic); - } - Self::Tar { builder } - } - Compression::TarZstd => { - let mut builder = Builder::new(Cursor::new(vec![])); - if deterministic { - builder.mode(tar::HeaderMode::Deterministic); - } - Self::TarZstd { writer, builder } - } - Compression::Zip => Self::Zip { - builder: Box::new(ZipWriter::new(writer)), - deterministic, - }, - } - } - - /// Does not include any files within the directory. - pub fn add_directory(&mut self, source: &Path, path_in_archive: &str) -> Result<(), TmcError> { - log::trace!("adding directory {path_in_archive}"); - match self { - Self::Tar { builder } => { - builder - .append_dir(path_in_archive, source) - .map_err(TmcError::TarWrite)?; - } - Self::TarZstd { builder, .. } => { - builder - .append_dir(path_in_archive, source) - .map_err(TmcError::TarWrite)?; - } - Self::Zip { - builder, - deterministic, - } => builder.add_directory(path_in_archive, zip_file_options(*deterministic))?, - } - Ok(()) - } - - pub fn add_file(&mut self, source: &Path, path_in_archive: &str) -> Result<(), TmcError> { - log::trace!("writing file {} as {}", source.display(), path_in_archive); - match self { - Self::Tar { builder } => builder - .append_path_with_name(source, path_in_archive) - .map_err(TmcError::TarWrite)?, - Self::TarZstd { builder, .. } => builder - .append_path_with_name(source, path_in_archive) - .map_err(TmcError::TarWrite)?, - Self::Zip { - builder, - deterministic, - } => { - let bytes = file_util::read_file(source)?; - builder.start_file(path_in_archive, zip_file_options(*deterministic))?; - builder - .write_all(&bytes) - .map_err(|e| TmcError::ZipWrite(source.into(), e))?; - } - } - Ok(()) - } - - pub fn finish(self) -> Result { - let res = match self { - Self::Tar { builder } => builder.into_inner().map_err(TmcError::TarWrite)?, - Self::TarZstd { - mut writer, - builder, - } => { - let tar_data = builder.into_inner().map_err(TmcError::TarWrite)?; - zstd::stream::copy_encode(tar_data.get_ref().as_slice(), &mut writer, 0) - .map_err(TmcError::ZstdWrite)?; - writer - } - Self::Zip { builder, .. } => builder.finish()?, - }; - Ok(res) - } -} - -fn zip_file_options(deterministic: bool) -> SimpleFileOptions { - let file_options = SimpleFileOptions::default().unix_permissions(0o755); - if deterministic { - file_options.last_modified_time( - DateTime::from_date_and_time(2023, 1, 1, 0, 0, 0).expect("known to work"), - ) - } else { - file_options - } -} diff --git a/crates/tmc-langs-plugins/src/compression.rs b/crates/tmc-langs-plugins/src/compression.rs index 2f0eeb81498..5af566606ce 100644 --- a/crates/tmc-langs-plugins/src/compression.rs +++ b/crates/tmc-langs-plugins/src/compression.rs @@ -1,12 +1,12 @@ //! Contains functions for compressing and uncompressing projects. -use crate::archive::ArchiveBuilder; -use blake3::{Hash, Hasher}; +use blake3::Hash; use std::{ io::{Cursor, Read, Seek}, path::{Path, PathBuf}, + u32, }; -use tmc_langs_framework::{Compression, StudentFilePolicy, TmcError}; +use tmc_langs_framework::{ArchiveBuilder, Compression, StudentFilePolicy, TmcError}; use tmc_langs_util::file_util; use walkdir::{DirEntry, WalkDir}; use zip::ZipArchive; @@ -21,8 +21,17 @@ pub fn compress_student_files( hash: bool, size_limit_mb: u32, ) -> Result<(Vec, Option), TmcError> { - let mut hasher = if hash { Some(Hasher::new()) } else { None }; - let mut writer = ArchiveBuilder::new(Cursor::new(vec![]), compression, deterministic); + let mut writer = ArchiveBuilder::new( + Cursor::new(vec![]), + compression, + size_limit_mb, + deterministic, + hash, + ); + let size_limit_b = usize::try_from(size_limit_mb) + .unwrap_or(usize::MAX) // saturating from... + .saturating_mul(1000 * 1000); + let mut total_size_b = 0; for entry in WalkDir::new(root_directory) .sort_by(|a, b| a.path().cmp(b.path())) @@ -34,7 +43,11 @@ pub fn compress_student_files( .path() .strip_prefix(root_directory) .expect("all entries are inside root"); - log::trace!("processing {}", entry.path().display()); + log::trace!( + "processing {} ({})", + entry.path().display(), + relative.display() + ); if policy.is_student_file(relative) { let path = root_directory .parent() @@ -47,21 +60,30 @@ pub fn compress_student_files( .unwrap_or_else(|| entry.path()); if entry.path().is_dir() { let path_in_archive = path_to_zip_compatible_string(path); - hasher - .as_mut() - .map(|h| h.update(path_in_archive.as_bytes())); writer.add_directory(entry.path(), &path_in_archive)?; } else { let contents = file_util::read_file(entry.path())?; - hasher.as_mut().map(|h| h.update(&contents)); - writer.add_file(entry.path(), &path_to_zip_compatible_string(path))?; + total_size_b += contents.len(); + if total_size_b > size_limit_b { + return Err(TmcError::ArchiveSizeLimitExceeded { + limit: size_limit_mb, + }); + } + let path_in_archive = path_to_zip_compatible_string(path); + writer.add_file(entry.path(), &path_in_archive)?; } } } - let hash = hasher.map(|h| h.finalize()); - let cursor = writer.finish()?; + let (cursor, hash) = writer.finish()?; + let size_limit_b = usize::try_from(size_limit_mb) + .unwrap_or(usize::MAX) + .saturating_mul(1000 * 1000); + if cursor.get_ref().len() > size_limit_b { + return Err(TmcError::ArchiveSizeLimitExceeded { + limit: size_limit_mb, + }); + } let data = cursor.into_inner(); - Compression::enforce_archive_size_limit(&data, size_limit_mb)?; Ok((data, hash)) } diff --git a/crates/tmc-langs-plugins/src/lib.rs b/crates/tmc-langs-plugins/src/lib.rs index 118bde4ec4c..2de32092887 100644 --- a/crates/tmc-langs-plugins/src/lib.rs +++ b/crates/tmc-langs-plugins/src/lib.rs @@ -2,7 +2,6 @@ //! Abstracts over the various language plugins. -pub mod archive; pub mod compression; mod error; diff --git a/crates/tmc-langs/Cargo.toml b/crates/tmc-langs/Cargo.toml index ae040fee3ce..a1dd8b0e248 100644 --- a/crates/tmc-langs/Cargo.toml +++ b/crates/tmc-langs/Cargo.toml @@ -49,7 +49,9 @@ nix = { version = "0.30.0", features = ["fs"] } [dev-dependencies] chrono = "0.4.19" +env_logger = "0.11.2" mockito = "1.0.2" +rpassword = "7.0.0" simple_logger = "5.0.0" tempfile = "3.3.0" diff --git a/crates/tmc-langs/src/lib.rs b/crates/tmc-langs/src/lib.rs index e6d0ad96bf3..ae722e31a52 100644 --- a/crates/tmc-langs/src/lib.rs +++ b/crates/tmc-langs/src/lib.rs @@ -570,12 +570,11 @@ pub fn login_with_token(token: String) -> tmc::Token { /// Reads the password from stdin. pub fn login_with_password( client: &mut tmc::TestMyCodeClient, - client_name: &str, email: String, password: String, ) -> Result { log::debug!("logging in with password"); - let token = client.authenticate(client_name, email, password)?; + let token = client.authenticate(email, password)?; Ok(token) } @@ -966,8 +965,7 @@ pub fn extract_project( plugin.extract_project(&mut archive, target_location, clean)?; } else { log::debug!( - "no matching language plugin found for {}, extracting naively", - target_location.display() + "no matching language plugin found for compressed project, extracting naively", ); let compressed_project = archive.into_inner(); extract_project_overwrite(compressed_project, target_location, compression)?; diff --git a/crates/tmc-langs/tests/integration.rs b/crates/tmc-langs/tests/integration.rs new file mode 100644 index 00000000000..2c57a44a7cd --- /dev/null +++ b/crates/tmc-langs/tests/integration.rs @@ -0,0 +1,234 @@ +use std::{ + fs::{File, OpenOptions}, + io::Seek, + path::Path, +}; +use tempfile::{NamedTempFile, TempDir}; +use tmc_langs::{Compression, LangsError, RunStatus, file_util}; +use tmc_testmycode_client::TestMyCodeClient; + +#[test] +#[ignore = "\ +Does a lot of work and requires admin credentials to access the solutions.\ +Should only be ran to verify bigger changes that may break some exercises.\ +It's recommended to save the log with something like cargo test | tee. +"] +fn test_policies_on_course_exercises() { + env_logger::init(); + let courses = &[ + 600, // mooc-java-programming-i + 625, // mooc-java-programming-ii + 1040, // mooc-working-with-text + 1489, // mooc-ohjelmointi-2025 + 1601, // centria-csharp-basics-IT00AJ56-3008 + ]; + let mut client = TestMyCodeClient::new( + "https://tmc.mooc.fi".parse().unwrap(), + "vscode_plugin".to_string(), + "3.4.0".to_string(), + ) + .unwrap(); + + let email = "daniel.x.martinez@helsinki.fi".to_string(); + let pass = rpassword::prompt_password("password").unwrap(); + client.authenticate(email, pass).unwrap(); + + let root = Path::new("../../test-cache"); + let solutions_dir = root.join("solutions"); + let templates_dir = root.join("templates"); + let submissions_dir = root.join("submissions"); + std::fs::create_dir_all(&solutions_dir).unwrap(); + std::fs::create_dir_all(&templates_dir).unwrap(); + + let mut failure = false; + for course in courses.iter().copied() { + println!("course {course}"); + let exercises = client.get_course_exercises(course).unwrap(); + for exercise in exercises { + println!("course {course} exercise {}", exercise.id); + // download template + let template_path = templates_dir.join(exercise.id.to_string()); + let template_zip_path = template_path.with_extension("zip"); + let template_zip = match File::open(&template_zip_path) { + Ok(file) => file, + _ => { + let mut template_file = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(&template_zip_path) + .unwrap(); + client + .download_exercise(exercise.id, &mut template_file) + .unwrap(); + template_file.seek(std::io::SeekFrom::Start(0)).unwrap(); + template_file + } + }; + // extract template + if !template_path.exists() { + if let Err(err) = extract_to_new_dir(&template_zip, &template_path) { + let _ = std::fs::remove_dir(&template_path); + failure = true; + println!("error: while extracting template {err:#?}"); + continue; + } + } + // run tests on template as sanity check + match tmc_langs::run_tests(&template_path) { + Ok(test_results) => { + if matches!(test_results.status, RunStatus::Passed) { + println!( + "warning: course {course} exercise {} ({}) tests succeeded on the template", + exercise.name, exercise.id + ); + } else { + println!( + "OK! course {course} exercise {} ({}) tests failed on the template", + exercise.name, exercise.id + ) + } + } + Err(err) => { + failure = true; + println!("error: failed to run tests on example solution, {err:#?}") + } + } + + // download solution + let solution_path = solutions_dir.join(exercise.id.to_string()); + let solution_zip_path = solution_path.with_extension("zip"); + let solution_zip = match File::open(&solution_zip_path) { + Ok(file) => file, + _ => { + let mut solution_zip = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .open(&solution_zip_path) + .unwrap(); + client + .download_model_solution_archive(exercise.id, &mut solution_zip) + .unwrap(); + solution_zip.seek(std::io::SeekFrom::Start(0)).unwrap(); + solution_zip + } + }; + // extract solution + if !solution_path.exists() { + if let Err(err) = extract_to_new_dir(&solution_zip, &solution_path) { + let _ = std::fs::remove_dir(&solution_path); + failure = true; + println!("error: while extracting solution {err:#?}"); + continue; + } + } + // run tests as sanity check + match tmc_langs::run_tests(&solution_path) { + Ok(test_results) => { + if !matches!(test_results.status, RunStatus::Passed) { + println!( + "warning: course {course} exercise {} ({}) tests failed on the example solution", + exercise.name, exercise.id + ); + } else { + println!( + "OK! course {course} exercise {} ({}) tests passed on the example solution", + exercise.name, exercise.id + ); + } + } + Err(err) => { + failure = true; + println!("error: failed to run tests on example solution, {err:#?}") + } + } + + // package solution as submission and extract over template + let submission_dir = submissions_dir.join(exercise.id.to_string()); + let submission_path = submissions_dir.join(exercise.id.to_string()); + let submission_zip_path = submission_path.with_extension("zip"); + if !submission_zip_path.exists() { + solution_into_submission(&template_path, &solution_zip, &submission_zip_path); + }; + // extract submission + if !submission_path.exists() { + let submission_zip = File::open(&submission_zip_path).unwrap(); + if let Err(err) = extract_to_new_dir(&submission_zip, &submission_path) { + let _ = std::fs::remove_dir(&submission_path); + failure = true; + println!("error: while extracting solution {err:#?}"); + continue; + } + } + // run tests, should pass + match tmc_langs::run_tests(&submission_dir) { + Ok(test_results) => { + if !matches!(test_results.status, RunStatus::Passed) { + failure = true; + println!( + "error: course {course} exercise {} ({}) tests failed on solution extracted over template", + exercise.name, exercise.id + ); + } else { + println!( + "OK! course {course} exercise {} ({}) tests passed on submission", + exercise.name, exercise.id + ); + } + } + Err(err) => { + failure = true; + println!("error: failed to run tests on submission, {err:#?}") + } + } + } + } + if failure { + panic!("one or more issues found, check logs for more info"); + } +} + +fn solution_into_submission(template: &Path, solution_zip: &File, target: &Path) { + // extract solution + let solution = TempDir::new().unwrap(); + tmc_langs::extract_project( + solution_zip, + solution.path(), + Compression::Zip, + false, + false, + ) + .unwrap(); + // package solution as a student submission + let submission_zip = NamedTempFile::new().unwrap(); + tmc_langs::compress_project_to( + solution.path(), + submission_zip.path(), + Compression::Zip, + true, + false, + ) + .unwrap(); + + tmc_langs::prepare_submission( + tmc_langs::PrepareSubmission { + archive: submission_zip.path(), + compression: Compression::Zip, + extract_naively: false, + }, + target, + true, + tmc_langs::TmcParams::default(), + template, + None, + Compression::Zip, + ) + .unwrap(); +} + +fn extract_to_new_dir(zip: &File, path: &Path) -> Result<(), LangsError> { + file_util::create_dir(path)?; + tmc_langs::extract_project(zip, path, Compression::Zip, false, false)?; + Ok(()) +} diff --git a/crates/tmc-testmycode-client/src/client.rs b/crates/tmc-testmycode-client/src/client.rs index b7d0af3158e..9e89a93c5ca 100644 --- a/crates/tmc-testmycode-client/src/client.rs +++ b/crates/tmc-testmycode-client/src/client.rs @@ -130,11 +130,10 @@ impl TestMyCodeClient { /// use tmc_testmycode_client::TestMyCodeClient; /// /// let mut client = TestMyCodeClient::new("https://tmc.mooc.fi".parse().unwrap(), "some_client".to_string(), "some_version".to_string()).unwrap(); - /// client.authenticate("client", "user".to_string(), "pass".to_string()).unwrap(); + /// client.authenticate("user".to_string(), "pass".to_string()).unwrap(); /// ``` pub fn authenticate( &mut self, - client_name: &str, email: String, password: String, ) -> TestMyCodeClientResult { @@ -146,7 +145,7 @@ impl TestMyCodeClient { TestMyCodeClientError::UrlParse(self.0.root_url.to_string() + "/oauth/token", e) })?; - let credentials = api_v8::get_credentials(self, client_name)?; + let credentials = api_v8::get_credentials(self)?; log::debug!("authenticating at {auth_url}"); let client = BasicClient::new(ClientId::new(credentials.application_id)) @@ -254,7 +253,9 @@ impl TestMyCodeClient { /// 123, /// Path::new("./exercises/python/123"), /// Some("my python solution".to_string()), - /// Some(Language::Eng)).unwrap(); + /// Some(Language::Eng), + /// 1, + /// ).unwrap(); /// ``` pub fn paste( &self, @@ -350,6 +351,16 @@ impl TestMyCodeClient { Ok(()) } + pub fn download_model_solution_archive( + &self, + exercise_id: u32, + target: &mut dyn Write, + ) -> TestMyCodeClientResult<()> { + self.require_authentication()?; + api_v8::core::download_exercise_solution(self, exercise_id, target)?; + Ok(()) + } + /// Fetches the course's information. Requires authentication. /// /// # Errors diff --git a/crates/tmc-testmycode-client/src/client/api_v8.rs b/crates/tmc-testmycode-client/src/client/api_v8.rs index 2be0e8d71ed..0ccec6066d2 100644 --- a/crates/tmc-testmycode-client/src/client/api_v8.rs +++ b/crates/tmc-testmycode-client/src/client/api_v8.rs @@ -163,10 +163,8 @@ pub fn post_form( /// get /api/v8/application/{client_name}/credentials /// Fetches oauth2 credentials info. -pub fn get_credentials( - client: &TestMyCodeClient, - client_name: &str, -) -> Result { +pub fn get_credentials(client: &TestMyCodeClient) -> Result { + let client_name = &client.0.client_name; let url = make_url( client, format!("/api/v8/application/{client_name}/credentials"), @@ -1072,7 +1070,7 @@ mod test { "#, ); - let _res = get_credentials(&client, "client").unwrap(); + let _res = get_credentials(&client).unwrap(); } #[test] diff --git a/crates/tmc-testmycode-client/src/lib.rs b/crates/tmc-testmycode-client/src/lib.rs index 7c0d1591715..b7ac63cbe8f 100644 --- a/crates/tmc-testmycode-client/src/lib.rs +++ b/crates/tmc-testmycode-client/src/lib.rs @@ -6,7 +6,7 @@ //! use tmc_testmycode_client::TestMyCodeClient; //! //! let mut client = TestMyCodeClient::new("https://tmc.mooc.fi".parse().unwrap(), "some_client".to_string(), "some_version".to_string()).unwrap(); -//! client.authenticate("client_name", "email".to_string(), "password".to_string()); +//! client.authenticate("email".to_string(), "password".to_string()); //! let organizations = client.get_organizations(); //! ``` //! diff --git a/crates/tmc-testmycode-client/tests/api_integration.rs b/crates/tmc-testmycode-client/tests/api_integration.rs index 73541743247..d55e69d141a 100644 --- a/crates/tmc-testmycode-client/tests/api_integration.rs +++ b/crates/tmc-testmycode-client/tests/api_integration.rs @@ -25,9 +25,7 @@ fn init_client() -> TestMyCodeClient { "1.0.0".to_string(), ) .unwrap(); - client - .authenticate("vscode_plugin", email, password) - .unwrap(); + client.authenticate(email, password).unwrap(); client } diff --git a/docker/Dockerfile b/docker/Dockerfile index 2fda62c4a71..c944e1a7857 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,7 +14,7 @@ RUN apt update \ # languages npm \ python3 \ - openjdk-17-jdk ant maven \ + openjdk-21-jdk ant maven \ dotnet-sdk-9.0 \ check valgrind \ r-base r-cran-devtools locales \ diff --git a/docs/tmcproject.md b/docs/tmcproject.md index 5a90b1f4c21..107e30c82a6 100644 --- a/docs/tmcproject.md +++ b/docs/tmcproject.md @@ -17,7 +17,7 @@ All of the keys listed below are optional. | fail_on_valgrind_error | Boolean | If set, the C plugin will attempt to run valgrind and fail the exercise if it discovers errors. | | minimum_python_version | Python version string | Must be "{major}.{minor}.{patch}", "{major}.{minor}" or "{major}". If set, the Python plugin will warn the user if the Python version being used is below the given minimum version. | | sandbox_image | The Docker image that should be used at the sandbox. | Should be the Docker registry path of the image. | -| submission_size_limit_mb | Number in megabytes (MB) | If set, overrides the default submission archive size limit of 500 MB. | +| submission_size_limit_mb | Number in megabytes (MB) | If set, overrides the default submission archive size limit of 1 MB (before packaging). | ## Example file contents @@ -38,7 +38,7 @@ no-tests: fail_on_valgrind_error: false minimum_python_version: "3.8" sandbox_image: "eu.gcr.io/moocfi-public/best-image" -submission_size_limit_mb: 500 +submission_size_limit_mb: 1 ``` ## Default student and exercise files diff --git a/docker.sh b/scripts/docker.sh similarity index 67% rename from docker.sh rename to scripts/docker.sh index 48d475bb059..46157d69196 100755 --- a/docker.sh +++ b/scripts/docker.sh @@ -1,5 +1,9 @@ #!/bin/sh +# Runs the command given as arguments in a docker container, e.g. +# sh scripts/docker.sh cargo test +# The special argument `interactive` starts an interactive shell session inside the container + CMD="${*:-cargo build && cp /build/target/debug/tmc-langs-cli /build/out/}" export DOCKER_BUILDKIT=1 docker build . -f docker/Dockerfile -t tmc-langs-rust diff --git a/scripts/tmc-batch-test.bash b/scripts/tmc-batch-test.bash new file mode 100644 index 00000000000..af641932e18 --- /dev/null +++ b/scripts/tmc-batch-test.bash @@ -0,0 +1,9 @@ +#!/bin/bash + +# Note: Asks for your tmc password in order to access all exercises and solutions +# Runs an integration test that downloads all the exercises (cached in ./test-cache) for various courses and verifies that +# 1. The tests fail on the template (some exercises are intended to pass on the template, so failing this is just a warning) +# 2. The tests pass on the example solution +# 3. The tests pass when the example solution is packaged like a submission and extracted over the template + +cargo test test_policies_on_course_exercises -- --ignored --nocapture | tee test-cache/out.log From b2957d822de5b467843c3c8f81f2ac706df4178e Mon Sep 17 00:00:00 2001 From: Heliozoa Date: Tue, 7 Oct 2025 14:18:25 +0300 Subject: [PATCH 2/3] Test fixes --- crates/plugins/csharp/src/policy.rs | 8 +-- crates/plugins/java/src/ant_policy.rs | 2 +- crates/plugins/java/src/maven_plugin.rs | 11 ++-- crates/plugins/java/src/maven_policy.rs | 5 +- crates/plugins/make/src/policy.rs | 9 ++- .../valgrind-failing-exercise/src/source.c | 1 + crates/plugins/r/src/policy.rs | 2 +- ...t_tar@make__valgrind-failing-exercise.snap | 2 +- ...t_zip@make__valgrind-failing-exercise.snap | 2 +- ..._zstd@make__valgrind-failing-exercise.snap | 2 +- crates/tmc-langs-framework/src/archive.rs | 61 ++++++++----------- crates/tmc-langs-plugins/src/compression.rs | 2 +- crates/tmc-langs-plugins/src/lib.rs | 2 +- .../valgrind-failing-exercise/src/source.c | 1 + 14 files changed, 50 insertions(+), 60 deletions(-) diff --git a/crates/plugins/csharp/src/policy.rs b/crates/plugins/csharp/src/policy.rs index bb460618761..067b751975b 100644 --- a/crates/plugins/csharp/src/policy.rs +++ b/crates/plugins/csharp/src/policy.rs @@ -33,14 +33,14 @@ mod test { fn file_in_binary_dir_is_not_student_file() { let policy = CSharpStudentFilePolicy::new(Path::new(".")).unwrap(); assert!(!policy.is_student_file(Path::new("src/bin/any/file"))); - assert!(!policy.is_student_file(Path::new("obj/any/src/file"))); + assert!(!policy.is_student_file(Path::new("obj/any/src/file.cs"))); } #[test] - fn file_in_src_is_student_file() { + fn cs_file_in_src_is_student_file() { let policy = CSharpStudentFilePolicy::new(Path::new(".")).unwrap(); - assert!(policy.is_student_file(Path::new("src/file"))); - assert!(policy.is_student_file(Path::new("src/any/file"))); + assert!(policy.is_student_file(Path::new("src/file.cs"))); + assert!(policy.is_student_file(Path::new("src/any/file.cs"))); } #[test] diff --git a/crates/plugins/java/src/ant_policy.rs b/crates/plugins/java/src/ant_policy.rs index a0d3d461dbd..e12d5b5101d 100644 --- a/crates/plugins/java/src/ant_policy.rs +++ b/crates/plugins/java/src/ant_policy.rs @@ -38,7 +38,7 @@ mod test { #[test] fn is_not_student_source_file() { let policy = AntStudentFilePolicy::new(Path::new(".")).unwrap(); - assert!(policy.is_student_file(Path::new("src/file"))); + assert!(!policy.is_student_file(Path::new("src/file"))); assert!(!policy.is_student_file(Path::new("file"))); assert!(!policy.is_student_file(Path::new("dir/src/file"))); assert!(!policy.is_student_file(Path::new("srca/file"))); diff --git a/crates/plugins/java/src/maven_plugin.rs b/crates/plugins/java/src/maven_plugin.rs index 0bb01f56bdd..89487914bcc 100644 --- a/crates/plugins/java/src/maven_plugin.rs +++ b/crates/plugins/java/src/maven_plugin.rs @@ -17,7 +17,7 @@ use tmc_langs_framework::{ Archive, ExerciseDesc, Language, LanguagePlugin, RunResult, StyleValidationResult, TmcCommand, TmcError, nom::IResult, nom_language::error::VerboseError, }; -use tmc_langs_util::{file_util, path_util}; +use tmc_langs_util::file_util; const MVN_ARCHIVE: &[u8] = include_bytes!("../deps/apache-maven-3.8.1-bin.tar.gz"); const MVN_PATH_IN_ARCHIVE: &str = "apache-maven-3.8.1"; // the name of the base directory in the maven archive @@ -137,12 +137,9 @@ impl LanguagePlugin for MavenPlugin { let next = iter.with_next(|file| { let file_path = file.path()?; - if file.is_file() && file_path.extension() == Some(OsStr::new("java")) { - // check if java file has src as ancestor - for ancestor in file_path.ancestors() { - if let Some(src_parent) = path_util::get_parent_of_named(ancestor, "src") { - return Ok(Break(Some(src_parent))); - } + if file.is_file() && file_path.file_name() == Some(OsStr::new("pom.xml")) { + if let Some(pom_parent) = file_path.parent() { + return Ok(Break(Some(pom_parent.to_path_buf()))); } } Ok(Continue(())) diff --git a/crates/plugins/java/src/maven_policy.rs b/crates/plugins/java/src/maven_policy.rs index 1a665f6f37c..09f1ef4c592 100644 --- a/crates/plugins/java/src/maven_policy.rs +++ b/crates/plugins/java/src/maven_policy.rs @@ -20,6 +20,7 @@ impl StudentFilePolicy for MavenStudentFilePolicy { } fn is_non_extra_student_file(&self, path: &Path) -> bool { + // technically pom.xml would need to be included to differentiate between maven and ant projects path.starts_with("src/main") && path.extension() == Some(OsStr::new("java")) } } @@ -31,8 +32,8 @@ mod test { #[test] fn is_student_file() { let policy = MavenStudentFilePolicy::new(Path::new(".")).unwrap(); - assert!(policy.is_student_file(Path::new("src/main/file"))); - assert!(policy.is_student_file(Path::new("src/main/dir/file"))); + assert!(policy.is_student_file(Path::new("src/main/file.java"))); + assert!(policy.is_student_file(Path::new("src/main/dir/file.java"))); } #[test] diff --git a/crates/plugins/make/src/policy.rs b/crates/plugins/make/src/policy.rs index c03c4b1f606..2c82d74cdf0 100644 --- a/crates/plugins/make/src/policy.rs +++ b/crates/plugins/make/src/policy.rs @@ -32,14 +32,17 @@ mod test { #[test] fn is_student_file() { let policy = MakeStudentFilePolicy::new(Path::new(".")).unwrap(); - assert!(policy.is_student_file(Path::new("src"))); - assert!(policy.is_student_file(Path::new("src/file"))); - assert!(policy.is_student_file(Path::new("src/dir/file"))); + assert!(policy.is_student_file(Path::new("src/file.c"))); + assert!(policy.is_student_file(Path::new("src/file.h"))); + assert!(policy.is_student_file(Path::new("src/dir/file.c"))); + assert!(policy.is_student_file(Path::new("src/dir/file.h"))); } #[test] fn is_not_student_source_file() { let policy = MakeStudentFilePolicy::new(Path::new(".")).unwrap(); + assert!(!policy.is_student_file(Path::new("a.c"))); + assert!(!policy.is_student_file(Path::new("a.h"))); assert!(!policy.is_student_file(Path::new("srcc"))); assert!(!policy.is_student_file(Path::new("dir/src/file"))); } diff --git a/crates/plugins/make/tests/data/valgrind-failing-exercise/src/source.c b/crates/plugins/make/tests/data/valgrind-failing-exercise/src/source.c index 14ca54234ad..79d01b2bd72 100644 --- a/crates/plugins/make/tests/data/valgrind-failing-exercise/src/source.c +++ b/crates/plugins/make/tests/data/valgrind-failing-exercise/src/source.c @@ -1,5 +1,6 @@ #include #include "source.h" +#include int one(void) { diff --git a/crates/plugins/r/src/policy.rs b/crates/plugins/r/src/policy.rs index 5bddef4e070..8888081f890 100644 --- a/crates/plugins/r/src/policy.rs +++ b/crates/plugins/r/src/policy.rs @@ -39,7 +39,6 @@ mod test { init(); let policy = RStudentFilePolicy::new(Path::new(".")).unwrap(); - assert!(policy.is_student_file(Path::new("R"))); assert!(policy.is_student_file(Path::new("R/file.R"))); } @@ -48,6 +47,7 @@ mod test { init(); let policy = RStudentFilePolicy::new(Path::new(".")).unwrap(); + assert!(!policy.is_student_file(Path::new("a.R"))); assert!(!policy.is_student_file(Path::new("dir/R"))); assert!(!policy.is_student_file(Path::new("dir/R/file"))); } diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap index 8a51821d93d..6cb1b8a671c 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_tar@make__valgrind-failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: f22f8d2f4502dc8c14e98c9b89f8413b92b219e9ad1aaf6dfc429d1cc177745d + output-data: fb42a43f95667107fd4b696ceadd19cda6595daa2c3afa825f53984fab4baa02 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap index 8a51821d93d..6cb1b8a671c 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zip@make__valgrind-failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: f22f8d2f4502dc8c14e98c9b89f8413b92b219e9ad1aaf6dfc429d1cc177745d + output-data: fb42a43f95667107fd4b696ceadd19cda6595daa2c3afa825f53984fab4baa02 diff --git a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap index 8a51821d93d..6cb1b8a671c 100644 --- a/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap +++ b/crates/tmc-langs-cli/tests/snapshots/integration__compress_project_zstd@make__valgrind-failing-exercise.snap @@ -9,4 +9,4 @@ message: compressed project from [PATH] to [PATH] result: executed-command data: output-data-kind: compressed-project-hash - output-data: f22f8d2f4502dc8c14e98c9b89f8413b92b219e9ad1aaf6dfc429d1cc177745d + output-data: fb42a43f95667107fd4b696ceadd19cda6595daa2c3afa825f53984fab4baa02 diff --git a/crates/tmc-langs-framework/src/archive.rs b/crates/tmc-langs-framework/src/archive.rs index 0f67889ec96..f14c69f1d39 100644 --- a/crates/tmc-langs-framework/src/archive.rs +++ b/crates/tmc-langs-framework/src/archive.rs @@ -295,15 +295,9 @@ pub enum Compression { } impl Compression { - pub fn compress( - self, - path: &Path, - hash: bool, - size_limit_mb: u32, - ) -> Result<(Vec, Option), TmcError> { - let mut builder = - ArchiveBuilder::new(Cursor::new(Vec::new()), self, size_limit_mb, true, hash); - walk_dir_for_compression(path, size_limit_mb, |entry, relative_path| { + pub fn compress(self, path: &Path, hash: bool) -> Result<(Vec, Option), TmcError> { + let mut builder = ArchiveBuilder::new(Cursor::new(Vec::new()), self, None, true, hash); + walk_dir_for_compression(path, |entry, relative_path| { if entry.path().is_dir() { builder.add_directory(entry.path(), relative_path)?; } else if entry.path().is_file() { @@ -312,23 +306,14 @@ impl Compression { Ok(()) })?; let (cursor, hash) = builder.finish()?; - if u32::try_from(cursor.get_ref().len()).unwrap_or(u32::MAX) > size_limit_mb { - return Err(TmcError::ArchiveSizeLimitExceeded { - limit: size_limit_mb, - }); - } Ok((cursor.into_inner(), hash)) } } fn walk_dir_for_compression( root: &Path, - size_limit_mb: u32, mut f: impl FnMut(&walkdir::DirEntry, &str) -> Result<(), TmcError>, ) -> Result<(), TmcError> { - let size_limit_b = u64::from(size_limit_mb).saturating_mul(1000 * 1000); - let mut size_total_b = 0; - let parent = root.parent().map(PathBuf::from).unwrap_or_default(); for entry in WalkDir::new(root) .sort_by_file_name() @@ -337,13 +322,6 @@ fn walk_dir_for_compression( .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME) { let entry = entry?; - let metadata = entry.metadata()?; - size_total_b += metadata.len(); - if size_total_b > size_limit_b { - return Err(TmcError::ArchiveSizeLimitExceeded { - limit: size_limit_mb, - }); - } let stripped = entry .path() .strip_prefix(&parent) @@ -381,8 +359,8 @@ impl FromStr for Compression { } pub struct ArchiveBuilder { - size_limit_b: usize, - size_limit_mb: u32, + size_limit_b: Option, + size_limit_mb: Option, size_total_b: usize, hasher: Option, kind: Kind, @@ -406,13 +384,15 @@ impl ArchiveBuilder { pub fn new( writer: W, compression: Compression, - size_limit_mb: u32, + size_limit_mb: Option, deterministic: bool, hash: bool, ) -> Self { - let size_limit_b = usize::try_from(size_limit_mb) - .unwrap_or(usize::MAX) - .saturating_mul(1000 * 1000); + let size_limit_b = size_limit_mb.map(|slmb| { + usize::try_from(slmb) + .unwrap_or(usize::MAX) + .saturating_mul(1000 * 1000) + }); let hasher = if hash { Some(Hasher::new()) } else { None }; let kind = match compression { Compression::Tar => { @@ -471,10 +451,12 @@ impl ArchiveBuilder { self.hash(path_in_archive.as_bytes()); let bytes = file_util::read_file(source)?; self.size_total_b += bytes.len(); - if self.size_total_b > self.size_limit_b { - return Err(TmcError::ArchiveSizeLimitExceeded { - limit: self.size_limit_mb, - }); + if let (Some(size_limit_b), Some(size_limit_mb)) = (self.size_limit_b, self.size_limit_mb) { + if self.size_total_b > size_limit_b { + return Err(TmcError::ArchiveSizeLimitExceeded { + limit: size_limit_mb, + }); + } } self.hash(&bytes); match &mut self.kind { @@ -538,8 +520,13 @@ mod test { #[test] fn exceeding_file_limit_causes_error() { - let mut builder = - ArchiveBuilder::new(Cursor::new(Vec::new()), Compression::Tar, 1, true, true); + let mut builder = ArchiveBuilder::new( + Cursor::new(Vec::new()), + Compression::Tar, + Some(1), + true, + true, + ); // write exactly 1MB, OK let mut temp = NamedTempFile::new().unwrap(); diff --git a/crates/tmc-langs-plugins/src/compression.rs b/crates/tmc-langs-plugins/src/compression.rs index 5af566606ce..ac6a3df62c6 100644 --- a/crates/tmc-langs-plugins/src/compression.rs +++ b/crates/tmc-langs-plugins/src/compression.rs @@ -24,7 +24,7 @@ pub fn compress_student_files( let mut writer = ArchiveBuilder::new( Cursor::new(vec![]), compression, - size_limit_mb, + Some(size_limit_mb), deterministic, hash, ); diff --git a/crates/tmc-langs-plugins/src/lib.rs b/crates/tmc-langs-plugins/src/lib.rs index 2de32092887..0d0b1211c46 100644 --- a/crates/tmc-langs-plugins/src/lib.rs +++ b/crates/tmc-langs-plugins/src/lib.rs @@ -56,7 +56,7 @@ pub fn compress_project( size_limit_mb: u32, ) -> Result<(Vec, Option), PluginError> { let (compressed, hash) = if naive { - compression.compress(path, hash, size_limit_mb)? + compression.compress(path, hash)? } else { let policy = get_student_file_policy(path)?; compression::compress_student_files( diff --git a/sample_exercises/make/valgrind-failing-exercise/src/source.c b/sample_exercises/make/valgrind-failing-exercise/src/source.c index 14ca54234ad..79d01b2bd72 100644 --- a/sample_exercises/make/valgrind-failing-exercise/src/source.c +++ b/sample_exercises/make/valgrind-failing-exercise/src/source.c @@ -1,5 +1,6 @@ #include #include "source.h" +#include int one(void) { From dfa206b25a03972ea1b31f5d7852037e61827261 Mon Sep 17 00:00:00 2001 From: Heliozoa Date: Tue, 7 Oct 2025 14:19:55 +0300 Subject: [PATCH 3/3] Clippy --- crates/tmc-testmycode-client/src/client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/tmc-testmycode-client/src/client.rs b/crates/tmc-testmycode-client/src/client.rs index 9e89a93c5ca..1bfef45efb9 100644 --- a/crates/tmc-testmycode-client/src/client.rs +++ b/crates/tmc-testmycode-client/src/client.rs @@ -24,7 +24,6 @@ use std::{ sync::Arc, thread, time::Duration, - u32, }; use tmc_langs_plugins::{Compression, Language}; use tmc_langs_util::progress_reporter;