#![cfg(all(e2e_test, unix))]
mod common;
use common::{
REGION, TestHelper, create_sized_file, create_temp_dir, generate_bucket_name, s7cmd_cmd,
};
use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;
use std::process::Stdio;
use std::time::Duration;
const STARTUP_DELAYS_MS: [u64; 3] = [2000, 5000, 9000];
const WAIT_TIMEOUT_SECS: u64 = 30;
static SIGINT_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
async fn run_with_sigint(cmd: &mut std::process::Command) -> Option<i32> {
for (attempt, delay_ms) in STARTUP_DELAYS_MS.iter().enumerate() {
let mut child = cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn s7cmd");
tokio::time::sleep(Duration::from_millis(*delay_ms)).await;
let pid = Pid::from_raw(child.id() as i32);
let _ = kill(pid, Signal::SIGINT);
let wait_handle = tokio::task::spawn_blocking(move || child.wait());
let status =
match tokio::time::timeout(Duration::from_secs(WAIT_TIMEOUT_SECS), wait_handle).await {
Ok(Ok(Ok(status))) => status,
Ok(Ok(Err(e))) => panic!("child.wait() failed: {e}"),
Ok(Err(e)) => panic!("spawn_blocking join failed: {e}"),
Err(_) => panic!("child did not exit within {WAIT_TIMEOUT_SECS}s after SIGINT"),
};
match status.code() {
Some(code) => return Some(code),
None => {
eprintln!(
"SIGINT at {delay_ms}ms killed the child before its handler was installed \
(attempt {}/{}); retrying with a longer delay",
attempt + 1,
STARTUP_DELAYS_MS.len(),
);
}
}
}
None
}
#[tokio::test]
async fn cancel_sync_sigint_does_not_hang() {
let _serial = SIGINT_TEST_LOCK.lock().await;
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
helper
.put_object(&bucket, "big.bin", vec![0u8; 30 * 1024 * 1024])
.await;
let local_dir = create_temp_dir();
let source = format!("s3://{bucket}/");
let mut cmd = s7cmd_cmd();
cmd.args([
"sync",
"--source-profile",
"s7cmd-e2e-test",
"--source-region",
REGION,
"--rate-limit-bandwidth",
"2MiB",
&source,
local_dir.to_str().unwrap(),
]);
let _code = run_with_sigint(&mut cmd).await;
helper.delete_bucket_with_cascade(&bucket).await;
let _ = std::fs::remove_dir_all(&local_dir);
}
#[tokio::test]
async fn cancel_ls_sigint_does_not_hang() {
let _serial = SIGINT_TEST_LOCK.lock().await;
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
for i in 0..200 {
helper
.put_object(&bucket, &format!("k{i:04}"), b"x".to_vec())
.await;
}
let target = format!("s3://{bucket}/");
let mut cmd = s7cmd_cmd();
cmd.args([
"ls",
"--target-profile",
"s7cmd-e2e-test",
"--target-region",
REGION,
"--recursive",
"--rate-limit-api",
"10",
&target,
]);
let code = run_with_sigint(&mut cmd).await;
assert_ne!(
code,
Some(2),
"invocation must be valid enough to reach the listing"
);
helper.delete_bucket_with_cascade(&bucket).await;
}
#[tokio::test]
async fn cancel_clean_sigint_does_not_hang() {
let _serial = SIGINT_TEST_LOCK.lock().await;
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
for i in 0..200 {
helper
.put_object(&bucket, &format!("k{i:04}"), b"x".to_vec())
.await;
}
let target = format!("s3://{bucket}/");
let mut cmd = s7cmd_cmd();
cmd.args([
"clean",
"--target-profile",
"s7cmd-e2e-test",
"--target-region",
REGION,
"--force",
"--batch-size",
"10",
"--rate-limit-objects",
"10",
&target,
]);
let _code = run_with_sigint(&mut cmd).await;
helper.delete_bucket_with_cascade(&bucket).await;
}
#[tokio::test]
async fn cancel_cp_sigint_exits_130() {
let _serial = SIGINT_TEST_LOCK.lock().await;
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
let local_dir = create_temp_dir();
let big = create_sized_file(&local_dir, "big.bin", 30 * 1024 * 1024);
let target = format!("s3://{bucket}/big.bin");
let mut cmd = s7cmd_cmd();
cmd.args([
"cp",
"--target-profile",
"s7cmd-e2e-test",
"--target-region",
REGION,
"--rate-limit-bandwidth",
"2MiB",
big.to_str().unwrap(),
&target,
]);
let code = run_with_sigint(&mut cmd).await;
assert_eq!(code, Some(130), "cp SIGINT must exit 130; got {code:?}");
helper.abort_all_multipart_uploads(&bucket).await;
helper.delete_bucket_with_cascade(&bucket).await;
let _ = std::fs::remove_dir_all(&local_dir);
}
#[tokio::test]
async fn cancel_mv_sigint_exits_130() {
let _serial = SIGINT_TEST_LOCK.lock().await;
let helper = TestHelper::new().await;
let src_bucket = generate_bucket_name();
let dst_bucket = generate_bucket_name();
helper.create_bucket(&src_bucket, REGION).await;
helper.create_bucket(&dst_bucket, REGION).await;
helper
.put_object(&src_bucket, "big.bin", vec![0u8; 30 * 1024 * 1024])
.await;
let source = format!("s3://{src_bucket}/big.bin");
let target = format!("s3://{dst_bucket}/big.bin");
let mut cmd = s7cmd_cmd();
cmd.args([
"mv",
"--source-profile",
"s7cmd-e2e-test",
"--source-region",
REGION,
"--target-profile",
"s7cmd-e2e-test",
"--target-region",
REGION,
"--rate-limit-bandwidth",
"2MiB",
&source,
&target,
]);
let code = run_with_sigint(&mut cmd).await;
assert_eq!(code, Some(130), "mv SIGINT must exit 130; got {code:?}");
helper.abort_all_multipart_uploads(&src_bucket).await;
helper.abort_all_multipart_uploads(&dst_bucket).await;
helper.delete_bucket_with_cascade(&src_bucket).await;
helper.delete_bucket_with_cascade(&dst_bucket).await;
}
#[tokio::test]
async fn cancel_ls_sigint_mid_paginated_listing_exits_0() {
let _serial = SIGINT_TEST_LOCK.lock().await;
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
for i in 0..150 {
helper
.put_object(&bucket, &format!("k{i:04}"), b"x".to_vec())
.await;
}
let target = format!("s3://{bucket}/");
let mut cmd = s7cmd_cmd();
cmd.args([
"ls",
"--target-profile",
"s7cmd-e2e-test",
"--target-region",
REGION,
"--recursive",
"--max-keys",
"1",
"--rate-limit-api",
"10",
&target,
]);
let code = run_with_sigint(&mut cmd).await;
assert_eq!(
code,
Some(0),
"cancelled listing must exit 0 (ls cancellation path)"
);
helper.delete_bucket_with_cascade(&bucket).await;
}
#[tokio::test]
async fn cancel_clean_sigint_mid_deletion_exits_0() {
let _serial = SIGINT_TEST_LOCK.lock().await;
let helper = TestHelper::new().await;
let bucket = generate_bucket_name();
helper.create_bucket(&bucket, REGION).await;
for i in 0..200 {
helper
.put_object(&bucket, &format!("k{i:04}"), b"x".to_vec())
.await;
}
let target = format!("s3://{bucket}/");
let mut cmd = s7cmd_cmd();
cmd.args([
"clean",
"--target-profile",
"s7cmd-e2e-test",
"--target-region",
REGION,
"--force",
"--batch-size",
"10",
"--rate-limit-objects",
"10",
&target,
]);
let code = run_with_sigint(&mut cmd).await;
assert_eq!(
code,
Some(0),
"cancelled deletion must exit 0 (clean cancellation path)"
);
helper.delete_bucket_with_cascade(&bucket).await;
}