• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 Code Intelligence GmbH
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //      http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 package com.code_intelligence.jazzer.junit;
16 
17 import static java.util.Arrays.stream;
18 import static java.util.Collections.newSetFromMap;
19 import static java.util.stream.Collectors.joining;
20 import static java.util.stream.Collectors.toList;
21 import static org.junit.jupiter.api.Named.named;
22 import static org.junit.jupiter.params.provider.Arguments.arguments;
23 
24 import com.code_intelligence.jazzer.utils.UnsafeProvider;
25 import com.code_intelligence.jazzer.utils.UnsafeUtils;
26 import java.io.File;
27 import java.io.IOException;
28 import java.lang.invoke.MethodType;
29 import java.lang.management.ManagementFactory;
30 import java.lang.reflect.Array;
31 import java.lang.reflect.Method;
32 import java.lang.reflect.Modifier;
33 import java.lang.reflect.Proxy;
34 import java.nio.file.FileVisitResult;
35 import java.nio.file.Files;
36 import java.nio.file.Path;
37 import java.nio.file.Paths;
38 import java.nio.file.SimpleFileVisitor;
39 import java.nio.file.attribute.BasicFileAttributes;
40 import java.time.Duration;
41 import java.util.Arrays;
42 import java.util.HashSet;
43 import java.util.IdentityHashMap;
44 import java.util.List;
45 import java.util.Optional;
46 import java.util.Set;
47 import java.util.regex.Pattern;
48 import java.util.stream.Stream;
49 import org.junit.jupiter.api.extension.ExtensionContext;
50 import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
51 import org.junit.jupiter.params.provider.Arguments;
52 
53 class Utils {
54   /**
55    * Returns the resource path of the inputs directory for a given test class and method. The path
56    * will have the form
57    * {@code <class name>Inputs/<method name>}
58    */
inputsDirectoryResourcePath(Class<?> testClass, Method testMethod)59   static String inputsDirectoryResourcePath(Class<?> testClass, Method testMethod) {
60     return testClass.getSimpleName() + "Inputs"
61         + "/" + testMethod.getName();
62   }
63 
inputsDirectoryResourcePath(Class<?> testClass)64   static String inputsDirectoryResourcePath(Class<?> testClass) {
65     return testClass.getSimpleName() + "Inputs";
66   }
67 
68   /**
69    * Returns the file system path of the inputs corpus directory in the source tree, if it exists.
70    * The directory is created if it does not exist, but the test resource directory itself exists.
71    */
inputsDirectorySourcePath( Class<?> testClass, Method testMethod, Path baseDir)72   static Optional<Path> inputsDirectorySourcePath(
73       Class<?> testClass, Method testMethod, Path baseDir) {
74     String inputsResourcePath = Utils.inputsDirectoryResourcePath(testClass, testMethod);
75     // Make the inputs resource path absolute.
76     if (!inputsResourcePath.startsWith("/")) {
77       String inputsPackage = testClass.getPackage().getName().replace('.', '/');
78       inputsResourcePath = "/" + inputsPackage + "/" + inputsResourcePath;
79     }
80 
81     // Following the Maven directory layout, we look up the inputs directory under
82     // src/test/resources. This should be correct also for multi-module projects as JUnit is usually
83     // launched in the current module's root directory.
84     Path testResourcesDirectory = baseDir.resolve("src").resolve("test").resolve("resources");
85     Path sourceInputsDirectory = testResourcesDirectory;
86     for (String segment : inputsResourcePath.split("/")) {
87       sourceInputsDirectory = sourceInputsDirectory.resolve(segment);
88     }
89     if (Files.isDirectory(sourceInputsDirectory)) {
90       return Optional.of(sourceInputsDirectory);
91     }
92     // If we can at least find the test resource directory, create the inputs directory.
93     if (!Files.isDirectory(testResourcesDirectory)) {
94       return Optional.empty();
95     }
96     try {
97       return Optional.of(Files.createDirectories(sourceInputsDirectory));
98     } catch (Exception e) {
99       return Optional.empty();
100     }
101   }
102 
generatedCorpusPath(Class<?> testClass, Method testMethod)103   static Path generatedCorpusPath(Class<?> testClass, Method testMethod) {
104     return Paths.get(".cifuzz-corpus", testClass.getName(), testMethod.getName());
105   }
106 
107   /**
108    * Returns a heuristic default value for jazzer.instrument based on the test class.
109    */
getLegacyInstrumentationFilter(Class<?> testClass)110   static String getLegacyInstrumentationFilter(Class<?> testClass) {
111     // This is an extremely rough "implementation" of the public suffix list algorithm
112     // (https://publicsuffix.org/): It tries to guess the shortest prefix of the package name that
113     // isn't public. It doesn't use the actual list, but instead assumes that every root segment as
114     // well as "com.github" are public. Examples:
115     // - com.example.Test --> com.example.**
116     // - com.example.foobar.Test --> com.example.**
117     // - com.github.someones.repo.Test --> com.github.someones.**
118     String packageName = testClass.getPackage().getName();
119     String[] packageSegments = packageName.split("\\.");
120     int numSegments = 2;
121     if (packageSegments.length > 2 && packageSegments[0].equals("com")
122         && packageSegments[1].equals("github")) {
123       numSegments = 3;
124     }
125     return Stream.concat(Arrays.stream(packageSegments).limit(numSegments), Stream.of("**"))
126         .collect(joining("."));
127   }
128 
129   private static final Pattern CLASSPATH_SPLITTER =
130       Pattern.compile(Pattern.quote(File.pathSeparator));
131 
132   /**
133    * Returns a heuristic default value for jazzer.instrument based on the files on the provided
134    * classpath.
135    */
getClassPathBasedInstrumentationFilter(String classPath)136   static Optional<String> getClassPathBasedInstrumentationFilter(String classPath) {
137     List<Path> includes =
138         CLASSPATH_SPLITTER.splitAsStream(classPath)
139             .map(Paths::get)
140             // We consider classpath entries that are directories rather than jar files to contain
141             // the classes of the current project rather than external dependencies. This is just a
142             // heuristic and breaks with build systems that package all classes in jar files, e.g.
143             // with Bazel.
144             .filter(Files::isDirectory)
145             .flatMap(root -> {
146               HashSet<Path> pkgs = new HashSet<>();
147               try {
148                 Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
149                   @Override
150                   public FileVisitResult preVisitDirectory(
151                       Path dir, BasicFileAttributes basicFileAttributes) throws IOException {
152                     try (Stream<Path> entries = Files.list(dir)) {
153                       // If a directory contains a .class file, we add an include filter matching it
154                       // and all subdirectories.
155                       // Special case: If there is a class defined at the root, only the unnamed
156                       // package is included, so continue with the traversal of subdirectories
157                       // to discover additional includes.
158                       if (entries.filter(path -> path.toString().endsWith(".class"))
159                               .anyMatch(Files::isRegularFile)) {
160                         Path pkgPath = root.relativize(dir);
161                         pkgs.add(pkgPath);
162                         if (pkgPath.toString().isEmpty()) {
163                           return FileVisitResult.CONTINUE;
164                         } else {
165                           return FileVisitResult.SKIP_SUBTREE;
166                         }
167                       }
168                     }
169                     return FileVisitResult.CONTINUE;
170                   }
171                 });
172               } catch (IOException e) {
173                 // This is only a best-effort heuristic anyway, ignore this directory.
174                 return Stream.of();
175               }
176               return pkgs.stream();
177             })
178             .distinct()
179             .collect(toList());
180     if (includes.isEmpty()) {
181       return Optional.empty();
182     }
183     return Optional.of(
184         includes.stream()
185             .map(Path::toString)
186             // For classes without a package, only include the unnamed package.
187             .map(path -> path.isEmpty() ? "*" : path.replace(File.separator, ".") + ".**")
188             .sorted()
189             // jazzer.instrument uses ',' as the separator.
190             .collect(joining(",")));
191   }
192 
193   private static final Pattern COVERAGE_AGENT_ARG =
194       Pattern.compile("-javaagent:.*(?:intellij-coverage-agent|jacoco).*");
isCoverageAgentPresent()195   static boolean isCoverageAgentPresent() {
196     return ManagementFactory.getRuntimeMXBean().getInputArguments().stream().anyMatch(
197         s -> COVERAGE_AGENT_ARG.matcher(s).matches());
198   }
199 
200   private static final boolean IS_FUZZING_ENV =
201       System.getenv("JAZZER_FUZZ") != null && !System.getenv("JAZZER_FUZZ").isEmpty();
isFuzzing(ExtensionContext extensionContext)202   static boolean isFuzzing(ExtensionContext extensionContext) {
203     return IS_FUZZING_ENV || runFromCommandLine(extensionContext);
204   }
205 
runFromCommandLine(ExtensionContext extensionContext)206   static boolean runFromCommandLine(ExtensionContext extensionContext) {
207     return extensionContext.getConfigurationParameter("jazzer.internal.commandLine")
208         .map(Boolean::parseBoolean)
209         .orElse(false);
210   }
211 
212   /**
213    * Returns true if and only if the value is equal to "true", "1", or "yes" case-insensitively.
214    */
permissivelyParseBoolean(String value)215   static boolean permissivelyParseBoolean(String value) {
216     return value.equalsIgnoreCase("true") || value.equals("1") || value.equalsIgnoreCase("yes");
217   }
218 
219   /**
220    * Convert the string to ISO 8601 (https://en.wikipedia.org/wiki/ISO_8601#Durations). We do not
221    * allow for duration units longer than hours, so we can always prepend PT.
222    */
durationStringToSeconds(String duration)223   static long durationStringToSeconds(String duration) {
224     String isoDuration =
225         "PT" + duration.replace("sec", "s").replace("min", "m").replace("hr", "h").replace(" ", "");
226     return Duration.parse(isoDuration).getSeconds();
227   }
228 
229   /**
230    * Creates {@link Arguments} for a single invocation of a parameterized test that can be
231    * identified as having been created in this way by {@link #isMarkedInvocation}.
232    *
233    * @param displayName the display name to assign to every argument
234    */
getMarkedArguments(Method method, String displayName)235   static Arguments getMarkedArguments(Method method, String displayName) {
236     return arguments(stream(method.getParameterTypes())
237                          .map(Utils::getMarkedInstance)
238                          // Wrap in named as toString may crash on marked instances.
239                          .map(arg -> named(displayName, arg))
240                          .toArray(Object[] ::new));
241   }
242 
243   /**
244    * @return {@code true} if and only if the arguments for this test method invocation were created
245    * with {@link #getMarkedArguments}
246    */
isMarkedInvocation(ReflectiveInvocationContext<Method> invocationContext)247   static boolean isMarkedInvocation(ReflectiveInvocationContext<Method> invocationContext) {
248     if (invocationContext.getArguments().stream().anyMatch(Utils::isMarkedInstance)) {
249       if (invocationContext.getArguments().stream().allMatch(Utils::isMarkedInstance)) {
250         return true;
251       }
252       throw new IllegalStateException(
253           "Some, but not all arguments were marked in invocation of " + invocationContext);
254     } else {
255       return false;
256     }
257   }
258 
259   private static final ClassValue<Object> uniqueInstanceCache = new ClassValue<Object>() {
260     @Override
261     protected Object computeValue(Class<?> clazz) {
262       return makeMarkedInstance(clazz);
263     }
264   };
265   private static final Set<Object> uniqueInstances = newSetFromMap(new IdentityHashMap<>());
266 
267   // Visible for testing.
getMarkedInstance(Class<T> clazz)268   static <T> T getMarkedInstance(Class<T> clazz) {
269     // makeMarkedInstance creates new classes, which is expensive and can cause the JVM to run out
270     // of metaspace. We thus cache the marked instances per class.
271     Object instance = uniqueInstanceCache.get(clazz);
272     uniqueInstances.add(instance);
273     return (T) instance;
274   }
275 
276   // Visible for testing.
isMarkedInstance(Object instance)277   static boolean isMarkedInstance(Object instance) {
278     return uniqueInstances.contains(instance);
279   }
280 
makeMarkedInstance(Class<?> clazz)281   private static Object makeMarkedInstance(Class<?> clazz) {
282     if (clazz == Class.class) {
283       return new Object() {}.getClass();
284     }
285     if (clazz.isArray()) {
286       return Array.newInstance(clazz.getComponentType(), 0);
287     }
288     if (clazz.isInterface()) {
289       return Proxy.newProxyInstance(
290           Utils.class.getClassLoader(), new Class[] {clazz}, (o, method, objects) -> null);
291     }
292 
293     if (clazz.isPrimitive()) {
294       clazz = MethodType.methodType(clazz).wrap().returnType();
295     } else if (Modifier.isAbstract(clazz.getModifiers())) {
296       clazz = UnsafeUtils.defineAnonymousConcreteSubclass(clazz);
297     }
298 
299     try {
300       return clazz.cast(UnsafeProvider.getUnsafe().allocateInstance(clazz));
301     } catch (InstantiationException e) {
302       throw new IllegalStateException(e);
303     }
304   }
305 }
306