1 /* 2 * Copyright (c) 2017 Google Inc. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you 5 * may not use this file except in compliance with the License. You may 6 * 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 13 * implied. See the License for the specific language governing 14 * permissions and limitations under the License. 15 */ 16 17 package com.android.vts.job; 18 19 import static com.googlecode.objectify.ObjectifyService.ofy; 20 21 import com.android.vts.entity.CodeCoverageEntity; 22 import com.android.vts.entity.DeviceInfoEntity; 23 import com.android.vts.entity.TestCoverageStatusEntity; 24 import com.android.vts.entity.TestRunEntity; 25 import com.android.vts.util.EmailHelper; 26 import com.google.appengine.api.datastore.DatastoreService; 27 import com.google.appengine.api.datastore.DatastoreServiceFactory; 28 import com.google.appengine.api.datastore.Entity; 29 import com.google.appengine.api.datastore.EntityNotFoundException; 30 import com.google.appengine.api.datastore.Key; 31 import com.google.appengine.api.datastore.KeyFactory; 32 import com.google.appengine.api.datastore.Query; 33 import com.google.appengine.api.taskqueue.Queue; 34 import com.google.appengine.api.taskqueue.QueueFactory; 35 import com.google.appengine.api.taskqueue.TaskOptions; 36 import java.io.IOException; 37 import java.io.UnsupportedEncodingException; 38 import java.math.RoundingMode; 39 import java.text.DecimalFormat; 40 import java.util.ArrayList; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Set; 44 import java.util.logging.Level; 45 import java.util.logging.Logger; 46 import javax.mail.Message; 47 import javax.mail.MessagingException; 48 import javax.servlet.http.HttpServletRequest; 49 import javax.servlet.http.HttpServletResponse; 50 import org.apache.commons.lang.StringUtils; 51 52 /** 53 * Coverage notification job. 54 */ 55 public class VtsCoverageAlertJobServlet extends BaseJobServlet { 56 57 private static final String COVERAGE_ALERT_URL = "/task/vts_coverage_job"; 58 protected static final Logger logger = 59 Logger.getLogger(VtsCoverageAlertJobServlet.class.getName()); 60 protected static final double CHANGE_ALERT_THRESHOLD = 0.05; 61 protected static final double GOOD_THRESHOLD = 0.7; 62 protected static final double BAD_THRESHOLD = 0.3; 63 64 protected static final DecimalFormat FORMATTER; 65 66 /** Initialize the decimal formatter. */ 67 static { 68 FORMATTER = new DecimalFormat("#.#"); 69 FORMATTER.setRoundingMode(RoundingMode.HALF_UP); 70 } 71 72 /** 73 * Gets a new coverage status and adds notification emails to the messages list. 74 * 75 * Send an email to notify subscribers in the event that a test goes up or down by more than 5%, 76 * becomes higher or lower than 70%, or becomes higher or lower than 30%. 77 * 78 * @param status The TestCoverageStatusEntity object for the test. 79 * @param testRunKey The key for TestRunEntity whose data to process and reflect in the state. 80 * @param link The string URL linking to the test's status table. 81 * @param emailAddresses The list of email addresses to send notifications to. 82 * @param messages The email Message queue. 83 * @returns TestCoverageStatusEntity or null if no update is available. 84 */ getTestCoverageStatus( TestCoverageStatusEntity status, Key testRunKey, String link, List<String> emailAddresses, List<Message> messages)85 public static TestCoverageStatusEntity getTestCoverageStatus( 86 TestCoverageStatusEntity status, 87 Key testRunKey, 88 String link, 89 List<String> emailAddresses, 90 List<Message> messages) 91 throws IOException { 92 DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); 93 94 String testName = status.getTestName(); 95 96 double previousPct; 97 double coveragePct; 98 if (status == null || status.getTotalLineCount() <= 0 || status.getCoveredLineCount() < 0) { 99 previousPct = 0; 100 } else { 101 previousPct = ((double) status.getCoveredLineCount()) / status.getTotalLineCount(); 102 } 103 104 Entity testRun; 105 try { 106 testRun = datastore.get(testRunKey); 107 } catch (EntityNotFoundException e) { 108 logger.log(Level.WARNING, "Test run not found: " + testRunKey); 109 return null; 110 } 111 112 TestRunEntity testRunEntity = TestRunEntity.fromEntity(testRun); 113 if (testRunEntity == null || !testRunEntity.getHasCodeCoverage()) { 114 return null; 115 } 116 CodeCoverageEntity codeCoverageEntity = testRunEntity.getCodeCoverageEntity(); 117 118 if (codeCoverageEntity.getTotalLineCount() <= 0 119 || codeCoverageEntity.getCoveredLineCount() < 0) { 120 coveragePct = 0; 121 } else { 122 coveragePct = 123 ((double) codeCoverageEntity.getCoveredLineCount()) 124 / codeCoverageEntity.getTotalLineCount(); 125 } 126 127 Set<String> buildIdList = new HashSet<>(); 128 Query deviceQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRun.getKey()); 129 List<DeviceInfoEntity> devices = new ArrayList<>(); 130 for (Entity device : datastore.prepare(deviceQuery).asIterable()) { 131 DeviceInfoEntity deviceEntity = DeviceInfoEntity.fromEntity(device); 132 if (deviceEntity == null) { 133 continue; 134 } 135 devices.add(deviceEntity); 136 buildIdList.add(deviceEntity.getBuildId()); 137 } 138 String deviceBuild = StringUtils.join(buildIdList, ", "); 139 String footer = EmailHelper.getEmailFooter(testRunEntity, devices, link); 140 141 String subject = null; 142 String body = null; 143 String subjectSuffix = " @ " + deviceBuild; 144 if (coveragePct >= GOOD_THRESHOLD && previousPct < GOOD_THRESHOLD) { 145 // Coverage entered the good zone 146 subject = 147 "Congratulations! " 148 + testName 149 + " has exceeded " 150 + FORMATTER.format(GOOD_THRESHOLD * 100) 151 + "% coverage" 152 + subjectSuffix; 153 body = 154 "Hello,<br><br>The " 155 + testName 156 + " has achieved " 157 + FORMATTER.format(coveragePct * 100) 158 + "% code coverage on device build ID(s): " 159 + deviceBuild 160 + "." 161 + footer; 162 } else if (coveragePct < GOOD_THRESHOLD && previousPct >= GOOD_THRESHOLD) { 163 // Coverage dropped out of the good zone 164 subject = 165 "Warning! " 166 + testName 167 + " has dropped below " 168 + FORMATTER.format(GOOD_THRESHOLD * 100) 169 + "% coverage" 170 + subjectSuffix; 171 ; 172 body = 173 "Hello,<br><br>The test " 174 + testName 175 + " has dropped to " 176 + FORMATTER.format(coveragePct * 100) 177 + "% code coverage on device build ID(s): " 178 + deviceBuild 179 + "." 180 + footer; 181 } else if (coveragePct <= BAD_THRESHOLD && previousPct > BAD_THRESHOLD) { 182 // Coverage entered into the bad zone 183 subject = 184 "Warning! " 185 + testName 186 + " has dropped below " 187 + FORMATTER.format(BAD_THRESHOLD * 100) 188 + "% coverage" 189 + subjectSuffix; 190 body = 191 "Hello,<br><br>The test " 192 + testName 193 + " has dropped to " 194 + FORMATTER.format(coveragePct * 100) 195 + "% code coverage on device build ID(s): " 196 + deviceBuild 197 + "." 198 + footer; 199 } else if (coveragePct > BAD_THRESHOLD && previousPct <= BAD_THRESHOLD) { 200 // Coverage emerged from the bad zone 201 subject = 202 "Congratulations! " 203 + testName 204 + " has exceeded " 205 + FORMATTER.format(BAD_THRESHOLD * 100) 206 + "% coverage" 207 + subjectSuffix; 208 body = 209 "Hello,<br><br>The test " 210 + testName 211 + " has achived " 212 + FORMATTER.format(coveragePct * 100) 213 + "% code coverage on device build ID(s): " 214 + deviceBuild 215 + "." 216 + footer; 217 } else if (coveragePct - previousPct < -CHANGE_ALERT_THRESHOLD) { 218 // Send a coverage drop alert 219 subject = 220 "Warning! " 221 + testName 222 + "'s code coverage has decreased by more than " 223 + FORMATTER.format(CHANGE_ALERT_THRESHOLD * 100) 224 + "%" 225 + subjectSuffix; 226 body = 227 "Hello,<br><br>The test " 228 + testName 229 + " has dropped from " 230 + FORMATTER.format(previousPct * 100) 231 + "% code coverage to " 232 + FORMATTER.format(coveragePct * 100) 233 + "% code coverage on device build ID(s): " 234 + deviceBuild 235 + "." 236 + footer; 237 } else if (coveragePct - previousPct > CHANGE_ALERT_THRESHOLD) { 238 // Send a coverage improvement alert 239 subject = 240 testName 241 + "'s code coverage has increased by more than " 242 + FORMATTER.format(CHANGE_ALERT_THRESHOLD * 100) 243 + "%" 244 + subjectSuffix; 245 body = 246 "Hello,<br><br>The test " 247 + testName 248 + " has increased from " 249 + FORMATTER.format(previousPct * 100) 250 + "% code coverage to " 251 + FORMATTER.format(coveragePct * 100) 252 + "% code coverage on device build ID(s): " 253 + deviceBuild 254 + "." 255 + footer; 256 } 257 if (subject != null && body != null) { 258 try { 259 messages.add(EmailHelper.composeEmail(emailAddresses, subject, body)); 260 } catch (MessagingException | UnsupportedEncodingException e) { 261 logger.log(Level.WARNING, "Error composing email : ", e); 262 } 263 } 264 return new TestCoverageStatusEntity( 265 testName, 266 testRunEntity.getStartTimestamp(), 267 codeCoverageEntity.getCoveredLineCount(), 268 codeCoverageEntity.getTotalLineCount(), 269 devices.size() > 0 ? devices.get(0).getId() : 0); 270 } 271 272 /** 273 * Add a task to process coverage data 274 * 275 * @param testRunKey The key of the test run whose data process. 276 */ addTask(Key testRunKey)277 public static void addTask(Key testRunKey) { 278 Queue queue = QueueFactory.getDefaultQueue(); 279 String keyString = KeyFactory.keyToString(testRunKey); 280 queue.add( 281 TaskOptions.Builder.withUrl(COVERAGE_ALERT_URL) 282 .param("runKey", keyString) 283 .method(TaskOptions.Method.POST)); 284 } 285 286 @Override doPost(HttpServletRequest request, HttpServletResponse response)287 public void doPost(HttpServletRequest request, HttpServletResponse response) 288 throws IOException { 289 String runKeyString = request.getParameter("runKey"); 290 291 Key testRunKey; 292 try { 293 testRunKey = KeyFactory.stringToKey(runKeyString); 294 } catch (IllegalArgumentException e) { 295 logger.log(Level.WARNING, "Invalid key specified: " + runKeyString); 296 return; 297 } 298 String testName = testRunKey.getParent().getName(); 299 300 TestCoverageStatusEntity status = ofy().load().type(TestCoverageStatusEntity.class).id(testName) 301 .now(); 302 if (status == null) { 303 status = new TestCoverageStatusEntity(testName, 0, -1, -1, 0); 304 } 305 306 StringBuffer fullUrl = request.getRequestURL(); 307 String baseUrl = fullUrl.substring(0, fullUrl.indexOf(request.getRequestURI())); 308 String link = baseUrl + "/show_tree?testName=" + testName; 309 TestCoverageStatusEntity newStatus; 310 List<Message> messageQueue = new ArrayList<>(); 311 try { 312 List<String> emails = EmailHelper.getSubscriberEmails(testRunKey.getParent()); 313 newStatus = getTestCoverageStatus(status, testRunKey, link, emails, messageQueue); 314 } catch (IOException e) { 315 logger.log(Level.SEVERE, e.toString()); 316 return; 317 } 318 319 if (newStatus == null) { 320 return; 321 } else { 322 if (status == null || status.getUpdatedTimestamp() < newStatus.getUpdatedTimestamp()) { 323 newStatus.save(); 324 EmailHelper.sendAll(messageQueue); 325 } 326 } 327 } 328 } 329