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.plugin.surefire.report;
20  
21  import java.io.BufferedOutputStream;
22  import java.io.File;
23  import java.io.FilterOutputStream;
24  import java.io.IOException;
25  import java.io.UncheckedIOException;
26  import java.nio.charset.Charset;
27  import java.nio.file.Files;
28  import java.util.Map;
29  import java.util.concurrent.ConcurrentHashMap;
30  
31  import org.apache.maven.plugin.surefire.booterclient.output.InPluginProcessDumpSingleton;
32  import org.apache.maven.surefire.api.report.TestOutputReportEntry;
33  import org.apache.maven.surefire.api.report.TestSetReportEntry;
34  
35  import static org.apache.maven.plugin.surefire.report.FileReporter.getReportFile;
36  import static org.apache.maven.surefire.api.util.internal.StringUtils.NL;
37  
38  /**
39   * Surefire output consumer proxy that writes test output to a {@link java.io.File} for each test suite.
40   *
41   * @author Kristian Rosenvold
42   * @author Carlos Sanchez
43   */
44  public class ConsoleOutputFileReporter implements TestcycleConsoleOutputReceiver {
45      private static final int STREAM_BUFFER_SIZE = 64 * 1024;
46  
47      private final File reportsDirectory;
48      private final String reportNameSuffix;
49      private final boolean usePhrasedFileName;
50      private final Integer forkNumber;
51      private final String encoding;
52  
53      private final Map<String, FilterOutputStream> outputStreams = new ConcurrentHashMap<>();
54  
55      private volatile String reportEntryName;
56  
57      public ConsoleOutputFileReporter(
58              File reportsDirectory,
59              String reportNameSuffix,
60              boolean usePhrasedFileName,
61              Integer forkNumber,
62              String encoding) {
63          this.reportsDirectory = reportsDirectory;
64          this.reportNameSuffix = reportNameSuffix;
65          this.usePhrasedFileName = usePhrasedFileName;
66          this.forkNumber = forkNumber;
67          this.encoding = encoding;
68      }
69  
70      @Override
71      public void testSetStarting(TestSetReportEntry reportEntry) {
72          String className = usePhrasedFileName ? reportEntry.getSourceText() : reportEntry.getSourceName();
73          reportEntryName = className;
74          try {
75              File file = getReportFile(reportsDirectory, className, reportNameSuffix, "-output.txt");
76              if (!reportsDirectory.exists()) {
77                  Files.createDirectories(reportsDirectory.toPath());
78              }
79              if (!Files.exists(file.toPath())) {
80                  Files.createFile(file.toPath());
81              }
82              outputStreams.put(
83                      className, new BufferedOutputStream(Files.newOutputStream(file.toPath()), STREAM_BUFFER_SIZE));
84          } catch (IOException e) {
85              throw new RuntimeException(e);
86          }
87      }
88  
89      @Override
90      public void testSetCompleted(TestSetReportEntry report) {}
91  
92      @Override
93      public void close() {
94          // Close all output streams in the map
95          for (FilterOutputStream stream : outputStreams.values()) {
96              try {
97                  stream.close();
98              } catch (IOException e) {
99                  dumpException(e);
100             }
101         }
102     }
103 
104     @Override
105     public synchronized void writeTestOutput(TestOutputReportEntry reportEntry) {
106         try {
107             // Determine the target class name based on stack trace or reportEntryName
108             String targetClassName = extractTestClassFromStack(reportEntry.getStack());
109             if (targetClassName == null) {
110                 targetClassName = reportEntryName;
111             }
112             // If still null, use "null" as the file name (for output before any test starts)
113             if (targetClassName == null) {
114                 targetClassName = "null";
115             }
116 
117             // Get or create output stream for this test class
118             FilterOutputStream os = outputStreams.computeIfAbsent(targetClassName, className -> {
119                 try {
120                     if (!reportsDirectory.exists()) {
121                         //noinspection ResultOfMethodCallIgnored
122                         reportsDirectory.mkdirs();
123                     }
124                     File file = getReportFile(reportsDirectory, className, reportNameSuffix, "-output.txt");
125                     return new BufferedOutputStream(Files.newOutputStream(file.toPath()), STREAM_BUFFER_SIZE);
126                 } catch (IOException e) {
127                     dumpException(e);
128                     throw new UncheckedIOException(e);
129                 }
130             });
131 
132             String output = reportEntry.getLog();
133             if (output == null) {
134                 output = "null";
135             }
136             Charset charset = Charset.forName(encoding);
137             os.write(output.getBytes(charset));
138             if (reportEntry.isNewLine()) {
139                 os.write(NL.getBytes(charset));
140             }
141         } catch (IOException e) {
142             dumpException(e);
143             throw new UncheckedIOException(e);
144         }
145     }
146 
147     /**
148      * Extracts the test class name from the stack trace.
149      * Stack format: className#method;className#method;...
150      * Returns the first class name that looks like a test class.
151      */
152     private String extractTestClassFromStack(String stack) {
153         if (stack == null || stack.isEmpty()) {
154             return null;
155         }
156         // The stack contains entries like "className#method;className#method;..."
157         // We look for the test class which typically is the first entry or an entry with "Test" in the name
158         String[] entries = stack.split(";");
159         for (String entry : entries) {
160             int hashIndex = entry.indexOf('#');
161             if (hashIndex > 0) {
162                 String className = entry.substring(0, hashIndex);
163                 if (outputStreams.containsKey(className)) {
164                     return className;
165                 }
166             }
167         }
168         return null;
169     }
170 
171     private void dumpException(IOException e) {
172         if (forkNumber == null) {
173             InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory);
174         } else {
175             InPluginProcessDumpSingleton.getSingleton()
176                     .dumpException(e, e.getLocalizedMessage(), reportsDirectory, forkNumber);
177         }
178     }
179 }