tmc_langs_java/
ant_plugin.rs

1//! Java Ant plugin.
2
3use crate::{
4    AntStudentFilePolicy, CompileResult, JvmWrapper, SEPARATOR, TestRun, error::JavaError,
5    java_plugin::JavaPlugin,
6};
7use std::{
8    env,
9    ffi::OsStr,
10    io::{Read, Seek},
11    ops::ControlFlow::{Break, Continue},
12    path::{Path, PathBuf},
13    time::Duration,
14};
15use tmc_langs_framework::{
16    Archive, ExerciseDesc, Language, LanguagePlugin, RunResult, StyleValidationResult, TmcCommand,
17    TmcError, nom::IResult, nom_language::error::VerboseError,
18};
19use tmc_langs_util::{file_util, path_util};
20use walkdir::WalkDir;
21
22pub struct AntPlugin {
23    jvm: JvmWrapper,
24}
25
26impl AntPlugin {
27    pub fn new() -> Result<Self, JavaError> {
28        let jvm = crate::instantiate_jvm()?;
29        Ok(Self { jvm })
30    }
31
32    fn get_ant_executable(&self) -> &'static str {
33        if cfg!(windows) {
34            let command = TmcCommand::piped("ant");
35            if let Ok(status) = command.with(|e| e.arg("-version")).status() {
36                if status.success() {
37                    return "ant";
38                }
39            }
40            // if ant not found on windows, try ant.bat
41            "ant.bat"
42        } else {
43            "ant"
44        }
45    }
46
47    /// Writes the bundled tmc-junit-runner into dest_path/lib/testrunner/tmc-junit-runner.jar
48    // TODO: check for updates
49    pub fn copy_tmc_junit_runner(dest_path: &Path) -> Result<(), JavaError> {
50        log::debug!("copying TMC Junit runner");
51
52        let runner_dir = dest_path.join("lib").join("testrunner");
53        let runner_path = runner_dir.join("tmc-junit-runner.jar");
54
55        // TODO: don't traverse symlinks
56        if !runner_path.exists() {
57            log::debug!("writing tmc-junit-runner to {}", runner_path.display());
58            file_util::write_to_file(super::TMC_JUNIT_RUNNER_BYTES, &runner_path)?;
59        } else {
60            log::debug!("already exists");
61        }
62        Ok(())
63    }
64}
65
66/// Project directory:
67/// Contains build.xml file.
68/// OR
69/// Contains src and test directories.
70impl LanguagePlugin for AntPlugin {
71    const PLUGIN_NAME: &'static str = "apache-ant";
72    const DEFAULT_SANDBOX_IMAGE: &'static str = "eu.gcr.io/moocfi-public/tmc-sandbox-java:latest";
73    const LINE_COMMENT: &'static str = "//";
74    const BLOCK_COMMENT: Option<(&'static str, &'static str)> = Some(("/*", "*/"));
75    type StudentFilePolicy = AntStudentFilePolicy;
76
77    fn check_code_style(
78        &self,
79        path: &Path,
80        locale: Language,
81    ) -> Result<Option<StyleValidationResult>, TmcError> {
82        Ok(Some(self.run_checkstyle(&locale, path)?))
83    }
84
85    /// Scans the exercise at the given path. Immediately exits if the target directory is not a valid exercise.
86    fn scan_exercise(&self, path: &Path, exercise_name: String) -> Result<ExerciseDesc, TmcError> {
87        if !Self::is_exercise_type_correct(path) {
88            return JavaError::InvalidExercise(path.to_path_buf()).into();
89        }
90
91        let compile_result = self.build(path)?;
92        Ok(self.scan_exercise_with_compile_result(path, exercise_name, compile_result)?)
93    }
94
95    fn run_tests_with_timeout(
96        &self,
97        project_root_path: &Path,
98        timeout: Option<Duration>,
99    ) -> Result<RunResult, TmcError> {
100        Ok(self.run_java_tests(project_root_path, timeout)?)
101    }
102
103    fn find_project_dir_in_archive<R: Read + Seek>(
104        archive: &mut Archive<R>,
105    ) -> Result<PathBuf, TmcError> {
106        let mut iter = archive.iter()?;
107        let mut src_parents = vec![];
108        let mut test_parents = vec![];
109        let project_dir = loop {
110            let next = iter.with_next(|file| {
111                let file_path = file.path()?;
112
113                if file.is_file() {
114                    // check for build.xml
115                    if let Some(parent) = path_util::get_parent_of_named(&file_path, "build.xml") {
116                        return Ok(Break(Some(parent)));
117                    }
118                } else if file.is_dir() {
119                    // check for src
120                    if let Some(src_parent) =
121                        path_util::get_parent_of_component_in_path(&file_path, "src")
122                    {
123                        if test_parents.contains(&src_parent) {
124                            // found a test in the same directory before, return
125                            return Ok(Break(Some(src_parent)));
126                        } else {
127                            src_parents.push(src_parent)
128                        }
129                    }
130
131                    // check for test
132                    if let Some(test_parent) =
133                        path_util::get_parent_of_component_in_path(&file_path, "test")
134                    {
135                        if src_parents.contains(&test_parent) {
136                            // found a test in the same directory before, return
137                            return Ok(Break(Some(test_parent)));
138                        } else {
139                            test_parents.push(test_parent)
140                        }
141                    }
142                }
143
144                Ok(Continue(()))
145            });
146            match next? {
147                Continue(_) => continue,
148                Break(project_dir) => break project_dir,
149            }
150        };
151
152        match project_dir {
153            Some(project_dir) => Ok(project_dir),
154            None => Err(TmcError::NoProjectDirInArchive),
155        }
156    }
157
158    /// Checks if the directory contains a build.xml file, or a src and a test directory.
159    fn is_exercise_type_correct(path: &Path) -> bool {
160        path.join("build.xml").is_file() || path.join("test").is_dir() && path.join("src").is_dir()
161    }
162
163    fn clean(&self, path: &Path) -> Result<(), TmcError> {
164        log::debug!("cleaning project at {}", path.display());
165
166        // TODO: is writing stdout and stderr to file really necessary?
167        let stdout_path = path.join("build_log.txt");
168        let stdout = file_util::create_file(&stdout_path)?;
169        let stderr_path = path.join("build_errors.txt");
170        let stderr = file_util::create_file(&stderr_path)?;
171
172        let ant_exec = self.get_ant_executable();
173        let _output = TmcCommand::new(ant_exec)
174            .with(|e| e.arg("clean").stdout(stdout).stderr(stderr).cwd(path))
175            .output_checked()?;
176        file_util::remove_file(&stdout_path)?;
177        file_util::remove_file(&stderr_path)?;
178        Ok(())
179    }
180
181    fn points_parser(i: &str) -> IResult<&str, Vec<&str>, VerboseError<&str>> {
182        Self::java_points_parser(i)
183    }
184
185    fn get_default_student_file_paths() -> Vec<PathBuf> {
186        vec![PathBuf::from("src")]
187    }
188
189    fn get_default_exercise_file_paths() -> Vec<PathBuf> {
190        vec![PathBuf::from("test")]
191    }
192}
193
194impl JavaPlugin for AntPlugin {
195    const TEST_DIR: &'static str = "test";
196
197    fn jvm(&self) -> &JvmWrapper {
198        &self.jvm
199    }
200
201    /// Constructs the class path for the given path.
202    fn get_project_class_path(&self, path: &Path) -> Result<String, JavaError> {
203        // canonicalize root path to avoid issues where the cwd and project root are different directories
204        let path = file_util::canonicalize(path)?;
205        let mut paths = vec![];
206
207        // add all .jar files in lib
208        let lib_dir = path.join("lib");
209        for entry in WalkDir::new(&lib_dir) {
210            let entry = entry?;
211
212            if entry.path().is_file() && entry.path().extension() == Some(OsStr::new("jar")) {
213                paths.push(entry.path().to_path_buf());
214            }
215        }
216        paths.push(lib_dir);
217        paths.push(path.join("build").join("test").join("classes"));
218        paths.push(path.join("build").join("classes"));
219
220        let java_home = Self::get_java_home()?;
221        // TODO: what's tools.jar?
222        let tools_jar_path = java_home.join("..").join("lib").join("tools.jar");
223        if tools_jar_path.exists() {
224            paths.push(tools_jar_path);
225        } else {
226            log::warn!("no tools.jar found; skip adding to class path");
227        }
228
229        // ignore non-UTF8 paths
230        let paths = paths
231            .into_iter()
232            .filter_map(|p| p.to_str().map(str::to_string))
233            .collect::<Vec<_>>();
234
235        // TODO: is it OK to not include the runner in the classpath?
236        Self::copy_tmc_junit_runner(&path)?;
237        Ok(paths.join(SEPARATOR))
238    }
239
240    fn build(&self, project_root_path: &Path) -> Result<CompileResult, JavaError> {
241        log::info!("building project at {}", project_root_path.display());
242
243        let ant_exec = self.get_ant_executable();
244        let output = TmcCommand::piped(ant_exec)
245            .with(|e| e.arg("compile-test").cwd(project_root_path))
246            .output()?;
247
248        // TODO: is it really necessary to write the logs in files?
249        log::debug!("stdout: {}", String::from_utf8_lossy(&output.stdout));
250        log::debug!("stderr: {}", String::from_utf8_lossy(&output.stderr));
251        let stdout_path = project_root_path.join("build_log.txt");
252        let stderr_path = project_root_path.join("build_errors.txt");
253        file_util::write_to_file(&output.stdout, stdout_path)?;
254        file_util::write_to_file(&output.stderr, stderr_path)?;
255
256        Ok(CompileResult {
257            status_code: output.status,
258            stdout: output.stdout,
259            stderr: output.stderr,
260        })
261    }
262
263    fn create_run_result_file(
264        &self,
265        path: &Path,
266        timeout: Option<Duration>,
267        compile_result: CompileResult,
268    ) -> Result<TestRun, JavaError> {
269        log::info!("running tests for project at {}", path.display());
270
271        // build java args
272        let mut arguments = vec![];
273        // JVM args
274        if let Ok(jvm_options) = env::var("JVM_OPTIONS") {
275            arguments.extend(
276                jvm_options
277                    .split(" +")
278                    .map(|s| s.trim())
279                    .filter(|s| !s.is_empty())
280                    .map(|s| s.to_string()),
281            )
282        }
283        // TMC args
284        let test_dir = path.join("test");
285        let result_file_name = "results.txt";
286        let result_file = path.join(result_file_name);
287        arguments.push(format!("-Dtmc.test_class_dir={}", test_dir.display()));
288        // we want to use path relative to the exercise path for the java command,
289        // and a path relative to the current directory for the rest of the program
290        arguments.push(format!("-Dtmc.results_file={result_file_name}"));
291        // TODO: endorsed libs?
292        let endorsed_libs_path = path.join("lib/endorsed");
293        if endorsed_libs_path.exists() {
294            arguments.push(format!(
295                "-Djava.endorsed.dirs={}",
296                endorsed_libs_path.display()
297            ));
298        }
299        // scan needs to be before getting class path
300        let exercise = self.scan_exercise_with_compile_result(
301            path,
302            format!("{}{}", path.display(), "/test"), // TODO: ?
303            compile_result,
304        )?;
305        // classpath
306        arguments.push("-cp".to_string());
307        let class_path = self.get_project_class_path(path)?;
308        arguments.push(class_path);
309        // main
310        arguments.push("fi.helsinki.cs.tmc.testrunner.Main".to_string());
311        // ?
312        for desc in exercise.tests {
313            let mut s = String::new();
314            s.push_str(&desc.name.replace(' ', "."));
315            s.push('{');
316            s.push_str(&desc.points.join(","));
317            s.push('}');
318            arguments.push(s);
319        }
320
321        log::debug!("java args '{}' in {}", arguments.join(" "), path.display());
322        let command = TmcCommand::piped("java").with(|e| e.cwd(path).args(&arguments));
323        let output = if let Some(timeout) = timeout {
324            command.output_with_timeout(timeout)?
325        } else {
326            command.output()?
327        };
328
329        Ok(TestRun {
330            test_results: result_file,
331            stdout: output.stdout,
332            stderr: output.stderr,
333        })
334    }
335}
336
337#[cfg(test)]
338#[allow(clippy::unwrap_used)]
339mod test {
340    use super::*;
341    use std::fs;
342    use tmc_langs_framework::{Archive, StyleValidationStrategy};
343    use tmc_langs_util::deserialize;
344    use zip::write::SimpleFileOptions;
345
346    fn init() {
347        use log::*;
348        use simple_logger::*;
349        let _ = SimpleLogger::new()
350            .with_level(LevelFilter::Debug)
351            // j4rs does a lot of logging
352            .with_module_level("j4rs", LevelFilter::Warn)
353            .init();
354    }
355
356    fn file_to(
357        target_dir: impl AsRef<std::path::Path>,
358        target_relative: impl AsRef<std::path::Path>,
359        contents: impl AsRef<[u8]>,
360    ) -> PathBuf {
361        let target = target_dir.as_ref().join(target_relative);
362        if let Some(parent) = target.parent() {
363            std::fs::create_dir_all(parent).unwrap();
364        }
365        std::fs::write(&target, contents.as_ref()).unwrap();
366        target
367    }
368
369    fn dir_to(
370        target_dir: impl AsRef<std::path::Path>,
371        target_relative: impl AsRef<std::path::Path>,
372    ) -> PathBuf {
373        let target = target_dir.as_ref().join(target_relative);
374        std::fs::create_dir_all(&target).unwrap();
375        target
376    }
377
378    fn dir_to_temp(source_dir: impl AsRef<std::path::Path>) -> tempfile::TempDir {
379        let temp = tempfile::TempDir::new().unwrap();
380        for entry in walkdir::WalkDir::new(&source_dir).min_depth(1) {
381            let entry = entry.unwrap();
382            let rela = entry.path().strip_prefix(&source_dir).unwrap();
383            let target = temp.path().join(rela);
384            if entry.path().is_dir() {
385                std::fs::create_dir(target).unwrap();
386            } else if entry.path().is_file() {
387                std::fs::copy(entry.path(), target).unwrap();
388            }
389        }
390        temp
391    }
392
393    fn dir_to_zip(source_dir: impl AsRef<std::path::Path>) -> Vec<u8> {
394        use std::io::Write;
395
396        let mut target = vec![];
397        let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut target));
398
399        for entry in walkdir::WalkDir::new(&source_dir)
400            .min_depth(1)
401            .sort_by(|a, b| a.path().cmp(b.path()))
402        {
403            let entry = entry.unwrap();
404            let rela = entry
405                .path()
406                .strip_prefix(&source_dir)
407                .unwrap()
408                .to_str()
409                .unwrap();
410            if entry.path().is_dir() {
411                zip.add_directory(rela, SimpleFileOptions::default())
412                    .unwrap();
413            } else if entry.path().is_file() {
414                zip.start_file(rela, SimpleFileOptions::default()).unwrap();
415                let bytes = std::fs::read(entry.path()).unwrap();
416                zip.write_all(&bytes).unwrap();
417            }
418        }
419
420        zip.finish().unwrap();
421        target
422    }
423
424    #[test]
425    fn copies_tmc_junit_runner() {
426        init();
427
428        let temp = tempfile::TempDir::new().unwrap();
429        let jar_dir = temp.path().join("dir");
430        let jar_path = jar_dir.join("lib/testrunner/tmc-junit-runner.jar");
431        assert!(!jar_path.exists());
432        AntPlugin::copy_tmc_junit_runner(&jar_dir).unwrap();
433        assert!(jar_path.exists());
434    }
435
436    #[test]
437    fn gets_project_class_path() {
438        init();
439
440        let temp = tempfile::TempDir::new().unwrap();
441        let test_path = temp.path().join("dir");
442        file_to(&test_path, "lib/junit-4.13.2.jar", "");
443        file_to(&test_path, "lib/edu-test-utils-0.5.0.jar", "");
444
445        let plugin = AntPlugin::new().unwrap();
446        let cp = plugin.get_project_class_path(&test_path).unwrap();
447
448        // get_project_class_path, do the same here to avoid mismatches in windows CI
449        let test_path = file_util::canonicalize(&test_path).unwrap();
450        let sep = std::path::MAIN_SEPARATOR;
451        let expected_junit = format!("{0}{1}lib{1}junit-4.13.2.jar", test_path.display(), sep);
452        assert!(
453            cp.contains(&expected_junit),
454            "Classpath {cp} did not contain junit (looked for {expected_junit})",
455        );
456        let expected_utils = format!(
457            "{0}{1}lib{1}edu-test-utils-0.5.0.jar",
458            test_path.display(),
459            sep
460        );
461        assert!(
462            cp.contains(&expected_utils),
463            "Classpath {cp} did not contain edu-test-utils (looked for {expected_utils}",
464        );
465        let expected_classes = format!("{0}{1}build{1}classes", test_path.display(), sep);
466        assert!(
467            cp.contains(&expected_classes),
468            "Classpath {cp} did not contain build{sep}classes (looked for {expected_classes}",
469        );
470        let expected_test_classes =
471            format!("{0}{1}build{1}test{1}classes", test_path.display(), sep);
472        assert!(
473            cp.contains(&expected_test_classes),
474            "Classpath {cp} did not contain build/test/classes (looked for {expected_test_classes}",
475        );
476        // tools.jar is in java home, tricky to test
477        /*
478        assert!(
479            cp.ends_with(&format!("{0}..{0}lib{0}tools.jar", sep)),
480            "Classpath was {}",
481            cp
482        );
483        */
484    }
485
486    #[test]
487    fn builds() {
488        init();
489
490        let temp_dir = dir_to_temp("tests/data/ant-exercise");
491        let plugin = AntPlugin::new().unwrap();
492        let compile_result = plugin.build(temp_dir.path()).unwrap();
493        assert!(compile_result.status_code.success());
494        // may contain unexpected output depending on machine config
495        // assert!(!compile_result.stdout.is_empty());
496        // assert!(compile_result.stderr.is_empty());
497    }
498
499    #[test]
500    fn creates_run_result_file() {
501        init();
502
503        let temp_dir = dir_to_temp("tests/data/ant-exercise");
504        let plugin = AntPlugin::new().unwrap();
505        let compile_result = plugin.build(temp_dir.path()).unwrap();
506        let test_run = plugin
507            .create_run_result_file(temp_dir.path(), None, compile_result)
508            .unwrap();
509        log::trace!("stdout: {}", String::from_utf8_lossy(&test_run.stdout));
510        log::debug!("stderr: {}", String::from_utf8_lossy(&test_run.stderr));
511        // may contain unexpected output depending on machine config
512        // assert!(test_run.stdout.is_empty());
513        // assert!(test_run.stderr.is_empty());
514        let res = fs::read_to_string(test_run.test_results).unwrap();
515        let test_cases: Vec<super::super::TestCase> = deserialize::json_from_str(&res).unwrap();
516
517        let test_case = &test_cases[0];
518        assert_eq!(test_case.class_name, "ArithTest");
519        assert_eq!(test_case.method_name, "testAdd");
520        assert_eq!(test_case.status, super::super::TestCaseStatus::Passed);
521        assert_eq!(test_case.point_names[0], "arith-funcs");
522        assert!(test_case.message.is_none());
523        assert!(test_case.exception.is_none());
524
525        let test_case = &test_cases[1];
526        assert_eq!(test_case.class_name, "ArithTest");
527        assert_eq!(test_case.method_name, "testSub");
528        assert_eq!(test_case.status, super::super::TestCaseStatus::Failed);
529        assert_eq!(test_case.point_names[0], "arith-funcs");
530        assert!(test_case.message.as_ref().unwrap().starts_with("expected:"));
531
532        let exception = test_case.exception.as_ref().unwrap();
533        // assert_eq!(exception.class_name, "java.lang.AssertionError");
534        assert!(exception.message.as_ref().unwrap().starts_with("expected:"));
535        // assert!(exception.cause.is_none());
536
537        let stack_trace = &exception.stack_trace[0];
538        assert_eq!(stack_trace.declaring_class, "org.junit.Assert");
539        assert_eq!(stack_trace.file_name.as_ref().unwrap(), "Assert.java");
540        assert_eq!(stack_trace.method_name, "fail");
541    }
542
543    #[test]
544    fn scans_exercise() {
545        init();
546
547        let temp_dir = dir_to_temp("tests/data/ant-exercise");
548        let plugin = AntPlugin::new().unwrap();
549        let exercises = plugin
550            .scan_exercise(temp_dir.path(), "test".to_string())
551            .unwrap();
552        assert_eq!(exercises.name, "test");
553        assert_eq!(exercises.tests.len(), 4);
554        assert_eq!(exercises.tests[0].name, "ArithTest testAdd");
555        assert_eq!(exercises.tests[0].points, ["arith-funcs"]);
556    }
557
558    #[test]
559    fn runs_checkstyle() {
560        init();
561
562        let temp_dir = dir_to_temp("tests/data/ant-exercise");
563        let plugin = AntPlugin::new().unwrap();
564        let checkstyle_result = plugin
565            .check_code_style(temp_dir.path(), Language::from_639_3("fin").unwrap())
566            .unwrap()
567            .unwrap();
568
569        assert_eq!(checkstyle_result.strategy, StyleValidationStrategy::Fail);
570        let validation_errors = checkstyle_result.validation_errors.unwrap();
571        let errors = validation_errors.get(Path::new("Arith.java")).unwrap();
572        assert_eq!(errors.len(), 1);
573        let error = &errors[0];
574        assert_eq!(error.column, 0);
575        assert_eq!(error.line, 7);
576        assert!(error.message.starts_with("Sisennys väärin"));
577        assert_eq!(
578            error.source_name,
579            "com.puppycrawl.tools.checkstyle.checks.indentation.IndentationCheck"
580        );
581    }
582
583    #[test]
584    fn runs_tests() {
585        init();
586
587        let temp_dir = dir_to_temp("tests/data/ant-exercise");
588        let plugin = AntPlugin::new().unwrap();
589        let test_result = plugin
590            .run_tests_with_timeout(Path::new(temp_dir.path()), None)
591            .unwrap();
592        log::debug!("{test_result:?}");
593        assert_eq!(
594            test_result.status,
595            tmc_langs_framework::RunStatus::TestsFailed
596        );
597    }
598
599    #[test]
600    fn runs_tests_with_timeout() {
601        init();
602
603        let temp_dir = dir_to_temp("tests/data/ant-exercise");
604        let plugin = AntPlugin::new().unwrap();
605        let test_result_err = plugin
606            .run_tests_with_timeout(Path::new(temp_dir.path()), Some(Duration::from_nanos(1)))
607            .unwrap_err();
608        log::debug!("{test_result_err:?}");
609
610        // verify that there's a timeout error in the source chain
611        use std::error::Error;
612        let mut source = test_result_err.source();
613        while let Some(inner) = source {
614            source = inner.source();
615            if let Some(cmd_error) = inner.downcast_ref::<tmc_langs_framework::CommandError>() {
616                if matches!(cmd_error, tmc_langs_framework::CommandError::TimeOut { .. }) {
617                    return;
618                }
619            }
620        }
621        panic!("no timeout error found");
622    }
623
624    #[test]
625    fn exercise_type_is_correct() {
626        let temp = tempfile::tempdir().unwrap();
627        file_to(&temp, "build.xml", "");
628        assert!(AntPlugin::is_exercise_type_correct(temp.path()));
629
630        let temp = tempfile::tempdir().unwrap();
631        dir_to(&temp, "test");
632        dir_to(&temp, "src");
633        assert!(AntPlugin::is_exercise_type_correct(temp.path()));
634    }
635
636    #[test]
637    fn exercise_type_is_not_correct() {
638        let temp = tempfile::tempdir().unwrap();
639        file_to(&temp, "buid.xml", "");
640        file_to(&temp, "dir/build.xml", "");
641        file_to(&temp, "test", "");
642        dir_to(&temp, "src");
643        assert!(!AntPlugin::is_exercise_type_correct(temp.path()));
644    }
645
646    #[test]
647    fn cleans() {
648        init();
649
650        let temp_dir = dir_to_temp("tests/data/ant-exercise");
651        let test_path = temp_dir.path();
652        let plugin = AntPlugin::new().unwrap();
653        plugin.clean(test_path).unwrap();
654    }
655
656    #[test]
657    fn finds_project_dir_in_zip() {
658        init();
659
660        let temp_dir = tempfile::tempdir().unwrap();
661        dir_to(&temp_dir, "Outer/Inner/ant-exercise/src");
662        dir_to(&temp_dir, "Outer/Inner/ant-exercise/test");
663
664        let zip_contents = dir_to_zip(&temp_dir);
665        let mut zip = Archive::zip(std::io::Cursor::new(zip_contents)).unwrap();
666        let dir = AntPlugin::find_project_dir_in_archive(&mut zip).unwrap();
667        assert_eq!(dir, Path::new("Outer/Inner/ant-exercise"));
668    }
669
670    #[test]
671    fn finds_project_dir_in_zip_build() {
672        init();
673
674        let temp_dir = tempfile::tempdir().unwrap();
675        file_to(&temp_dir, "Outer/Inner/ant-exercise/build.xml", "build!");
676
677        let zip_contents = dir_to_zip(&temp_dir);
678        let mut zip = Archive::zip(std::io::Cursor::new(zip_contents)).unwrap();
679        let dir = AntPlugin::find_project_dir_in_archive(&mut zip).unwrap();
680        assert_eq!(dir, Path::new("Outer/Inner/ant-exercise"));
681    }
682
683    #[test]
684    fn doesnt_find_project_dir_in_zip() {
685        init();
686
687        let temp_dir = tempfile::tempdir().unwrap();
688        dir_to(&temp_dir, "Outer/Inner/ant-exercise/srcb");
689
690        let zip_contents = dir_to_zip(&temp_dir);
691        let mut zip = Archive::zip(std::io::Cursor::new(zip_contents)).unwrap();
692        let dir = AntPlugin::find_project_dir_in_archive(&mut zip);
693        assert!(dir.is_err());
694    }
695}