blob: ff61a513ad0b791aba81f563590796358bbf2345 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* 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.tradefed.testtype;
import static com.android.tradefed.testtype.JacocoCodeCoverageReportFormat.HTML;
import com.android.tradefed.build.VersionedFile;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.Option.Importance;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.CommandResult;
import com.android.tradefed.util.CommandStatus;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* An {@link IRemoteTest} that generates a code coverage report by generating build.xml on the fly
* and executing the ant command. This test type supports collecting coverage information from
* classes that have already been instrumented by the Jacoco compiler.
*/
@OptionClass(alias = "jacoco-coverage")
public class JacocoCodeCoverageTest extends CodeCoverageTestBase<JacocoCodeCoverageReportFormat> {
private static final String ANT_BUILD_XML_PATH = "/jacoco/ant-build.xml";
@Option(name = "report-format",
description = "Which output format(s) to use when generating the coverage report. "
+ "May be repeated to output multiple formats. "
+ "Defaults to HTML if unspecified.")
private List<JacocoCodeCoverageReportFormat> mReportFormat = new ArrayList<>();
@Option(name = "report-timeout",
description = "Maximum time to wait for coverage report generation to complete",
isTimeVal = true)
private long mReportTimeout = 10 * 60 * 1000; // 10 Minutes
@Option(name = "report-classes", mandatory = true, importance = Importance.ALWAYS,
description = "Regex pattern used to select the instrumented class files "
+ "that should be used to generate the report. May be repeated.")
private List<String> mClassFilesFilter = new ArrayList<>();
@Option(name = "jacocoant", mandatory = true, importance = Importance.ALWAYS,
description = "The build artifact name for the specific jacoco ant jar "
+ "that should be used to generate the report.")
private String mJacocoAnt;
@Option(name = "ant-path",
description = "The location of the ant binary. Default use the one in $PATH")
private String mAntBinary = "ant";
void setReportFormat(List<JacocoCodeCoverageReportFormat> reportFormat) {
mReportFormat = reportFormat;
}
void setClassFilesFilters(List<String> classFiles) {
mClassFilesFilter = classFiles;
}
List<String> getClassFilesFilters() {
return mClassFilesFilter;
}
void setJacocoAnt(String jacocoAnt) {
mJacocoAnt = jacocoAnt;
}
long getReportTimeout() {
return mReportTimeout;
}
@Override
protected List<JacocoCodeCoverageReportFormat> getReportFormat() {
return mReportFormat.isEmpty() ? Arrays.asList(HTML) : mReportFormat;
}
@Override
protected File generateCoverageReport(Collection<File> executionFiles,
JacocoCodeCoverageReportFormat format) throws IOException {
if (executionFiles == null || executionFiles.isEmpty()) {
throw new IllegalArgumentException("Could not find any exec file");
}
File jacocoFile = getBuild().getFile(mJacocoAnt);
if (jacocoFile == null) {
throw new IOException("Could not find jacocoant jar file " + mJacocoAnt
+ ". Make sure the file is published as a build artifact, "
+ "and that your build provider is configured to download it.");
}
Collection<VersionedFile> buildFiles = getBuild().getFiles();
Stream<File> buildFilesStream = buildFiles.stream().map(VersionedFile::getFile);
List<File> classFiles = buildFilesStream.filter(buildFile -> isClassFile(buildFile))
.collect(Collectors.toList());
if (classFiles == null || classFiles.isEmpty()) {
throw new IOException("No class files found");
}
File reportDest = format.equals(HTML) ? FileUtil.createTempDir("report")
: FileUtil.createTempFile("report", "." + format.getLogDataType().getFileExt());
File buildXml = getBuildXml(executionFiles, classFiles, reportDest, jacocoFile, format);
try {
runAntBuild(buildXml);
return reportDest;
} catch (IOException e) {
FileUtil.recursiveDelete(reportDest);
throw e;
} finally {
FileUtil.deleteFile(buildXml);
}
}
/** Calls {@link RunUtil#runTimedCmd(long, String[])}. Exposed for unit testing. */
@VisibleForTesting
CommandResult runTimedCmd(long timeout, String[] command) {
return RunUtil.getDefault().runTimedCmd(timeout, command);
}
/**
* Get build.xml from template and replace filesets to their corresponding value
*/
File getBuildXml(Collection<File> executionFiles, List<File> allclassesJars, File reportDest,
File jacocoFile, JacocoCodeCoverageReportFormat reportFormat) throws IOException {
InputStream template = getClass().getResourceAsStream(ANT_BUILD_XML_PATH);
if (template == null) {
throw new IOException("Could not find " + ANT_BUILD_XML_PATH);
}
String text = StreamUtil.getStringFromStream(template);
File buildXml = FileUtil.createTempFile("build", ".xml");
text = text.replaceAll(Pattern.quote("${jacocoant}"), jacocoFile.getAbsolutePath());
text = text.replaceAll(Pattern.quote("${classfileset}"), getFileTags(allclassesJars));
text = text.replaceAll(Pattern.quote("${execfileset}"), getFileTags(executionFiles));
text = text.replaceAll(Pattern.quote("${reportdestdirs}"),
String.format(reportFormat.getAntTagFormat(), reportDest.getAbsolutePath()));
FileUtil.writeToFile(text, buildXml);
return buildXml;
}
/**
* Generate file tag with given files
*/
String getFileTags(Collection<File> files) {
StringBuilder fileset = new StringBuilder();
for (File file : files) {
fileset.append("<file file=\"" + file.getAbsolutePath() + "\" />\n");
}
return fileset.toString();
}
/**
* Test if build file matches any of the regex patterns provided to get classfile jars
*/
@VisibleForTesting
boolean isClassFile(File buildFile) {
return getClassFilesFilters().stream()
.anyMatch(classFileFilter -> Pattern.matches(classFileFilter, buildFile.getPath()));
}
/**
* Run the ant build using the provided option
*/
private void runAntBuild(File buildXml) throws IOException {
// Run ant to generate the coverage report
String[] cmd = {
mAntBinary, "-verbose", "-f", buildXml.getAbsolutePath()
};
CommandResult result = runTimedCmd(mReportTimeout, cmd);
if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
CLog.e("Jacoco ant report generation failed [Status=%s]",
result.getStatus().toString());
throw new IOException(result.getStderr());
}
}
}