@@ -8,20 +8,127 @@ use tempfile::TempDir;
88static SHARED_EMPTY_GIT_REPO_TEMPLATE : OnceLock < TempDir > = OnceLock :: new ( ) ;
99static SHARED_COMMITTED_GIT_REPO_TEMPLATE : OnceLock < TempDir > = OnceLock :: new ( ) ;
1010
11+ /// An explicit executable selection for a hermetic test command.
12+ ///
13+ /// Fixture commands never resolve `homeboy` through `PATH`: integration tests
14+ /// select Cargo's binary and unit tests can select their current test binary.
15+ #[ derive( Clone , Copy , Debug , Eq , PartialEq ) ]
16+ pub enum TestBinary {
17+ CurrentTest ,
18+ HomeboyFixture ,
19+ }
20+
21+ /// Isolated filesystem and process environment for one test.
22+ ///
23+ /// Constructing this type does not mutate the parent process. Prefer
24+ /// [`HermeticTestContext::command`] for subprocess tests. `HomeGuard` exists
25+ /// only for legacy in-process tests whose dependencies still read environment
26+ /// variables directly.
27+ pub struct HermeticTestContext {
28+ root : TempDir ,
29+ runtime : TempDir ,
30+ invocation_runtime : TempDir ,
31+ }
32+
33+ impl HermeticTestContext {
34+ pub fn new ( ) -> Self {
35+ let context = Self {
36+ root : exec_capable_tempdir ( ) ,
37+ runtime : exec_capable_tempdir ( ) ,
38+ invocation_runtime : short_invocation_tempdir ( ) ,
39+ } ;
40+ for path in [
41+ context. root ( ) . join ( ".config" ) ,
42+ context. data_dir ( ) ,
43+ context. artifact_dir ( ) ,
44+ context. temp_dir ( ) ,
45+ context. daemon_dir ( ) ,
46+ context. runner_dir ( ) ,
47+ ] {
48+ fs:: create_dir_all ( path) . expect ( "create hermetic test path" ) ;
49+ }
50+ context
51+ }
52+
53+ pub fn root ( & self ) -> & Path {
54+ self . root . path ( )
55+ }
56+
57+ pub fn home ( & self ) -> & Path {
58+ self . root ( )
59+ }
60+
61+ pub fn config_dir ( & self ) -> PathBuf {
62+ self . home ( ) . join ( ".config/homeboy" )
63+ }
64+
65+ pub fn data_dir ( & self ) -> PathBuf {
66+ self . root ( ) . join ( "data/homeboy" )
67+ }
68+
69+ pub fn artifact_dir ( & self ) -> PathBuf {
70+ self . root ( ) . join ( "artifacts" )
71+ }
72+
73+ pub fn runtime_dir ( & self ) -> & Path {
74+ self . runtime . path ( )
75+ }
76+
77+ pub fn temp_dir ( & self ) -> PathBuf {
78+ self . root ( ) . join ( "tmp" )
79+ }
80+
81+ pub fn daemon_dir ( & self ) -> PathBuf {
82+ self . config_dir ( ) . join ( "daemon" )
83+ }
84+
85+ pub fn runner_dir ( & self ) -> PathBuf {
86+ self . config_dir ( ) . join ( "runners" )
87+ }
88+
89+ pub fn binary_path ( & self , binary : TestBinary ) -> PathBuf {
90+ match binary {
91+ TestBinary :: CurrentTest => std:: env:: current_exe ( ) . expect ( "current test executable" ) ,
92+ TestBinary :: HomeboyFixture => PathBuf :: from (
93+ std:: env:: var_os ( "CARGO_BIN_EXE_homeboy" )
94+ . expect ( "CARGO_BIN_EXE_homeboy fixture binary" ) ,
95+ ) ,
96+ }
97+ }
98+
99+ /// Build a command whose Homeboy state is wholly owned by this context.
100+ pub fn command ( & self , binary : TestBinary ) -> Command {
101+ let mut command = Command :: new ( self . binary_path ( binary) ) ;
102+ command
103+ . env ( "HOME" , self . home ( ) )
104+ . env ( "XDG_CONFIG_HOME" , self . root ( ) . join ( ".config" ) )
105+ . env ( "XDG_DATA_HOME" , self . root ( ) . join ( "data" ) )
106+ . env ( "HOMEBOY_ARTIFACT_ROOT" , self . artifact_dir ( ) )
107+ . env ( "HOMEBOY_RUNTIME_TMPDIR" , self . runtime_dir ( ) )
108+ . env ( "TMPDIR" , self . temp_dir ( ) )
109+ . env (
110+ crate :: core:: engine:: invocation:: HOMEBOY_INVOCATION_RUNTIME_DIR_ENV ,
111+ self . invocation_runtime . path ( ) ,
112+ )
113+ . env ( "HOMEBOY_NO_UPDATE_CHECK" , "1" ) ;
114+ command
115+ }
116+ }
117+
118+ impl Default for HermeticTestContext {
119+ fn default ( ) -> Self {
120+ Self :: new ( )
121+ }
122+ }
123+
11124pub ( crate ) struct HomeGuard {
12125 prior : Option < String > ,
13126 prior_xdg_data_home : Option < String > ,
14127 prior_artifact_root : Option < String > ,
15128 prior_runtime_tmpdir : Option < String > ,
16129 prior_invocation_runtime : Option < String > ,
17130 prior_no_update_check : Option < String > ,
18- dir : TempDir ,
19- _runtime_dir : TempDir ,
20- /// Held alongside `dir` so the short invocation runtime tempdir is
21- /// dropped only after the test completes. Distinct from `dir` so the
22- /// invocation root can live on a short path (e.g. `/tmp/hb-XXXX`)
23- /// regardless of where `$TMPDIR` lands.
24- _inv_dir : Option < TempDir > ,
131+ context : HermeticTestContext ,
25132 _guard : MutexGuard < ' static , ( ) > ,
26133}
27134
@@ -75,8 +182,7 @@ impl AuditHomeGuard {
75182impl HomeGuard {
76183 pub ( crate ) fn new ( ) -> Self {
77184 let guard = home_lock ( ) . lock ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
78- crate :: core:: defaults:: reset_config_cache_for_test ( ) ;
79- crate :: commands:: utils:: entity_suggest:: reset_entity_suggestion_cache_for_test ( ) ;
185+ reset_cached_test_state ( ) ;
80186 let prior = std:: env:: var ( "HOME" ) . ok ( ) ;
81187 let prior_xdg_data_home = std:: env:: var ( "XDG_DATA_HOME" ) . ok ( ) ;
82188 let prior_artifact_root = std:: env:: var ( "HOMEBOY_ARTIFACT_ROOT" ) . ok ( ) ;
@@ -90,23 +196,23 @@ impl HomeGuard {
90196 // failing every capability-script test with exit 126 (#6760). Anchor
91197 // it (and the runtime tmpdir, which also hosts executables) on an
92198 // exec-capable root.
93- let dir = exec_capable_tempdir ( ) ;
94- std:: env:: set_var ( "HOME" , dir. path ( ) ) ;
95- std:: env:: set_var ( "XDG_DATA_HOME" , dir. path ( ) . join ( ".local" ) . join ( "share" ) ) ;
199+ let context = HermeticTestContext :: new ( ) ;
200+ std:: env:: set_var ( "HOME" , context. home ( ) ) ;
201+ // Preserve the legacy in-process defaults while the subprocess context
202+ // uses explicit paths. These tests exercise fallback path resolution.
203+ std:: env:: set_var ( "XDG_DATA_HOME" , context. home ( ) . join ( ".local" ) . join ( "share" ) ) ;
96204 std:: env:: remove_var ( "HOMEBOY_ARTIFACT_ROOT" ) ;
97205 std:: env:: set_var ( "HOMEBOY_NO_UPDATE_CHECK" , "1" ) ;
98- let runtime_dir = exec_capable_tempdir ( ) ;
99- std:: env:: set_var ( "HOMEBOY_RUNTIME_TMPDIR" , runtime_dir. path ( ) ) ;
206+ std:: env:: set_var ( "HOMEBOY_RUNTIME_TMPDIR" , context. runtime_dir ( ) ) ;
100207 crate :: core:: set_artifact_root_override ( None ) ;
101208 // Pin invocation runtime to a SHORT tempdir, isolated from `$TMPDIR`
102209 // and from the home tempdir (which itself can already live on a long
103210 // path on macOS, e.g. `/var/folders/<14>/T/.tmpXXXXXX/...`). Using
104211 // `/tmp` directly keeps tests within the platform `sockaddr_un`
105212 // budget regardless of host configuration.
106- let inv_dir = short_invocation_tempdir ( ) ;
107213 std:: env:: set_var (
108214 crate :: core:: engine:: invocation:: HOMEBOY_INVOCATION_RUNTIME_DIR_ENV ,
109- inv_dir . path ( ) ,
215+ context . invocation_runtime . path ( ) ,
110216 ) ;
111217 Self {
112218 prior,
@@ -115,9 +221,7 @@ impl HomeGuard {
115221 prior_runtime_tmpdir,
116222 prior_invocation_runtime,
117223 prior_no_update_check,
118- dir,
119- _runtime_dir : runtime_dir,
120- _inv_dir : Some ( inv_dir) ,
224+ context,
121225 _guard : guard,
122226 }
123227 }
@@ -227,7 +331,7 @@ pub(crate) fn exec_capable_tempdir() -> TempDir {
227331 #[ cfg( unix) ]
228332 {
229333 let mut roots: Vec < PathBuf > = Vec :: new ( ) ;
230- let mut push = |path : PathBuf , roots : & mut Vec < PathBuf > | {
334+ let push = |path : PathBuf , roots : & mut Vec < PathBuf > | {
231335 if path. is_dir ( ) && !roots. contains ( & path) {
232336 roots. push ( path) ;
233337 }
@@ -286,21 +390,29 @@ impl Drop for HomeGuard {
286390 Some ( value) => std:: env:: set_var ( "HOMEBOY_NO_UPDATE_CHECK" , value) ,
287391 None => std:: env:: remove_var ( "HOMEBOY_NO_UPDATE_CHECK" ) ,
288392 }
289- crate :: core:: defaults:: reset_config_cache_for_test ( ) ;
290- crate :: commands:: utils:: entity_suggest:: reset_entity_suggestion_cache_for_test ( ) ;
393+ reset_cached_test_state ( ) ;
291394 }
292395}
293396
294397pub ( crate ) fn with_isolated_home < R > ( body : impl FnOnce ( & TempDir ) -> R ) -> R {
295398 let home = HomeGuard :: new ( ) ;
296- body ( & home. dir )
399+ body ( & home. context . root )
297400}
298401
299402pub ( crate ) fn with_isolated_audit_home < R > ( body : impl FnOnce ( & TempDir ) -> R ) -> R {
300403 let guard = AuditHomeGuard :: new ( ) ;
301- body ( & guard. home . dir )
404+ body ( & guard. home . context . root )
302405}
303406
407+ #[ cfg( test) ]
408+ fn reset_cached_test_state ( ) {
409+ crate :: core:: defaults:: reset_config_cache_for_test ( ) ;
410+ crate :: commands:: utils:: entity_suggest:: reset_entity_suggestion_cache_for_test ( ) ;
411+ }
412+
413+ #[ cfg( not( test) ) ]
414+ fn reset_cached_test_state ( ) { }
415+
304416pub ( crate ) fn write_source_extension ( home : & std:: path:: Path , id : & str , file_extension : & str ) {
305417 let extension_dir = home. join ( ".config/homeboy/extensions" ) . join ( id) ;
306418 std:: fs:: create_dir_all ( & extension_dir) . expect ( "extension dir" ) ;
0 commit comments