Skip to content
Open
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
81 changes: 0 additions & 81 deletions e2e/rust/src/harness/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,87 +390,6 @@ impl SandboxGuard {
})
}

/// Create a detached scratch sandbox with pre-loaded files, then exec a
/// command in it.
///
/// Equivalent to:
/// ```text
/// openshell sandbox create --detach --upload <local>:<dest> [extra_args...]
/// openshell sandbox exec <name> -- <command>
/// ```
///
/// The `--no-git-ignore` flag is passed to avoid needing a git repository.
///
/// # Errors
///
/// Returns an error if the CLI exits with a non-zero status or the sandbox
/// name cannot be parsed.
pub async fn create_with_upload(
upload_local: &str,
upload_dest: &str,
command: &[&str],
) -> Result<Self, String> {
Self::create_with_uploads(&[(upload_local, upload_dest)], command).await
}

/// Create a sandbox with multiple `--upload` specs.
///
/// Each element of `uploads` is a `(local_path, sandbox_dest)` pair.
/// `--no-git-ignore` is applied to all uploads.
pub async fn create_with_uploads(
uploads: &[(&str, &str)],
command: &[&str],
) -> Result<Self, String> {
let mut cmd = openshell_cmd();
cmd.arg("sandbox").arg("create").arg("--detach");
add_unique_name_if_missing(&mut cmd, &[]);
add_test_image_if_missing(&mut cmd, &[]);
for (local, dest) in uploads {
cmd.arg("--upload").arg(format!("{local}:{dest}"));
}
cmd.arg("--no-git-ignore");
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());

let output = timeout(SANDBOX_READY_TIMEOUT, cmd.output())
.await
.map_err(|_| {
format!("sandbox create --upload timed out after {SANDBOX_READY_TIMEOUT:?}")
})?
.map_err(|e| format!("failed to spawn openshell: {e}"))?;

let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let combined = format!("{stdout}{stderr}");

if !output.status.success() {
return Err(format!(
"sandbox create --upload failed (exit {:?}):\n{combined}",
output.status.code()
));
}

let name = extract_sandbox_name(&combined).ok_or_else(|| {
format!("could not parse sandbox name from create output:\n{combined}")
})?;

let mut guard = Self {
name,
create_output: combined,
child: None,
cleaned_up: false,
};

match guard.exec(command).await {
Ok(exec_output) => guard.create_output.push_str(&exec_output),
Err(err) => {
guard.cleanup().await;
return Err(err);
}
}

Ok(guard)
}

/// Upload local files to the sandbox via `openshell sandbox upload`.
///
/// # Arguments
Expand Down
113 changes: 0 additions & 113 deletions e2e/rust/tests/upload_create.rs

This file was deleted.

166 changes: 166 additions & 0 deletions e2e/suites/conformance/cli/tests/file_transfer/create_upload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! Stories for `sandbox create --upload`.
//!
//! Preconditions: the target supplies the default sandbox image. Each story
//! writes its upload sources to a local temporary directory.
//!
//! Permissions: an identity allowed to create, execute in, and delete sandboxes.
//!
//! Side effects: each story creates one sandbox named from the run id and
//! deletes it.
//!
//! Known limitation: `--upload` cannot be combined with a command, so stories
//! check the files once `create` returns, not that they arrived before a main
//! process started.

use super::helpers::{create_with_uploads, delete_sandbox, expect_stdout, fs_error};
use openshell_e2e_support::OpenShellRunner;
use std::fs;

