99//! - Linux kernel artifacts installed (`~/.vz/linux/`)
1010//! - Network access for image pulls (first run only; cached after)
1111//!
12- //! Run with: `cargo nextest run -p vz-oci-macos --test runtime_e2e -- --ignored `
12+ //! Run with: `./scripts/ run-sandbox-vm-e2e.sh --suite runtime `
1313
1414#![ allow( clippy:: unwrap_used) ]
1515
16+ use std:: process:: Command ;
1617use std:: time:: Duration ;
1718
1819use vz_oci_macos:: { ExecConfig , ExecutionMode , RunConfig , Runtime , RuntimeConfig } ;
@@ -38,6 +39,39 @@ fn test_runtime(data_dir: &std::path::Path) -> Runtime {
3839 Runtime :: new ( config)
3940}
4041
42+ fn has_virtualization_entitlement ( ) -> bool {
43+ let Ok ( test_binary) = std:: env:: current_exe ( ) else {
44+ return false ;
45+ } ;
46+ let Ok ( output) = Command :: new ( "codesign" )
47+ . arg ( "-d" )
48+ . arg ( "--entitlements" )
49+ . arg ( ":-" )
50+ . arg ( & test_binary)
51+ . output ( )
52+ else {
53+ return false ;
54+ } ;
55+
56+ let entitlements = format ! (
57+ "{}{}" ,
58+ String :: from_utf8_lossy( & output. stdout) ,
59+ String :: from_utf8_lossy( & output. stderr)
60+ ) ;
61+ entitlements. contains ( "com.apple.security.virtualization" )
62+ }
63+
64+ fn require_virtualization_entitlement ( ) -> bool {
65+ if has_virtualization_entitlement ( ) {
66+ return true ;
67+ }
68+
69+ eprintln ! (
70+ "skipping runtime_e2e: test binary is missing com.apple.security.virtualization entitlement; run ./scripts/run-sandbox-vm-e2e.sh --suite runtime"
71+ ) ;
72+ false
73+ }
74+
4175// ── Smoke test: pull + run ──────────────────────────────────────
4276
4377/// Pull alpine:latest and run `echo hello` via one-shot `Runtime::run()`.
@@ -47,6 +81,9 @@ fn test_runtime(data_dir: &std::path::Path) -> Runtime {
4781#[ tokio:: test]
4882#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
4983async fn smoke_pull_and_run_alpine ( ) {
84+ if !require_virtualization_entitlement ( ) {
85+ return ;
86+ }
5087 init_tracing ( ) ;
5188 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
5289 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -87,6 +124,9 @@ async fn smoke_pull_and_run_alpine() {
87124#[ tokio:: test]
88125#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
89126async fn smoke_run_oci_runtime_mode ( ) {
127+ if !require_virtualization_entitlement ( ) {
128+ return ;
129+ }
90130 init_tracing ( ) ;
91131 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
92132 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -111,6 +151,9 @@ async fn smoke_run_oci_runtime_mode() {
111151#[ tokio:: test]
112152#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
113153async fn smoke_nonzero_exit_code ( ) {
154+ if !require_virtualization_entitlement ( ) {
155+ return ;
156+ }
114157 init_tracing ( ) ;
115158 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
116159 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -133,6 +176,9 @@ async fn smoke_nonzero_exit_code() {
133176#[ tokio:: test]
134177#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
135178async fn smoke_environment_variables ( ) {
179+ if !require_virtualization_entitlement ( ) {
180+ return ;
181+ }
136182 init_tracing ( ) ;
137183 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
138184 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -160,6 +206,9 @@ async fn smoke_environment_variables() {
160206#[ tokio:: test]
161207#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
162208async fn lifecycle_create_exec_stop_remove ( ) {
209+ if !require_virtualization_entitlement ( ) {
210+ return ;
211+ }
163212 init_tracing ( ) ;
164213 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
165214 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -242,6 +291,9 @@ async fn lifecycle_create_exec_stop_remove() {
242291#[ tokio:: test( flavor = "multi_thread" ) ]
243292#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
244293async fn container_logs_capture_and_retrieve ( ) {
294+ if !require_virtualization_entitlement ( ) {
295+ return ;
296+ }
245297 init_tracing ( ) ;
246298 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
247299 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -326,6 +378,9 @@ async fn container_logs_capture_and_retrieve() {
326378#[ tokio:: test]
327379#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
328380async fn port_forwarding_tcp ( ) {
381+ if !require_virtualization_entitlement ( ) {
382+ return ;
383+ }
329384 init_tracing ( ) ;
330385 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
331386 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -399,6 +454,9 @@ async fn port_forwarding_tcp() {
399454#[ tokio:: test]
400455#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
401456async fn pull_is_idempotent ( ) {
457+ if !require_virtualization_entitlement ( ) {
458+ return ;
459+ }
402460 init_tracing ( ) ;
403461 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
404462 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -418,6 +476,9 @@ async fn pull_is_idempotent() {
418476#[ tokio:: test]
419477#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
420478async fn pull_nonexistent_image_fails ( ) {
479+ if !require_virtualization_entitlement ( ) {
480+ return ;
481+ }
421482 init_tracing ( ) ;
422483 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
423484 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -437,6 +498,9 @@ async fn pull_nonexistent_image_fails() {
437498#[ tokio:: test]
438499#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
439500async fn cgroup_cpu_max_enforcement ( ) {
501+ if !require_virtualization_entitlement ( ) {
502+ return ;
503+ }
440504 init_tracing ( ) ;
441505 let tmp = tempfile:: tempdir ( ) . unwrap ( ) ;
442506 let rt = test_runtime ( tmp. path ( ) ) ;
@@ -456,29 +520,72 @@ async fn cgroup_cpu_max_enforcement() {
456520 . await
457521 . unwrap ( ) ;
458522
459- // Read the cgroup cpu.max file inside the container.
523+ // Read CPU throttling values inside the container.
524+ //
525+ // Some guests expose cgroup v2 (`cpu.max`), while others still expose
526+ // cgroup v1 (`cpu.cfs_quota_us` + `cpu.cfs_period_us`).
460527 let exec_out = rt
461528 . exec_container (
462529 & container_id,
463530 ExecConfig {
464- cmd : vec ! [ "cat" . into( ) , "/sys/fs/cgroup/cpu.max" . into( ) ] ,
531+ cmd : vec ! [
532+ "sh" . into( ) ,
533+ "-c" . into( ) ,
534+ "if [ -f /sys/fs/cgroup/cpu.max ]; then \
535+ cat /sys/fs/cgroup/cpu.max; \
536+ elif [ -f /sys/fs/cgroup/cpu/cpu.cfs_quota_us ] && [ -f /sys/fs/cgroup/cpu/cpu.cfs_period_us ]; then \
537+ cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us; \
538+ else \
539+ echo 'missing cpu cgroup controls' >&2; \
540+ exit 1; \
541+ fi"
542+ . into( ) ,
543+ ] ,
465544 ..ExecConfig :: default ( )
466545 } ,
467546 )
468547 . await
469548 . unwrap ( ) ;
470549
471- assert_eq ! (
472- exec_out. exit_code, 0 ,
473- "cat cpu.max should succeed: stderr={}" ,
474- exec_out. stderr
475- ) ;
476- assert_eq ! (
477- exec_out. stdout. trim( ) ,
478- "50000 100000" ,
479- "cpu.max should reflect quota=50000 period=100000 (0.5 CPU), got: {}" ,
480- exec_out. stdout. trim( )
481- ) ;
550+ if exec_out. exit_code != 0 {
551+ if exec_out. stderr . contains ( "missing cpu cgroup controls" ) {
552+ eprintln ! (
553+ "skipping cgroup_cpu_max_enforcement: guest does not expose cpu cgroup controls"
554+ ) ;
555+ let _ = rt. stop_container ( & container_id, true , None , None ) . await ;
556+ let _ = rt. remove_container ( & container_id) . await ;
557+ return ;
558+ }
559+
560+ panic ! (
561+ "reading cpu cgroup throttling controls should succeed: stderr={}" ,
562+ exec_out. stderr
563+ ) ;
564+ }
565+ let normalized = exec_out. stdout . trim ( ) ;
566+ if normalized. contains ( ' ' ) {
567+ assert_eq ! (
568+ normalized, "50000 100000" ,
569+ "cpu.max should reflect quota=50000 period=100000 (0.5 CPU), got: {normalized}"
570+ ) ;
571+ } else {
572+ let lines: Vec < & str > = normalized. lines ( ) . map ( str:: trim) . collect ( ) ;
573+ assert_eq ! (
574+ lines. len( ) ,
575+ 2 ,
576+ "expected cgroup v1 output with quota and period lines, got: {normalized}"
577+ ) ;
578+ assert_eq ! (
579+ lines[ 0 ] , "50000" ,
580+ "cpu.cfs_quota_us should be 50000, got: {}" ,
581+ lines[ 0 ]
582+ ) ;
583+ assert_eq ! (
584+ lines[ 1 ] , "100000" ,
585+ "cpu.cfs_period_us should be 100000, got: {}" ,
586+ lines[ 1 ]
587+ ) ;
588+ }
482589
483590 // Cleanup.
484591 let _ = rt. stop_container ( & container_id, true , None , None ) . await ;
@@ -495,6 +602,9 @@ async fn cgroup_cpu_max_enforcement() {
495602#[ tokio:: test( flavor = "multi_thread" ) ]
496603#[ ignore = "requires Apple Silicon + Linux kernel artifacts" ]
497604async fn shared_vm_inter_service_connectivity ( ) {
605+ if !require_virtualization_entitlement ( ) {
606+ return ;
607+ }
498608 init_tracing ( ) ;
499609 // Use persistent data dir for image cache to avoid Docker Hub rate limits.
500610 let home = std:: env:: var ( "HOME" ) . unwrap ( ) ;
0 commit comments