@@ -65,7 +65,8 @@ impl TraceSessionBootstrap {
6565 enverr ! ( ErrorCode :: Io , "failed to collect program metadata" )
6666 . with_context ( "details" , err. to_string ( ) )
6767 } ) ?;
68- let trace_filter = load_trace_filter ( explicit_trace_filters, & metadata. program ) ?;
68+ let trace_filter =
69+ load_trace_filter ( explicit_trace_filters, activation_path, & metadata. program ) ?;
6970 Ok ( Self {
7071 trace_directory : trace_directory. to_path_buf ( ) ,
7172 format,
@@ -164,11 +165,12 @@ pub fn collect_program_metadata(py: Python<'_>) -> PyResult<ProgramMetadata> {
164165
165166fn load_trace_filter (
166167 explicit : Option < & [ PathBuf ] > ,
168+ activation_path : Option < & Path > ,
167169 program : & str ,
168170) -> Result < Option < Arc < TraceFilterEngine > > > {
169171 let mut chain: Vec < PathBuf > = Vec :: new ( ) ;
170172
171- if let Some ( default) = discover_default_trace_filter ( program) ? {
173+ if let Some ( default) = discover_default_trace_filter ( activation_path , program) ? {
172174 chain. push ( default) ;
173175 }
174176
@@ -183,17 +185,42 @@ fn load_trace_filter(
183185 Ok ( Some ( Arc :: new ( TraceFilterEngine :: new ( config) ) ) )
184186}
185187
186- fn discover_default_trace_filter ( program : & str ) -> Result < Option < PathBuf > > {
187- let start_dir = resolve_program_directory ( program) ?;
188- let mut current: Option < & Path > = Some ( start_dir. as_path ( ) ) ;
189- while let Some ( dir) = current {
188+ fn discover_default_trace_filter (
189+ activation_path : Option < & Path > ,
190+ program : & str ,
191+ ) -> Result < Option < PathBuf > > {
192+ if let Some ( path) = activation_path {
193+ let activation_dir = resolve_activation_directory ( path) ?;
194+ if let Some ( found) = discover_filter_near ( activation_dir) {
195+ return Ok ( Some ( found) ) ;
196+ }
197+ }
198+
199+ let program_dir = resolve_program_directory ( program) ?;
200+ if let Some ( found) = discover_filter_near ( program_dir) {
201+ return Ok ( Some ( found) ) ;
202+ }
203+ Ok ( None )
204+ }
205+
206+ fn discover_filter_near ( mut dir : PathBuf ) -> Option < PathBuf > {
207+ loop {
190208 let candidate = dir. join ( TRACE_FILTER_DIR ) . join ( TRACE_FILTER_FILE ) ;
191209 if matches ! ( fs:: metadata( & candidate) , Ok ( metadata) if metadata. is_file( ) ) {
192- return Ok ( Some ( candidate) ) ;
210+ return Some ( candidate) ;
211+ }
212+ if !dir. pop ( ) {
213+ break ;
193214 }
194- current = dir. parent ( ) ;
195215 }
196- Ok ( None )
216+ None
217+ }
218+
219+ fn resolve_activation_directory ( path : & Path ) -> Result < PathBuf > {
220+ if path. as_os_str ( ) . is_empty ( ) {
221+ return current_directory ( ) ;
222+ }
223+ resolve_path_directory ( path)
197224}
198225
199226fn resolve_program_directory ( program : & str ) -> Result < PathBuf > {
@@ -203,6 +230,10 @@ fn resolve_program_directory(program: &str) -> Result<PathBuf> {
203230 }
204231
205232 let path = Path :: new ( trimmed) ;
233+ resolve_path_directory ( path)
234+ }
235+
236+ fn resolve_path_directory ( path : & Path ) -> Result < PathBuf > {
206237 if path. is_absolute ( ) {
207238 if path. is_dir ( ) {
208239 return Ok ( path. to_path_buf ( ) ) ;
@@ -434,6 +465,76 @@ mod tests {
434465 } ) ;
435466 }
436467
468+ #[ test]
469+ fn prepare_bootstrap_prefers_activation_path_filter_over_host_program ( ) {
470+ Python :: with_gil ( |py| {
471+ let project = tempdir ( ) . expect ( "project" ) ;
472+ let project_root = project. path ( ) ;
473+ let trace_dir = project_root. join ( "trace-out" ) ;
474+
475+ let wrapper_dir = project_root. join ( "wrapper" ) ;
476+ std:: fs:: create_dir_all ( & wrapper_dir) . expect ( "create wrapper dir" ) ;
477+ let wrapper_script = wrapper_dir. join ( "launcher.py" ) ;
478+ std:: fs:: write ( & wrapper_script, "print('wrapper')\n " ) . expect ( "write wrapper" ) ;
479+
480+ let target_dir = project_root. join ( "target" ) ;
481+ std:: fs:: create_dir_all ( & target_dir) . expect ( "create target dir" ) ;
482+ let target_script = target_dir. join ( "app.py" ) ;
483+ std:: fs:: write ( & target_script, "print('target')\n " ) . expect ( "write target" ) ;
484+
485+ let filters_dir = target_dir. join ( TRACE_FILTER_DIR ) ;
486+ std:: fs:: create_dir_all ( & filters_dir) . expect ( "create filter dir" ) ;
487+ let filter_path = filters_dir. join ( TRACE_FILTER_FILE ) ;
488+ std:: fs:: write (
489+ & filter_path,
490+ r#"
491+ [meta]
492+ name = "target"
493+ version = 1
494+
495+ [scope]
496+ default_exec = "trace"
497+ default_value_action = "allow"
498+
499+ [[scope.rules]]
500+ selector = "pkg:target"
501+ exec = "trace"
502+ value_default = "allow"
503+ "# ,
504+ )
505+ . expect ( "write project filter" ) ;
506+
507+ let sys = py. import ( "sys" ) . expect ( "import sys" ) ;
508+ let original = sys. getattr ( "argv" ) . expect ( "argv" ) . unbind ( ) ;
509+ let argv = PyList :: new (
510+ py,
511+ [ wrapper_script. to_str ( ) . expect ( "utf8 path" ) , "--forwarded" ] ,
512+ )
513+ . expect ( "argv" ) ;
514+ sys. setattr ( "argv" , argv) . expect ( "set argv" ) ;
515+
516+ let result = TraceSessionBootstrap :: prepare (
517+ py,
518+ trace_dir. as_path ( ) ,
519+ "json" ,
520+ Some ( target_script. as_path ( ) ) ,
521+ None ,
522+ ) ;
523+ sys. setattr ( "argv" , original. bind ( py) )
524+ . expect ( "restore argv" ) ;
525+
526+ let bootstrap = result. expect ( "bootstrap" ) ;
527+ let engine = bootstrap. trace_filter ( ) . expect ( "filter engine" ) ;
528+ let summary = engine. summary ( ) ;
529+ assert_eq ! ( summary. entries. len( ) , 2 ) ;
530+ assert_eq ! (
531+ summary. entries[ 0 ] . path,
532+ PathBuf :: from( "<inline:builtin-default>" )
533+ ) ;
534+ assert_eq ! ( summary. entries[ 1 ] . path, filter_path) ;
535+ } ) ;
536+ }
537+
437538 #[ test]
438539 fn prepare_bootstrap_merges_explicit_trace_filters ( ) {
439540 Python :: with_gil ( |py| {
0 commit comments