/// Verify `sandbox create --upload <directory>:<destination>` keeps the
/// directory's basename under the destination.
#[tokio::test]
async fn create_with_upload_directory_preserves_source_basename() {
let mut runner = OpenShellRunner::from_env("file-transfer/create-upload-directory")
.expect("candidate openshell CLI is available");
let result = async {
runner.check_gateway_status().await?;
let runner = &mut runner;
let local = tempfile::tempdir().map_err(fs_error("create temporary directory"))?;
let source = local.path().join("project");
fs::create_dir_all(source.join("src")).map_err(fs_error("create project/src"))?;
fs::write(source.join("marker.txt"), "upload-create-marker")
.map_err(fs_error("write marker.txt"))?;
fs::write(source.join("src/main.py"), "print('hello')")
.map_err(fs_error("write main.py"))?;

let sandbox_name = format!("ct-{}-cu1", runner.id());
create_with_uploads(
runner,
"create-upload-directory/create",
&sandbox_name,
&[(&source, "/sandbox/data")],
)
.await?;

expect_stdout(
runner,
&sandbox_name,
"create-upload-directory/marker",
"cat /sandbox/data/project/marker.txt",
"upload-create-marker",
)
.await?;
expect_stdout(
runner,
&sandbox_name,
"create-upload-directory/nested",
"cat /sandbox/data/project/src/main.py",
"print('hello')",
)
.await?;
expect_stdout(
runner,
&sandbox_name,
"create-upload-directory/not-flattened",
"test ! -e /sandbox/data/marker.txt && echo absent",
"absent\n",
)
.await?;
delete_sandbox(runner, &sandbox_name).await
}
.await;
if let Err(error) = runner.finish(result).await {
panic!("file-transfer/create-upload-directory conformance story failed:\n{error}");
}
}

/// Verify several `--upload` flags on one `sandbox create` all place their
/// files at their own destinations.
#[tokio::test]
async fn create_with_multiple_uploads() {
let mut runner = OpenShellRunner::from_env("file-transfer/create-upload-multiple")
.expect("candidate openshell CLI is available");
let result = async {
runner.check_gateway_status().await?;
let runner = &mut runner;
let local = tempfile::tempdir().map_err(fs_error("create temporary directory"))?;
let alpha = local.path().join("alpha");
let beta = local.path().join("beta");
fs::create_dir_all(&alpha).map_err(fs_error("create alpha"))?;
fs::create_dir_all(&beta).map_err(fs_error("create beta"))?;
fs::write(alpha.join("a.txt"), "content-alpha").map_err(fs_error("write a.txt"))?;
fs::write(beta.join("b.txt"), "content-beta").map_err(fs_error("write b.txt"))?;

let sandbox_name = format!("ct-{}-cu2", runner.id());
create_with_uploads(
runner,
"create-upload-multiple/create",
&sandbox_name,
&[(&alpha, "/sandbox/alpha"), (&beta, "/sandbox/beta")],
)
.await?;

expect_stdout(
runner,
&sandbox_name,
"create-upload-multiple/alpha",
"cat /sandbox/alpha/alpha/a.txt",
"content-alpha",
)
.await?;
expect_stdout(
runner,
&sandbox_name,
"create-upload-multiple/beta",
"cat /sandbox/beta/beta/b.txt",
"content-beta",
)
.await?;
delete_sandbox(runner, &sandbox_name).await
}
.await;
if let Err(error) = runner.finish(result).await {
panic!("file-transfer/create-upload-multiple conformance story failed:\n{error}");
}
}

/// Verify `sandbox create --upload <file>:<destination>` places a single file
/// at the destination.
#[tokio::test]
async fn create_with_upload_single_file() {
let mut runner = OpenShellRunner::from_env("file-transfer/create-upload-file")
.expect("candidate openshell CLI is available");
let result = async {
runner.check_gateway_status().await?;
let runner = &mut runner;
let local = tempfile::tempdir().map_err(fs_error("create temporary directory"))?;
let source = local.path().join("config.txt");
fs::write(&source, "single-file-upload-test").map_err(fs_error("write config.txt"))?;

let sandbox_name = format!("ct-{}-cu3", runner.id());
create_with_uploads(
runner,
"create-upload-file/create",
&sandbox_name,
&[(&source, "/sandbox")],
)
.await?;

expect_stdout(
runner,
&sandbox_name,
"create-upload-file/read",
"cat /sandbox/config.txt",
"single-file-upload-test",
)
.await?;
delete_sandbox(runner, &sandbox_name).await
}
.await;
if let Err(error) = runner.finish(result).await {
panic!("file-transfer/create-upload-file conformance story failed:\n{error}");
}
}
Loading
Loading