blob: e06b6c4f1be533dc77633296f12314c9eb74c2fe [file] [log] [blame]
// Copyright 2016 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.devtools.build.lib.worker;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.common.hash.HashCode;
import com.google.devtools.build.lib.actions.ActionExecutionContext;
import com.google.devtools.build.lib.actions.ExecException;
import com.google.devtools.build.lib.actions.ExecutionStrategy;
import com.google.devtools.build.lib.actions.Spawn;
import com.google.devtools.build.lib.actions.TestExecException;
import com.google.devtools.build.lib.actions.UserExecException;
import com.google.devtools.build.lib.analysis.test.TestActionContext;
import com.google.devtools.build.lib.analysis.test.TestRunnerAction;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.exec.ExecutionOptions;
import com.google.devtools.build.lib.exec.StandaloneTestResult;
import com.google.devtools.build.lib.exec.StandaloneTestStrategy;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
import com.google.devtools.build.lib.view.test.TestStatus.TestCase;
import com.google.devtools.build.lib.view.test.TestStatus.TestResultData;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkRequest;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkResponse;
import com.google.devtools.common.options.OptionsClassProvider;
import com.google.protobuf.InvalidProtocolBufferException;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
/**
* Runs TestRunnerAction actions in a worker. This is still experimental WIP.
* Do not use this strategy to run tests. <br>
*
* TODO(kush): List to things to cosider: <br>
* 1. Figure out if/how to honor the actions's execution info:
* action.getTestProperties().getExecutionInfo() <br>
* 2. Figure out how to stream intermediate output when running in a Worker or block streamed
* outputs for this strategy. <br>
* 3. Figure out how to add timeout facility. <br>
*/
@ExecutionStrategy(contextType = TestActionContext.class, name = { "experimental_worker" })
public class WorkerTestStrategy extends StandaloneTestStrategy {
private final WorkerPool workerPool;
private final Multimap<String, String> extraFlags;
public WorkerTestStrategy(
CommandEnvironment env,
OptionsClassProvider requestOptions,
WorkerPool workerPool,
Multimap<String, String> extraFlags) {
super(
requestOptions.getOptions(ExecutionOptions.class),
env.getBlazeWorkspace().getBinTools(),
env.getWorkspace());
this.workerPool = workerPool;
this.extraFlags = extraFlags;
}
@Override
protected StandaloneTestResult executeTest(
TestRunnerAction action, Spawn spawn, ActionExecutionContext actionExecutionContext)
throws ExecException, InterruptedException, IOException {
if (!action.getConfiguration().compatibleWithStrategy("experimental_worker")) {
throw new UserExecException(
"Build configuration not compatible with experimental_worker "
+ "strategy. Make sure you set the explicit_java_test_deps and "
+ "experimental_testrunner flags to true.");
}
if (!action.useTestRunner()) {
throw new UserExecException(
"Tests that do not use the experimental test runner are incompatible with the persistent"
+ " worker test strategy. Please use another test strategy");
}
if (action.isCoverageMode()) {
throw new UserExecException("Coverage is currently incompatible"
+ " with the persistent worker test strategy. Please use another test strategy");
}
List<String> startupArgs = getStartUpArgs(action);
return execInWorker(
action,
spawn,
actionExecutionContext,
addPersistentRunnerVars(spawn.getEnvironment()),
startupArgs,
actionExecutionContext.getExecRoot());
}
private StandaloneTestResult execInWorker(
TestRunnerAction action,
Spawn spawn,
ActionExecutionContext actionExecutionContext,
Map<String, String> environment,
List<String> startupArgs,
Path execRoot)
throws ExecException, InterruptedException, IOException {
// TODO(kush): Remove once we're out of the experimental phase.
actionExecutionContext
.getEventHandler()
.handle(
Event.warn(
"RUNNING TEST IN AN EXPERIMENTAL PERSISTENT WORKER. RESULTS MAY BE INACCURATE"));
TestResultData.Builder builder = TestResultData.newBuilder();
Path testLogPath = action.getTestLog().getPath();
Worker worker = null;
WorkerKey key = null;
long startTime = actionExecutionContext.getClock().currentTimeMillis();
try {
SortedMap<PathFragment, HashCode> workerFiles =
WorkerFilesHash.getWorkerFilesWithHashes(
spawn,
actionExecutionContext.getArtifactExpander(),
actionExecutionContext.getActionInputFileCache());
HashCode workerFilesCombinedHash = WorkerFilesHash.getCombinedHash(workerFiles);
key =
new WorkerKey(
startupArgs,
environment,
execRoot,
action.getMnemonic(),
workerFilesCombinedHash,
workerFiles,
ImmutableMap.<PathFragment, Path>of(),
ImmutableSet.<PathFragment>of(),
/*mustBeSandboxed=*/ false);
worker = workerPool.borrowObject(key);
WorkRequest request = WorkRequest.getDefaultInstance();
request.writeDelimitedTo(worker.getOutputStream());
worker.getOutputStream().flush();
RecordingInputStream recordingStream = new RecordingInputStream(worker.getInputStream());
recordingStream.startRecording(4096);
WorkResponse response;
try {
// response can be null when the worker has already closed stdout at this point and thus the
// InputStream is at EOF.
response = WorkResponse.parseDelimitedFrom(recordingStream);
} catch (InvalidProtocolBufferException e) {
// If protobuf couldn't parse the response, try to print whatever the failing worker wrote
// to stdout - it's probably a stack trace or some kind of error message that will help the
// user figure out why the compiler is failing.
recordingStream.readRemaining();
String data = recordingStream.getRecordedDataAsString();
ErrorMessage errorMessage =
ErrorMessage.builder()
.message("Worker process returned an unparseable WorkResponse:")
.exception(e)
.logText(data)
.build();
actionExecutionContext.getEventHandler().handle(Event.warn(errorMessage.toString()));
throw e;
}
worker.finishExecution(key);
if (response == null) {
throw new UserExecException(
ErrorMessage.builder()
.message(
"Worker process did not return a WorkResponse. This is usually caused by a bug"
+ " in the worker, thus dumping its log file for debugging purposes:")
.logFile(worker.getLogFile())
.logSizeLimit(4096)
.build()
.toString());
}
actionExecutionContext.getFileOutErr().getErrorStream().write(
response.getOutputBytes().toByteArray());
long duration = actionExecutionContext.getClock().currentTimeMillis() - startTime;
builder.addTestTimes(duration);
builder.setRunDurationMillis(duration);
if (response.getExitCode() == 0) {
builder
.setTestPassed(true)
.setStatus(BlazeTestStatus.PASSED)
.setPassedLog(testLogPath.getPathString());
} else {
builder
.setTestPassed(false)
.setStatus(BlazeTestStatus.FAILED)
.addFailedLogs(testLogPath.getPathString());
}
TestCase details = parseTestResult(
action.resolve(actionExecutionContext.getExecRoot()).getXmlOutputPath());
if (details != null) {
builder.setTestCase(details);
}
return StandaloneTestResult.create(ImmutableSet.of(), builder.build());
} catch (IOException | InterruptedException e) {
if (worker != null) {
workerPool.invalidateObject(key, worker);
worker = null;
}
throw new TestExecException(e.getMessage());
} finally {
if (worker != null) {
workerPool.returnObject(key, worker);
}
}
}
private static Map<String, String> addPersistentRunnerVars(Map<String, String> originalEnv)
throws UserExecException {
if (originalEnv.containsKey("PERSISTENT_TEST_RUNNER")) {
throw new UserExecException(
"Found clashing environment variable with persistent_test_runner."
+ " Please use another test strategy");
}
return ImmutableMap.<String, String>builder()
.putAll(originalEnv)
.put("PERSISTENT_TEST_RUNNER", "true")
.build();
}
private List<String> getStartUpArgs(TestRunnerAction action) throws ExecException {
List<String> args = getArgs(/*coverageScript=*/ "coverage-is-not-supported", action);
ImmutableList.Builder<String> startupArgs = ImmutableList.builder();
// Add test setup with no echo to prevent stdout corruption.
startupArgs.add(args.get(0)).add("--no_echo");
// Add remaining of the original args.
startupArgs.addAll(args.subList(1, args.size()));
// Add additional flags requested for this invocation.
startupArgs.addAll(MoreObjects.firstNonNull(
extraFlags.get(action.getMnemonic()), ImmutableList.<String>of()));
return startupArgs.build();
}
}