blob: f46506332f1aa2d3ad1d87c0189c11933fbbf77c [file] [log] [blame]
// Copyright 2019 The Bazel Authors. All Rights Reserved.
//
// 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.google.testing.junit.runner;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.hash.HashCode;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.hash.HashingInputStream;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkRequest;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/** A utility class for running Java tests using persistent workers. */
final class PersistentTestRunner {
private PersistentTestRunner() {
// utility class; should not be instantiated
}
/**
* Runs new tests in the same process. Communicates with bazel using the worker's protocol. Reads
* a {@link WorkRequest} sent by bazel and sends back a {@link WorkResponse}.
*
* <p>Uses three different classloaders:
*
* <ul>
* <li>A classloader for direct dependencies: loads the test's classes and the first two layers
* of its dependencies.
* <li>A classloader for transitive dependencies: loads the remaining classes from the
* transitive dependencies.
* <li>The system classloader: initialized at JVM startup time; loads the test runner's classes.
* </ul>
*
* <p>The classloaders have a child-parent relationship: direct CL -> transitive CL -> system CL.
*
* <p>The direct dependencies classloader is the current thread's classloader.
*
* <p>The transitive dependencies classloader checks if a class was already loaded by the direct
* dependencies classloader if it did not succeed in loading the class itself. This is required
* for classes loaded by the transitive classloader that reference classes in the direct
* dependencies classloader.
*
* <p>The default class loading logic in {@link ClassLoader} applies in all other cases.
*
* <p>The direct and transitive classloaders are rebuilt before every test run only if the
* combined hash of the jars to be loaded has changed.
*/
static int runPersistentTestRunner(
String suitClassName, String workspacePrefix, SuiteTestRunner suiteTestRunner) {
PrintStream originalStdOut = System.out;
PrintStream originalStdErr = System.err;
// TODO(elenairina): Remove this variable after cl/282553936 is released.
String legacyTestRuntimeClasspathFile = System.getenv("TEST_RUNTIME_CLASSPATH_FILE");
String directClasspathFile = System.getenv("TEST_DIRECT_CLASSPATH_FILE");
String transitiveClasspathFile = System.getenv("TEST_TRANSITIVE_CLASSPATH_FILE");
String absolutePathPrefix = System.getenv("JAVA_RUNFILES") + File.separator + workspacePrefix;
PersistentTestRunnerClassLoader transitiveDepsClassLoader = null;
PersistentTestRunnerClassLoader directDepsClassLoader = null;
// Reading the work requests and solving them in sequence is not a problem because Bazel creates
// up to --worker_max_instances (defaults to 4) instances per worker key.
while (true) {
try {
WorkRequest request = WorkRequest.parseDelimitedFrom(System.in);
if (request == null) {
// null is only returned when the stream reaches EOF
break;
}
if (legacyTestRuntimeClasspathFile != null) {
// Re-use the same classloader variable in the legacy case for simplicity.
directDepsClassLoader =
maybeRecreateClassLoader(
getFilesWithAbsolutePathPrefixFromFile(
legacyTestRuntimeClasspathFile, absolutePathPrefix),
ClassLoader.getSystemClassLoader(),
null);
} else {
transitiveDepsClassLoader =
maybeRecreateClassLoader(
getFilesWithAbsolutePathPrefixFromFile(
transitiveClasspathFile, absolutePathPrefix),
ClassLoader.getSystemClassLoader(),
transitiveDepsClassLoader);
directDepsClassLoader =
maybeRecreateClassLoader(
getFilesWithAbsolutePathPrefixFromFile(directClasspathFile, absolutePathPrefix),
transitiveDepsClassLoader,
directDepsClassLoader);
transitiveDepsClassLoader.setChild(directDepsClassLoader);
}
Thread.currentThread().setContextClassLoader(directDepsClassLoader);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PrintStream printStream = new PrintStream(outputStream, true);
System.setOut(printStream);
System.setErr(printStream);
String[] arguments = request.getArgumentsList().toArray(new String[0]);
int exitCode = -1;
try {
exitCode =
suiteTestRunner.runTestsInSuite(
suitClassName, arguments, directDepsClassLoader, /* resolve= */ true);
} finally {
System.setOut(originalStdOut);
System.setErr(originalStdErr);
}
WorkResponse response =
WorkResponse.newBuilder()
.setOutput(outputStream.toString())
.setExitCode(exitCode)
.build();
response.writeDelimitedTo(System.out);
System.out.flush();
} catch (IOException e) {
e.printStackTrace();
return 1;
}
}
return 0;
}
/**
* Returns a {@link Class} with the given name. Loads the class for the given classloader, or from
* the system classloader if none is specified.
*/
static Class<?> getTestClass(String name, ClassLoader classLoader, boolean resolve) {
if (name == null) {
return null;
}
try {
if (classLoader == null) {
return Class.forName(name);
}
return Class.forName(name, resolve, classLoader);
} catch (ClassNotFoundException e) {
return null;
}
}
/**
* Returns a classloader that loads the given jars, only if the combined hash of their content is
* different than the one for the given previous classloader.
*
* <p>Returns previousClassLoader if the hashes are the same.
*
* <p>Needs to be called before every test run to avoid having stale classes. A class already
* loaded in a classloader can not be unloaded. To overcome this a new classloader has to be
* created at every test run.
*/
private static PersistentTestRunnerClassLoader maybeRecreateClassLoader(
List<File> runtimeJars,
ClassLoader parent,
@Nullable PersistentTestRunnerClassLoader previousClassLoader)
throws IOException {
HashCode combinedHash = getCombinedHashForFiles(runtimeJars);
if (previousClassLoader != null && combinedHash.equals(previousClassLoader.getChecksum())) {
return previousClassLoader;
}
return new PersistentTestRunnerClassLoader(
convertFileListToURLArray(runtimeJars), parent, combinedHash);
}
private static HashCode getCombinedHashForFiles(List<File> files) throws IOException {
Hasher hasher = Hashing.sha256().newHasher();
for (File file : files) {
hasher.putBytes(getFileHash(file).asBytes());
}
return hasher.hash();
}
private static HashCode getFileHash(File file) throws IOException {
InputStream inputStream = new FileInputStream(file);
HashingInputStream hashingStream = new HashingInputStream(Hashing.sha256(), inputStream);
ByteStreams.copy(hashingStream, ByteStreams.nullOutputStream());
return hashingStream.hash();
}
private static URL[] convertFileListToURLArray(List<File> jars) throws IOException {
URL[] urls = new URL[jars.size()];
for (int i = 0; i < jars.size(); i++) {
urls[i] = jars.get(i).toURI().toURL();
}
return urls;
}
private static List<File> getFilesWithAbsolutePathPrefixFromFile(
String runtimeClasspathFilename, String absolutePathPrefix) throws IOException {
return Files.readLines(new File(runtimeClasspathFilename), UTF_8).stream()
.map(entry -> new File(absolutePathPrefix + entry))
.collect(Collectors.toList());
}
static boolean isPersistentTestRunner() {
return Boolean.parseBoolean(System.getenv("PERSISTENT_TEST_RUNNER"));
}
}