Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/tower-runtime/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ pub enum Error {

#[snafu(display("cancelled"))]
Cancelled,

#[snafu(display("dependency installation failed"))]
DependencyInstallationFailed,
}

impl From<std::io::Error> for Error {
Expand Down
16 changes: 14 additions & 2 deletions crates/tower-runtime/src/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,13 @@ async fn execute_local_app(
));

// Wait for venv to finish up.
wait_for_process(ctx.clone(), &cancel_token, child).await;
let res = wait_for_process(ctx.clone(), &cancel_token, child).await;

if res != 0 {
// If the venv process failed, we want to return an error.
let _ = sx.send(res);
return Err(Error::VirtualEnvCreationFailed);
}

// Check once more if the process was cancelled before we do a uv sync. The sync itself,
// once started, will take a while and we have logic for checking for cancellation.
Expand Down Expand Up @@ -269,7 +275,13 @@ async fn execute_local_app(
));

// Let's wait for the setup to finish. We don't care about the results.
wait_for_process(ctx.clone(), &cancel_token, child).await;
let res = wait_for_process(ctx.clone(), &cancel_token, child).await;

if res != 0 {
// If the sync process failed, we want to return an error.
let _ = sx.send(res);
return Err(Error::DependencyInstallationFailed);
}
}
}

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[app]
name = "05-broken-dependencies"
script = "./main.py"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
print("This should never run because dependency installation should fail")
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "05-broken-dependencies"
version = "0.1.0"
description = "App with a non-existent dependency to test failure handling"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"this-package-definitely-does-not-exist-xyz-123456789>=1.0.0",
]
47 changes: 47 additions & 0 deletions crates/tower-runtime/tests/local_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,50 @@ async fn test_running_app_with_secret() {
let status = app.status().await.expect("Failed to get app status");
assert!(status == Status::Exited, "App should be running");
}

#[tokio::test]
async fn test_abort_on_dependency_installation_failure() {
debug!("Running 05-broken-dependencies");
// This test verifies that when dependency installation fails (uv sync returns non-zero),
// the app correctly reports a Crashed status rather than continuing execution.
let broken_deps_dir = get_example_app_dir("05-broken-dependencies");
let package = build_package_from_dir(&broken_deps_dir).await;
let (sender, mut receiver) = unbounded_channel();

let opts = StartOptions {
ctx: tower_telemetry::Context::new(),
package,
output_sender: sender,
cwd: None,
environment: "local".to_string(),
secrets: HashMap::new(),
parameters: HashMap::new(),
env_vars: HashMap::new(),
cache_dir: Some(config::default_cache_dir()),
};

// Start the app using the LocalApp runtime
let app = LocalApp::start(opts).await.expect("Failed to start app");

// Drain all output - we need to consume the channel for the app to complete
while let Some(output) = receiver.recv().await {
debug!("Received output: {:?}", output.line);
}

// The status should be Crashed since dependency installation failed
let status = app.status().await.expect("Failed to get app status");
match status {
Status::Crashed { code } => {
assert!(code != 0, "Exit code should be non-zero, got {}", code);
}
Status::Exited => {
panic!("App should have crashed due to dependency installation failure, not exited successfully");
}
Status::Running => {
panic!("App should not still be running");
}
Status::None => {
panic!("App should have a status");
}
}
}