@@ -60,22 +60,14 @@ fn build_source_url(relative_path: &Path) -> String {
6060 format ! ( "~/{}" , path_as_url( & package_path_jvm_ext) )
6161}
6262
63- /// Records a dropped duplicate file whose URL was already seen.
64- #[ derive( Debug , PartialEq , Eq ) ]
65- struct UrlCollision {
66- url : String ,
67- skipped_path : PathBuf ,
68- kept_path : PathBuf ,
69- }
70-
7163/// Turns walked source files into `SourceFile`s for bundling, filtering out
7264/// build-output directories and deduplicating by URL.
7365///
7466/// Android build variants can contribute the same FQCN from different source
7567/// sets (e.g. `src/main/` and `src/debug/` both defining `com.example.Foo`).
7668/// After stripping, both map to the same URL — this keeps the first-seen
77- /// entry and records the rest as collisions so the caller can warn the user .
78- fn build_source_files ( sources : Vec < ReleaseFileMatch > ) -> ( Vec < SourceFile > , Vec < UrlCollision > ) {
69+ /// entry and warns the user about the rest .
70+ fn build_source_files ( sources : Vec < ReleaseFileMatch > ) -> Vec < SourceFile > {
7971 let candidates = sources. into_iter ( ) . filter_map ( |source| {
8072 let local_path = source. path . strip_prefix ( & source. base_path ) . unwrap ( ) ;
8173 if is_in_ambiguous_build_dir ( local_path) {
@@ -88,16 +80,18 @@ fn build_source_files(sources: Vec<ReleaseFileMatch>) -> (Vec<SourceFile>, Vec<U
8880
8981 let mut seen_urls: HashMap < String , usize > = HashMap :: new ( ) ;
9082 let mut files: Vec < SourceFile > = Vec :: new ( ) ;
91- let mut collisions: Vec < UrlCollision > = Vec :: new ( ) ;
9283
9384 for ( url, source) in candidates {
9485 match seen_urls. entry ( url) {
9586 Entry :: Occupied ( existing) => {
96- collisions. push ( UrlCollision {
97- url : existing. key ( ) . clone ( ) ,
98- skipped_path : source. path ,
99- kept_path : files[ * existing. get ( ) ] . path . clone ( ) ,
100- } ) ;
87+ warn ! (
88+ "URL collision on {}: skipping '{}' (already bundled from '{}'). \
89+ Use --exclude to drop the unwanted source set \
90+ (e.g. --exclude='**/src/debug/**').",
91+ existing. key( ) ,
92+ source. path. display( ) ,
93+ files[ * existing. get( ) ] . path. display( ) ,
94+ ) ;
10195 }
10296 Entry :: Vacant ( slot) => {
10397 let url = slot. key ( ) . clone ( ) ;
@@ -114,7 +108,7 @@ fn build_source_files(sources: Vec<ReleaseFileMatch>) -> (Vec<SourceFile>, Vec<U
114108 }
115109 }
116110 }
117- ( files, collisions )
111+ files
118112}
119113
120114/// Safe to exclude globally — can never be valid JVM package names.
@@ -247,17 +241,7 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
247241 . sort_entries ( true )
248242 . collect_files ( ) ?;
249243
250- let ( files, collisions) = build_source_files ( sources) ;
251- for c in & collisions {
252- warn ! (
253- "URL collision on {}: skipping '{}' (already bundled from '{}'). \
254- Use --exclude to drop the unwanted source set \
255- (e.g. --exclude='**/src/debug/**').",
256- c. url,
257- c. skipped_path. display( ) ,
258- c. kept_path. display( ) ,
259- ) ;
260- }
244+ let files = build_source_files ( sources) ;
261245
262246 let tempfile = source_bundle:: build ( context, files, Some ( * debug_id) )
263247 . context ( "Unable to create source bundle" ) ?;
@@ -268,6 +252,57 @@ pub fn execute(matches: &ArgMatches) -> Result<()> {
268252 Ok ( ( ) )
269253}
270254
255+ #[ cfg( test) ]
256+ mod log_capture {
257+ use log:: { Level , LevelFilter , Log , Metadata , Record } ;
258+ use std:: cell:: RefCell ;
259+ use std:: sync:: Once ;
260+
261+ thread_local ! {
262+ static BUFFER : RefCell <Vec <( Level , String ) >> = const { RefCell :: new( Vec :: new( ) ) } ;
263+ }
264+
265+ struct CaptureLogger ;
266+
267+ impl Log for CaptureLogger {
268+ fn enabled ( & self , _: & Metadata ) -> bool {
269+ true
270+ }
271+
272+ fn log ( & self , record : & Record ) {
273+ BUFFER . with ( |b| {
274+ b. borrow_mut ( )
275+ . push ( ( record. level ( ) , record. args ( ) . to_string ( ) ) )
276+ } ) ;
277+ }
278+
279+ fn flush ( & self ) { }
280+ }
281+
282+ static LOGGER : CaptureLogger = CaptureLogger ;
283+
284+ /// Installs the capture logger (once per process) and clears this
285+ /// thread's buffer so a test starts from a clean slate.
286+ pub fn setup ( ) {
287+ static ONCE : Once = Once :: new ( ) ;
288+ ONCE . call_once ( || {
289+ let _ = log:: set_logger ( & LOGGER ) ;
290+ log:: set_max_level ( LevelFilter :: Trace ) ;
291+ } ) ;
292+ BUFFER . with ( |b| b. borrow_mut ( ) . clear ( ) ) ;
293+ }
294+
295+ pub fn warnings ( ) -> Vec < String > {
296+ BUFFER . with ( |b| {
297+ b. borrow ( )
298+ . iter ( )
299+ . filter ( |( lvl, _) | * lvl == Level :: Warn )
300+ . map ( |( _, msg) | msg. clone ( ) )
301+ . collect ( )
302+ } )
303+ }
304+ }
305+
271306#[ cfg( test) ]
272307mod tests {
273308 use super :: * ;
@@ -452,14 +487,16 @@ mod tests {
452487 }
453488
454489 #[ test]
455- fn test_build_source_files_records_collision_for_android_variants ( ) {
490+ fn test_build_source_files_warns_on_collision_for_android_variants ( ) {
456491 // Sources arrive pre-sorted from `ReleaseFileSearch` (which configures
457492 // `WalkBuilder::sort_by_file_name`); the first-seen wins in the dedup.
493+ log_capture:: setup ( ) ;
494+
458495 let sources = vec ! [
459496 fake_source( "/app" , "src/debug/java/com/example/Config.java" ) ,
460497 fake_source( "/app" , "src/main/java/com/example/Config.java" ) ,
461498 ] ;
462- let ( files, collisions ) = build_source_files ( sources) ;
499+ let files = build_source_files ( sources) ;
463500
464501 assert_eq ! ( files. len( ) , 1 ) ;
465502 assert_eq ! ( files[ 0 ] . url, "~/com/example/Config.jvm" ) ;
@@ -468,26 +505,33 @@ mod tests {
468505 Path :: new( "/app/src/debug/java/com/example/Config.java" )
469506 ) ;
470507
471- assert_eq ! ( collisions. len( ) , 1 ) ;
472- assert_eq ! ( collisions[ 0 ] . url, "~/com/example/Config.jvm" ) ;
473- assert_eq ! (
474- collisions[ 0 ] . skipped_path,
475- Path :: new( "/app/src/main/java/com/example/Config.java" )
508+ let warnings = log_capture:: warnings ( ) ;
509+ assert_eq ! ( warnings. len( ) , 1 ) ;
510+ let msg = & warnings[ 0 ] ;
511+ assert ! (
512+ msg. contains( "URL collision on ~/com/example/Config.jvm" ) ,
513+ "{msg}"
476514 ) ;
477- assert_eq ! (
478- collisions[ 0 ] . kept_path,
479- Path :: new( "/app/src/debug/java/com/example/Config.java" )
515+ assert ! (
516+ msg. contains( "/app/src/main/java/com/example/Config.java" ) ,
517+ "missing skipped path in: {msg}"
518+ ) ;
519+ assert ! (
520+ msg. contains( "/app/src/debug/java/com/example/Config.java" ) ,
521+ "missing kept path in: {msg}"
480522 ) ;
481523 }
482524
483525 #[ test]
484526 fn test_build_source_files_keeps_distinct_urls ( ) {
527+ log_capture:: setup ( ) ;
528+
485529 let sources = vec ! [
486530 fake_source( "/app" , "src/main/java/com/example/Foo.java" ) ,
487531 fake_source( "/app" , "src/main/java/com/example/Bar.java" ) ,
488532 ] ;
489- let ( files, collisions ) = build_source_files ( sources) ;
533+ let files = build_source_files ( sources) ;
490534 assert_eq ! ( files. len( ) , 2 ) ;
491- assert ! ( collisions . is_empty( ) ) ;
535+ assert ! ( log_capture :: warnings ( ) . is_empty( ) ) ;
492536 }
493537}
0 commit comments