Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub struct Config {
#[serde(default)]
pub hooks: HooksConfig,
#[serde(default)]
pub limits: LimitsConfig,
#[serde(default)]
pub gradle: GradleConfig,
}

Expand Down Expand Up @@ -96,6 +98,37 @@ impl Default for TelemetryConfig {
}
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LimitsConfig {
/// Max total grep results to show (default: 200)
pub grep_max_results: usize,
/// Max matches per file in grep output (default: 25)
pub grep_max_per_file: usize,
/// Max staged/modified files shown in git status (default: 15)
pub status_max_files: usize,
/// Max untracked files shown in git status (default: 10)
pub status_max_untracked: usize,
/// Max chars for parser passthrough fallback (default: 2000)
pub passthrough_max_chars: usize,
}

impl Default for LimitsConfig {
fn default() -> Self {
Self {
grep_max_results: 200,
grep_max_per_file: 25,
status_max_files: 15,
status_max_untracked: 10,
passthrough_max_chars: 2000,
}
}
}

/// Get limits config. Falls back to defaults if config can't be loaded.
pub fn limits() -> LimitsConfig {
Config::load().map(|c| c.limits).unwrap_or_default()
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GradleConfig {
/// Package prefixes treated as "user code" in stack traces (kept, not dropped).
Expand Down
6 changes: 1 addition & 5 deletions src/gradle/batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,7 @@ fn detect_task_type_from_name(task_name: &str) -> TaskType {
fn filter_section_content(content: &str, task_type: &TaskType, task_name: &str) -> String {
match task_type {
TaskType::Compile => compile::filter_compile(content),
TaskType::Test => {
let name = task_name.rsplit(':').next().unwrap_or(task_name);
let is_integration = test_filter::is_integration_task_name(name);
test_filter::filter_test(content, is_integration)
}
TaskType::Test => test_filter::filter_test(content),
TaskType::Detekt => detekt::filter_detekt(content),
_ => content.to_string(),
}
Expand Down
48 changes: 3 additions & 45 deletions src/gradle/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,6 @@ pub fn detect_task_type_from_output(raw: &str) -> TaskType {
}
}

/// Returns true if any of the given args refer to an integration/component/instrumented test task.
pub fn is_integration_test(args: &[String]) -> bool {
args.iter().any(|arg| {
let task_name = match arg.rfind(':') {
Some(pos) => arg[pos + 1..].to_ascii_lowercase(),
None => arg.to_ascii_lowercase(),
};
test_filter::is_integration_task_name(&task_name)
})
}

/// Find the gradle executable: prefer ./gradlew walking up parent dirs, fall back to gradle on PATH.
fn find_gradle_executable() -> String {
let candidates = [
Expand Down Expand Up @@ -240,8 +229,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
if task_type == TaskType::Generic {
task_type = detect_task_type_from_output(&raw);
}
let is_integration = is_integration_test(args);
let filtered = filter_gradle_output(&raw, &task_type, is_integration);
let filtered = filter_gradle_output(&raw, &task_type);

let exit_code = output
.status
Expand Down Expand Up @@ -272,7 +260,7 @@ pub fn run(args: &[String], verbose: u8) -> Result<()> {
}

/// Apply task-type-specific filtering to gradle output.
pub fn filter_gradle_output(raw: &str, task_type: &TaskType, is_integration: bool) -> String {
pub fn filter_gradle_output(raw: &str, task_type: &TaskType) -> String {
// For batch runs (multiple executed tasks), use batch filter on raw input
// regardless of detected task type — batch filter splits by task boundaries
// and applies per-section filters, preserving per-task context.
Expand All @@ -285,7 +273,7 @@ pub fn filter_gradle_output(raw: &str, task_type: &TaskType, is_integration: boo

match task_type {
TaskType::Compile => compile::filter_compile(&filtered),
TaskType::Test => test_filter::filter_test(&filtered, is_integration),
TaskType::Test => test_filter::filter_test(&filtered),
TaskType::Detekt => detekt::filter_detekt(&filtered),
TaskType::Health => health::filter_health(&filtered),
TaskType::Proto => proto::filter_proto(&filtered),
Expand Down Expand Up @@ -589,30 +577,6 @@ mod tests {
assert_eq!(detect_task_type_from_output(output), TaskType::Generic);
}

// --- is_integration_test tests ---

#[test]
fn test_is_integration_test_positive() {
let args = vec![":app:billing:integrationTest".to_string()];
assert!(is_integration_test(&args));
}

#[test]
fn test_is_integration_test_negative() {
let args = vec![":app:billing:test".to_string()];
assert!(!is_integration_test(&args));
}

#[test]
fn test_is_integration_test_mixed_args() {
let args = vec![
"--continue".to_string(),
":app:billing:integrationTest".to_string(),
"--info".to_string(),
];
assert!(is_integration_test(&args));
}

// --- case-insensitive matching tests ---

#[test]
Expand Down Expand Up @@ -651,12 +615,6 @@ mod tests {
assert_eq!(detect_task_type(&args), TaskType::Proto);
}

#[test]
fn test_is_integration_test_case_insensitive() {
let args = vec![":app:billing:IntegrationTest".to_string()];
assert!(is_integration_test(&args));
}

// --- stderr noise filtering tests ---

#[test]
Expand Down
51 changes: 10 additions & 41 deletions src/gradle/test_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,6 @@ pub fn matches_task(task_name: &str) -> bool {
|| t.starts_with("connected")
}

/// Returns true if the task name specifically refers to an integration/component/instrumented test.
/// Used to enable integration-specific noise filtering (Hibernate, Spring, etc.)
/// Case-insensitive: callers may pass lowercase (CLI args) or original casing.
pub fn is_integration_task_name(task_name: &str) -> bool {
let t = task_name.to_ascii_lowercase();
t == "integrationtest"
|| t == "componenttest"
|| t.contains("androidtest")
|| t.starts_with("connected")
}

/// Built-in framework prefixes that are always dropped from stack traces.
/// These are JDK/Kotlin stdlib and internal packages — universally noise.
const BUILTIN_FRAMEWORK_PREFIXES: &[&str] = &[
Expand Down Expand Up @@ -91,7 +80,7 @@ fn build_framework_regex(drop_frame_packages: &[String]) -> Regex {
}

/// Apply TEST-specific filtering on top of globally-filtered output.
pub fn filter_test(input: &str, _is_integration: bool) -> String {
pub fn filter_test(input: &str) -> String {
let (user_packages, drop_frame_packages) = load_config();
filter_test_with_config(input, &user_packages, &drop_frame_packages)
}
Expand All @@ -108,11 +97,7 @@ fn load_config() -> (Vec<String>, Vec<String>) {
}

/// Core test filter logic, testable with explicit config.
pub fn filter_test_with_packages(
input: &str,
_is_integration: bool,
user_packages: &[String],
) -> String {
pub fn filter_test_with_packages(input: &str, user_packages: &[String]) -> String {
let drop_frame_packages = crate::config::default_drop_frame_packages();
filter_test_with_config(input, user_packages, &drop_frame_packages)
}
Expand Down Expand Up @@ -399,21 +384,6 @@ mod tests {
assert!(matches_task("connectedAndroidTest"));
}

// --- is_integration_task_name tests ---

#[test]
fn test_integration_task_name_positive() {
assert!(is_integration_task_name("integrationTest"));
assert!(is_integration_task_name("componentTest"));
assert!(is_integration_task_name("connectedDebugAndroidTest"));
}

#[test]
fn test_integration_task_name_negative() {
assert!(!is_integration_task_name("test"));
assert!(!is_integration_task_name("testDebugUnitTest"));
}

// --- build_framework_regex tests ---

#[test]
Expand Down Expand Up @@ -463,40 +433,39 @@ mod tests {
fn test_test_success_snapshot() {
let input = include_str!("../../tests/fixtures/gradle/test_success_raw.txt");
let globally_filtered = apply_global_filters(input);
let output = filter_test(&globally_filtered, false);
let output = filter_test(&globally_filtered);
assert_snapshot!(output);
}

#[test]
fn test_test_failure_snapshot() {
let input = include_str!("../../tests/fixtures/gradle/test_failure_raw.txt");
let globally_filtered = apply_global_filters(input);
let output = filter_test(&globally_filtered, false);
let output = filter_test(&globally_filtered);
assert_snapshot!(output);
}

#[test]
fn test_test_failure_with_user_packages_snapshot() {
let input = include_str!("../../tests/fixtures/gradle/test_failure_raw.txt");
let globally_filtered = apply_global_filters(input);
let output =
filter_test_with_packages(&globally_filtered, false, &["com.example".to_string()]);
let output = filter_test_with_packages(&globally_filtered, &["com.example".to_string()]);
assert_snapshot!(output);
}

#[test]
fn test_integration_test_failure_snapshot() {
let input = include_str!("../../tests/fixtures/gradle/integration_test_failure_raw.txt");
let globally_filtered = apply_global_filters(input);
let output = filter_test(&globally_filtered, true);
let output = filter_test(&globally_filtered);
assert_snapshot!(output);
}

#[test]
fn test_test_failure_token_savings() {
let input = include_str!("../../tests/fixtures/gradle/test_failure_raw.txt");
let globally_filtered = apply_global_filters(input);
let output = filter_test(&globally_filtered, false);
let output = filter_test(&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);
Expand All @@ -513,7 +482,7 @@ mod tests {
fn test_passing_tests_dropped() {
let input = include_str!("../../tests/fixtures/gradle/test_failure_raw.txt");
let globally_filtered = apply_global_filters(input);
let output = filter_test(&globally_filtered, false);
let output = filter_test(&globally_filtered);
assert!(
!output.contains("PASSED"),
"Passing test lines should be dropped"
Expand All @@ -524,7 +493,7 @@ mod tests {
fn test_failures_preserved() {
let input = include_str!("../../tests/fixtures/gradle/test_failure_raw.txt");
let globally_filtered = apply_global_filters(input);
let output = filter_test(&globally_filtered, false);
let output = filter_test(&globally_filtered);
assert!(output.contains("testChargeAmount FAILED"));
assert!(output.contains("testRefundProcess FAILED"));
assert!(output.contains("testCreateOrder FAILED"));
Expand Down Expand Up @@ -643,7 +612,7 @@ mod tests {
fn test_standard_out_dropped() {
let input = include_str!("../../tests/fixtures/gradle/test_failure_raw.txt");
let globally_filtered = apply_global_filters(input);
let output = filter_test(&globally_filtered, false);
let output = filter_test(&globally_filtered);
assert!(
!output.contains("STANDARD_OUT"),
"STANDARD_OUT header should be dropped"
Expand Down
Loading