/**
* Copyright (c) 2000-present Liferay, Inc. All rights reserved.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*/
package com.liferay.whip.agent;
import com.liferay.whip.coveragedata.ClassData;
import com.liferay.whip.coveragedata.LineData;
import com.liferay.whip.coveragedata.ProjectData;
import com.liferay.whip.coveragedata.ProjectDataUtil;
import com.liferay.whip.instrument.WhipClassFileTransformer;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.ArrayList;
import java.util.List;
/**
* @author Shuyang Zhou
*/
public class InstrumentationAgent {
public static synchronized void assertCoverage(
boolean includeInnerClasses, Class<?>... classes) {
if (!_dynamicallyInstrumented) {
return;
}
_instrumentation.removeTransformer(_whipClassFileTransformer);
_whipClassFileTransformer = null;
try {
ProjectData projectData = ProjectDataUtil.captureProjectData(
false, false);
List<AssertionError> assertionErrors = new ArrayList<>();
for (Class<?> clazz : classes) {
if (clazz.isSynthetic()) {
continue;
}
ClassData classData = projectData.getClassData(clazz.getName());
_assertClassDataCoverage(assertionErrors, clazz, classData);
if (includeInnerClasses) {
Class<?>[] declaredClasses = clazz.getDeclaredClasses();
declaredClass:
for (Class<?> declaredClass : declaredClasses) {
if (declaredClass.isSynthetic()) {
continue;
}
for (Class<?> clazz2 : classes) {
if (clazz2.equals(declaredClass)) {
continue declaredClass;
}
}
classData = projectData.getClassData(
declaredClass.getName());
_assertClassDataCoverage(
assertionErrors, declaredClass, classData);
}
}
}
if (!assertionErrors.isEmpty()) {
AssertionError assertionError = assertionErrors.get(0);
for (int i = 1; i < assertionErrors.size(); i++) {
assertionError.addSuppressed(assertionErrors.get(i));
}
throw assertionError;
}
}
finally {
_dynamicallyInstrumented = false;
if (_originalClassDefinitions != null) {
try {
List<ClassDefinition> classDefinitions = new ArrayList<>(
_originalClassDefinitions.size());
for (OriginalClassDefinition originalClassDefinition :
_originalClassDefinitions) {
ClassDefinition classDefinition =
originalClassDefinition.toClassDefinition();
if (classDefinition != null) {
classDefinitions.add(classDefinition);
}
}
_originalClassDefinitions = null;
_instrumentation.redefineClasses(
classDefinitions.toArray(
new ClassDefinition[classDefinitions.size()]));
}
catch (Exception e) {
throw new RuntimeException(
"Unable to uninstrument classes", e);
}
}
}
}
public static synchronized void dynamicallyInstrument(
String[] includes, String[] excludes)
throws UnmodifiableClassException {
if ((_instrumentation == null) || _dynamicallyInstrumented) {
return;
}
if (includes == null) {
includes = _includes;
}
if (excludes == null) {
excludes = _excludes;
}
String agentLine = System.getProperty("whip.agent");
int index = agentLine.indexOf('=');
if (index != -1) {
StringBuilder sb = new StringBuilder();
sb.append(agentLine, 0, index + 1);
for (String include : includes) {
sb.append(include);
sb.append(',');
}
if (includes.length > 0) {
sb.setLength(sb.length() - 1);
}
sb.append(';');
for (String exclude : excludes) {
sb.append(exclude);
sb.append(',');
}
if (excludes.length > 0) {
sb.setLength(sb.length() - 1);
}
System.setProperty("whip.agent", sb.toString());
}
if (_whipClassFileTransformer == null) {
_whipClassFileTransformer = new WhipClassFileTransformer(
includes, excludes);
}
_instrumentation.addTransformer(_whipClassFileTransformer, true);
Class<?>[] allLoadedClasses = _instrumentation.getAllLoadedClasses();
List<Class<?>> modifiableClasses = new ArrayList<>();
for (Class<?> loadedClass : allLoadedClasses) {
if (_instrumentation.isModifiableClass(loadedClass)) {
String className = loadedClass.getName();
className = className.replace('.', '/');
if (_whipClassFileTransformer.matches(className)) {
modifiableClasses.add(loadedClass);
}
}
}
_instrumentation.retransformClasses(
modifiableClasses.toArray(new Class<?>[modifiableClasses.size()]));
_dynamicallyInstrumented = true;
_originalClassDefinitions = null;
}
public static File getDataFile() {
return _dataFile;
}
public static File getLockFile() {
return _lockFile;
}
public static synchronized void premain(
String agentArguments, Instrumentation instrumentation) {
String[] arguments = agentArguments.split(";");
String[] includes = arguments[0].split(",");
String[] excludes = arguments[1].split(",");
if (Boolean.getBoolean("whip.static.instrument")) {
final WhipClassFileTransformer whipClassFileTransformer =
new WhipClassFileTransformer(includes, excludes);
instrumentation.addTransformer(whipClassFileTransformer);
Runtime runtime = Runtime.getRuntime();
runtime.addShutdownHook(
new Thread() {
@Override
public void run() {
ProjectDataUtil.captureProjectData(
true,
Boolean.getBoolean(
"whip.static.instrument.use.data.file"));
}
});
}
else if (instrumentation.isRedefineClassesSupported() &&
instrumentation.isRetransformClassesSupported()) {
_instrumentation = instrumentation;
_includes = includes;
_excludes = excludes;
// Forcibly clear the data file to make sure that the coverage
// assert is based on the current test
_dataFile.delete();
}
else {
StringBuilder sb = new StringBuilder();
sb.append("Current JVM is not capable for dynamic ");
sb.append("instrumententation. Instrumentation ");
if (instrumentation.isRetransformClassesSupported()) {
sb.append("supports ");
}
else {
sb.append("does not support ");
}
sb.append("restranforming classes. Instrumentation ");
if (instrumentation.isRedefineClassesSupported()) {
sb.append("supports ");
}
else {
sb.append("does not support ");
}
sb.append("redefining classes. Dynamic instrumententation is ");
sb.append("disabled.");
System.out.println(sb.toString());
}
}
public static synchronized void recordInstrumentation(
ClassLoader classLoader, String name, byte[] bytes) {
if (!_dynamicallyInstrumented) {
return;
}
if (_originalClassDefinitions == null) {
_originalClassDefinitions = new ArrayList<>();
}
OriginalClassDefinition originalClassDefinition =
new OriginalClassDefinition(classLoader, name, bytes);
_originalClassDefinitions.add(originalClassDefinition);
}
private static void _assertClassDataCoverage(
List<AssertionError> assertionErrors, Class<?> clazz,
ClassData classData) {
if (clazz.isInterface()) {
return;
}
if ((classData.getBranchCoverageRate() != 1.0) ||
(classData.getLineCoverageRate() != 1.0)) {
StringBuilder sb = new StringBuilder();
sb.append("%n[Whip] %s is not fully covered.%n[Whip]Branch ");
sb.append("coverage rate : %.2f, line coverage rate : ");
sb.append("%.2f.%n[Whip]Please rerun test with ");
sb.append("-Djunit.code.coverage=true to see coverage report.%n");
System.out.printf(
sb.toString(), classData.getName(),
classData.getBranchCoverageRate(),
classData.getLineCoverageRate());
for (LineData lineData : classData.getLines()) {
if (lineData.isCovered()) {
continue;
}
System.out.printf(
"[Whip] %s line %d is not covered %n", classData.getName(),
lineData.getLineNumber());
}
assertionErrors.add(
new AssertionError(
classData.getName() + " is not fully covered"));
return;
}
System.out.printf("[Whip] %s is fully covered.%n", classData.getName());
}
private static final File _dataFile;
private static boolean _dynamicallyInstrumented;
private static String[] _excludes;
private static String[] _includes;
private static Instrumentation _instrumentation;
private static final File _lockFile;
private static List<OriginalClassDefinition> _originalClassDefinitions;
private static WhipClassFileTransformer _whipClassFileTransformer;
static {
_dataFile = new File(System.getProperty("whip.datafile"));
File parentFolder = _dataFile.getParentFile();
if (!parentFolder.exists()) {
parentFolder.mkdirs();
}
// OS wide lock file is created by the first started process where we
// know for sure that there is no race condition. Acquiring an exclusive
// lock on this lock file prevents losing updates on the data file.
_lockFile = new File(parentFolder, "lock");
try {
_lockFile.createNewFile();
}
catch (IOException ioe) {
throw new ExceptionInInitializerError(ioe);
}
}
private static class OriginalClassDefinition {
public ClassDefinition toClassDefinition() {
try {
Class<?> clazz = Class.forName(_className, true, _classLoader);
return new ClassDefinition(clazz, _bytes);
}
catch (Throwable t) {
return null;
}
}
private OriginalClassDefinition(
ClassLoader classLoader, String className, byte[] bytes) {
_classLoader = classLoader;
_className = className.replace('/', '.');
_bytes = bytes;
}
private final byte[] _bytes;
private final ClassLoader _classLoader;
private final String _className;
}
}