Skip to main content

java_manager/
execute.rs

1//! Running Java programs with controlled output and redirection.
2
3use crate::{JavaError, JavaInfo};
4use std::fs::{File, OpenOptions};
5use std::io::{BufRead, BufReader};
6use std::path::{Path, PathBuf};
7use std::process::{Command, Stdio};
8use std::thread;
9use std::time::Duration;
10
11/// Controls which output streams are printed to the console.
12///
13/// Used internally by [`JavaInfo::execute`] and its variants.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum OutputMode {
16    /// Print both stdout and stderr.
17    Both,
18    /// Print only stdout; stderr is discarded.
19    OutputOnly,
20    /// Print only stderr; stdout is discarded.
21    ErrorOnly,
22}
23
24impl JavaInfo {
25    /// Executes the Java executable with the given arguments, printing both
26    /// stdout and stderr to the console.
27    ///
28    /// The argument string is split using shell‑like rules (via `shell_words`).
29    /// The child process's stdout and stderr are captured and printed line by line
30    /// while the process runs.
31    ///
32    /// # Errors
33    ///
34    /// Returns `JavaError::IoError` if spawning or waiting fails.
35    /// Returns `JavaError::Other` if the argument string cannot be parsed.
36    /// Returns `JavaError::ExecutionFailed` if the Java process exits with a non‑zero status.
37    ///
38    /// # Examples
39    ///
40    /// ```no_run
41    /// # use java_manager::JavaInfo;
42    /// # let java = JavaInfo::new("/path/to/java".into())?;
43    /// java.execute("-version")?;
44    /// # Ok::<_, java_manager::JavaError>(())
45    /// ```
46    pub fn execute(&self, args: &str) -> Result<(), JavaError> {
47        self.run_java(args, OutputMode::Both)
48    }
49
50    /// Executes the Java executable, printing only stderr to the console.
51    /// Stdout is captured and discarded.
52    ///
53    /// See [`execute`](JavaInfo::execute) for details.
54    ///
55    /// # Errors
56    ///
57    /// Same as [`execute`](JavaInfo::execute).
58    pub fn execute_with_error(&self, args: &str) -> Result<(), JavaError> {
59        self.run_java(args, OutputMode::ErrorOnly)
60    }
61
62    /// Executes the Java executable, printing only stdout to the console.
63    /// Stderr is captured and discarded.
64    ///
65    /// See [`execute`](JavaInfo::execute) for details.
66    ///
67    /// # Errors
68    ///
69    /// Same as [`execute`](JavaInfo::execute).
70    pub fn execute_with_output(&self, args: &str) -> Result<(), JavaError> {
71        self.run_java(args, OutputMode::OutputOnly)
72    }
73
74    /// Internal implementation of Java execution with configurable output.
75    fn run_java(&self, args: &str, mode: OutputMode) -> Result<(), JavaError> {
76        let java_exe = self.java_executable()?;
77
78        let arg_vec = shell_words::split(args)
79            .map_err(|e| JavaError::Other(format!("Failed to parse arguments: {}", e)))?;
80
81        let mut cmd = Command::new(java_exe);
82        cmd.args(&arg_vec);
83
84        cmd.stdout(Stdio::piped());
85        cmd.stderr(Stdio::piped());
86
87        let mut child = cmd.spawn().map_err(JavaError::IoError)?;
88
89        let stdout = child.stdout.take().expect("Failed to get stdout pipe");
90        let stderr = child.stderr.take().expect("Failed to get stderr pipe");
91
92        let stdout_handle = if matches!(mode, OutputMode::Both | OutputMode::OutputOnly) {
93            Some(thread::spawn(move || {
94                let reader = BufReader::new(stdout);
95                for line in reader.lines().map_while(Result::ok) {
96                    println!("{}", line);
97                }
98            }))
99        } else {
100            None
101        };
102
103        let stderr_handle = if matches!(mode, OutputMode::Both | OutputMode::ErrorOnly) {
104            Some(thread::spawn(move || {
105                let reader = BufReader::new(stderr);
106                for line in reader.lines().map_while(Result::ok) {
107                    eprintln!("{}", line);
108                }
109            }))
110        } else {
111            None
112        };
113
114        let status = child.wait().map_err(JavaError::IoError)?;
115
116        if let Some(handle) = stdout_handle {
117            handle.join().unwrap();
118        }
119        if let Some(handle) = stderr_handle {
120            handle.join().unwrap();
121        }
122
123        if status.success() {
124            Ok(())
125        } else {
126            Err(JavaError::ExecutionFailed(format!(
127                "Execution failed: {}",
128                status.code().unwrap()
129            )))
130        }
131    }
132
133    /// Returns the path to the `java` executable inside this installation's `JAVA_HOME/bin`.
134    ///
135    /// # Errors
136    ///
137    /// Returns `JavaError::NotFound` if the executable does not exist.
138    fn java_executable(&self) -> Result<PathBuf, JavaError> {
139        let java_home = &self.java_home;
140        let exe_name = if cfg!(windows) { "java.exe" } else { "java" };
141        let java_exe = java_home.join("bin").join(exe_name);
142        if java_exe.exists() {
143            Ok(java_exe)
144        } else {
145            Err(JavaError::NotFound(format!(
146                "Java executable not found: {:?}",
147                java_exe
148            )))
149        }
150    }
151}
152
153/// A builder for configuring and executing a Java program (JAR or main class).
154///
155/// This struct allows you to set the Java runtime, JAR file or main class,
156/// memory limits, program arguments, and I/O redirection before spawning the
157/// process.
158///
159/// # Examples
160///
161/// ```no_run
162/// use java_manager::{JavaRunner, JavaRedirect};
163///
164/// # let java = java_manager::java_home().unwrap();
165/// JavaRunner::new()
166///     .java(java)
167///     .jar("myapp.jar")
168///     .min_memory(256 * 1024 * 1024)   // 256 MB
169///     .max_memory(1024 * 1024 * 1024)  // 1 GB
170///     .arg("--server")
171///     .redirect(JavaRedirect::new().output("out.log").error("err.log"))
172///     .execute()?;
173/// # Ok::<_, java_manager::JavaError>(())
174/// ```
175#[derive(Debug, Default)]
176pub struct JavaRunner {
177    java: Option<JavaInfo>,
178    jar: Option<PathBuf>,
179    min_memory: Option<String>,
180    max_memory: Option<String>,
181    main_class: Option<String>,
182    args: Vec<String>,
183    redirect: JavaRedirect,
184    classpath: Option<String>,
185    module_path: Option<PathBuf>,
186    add_opens: Vec<String>,
187    add_exports: Vec<String>,
188    system_properties: Vec<String>,
189    env_vars: Vec<(String, String)>,
190    working_dir: Option<PathBuf>,
191    timeout: Option<Duration>,
192}
193
194/// I/O redirection options for a Java process.
195///
196/// Use the builder methods to specify files for stdout, stderr, and stdin.
197/// If a stream is not redirected, it will inherit the parent's corresponding
198/// stream (i.e., print to console or read from keyboard).
199#[derive(Debug, Default)]
200pub struct JavaRedirect {
201    output: Option<PathBuf>,
202    error: Option<PathBuf>,
203    input: Option<PathBuf>,
204    append_output: bool,
205    append_error: bool,
206}
207
208impl JavaRedirect {
209    /// Creates a new empty redirection configuration.
210    pub fn new() -> Self {
211        Self::default()
212    }
213
214    /// Redirects the Java process's standard output to the given file.
215    /// The file will be created (or truncated) before execution.
216    pub fn output(mut self, path: impl AsRef<Path>) -> Self {
217        self.output = Some(path.as_ref().to_path_buf());
218        self
219    }
220
221    /// Redirects the Java process's standard error to the given file.
222    /// The file will be created (or truncated) before execution.
223    pub fn error(mut self, path: impl AsRef<Path>) -> Self {
224        self.error = Some(path.as_ref().to_path_buf());
225        self
226    }
227
228    /// Redirects the Java process's standard input from the given file.
229    /// The file must exist and be readable.
230    pub fn input(mut self, path: impl AsRef<Path>) -> Self {
231        self.input = Some(path.as_ref().to_path_buf());
232        self
233    }
234
235    /// Append to the output file instead of truncating.
236    /// Only has an effect when [`output`](Self::output) is also set.
237    pub fn append_output(mut self) -> Self {
238        self.append_output = true;
239        self
240    }
241
242    /// Append to the error file instead of truncating.
243    /// Only has an effect when [`error`](Self::error) is also set.
244    pub fn append_error(mut self) -> Self {
245        self.append_error = true;
246        self
247    }
248}
249
250impl JavaRunner {
251    /// Creates a new builder with default settings.
252    pub fn new() -> Self {
253        Self::default()
254    }
255
256    /// Sets the Java installation to use.
257    ///
258    /// This is mandatory before calling `execute`.
259    pub fn java(mut self, java: JavaInfo) -> Self {
260        self.java = Some(java);
261        self
262    }
263
264    /// Sets the JAR file to execute (implies the `-jar` flag).
265    ///
266    /// Either `jar` or `main_class` must be set.
267    pub fn jar(mut self, jar: impl AsRef<Path>) -> Self {
268        self.jar = Some(jar.as_ref().to_path_buf());
269        self
270    }
271
272    /// Sets the initial heap size (`-Xms`).
273    ///
274    /// The value is given in bytes and will be formatted as a memory string
275    /// (e.g., `256m`, `1g`). If the size is not a multiple of a megabyte or gigabyte,
276    /// it will be rounded to the nearest megabyte.
277    pub fn min_memory(mut self, bytes: usize) -> Self {
278        self.min_memory = Some(format_memory(bytes));
279        self
280    }
281
282    /// Sets the maximum heap size (`-Xmx`).
283    ///
284    /// See [`min_memory`](JavaRunner::min_memory) for formatting details.
285    pub fn max_memory(mut self, bytes: usize) -> Self {
286        self.max_memory = Some(format_memory(bytes));
287        self
288    }
289
290    /// Sets the main class to execute (instead of a JAR file).
291    ///
292    /// Either `jar` or `main_class` must be set.
293    pub fn main_class(mut self, class: impl Into<String>) -> Self {
294        self.main_class = Some(class.into());
295        self
296    }
297
298    /// Adds a single argument to be passed to the Java program.
299    ///
300    /// Arguments are appended in the order they are added.
301    pub fn arg(mut self, arg: impl Into<String>) -> Self {
302        self.args.push(arg.into());
303        self
304    }
305
306    /// Sets I/O redirection options.
307    pub fn redirect(mut self, redirect: JavaRedirect) -> Self {
308        self.redirect = redirect;
309        self
310    }
311
312    /// Sets the classpath (`-cp` / `-classpath`).
313    ///
314    /// Paths are joined with the platform-specific separator (`;` on Windows, `:` otherwise).
315    pub fn classpath(mut self, paths: &[impl AsRef<Path>]) -> Self {
316        let separator = if cfg!(windows) { ";" } else { ":" };
317        let joined: Vec<String> = paths
318            .iter()
319            .map(|p| p.as_ref().to_string_lossy().to_string())
320            .collect();
321        self.classpath = Some(joined.join(separator));
322        self
323    }
324
325    /// Sets the module path (`--module-path`).
326    pub fn module_path(mut self, path: impl AsRef<Path>) -> Self {
327        self.module_path = Some(path.as_ref().to_path_buf());
328        self
329    }
330
331    /// Adds a `--add-opens` flag (e.g., `java.base/java.lang=ALL-UNNAMED`).
332    pub fn add_opens(
333        mut self,
334        module: impl Into<String>,
335        package: impl Into<String>,
336        target: impl Into<String>,
337    ) -> Self {
338        self.add_opens.push(format!(
339            "{}/{}.{}",
340            module.into(),
341            package.into(),
342            target.into()
343        ));
344        self
345    }
346
347    /// Adds a `--add-exports` flag (e.g., `java.base/com.sun.internal=ALL-UNNAMED`).
348    pub fn add_exports(
349        mut self,
350        module: impl Into<String>,
351        package: impl Into<String>,
352        target: impl Into<String>,
353    ) -> Self {
354        self.add_exports.push(format!(
355            "{}/{}.{}",
356            module.into(),
357            package.into(),
358            target.into()
359        ));
360        self
361    }
362
363    /// Sets a system property (`-Dkey=value`).
364    pub fn system_property(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
365        self.system_properties
366            .push(format!("-D{}={}", key.into(), value.into()));
367        self
368    }
369
370    /// Sets an environment variable for the child Java process.
371    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
372        self.env_vars.push((key.into(), value.into()));
373        self
374    }
375
376    /// Sets the working directory for the child Java process.
377    pub fn working_dir(mut self, path: impl AsRef<Path>) -> Self {
378        self.working_dir = Some(path.as_ref().to_path_buf());
379        self
380    }
381
382    /// Sets a timeout for the Java process.
383    ///
384    /// If the process runs longer than the specified duration, it will be killed.
385    /// A `Duration::ZERO` or `None` means no timeout.
386    pub fn timeout(mut self, duration: Duration) -> Self {
387        self.timeout = Some(duration);
388        self
389    }
390
391    /// Executes the configured Java program.
392    ///
393    /// # Errors
394    ///
395    /// Returns `JavaError::Other` if no Java installation has been set, or if
396    /// neither a JAR file nor a main class has been specified.
397    /// Returns `JavaError::NotFound` if the Java executable does not exist.
398    /// Returns `JavaError::IoError` if file operations or process spawning fail.
399    /// Returns `JavaError::ExecutionFailed` if the Java process exits with a non‑zero status.
400    pub fn execute(&self) -> Result<(), JavaError> {
401        let java = self.java.as_ref().ok_or_else(|| {
402            JavaError::Other("Must set Java environment via `.java(...)`".to_string())
403        })?;
404        let java_exe = java.java_executable()?;
405
406        let mut cmd = Command::new(java_exe);
407
408        // Classpath
409        if let Some(cp) = &self.classpath {
410            cmd.arg("-cp");
411            cmd.arg(cp);
412        }
413
414        // Module path
415        if let Some(mp) = &self.module_path {
416            cmd.arg("--module-path");
417            cmd.arg(mp);
418        }
419
420        // --add-opens
421        for open in &self.add_opens {
422            cmd.arg("--add-opens");
423            cmd.arg(open);
424        }
425
426        // --add-exports
427        for export in &self.add_exports {
428            cmd.arg("--add-exports");
429            cmd.arg(export);
430        }
431
432        // System properties
433        for prop in &self.system_properties {
434            cmd.arg(prop);
435        }
436
437        if let Some(min) = &self.min_memory {
438            cmd.arg(format!("-Xms{}", min));
439        }
440        if let Some(max) = &self.max_memory {
441            cmd.arg(format!("-Xmx{}", max));
442        }
443
444        if let Some(jar) = &self.jar {
445            cmd.arg("-jar");
446            cmd.arg(jar);
447        } else if let Some(main) = &self.main_class {
448            cmd.arg(main);
449        } else {
450            return Err(JavaError::Other(
451                "Must specify JAR file or main class".into(),
452            ));
453        }
454
455        cmd.args(&self.args);
456
457        // Environment variables
458        for (key, value) in &self.env_vars {
459            cmd.env(key, value);
460        }
461
462        // Working directory
463        if let Some(dir) = &self.working_dir {
464            cmd.current_dir(dir);
465        }
466
467        // Configure redirection
468        if let Some(output) = &self.redirect.output {
469            let file = if self.redirect.append_output {
470                OpenOptions::new().append(true).create(true).open(output)
471            } else {
472                File::create(output)
473            }
474            .map_err(JavaError::IoError)?;
475            cmd.stdout(Stdio::from(file));
476        } else {
477            cmd.stdout(Stdio::inherit());
478        }
479
480        if let Some(error) = &self.redirect.error {
481            let file = if self.redirect.append_error {
482                OpenOptions::new().append(true).create(true).open(error)
483            } else {
484                File::create(error)
485            }
486            .map_err(JavaError::IoError)?;
487            cmd.stderr(Stdio::from(file));
488        } else {
489            cmd.stderr(Stdio::inherit());
490        }
491
492        if let Some(input) = &self.redirect.input {
493            let file = File::open(input).map_err(JavaError::IoError)?;
494            cmd.stdin(Stdio::from(file));
495        } else {
496            cmd.stdin(Stdio::inherit());
497        }
498
499        // Timeout handling
500        if let Some(timeout) = self.timeout {
501            let mut child = cmd.spawn().map_err(JavaError::IoError)?;
502            let start = std::time::Instant::now();
503            loop {
504                if child.try_wait().map_err(JavaError::IoError)?.is_some() {
505                    // Process completed within the timeout
506                    let status = child.wait().map_err(JavaError::IoError)?;
507                    return if status.success() {
508                        Ok(())
509                    } else {
510                        Err(JavaError::ExecutionFailed(format!(
511                            "Execution failed: {}",
512                            status.code().unwrap()
513                        )))
514                    };
515                }
516                if start.elapsed() >= timeout {
517                    // Timeout reached, kill the process
518                    kill_process(&child)?;
519                    return Err(JavaError::ExecutionFailed(format!(
520                        "Process timed out after {}ms",
521                        timeout.as_millis()
522                    )));
523                }
524                std::thread::sleep(Duration::from_millis(50));
525            }
526        }
527
528        let status = cmd.status().map_err(JavaError::IoError)?;
529
530        if status.success() {
531            Ok(())
532        } else {
533            Err(JavaError::ExecutionFailed(format!(
534                "Execution failed: {}",
535                status.code().unwrap()
536            )))
537        }
538    }
539}
540
541/// Formats a memory size in bytes into a Java‑compatible string (`<n>m` or `<n>g`).
542///
543/// If the size is an exact multiple of 1 GiB, it is formatted as `<n>g`.
544/// Otherwise, if it is an exact multiple of 1 MiB, it is formatted as `<n>m`.
545/// If neither, it is rounded to the nearest mebibyte and formatted as `<n>m`.
546fn format_memory(bytes: usize) -> String {
547    const MB: usize = 1024 * 1024;
548    const GB: usize = MB * 1024;
549
550    if bytes.is_multiple_of(GB) {
551        format!("{}g", bytes / GB)
552    } else if bytes.is_multiple_of(MB) {
553        format!("{}m", bytes / MB)
554    } else {
555        let mb = (bytes + MB / 2) / MB;
556        format!("{}m", mb)
557    }
558}
559
560fn kill_process(child: &std::process::Child) -> Result<(), JavaError> {
561    // The Child struct doesn't have a kill method on & reference,
562    // but we can use taskkill (Windows) or kill command (Unix)
563    let pid = child.id();
564    #[cfg(windows)]
565    let status = std::process::Command::new("taskkill")
566        .args(["/PID", &pid.to_string(), "/F"])
567        .status()
568        .map_err(JavaError::IoError)?;
569    #[cfg(not(windows))]
570    let status = std::process::Command::new("kill")
571        .args(["-9", &pid.to_string()])
572        .status()
573        .map_err(JavaError::IoError)?;
574
575    if status.success() {
576        Ok(())
577    } else {
578        Err(JavaError::Other(format!("Failed to kill process {}", pid)))
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585    use crate::JavaInfo;
586
587    #[test]
588    fn test_format_memory_exact_mb() {
589        assert_eq!(format_memory(256 * 1024 * 1024), "256m");
590    }
591
592    #[test]
593    fn test_format_memory_exact_gb() {
594        assert_eq!(format_memory(2 * 1024 * 1024 * 1024), "2g");
595    }
596
597    #[test]
598    fn test_format_memory_rounded() {
599        let result = format_memory(100 * 1024 * 1024 + 512 * 1024);
600        assert!(result.ends_with('m'));
601        let num: usize = result[..result.len() - 1].parse().unwrap();
602        assert!(num >= 100);
603    }
604
605    #[test]
606    fn test_format_memory_zero() {
607        assert_eq!(format_memory(0), "0g");
608    }
609
610    #[test]
611    fn test_format_memory_small() {
612        let result = format_memory(1);
613        assert_eq!(result, "0m");
614    }
615
616    #[test]
617    fn test_runner_missing_java() {
618        let result = JavaRunner::new().jar("test.jar").execute();
619        assert!(result.is_err());
620        let err = result.unwrap_err().to_string();
621        assert!(err.contains("Must set Java environment"));
622    }
623
624    #[test]
625    fn test_runner_missing_jar_or_main() {
626        // JavaInfo::default() has empty java_home, so java_executable() fails first
627        let info = JavaInfo::default();
628        let result = JavaRunner::new().java(info).execute();
629        assert!(result.is_err());
630    }
631
632    #[test]
633    fn test_java_redirect_default() {
634        let r = JavaRedirect::new();
635        assert!(r.output.is_none());
636        assert!(r.error.is_none());
637        assert!(r.input.is_none());
638        assert!(!r.append_output);
639        assert!(!r.append_error);
640    }
641
642    #[test]
643    fn test_java_redirect_append() {
644        let r = JavaRedirect::new()
645            .output("out.log")
646            .append_output()
647            .error("err.log")
648            .append_error();
649        assert!(r.append_output);
650        assert!(r.append_error);
651    }
652
653    #[test]
654    fn test_runner_builder_methods() {
655        let runner = JavaRunner::new()
656            .system_property("foo", "bar")
657            .add_opens("java.base", "java.lang", "ALL-UNNAMED")
658            .add_exports("java.base", "com.sun.internal", "ALL-UNNAMED");
659        assert_eq!(runner.system_properties, vec!["-Dfoo=bar"]);
660        assert_eq!(runner.add_opens, vec!["java.base/java.lang.ALL-UNNAMED"]);
661        assert_eq!(
662            runner.add_exports,
663            vec!["java.base/com.sun.internal.ALL-UNNAMED"]
664        );
665    }
666
667    #[test]
668    fn test_output_mode_debug_clone() {
669        let mode = OutputMode::Both;
670        let cloned = mode;
671        assert_eq!(mode, cloned);
672        let _ = format!("{:?}", mode);
673    }
674
675    #[test]
676    fn test_format_memory_1gb_plus_1b() {
677        let result = format_memory(1024 * 1024 * 1024 + 1);
678        assert_eq!(result, "1024m");
679    }
680
681    #[test]
682    fn test_format_memory_1_mib() {
683        assert_eq!(format_memory(1024 * 1024), "1m");
684    }
685
686    #[test]
687    fn test_format_memory_1024_mib_is_1g() {
688        assert_eq!(format_memory(1024 * 1024 * 1024), "1g");
689    }
690
691    #[test]
692    fn test_runner_env_var() {
693        let runner = JavaRunner::new().env("MY_VAR", "my_value");
694        assert_eq!(runner.env_vars, vec![("MY_VAR".into(), "my_value".into())]);
695    }
696
697    #[test]
698    fn test_runner_working_dir() {
699        let runner = JavaRunner::new().working_dir("/tmp");
700        assert_eq!(runner.working_dir, Some(PathBuf::from("/tmp")));
701    }
702
703    #[test]
704    fn test_runner_timeout() {
705        let runner = JavaRunner::new().timeout(Duration::from_secs(30));
706        assert_eq!(runner.timeout, Some(Duration::from_secs(30)));
707    }
708
709    #[test]
710    fn test_runner_classpath() {
711        let separator = if cfg!(windows) { ";" } else { ":" };
712        let runner = JavaRunner::new().classpath(&["lib/a.jar", "config"]);
713        assert_eq!(
714            runner.classpath,
715            Some(format!("lib/a.jar{separator}config"))
716        );
717    }
718
719    #[test]
720    fn test_runner_module_path() {
721        let runner = JavaRunner::new().module_path("./modules");
722        assert_eq!(runner.module_path, Some(PathBuf::from("./modules")));
723    }
724
725    #[test]
726    fn test_runner_multiple_args() {
727        let runner = JavaRunner::new().arg("--verbose").arg("--debug");
728        assert_eq!(runner.args, vec!["--verbose", "--debug"]);
729    }
730
731    #[test]
732    fn test_runner_all_builder_methods() {
733        let runner = JavaRunner::new()
734            .classpath(&["lib/*"])
735            .module_path("mods")
736            .add_opens("java.base", "java.lang", "ALL-UNNAMED")
737            .add_exports("java.base", "sun.security", "ALL-UNNAMED")
738            .system_property("key", "val")
739            .env("HOME", "/root")
740            .working_dir("/app")
741            .timeout(Duration::from_secs(10))
742            .min_memory(256 * 1024 * 1024)
743            .max_memory(1024 * 1024 * 1024);
744
745        assert!(runner.classpath.is_some());
746        assert!(runner.module_path.is_some());
747        assert_eq!(runner.add_opens.len(), 1);
748        assert_eq!(runner.add_exports.len(), 1);
749        assert_eq!(runner.system_properties.len(), 1);
750        assert_eq!(runner.env_vars.len(), 1);
751        assert!(runner.working_dir.is_some());
752        assert_eq!(runner.timeout, Some(Duration::from_secs(10)));
753        assert!(runner.min_memory.is_some());
754        assert!(runner.max_memory.is_some());
755    }
756}