Skip to content

Commit

Permalink
junit: Compute a default for jazzer.instrument based on the class path
Browse files Browse the repository at this point in the history
The new heuristic walks the classpath directory and considers all
directories (but not jar files) to constitute the classes of the current
project. All subdirectories that include `.class` files are translated
into packages and all their subpackages are subject to instrumentation.

This is expected to give much better results than the previous
heuristic, which only considered the first two or three segments of the
test class package.
  • Loading branch information
fmeum committed May 31, 2023
1 parent 7883e98 commit 6f5b3c5
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

package com.code_intelligence.jazzer.junit;

import static com.code_intelligence.jazzer.junit.Utils.getClassPathBasedInstrumentationFilter;
import static com.code_intelligence.jazzer.junit.Utils.getLegacyInstrumentationFilter;

import java.io.File;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.extension.ExtensionContext;
Expand Down Expand Up @@ -48,7 +51,11 @@ static void forFuzzing(ExtensionContext executionRequest) {
String instrumentationFilter =
executionRequest.getConfigurationParameter("jazzer.instrument")
.orElseGet(
() -> Utils.defaultInstrumentationFilter(executionRequest.getRequiredTestClass()));
()
-> getClassPathBasedInstrumentationFilter(System.getProperty("java.class.path"))
.orElseGet(()
-> getLegacyInstrumentationFilter(
executionRequest.getRequiredTestClass())));
String filter = String.join(File.pathSeparator, instrumentationFilter.split(","));
System.setProperty("jazzer.custom_hook_includes", filter);
System.setProperty("jazzer.instrumentation_includes", filter);
Expand Down
81 changes: 78 additions & 3 deletions src/main/java/com/code_intelligence/jazzer/junit/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,35 @@

import static java.util.Arrays.stream;
import static java.util.Collections.newSetFromMap;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import com.code_intelligence.jazzer.utils.UnsafeProvider;
import com.code_intelligence.jazzer.utils.UnsafeUtils;
import java.io.File;
import java.io.IOException;
import java.lang.invoke.MethodType;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
Expand Down Expand Up @@ -96,7 +104,10 @@ static Path generatedCorpusPath(Class<?> testClass, Method testMethod) {
return Paths.get(".cifuzz-corpus", testClass.getName(), testMethod.getName());
}

