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          try {
74              File file = getReportFile(reportsDirectory, className, reportNameSuffix, "-output.txt");
75              if (!reportsDirectory.exists()) {
76                  Files.createDirectories(reportsDirectory.toPath());
77              }
78              if (!Files.exists(file.toPath())) {
79                  Files.createFile(file.toPath());
80              }
81              outputStreams.put(
82                      className, new BufferedOutputStream(Files.newOutputStream(file.toPath()), STREAM_BUFFER_SIZE));
83          } catch (IOException e) {
84              throw new RuntimeException(e);
85          }
86      }
87  
88      @Override
89      public void testSetCompleted(TestSetReportEntry report) {}
90  
91      @Override
92      public void close() {
93          // Close all output streams in the map
94          for (FilterOutputStream stream : outputStreams.values()) {
95              try {
96                  stream.close();
97              } catch (IOException e) {
98                  dumpException(e);
99              }
100         }
101     }
102 
103     @Override
104     public synchronized void writeTestOutput(TestOutputReportEntry reportEntry) {
105         try {
106             // Determine the target class name based on stack trace or reportEntryName
107             String targetClassName = extractTestClassFromStack(reportEntry.getStack());
108             if (targetClassName == null) {
109                 targetClassName = reportEntryName;
110             }
111             // If still null, use "null" as the file name (for output before any test starts)
112             if (targetClassName == null) {
113                 targetClassName = "null";
114             }
115 
116             // Get or create output stream for this test class
117             FilterOutputStream os = outputStreams.computeIfAbsent(targetClassName, className -> {
118                 try {
119                     if (!reportsDirectory.exists()) {
120                         //noinspection ResultOfMethodCallIgnored
121                         reportsDirectory.mkdirs();
122                     }
123                     File file = getReportFile(reportsDirectory, className, reportNameSuffix, "-output.txt");
124                     return new BufferedOutputStream(Files.newOutputStream(file.toPath()), STREAM_BUFFER_SIZE);
125                 } catch (IOException e) {
126                     dumpException(e);
127                     throw new UncheckedIOException(e);
128                 }
129             });
130 
131             String output = reportEntry.getLog();
132             if (output == null) {
133                 output = "null";
134             }
135             Charset charset = Charset.forName(encoding);
136             os.write(output.getBytes(charset));
137             if (reportEntry.isNewLine()) {
138                 os.write(NL.getBytes(charset));
139             }
140         } catch (IOException e) {
141             dumpException(e);
142             throw new UncheckedIOException(e);
143         }
144     }
145 
146     /**
147      * Extracts the test class name from the stack trace.
148      * Stack format: className#method;className#method;...
149      * Returns the first class name that looks like a test class.
150      */
151     private String extractTestClassFromStack(String stack) {
152         if (stack == null || stack.isEmpty()) {
153             return null;
154         }
155         // The stack contains entries like "className#method;className#method;..."
156         // We look for the test class which typically is the first entry or an entry with "Test" in the name
157         String[] entries = stack.split(";");
158         for (String entry : entries) {
159             int hashIndex = entry.indexOf('#');
160             if (hashIndex > 0) {
161                 String className = entry.substring(0, hashIndex);
162                 if (outputStreams.containsKey(className)) {
163                     return className;
164                 }
165             }
166         }
167         return null;
168     }
169 
170     private void dumpException(IOException e) {
171         if (forkNumber == null) {
172             InPluginProcessDumpSingleton.getSingleton().dumpException(e, e.getLocalizedMessage(), reportsDirectory);
173         } else {
174             InPluginProcessDumpSingleton.getSingleton()
175                     .dumpException(e, e.getLocalizedMessage(), reportsDirectory, forkNumber);
176         }
177     }
178 }