use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::Instant;
use super::{PhaseTimings, TestCase, TestPhase, TestResult, TestTimeout};
#[cfg(feature = "hostlib")]
fn reset_hostlib_state() {
harn_hostlib::fs_snapshot::reset_all_sessions();
}
#[cfg(not(feature = "hostlib"))]
fn reset_hostlib_state() {}
fn install_user_test_event_log_if_unset() {
if std::env::var_os(harn_vm::event_log::HARN_EVENT_LOG_BACKEND_ENV).is_some() {
return;
}
harn_vm::event_log::install_memory_for_current_thread(
harn_vm::RuntimeLimits::DEFAULT.default_event_log_queue_depth,
);
}
fn register_manifest_host_operations(extensions: &crate::package::RuntimeExtensions) {
let (Some(manifest), Some(manifest_dir)) = (
extensions.root_manifest.as_ref(),
extensions.root_manifest_dir.as_deref(),
) else {
return;
};
let check = crate::package::absolutize_check_config_paths(manifest.check.clone(), manifest_dir);
for (capability, operations) in crate::commands::check::load_host_capabilities(&check) {
for operation in operations {
harn_vm::stdlib::host::register_scoped_mockable_host_operation(
&capability,
&operation,
"Host operation declared by the project manifest.",
);
}
}
}
#[derive(Debug)]
enum CaseOutcome {
Passed,
RuntimeError(String),
ExecutionTimedOut,
}
pub(super) async fn execute_case(
case: &TestCase,
execution_cwd: &Path,
timeout_ms: u64,
cli_skill_dirs: &[PathBuf],
prepared_module_cache: &harn_vm::PreparedModuleCache,
stdio_available: bool,
#[cfg(test)] setup_delay_ms: u64,
) -> TestResult {
harn_vm::reset_thread_local_state();
let _stdio_guard = (!stdio_available).then(harn_vm::reserve_stdio_for_current_thread);
reset_hostlib_state();
let mut phases = PhaseTimings::default();
let total_start = Instant::now();
let compile_start = Instant::now();
let compiler = harn_vm::Compiler::new();
let chunk = match if case.bindings.is_empty() {
compiler.compile_named(&case.program, &case.pipeline_name)
} else {
compiler.compile_named_with_param_globals(&case.program, &case.pipeline_name)
} {
Ok(c) => c,
Err(e) => {
phases.compile_ms = compile_start.elapsed().as_millis() as u64;
return TestResult {
name: case.name.clone(),
file: case.file.display().to_string(),
passed: false,
error: Some(format!("Compile error: {e}")),
timeout: None,
duration_ms: total_start.elapsed().as_millis() as u64,
phases,
};
}
};
phases.compile_ms = compile_start.elapsed().as_millis() as u64;
let local = tokio::task::LocalSet::new();
let file_display = case.file.display().to_string();
let setup_start = Instant::now();
let result = local
.run_until(async {
let mut vm = harn_vm::Vm::new();
vm.set_prepared_module_cache(prepared_module_cache.clone());
harn_vm::register_vm_stdlib(&mut vm);
crate::install_default_hostlib(&mut vm);
let source_parent = case.file.parent().unwrap_or(Path::new("."));
let project_root = harn_vm::stdlib::process::find_project_root(source_parent);
let test_state = tempfile::Builder::new()
.prefix("harn-user-test-state-")
.tempdir()
.map_err(|error| format!("failed to create test state directory: {error}"))?;
let store_base = test_state.path();
let source_dir = source_parent.to_string_lossy().into_owned();
install_user_test_event_log_if_unset();
harn_vm::register_store_builtins(&mut vm, store_base);
harn_vm::register_metadata_builtins(&mut vm, store_base);
let pipeline_name = case
.file
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("test");
harn_vm::register_checkpoint_builtins(&mut vm, store_base, pipeline_name);
vm.set_source_info(&file_display, &case.source);
harn_vm::stdlib::process::set_thread_execution_context(Some(
harn_vm::orchestration::RunExecutionRecord {
cwd: Some(execution_cwd.to_string_lossy().into_owned()),
project_root: project_root
.as_ref()
.map(|root| root.to_string_lossy().into_owned()),
source_dir: Some(source_dir),
env: BTreeMap::new(),
adapter: None,
repo_path: None,
worktree_path: None,
branch: None,
base_ref: None,
cleanup: None,
},
));
if let Some(ref root) = project_root {
vm.set_project_root(root);
}
if let Some(parent) = case.file.parent() {
if !parent.as_os_str().is_empty() {
vm.set_source_dir(parent);
}
}
let loaded =
crate::skill_loader::load_skills(&crate::skill_loader::SkillLoaderInputs {
cli_dirs: cli_skill_dirs.to_vec(),
source_path: Some(case.file.clone()),
});
crate::skill_loader::emit_loader_warnings(&loaded.loader_warnings);
crate::skill_loader::install_skills_global(&mut vm, &loaded);
let extensions = crate::package::try_load_runtime_extensions(&case.file)
.map_err(|error| format!("failed to load runtime extensions: {error}"))?;
register_manifest_host_operations(&extensions);
crate::package::install_runtime_extensions(&extensions);
crate::package::install_manifest_triggers_with_mode(&mut vm, &extensions, true)
.await
.map_err(|error| format!("failed to install manifest triggers: {error}"))?;
crate::package::install_manifest_hooks_with_mode(&mut vm, &extensions, true)
.await
.map_err(|error| format!("failed to install manifest hooks: {error}"))?;
vm.set_harness(harn_vm::Harness::real());
for (name, value) in &case.bindings {
vm.set_global(name, value.clone());
}
#[cfg(test)]
wait_for_test_setup_delay(setup_delay_ms).await;
let setup_ms = setup_start.elapsed().as_millis() as u64;
let exec_start = Instant::now();
let outcome = match vm
.execute_with_timeout(&chunk, std::time::Duration::from_millis(timeout_ms))
.await
{
Ok(_) => CaseOutcome::Passed,
Err(harn_vm::VmError::ExecutionDeadlineExceeded) => CaseOutcome::ExecutionTimedOut,
Err(error) => CaseOutcome::RuntimeError(vm.format_runtime_error(&error)),
};
let execute_ms = exec_start.elapsed().as_millis() as u64;
let execute_ms = if matches!(outcome, CaseOutcome::ExecutionTimedOut) {
execute_ms.max(timeout_ms)
} else {
execute_ms
};
harn_vm::egress::reset_egress_policy_for_host();
Ok::<_, String>((outcome, setup_ms, execute_ms))
})
.await;
let failed_setup_ms = result
.as_ref()
.err()
.map(|_| setup_start.elapsed().as_millis() as u64);
let teardown_start = Instant::now();
harn_vm::reset_thread_local_state();
reset_hostlib_state();
phases.teardown_ms = teardown_start.elapsed().as_millis() as u64;
let elapsed_ms = total_start.elapsed().as_millis() as u64;
let (passed, error, timeout, duration_ms) = match result {
Ok((outcome, setup_ms, execute_ms)) => {
phases.setup_ms = setup_ms;
phases.execute_ms = execute_ms;
match outcome {
CaseOutcome::Passed => (true, None, None, elapsed_ms),
CaseOutcome::RuntimeError(message) => (false, Some(message), None, elapsed_ms),
CaseOutcome::ExecutionTimedOut => (
false,
Some(format!("execute phase timed out after {timeout_ms}ms")),
Some(TestTimeout {
phase: TestPhase::Execute,
limit_ms: timeout_ms,
}),
elapsed_ms,
),
}
}
Err(setup_error) => {
phases.setup_ms = failed_setup_ms.unwrap_or_default();
(false, Some(setup_error), None, elapsed_ms)
}
};
TestResult {
name: case.name.clone(),
file: file_display,
passed,
error,
timeout,
duration_ms,
phases,
}
}
#[cfg(test)]
async fn wait_for_test_setup_delay(delay_ms: u64) {
if delay_ms == 0 {
return;
}
tokio::task::spawn_blocking(move || {
let state = std::sync::Mutex::new(());
let guard = state.lock().unwrap();
let condition = std::sync::Condvar::new();
let _ = condition
.wait_timeout_while(guard, std::time::Duration::from_millis(delay_ms), |_| true)
.unwrap();
})
.await
.unwrap();
}