Skip to content

Commit 246eb5f

Browse files
romtsnclaude
andcommitted
ref(bundle-jvm): Inline collision warning and drop UrlCollision
Addresses the second low-priority review comment on #3275: emit the URL-collision warning directly inside build_source_files instead of returning a Vec<UrlCollision> for the caller to walk. - build_source_files now returns just Vec<SourceFile> - UrlCollision struct removed - Per-collision PathBuf+String clones (previously held in UrlCollision) are eliminated; the function borrows the kept path from the files Vec and the URL from the HashMap key - Add a test-only log_capture module (thread-local buffer, one-shot Once-guarded logger install) so the collision tests can assert on the actual warn! output instead of the collision list No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a3e41aa commit 246eb5f

1 file changed

Lines changed: 84 additions & 40 deletions

File tree

src/commands/debug_files/bundle_jvm.rs

Lines changed: 84 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
272307
mod 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

Comments
 (0)