#[cfg(feature = "tokio-console")]
use std::time::Duration;
use kernal_api::{shell_spec, ProcessCaptureError, SpawnSpec, StreamMode};
#[test]
fn async_process_hal_captures_output() {
let runtime = kernal_api::async_engine::RuntimeBuilder::current_thread()
.enable_all()
.build()
.expect("facade-owned runtime");
runtime.run(async {
#[cfg(windows)]
let spec = shell_spec("echo kernal-api & echo kernal-api-error 1>&2");
#[cfg(not(windows))]
let spec = shell_spec("printf kernal-api; printf kernal-api-error >&2");
let output = spec
.stdout(StreamMode::Piped)
.stderr(StreamMode::Piped)
.spawn()
.await
.expect("spawn through the shared HAL")
.wait_with_output_bounded(1024)
.await
.expect("capture output");
assert!(output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).contains("kernal-api"));
assert!(String::from_utf8_lossy(&output.stderr).contains("kernal-api-error"));
let missing = SpawnSpec::new("kernal-api-program-that-does-not-exist")
.spawn()
.await;
assert!(missing.is_err());
});
}
#[test]
fn async_process_hal_reports_a_finite_capture_that_exceeds_its_aggregate_bound() {
let runtime = kernal_api::async_engine::RuntimeBuilder::current_thread()
.enable_all()
.build()
.expect("facade-owned runtime");
runtime.run(async {
#[cfg(windows)]
let spec = shell_spec("<nul set /p =12345");
#[cfg(not(windows))]
let spec = shell_spec("printf 12345");
let error = tokio::time::timeout(
std::time::Duration::from_secs(2),
spec.stdout(StreamMode::Piped)
.stderr(StreamMode::Piped)
.spawn()
.await
.expect("spawn through the shared HAL")
.wait_with_output_bounded(4),
)
.await
.expect("finite child reaches EOF after excess output is drained")
.expect_err("aggregate output limit must reject retained output");
assert!(matches!(
error,
ProcessCaptureError::OutputLimitExceeded { limit: 4 }
));
});
}
#[test]
fn async_process_hal_kills_and_does_not_signal_an_unowned_group() {
let runtime = kernal_api::async_engine::RuntimeBuilder::current_thread()
.enable_all()
.build()
.expect("facade-owned runtime");
runtime.run(async {
#[cfg(windows)]
let spec = shell_spec("ping 127.0.0.1 -n 30 > nul");
#[cfg(not(windows))]
let spec = shell_spec("sleep 30");
let mut child = spec
.stdin(StreamMode::Null)
.create_process_group(false)
.spawn()
.await
.expect("spawn a killable child through the shared HAL");
assert!(child.id().is_some(), "a live child has an identity");
assert!(!child
.terminate_group_soft()
.await
.expect("an unowned group is a safe no-op"));
child.kill().await.expect("hard kill");
assert_eq!(child.id(), None, "a killed child PID is not reusable");
assert!(!child.wait().await.expect("reap killed child").success());
});
}
#[test]
fn async_process_hal_soft_terminates_an_owned_group() {
let runtime = kernal_api::async_engine::RuntimeBuilder::current_thread()
.enable_all()
.build()
.expect("facade-owned runtime");
runtime.run(async {
#[cfg(windows)]
let spec = shell_spec("ping 127.0.0.1 -n 30 > nul");
#[cfg(not(windows))]
let spec = shell_spec("sleep 30");
let mut child = spec
.stdin(StreamMode::Null)
.create_process_group(true)
.spawn()
.await
.expect("spawn a child-owned group through the shared HAL");
assert!(child
.terminate_group_soft()
.await
.expect("request graceful group termination"));
#[cfg(not(windows))]
{
let status = tokio::time::timeout(std::time::Duration::from_secs(10), child.wait())
.await
.expect("the owned group signal ends the POSIX child")
.expect("wait for signalled child");
assert!(!status.success());
}
#[cfg(windows)]
{
let _ = tokio::time::timeout(std::time::Duration::from_secs(10), child.wait()).await;
let _ = child.kill().await;
}
});
}
#[test]
fn async_process_hal_does_not_soft_signal_a_completed_group() {
let runtime = kernal_api::async_engine::RuntimeBuilder::current_thread()
.enable_all()
.build()
.expect("facade-owned runtime");
runtime.run(async {
#[cfg(windows)]
let spec = shell_spec("exit /b 0");
#[cfg(not(windows))]
let spec = shell_spec("exit 0");
let mut child = spec
.create_process_group(true)
.spawn()
.await
.expect("spawn a child-owned group through the shared HAL");
assert!(child
.wait()
.await
.expect("wait for completed child")
.success());
assert!(!child
.terminate_group_soft()
.await
.expect("completed child has no group left to signal"));
});
}
#[test]
fn async_process_hal_clears_pid_after_a_natural_wait() {
let runtime = kernal_api::async_engine::RuntimeBuilder::current_thread()
.enable_all()
.build()
.expect("facade-owned runtime");
runtime.run(async {
#[cfg(windows)]
let spec = shell_spec("exit /b 0");
#[cfg(not(windows))]
let spec = shell_spec("exit 0");
let mut child = spec.spawn().await.expect("spawn a naturally exiting child");
assert!(child.id().is_some(), "a live child has an identity");
assert!(child.wait().await.expect("wait for child").success());
assert_eq!(child.id(), None, "a waited child PID is not reusable");
});
}
#[cfg(feature = "profile")]
#[test]
fn cpu_and_async_profiles_share_one_pprof_schema() {
use kernal_api::profile::async_profile::{to_pprof, TaskSample};
use kernal_api::profile::export::to_pprof_bytes;
use kernal_api::profile::symbolize::Frame;
use kernal_api::profile::{ProfileMetrics, ResolvedSample, SessionResult};
let cpu = SessionResult {
samples: vec![ResolvedSample {
os_tid: 7,
frames: vec![Frame {
function: "worker".to_string(),
module: "fixture".to_string(),
relative_address: 0x10,
}],
truncated: false,
}],
metrics: ProfileMetrics {
samples_captured: 1,
duration_nanos: 1_000_000,
hz: 99,
..ProfileMetrics::default()
},
start_unix_nanos: 1,
period_nanos: 10_101_010,
};
assert!(!to_pprof_bytes(&cpu).is_empty());
let tasks = vec![TaskSample {
spawn_stack: vec!["root".into(), "waiting".into()],
idle_nanos: 9,
busy_nanos: 1,
scheduled_nanos: 2,
polls: 3,
wakes: 4,
name: "task".into(),
}];
assert!(!to_pprof(&tasks).is_empty());
}
#[cfg(feature = "allocator")]
#[test]
fn allocator_facade_exposes_dormant_heap_profiler() {
assert!(!kernal_api::allocator::is_enabled());
let _allocator_type = std::any::TypeId::of::<kernal_api::allocator::Allocator>();
}
#[cfg(feature = "tokio-console")]
#[test]
fn runtime_profile_configuration_is_product_neutral() {
let config = kernal_api::async_engine::DiagnosticsConfig::new("127.0.0.1:6669")
.with_publish_interval(Duration::from_millis(20));
assert_eq!(config.bind(), "127.0.0.1:6669");
assert_eq!(config.publish_interval(), Some(Duration::from_millis(20)));
}