1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.maven.surefire.api.report;
20
21 import java.util.Arrays;
22 import java.util.Collections;
23 import java.util.HashSet;
24 import java.util.List;
25 import java.util.Set;
26 import java.util.stream.Collectors;
27
28 /**
29 * Utility class for stack trace capture with filtering and truncation to reduce memory consumption.
30 * Filters out framework classes and limits to a maximum number of frames.
31 *
32 * @since 3.6.0
33 */
34 public class StackTraceProvider {
35
36 // 15 frames is enough to capture the test class after surefire framework frames
37 // while still providing ~50% memory savings vs unbounded stacks (typically 25-30 frames)
38 public static final int DEFAULT_MAX_FRAMES = 15;
39
40 private static volatile int maxFrames = DEFAULT_MAX_FRAMES;
41
42 // Only filter JDK internal classes by default.
43 // Framework classes (junit, surefire, etc.) are NOT filtered by default because:
44 // 1. Test classes might be in framework packages (e.g., during framework's own tests)
45 // 2. The consumer (ConsoleOutputFileReporter) needs to find the test class in the stack
46 // Users can add additional prefixes via configuration if needed.
47 private static final Set<String> DEFAULT_FRAMEWORK_PREFIXES =
48 new HashSet<>(Arrays.asList("java.", "javax.", "sun.", "jdk."));
49
50 private static volatile Set<String> frameworkPrefixes = DEFAULT_FRAMEWORK_PREFIXES;
51
52 /**
53 * Configure framework prefixes to filter from stack traces.
54 * When specified, this REPLACES the default prefixes (does not add to them).
55 * To disable all filtering, pass an empty string.
56 * To use defaults, pass null.
57 *
58 * @param prefixes comma-separated list of package prefixes to filter, or empty to disable filtering
59 */
60 public static void configure(String prefixes) {
61 configure(prefixes, DEFAULT_MAX_FRAMES);
62 }
63
64 /**
65 * Configure framework prefixes and maximum frame count for stack traces.
66 *
67 * @param prefixes comma-separated list of package prefixes to filter, or empty to disable filtering, or null
68 * for defaults
69 * @param maxFrameCount maximum number of stack trace frames to capture; 0 or negative disables stack trace capture
70 */
71 public static void configure(String prefixes, int maxFrameCount) {
72 if (prefixes == null) {
73 // null means use defaults
74 frameworkPrefixes = DEFAULT_FRAMEWORK_PREFIXES;
75 } else if (prefixes.trim().isEmpty()) {
76 // empty string means no filtering
77 frameworkPrefixes = new HashSet<>();
78 } else {
79 // explicit prefixes replace defaults
80 Set<String> customPrefixes = new HashSet<>();
81 for (String prefix : prefixes.split(",")) {
82 String trimmed = prefix.trim();
83 if (!trimmed.isEmpty()) {
84 customPrefixes.add(trimmed);
85 }
86 }
87 frameworkPrefixes = customPrefixes;
88 }
89 maxFrames = maxFrameCount;
90 }
91
92 /**
93 * Returns the stack trace as a list of "classname#methodname" strings.
94 * Filters out framework classes and limits to {@value #DEFAULT_MAX_FRAMES} frames by default.
95 * Returns an empty list if max frames is set to 0 or negative.
96 *
97 * @return the filtered and truncated stack trace
98 */
99 static List<String> getStack() {
100 if (maxFrames <= 0) {
101 return Collections.emptyList();
102 }
103 return Arrays.stream(Thread.currentThread().getStackTrace())
104 .filter(e -> !isFrameworkClass(e.getClassName()))
105 .limit(maxFrames)
106 .map(e -> e.getClassName() + "#" + e.getMethodName())
107 .collect(Collectors.toList());
108 }
109
110 private static boolean isFrameworkClass(String className) {
111 for (String prefix : frameworkPrefixes) {
112 if (className.startsWith(prefix)) {
113 return true;
114 }
115 }
116 return false;
117 }
118 }