blob: 106ffb51cfda3897debfbfbba088560b8e2afb14 [file] [log] [blame]
/*
* 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<String> emailAddresses,
List<Message> 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<String> buildIdList = new HashSet<>();
Query deviceQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRun.getKey());
List<DeviceInfoEntity> 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,<br><br>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,<br><br>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,<br><br>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,<br><br>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,<br><br>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,<br><br>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());
}
/**
* 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);
}
StringBuffer fullUrl = request.getRequestURL();
String baseUrl = fullUrl.substring(0, fullUrl.indexOf(request.getRequestURI()));
String link = baseUrl + "/show_tree?testName=" + testName;
TestCoverageStatusEntity newStatus;
List<Message> messageQueue = new ArrayList<>();
try {
List<String> 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);
}
}
}
}