From 4e05d0af9d32f0cb5de614b92ac3735c2aad5384 Mon Sep 17 00:00:00 2001 From: Defelo Date: Mon, 8 May 2023 21:30:19 +0200 Subject: [PATCH 01/11] Fix coverage reporting in integration tests --- nix/dev/shell.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nix/dev/shell.nix b/nix/dev/shell.nix index 6edae0d..595c8f6 100644 --- a/nix/dev/shell.nix +++ b/nix/dev/shell.nix @@ -63,14 +63,16 @@ }; test-script = pkgs.writeShellScript "integration-tests.sh" '' rm -rf programs jobs - cargo llvm-cov run -r --locked --lcov --output-path lcov.info -F test_api & + cargo llvm-cov run --lcov --output-path lcov-server.info --release --locked -F test_api & pid=$! while ! ${pkgs.curl}/bin/curl -so/dev/null localhost:8000; do sleep 1 done - cargo test --locked --all-features --all-targets --no-fail-fast -- --ignored + cargo llvm-cov test --lcov --output-path lcov-tests.info --locked --all-features --all-targets --no-fail-fast -- --include-ignored out=$? ${pkgs.curl}/bin/curl -X POST localhost:8000/test/exit + wait $pid + ${pkgs.lcov}/bin/lcov -a lcov-server.info -a lcov-tests.info -o lcov.info exit $out ''; scripts = pkgs.stdenv.mkDerivation { @@ -80,7 +82,7 @@ }; in { default = pkgs.mkShell ({ - packages = [pkgs.nsjail pkgs.cargo-llvm-cov time scripts]; + packages = [pkgs.nsjail pkgs.cargo-llvm-cov pkgs.lcov time scripts]; RUST_LOG = "info,sandkasten=trace,difft=off"; } // test-env); From c2da4137dbf396f07ecd67927bdb5e52321cd8b4 Mon Sep 17 00:00:00 2001 From: Defelo Date: Mon, 8 May 2023 21:30:37 +0200 Subject: [PATCH 02/11] Add proptest cases parameter to integration-tests script --- nix/dev/shell.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/dev/shell.nix b/nix/dev/shell.nix index 595c8f6..bf3e0d9 100644 --- a/nix/dev/shell.nix +++ b/nix/dev/shell.nix @@ -62,6 +62,7 @@ })); }; test-script = pkgs.writeShellScript "integration-tests.sh" '' + export PROPTEST_CASES=''${1:-256} rm -rf programs jobs cargo llvm-cov run --lcov --output-path lcov-server.info --release --locked -F test_api & pid=$! From 030982c175644083ba593078785726e217024e44 Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 15:01:56 +0200 Subject: [PATCH 03/11] Add cov script to dev shell --- .gitignore | 2 ++ README.md | 3 ++- nix/dev/shell.nix | 11 ++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6378b25..bbe6a41 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /result /box /program +/lcov*.info +/lcov_html diff --git a/README.md b/README.md index 655498b..c25fc23 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,8 @@ need to have a Sandkasten instance running on `127.0.0.1:8000`. You can also spe instance via the `TARGET` environment variable. If you only want to run the integration tests that do not require a nix development shell, you can omit the `-F nix`. In the development shell you can also run the `integration-tests` command to automatically start a temporary sandkasten instance and -run the integration tests against it. +run the integration tests against it. There is also a `cov` command that runs the integration tests +and writes an html coverage report to `lcov_html/index.html`. ### Packages All packages are defined using nix expressions in diff --git a/nix/dev/shell.nix b/nix/dev/shell.nix index bf3e0d9..5525c37 100644 --- a/nix/dev/shell.nix +++ b/nix/dev/shell.nix @@ -76,10 +76,19 @@ ${pkgs.lcov}/bin/lcov -a lcov-server.info -a lcov-tests.info -o lcov.info exit $out ''; + cov = pkgs.writeShellScript "cov.sh" '' + rm -rf lcov*.info lcov_html + ${test-script} "''${1:-16}" + ${pkgs.lcov}/bin/genhtml -o lcov_html lcov.info + ''; scripts = pkgs.stdenv.mkDerivation { name = "scripts"; unpackPhase = "true"; - installPhase = "mkdir -p $out/bin && ln -s ${test-script} $out/bin/integration-tests"; + installPhase = '' + mkdir -p $out/bin \ + && ln -s ${test-script} $out/bin/integration-tests \ + && ln -s ${cov} $out/bin/cov + ''; }; in { default = pkgs.mkShell ({ From 039a8f0e4e252390f3492be6d380b5281db48d1c Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 15:16:33 +0200 Subject: [PATCH 04/11] Add async_client tests --- tests/async_client.rs | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 tests/async_client.rs diff --git a/tests/async_client.rs b/tests/async_client.rs new file mode 100644 index 0000000..c807c8d --- /dev/null +++ b/tests/async_client.rs @@ -0,0 +1,73 @@ +use sandkasten_client::{ + schemas::programs::{BuildRequest, BuildRunRequest, File}, + SandkastenClient, +}; + +#[tokio::test] +#[ignore] +async fn test_environments() { + let environments = client().list_environments().await.unwrap(); + assert_eq!(environments.get("python").unwrap().name, "Python"); + assert_eq!(environments.get("rust").unwrap().name, "Rust"); +} + +#[tokio::test] +#[ignore] +async fn test_build_run() { + let result = client() + .build_and_run(&BuildRunRequest { + build: BuildRequest { + environment: "rust".into(), + files: vec![File { + name: "test.rs".into(), + content: "fn main() { print!(\"Hello World!\"); }".into(), + }], + ..Default::default() + }, + run: Default::default(), + }) + .await + .unwrap(); + assert_eq!(result.build.unwrap().status, 0); + assert_eq!(result.run.status, 0); + assert_eq!(result.run.stdout, "Hello World!"); + assert!(result.run.stderr.is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_build_then_run() { + let result = client() + .build(&BuildRequest { + environment: "rust".into(), + files: vec![File { + name: "test.rs".into(), + content: "fn main() { print!(\"Hello World!\"); }".into(), + }], + ..Default::default() + }) + .await + .unwrap(); + + let build = result.compile_result.unwrap(); + assert_eq!(build.status, 0); + assert!(build.stdout.is_empty()); + assert!(build.stderr.is_empty()); + + let result = client() + .run(result.program_id, &Default::default()) + .await + .unwrap(); + assert_eq!(result.status, 0); + assert_eq!(result.stdout, "Hello World!"); + assert!(result.stderr.is_empty()); +} + +fn client() -> SandkastenClient { + SandkastenClient::new( + option_env!("TARGET_HOST") + .unwrap_or("http://127.0.0.1:8000") + .parse() + .unwrap(), + ) +} From 2535b4c8885b070bedc1a4cac277b8fbd15d134b Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 15:48:02 +0200 Subject: [PATCH 05/11] Add api error tests --- tests/api.rs | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 257 insertions(+), 5 deletions(-) diff --git a/tests/api.rs b/tests/api.rs index 370df63..e553dc0 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -1,10 +1,13 @@ use indoc::formatdoc; -use sandkasten_client::schemas::{ - programs::{ - BuildRequest, BuildRunError, BuildRunRequest, BuildRunResult, EnvVar, File, RunRequest, - RunResult, +use sandkasten_client::{ + schemas::{ + programs::{ + BuildError, BuildRequest, BuildRunError, BuildRunRequest, BuildRunResult, EnvVar, File, + LimitsOpt, RunError, RunRequest, RunResult, + }, + ErrorResponse, }, - ErrorResponse, + Error, }; use crate::common::client; @@ -220,3 +223,252 @@ fn test_build_then_run() { assert_eq!(run.status, 0); assert_eq!(run.stdout, "hello world\n"); } + +#[test] +#[ignore] +fn test_build_run_errors() { + let client = client(); + + let Error::ErrorResponse(err) = client + .build_and_run(&BuildRunRequest { + build: BuildRequest { + environment: "this_environment_does_not_exist".into(), + files: vec![File { + name: "test".into(), + content: "".into(), + }], + ..Default::default() + }, + run: Default::default(), + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(BuildRunError::EnvironmentNotFound) + )); + + let Error::ErrorResponse(err) = client + .build_and_run(&BuildRunRequest { + build: BuildRequest { + environment: "python".into(), + files: vec![File { + name: ".".into(), + content: "".into(), + }], + ..Default::default() + }, + run: Default::default(), + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(BuildRunError::InvalidFileNames) + )); + + let Error::ErrorResponse(err) = client + .build_and_run(&BuildRunRequest { + build: BuildRequest { + environment: "python".into(), + files: vec![File { + name: "test.py".into(), + content: "".into(), + }], + env_vars: vec![EnvVar {name: "_".into(), value: "".into()}], + ..Default::default() + }, + run: Default::default(), + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(BuildRunError::InvalidEnvVars) + )); + + let Error::ErrorResponse(err) = client + .build_and_run(&BuildRunRequest { + build: BuildRequest { + environment: "rust".into(), + files: vec![File { + name: "test.rs".into(), + content: "fn main() {}".into(), + }], + compile_limits: LimitsOpt {cpus: Some(4096), ..Default::default()}, + ..Default::default() + }, + run: Default::default(), + }) + .unwrap_err() else { panic!() }; + let ErrorResponse::Inner(BuildRunError::CompileLimitsExceeded(mut les)) = *err else {panic!()}; + let le = les.pop().unwrap(); + assert_eq!(le.name, "cpus"); + assert_eq!(le.max_value, 1); + assert!(les.pop().is_none()); + + let Error::ErrorResponse(err) = client + .build_and_run(&BuildRunRequest { + build: BuildRequest { + environment: "rust".into(), + files: vec![File { + name: "test.rs".into(), + content: "fn main() {}".into(), + }], + ..Default::default() + }, + run: RunRequest { + run_limits: LimitsOpt { + time: Some(65536), ..Default::default() + }, + ..Default::default() + }, + }) + .unwrap_err() else { panic!() }; + let ErrorResponse::Inner(BuildRunError::RunLimitsExceeded(mut les)) = *err else {panic!()}; + let le = les.pop().unwrap(); + assert_eq!(le.name, "time"); + assert_eq!(le.max_value, 5); + assert!(les.pop().is_none()); +} + +#[test] +#[ignore] +fn test_build_errors() { + let client = client(); + + let Error::ErrorResponse(err) = client + .build(&BuildRequest { + environment: "this_environment_does_not_exist".into(), + files: vec![File { + name: "test".into(), + content: "".into(), + }], + ..Default::default() + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(BuildError::EnvironmentNotFound) + )); + + let Error::ErrorResponse(err) = client + .build(&BuildRequest { + environment: "python".into(), + files: vec![File { + name: ".".into(), + content: "".into(), + }], + ..Default::default() + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(BuildError::InvalidFileNames) + )); + + let Error::ErrorResponse(err) = client + .build(&BuildRequest { + environment: "python".into(), + files: vec![File { + name: "test.py".into(), + content: "".into(), + }], + env_vars: vec![EnvVar { + name: "_".into(), + value: "".into(), + }], + ..Default::default() + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(BuildError::InvalidEnvVars) + )); + + let Error::ErrorResponse(err) = client + .build(&BuildRequest { + environment: "rust".into(), + files: vec![File { + name: "test.rs".into(), + content: "fn main() {}".into(), + }], + compile_limits: LimitsOpt { + cpus: Some(4096), + ..Default::default() + }, + ..Default::default() + }) + .unwrap_err() else { panic!() }; + let ErrorResponse::Inner(BuildError::CompileLimitsExceeded(mut les)) = *err else {panic!()}; + let le = les.pop().unwrap(); + assert_eq!(le.name, "cpus"); + assert_eq!(le.max_value, 1); + assert!(les.pop().is_none()); +} + +#[test] +#[ignore] +fn test_run_errors() { + let client = client(); + + let program_id = client + .build(&BuildRequest { + environment: "python".into(), + files: vec![File { + name: "test.py".into(), + content: "print('Hello World')".into(), + }], + ..Default::default() + }) + .unwrap() + .program_id; + + let Error::ErrorResponse(err) = client + .run("00000000-0000-0000-0000-000000000000", &Default::default()) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(RunError::ProgramNotFound) + )); + + let Error::ErrorResponse(err) = client + .run(program_id, &RunRequest { + files: vec![File { + name: ".".into(), + content: "".into(), + }], + ..Default::default() + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(RunError::InvalidFileNames) + )); + + let Error::ErrorResponse(err) = client + .run(program_id, &RunRequest { + env_vars: vec![EnvVar { + name: "_".into(), + value: "".into(), + }], + ..Default::default() + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(RunError::InvalidEnvVars) + )); + + let Error::ErrorResponse(err) = client + .run(program_id, &RunRequest { + run_limits: LimitsOpt { + cpus: Some(4096), + ..Default::default() + }, + ..Default::default() + }) + .unwrap_err() else { panic!() }; + let ErrorResponse::Inner(RunError::RunLimitsExceeded(mut les)) = *err else {panic!()}; + let le = les.pop().unwrap(); + assert_eq!(le.name, "cpus"); + assert_eq!(le.max_value, 1); + assert!(les.pop().is_none()); +} From 20461100fa8dd578aa69a1e41d2060ed5a12b49b Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 18:46:49 +0200 Subject: [PATCH 06/11] Add network test --- nix/dev/shell.nix | 1 + tests/api.rs | 29 +++++++++++++++++++++++++++++ tests/attacks.rs | 8 +++++++- 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/nix/dev/shell.nix b/nix/dev/shell.nix index 5525c37..defd857 100644 --- a/nix/dev/shell.nix +++ b/nix/dev/shell.nix @@ -59,6 +59,7 @@ jobs_dir = "jobs"; program_ttl = 60; prune_programs_interval = 30; + run_limits = config.run_limits // {network = true;}; })); }; test-script = pkgs.writeShellScript "integration-tests.sh" '' diff --git a/tests/api.rs b/tests/api.rs index e553dc0..2703401 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -1,4 +1,5 @@ use indoc::formatdoc; +use regex::Regex; use sandkasten_client::{ schemas::{ programs::{ @@ -472,3 +473,31 @@ fn test_run_errors() { assert_eq!(le.max_value, 1); assert!(les.pop().is_none()); } + +#[test] +#[ignore] +fn test_network() { + let result = client() + .build_and_run(&BuildRunRequest { + build: BuildRequest { + environment: "python".into(), + files: vec![File { + name: "test.py".into(), + content: formatdoc! {r#" + from http.client import * + c=HTTPConnection("ip6.me") + c.request("GET", "http://ip6.me/api/") + r=c.getresponse() + print(r.status, r.read().decode().strip(), end='') + "#}, + }], + ..Default::default() + }, + run: Default::default(), + }) + .unwrap(); + assert_eq!(result.run.status, 0); + let re = Regex::new(r"^200 IPv[46],[^,]+,.+$").unwrap(); + assert!(re.is_match(&result.run.stdout)); + assert!(result.run.stderr.is_empty()); +} diff --git a/tests/attacks.rs b/tests/attacks.rs index fba104c..3c0f37a 100644 --- a/tests/attacks.rs +++ b/tests/attacks.rs @@ -25,7 +25,13 @@ fn test_no_internet() { env_vars: vec![], compile_limits: Default::default(), }, - run: Default::default(), + run: RunRequest { + run_limits: LimitsOpt { + network: Some(false), + ..Default::default() + }, + ..Default::default() + }, }) .unwrap(); assert_eq!(response.run.status, 1); From 7738b17b060ccf8a4c014d7a4c2edf492ba05891 Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 19:58:31 +0200 Subject: [PATCH 07/11] Fix program id hashing --- src/program.rs | 2 +- tests/api.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/program.rs b/src/program.rs index d5bad18..65406b4 100644 --- a/src/program.rs +++ b/src/program.rs @@ -39,7 +39,7 @@ pub async fn build_program( &env.version, &env.compile_script, &data.files, - &data.environment, + &data.env_vars, ))?) .finalize(); let id = Uuid::from_u128( diff --git a/tests/api.rs b/tests/api.rs index 2703401..fe4b7b4 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -293,8 +293,11 @@ fn test_build_run_errors() { name: "test.rs".into(), content: "fn main() {}".into(), }], + env_vars: vec![EnvVar { + name: "x".into(), + value: uuid::Uuid::new_v4().to_string(), + }], compile_limits: LimitsOpt {cpus: Some(4096), ..Default::default()}, - ..Default::default() }, run: Default::default(), }) @@ -391,11 +394,14 @@ fn test_build_errors() { name: "test.rs".into(), content: "fn main() {}".into(), }], + env_vars: vec![EnvVar { + name: "x".into(), + value: uuid::Uuid::new_v4().to_string(), + }], compile_limits: LimitsOpt { cpus: Some(4096), ..Default::default() }, - ..Default::default() }) .unwrap_err() else { panic!() }; let ErrorResponse::Inner(BuildError::CompileLimitsExceeded(mut les)) = *err else {panic!()}; From bcfe53c5c82a00d0a283df4b90267f3334daa297 Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 19:59:39 +0200 Subject: [PATCH 08/11] Fix and test build race condition --- src/program.rs | 9 +++++++++ tests/api.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/program.rs b/src/program.rs index 65406b4..306aed5 100644 --- a/src/program.rs +++ b/src/program.rs @@ -67,6 +67,15 @@ pub async fn build_program( fs::write(path.join("compile_result"), serialized).await?; } fs::write(path.join("ok"), []).await?; + fs::write( + path.join("last_run"), + time::SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(), + ) + .await?; Ok(( BuildResult { program_id: id, diff --git a/tests/api.rs b/tests/api.rs index fe4b7b4..f2a43bf 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use indoc::formatdoc; use regex::Regex; use sandkasten_client::{ @@ -507,3 +509,55 @@ fn test_network() { assert!(re.is_match(&result.run.stdout)); assert!(result.run.stderr.is_empty()); } + +#[test] +#[ignore] +fn test_build_race() { + let client = Arc::new(client()); + + for _ in 0..16 { + let x = uuid::Uuid::new_v4(); + let threads = (0..256) + .map(|i| { + let client = Arc::clone(&client); + std::thread::spawn(move || { + client.build(&BuildRequest { + environment: "rust".into(), + files: vec![File { + name: "test.rs".into(), + content: "fn main() { println!(\"hi there\"); }".into(), + }], + env_vars: vec![EnvVar { + name: "x".into(), + value: format!("{x} {}", i / 64), + }], + ..Default::default() + }) + }) + }) + .collect::>(); + let results = threads + .into_iter() + .map(|t| t.join().unwrap().unwrap()) + .collect::>(); + let res = results.first().unwrap(); + assert!(results + .iter() + .take(64) + .all(|r| r.program_id == res.program_id)); + assert!(results + .iter() + .all(|r| r.compile_result.as_ref().unwrap().status == 0)); + assert!(results + .iter() + .all(|r| r.compile_result.as_ref().unwrap().stdout.is_empty())); + assert!(results + .iter() + .all(|r| r.compile_result.as_ref().unwrap().stderr.is_empty())); + + let run = client.run(res.program_id, &Default::default()).unwrap(); + assert_eq!(run.status, 0); + assert_eq!(run.stdout, "hi there\n"); + assert!(run.stderr.is_empty()); + } +} From 7e1fe86dd6237d0800ac7a2baa0ab9acbee531e2 Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 21:55:19 +0200 Subject: [PATCH 09/11] Remove unused delete_program function --- src/program.rs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/program.rs b/src/program.rs index 306aed5..bbb7f9f 100644 --- a/src/program.rs +++ b/src/program.rs @@ -173,16 +173,6 @@ pub async fn run_program( .await??) } -/// Delete a program's directly and all its contents. -pub async fn delete_program(config: &Config, program_id: Uuid) -> Result<(), DeleteProgramError> { - let path = config.programs_dir.join(program_id.to_string()); - if !fs::try_exists(&path).await? { - return Err(DeleteProgramError::ProgramNotFound); - } - fs::remove_dir_all(path).await?; - Ok(()) -} - pub async fn prune_programs( config: &Config, program_lock: Arc>, @@ -273,14 +263,6 @@ pub enum RunProgramError { LimitsExceeded(Vec), } -#[derive(Debug, Error)] -pub enum DeleteProgramError { - #[error("program does not exist")] - ProgramNotFound, - #[error("io error: {0}")] - IOError(#[from] std::io::Error), -} - async fn store_program( config: &Config, environments: &Environments, From 09abfae70823c46cf7ea5d59cff7ba778cae0cad Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 22:10:40 +0200 Subject: [PATCH 10/11] Add compile error case to build errors test --- tests/api.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/api.rs b/tests/api.rs index f2a43bf..7736340 100644 --- a/tests/api.rs +++ b/tests/api.rs @@ -355,6 +355,21 @@ fn test_build_errors() { ErrorResponse::Inner(BuildError::EnvironmentNotFound) )); + let Error::ErrorResponse(err) = client + .build(&BuildRequest { + environment: "rust".into(), + files: vec![File { + name: "test.rs".into(), + content: "".into(), + }], + ..Default::default() + }) + .unwrap_err() else { panic!() }; + assert!(matches!( + *err, + ErrorResponse::Inner(BuildError::CompileError(_)) + )); + let Error::ErrorResponse(err) = client .build(&BuildRequest { environment: "python".into(), From dd090930af7c31e72eea643ce76ea09583ea3b85 Mon Sep 17 00:00:00 2001 From: Defelo Date: Tue, 9 May 2023 22:27:43 +0200 Subject: [PATCH 11/11] Fix error on compile limits exceeded --- src/program.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/program.rs b/src/program.rs index bbb7f9f..4fdb7a5 100644 --- a/src/program.rs +++ b/src/program.rs @@ -86,8 +86,10 @@ pub async fn build_program( )) } Err(err) => { - if let Err(err) = fs::remove_dir_all(&path).await { - error!("could not remove program directory {path:?}: {err}"); + if fs::try_exists(&path).await? { + if let Err(err) = fs::remove_dir_all(&path).await { + error!("could not remove program directory {path:?}: {err}"); + } } Err(err) }