mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-03 02:24:33 +00:00
SandboxSpec had a Local variant beside the provider spec, and a local sandbox was created by hand over a bare Host provider: no workspace, no provider connection, its own reconnect, and its own push rule for the designated directory. The local kind is now one more SandboxSpec: SandboxSpec::local names the directory on a HostDirectory spec with a skip clone, and provider_sandbox builds it like a plugin kind, creating the directory when missing since the Host provider requires it to exist. Every RunSandbox carries a workspace; a handle wrapped as is gets the workspace of its own working directory. The push rule is one rule for every checkout: a checkout fabro cloned pushes with the credentials it was cloned with, and any other checkout pushes when it has an origin, with whatever credentials it carries. A local run therefore pushes the same way before and after a resume; before, a reconnected local sandbox carried an attached workspace that never pushed while a fresh one did. Reconnect uses the recorded id for every kind. The recompute of a local id from its directory, kept for records written before directories had ids, is gone, and test fixtures that wrote made-up local ids derive them through test_support::local_sandbox_id instead. A local run's record now carries its workspace layout like every provider-chosen directory, and the sandbox.initializing event precedes the driver's create events for local as for every other kind. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
321 lines
10 KiB
Rust
321 lines
10 KiB
Rust
//! E2E tests for `fabro cp` against local and Docker sandbox backends.
|
|
//!
|
|
//! Local tests run without `#[ignore]` (no external dependencies).
|
|
//! Docker tests require a Docker daemon and are marked `#[ignore]`.
|
|
//! Run Docker tests with: `cargo test --package arc-workflows --test
|
|
//! cp_integration -- --ignored`
|
|
|
|
#![allow(
|
|
clippy::ignore_without_reason,
|
|
reason = "This integration module intentionally uses concise ignored-test markers."
|
|
)]
|
|
#![expect(
|
|
clippy::disallowed_methods,
|
|
reason = "This integration test stages sandbox fixtures with sync std::fs."
|
|
)]
|
|
|
|
use fabro_sandbox::reconnect::reconnect_for_run;
|
|
use fabro_sandbox::test_support::local_sandbox_id;
|
|
use fabro_sandbox::{CloneRequest, ProviderAccess, provider_sandbox};
|
|
use fabro_types::{RunSandboxInstance, RunSandboxRuntime, SandboxProviderKind};
|
|
use sandbox_driver::{SandboxSource, SandboxSpec};
|
|
|
|
const DOCKER_CP_IMAGE: &str = "buildpack-deps:noble";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Local sandbox
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async fn local_record(working_directory: &std::path::Path) -> RunSandboxInstance {
|
|
RunSandboxInstance {
|
|
provider: SandboxProviderKind::LOCAL,
|
|
image: None,
|
|
snapshot: None,
|
|
runtime: RunSandboxRuntime {
|
|
id: local_sandbox_id(working_directory).await,
|
|
working_directory: working_directory.to_string_lossy().to_string(),
|
|
repo_cloned: None,
|
|
clone_origin_url: None,
|
|
clone_branch: None,
|
|
workspace_root: None,
|
|
repos_root: None,
|
|
primary_repo_path: None,
|
|
primary_repo_link: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_cp_upload_download_round_trip() {
|
|
let sandbox_dir = tempfile::tempdir().unwrap();
|
|
let scratch = tempfile::tempdir().unwrap();
|
|
|
|
let record = local_record(sandbox_dir.path()).await;
|
|
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
|
.await
|
|
.expect("reconnect local");
|
|
|
|
// Upload a text file
|
|
let content = b"hello from local cp test\n";
|
|
let local_src = scratch.path().join("upload.txt");
|
|
std::fs::write(&local_src, content).unwrap();
|
|
|
|
sandbox
|
|
.upload_file_from_local(&local_src, "cp_test.txt")
|
|
.await
|
|
.expect("upload text");
|
|
|
|
// Verify it landed in the sandbox working directory
|
|
assert!(sandbox_dir.path().join("cp_test.txt").exists());
|
|
|
|
// Download it back
|
|
let local_dst = scratch.path().join("download.txt");
|
|
sandbox
|
|
.download_file_to_local("cp_test.txt", &local_dst)
|
|
.await
|
|
.expect("download text");
|
|
|
|
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_cp_binary_round_trip() {
|
|
let sandbox_dir = tempfile::tempdir().unwrap();
|
|
let scratch = tempfile::tempdir().unwrap();
|
|
|
|
let record = local_record(sandbox_dir.path()).await;
|
|
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
|
.await
|
|
.expect("reconnect local");
|
|
|
|
// All 256 byte values
|
|
let binary: Vec<u8> = (0..=255).collect();
|
|
let local_src = scratch.path().join("binary.bin");
|
|
std::fs::write(&local_src, &binary).unwrap();
|
|
|
|
sandbox
|
|
.upload_file_from_local(&local_src, "binary.bin")
|
|
.await
|
|
.expect("upload binary");
|
|
|
|
let local_dst = scratch.path().join("binary_dl.bin");
|
|
sandbox
|
|
.download_file_to_local("binary.bin", &local_dst)
|
|
.await
|
|
.expect("download binary");
|
|
|
|
assert_eq!(std::fs::read(&local_dst).unwrap(), binary);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn local_cp_creates_parent_dirs() {
|
|
let sandbox_dir = tempfile::tempdir().unwrap();
|
|
let scratch = tempfile::tempdir().unwrap();
|
|
|
|
let record = local_record(sandbox_dir.path()).await;
|
|
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
|
.await
|
|
.expect("reconnect local");
|
|
|
|
let content = b"nested file\n";
|
|
let local_src = scratch.path().join("nested.txt");
|
|
std::fs::write(&local_src, content).unwrap();
|
|
|
|
// Upload to a nested path that doesn't exist yet
|
|
sandbox
|
|
.upload_file_from_local(&local_src, "a/b/c/nested.txt")
|
|
.await
|
|
.expect("upload to nested path");
|
|
|
|
assert!(sandbox_dir.path().join("a/b/c/nested.txt").exists());
|
|
|
|
// Download to a nested local path that doesn't exist yet
|
|
let local_dst = scratch.path().join("x/y/z/nested.txt");
|
|
sandbox
|
|
.download_file_to_local("a/b/c/nested.txt", &local_dst)
|
|
.await
|
|
.expect("download to nested path");
|
|
|
|
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Docker sandbox
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn docker_record(container_id: &str) -> RunSandboxInstance {
|
|
RunSandboxInstance {
|
|
provider: SandboxProviderKind::DOCKER,
|
|
image: None,
|
|
snapshot: None,
|
|
runtime: RunSandboxRuntime {
|
|
id: container_id.to_string(),
|
|
working_directory: "/workspace".to_string(),
|
|
repo_cloned: Some(false),
|
|
clone_origin_url: None,
|
|
clone_branch: None,
|
|
workspace_root: Some("/workspace".to_string()),
|
|
repos_root: Some("/repos".to_string()),
|
|
primary_repo_path: None,
|
|
primary_repo_link: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
struct DockerCpContainer {
|
|
id: String,
|
|
cleanup: bool,
|
|
}
|
|
|
|
impl Drop for DockerCpContainer {
|
|
fn drop(&mut self) {
|
|
if self.cleanup {
|
|
let _ = std::process::Command::new("docker")
|
|
.args(["rm", "-f", &self.id])
|
|
.output();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A container the driver created, so a reconnect by id finds it: the
|
|
/// driver attaches only to containers carrying its own label, the way
|
|
/// fabro's ownership scope attaches only to those carrying fabro's.
|
|
async fn docker_cp_container() -> DockerCpContainer {
|
|
if let Ok(id) = std::env::var("FABRO_DOCKER_CP_CONTAINER") {
|
|
return DockerCpContainer { id, cleanup: false };
|
|
}
|
|
|
|
ensure_docker_image(DOCKER_CP_IMAGE);
|
|
let sandbox = provider_sandbox(
|
|
SandboxProviderKind::DOCKER,
|
|
&ProviderAccess::default(),
|
|
SandboxSpec::new(SandboxSource::Image {
|
|
reference: DOCKER_CP_IMAGE.to_string(),
|
|
}),
|
|
&CloneRequest::none(),
|
|
None,
|
|
None,
|
|
)
|
|
.await
|
|
.expect("docker sandbox should construct");
|
|
sandbox
|
|
.initialize()
|
|
.await
|
|
.expect("docker sandbox should initialize");
|
|
let id = sandbox.sandbox_info();
|
|
assert!(
|
|
!id.is_empty(),
|
|
"the docker sandbox should have a container id"
|
|
);
|
|
DockerCpContainer { id, cleanup: true }
|
|
}
|
|
|
|
fn ensure_docker_image(image: &str) {
|
|
let inspect = std::process::Command::new("docker")
|
|
.args(["image", "inspect", image])
|
|
.output()
|
|
.expect("docker image inspect should execute");
|
|
if inspect.status.success() {
|
|
return;
|
|
}
|
|
|
|
let pull = std::process::Command::new("docker")
|
|
.args(["pull", image])
|
|
.output()
|
|
.expect("docker pull should execute");
|
|
assert!(
|
|
pull.status.success(),
|
|
"docker pull {image} failed\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&pull.stdout),
|
|
String::from_utf8_lossy(&pull.stderr)
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // requires Docker daemon
|
|
async fn docker_cp_upload_download_round_trip() {
|
|
let container = docker_cp_container().await;
|
|
let scratch = tempfile::tempdir().unwrap();
|
|
|
|
let record = docker_record(&container.id);
|
|
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
|
.await
|
|
.expect("reconnect docker");
|
|
|
|
// Upload a text file
|
|
let content = b"hello from docker cp test\n";
|
|
let local_src = scratch.path().join("upload.txt");
|
|
std::fs::write(&local_src, content).unwrap();
|
|
|
|
sandbox
|
|
.upload_file_from_local(&local_src, "cp_test.txt")
|
|
.await
|
|
.expect("upload text");
|
|
|
|
// Download it back
|
|
let local_dst = scratch.path().join("download.txt");
|
|
sandbox
|
|
.download_file_to_local("cp_test.txt", &local_dst)
|
|
.await
|
|
.expect("download text");
|
|
|
|
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // requires Docker daemon
|
|
async fn docker_cp_binary_round_trip() {
|
|
let container = docker_cp_container().await;
|
|
let scratch = tempfile::tempdir().unwrap();
|
|
|
|
let record = docker_record(&container.id);
|
|
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
|
.await
|
|
.expect("reconnect docker");
|
|
|
|
let binary: Vec<u8> = (0..=255).collect();
|
|
let local_src = scratch.path().join("binary.bin");
|
|
std::fs::write(&local_src, &binary).unwrap();
|
|
|
|
sandbox
|
|
.upload_file_from_local(&local_src, "binary.bin")
|
|
.await
|
|
.expect("upload binary");
|
|
|
|
let local_dst = scratch.path().join("binary_dl.bin");
|
|
sandbox
|
|
.download_file_to_local("binary.bin", &local_dst)
|
|
.await
|
|
.expect("download binary");
|
|
|
|
assert_eq!(std::fs::read(&local_dst).unwrap(), binary);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore] // requires Docker daemon
|
|
async fn docker_cp_creates_parent_dirs() {
|
|
let container = docker_cp_container().await;
|
|
let scratch = tempfile::tempdir().unwrap();
|
|
|
|
let record = docker_record(&container.id);
|
|
let sandbox = reconnect_for_run(&record, &ProviderAccess::default(), None, None)
|
|
.await
|
|
.expect("reconnect docker");
|
|
|
|
let content = b"nested docker file\n";
|
|
let local_src = scratch.path().join("nested.txt");
|
|
std::fs::write(&local_src, content).unwrap();
|
|
|
|
sandbox
|
|
.upload_file_from_local(&local_src, "deep/nested/file.txt")
|
|
.await
|
|
.expect("upload to nested path");
|
|
|
|
let local_dst = scratch.path().join("p/q/file.txt");
|
|
sandbox
|
|
.download_file_to_local("deep/nested/file.txt", &local_dst)
|
|
.await
|
|
.expect("download to nested path");
|
|
|
|
assert_eq!(std::fs::read(&local_dst).unwrap(), content);
|
|
}
|