1use crate::{Compression, data::TmcParams, error::LangsError, extract_project_overwrite};
4use once_cell::sync::Lazy;
5use std::{
6 io::{Cursor, Write},
7 ops::ControlFlow::{Break, Continue},
8 path::{Path, PathBuf},
9 sync::Mutex,
10};
11use tmc_langs_framework::{Archive, TmcProjectYml};
12use tmc_langs_plugins::PluginType;
13use tmc_langs_util::{FileError, file_util};
14use walkdir::WalkDir;
15use zip::{ZipWriter, write::SimpleFileOptions};
16
17static MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
18
19pub struct PrepareSubmission<'a> {
20 pub archive: &'a Path,
21 pub compression: Compression,
22 pub extract_naively: bool,
23}
24
25pub fn prepare_submission(
28 submission: PrepareSubmission,
29 target_path: &Path,
30 no_archive_prefix: bool,
31 tmc_params: TmcParams,
32 clone_path: &Path,
33 stub_archive: Option<(&Path, Compression)>,
34 output_format: Compression,
35) -> Result<String, LangsError> {
36 let _m = MUTEX.lock().map_err(|_| LangsError::MutexError)?;
38 log::debug!("preparing submission for {}", submission.archive.display());
39
40 let plugin = PluginType::from_exercise(clone_path)?;
41 let policy = tmc_langs_plugins::get_student_file_policy(clone_path)?;
42
43 let extract_dest = tempfile::tempdir().map_err(LangsError::TempDir)?;
44 let extract_dest_path = extract_dest.path().to_path_buf();
45
46 log::debug!("extracting stub");
48 let ignore_list = [
49 ".DS_Store",
50 "desktop.ini",
51 "Thumbs.db",
52 ".directory",
53 "__MACOSX",
54 file_util::LOCK_FILE_NAME,
55 ];
56 if let Some((stub_zip, compression)) = stub_archive {
57 extract_with_filter(
59 plugin,
60 stub_zip,
61 compression,
62 |path| {
63 let relative_path = path.strip_prefix(clone_path).unwrap_or(path);
64
65 policy.is_student_file(relative_path)
67 || relative_path.components().any(|c| {
68 c.as_os_str()
69 .to_str()
70 .map(|s| ignore_list.contains(&s))
71 .unwrap_or_default()
72 })
73 },
74 &extract_dest_path,
75 false,
76 )?;
77 } else {
78 for entry in WalkDir::new(clone_path).min_depth(1) {
80 let entry = entry?;
81
82 let relative_path = entry
83 .path()
84 .strip_prefix(clone_path)
85 .expect("entry is in clone path");
86
87 if policy.is_student_file(relative_path)
89 || relative_path.components().any(|c| {
90 c.as_os_str()
91 .to_str()
92 .map(|s| ignore_list.contains(&s))
93 .unwrap_or_default()
94 })
95 {
96 continue;
98 }
99
100 let target_path = extract_dest_path.join(relative_path);
101 if entry.path().is_file() {
102 file_util::copy(entry.path(), target_path)?;
103 } else if entry.path().is_dir() {
104 file_util::create_dir(target_path)?;
105 }
106 }
107 }
108
109 log::debug!("extracting student files");
111 let file = file_util::open_file(submission.archive)?;
112 if submission.extract_naively {
113 let tmcproject_path = extract_dest_path.join(".tmcproject.yml");
115 let preserved_tmcproject = if tmcproject_path.exists() {
116 Some(
117 std::fs::read(&tmcproject_path)
118 .map_err(|e| FileError::FileRead(tmcproject_path.clone(), e))?,
119 )
120 } else {
121 None
122 };
123
124 extract_project_overwrite(file, &extract_dest_path, submission.compression)?;
125
126 if let Some(bytes) = preserved_tmcproject {
127 file_util::write_to_file(&bytes, &tmcproject_path)?;
129 }
130 } else {
131 plugin.extract_student_files(file, submission.compression, &extract_dest_path)?;
133 }
134
135 log::debug!("extracting ide files");
137 let ide_files = [
138 "nbproject",
140 ".classpath",
142 ".project",
143 ".settings",
144 ".idea",
146 ];
147 extract_with_filter(
148 plugin,
149 submission.archive,
150 submission.compression,
151 |path| {
152 path.components().all(|c| {
153 c.as_os_str()
154 .to_str()
155 .map(|s| !ide_files.contains(&s))
156 .unwrap_or_default()
157 })
158 },
159 &extract_dest_path,
160 submission.extract_naively,
161 )?;
162
163 if tmc_params.0.is_empty() {
165 log::debug!("no tmc params to write");
166 } else {
167 log::debug!("writing .tmcparams");
168 let tmc_params_path = extract_dest_path.join(".tmcparams");
169 let mut tmc_params_file = file_util::create_file(&tmc_params_path)?;
170 for (key, value) in tmc_params.0 {
171 let export = format!("export {key}={value}\n");
173 log::debug!("{export}");
174 tmc_params_file
175 .write_all(export.as_bytes())
176 .map_err(|e| FileError::FileWrite(tmc_params_path.clone(), e))?;
177 }
178 }
179
180 log::debug!("creating submission archive");
182 let exercise_name = clone_path.file_name();
183 let course_name = clone_path.parent().and_then(Path::file_name);
184 let prefix = if no_archive_prefix {
185 PathBuf::new()
186 } else {
187 match (course_name, exercise_name) {
188 (Some(course_name), Some(exercise_name)) => Path::new(course_name).join(exercise_name),
189 _ => {
190 log::warn!(
191 "was not able to find exercise and/or course name from clone path {}",
192 clone_path.display()
193 );
194 PathBuf::new()
195 }
196 }
197 };
198 let archive_file = file_util::create_file(target_path)?;
199 match output_format {
200 Compression::Tar => {
201 let mut archive = tar::Builder::new(archive_file);
202 for entry in WalkDir::new(&extract_dest_path)
203 .into_iter()
204 .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME)
205 .skip(1)
206 {
207 let entry = entry?;
208 let entry_path = entry.path();
209 let stripped = prefix.join(
210 entry_path
211 .strip_prefix(&extract_dest_path)
212 .expect("the entry is inside dest"),
213 );
214 log::debug!(
215 "adding {} to tar at {}",
216 entry_path.display(),
217 stripped.display()
218 );
219 if entry_path.is_dir() {
220 archive
221 .append_dir(&stripped, entry_path)
222 .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?;
223 } else {
224 archive
225 .append_path_with_name(entry_path, stripped.to_string_lossy().as_ref())
226 .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?;
227 }
228 }
229 archive
230 .finish()
231 .map_err(|e| LangsError::TarAppend(extract_dest_path.clone(), e))?;
232 }
233 Compression::Zip => {
234 let mut archive = ZipWriter::new(archive_file);
235 for entry in WalkDir::new(&extract_dest_path)
236 .into_iter()
237 .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME)
238 .skip(1)
239 {
240 let entry = entry?;
241 let entry_path = entry.path();
242 let stripped = prefix.join(
243 entry_path
244 .strip_prefix(&extract_dest_path)
245 .expect("the entry is inside dest"),
246 );
247 log::debug!(
248 "adding {} to zip at {}",
249 entry_path.display(),
250 stripped.display()
251 );
252 if entry_path.is_dir() {
253 archive.add_directory(
254 stripped.to_string_lossy(),
255 SimpleFileOptions::default().unix_permissions(0o755),
256 )?;
257 } else {
258 archive.start_file(
259 stripped.to_string_lossy(),
260 SimpleFileOptions::default().unix_permissions(0o755),
261 )?;
262 let mut file = file_util::open_file(entry_path)?;
263 std::io::copy(&mut file, &mut archive)
264 .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?;
265 }
266 }
267 archive.finish()?;
268 }
269 Compression::TarZstd => {
270 let buf = Cursor::new(vec![]);
271 let mut archive = tar::Builder::new(buf);
272 for entry in WalkDir::new(&extract_dest_path)
273 .into_iter()
274 .filter_entry(|e| e.file_name() != file_util::LOCK_FILE_NAME)
275 .skip(1)
276 {
277 let entry = entry?;
278 let entry_path = entry.path();
279 let stripped = prefix.join(
280 entry_path
281 .strip_prefix(&extract_dest_path)
282 .expect("the entry is inside dest"),
283 );
284 log::debug!(
285 "adding {} to zip at {}",
286 entry_path.display(),
287 stripped.display()
288 );
289 if entry_path.is_dir() {
290 archive
291 .append_dir(stripped.to_string_lossy().as_ref(), entry_path)
292 .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?;
293 } else {
294 archive
295 .append_path_with_name(entry_path, stripped.to_string_lossy().as_ref())
296 .map_err(|e| LangsError::TarAppend(entry_path.to_path_buf(), e))?;
297 }
298 }
299
300 archive.finish().map_err(LangsError::TarFinish)?;
301 let mut tar = archive.into_inner().map_err(LangsError::TarIntoInner)?;
302 tar.set_position(0); zstd::stream::copy_encode(tar, archive_file, 0).map_err(LangsError::Zstd)?;
304 }
305 }
306
307 let sandbox_image = match TmcProjectYml::load(clone_path)?.and_then(|c| c.sandbox_image) {
309 Some(sandbox_image) => sandbox_image,
310 None => crate::get_default_sandbox_image(clone_path)?.to_string(),
311 };
312 Ok(sandbox_image)
313}
314
315fn extract_with_filter<F: Fn(&Path) -> bool>(
316 plugin: PluginType,
317 archive: &Path,
318 compression: Compression,
319 exclude_filter: F,
320 dest: &Path,
321 naive: bool,
322) -> Result<(), LangsError> {
323 let file = file_util::open_file(archive)?;
324 let mut zip = Archive::new(file, compression)?;
325 let project_dir_in_archive = if naive {
326 Ok(PathBuf::new())
327 } else {
328 plugin.safe_find_project_dir_in_archive(&mut zip)
329 }?;
330
331 let mut iter = zip.iter()?;
332 loop {
333 let next = iter.with_next::<(), _>(|mut file| {
334 if file.is_file() {
335 if let Ok(path) = file.path()?.strip_prefix(&project_dir_in_archive) {
336 if exclude_filter(path) {
337 return Ok(Continue(()));
339 };
340 let target = dest.join(path);
341 file_util::read_to_file(&mut file, &target)?;
342 }
343 }
344 Ok(Continue(()))
345 });
346 match next? {
347 Continue(_) => continue,
348 Break(_) => break,
349 }
350 }
351 Ok(())
352}
353
354#[cfg(test)]
355#[cfg(target_os = "linux")] #[allow(clippy::unwrap_used)]
357mod test {
358 use super::*;
359 use std::{fs, path::PathBuf};
360 use tempfile::TempDir;
361 use walkdir::WalkDir;
362
363 const MAVEN_CLONE: &str = "tests/data/some_course/MavenExercise";
364 const MAVEN_ZIP: &str = "tests/data/MavenExercise.zip";
365
366 const MAKE_CLONE: &str = "tests/data/some_course/MakeExercise";
367 const MAKE_ZIP: &str = "tests/data/MakeExercise.zip";
368
369 const PYTHON_CLONE: &str = "tests/data/some_course/PythonExercise";
370 const PYTHON_ZIP: &str = "tests/data/PythonExercise.zip";
371
372 fn init() {
373 use log::*;
374 use simple_logger::*;
375 let _ = SimpleLogger::new()
376 .with_level(LevelFilter::Debug)
377 .with_module_level("j4rs", LevelFilter::Warn)
378 .env()
379 .init();
380 }
381
382 fn generic_submission(clone: &str, zip: &str) -> (TempDir, PathBuf) {
383 let temp = tempfile::tempdir().unwrap();
384 let output_archive = temp.path().join("output.tar");
385
386 let mut tmc_params = TmcParams::new();
387 tmc_params.insert_string("param_one", "value_one").unwrap();
388 tmc_params
389 .insert_array("param_two", vec!["value_two", "value_three"])
390 .unwrap();
391 prepare_submission(
392 PrepareSubmission {
393 archive: Path::new(zip),
394 compression: Compression::Zip,
395 extract_naively: false,
396 },
397 &output_archive,
398 false,
399 tmc_params,
400 Path::new(clone),
401 None,
402 Compression::Tar,
403 )
404 .unwrap();
405 assert!(output_archive.exists());
406
407 let output_file = file_util::open_file(&output_archive).unwrap();
408 let mut archive = tar::Archive::new(output_file);
409 let output_extracted = temp.path().join("output");
410 archive.unpack(&output_extracted).unwrap();
411 for entry in WalkDir::new(temp.path()) {
412 log::debug!("file {}", entry.unwrap().path().display());
413 }
414 (temp, output_extracted)
415 }
416
417 #[test]
418 fn package_has_expected_files() {
419 init();
420 let (_temp, output) = generic_submission(MAVEN_CLONE, MAVEN_ZIP);
421 assert!(
423 output
424 .join("some_course/MavenExercise/src/main/java/SimpleStuff.java")
425 .exists()
426 );
427 assert!(
428 output
429 .join("some_course/MavenExercise/src/test/java/SimpleTest.java")
430 .exists()
431 );
432 assert!(
433 output
434 .join("some_course/MavenExercise/src/test/java/SimpleHiddenTest.java")
435 .exists()
436 );
437 assert!(output.join("some_course/MavenExercise/pom.xml").exists());
438 }
439
440 #[test]
441 fn package_doesnt_have_unwanted_files() {
442 init();
443 let (_temp, output) = generic_submission(MAVEN_CLONE, MAVEN_ZIP);
444
445 assert!(!output.join("some_course/MavenExercise/__MACOSX").exists());
447 assert!(
448 !output
449 .join("some_course/MavenExercise/src/test/java/MadeUpTest.java")
450 .exists()
451 );
452 }
453
454 #[test]
455 fn modified_test_file_not_included_in_package() {
456 init();
457 let (_temp, output) = generic_submission(MAVEN_CLONE, MAVEN_ZIP);
458
459 let zipfile = file_util::open_file(MAVEN_ZIP).unwrap();
461 let mut zip = zip::ZipArchive::new(zipfile).unwrap();
462 let mut modified = zip
463 .by_name("MavenExercise/src/test/java/SimpleTest.java")
464 .unwrap();
465 let mut writer: Vec<u8> = vec![];
466 std::io::copy(&mut modified, &mut writer).unwrap();
467 let contents = String::from_utf8(writer).unwrap();
468 assert!(contents.contains("MODIFIED"));
469 let test_file = fs::read_to_string(
471 output.join("some_course/MavenExercise/src/test/java/SimpleTest.java"),
472 )
473 .unwrap();
474 assert!(!test_file.contains("MODIFIED"));
475 }
476
477 #[test]
478 fn writes_tmcparams() {
479 init();
480 let (_temp, output) = generic_submission(MAVEN_CLONE, MAVEN_ZIP);
481
482 let param_file = output.join("some_course/MavenExercise/.tmcparams");
483 assert!(param_file.exists());
484 let conts = fs::read_to_string(param_file).unwrap();
485 log::debug!("tmcparams {conts}");
486 let lines: Vec<_> = conts.lines().collect();
487 assert_eq!(lines.len(), 2);
488 assert!(lines.contains(&"export param_one=value_one"));
489 assert!(lines.contains(&"export param_two=( value_two value_three )"));
490 }
491
492 #[test]
493 fn packages_without_prefix() {
494 init();
495
496 let temp = tempfile::tempdir().unwrap();
497 let output = temp.path().join("output.tar");
498
499 assert!(!output.exists());
500 prepare_submission(
501 PrepareSubmission {
502 archive: Path::new(MAVEN_ZIP),
503 compression: Compression::Zip,
504 extract_naively: false,
505 },
506 &output,
507 true,
508 TmcParams::new(),
509 Path::new(MAVEN_CLONE),
510 None,
511 Compression::Tar,
512 )
513 .unwrap();
514 assert!(output.exists());
515
516 let output = file_util::open_file(output).unwrap();
517 let mut archive = tar::Archive::new(output);
518 let output = temp.path().join("output");
519 archive.unpack(&output).unwrap();
520 for entry in WalkDir::new(temp.path()) {
521 log::debug!("{}", entry.unwrap().path().display());
522 }
523 assert!(output.join("src/test/java/SimpleHiddenTest.java").exists());
524 assert!(output.join("pom.xml").exists());
525 }
526
527 #[test]
528 fn packages_with_output_zstd() {
529 init();
530
531 let temp = tempfile::tempdir().unwrap();
532 let output = temp.path().join("output.tar.zst");
533
534 assert!(!output.exists());
535 prepare_submission(
536 PrepareSubmission {
537 archive: Path::new(MAVEN_ZIP),
538 compression: Compression::Zip,
539 extract_naively: false,
540 },
541 &output,
542 false,
543 TmcParams::new(),
544 Path::new(MAVEN_CLONE),
545 None,
546 Compression::TarZstd,
547 )
548 .unwrap();
549 assert!(output.exists());
550
551 let output = file_util::open_file(output).unwrap();
552 let output = std::io::Cursor::new(zstd::decode_all(output).unwrap());
553 let mut archive = tar::Archive::new(output);
554 let output = temp.path().join("output");
555 archive.unpack(&output).unwrap();
556 for entry in WalkDir::new(temp.path()) {
557 log::debug!("{}", entry.unwrap().path().display());
558 }
559 assert!(
560 output
561 .join("some_course/MavenExercise/src/test/java/SimpleHiddenTest.java")
562 .exists()
563 );
564 assert!(output.join("some_course/MavenExercise/pom.xml").exists());
565 }
566
567 #[test]
568 fn packages_with_output_zip() {
569 init();
570
571 let temp = tempfile::tempdir().unwrap();
572 let output = temp.path().join("output.zip");
573
574 assert!(!output.exists());
575 prepare_submission(
576 PrepareSubmission {
577 archive: Path::new(MAVEN_ZIP),
578 compression: Compression::Zip,
579 extract_naively: false,
580 },
581 &output,
582 false,
583 TmcParams::new(),
584 Path::new(MAVEN_CLONE),
585 None,
586 Compression::Zip,
587 )
588 .unwrap();
589 assert!(output.exists());
590
591 let output = file_util::open_file(output).unwrap();
592 let mut archive = zip::ZipArchive::new(output).unwrap();
593 archive
594 .by_name("some_course/MavenExercise/src/test/java/SimpleHiddenTest.java")
595 .unwrap();
596 }
597
598 #[test]
599 fn packages_without_prefix_and_output_zip() {
600 init();
601
602 let temp = tempfile::tempdir().unwrap();
603 let output = temp.path().join("output.zip");
604
605 assert!(!output.exists());
606 prepare_submission(
607 PrepareSubmission {
608 archive: Path::new(MAVEN_ZIP),
609 compression: Compression::Zip,
610 extract_naively: false,
611 },
612 &output,
613 true,
614 TmcParams::new(),
615 Path::new(MAVEN_CLONE),
616 None,
617 Compression::Zip,
618 )
619 .unwrap();
620 assert!(output.exists());
621
622 let output = file_util::open_file(output).unwrap();
623 let mut archive = zip::ZipArchive::new(output).unwrap();
624 archive
625 .by_name("src/test/java/SimpleHiddenTest.java")
626 .unwrap();
627 archive.by_name("pom.xml").unwrap();
628 }
629
630 #[test]
631 fn package_with_stub_tests() {
632 init();
633
634 let temp = tempfile::tempdir().unwrap();
635 let output_arch = temp.path().join("output.tar");
636
637 assert!(!output_arch.exists());
638 prepare_submission(
639 PrepareSubmission {
640 archive: Path::new(MAVEN_ZIP),
641 compression: Compression::Zip,
642 extract_naively: false,
643 },
644 &output_arch,
645 false,
646 TmcParams::new(),
647 Path::new(MAVEN_CLONE),
648 Some((Path::new("tests/data/MavenStub.zip"), Compression::Zip)),
649 Compression::Tar,
650 )
651 .unwrap();
652 assert!(output_arch.exists());
653
654 let output_file = file_util::open_file(&output_arch).unwrap();
655 let mut archive = tar::Archive::new(output_file);
656 let output_extracted = temp.path().join("output");
657 archive.unpack(&output_extracted).unwrap();
658 for entry in WalkDir::new(temp.path()) {
659 log::debug!("{}", entry.unwrap().path().display());
660 }
661
662 assert!(
664 output_extracted
665 .join("some_course/MavenExercise/src/test/java/SimpleTest.java")
666 .exists()
667 );
668 assert!(
669 !output_extracted
670 .join("some_course/MavenExercise/src/test/java/SimpleHiddenTest.java")
671 .exists()
672 );
673 }
674
675 #[test]
676 fn stub_tmcproject_yml_overrides_student_in_naive_mode() {
677 init();
678
679 let temp = tempfile::tempdir().unwrap();
681 let clone_root = temp.path().join("some_course");
682 file_util::create_dir_all(&clone_root).unwrap();
683 let src_clone = Path::new(PYTHON_CLONE);
684 file_util::copy(src_clone, &clone_root).unwrap();
685
686 let stub_ex_dir = clone_root.join("PythonExercise");
687 file_util::write_to_file(b"key: stub", stub_ex_dir.join(".tmcproject.yml")).unwrap();
689
690 let sub_zip_path = temp.path().join("submission.zip");
692 let sub_zip_file = file_util::create_file(&sub_zip_path).unwrap();
693 let mut zw = zip::ZipWriter::new(sub_zip_file);
694 let opts = SimpleFileOptions::default();
695 zw.add_directory("PythonExercise", opts).unwrap();
696 zw.add_directory("PythonExercise/src", opts).unwrap();
697 zw.start_file("PythonExercise/__init__.py", opts).unwrap();
698 std::io::Write::write_all(&mut zw, b"print('student')\n").unwrap();
699 zw.start_file("PythonExercise/src/__main__.py", opts)
700 .unwrap();
701 std::io::Write::write_all(&mut zw, b"print('student')\n").unwrap();
702 zw.start_file("PythonExercise/.tmcproject.yml", opts)
703 .unwrap();
704 std::io::Write::write_all(&mut zw, b"key: student\n").unwrap();
705 zw.finish().unwrap();
706
707 let output_arch = temp.path().join("out.tar");
708 prepare_submission(
709 PrepareSubmission {
710 archive: &sub_zip_path,
711 compression: Compression::Zip,
712 extract_naively: false,
713 },
714 &output_arch,
715 false,
716 TmcParams::new(),
717 &stub_ex_dir,
718 None,
719 Compression::Tar,
720 )
721 .unwrap();
722 assert!(output_arch.exists());
723
724 let output_file = file_util::open_file(&output_arch).unwrap();
726 let mut archive = tar::Archive::new(output_file);
727 let output_extracted = temp.path().join("output");
728 archive.unpack(&output_extracted).unwrap();
729
730 for file in WalkDir::new(&output_extracted) {
731 let file = file.unwrap();
732 println!("{}", file.path().display());
733 }
734
735 let yml =
737 fs::read_to_string(output_extracted.join("some_course/PythonExercise/.tmcproject.yml"))
738 .unwrap();
739 assert!(yml.contains("key: stub"));
740 assert!(!yml.contains("key: student"));
741
742 assert!(
744 output_extracted
745 .join("some_course/PythonExercise/src/__main__.py")
746 .exists()
747 );
748 assert!(
749 output_extracted
750 .join("some_course/PythonExercise/test/test_greeter.py")
751 .exists()
752 );
753 assert!(
754 output_extracted
755 .join("some_course/PythonExercise/__init__.py")
756 .exists()
757 );
758 }
759
760 #[test]
761 fn prepare_make_submission() {
762 init();
763 let (_temp, output) = generic_submission(MAKE_CLONE, MAKE_ZIP);
764
765 assert!(output.join("some_course/MakeExercise/src/main.c").exists());
767 assert!(
768 output
769 .join("some_course/MakeExercise/test/test_source.c")
770 .exists()
771 );
772 assert!(output.join("some_course/MakeExercise/Makefile").exists());
773 }
774
775 #[test]
776 fn prepare_langs_submission() {
777 init();
778 let (_temp, output) = generic_submission(PYTHON_CLONE, PYTHON_ZIP);
779
780 assert!(
782 output
783 .join("some_course/PythonExercise/src/__main__.py")
784 .exists()
785 );
786 assert!(
787 output
788 .join("some_course/PythonExercise/test/test_greeter.py")
789 .exists()
790 );
791 assert!(
793 output
794 .join("some_course/PythonExercise/__init__.py")
795 .exists()
796 );
797 }
798
799 #[test]
800 fn includes_files_in_root_dir_from_exercise() {
801 init();
802
803 let temp = tempfile::tempdir().unwrap();
804 let clone_root = temp.path().join("some_course");
805 file_util::create_dir_all(&clone_root).unwrap();
806
807 let src_clone = Path::new(MAVEN_CLONE);
809 file_util::copy(src_clone, &clone_root).unwrap();
810
811 let exercise_dir = clone_root.join("MavenExercise");
812
813 let repo_file_path = exercise_dir.join("foo.txt");
815 file_util::write_to_file(b"repohello", &repo_file_path).unwrap();
816
817 let sub_zip_path = temp.path().join("submission.zip");
820 let sub_zip_file = file_util::create_file(&sub_zip_path).unwrap();
821 let mut zw = zip::ZipWriter::new(sub_zip_file);
822 let opts = SimpleFileOptions::default();
823
824 zw.add_directory("MavenExercise", opts).unwrap();
826 zw.add_directory("MavenExercise/src", opts).unwrap();
827 zw.add_directory("MavenExercise/src/main", opts).unwrap();
828 zw.add_directory("MavenExercise/src/main/java", opts)
829 .unwrap();
830
831 zw.start_file("MavenExercise/foo.txt", opts).unwrap();
833 std::io::Write::write_all(&mut zw, b"submissionhello").unwrap();
834
835 zw.start_file("MavenExercise/src/main/java/SimpleStuff.java", opts)
837 .unwrap();
838 std::io::Write::write_all(&mut zw, b"public class SimpleStuff { }").unwrap();
839
840 zw.finish().unwrap();
841
842 let output_arch = temp.path().join("out.tar");
843 prepare_submission(
844 PrepareSubmission {
845 archive: &sub_zip_path,
846 compression: Compression::Zip,
847 extract_naively: false,
848 },
849 &output_arch,
850 false,
851 TmcParams::new(),
852 &exercise_dir,
853 None,
854 Compression::Tar,
855 )
856 .unwrap();
857 assert!(output_arch.exists());
858
859 let output_file = file_util::open_file(&output_arch).unwrap();
861 let mut archive = tar::Archive::new(output_file);
862 let output_extracted = temp.path().join("output");
863 archive.unpack(&output_extracted).unwrap();
864
865 let foo_file = output_extracted.join("some_course/MavenExercise/foo.txt");
867 assert!(
868 foo_file.exists(),
869 "foo.txt should be included in the archive"
870 );
871 let content = fs::read_to_string(foo_file).unwrap();
872 assert_eq!(
873 content, "repohello",
874 "Should use repo content, not submission content"
875 );
876 }
877}