• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.platform.test.ravenwood;
17 
18 import android.util.Log;
19 
20 import org.junit.runner.Description;
21 import org.junit.runner.notification.Failure;
22 import org.junit.runner.notification.RunListener;
23 import org.junit.runner.notification.RunNotifier;
24 
25 import java.io.File;
26 import java.io.IOException;
27 import java.io.PrintWriter;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.Paths;
31 import java.time.LocalDateTime;
32 import java.time.format.DateTimeFormatter;
33 import java.util.LinkedHashMap;
34 import java.util.Map;
35 
36 /**
37  * Collect test result stats and write them into a CSV file containing the test results.
38  *
39  * The output file is created as `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_[TIMESTAMP].csv`.
40  * A symlink to the latest result will be created as
41  * `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_latest.csv`.
42  */
43 public class RavenwoodTestStats {
44     private static final String TAG = com.android.ravenwood.common.RavenwoodCommonUtils.TAG;
45     private static final String HEADER = "Module,Class,OuterClass,Passed,Failed,Skipped";
46 
47     private static RavenwoodTestStats sInstance;
48 
49     /**
50      * @return a singleton instance.
51      */
getInstance()52     public static RavenwoodTestStats getInstance() {
53         if (sInstance == null) {
54             sInstance = new RavenwoodTestStats();
55         }
56         return sInstance;
57     }
58 
59     /**
60      * Represents a test result.
61      */
62     public enum Result {
63         Passed,
64         Failed,
65         Skipped,
66     }
67 
68     private final File mOutputFile;
69     private final PrintWriter mOutputWriter;
70     private final String mTestModuleName;
71 
72     public final Map<String, Map<String, Result>> mStats = new LinkedHashMap<>();
73 
74     /** Ctor */
RavenwoodTestStats()75     public RavenwoodTestStats() {
76         mTestModuleName = guessTestModuleName();
77 
78         var basename = "Ravenwood-stats_" + mTestModuleName + "_";
79 
80         // Get the current time
81         LocalDateTime now = LocalDateTime.now();
82         DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
83 
84         var tmpdir = System.getProperty("java.io.tmpdir");
85         mOutputFile = new File(tmpdir, basename + now.format(fmt) + ".csv");
86 
87         try {
88             mOutputWriter = new PrintWriter(mOutputFile);
89         } catch (IOException e) {
90             throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
91         }
92 
93         // Create the "latest" symlink.
94         Path symlink = Paths.get(tmpdir, basename + "latest.csv");
95         try {
96             Files.deleteIfExists(symlink);
97             Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName()));
98 
99         } catch (IOException e) {
100             throw new RuntimeException("Failed to create logfile. File=" + mOutputFile, e);
101         }
102 
103         Log.i(TAG, "Test result stats file: " + mOutputFile);
104 
105         // Print the header.
106         mOutputWriter.println(HEADER);
107         mOutputWriter.flush();
108     }
109 
guessTestModuleName()110     private String guessTestModuleName() {
111         // Assume the current directory name is the test module name.
112         File cwd;
113         try {
114             cwd = new File(".").getCanonicalFile();
115         } catch (IOException e) {
116             throw new RuntimeException("Failed to get the current directory", e);
117         }
118         return cwd.getName();
119     }
120 
addResult(String className, String methodName, Result result)121     private void addResult(String className, String methodName,
122             Result result) {
123         mStats.compute(className, (className_, value) -> {
124             if (value == null) {
125                 value = new LinkedHashMap<>();
126             }
127             // If the result is already set, don't overwrite it.
128             if (!value.containsKey(methodName)) {
129                 value.put(methodName, result);
130             }
131             return value;
132         });
133     }
134 
135     /**
136      * Call it when a test method is finished.
137      */
onTestFinished(String className, String testName, Result result)138     private void onTestFinished(String className, String testName, Result result) {
139         addResult(className, testName, result);
140     }
141 
142     /**
143      * Dump all the results and clear it.
144      */
dumpAllAndClear()145     private void dumpAllAndClear() {
146         for (var entry : mStats.entrySet()) {
147             int passed = 0;
148             int skipped = 0;
149             int failed = 0;
150             var className = entry.getKey();
151 
152             for (var e : entry.getValue().values()) {
153                 switch (e) {
154                     case Passed:
155                         passed++;
156                         break;
157                     case Skipped:
158                         skipped++;
159                         break;
160                     case Failed:
161                         failed++;
162                         break;
163                 }
164             }
165 
166             mOutputWriter.printf("%s,%s,%s,%d,%d,%d\n",
167                     mTestModuleName, className, getOuterClassName(className),
168                     passed, failed, skipped);
169         }
170         mOutputWriter.flush();
171         mStats.clear();
172     }
173 
getOuterClassName(String className)174     private static String getOuterClassName(String className) {
175         // Just delete the '$', because I'm not sure if the className we get here is actaully a
176         // valid class name that does exist. (it might have a parameter name, etc?)
177         int p = className.indexOf('$');
178         if (p < 0) {
179             return className;
180         }
181         return className.substring(0, p);
182     }
183 
attachToRunNotifier(RunNotifier notifier)184     public void attachToRunNotifier(RunNotifier notifier) {
185         notifier.addListener(mRunListener);
186     }
187 
188     private final RunListener mRunListener = new RunListener() {
189         @Override
190         public void testSuiteStarted(Description description) {
191             Log.d(TAG, "testSuiteStarted: " + description);
192         }
193 
194         @Override
195         public void testSuiteFinished(Description description) {
196             Log.d(TAG, "testSuiteFinished: " + description);
197         }
198 
199         @Override
200         public void testRunStarted(Description description) {
201             Log.d(TAG, "testRunStarted: " + description);
202         }
203 
204         @Override
205         public void testRunFinished(org.junit.runner.Result result) {
206             Log.d(TAG, "testRunFinished: " + result);
207 
208             dumpAllAndClear();
209         }
210 
211         @Override
212         public void testStarted(Description description) {
213             Log.d(TAG, "  testStarted: " + description);
214         }
215 
216         @Override
217         public void testFinished(Description description) {
218             Log.d(TAG, "  testFinished: " + description);
219 
220             // Send "Passed", but if there's already another result sent for this, this won't
221             // override it.
222             onTestFinished(description.getClassName(), description.getMethodName(), Result.Passed);
223         }
224 
225         @Override
226         public void testFailure(Failure failure) {
227             Log.d(TAG, "    testFailure: " + failure);
228 
229             var description = failure.getDescription();
230             onTestFinished(description.getClassName(), description.getMethodName(), Result.Failed);
231         }
232 
233         @Override
234         public void testAssumptionFailure(Failure failure) {
235             Log.d(TAG, "    testAssumptionFailure: " + failure);
236             var description = failure.getDescription();
237             onTestFinished(description.getClassName(), description.getMethodName(), Result.Skipped);
238         }
239 
240         @Override
241         public void testIgnored(Description description) {
242             Log.d(TAG, "    testIgnored: " + description);
243             onTestFinished(description.getClassName(), description.getMethodName(), Result.Skipped);
244         }
245     };
246 }
247