static String defaultInstrumentationFilter(Class<?> testClass) {
/**
* Returns a heuristic default value for jazzer.instrument based on the test class.
*/
static String getLegacyInstrumentationFilter(Class<?> testClass) {
// This is an extremely rough "implementation" of the public suffix list algorithm
// (https://publicsuffix.org/): It tries to guess the shortest prefix of the package name that
// isn't public. It doesn't use the actual list, but instead assumes that every root segment as
Expand All @@ -112,7 +123,71 @@ static String defaultInstrumentationFilter(Class<?> testClass) {
numSegments = 3;
}
return Stream.concat(Arrays.stream(packageSegments).limit(numSegments), Stream.of("**"))
.collect(Collectors.joining("."));
.collect(joining("."));
}

private static final Pattern CLASSPATH_SPLITTER =
Pattern.compile(Pattern.quote(File.pathSeparator));

/**
* Returns a heuristic default value for jazzer.instrument based on the files on the provided
* classpath.
*/
static Optional<String> getClassPathBasedInstrumentationFilter(String classPath) {
List<Path> includes =
CLASSPATH_SPLITTER.splitAsStream(classPath)
.map(Paths::get)
// We consider classpath entries that are directories rather than jar files to contain
// the classes of the current project rather than external dependencies. This is just a
// heuristic and breaks with build systems that package all classes in jar files, e.g.
// with Bazel.
.filter(Files::isDirectory)
.flatMap(root -> {
HashSet<Path> pkgs = new HashSet<>();
try {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(
Path dir, BasicFileAttributes basicFileAttributes) throws IOException {
try (Stream<Path> entries = Files.list(dir)) {
// If a directory contains a .class file, we add an include filter matching it
// and all subdirectories.
// Special case: If there is a class defined at the root, only the unnamed
// package is included, so continue with the traversal of subdirectories
// to discover additional includes.
if (entries.filter(path -> path.toString().endsWith(".class"))
.anyMatch(Files::isRegularFile)) {
Path pkgPath = root.relativize(dir);
pkgs.add(pkgPath);
if (pkgPath.toString().isEmpty()) {
return FileVisitResult.CONTINUE;
} else {
return FileVisitResult.SKIP_SUBTREE;
}
}
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
// This is only a best-effort heuristic anyway, ignore this directory.
return Stream.of();
}
return pkgs.stream();
})
.distinct()
.collect(toList());
if (includes.isEmpty()) {
return Optional.empty();
}
return Optional.of(
includes.stream()
.map(Path::toString)
// For classes without a package, only include the unnamed package.
.map(path -> path.isEmpty() ? "*" : path.replace(File.separator, ".") + ".**")
.sorted()
// jazzer.instrument uses ',' as the separator.
.collect(joining(",")));
}

private static final Pattern COVERAGE_AGENT_ARG =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ java_junit5_test(
srcs = ["UtilsTest.java"],
deps = JUNIT5_DEPS + [
"//src/main/java/com/code_intelligence/jazzer/junit:utils",
"@maven//:com_google_truth_extensions_truth_java8_extension",
"@maven//:com_google_truth_truth",
"@maven//:org_junit_jupiter_junit_jupiter_api",
"@maven//:org_junit_jupiter_junit_jupiter_params",
Expand Down
50 changes: 50 additions & 0 deletions src/test/java/com/code_intelligence/jazzer/junit/UtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@
import static com.code_intelligence.jazzer.junit.Utils.isMarkedInstance;
import static com.code_intelligence.jazzer.junit.Utils.isMarkedInvocation;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.createFile;
import static java.util.Arrays.stream;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.joining;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.util.AbstractList;
import java.util.AbstractMap;
import java.util.Arrays;
Expand All @@ -38,12 +45,15 @@
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

public class UtilsTest implements InvocationInterceptor {
@TempDir Path temp;

@Test
void testDurationStringToSeconds() {
assertThat(durationStringToSeconds("1m")).isEqualTo(60);
Expand Down Expand Up @@ -98,4 +108,44 @@ public void interceptTestTemplateMethod(Invocation<Void> invocation,
argumentsExpectedToBeMarked = !argumentsExpectedToBeMarked;
invocation.proceed();
}

@Test
public void testGetClassPathBasedInstrumentationFilter() throws IOException {
Path firstDir = createDirectories(temp.resolve("first_dir"));
Path orgExample = createDirectories(firstDir.resolve("org").resolve("example"));
createFile(orgExample.resolve("Application.class"));

Path nonExistentDir = temp.resolve("does not exist");

Path secondDir = createDirectories(temp.resolve("second").resolve("dir"));
createFile(secondDir.resolve("Root.class"));
Path comExampleProject =
createDirectories(secondDir.resolve("com").resolve("example").resolve("project"));
createFile(comExampleProject.resolve("Main.class"));
Path comExampleOtherProject =
createDirectories(secondDir.resolve("com").resolve("example").resolve("other_project"));
createFile(comExampleOtherProject.resolve("Lib.class"));

Path emptyDir = createDirectories(temp.resolve("some").resolve("empty").resolve("dir"));

Path firstJar = createFile(temp.resolve("first.jar"));
Path secondJar = createFile(temp.resolve("second.jar"));

assertThat(Utils.getClassPathBasedInstrumentationFilter(makeClassPath(
firstDir, firstJar, nonExistentDir, secondDir, secondJar, emptyDir)))
.hasValue("*,com.example.other_project.**,com.example.project.**,org.example.**");
}

@Test
public void testGetClassPathBasedInstrumentationFilter_noDirs() throws IOException {
Path firstJar = createFile(temp.resolve("first.jar"));
Path secondJar = createFile(temp.resolve("second.jar"));

assertThat(Utils.getClassPathBasedInstrumentationFilter(makeClassPath(firstJar, secondJar)))
.isEmpty();
}

private static String makeClassPath(Path... paths) {
return Arrays.stream(paths).map(Path::toString).collect(joining(File.pathSeparator));
}
}

0 comments on commit 6f5b3c5

Please sign in to comment.