View Javadoc
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 }