diff --git a/Cargo.lock b/Cargo.lock index 0eb6286e..1cbc1298 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -55,15 +55,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -139,9 +139,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -190,9 +190,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -202,15 +202,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -222,6 +222,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -324,10 +336,10 @@ dependencies = [ ] [[package]] -name = "env_home" -version = "0.1.0" +name = "encode_unicode" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "equivalent" @@ -651,6 +663,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "insta" +version = "1.46.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e82db8c87c7f1ccecb34ce0c24399b8a73081427f3c7c50a5d597925356115e4" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -687,9 +711,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" @@ -756,9 +780,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -902,6 +926,7 @@ dependencies = [ "flate2", "hostname", "ignore", + "insta", "lazy_static", "quick-xml", "regex", @@ -1076,6 +1101,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "smallvec" version = "1.15.1" @@ -1124,9 +1155,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -1415,13 +1446,11 @@ dependencies = [ [[package]] name = "which" -version = "8.0.1" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a824aeba0fbb27264f815ada4cff43d65b1741b7a4ed7629ff9089148c4a4e0" +checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459" dependencies = [ - "env_home", - "rustix", - "winsafe", + "libc", ] [[package]] @@ -1658,12 +1687,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -1783,18 +1806,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", diff --git a/src/gradle/compile.rs b/src/gradle/compile.rs new file mode 100644 index 00000000..8eaf858b --- /dev/null +++ b/src/gradle/compile.rs @@ -0,0 +1,214 @@ +use lazy_static::lazy_static; +use regex::RegexSet; + +use super::paths::normalize_paths; + +lazy_static! { + /// COMPILE-specific noise patterns compiled into a single RegexSet. + static ref COMPILE_NOISE: RegexSet = RegexSet::new([ + // kapt/KSP annotation processing noise + r"^(Annotation processing|kapt|ksp|KSP)", + r"kaptGenerateStubs", + // Incremental compilation messages + r"^(Incremental compilation|Performing full compilation|Full recompilation)", + // Resource processing + r"^Resource processing completed", + ]).unwrap(); +} + +/// Returns true if the task name is a compile task. +/// Matches any source set: compileKotlin, compileTestKotlin, compileIntegrationTestJava, etc. +/// Expects lowercase input from detect_task_type; output-based detection passes properly-cased names. +pub fn matches_task(task_name: &str) -> bool { + let t = task_name.to_ascii_lowercase(); + (t.starts_with("compile") && (t.ends_with("kotlin") || t.ends_with("java"))) + || t.ends_with("classes") +} + +/// Apply COMPILE-specific filtering on top of globally-filtered output. +/// +/// Drops kapt/KSP noise, incremental compilation info, resource processing. +/// Normalizes absolute paths to repo-relative. +pub fn filter_compile(input: &str) -> String { + let mut result = Vec::new(); + let mut task_names: Vec = Vec::new(); + + for line in input.lines() { + let trimmed = line.trim(); + + // Track executed tasks for ✓ summary + if trimmed.starts_with("> Task ") && trimmed.ends_with("compileKotlin") { + if let Some(task) = trimmed.strip_prefix("> Task ") { + task_names.push(task.to_string()); + } + continue; + } + + // Drop COMPILE-specific noise + if COMPILE_NOISE.is_match(trimmed) { + continue; + } + + result.push(line.to_string()); + } + + let mut output = normalize_paths(&result.join("\n")); + + // Add task ✓ summary after BUILD line + if !task_names.is_empty() { + let summary: Vec = task_names.iter().map(|t| format!("{} ✓", t)).collect(); + // Insert after BUILD SUCCESSFUL/FAILED line + if let Some(pos) = output.find("BUILD SUCCESSFUL") { + if let Some(end) = output[pos..].find('\n') { + let insert_pos = pos + end; + let tasks_str = summary.join("\n"); + output.insert_str(insert_pos, &format!("\n{}", tasks_str)); + } + } + } + + // Trim leading/trailing blank lines + let trimmed: Vec<&str> = output.lines().collect(); + let start = trimmed + .iter() + .position(|l| !l.trim().is_empty()) + .unwrap_or(0); + let end = trimmed + .iter() + .rposition(|l| !l.trim().is_empty()) + .map(|i| i + 1) + .unwrap_or(trimmed.len()); + trimmed[start..end].join("\n") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::gradle::global::apply_global_filters; + use insta::assert_snapshot; + + fn count_tokens(text: &str) -> usize { + text.split_whitespace().count() + } + + // --- matches_task tests --- + + #[test] + fn test_matches_compile_kotlin() { + assert!(matches_task("compileKotlin")); + } + + #[test] + fn test_matches_compile_test_kotlin() { + assert!(matches_task("compileTestKotlin")); + } + + #[test] + fn test_matches_compile_integration_test_kotlin() { + assert!(matches_task("compileIntegrationTestKotlin")); + } + + #[test] + fn test_matches_compile_java() { + assert!(matches_task("compileJava")); + } + + #[test] + fn test_matches_classes() { + assert!(matches_task("testClasses")); + assert!(matches_task("integrationTestClasses")); + } + + #[test] + fn test_matches_android_variant_compile() { + assert!(matches_task("compileDebugKotlin")); + assert!(matches_task("compileReleaseJava")); + } + + #[test] + fn test_no_match_test() { + assert!(!matches_task("test")); + } + + #[test] + fn test_no_match_detekt() { + assert!(!matches_task("detekt")); + } + + // --- filter tests --- + + #[test] + fn test_compile_success_snapshot() { + let input = include_str!("../../tests/fixtures/gradle/compile_success_raw.txt"); + let globally_filtered = apply_global_filters(input); + let output = filter_compile(&globally_filtered); + assert_snapshot!(output); + } + + #[test] + fn test_compile_failure_snapshot() { + let input = include_str!("../../tests/fixtures/gradle/compile_failure_raw.txt"); + let globally_filtered = apply_global_filters(input); + let output = filter_compile(&globally_filtered); + assert_snapshot!(output); + } + + #[test] + fn test_compile_failure_token_savings() { + let input = include_str!("../../tests/fixtures/gradle/compile_failure_raw.txt"); + let globally_filtered = apply_global_filters(input); + let output = filter_compile(&globally_filtered); + let input_tokens = count_tokens(input); + let output_tokens = count_tokens(&output); + let savings = 100.0 - (output_tokens as f64 / input_tokens as f64 * 100.0); + assert!( + savings >= 60.0, + "Expected >=60% savings on compile failure, got {:.1}% (input={}, output={})", + savings, + input_tokens, + output_tokens + ); + } + + #[test] + fn test_compile_preserves_error_lines() { + let input = include_str!("../../tests/fixtures/gradle/compile_failure_raw.txt"); + let globally_filtered = apply_global_filters(input); + let output = filter_compile(&globally_filtered); + assert!(output.contains("Unresolved reference: Bar")); + assert!(output.contains("Type mismatch: expected String, got Int")); + } + + #[test] + fn test_compile_normalizes_paths() { + let input = include_str!("../../tests/fixtures/gradle/compile_failure_raw.txt"); + let globally_filtered = apply_global_filters(input); + let output = filter_compile(&globally_filtered); + assert!( + !output.contains("/Users/developer/"), + "Absolute paths should be normalized" + ); + assert!( + output.contains("app-payments/src/main/kotlin/com/example/payments/Foo.kt"), + "Path should be relative" + ); + } + + #[test] + fn test_compile_drops_kapt_noise() { + let input = "Annotation processing was successful. Generated 42 files.\nkaptGenerateStubsKotlin completed\ne: Foo.kt:1 Error\nBUILD FAILED in 5s"; + let output = filter_compile(input); + assert!(!output.contains("Annotation processing")); + assert!(!output.contains("kaptGenerateStubs")); + assert!(output.contains("Error")); + } + + #[test] + fn test_compile_drops_incremental() { + let input = "Incremental compilation was started but abandoned\nPerforming full compilation\ne: Foo.kt:1 Error\nBUILD FAILED in 5s"; + let output = filter_compile(input); + assert!(!output.contains("Incremental compilation")); + assert!(!output.contains("Performing full")); + assert!(output.contains("Error")); + } +} diff --git a/src/gradle/mod.rs b/src/gradle/mod.rs index 2cc44363..5fdb2455 100644 --- a/src/gradle/mod.rs +++ b/src/gradle/mod.rs @@ -1,3 +1,4 @@ +pub mod compile; pub mod global; pub mod paths; @@ -19,7 +20,8 @@ pub enum TaskType { /// Registry of task type matchers, checked in priority order. /// Registry of task type matchers, checked in priority order. /// Per-module matchers added as filters land in subsequent PRs. -const TASK_TYPE_REGISTRY: &[(fn(&str) -> bool, TaskType)] = &[]; +const TASK_TYPE_REGISTRY: &[(fn(&str) -> bool, TaskType)] = + &[(compile::matches_task, TaskType::Compile)]; /// Detect the task type from gradle arguments. /// @@ -252,6 +254,7 @@ pub fn filter_gradle_output(raw: &str, task_type: &TaskType) -> String { let filtered = global::apply_global_filters(raw); match task_type { + TaskType::Compile => compile::filter_compile(&filtered), TaskType::Generic => filtered, // Per-task filters added in subsequent PRs _ => filtered, diff --git a/src/gradle/snapshots/rtk__gradle__compile__tests__compile_failure_snapshot.snap b/src/gradle/snapshots/rtk__gradle__compile__tests__compile_failure_snapshot.snap new file mode 100644 index 00000000..81f93318 --- /dev/null +++ b/src/gradle/snapshots/rtk__gradle__compile__tests__compile_failure_snapshot.snap @@ -0,0 +1,22 @@ +--- +source: src/gradle/compile.rs +expression: output +--- +> Task :backend:app-payments:compileKotlin FAILED +w: app-payments/src/main/kotlin/com/example/payments/internal/ProcessorFactory.kt:12:5 'inject' is deprecated. Use constructor injection instead. +e: app-payments/src/main/kotlin/com/example/payments/Foo.kt:42:5 Unresolved reference: Bar + val x: Bar = TODO() + ^ +e: app-payments/src/main/kotlin/com/example/payments/Foo.kt:55:10 Type mismatch: expected String, got Int + fun process(): String = computeValue() + ^ +w: app-payments/src/main/kotlin/com/example/payments/legacy/OldService.kt:8:1 Parameter 'unused' is never used + +FAILURE: Build failed with an exception. + +* What went wrong: +Execution failed for task ':backend:app-payments:compileKotlin'. +> Compilation failed; see the compiler error output for details. + +BUILD FAILED in 5s +6 actionable tasks: 2 executed, 4 up-to-date diff --git a/src/gradle/snapshots/rtk__gradle__compile__tests__compile_success_snapshot.snap b/src/gradle/snapshots/rtk__gradle__compile__tests__compile_success_snapshot.snap new file mode 100644 index 00000000..43fdf417 --- /dev/null +++ b/src/gradle/snapshots/rtk__gradle__compile__tests__compile_success_snapshot.snap @@ -0,0 +1,6 @@ +--- +source: src/gradle/compile.rs +expression: output +--- +BUILD SUCCESSFUL in 1m 38s +1558 actionable tasks: 210 executed, 583 from cache, 765 up-to-date diff --git a/tests/fixtures/gradle/compile_failure_raw.txt b/tests/fixtures/gradle/compile_failure_raw.txt new file mode 100644 index 00000000..d7abcf40 --- /dev/null +++ b/tests/fixtures/gradle/compile_failure_raw.txt @@ -0,0 +1,73 @@ +Configuration on demand is an incubating feature. +Parallel Configuration Cache is an incubating feature. +Calculating task graph as no cached configuration is available for tasks: :backend:app-payments:compileKotlin +VFS> Statistics since last build: +VFS> > Stat: Executed stat() x 0. getUnixMode() x 0 +VFS> > FileHasher: Hashed 0 files (0 bytes) +Received 12 file system events since last build while watching 1 locations +Virtual file system retained information about 125611 files, 17699 directories and 769 missing files since last build + +> Task :protos:protos-core-versioning:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-currency:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :events:checkKotlinGradlePluginConfigurationErrors SKIPPED +> Task :protos:protos-core-versioning:generateMainProtos UP-TO-DATE +> Task :protos:protos-core-versioning:generateProtos UP-TO-DATE +> Task :protos:protos-core-versioning:processResources UP-TO-DATE +> Task :protos:protos-core-versioning:compileKotlin UP-TO-DATE +> Task :protos:protos-core-versioning:compileJava NO-SOURCE +> Task :protos:protos-core-versioning:classes UP-TO-DATE +> Task :protos:protos-core-versioning:jar UP-TO-DATE +> Task :core:core-extensions:processResources NO-SOURCE +> Task :core:core-extensions:compileKotlin FROM-CACHE +> Task :core:core-extensions:classes UP-TO-DATE +> Task :core:core-extensions:jar UP-TO-DATE +> Task :backend:app-common:kspKotlin FROM-CACHE +> Task :backend:app-common:compileKotlin FROM-CACHE +> Task :backend:app-common:compileJava NO-SOURCE +> Task :backend:app-common:classes UP-TO-DATE +> Task :backend:app-common:jar +> Task :backend:app-payments:kaptGenerateStubsKotlin UP-TO-DATE +> Task :backend:app-payments:kaptKotlin UP-TO-DATE +> Task :backend:app-payments:compileKotlin FAILED +w: /Users/developer/example-backend/backend/app-payments/src/main/kotlin/com/example/payments/internal/ProcessorFactory.kt:12:5 'inject' is deprecated. Use constructor injection instead. +e: /Users/developer/example-backend/backend/app-payments/src/main/kotlin/com/example/payments/Foo.kt:42:5 Unresolved reference: Bar + val x: Bar = TODO() + ^ +e: /Users/developer/example-backend/backend/app-payments/src/main/kotlin/com/example/payments/Foo.kt:55:10 Type mismatch: expected String, got Int + fun process(): String = computeValue() + ^ +w: /Users/developer/example-backend/backend/app-payments/src/main/kotlin/com/example/payments/legacy/OldService.kt:8:1 Parameter 'unused' is never used +Annotation processing was successful. Generated 42 files. +Incremental compilation was started but abandoned because of changes in the classpath +Performing full compilation as incremental compilation is not supported for this build configuration +Resource processing completed in 250ms + +[Incubating] Problems report is available at: file:///Users/developer/example-backend/build/reports/problems/problems-report.html + +Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0. + +You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins. + +For more on this, please refer to https://docs.gradle.org/8.14/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation. + +FAILURE: Build failed with an exception. + +* What went wrong: +Execution failed for task ':backend:app-payments:compileKotlin'. +> Compilation failed; see the compiler error output for details. + +* Try: +> Run with --stacktrace option to get the stack trace. +> Run with --info or --debug option to get more log output. +> Run with --scan to get full insights. +> Get more help at https://help.gradle.org. + +BUILD FAILED in 5s +6 actionable tasks: 2 executed, 4 up-to-date +Received 1024 file system events during the current build while watching 1 locations +Virtual file system retains information about 125611 files, 17699 directories and 769 missing files until next build + +Publishing build scan... +https://scans.gradle.com/s/abc123xyz + +Configuration cache entry stored.