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