Skip to content

Commit

Permalink
Capture all classloaders loading mutant
Browse files Browse the repository at this point in the history
  • Loading branch information
Henry Coles committed Jul 29, 2022
1 parent 72735bc commit 75056c8
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public List<List<MutationDetails>> groupMutations(
final Map<ClassName, Collection<MutationDetails>> bucketed = FCollection
.bucket(mutations, MutationDetails::getClassName);
final List<List<MutationDetails>> chunked = new ArrayList<>();
for (final Collection<MutationDetails> each : bucketed.values()) {
shrinkToMaximumUnitSize(chunked, each);
for (final Map.Entry<ClassName,Collection<MutationDetails>> each : bucketed.entrySet()) {
shrinkToMaximumUnitSize(chunked, each.getValue());
}

return chunked;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.classloaders;

import java.util.function.IntSupplier;

public class MuteeInOtherClassloader implements IntSupplier {

@Override
public int getAsInt() {
pointless();
System.out.println("Can't kill me");
return 42;
}

public void pointless() {
System.out.println("Can't kill me either");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.classloaders;

import org.junit.Test;
import org.pitest.classpath.ClassPath;
import org.pitest.mutationtest.execute.DefaultPITClassloader;

import java.util.function.IntSupplier;

import static org.junit.Assert.assertEquals;

public class MuteeInOtherClassloaderPooledTest {

// will persist between mutants
static DefaultPITClassloader otherLoader = new DefaultPITClassloader(new ClassPath(), null);

@Test
public void returns42() throws Exception {
Class<?> clz = otherLoader.loadClass(MuteeInOtherClassloader.class.getName());
IntSupplier underTest = (IntSupplier) clz.getConstructor().newInstance();
assertEquals(42, underTest.getAsInt());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.classloaders;

import org.junit.Test;
import org.pitest.classpath.ClassPath;
import org.pitest.mutationtest.execute.DefaultPITClassloader;

import java.util.function.IntSupplier;

import static org.junit.Assert.assertEquals;

public class MuteeInOtherClassloaderTest {

@Test
public void returns42() throws Exception {
DefaultPITClassloader otherLoader = new DefaultPITClassloader(new ClassPath(), null);
Class<?> clz = otherLoader.loadClass(MuteeInOtherClassloader.class.getName());
IntSupplier underTest = (IntSupplier) clz.getConstructor().newInstance();
assertEquals(42, underTest.getAsInt());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
import java.util.Collections;
import java.util.List;

import com.example.classloaders.MuteeInOtherClassloader;
import com.example.classloaders.MuteeInOtherClassloaderPooledTest;
import com.example.classloaders.MuteeInOtherClassloaderTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
Expand Down Expand Up @@ -428,6 +431,34 @@ public void shouldRecoverFromMinionThatDoesNotStart() {
// just running without a hang then erroring is success
}

@Test
public void handlesMutantsInOtherClassLoaders() {
setMutators("PRIMITIVE_RETURNS", "VOID_METHOD_CALLS");

this.data
.setTargetClasses(asGlobs(MuteeInOtherClassloader.class));
this.data
.setTargetTests(predicateFor(MuteeInOtherClassloaderTest.class));

createAndRun();

verifyResults(KILLED, SURVIVED, SURVIVED, SURVIVED);
}

@Test
public void handlesMutantsInPooledClassLoaders() {
setMutators("PRIMITIVE_RETURNS", "VOID_METHOD_CALLS");

this.data
.setTargetClasses(asGlobs(MuteeInOtherClassloader.class));
this.data
.setTargetTests(predicateFor(MuteeInOtherClassloaderPooledTest.class));

createAndRun();

verifyResults(KILLED, SURVIVED, SURVIVED, SURVIVED);
}

@Generated
public static class AnnotatedToAvoidAtClassLevel {
public int mutateMe() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ private CoverageOptions createCoverageOptions(TestPluginArguments configuration)
configuration, this.data.getVerbosity());
}

protected void setMutators(final String mutator) {
protected void setMutators(final String... mutator) {
this.data.setMutators(Arrays.asList(mutator));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,10 @@ public void shouldWorkWithQuarkus() throws Exception {

String actual = readResults(testDir);
// Test is flaky. Needs investigation
//assertThat(actual)
// .contains(
// "<mutation detected='false' status='SURVIVED' numberOfTestsRun='2'>" +
// "<sourceFile>ExampleController.java</sourceFile>");
assertThat(actual)
.contains(
"<mutation detected='false' status='SURVIVED' numberOfTestsRun='2'>" +
"<sourceFile>ExampleController.java</sourceFile>");

assertThat(actual)
.contains(
Expand Down
2 changes: 2 additions & 0 deletions pitest/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@
<artifactId>equalsverifier</artifactId>
<scope>test</scope>
</dependency>


</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.WeakHashMap;

/**
* Pitest mainly inserts mutants by calling Instrumentation.redefineClasses using
Expand All @@ -22,83 +22,42 @@
* the byte array (modifying by rerunning the mutator might result in a different mutant for
* the same id if the input has changed).
*
* So, sometimes we want to transform the class, sometimes we don't.
*
* Not found a way to reliably work out which we need to do, so need to maintain
* a list. As mutants unexpectedly surviving are far more likely to be reported than
* ones unexpectedly killed, we list the loaders we should mutate for (currently just
* quarkus, but others likely exist).
* This transformer identifies loaders that try to load the target class, and stores
* a weak reference to them to ensure that all copies of the class are mutated.
*
*/
public class CatchNewClassLoadersTransformer implements ClassFileTransformer {

private static String targetClass;
private static byte[] currentMutant;
private static String currentClass;

// The context classloader at the point pitest started.
// we do not want to transform classes from this as they are already handled
// by the primary mechanism
private static ClassLoader ignore;

// Map of loaders we have transformed the current class in, so we can restore them
static final Map<ClassLoader, byte[]> ORIGINAL_LOADER_CLASSES = new ConcurrentHashMap<>();

public static synchronized void setLoader(ClassLoader loader) {
ignore = loader;
}
// What we want is a ConcurrentWeakHasSet, since that doesn't exist without writing one ourselves
// we'll abuse a WeakHashMap and live with the synchronization
static final Map<ClassLoader, Object> CLASS_LOADERS = Collections.synchronizedMap(new WeakHashMap<>());

public static synchronized void setMutant(String className, byte[] mutant) {
String toRestore = currentClass;
targetClass = className;
currentMutant = mutant;

// prevent transforming again when we restore if the same class is mutated twice
currentClass = null;

restoreClasses(toRestore);

currentClass = className;
}

private static void restoreClasses(String toRestore) {
for (Map.Entry<ClassLoader, byte[]> each : ORIGINAL_LOADER_CLASSES.entrySet()) {
final Class<?> clazz = checkClassForLoader(each.getKey(), toRestore);
for (ClassLoader each : CLASS_LOADERS.keySet()) {
final Class<?> clazz = checkClassForLoader(each, className);
if (clazz != null) {
HotSwapAgent.hotSwap(clazz, each.getValue());
HotSwapAgent.hotSwap(clazz, mutant);
}
}
ORIGINAL_LOADER_CLASSES.clear();
}

@Override
public byte[] transform(final ClassLoader loader, final String className,
final Class<?> classBeingRedefined,
final ProtectionDomain protectionDomain, final byte[] classfileBuffer) {
if (loader == null || ignore == loader) {
return null;
}

// Only loader identified so far where mutants must be inserted is Quarkus, but
// others likely exist. At least one loader (gwtmockito) results incorrectly
// killed mutants if we insert.
if (!loader.getClass().getName().startsWith("io.quarkus.bootstrap.classloading")) {
return null;
}

if (className.equals(currentClass)) {
// skip if class already loaded
if (classBeingRedefined != null) {
return null;
}

// avoid restoring an already mutated class
// Not clear if this situation is possible, but check left in
// out of fear.
if (!Arrays.equals(classfileBuffer, currentMutant)) {
ORIGINAL_LOADER_CLASSES.put(loader, classfileBuffer);
}

if (className.equals(targetClass)) {
CLASS_LOADERS.put(loader, null);
// we might be mid-mutation so return the mutated bytes
return currentMutant;
}

return null;
}

Expand Down
60 changes: 15 additions & 45 deletions pitest/src/main/java/org/pitest/mutationtest/execute/HotSwap.java
Original file line number Diff line number Diff line change
@@ -1,62 +1,32 @@
package org.pitest.mutationtest.execute;

import org.pitest.boot.HotSwapAgent;
import org.pitest.classinfo.ClassByteArraySource;
import org.pitest.classinfo.ClassName;
import org.pitest.functional.F3;
import org.pitest.util.Unchecked;

class HotSwap implements F3<ClassName, ClassLoader, byte[], Boolean> {
/**
* Since pitest 1.9.4 there is an implicit assumption that pitest will never mutate
* more than once class within the same jvm. If this assumption is ever broken, mutants
* from the previous class would remain active and invalidate results.
*/
class HotSwap {

private final ClassByteArraySource byteSource;
private byte[] lastClassPreMutation;
private ClassName lastMutatedClass;
private ClassLoader lastUsedLoader;

HotSwap(final ClassByteArraySource byteSource) {
this.byteSource = byteSource;
}

@Override
public Boolean apply(final ClassName clazzName, final ClassLoader loader,
final byte[] b) {
public Boolean insertClass(final ClassName clazzName, ClassLoader loader, final byte[] mutantBytes) {
try {
// Some frameworks (eg quarkus) run tests in non delegating
// classloaders. Need to make sure these are transformed too
CatchNewClassLoadersTransformer.setMutant(clazzName.asInternalName(), mutantBytes);

System.out.println("Hotswap loader " + loader);

restoreLastClass(this.byteSource, clazzName, loader);
this.lastUsedLoader = loader;
// trigger loading for the current loader
Class<?> clazz = Class.forName(clazzName.asJavaName(), false, loader);
return HotSwapAgent.hotSwap(clazz, b);
} catch (final ClassNotFoundException e) {
throw Unchecked.translateCheckedException(e);
}

}

private void restoreLastClass(final ClassByteArraySource byteSource,
final ClassName clazzName, final ClassLoader loader)
throws ClassNotFoundException {
if ((this.lastMutatedClass != null)
&& !this.lastMutatedClass.equals(clazzName)) {
restoreForLoader(this.lastUsedLoader);
restoreForLoader(loader);
}
// will still need to explicitly swap it... not clear why the transformed does not do this
return HotSwapAgent.hotSwap(clazz, mutantBytes);

if ((this.lastMutatedClass == null)
|| !this.lastMutatedClass.equals(clazzName)) {
this.lastClassPreMutation = byteSource.getBytes(clazzName.asJavaName())
.get();
} catch (final ClassNotFoundException e) {
throw Unchecked.translateCheckedException(e);
}

this.lastMutatedClass = clazzName;
}

private void restoreForLoader(ClassLoader loader)
throws ClassNotFoundException {
final Class<?> clazz = Class.forName(this.lastMutatedClass.asJavaName(), false,
loader);
HotSwapAgent.hotSwap(clazz, this.lastClassPreMutation);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,11 @@
*/
package org.pitest.mutationtest.execute;

import java.io.IOException;
import java.lang.management.MemoryNotificationInfo;
import java.net.Socket;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import javax.management.NotificationListener;
import javax.management.openmbean.CompositeData;

import org.pitest.boot.HotSwapAgent;
import org.pitest.classinfo.CachingByteArraySource;
import org.pitest.classinfo.ClassByteArraySource;
import org.pitest.classinfo.ClassName;
import org.pitest.classpath.ClassloaderByteArraySource;
import org.pitest.functional.F3;
import org.pitest.functional.prelude.Prelude;
import org.pitest.mutationtest.EngineArguments;
import org.pitest.mutationtest.config.ClientPluginServices;
Expand All @@ -50,6 +37,17 @@
import org.pitest.util.Log;
import org.pitest.util.SafeDataInputStream;

import javax.management.NotificationListener;
import javax.management.openmbean.CompositeData;
import java.io.IOException;
import java.lang.management.MemoryNotificationInfo;
import java.net.Socket;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class MutationTestMinion {

private static final Logger LOG = Log.getLogger();
Expand Down Expand Up @@ -79,13 +77,10 @@ public void run() {

final ClassLoader loader = IsolationUtils.getContextClassLoader();

CatchNewClassLoadersTransformer.setLoader(loader);

final ClassByteArraySource byteSource = new CachingByteArraySource(new ClassloaderByteArraySource(
loader), CACHE_SIZE);

final F3<ClassName, ClassLoader, byte[], Boolean> hotswap = new HotSwap(
byteSource);
final HotSwap hotswap = new HotSwap();

final MutationEngine engine = createEngine(paramsFromParent.engine, paramsFromParent.engineArgs);

Expand Down

0 comments on commit 75056c8

Please sign in to comment.