/* * Copyright (c) 2017 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you * may not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or * implied. See the License for the specific language governing * permissions and limitations under the License. */ package com.android.vts.job; import static com.googlecode.objectify.ObjectifyService.ofy; import com.android.vts.entity.CodeCoverageEntity; import com.android.vts.entity.DeviceInfoEntity; import com.android.vts.entity.TestCoverageStatusEntity; import com.android.vts.entity.TestRunEntity; import com.android.vts.util.EmailHelper; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.EntityNotFoundException; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.datastore.Query; import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.api.taskqueue.QueueFactory; import com.google.appengine.api.taskqueue.TaskOptions; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.math.RoundingMode; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.mail.Message; import javax.mail.MessagingException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; /** * Coverage notification job. */ public class VtsCoverageAlertJobServlet extends BaseJobServlet { private static final String COVERAGE_ALERT_URL = "/task/vts_coverage_job"; protected static final Logger logger = Logger.getLogger(VtsCoverageAlertJobServlet.class.getName()); protected static final double CHANGE_ALERT_THRESHOLD = 0.05; protected static final double GOOD_THRESHOLD = 0.7; protected static final double BAD_THRESHOLD = 0.3; protected static final DecimalFormat FORMATTER; /** Initialize the decimal formatter. */ static { FORMATTER = new DecimalFormat("#.#"); FORMATTER.setRoundingMode(RoundingMode.HALF_UP); } /** * Gets a new coverage status and adds notification emails to the messages list. * * Send an email to notify subscribers in the event that a test goes up or down by more than 5%, * becomes higher or lower than 70%, or becomes higher or lower than 30%. * * @param status The TestCoverageStatusEntity object for the test. * @param testRunKey The key for TestRunEntity whose data to process and reflect in the state. * @param link The string URL linking to the test's status table. * @param emailAddresses The list of email addresses to send notifications to. * @param messages The email Message queue. * @returns TestCoverageStatusEntity or null if no update is available. */ public static TestCoverageStatusEntity getTestCoverageStatus( TestCoverageStatusEntity status, Key testRunKey, String link, List emailAddresses, List messages) throws IOException { DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); String testName = status.getTestName(); double previousPct; double coveragePct; if (status == null || status.getTotalLineCount() <= 0 || status.getCoveredLineCount() < 0) { previousPct = 0; } else { previousPct = ((double) status.getCoveredLineCount()) / status.getTotalLineCount(); } Entity testRun; try { testRun = datastore.get(testRunKey); } catch (EntityNotFoundException e) { logger.log(Level.WARNING, "Test run not found: " + testRunKey); return null; } TestRunEntity testRunEntity = TestRunEntity.fromEntity(testRun); if (testRunEntity == null || !testRunEntity.getHasCodeCoverage()) { return null; } CodeCoverageEntity codeCoverageEntity = testRunEntity.getCodeCoverageEntity(); if (codeCoverageEntity.getTotalLineCount() <= 0 || codeCoverageEntity.getCoveredLineCount() < 0) { coveragePct = 0; } else { coveragePct = ((double) codeCoverageEntity.getCoveredLineCount()) / codeCoverageEntity.getTotalLineCount(); } Set buildIdList = new HashSet<>(); Query deviceQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRun.getKey()); List devices = new ArrayList<>(); for (Entity device : datastore.prepare(deviceQuery).asIterable()) { DeviceInfoEntity deviceEntity = DeviceInfoEntity.fromEntity(device); if (deviceEntity == null) { continue; } devices.add(deviceEntity); buildIdList.add(deviceEntity.getBuildId()); } String deviceBuild = StringUtils.join(buildIdList, ", "); String footer = EmailHelper.getEmailFooter(testRunEntity, devices, link); String subject = null; String body = null; String subjectSuffix = " @ " + deviceBuild; if (coveragePct >= GOOD_THRESHOLD && previousPct < GOOD_THRESHOLD) { // Coverage entered the good zone subject = "Congratulations! " + testName + " has exceeded " + FORMATTER.format(GOOD_THRESHOLD * 100) + "% coverage" + subjectSuffix; body = "Hello,

The " + testName + " has achieved " + FORMATTER.format(coveragePct * 100) + "% code coverage on device build ID(s): " + deviceBuild + "." + footer; } else if (coveragePct < GOOD_THRESHOLD && previousPct >= GOOD_THRESHOLD) { // Coverage dropped out of the good zone subject = "Warning! " + testName + " has dropped below " + FORMATTER.format(GOOD_THRESHOLD * 100) + "% coverage" + subjectSuffix; ; body = "Hello,

The test " + testName + " has dropped to " + FORMATTER.format(coveragePct * 100) + "% code coverage on device build ID(s): " + deviceBuild + "." + footer; } else if (coveragePct <= BAD_THRESHOLD && previousPct > BAD_THRESHOLD) { // Coverage entered into the bad zone subject = "Warning! " + testName + " has dropped below " + FORMATTER.format(BAD_THRESHOLD * 100) + "% coverage" + subjectSuffix; body = "Hello,

The test " + testName + " has dropped to " + FORMATTER.format(coveragePct * 100) + "% code coverage on device build ID(s): " + deviceBuild + "." + footer; } else if (coveragePct > BAD_THRESHOLD && previousPct <= BAD_THRESHOLD) { // Coverage emerged from the bad zone subject = "Congratulations! " + testName + " has exceeded " + FORMATTER.format(BAD_THRESHOLD * 100) + "% coverage" + subjectSuffix; body = "Hello,

The test " + testName + " has achived " + FORMATTER.format(coveragePct * 100) + "% code coverage on device build ID(s): " + deviceBuild + "." + footer; } else if (coveragePct - previousPct < -CHANGE_ALERT_THRESHOLD) { // Send a coverage drop alert subject = "Warning! " + testName + "'s code coverage has decreased by more than " + FORMATTER.format(CHANGE_ALERT_THRESHOLD * 100) + "%" + subjectSuffix; body = "Hello,

The test " + testName + " has dropped from " + FORMATTER.format(previousPct * 100) + "% code coverage to " + FORMATTER.format(coveragePct * 100) + "% code coverage on device build ID(s): " + deviceBuild + "." + footer; } else if (coveragePct - previousPct > CHANGE_ALERT_THRESHOLD) { // Send a coverage improvement alert subject = testName + "'s code coverage has increased by more than " + FORMATTER.format(CHANGE_ALERT_THRESHOLD * 100) + "%" + subjectSuffix; body = "Hello,

The test " + testName + " has increased from " + FORMATTER.format(previousPct * 100) + "% code coverage to " + FORMATTER.format(coveragePct * 100) + "% code coverage on device build ID(s): " + deviceBuild + "." + footer; } if (subject != null && body != null) { try { messages.add(EmailHelper.composeEmail(emailAddresses, subject, body)); } catch (MessagingException | UnsupportedEncodingException e) { logger.log(Level.WARNING, "Error composing email : ", e); } } return new TestCoverageStatusEntity( testName, testRunEntity.getStartTimestamp(), codeCoverageEntity.getCoveredLineCount(), codeCoverageEntity.getTotalLineCount(), devices.size() > 0 ? devices.get(0).getId() : 0); } /** * Add a task to process coverage data * * @param testRunKey The key of the test run whose data process. */ public static void addTask(Key testRunKey) { Queue queue = QueueFactory.getDefaultQueue(); String keyString = KeyFactory.keyToString(testRunKey); queue.add( TaskOptions.Builder.withUrl(COVERAGE_ALERT_URL) .param("runKey", keyString) .method(TaskOptions.Method.POST)); } @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { String runKeyString = request.getParameter("runKey"); Key testRunKey; try { testRunKey = KeyFactory.stringToKey(runKeyString); } catch (IllegalArgumentException e) { logger.log(Level.WARNING, "Invalid key specified: " + runKeyString); return; } String testName = testRunKey.getParent().getName(); TestCoverageStatusEntity status = ofy().load().type(TestCoverageStatusEntity.class).id(testName) .now(); if (status == null) { status = new TestCoverageStatusEntity(testName, 0, -1, -1, 0); } StringBuffer fullUrl = request.getRequestURL(); String baseUrl = fullUrl.substring(0, fullUrl.indexOf(request.getRequestURI())); String link = baseUrl + "/show_tree?testName=" + testName; TestCoverageStatusEntity newStatus; List messageQueue = new ArrayList<>(); try { List emails = EmailHelper.getSubscriberEmails(testRunKey.getParent()); newStatus = getTestCoverageStatus(status, testRunKey, link, emails, messageQueue); } catch (IOException e) { logger.log(Level.SEVERE, e.toString()); return; } if (newStatus == null) { return; } else { if (status == null || status.getUpdatedTimestamp() < newStatus.getUpdatedTimestamp()) { newStatus.save(); EmailHelper.sendAll(messageQueue); } } } }