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 @@ -386,87 +386,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.

1 change: 1 addition & 0 deletions tests/suites/conformance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ nextest run the tests as the library's unit tests. The crate also builds the
| `persistent-workspace` | Files in `/sandbox` survive stop and start |
| `policy-advisor` | `policy.local` accepts a proposed rule; a denied address or hostname yields one draft rule |
| `file-transfer` | `sandbox upload` and `download`: round trips, file modes and symlinks outside the workspace, `.gitignore` filtering, workspace-escape and option-looking names, a large upload |
| `create-upload` | `sandbox create --upload`: directory basename and nested files, multiple uploads, a single file |
| `credential-gating` | Provider credentials: admission of credentialed endpoints, REST body placeholders and rewriting, WebSocket binary frames, live policy binding |
| `image` | A sandbox runs the default image or the one it is created from |
| `labels` | Labels are stored and select sandboxes in `sandbox list` |
Expand Down
119 changes: 119 additions & 0 deletions tests/suites/conformance/src/create_upload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

//! `sandbox create --upload`.
//!
//! Ported from `e2e/rust/tests/upload_create.rs`, reworked in
//! NVIDIA/OpenShell#4606 by politerealism. The behavior is not driver-specific.
//!
//! `--upload` cannot be combined with a command, so these tests create the
//! sandbox with the target's default image and workload and check the files
//! once `create` returns.

use std::fs;
use std::path::Path;

use tempfile::TempDir;
use testkit::{Context, ResultExt as _, Sandbox};

/// `--upload <directory>:<destination>` keeps the directory's basename under
/// the destination, preserves nested files, and does not flatten the directory.
#[testkit::test]
async fn create_with_upload_directory_preserves_source_basename(
cx: &mut Context,
) -> testkit::Result {
let local = scratch()?;
let source = local.path().join("project");
write(&source.join("marker.txt"), "upload-create-marker")?;
write(&source.join("src/main.py"), "print('hello')")?;

let sandbox = create_with_uploads(cx, "cu1", &[(&source, "/sandbox/data")]).await?;
expect_file(
&sandbox,
"/sandbox/data/project/marker.txt",
"upload-create-marker",
)
.await?;
expect_file(
&sandbox,
"/sandbox/data/project/src/main.py",
"print('hello')",
)
.await?;
sandbox
.exec(["sh", "-c", "test ! -e /sandbox/data/marker.txt"])
.await?
.success()?;
sandbox.delete().await
}

/// Several `--upload` flags on one `sandbox create` each place their files at
/// their own destination.
#[testkit::test]
async fn create_with_multiple_uploads(cx: &mut Context) -> testkit::Result {
let local = scratch()?;
let alpha = local.path().join("alpha");
let beta = local.path().join("beta");
write(&alpha.join("a.txt"), "content-alpha")?;
write(&beta.join("b.txt"), "content-beta")?;

let sandbox = create_with_uploads(
cx,
"cu2",
&[(&alpha, "/sandbox/alpha"), (&beta, "/sandbox/beta")],
)
.await?;
expect_file(&sandbox, "/sandbox/alpha/alpha/a.txt", "content-alpha").await?;
expect_file(&sandbox, "/sandbox/beta/beta/b.txt", "content-beta").await?;
sandbox.delete().await
}

/// `--upload <file>:<destination>` places a single file at the destination.
#[testkit::test]
async fn create_with_upload_single_file(cx: &mut Context) -> testkit::Result {
let local = scratch()?;
let source = local.path().join("config.txt");
write(&source, "single-file-upload-test")?;

let sandbox = create_with_uploads(cx, "cu3", &[(&source, "/sandbox")]).await?;
expect_file(&sandbox, "/sandbox/config.txt", "single-file-upload-test").await?;
sandbox.delete().await
}

fn scratch() -> testkit::Result<TempDir> {
tempfile::tempdir().context("create a local scratch directory")
}

fn write(path: &Path, content: &str) -> testkit::Result {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).context(format!("create {}", parent.display()))?;
}
fs::write(path, content).context(format!("write {}", path.display()))
}

/// Create a detached sandbox with one `--upload <source>:<destination>` flag
/// per entry and wait until it is Ready.
async fn create_with_uploads(
cx: &Context,
suffix: &str,
uploads: &[(&Path, &str)],
) -> testkit::Result<Sandbox> {
let args = uploads.iter().flat_map(|(source, destination)| {
[
"--upload".to_string(),
format!("{}:{destination}", source.display()),
]
});
cx.cli_sandbox(suffix).args(args).create_ready().await
}

/// Require the sandbox file at `path` to contain exactly `expected`.
async fn expect_file(sandbox: &Sandbox, path: &str, expected: &str) -> testkit::Result {
let output = sandbox.exec(["cat", path]).await?.success()?;
testkit::ensure!(
output.stdout == expected,
"{path} contains {:?}; expected {expected:?}",
output.stdout
);
Ok(())
}
1 change: 1 addition & 0 deletions tests/suites/conformance/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! program in `src/main.rs` runs them by linking this library.

mod cli;
mod create_upload;
mod credential_gating;
mod file_transfer;
mod filesystem_policy;
Expand Down
Loading