working on runtime executions

This commit is contained in:
2026-02-16 22:04:20 -06:00
parent f52320f889
commit 904ede04be
99 changed files with 6778 additions and 5929 deletions

View File

@@ -1,248 +1,542 @@
//! Integration tests for Python virtual environment dependency isolation
//! Integration tests for runtime environment and dependency isolation
//!
//! Tests the end-to-end flow of creating isolated Python environments
//! for packs with dependencies.
//! Tests the end-to-end flow of creating isolated runtime environments
//! for packs using the ProcessRuntime configuration-driven approach.
//!
//! Environment directories are placed at:
//! {runtime_envs_dir}/{pack_ref}/{runtime_name}
//! e.g., /tmp/.../runtime_envs/testpack/python
//! This keeps the pack directory clean and read-only.
use attune_worker::runtime::{
DependencyManager, DependencyManagerRegistry, DependencySpec, PythonVenvManager,
use attune_common::models::runtime::{
DependencyConfig, EnvironmentConfig, InterpreterConfig, RuntimeExecutionConfig,
};
use attune_worker::runtime::process::ProcessRuntime;
use attune_worker::runtime::ExecutionContext;
use attune_worker::runtime::Runtime;
use attune_worker::runtime::{OutputFormat, ParameterDelivery, ParameterFormat};
use std::collections::HashMap;
use std::path::PathBuf;
use tempfile::TempDir;
#[tokio::test]
async fn test_python_venv_creation() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
fn make_python_config() -> RuntimeExecutionConfig {
RuntimeExecutionConfig {
interpreter: InterpreterConfig {
binary: "python3".to_string(),
args: vec!["-u".to_string()],
file_extension: Some(".py".to_string()),
},
environment: Some(EnvironmentConfig {
env_type: "virtualenv".to_string(),
dir_name: ".venv".to_string(),
create_command: vec![
"python3".to_string(),
"-m".to_string(),
"venv".to_string(),
"{env_dir}".to_string(),
],
interpreter_path: Some("{env_dir}/bin/python3".to_string()),
}),
dependencies: Some(DependencyConfig {
manifest_file: "requirements.txt".to_string(),
install_command: vec![
"{interpreter}".to_string(),
"-m".to_string(),
"pip".to_string(),
"install".to_string(),
"-r".to_string(),
"{manifest_path}".to_string(),
],
}),
}
}
let spec = DependencySpec::new("python").with_dependency("requests==2.28.0");
fn make_shell_config() -> RuntimeExecutionConfig {
RuntimeExecutionConfig {
interpreter: InterpreterConfig {
binary: "/bin/bash".to_string(),
args: vec![],
file_extension: Some(".sh".to_string()),
},
environment: None,
dependencies: None,
}
}
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
assert_eq!(env_info.runtime, "python");
assert!(env_info.is_valid);
assert!(env_info.path.exists());
assert!(env_info.executable_path.exists());
fn make_context(action_ref: &str, entry_point: &str, runtime_name: &str) -> ExecutionContext {
ExecutionContext {
execution_id: 1,
action_ref: action_ref.to_string(),
parameters: HashMap::new(),
env: HashMap::new(),
secrets: HashMap::new(),
timeout: Some(30),
working_dir: None,
entry_point: entry_point.to_string(),
code: None,
code_path: None,
runtime_name: Some(runtime_name.to_string()),
max_stdout_bytes: 10 * 1024 * 1024,
max_stderr_bytes: 10 * 1024 * 1024,
parameter_delivery: ParameterDelivery::default(),
parameter_format: ParameterFormat::default(),
output_format: OutputFormat::default(),
}
}
#[tokio::test]
async fn test_venv_idempotency() {
async fn test_python_venv_creation_via_process_runtime() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let spec = DependencySpec::new("python").with_dependency("requests==2.28.0");
let env_dir = runtime_envs_dir.join("testpack").join("python");
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// Setup the pack environment (creates venv at external location)
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Failed to create venv environment");
// Verify venv was created at the external runtime_envs location
assert!(env_dir.exists(), "Virtualenv directory should exist at external location");
let venv_python = env_dir.join("bin").join("python3");
assert!(
venv_python.exists(),
"Virtualenv python3 binary should exist"
);
// Verify pack directory was NOT modified
assert!(
!pack_dir.join(".venv").exists(),
"Pack directory should not contain .venv — environments are external"
);
}
#[tokio::test]
async fn test_venv_creation_is_idempotent() {
let temp_dir = TempDir::new().unwrap();
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let env_dir = runtime_envs_dir.join("testpack").join("python");
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// Create environment first time
let env_info1 = manager
.ensure_environment("test_pack", &spec)
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Failed to create environment");
let created_at1 = env_info1.created_at;
assert!(env_dir.exists());
// Call ensure_environment again with same dependencies
let env_info2 = manager
.ensure_environment("test_pack", &spec)
// Create environment second time — should succeed without error
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Failed to ensure environment");
.expect("Second setup should succeed (idempotent)");
// Should return existing environment (same created_at)
assert_eq!(env_info1.created_at, env_info2.created_at);
assert_eq!(created_at1, env_info2.created_at);
assert!(env_dir.exists());
}
#[tokio::test]
async fn test_venv_update_on_dependency_change() {
async fn test_dependency_installation() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let spec1 = DependencySpec::new("python").with_dependency("requests==2.28.0");
let env_dir = runtime_envs_dir.join("testpack").join("python");
// Create environment with first set of dependencies
let env_info1 = manager
.ensure_environment("test_pack", &spec1)
// Write a requirements.txt with a simple, fast-to-install package
std::fs::write(
pack_dir.join("requirements.txt"),
"pip>=21.0\n", // pip is already installed, so this is fast
)
.unwrap();
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// Setup creates the venv and installs dependencies
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Failed to setup environment with dependencies");
assert!(env_dir.exists());
}
#[tokio::test]
async fn test_no_environment_for_shell_runtime() {
let temp_dir = TempDir::new().unwrap();
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let env_dir = runtime_envs_dir.join("testpack").join("shell");
let runtime = ProcessRuntime::new(
"shell".to_string(),
make_shell_config(),
packs_base_dir,
runtime_envs_dir,
);
// Shell runtime has no environment config — should be a no-op
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Shell setup should succeed (no environment to create)");
// No environment should exist
assert!(!env_dir.exists());
assert!(!pack_dir.join(".venv").exists());
assert!(!pack_dir.join("node_modules").exists());
}
#[tokio::test]
async fn test_pack_has_dependencies_detection() {
let temp_dir = TempDir::new().unwrap();
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// No requirements.txt yet
assert!(
!runtime.pack_has_dependencies(&pack_dir),
"Should not detect dependencies without manifest file"
);
// Create requirements.txt
std::fs::write(pack_dir.join("requirements.txt"), "requests>=2.28.0\n").unwrap();
assert!(
runtime.pack_has_dependencies(&pack_dir),
"Should detect dependencies when manifest file exists"
);
}
#[tokio::test]
async fn test_environment_exists_detection() {
let temp_dir = TempDir::new().unwrap();
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let env_dir = runtime_envs_dir.join("testpack").join("python");
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// No venv yet — environment_exists uses pack_ref string
assert!(
!runtime.environment_exists("testpack"),
"Environment should not exist before setup"
);
// Create the venv
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Failed to create environment");
let created_at1 = env_info1.created_at;
// Give it a moment to ensure timestamp difference
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
// Change dependencies
let spec2 = DependencySpec::new("python").with_dependency("requests==2.29.0");
// Should recreate environment
let env_info2 = manager
.ensure_environment("test_pack", &spec2)
.await
.expect("Failed to update environment");
// Updated timestamp should be newer
assert!(env_info2.updated_at >= created_at1);
assert!(
runtime.environment_exists("testpack"),
"Environment should exist after setup"
);
}
#[tokio::test]
async fn test_multiple_pack_isolation() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let spec1 = DependencySpec::new("python").with_dependency("requests==2.28.0");
let spec2 = DependencySpec::new("python").with_dependency("flask==2.3.0");
let pack_a_dir = packs_base_dir.join("pack_a");
let pack_b_dir = packs_base_dir.join("pack_b");
std::fs::create_dir_all(&pack_a_dir).unwrap();
std::fs::create_dir_all(&pack_b_dir).unwrap();
// Create environments for two different packs
let env1 = manager
.ensure_environment("pack_a", &spec1)
let env_dir_a = runtime_envs_dir.join("pack_a").join("python");
let env_dir_b = runtime_envs_dir.join("pack_b").join("python");
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// Setup environments for two different packs
runtime
.setup_pack_environment(&pack_a_dir, &env_dir_a)
.await
.expect("Failed to create environment for pack_a");
.expect("Failed to setup pack_a");
let env2 = manager
.ensure_environment("pack_b", &spec2)
runtime
.setup_pack_environment(&pack_b_dir, &env_dir_b)
.await
.expect("Failed to create environment for pack_b");
.expect("Failed to setup pack_b");
// Should have different paths
assert_ne!(env1.path, env2.path);
assert_ne!(env1.executable_path, env2.executable_path);
// Each pack should have its own venv at the external location
assert!(env_dir_a.exists(), "pack_a should have its own venv");
assert!(env_dir_b.exists(), "pack_b should have its own venv");
assert_ne!(env_dir_a, env_dir_b, "Venvs should be in different directories");
// Both should be valid
assert!(env1.is_valid);
assert!(env2.is_valid);
// Pack directories should remain clean
assert!(!pack_a_dir.join(".venv").exists(), "pack_a dir should not contain .venv");
assert!(!pack_b_dir.join(".venv").exists(), "pack_b dir should not contain .venv");
}
#[tokio::test]
async fn test_get_executable_path() {
async fn test_execute_python_action_with_venv() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
let actions_dir = pack_dir.join("actions");
std::fs::create_dir_all(&actions_dir).unwrap();
let spec = DependencySpec::new("python");
let env_dir = runtime_envs_dir.join("testpack").join("python");
manager
.ensure_environment("test_pack", &spec)
// Write a Python script
std::fs::write(
actions_dir.join("hello.py"),
r#"
import sys
print(f"Python from: {sys.executable}")
print("Hello from venv action!")
"#,
)
.unwrap();
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// Setup the venv first
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Failed to create environment");
.expect("Failed to setup venv");
let python_path = manager
.get_executable_path("test_pack")
.await
.expect("Failed to get executable path");
// Now execute the action
let mut context = make_context("testpack.hello", "hello.py", "python");
context.code_path = Some(actions_dir.join("hello.py"));
assert!(python_path.exists());
assert!(python_path.to_string_lossy().contains("test_pack"));
let result = runtime.execute(context).await.unwrap();
assert_eq!(result.exit_code, 0, "Action should succeed");
assert!(
result.stdout.contains("Hello from venv action!"),
"Should see output from action. Got: {}",
result.stdout
);
// Verify it's using the venv Python (at external runtime_envs location)
assert!(
result.stdout.contains("runtime_envs"),
"Should be using the venv python from external runtime_envs dir. Got: {}",
result.stdout
);
}
#[tokio::test]
async fn test_validate_environment() {
async fn test_execute_shell_action_no_venv() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
let actions_dir = pack_dir.join("actions");
std::fs::create_dir_all(&actions_dir).unwrap();
// Non-existent environment should not be valid
let is_valid = manager
.validate_environment("nonexistent")
.await
.expect("Validation check failed");
assert!(!is_valid);
std::fs::write(
actions_dir.join("greet.sh"),
"#!/bin/bash\necho 'Hello from shell!'",
)
.unwrap();
// Create environment
let spec = DependencySpec::new("python");
manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
let runtime = ProcessRuntime::new(
"shell".to_string(),
make_shell_config(),
packs_base_dir,
runtime_envs_dir,
);
// Should now be valid
let is_valid = manager
.validate_environment("test_pack")
.await
.expect("Validation check failed");
assert!(is_valid);
let mut context = make_context("testpack.greet", "greet.sh", "shell");
context.code_path = Some(actions_dir.join("greet.sh"));
let result = runtime.execute(context).await.unwrap();
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("Hello from shell!"));
}
#[tokio::test]
async fn test_remove_environment() {
async fn test_working_directory_is_pack_dir() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
let actions_dir = pack_dir.join("actions");
std::fs::create_dir_all(&actions_dir).unwrap();
let spec = DependencySpec::new("python");
// Script that prints the working directory
std::fs::write(actions_dir.join("cwd.sh"), "#!/bin/bash\npwd").unwrap();
// Create environment
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
let runtime = ProcessRuntime::new(
"shell".to_string(),
make_shell_config(),
packs_base_dir,
runtime_envs_dir,
);
let path = env_info.path.clone();
assert!(path.exists());
let mut context = make_context("testpack.cwd", "cwd.sh", "shell");
context.code_path = Some(actions_dir.join("cwd.sh"));
// Remove environment
manager
.remove_environment("test_pack")
.await
.expect("Failed to remove environment");
let result = runtime.execute(context).await.unwrap();
assert!(!path.exists());
// Get environment should return None
let env = manager
.get_environment("test_pack")
.await
.expect("Failed to get environment");
assert!(env.is_none());
assert_eq!(result.exit_code, 0);
let output_path = result.stdout.trim();
assert_eq!(
output_path,
pack_dir.to_string_lossy().as_ref(),
"Working directory should be the pack directory"
);
}
#[tokio::test]
async fn test_list_environments() {
async fn test_interpreter_resolution_with_venv() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let spec = DependencySpec::new("python");
let env_dir = runtime_envs_dir.join("testpack").join("python");
// Create multiple environments
manager
.ensure_environment("pack_a", &spec)
let config = make_python_config();
let runtime = ProcessRuntime::new(
"python".to_string(),
config.clone(),
packs_base_dir,
runtime_envs_dir,
);
// Before venv creation — should resolve to system python
let interpreter = config.resolve_interpreter_with_env(&pack_dir, Some(&env_dir));
assert_eq!(
interpreter,
PathBuf::from("python3"),
"Without venv, should use system python"
);
// Create venv at external location
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Failed to create pack_a");
.expect("Failed to create venv");
manager
.ensure_environment("pack_b", &spec)
.await
.expect("Failed to create pack_b");
manager
.ensure_environment("pack_c", &spec)
.await
.expect("Failed to create pack_c");
// List should return all three
let environments = manager
.list_environments()
.await
.expect("Failed to list environments");
assert_eq!(environments.len(), 3);
// After venv creation — should resolve to venv python at external location
let interpreter = config.resolve_interpreter_with_env(&pack_dir, Some(&env_dir));
let expected_venv_python = env_dir.join("bin").join("python3");
assert_eq!(
interpreter, expected_venv_python,
"With venv, should use venv python from external runtime_envs dir"
);
}
#[tokio::test]
async fn test_dependency_manager_registry() {
async fn test_skip_deps_install_without_manifest() {
let temp_dir = TempDir::new().unwrap();
let mut registry = DependencyManagerRegistry::new();
let packs_base_dir = temp_dir.path().join("packs");
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let pack_dir = packs_base_dir.join("testpack");
std::fs::create_dir_all(&pack_dir).unwrap();
let python_manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
registry.register(Box::new(python_manager));
let env_dir = runtime_envs_dir.join("testpack").join("python");
// Should support python
assert!(registry.supports("python"));
assert!(!registry.supports("nodejs"));
// No requirements.txt — install_dependencies should be a no-op
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
packs_base_dir,
runtime_envs_dir,
);
// Should be able to get manager
let manager = registry.get("python");
assert!(manager.is_some());
assert_eq!(manager.unwrap().runtime_type(), "python");
// Setup should still create the venv but skip dependency installation
runtime
.setup_pack_environment(&pack_dir, &env_dir)
.await
.expect("Setup should succeed without manifest");
assert!(
env_dir.exists(),
"Venv should still be created at external location"
);
}
#[tokio::test]
async fn test_dependency_spec_builder() {
async fn test_runtime_config_matches_file_extension() {
let config = make_python_config();
assert!(config.matches_file_extension(std::path::Path::new("hello.py")));
assert!(config.matches_file_extension(std::path::Path::new(
"/opt/attune/packs/mypack/actions/script.py"
)));
assert!(!config.matches_file_extension(std::path::Path::new("hello.sh")));
assert!(!config.matches_file_extension(std::path::Path::new("hello.js")));
let shell_config = make_shell_config();
assert!(shell_config.matches_file_extension(std::path::Path::new("run.sh")));
assert!(!shell_config.matches_file_extension(std::path::Path::new("run.py")));
}
#[tokio::test]
async fn test_dependency_spec_builder_still_works() {
// The DependencySpec types are still available for generic use
use attune_worker::runtime::DependencySpec;
let spec = DependencySpec::new("python")
.with_dependency("requests==2.28.0")
.with_dependency("flask>=2.0.0")
@@ -256,122 +550,68 @@ async fn test_dependency_spec_builder() {
}
#[tokio::test]
async fn test_requirements_file_content() {
async fn test_process_runtime_setup_and_validate() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let runtime_envs_dir = temp_dir.path().join("runtime_envs");
let requirements = "requests==2.28.0\nflask==2.3.0\npydantic>=2.0.0";
let spec = DependencySpec::new("python").with_requirements_file(requirements.to_string());
let shell_runtime = ProcessRuntime::new(
"shell".to_string(),
make_shell_config(),
temp_dir.path().to_path_buf(),
runtime_envs_dir.clone(),
);
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment with requirements file");
// Setup and validate should succeed for shell
shell_runtime.setup().await.unwrap();
shell_runtime.validate().await.unwrap();
assert!(env_info.is_valid);
assert!(env_info.installed_dependencies.len() > 0);
let python_runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
temp_dir.path().to_path_buf(),
runtime_envs_dir,
);
// Setup and validate should succeed for python (warns if not available)
python_runtime.setup().await.unwrap();
python_runtime.validate().await.unwrap();
}
#[tokio::test]
async fn test_pack_ref_sanitization() {
async fn test_can_execute_by_runtime_name() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python");
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
temp_dir.path().to_path_buf(),
temp_dir.path().join("runtime_envs"),
);
// Pack refs with special characters should be sanitized
let env_info = manager
.ensure_environment("core.http", &spec)
.await
.expect("Failed to create environment");
let context = make_context("mypack.hello", "hello.py", "python");
assert!(runtime.can_execute(&context));
// Path should not contain dots
let path_str = env_info.path.to_string_lossy();
assert!(path_str.contains("core_http"));
assert!(!path_str.contains("core.http"));
let wrong_context = make_context("mypack.hello", "hello.py", "shell");
assert!(!runtime.can_execute(&wrong_context));
}
#[tokio::test]
async fn test_needs_update_detection() {
async fn test_can_execute_by_file_extension() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec1 = DependencySpec::new("python").with_dependency("requests==2.28.0");
let runtime = ProcessRuntime::new(
"python".to_string(),
make_python_config(),
temp_dir.path().to_path_buf(),
temp_dir.path().join("runtime_envs"),
);
// Non-existent environment needs update
let needs_update = manager
.needs_update("test_pack", &spec1)
.await
.expect("Failed to check update status");
assert!(needs_update);
let mut context = make_context("mypack.hello", "hello.py", "");
context.runtime_name = None;
context.code_path = Some(PathBuf::from("/tmp/packs/mypack/actions/hello.py"));
assert!(runtime.can_execute(&context));
// Create environment
manager
.ensure_environment("test_pack", &spec1)
.await
.expect("Failed to create environment");
// Same spec should not need update
let needs_update = manager
.needs_update("test_pack", &spec1)
.await
.expect("Failed to check update status");
assert!(!needs_update);
// Different spec should need update
let spec2 = DependencySpec::new("python").with_dependency("requests==2.29.0");
let needs_update = manager
.needs_update("test_pack", &spec2)
.await
.expect("Failed to check update status");
assert!(needs_update);
}
#[tokio::test]
async fn test_empty_dependencies() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
// Pack with no dependencies should still create venv
let spec = DependencySpec::new("python");
assert!(!spec.has_dependencies());
let env_info = manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment without dependencies");
assert!(env_info.is_valid);
assert!(env_info.path.exists());
}
#[tokio::test]
async fn test_get_environment_caching() {
let temp_dir = TempDir::new().unwrap();
let manager = PythonVenvManager::new(temp_dir.path().to_path_buf());
let spec = DependencySpec::new("python");
// Create environment
manager
.ensure_environment("test_pack", &spec)
.await
.expect("Failed to create environment");
// First get_environment should read from disk
let env1 = manager
.get_environment("test_pack")
.await
.expect("Failed to get environment")
.expect("Environment not found");
// Second get_environment should use cache
let env2 = manager
.get_environment("test_pack")
.await
.expect("Failed to get environment")
.expect("Environment not found");
assert_eq!(env1.id, env2.id);
assert_eq!(env1.path, env2.path);
context.code_path = Some(PathBuf::from("/tmp/packs/mypack/actions/hello.sh"));
context.entry_point = "hello.sh".to_string();
assert!(!runtime.can_execute(&context));
}