| // Copyright 2017 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.exec; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static org.junit.Assert.assertThrows; | |
| import static org.mockito.ArgumentMatchers.any; | |
| import static org.mockito.ArgumentMatchers.eq; | |
| import static org.mockito.Mockito.mock; | |
| import static org.mockito.Mockito.never; | |
| import static org.mockito.Mockito.verify; | |
| import static org.mockito.Mockito.when; | |
| import com.google.devtools.build.lib.actions.ActionExecutionContext; | |
| import com.google.devtools.build.lib.actions.Artifact; | |
| import com.google.devtools.build.lib.actions.ArtifactRoot; | |
| import com.google.devtools.build.lib.actions.FutureSpawn; | |
| import com.google.devtools.build.lib.actions.MetadataProvider; | |
| import com.google.devtools.build.lib.actions.Spawn; | |
| import com.google.devtools.build.lib.actions.SpawnResult; | |
| import com.google.devtools.build.lib.actions.SpawnResult.Status; | |
| import com.google.devtools.build.lib.actions.util.ActionsTestUtil; | |
| import com.google.devtools.build.lib.analysis.platform.PlatformInfo; | |
| import com.google.devtools.build.lib.events.StoredEventHandler; | |
| import com.google.devtools.build.lib.exec.Protos.Digest; | |
| import com.google.devtools.build.lib.exec.Protos.EnvironmentVariable; | |
| import com.google.devtools.build.lib.exec.Protos.File; | |
| import com.google.devtools.build.lib.exec.Protos.Platform; | |
| import com.google.devtools.build.lib.exec.Protos.SpawnExec; | |
| import com.google.devtools.build.lib.exec.SpawnCache.CacheHandle; | |
| import com.google.devtools.build.lib.exec.SpawnRunner.SpawnExecutionContext; | |
| import com.google.devtools.build.lib.exec.util.SpawnBuilder; | |
| import com.google.devtools.build.lib.remote.options.RemoteOptions; | |
| import com.google.devtools.build.lib.testutil.Scratch; | |
| import com.google.devtools.build.lib.testutil.Suite; | |
| import com.google.devtools.build.lib.testutil.TestSpec; | |
| import com.google.devtools.build.lib.util.io.MessageOutputStream; | |
| import com.google.devtools.build.lib.vfs.DigestHashFunction; | |
| import com.google.devtools.build.lib.vfs.FileSystem; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.Root; | |
| import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; | |
| import com.google.devtools.common.options.Options; | |
| import java.util.List; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| import org.mockito.Mock; | |
| import org.mockito.MockitoAnnotations; | |
| /** Tests for {@link BlazeExecutor}. */ | |
| @RunWith(JUnit4.class) | |
| @TestSpec(size = Suite.SMALL_TESTS) | |
| public class AbstractSpawnStrategyTest { | |
| private static class TestedSpawnStrategy extends AbstractSpawnStrategy { | |
| public TestedSpawnStrategy(Path execRoot, SpawnRunner spawnRunner) { | |
| super(execRoot, spawnRunner); | |
| } | |
| } | |
| private static final Spawn SIMPLE_SPAWN = | |
| new SpawnBuilder("/bin/echo", "Hi!").withEnvironment("VARIABLE", "value").build(); | |
| private final FileSystem fs = new InMemoryFileSystem(DigestHashFunction.SHA256); | |
| private final Path execRoot = fs.getPath("/execroot"); | |
| private Scratch scratch; | |
| private ArtifactRoot rootDir; | |
| @Mock private SpawnRunner spawnRunner; | |
| @Mock private ActionExecutionContext actionExecutionContext; | |
| @Mock private MessageOutputStream messageOutput; | |
| @Before | |
| public final void setUp() throws Exception { | |
| MockitoAnnotations.initMocks(this); | |
| scratch = new Scratch(fs); | |
| rootDir = ArtifactRoot.asSourceRoot(Root.fromPath(scratch.dir("/execroot"))); | |
| StoredEventHandler eventHandler = new StoredEventHandler(); | |
| when(actionExecutionContext.getEventHandler()).thenReturn(eventHandler); | |
| } | |
| @Test | |
| public void testZeroExit() throws Exception { | |
| when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(SpawnCache.NO_CACHE); | |
| when(actionExecutionContext.getExecRoot()).thenReturn(execRoot); | |
| SpawnResult spawnResult = | |
| new SpawnResult.Builder().setStatus(Status.SUCCESS).setRunnerName("test").build(); | |
| when(spawnRunner.execAsync(any(Spawn.class), any(SpawnExecutionContext.class))) | |
| .thenReturn(FutureSpawn.immediate(spawnResult)); | |
| List<SpawnResult> spawnResults = | |
| new TestedSpawnStrategy(execRoot, spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); | |
| assertThat(spawnResults).containsExactly(spawnResult); | |
| // Must only be called exactly once. | |
| verify(spawnRunner).execAsync(any(Spawn.class), any(SpawnExecutionContext.class)); | |
| } | |
| @Test | |
| public void testNonZeroExit() throws Exception { | |
| when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(SpawnCache.NO_CACHE); | |
| when(actionExecutionContext.getExecRoot()).thenReturn(execRoot); | |
| SpawnResult result = | |
| new SpawnResult.Builder() | |
| .setStatus(Status.NON_ZERO_EXIT) | |
| .setExitCode(1) | |
| .setRunnerName("test") | |
| .build(); | |
| when(spawnRunner.execAsync(any(Spawn.class), any(SpawnExecutionContext.class))) | |
| .thenReturn(FutureSpawn.immediate(result)); | |
| SpawnExecException e = | |
| assertThrows( | |
| SpawnExecException.class, | |
| () -> | |
| // Ignoring the List<SpawnResult> return value. | |
| new TestedSpawnStrategy(execRoot, spawnRunner) | |
| .exec(SIMPLE_SPAWN, actionExecutionContext)); | |
| assertThat(e.getSpawnResult()).isSameInstanceAs(result); | |
| // Must only be called exactly once. | |
| verify(spawnRunner).execAsync(any(Spawn.class), any(SpawnExecutionContext.class)); | |
| } | |
| @Test | |
| public void testCacheHit() throws Exception { | |
| SpawnCache cache = mock(SpawnCache.class); | |
| SpawnResult spawnResult = | |
| new SpawnResult.Builder().setStatus(Status.SUCCESS).setRunnerName("test").build(); | |
| when(cache.lookup(any(Spawn.class), any(SpawnExecutionContext.class))) | |
| .thenReturn(SpawnCache.success(spawnResult)); | |
| when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(cache); | |
| when(actionExecutionContext.getExecRoot()).thenReturn(execRoot); | |
| List<SpawnResult> spawnResults = | |
| new TestedSpawnStrategy(execRoot, spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); | |
| assertThat(spawnResults).containsExactly(spawnResult); | |
| verify(spawnRunner, never()).execAsync(any(Spawn.class), any(SpawnExecutionContext.class)); | |
| } | |
| @Test | |
| public void testCacheMiss() throws Exception { | |
| SpawnCache cache = mock(SpawnCache.class); | |
| CacheHandle entry = mock(CacheHandle.class); | |
| when(cache.lookup(any(Spawn.class), any(SpawnExecutionContext.class))).thenReturn(entry); | |
| when(entry.hasResult()).thenReturn(false); | |
| when(entry.willStore()).thenReturn(true); | |
| when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(cache); | |
| when(actionExecutionContext.getExecRoot()).thenReturn(execRoot); | |
| SpawnResult spawnResult = | |
| new SpawnResult.Builder().setStatus(Status.SUCCESS).setRunnerName("test").build(); | |
| when(spawnRunner.execAsync(any(Spawn.class), any(SpawnExecutionContext.class))) | |
| .thenReturn(FutureSpawn.immediate(spawnResult)); | |
| List<SpawnResult> spawnResults = | |
| new TestedSpawnStrategy(execRoot, spawnRunner).exec(SIMPLE_SPAWN, actionExecutionContext); | |
| assertThat(spawnResults).containsExactly(spawnResult); | |
| // Must only be called exactly once. | |
| verify(spawnRunner).execAsync(any(Spawn.class), any(SpawnExecutionContext.class)); | |
| verify(entry).store(eq(spawnResult)); | |
| } | |
| @Test | |
| public void testCacheMissWithNonZeroExit() throws Exception { | |
| SpawnCache cache = mock(SpawnCache.class); | |
| CacheHandle entry = mock(CacheHandle.class); | |
| when(cache.lookup(any(Spawn.class), any(SpawnExecutionContext.class))).thenReturn(entry); | |
| when(entry.hasResult()).thenReturn(false); | |
| when(entry.willStore()).thenReturn(true); | |
| when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(cache); | |
| when(actionExecutionContext.getExecRoot()).thenReturn(execRoot); | |
| SpawnResult result = | |
| new SpawnResult.Builder() | |
| .setStatus(Status.NON_ZERO_EXIT) | |
| .setExitCode(1) | |
| .setRunnerName("test") | |
| .build(); | |
| when(spawnRunner.execAsync(any(Spawn.class), any(SpawnExecutionContext.class))) | |
| .thenReturn(FutureSpawn.immediate(result)); | |
| SpawnExecException e = | |
| assertThrows( | |
| SpawnExecException.class, | |
| () -> | |
| // Ignoring the List<SpawnResult> return value. | |
| new TestedSpawnStrategy(execRoot, spawnRunner) | |
| .exec(SIMPLE_SPAWN, actionExecutionContext)); | |
| assertThat(e.getSpawnResult()).isSameInstanceAs(result); | |
| // Must only be called exactly once. | |
| verify(spawnRunner).execAsync(any(Spawn.class), any(SpawnExecutionContext.class)); | |
| verify(entry).store(eq(result)); | |
| } | |
| @Test | |
| public void testLogSpawn() throws Exception { | |
| setUpExecutionContext(/* remoteOptions= */ null); | |
| Artifact input = ActionsTestUtil.createArtifact(rootDir, scratch.file("/execroot/foo", "1")); | |
| scratch.file("/execroot/out1", "123"); | |
| scratch.file("/execroot/out2", "123"); | |
| Spawn spawn = | |
| new SpawnBuilder("/bin/echo", "Foo!") | |
| .withEnvironment("FOO", "v1") | |
| .withEnvironment("BAR", "v2") | |
| .withMnemonic("MyMnemonic") | |
| .withProgressMessage("my progress message") | |
| .withInput(input) | |
| .withOutputs("out2", "out1") | |
| .build(); | |
| assertThrows( | |
| SpawnExecException.class, | |
| () -> new TestedSpawnStrategy(execRoot, spawnRunner).exec(spawn, actionExecutionContext)); | |
| SpawnExec expectedSpawnLog = | |
| SpawnExec.newBuilder() | |
| .addCommandArgs("/bin/echo") | |
| .addCommandArgs("Foo!") | |
| .addEnvironmentVariables( | |
| EnvironmentVariable.newBuilder().setName("BAR").setValue("v2").build()) | |
| .addEnvironmentVariables( | |
| EnvironmentVariable.newBuilder().setName("FOO").setValue("v1").build()) | |
| .addInputs( | |
| File.newBuilder() | |
| .setPath("foo") | |
| .setDigest( | |
| Digest.newBuilder() | |
| .setHash( | |
| "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865") | |
| .setSizeBytes(2) | |
| .setHashFunctionName("SHA-256") | |
| .build()) | |
| .build()) | |
| .addListedOutputs("out1") | |
| .addListedOutputs("out2") | |
| .addActualOutputs( | |
| File.newBuilder() | |
| .setPath("out1") | |
| .setDigest( | |
| Digest.newBuilder() | |
| .setHash( | |
| "181210f8f9c779c26da1d9b2075bde0127302ee0e3fca38c9a83f5b1dd8e5d3b") | |
| .setSizeBytes(4) | |
| .setHashFunctionName("SHA-256") | |
| .build()) | |
| .build()) | |
| .addActualOutputs( | |
| File.newBuilder() | |
| .setPath("out2") | |
| .setDigest( | |
| Digest.newBuilder() | |
| .setHash( | |
| "181210f8f9c779c26da1d9b2075bde0127302ee0e3fca38c9a83f5b1dd8e5d3b") | |
| .setSizeBytes(4) | |
| .setHashFunctionName("SHA-256") | |
| .build()) | |
| .build()) | |
| .setStatus("NON_ZERO_EXIT") | |
| .setExitCode(23) | |
| .setRemotable(true) | |
| .setCacheable(true) | |
| .setProgressMessage("my progress message") | |
| .setMnemonic("MyMnemonic") | |
| .setRunner("runner") | |
| .build(); | |
| verify(messageOutput).write(expectedSpawnLog); | |
| } | |
| @Test | |
| public void testLogSpawn_noPlatform_noLoggedPlatform() throws Exception { | |
| setUpExecutionContext(/* remoteOptions= */ null); | |
| Spawn spawn = new SpawnBuilder("cmd").build(); | |
| assertThrows( | |
| SpawnExecException.class, | |
| () -> new TestedSpawnStrategy(execRoot, spawnRunner).exec(spawn, actionExecutionContext)); | |
| SpawnExec expected = defaultSpawnExecBuilder("cmd").build(); | |
| verify(messageOutput).write(expected); | |
| } | |
| @Test | |
| public void testLogSpawn_DefaultPlatform_getsLogged() throws Exception { | |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); | |
| remoteOptions.remoteDefaultPlatformProperties = | |
| String.join( | |
| "\n", | |
| "properties: {", | |
| " name: \"b\"", | |
| " value: \"2\"", | |
| "}", | |
| "properties: {", | |
| " name: \"a\"", | |
| " value: \"1\"", | |
| "}"); | |
| setUpExecutionContext(remoteOptions); | |
| Spawn spawn = new SpawnBuilder("cmd").build(); | |
| assertThrows( | |
| SpawnExecException.class, | |
| () -> new TestedSpawnStrategy(execRoot, spawnRunner).exec(spawn, actionExecutionContext)); | |
| Platform platform = | |
| Platform.newBuilder() | |
| .addProperties(Platform.Property.newBuilder().setName("a").setValue("1")) | |
| .addProperties(Platform.Property.newBuilder().setName("b").setValue("2")) | |
| .build(); | |
| SpawnExec expected = defaultSpawnExecBuilder("cmd").setPlatform(platform).build(); | |
| verify(messageOutput).write(expected); // output will reflect default properties | |
| } | |
| @Test | |
| public void testLogSpawn_SpecifiedPlatform_overridesDefault() throws Exception { | |
| RemoteOptions remoteOptions = Options.getDefaults(RemoteOptions.class); | |
| remoteOptions.remoteDefaultPlatformProperties = | |
| String.join( | |
| "\n", | |
| "properties: {", | |
| " name: \"b\"", | |
| " value: \"2\"", | |
| "}", | |
| "properties: {", | |
| " name: \"a\"", | |
| " value: \"1\"", | |
| "}"); | |
| setUpExecutionContext(remoteOptions); | |
| PlatformInfo platformInfo = | |
| PlatformInfo.builder() | |
| .setRemoteExecutionProperties( | |
| String.join( | |
| "\n", | |
| "properties: {", | |
| " name: \"new\"", | |
| " value: \"val\"", | |
| "}", | |
| "properties: {", | |
| " name: \"new2\"", | |
| " value: \"val2\"", | |
| "}")) | |
| .build(); | |
| Spawn spawn = new SpawnBuilder("cmd").withPlatform(platformInfo).build(); | |
| assertThrows( | |
| SpawnExecException.class, | |
| () -> new TestedSpawnStrategy(execRoot, spawnRunner).exec(spawn, actionExecutionContext)); | |
| // If the platform is specified on the spawn, it is used even if a default if specified in the | |
| // options. | |
| Platform platform = | |
| Platform.newBuilder() | |
| .addProperties(Platform.Property.newBuilder().setName("new").setValue("val")) | |
| .addProperties(Platform.Property.newBuilder().setName("new2").setValue("val2")) | |
| .build(); | |
| SpawnExec expected = defaultSpawnExecBuilder("cmd").setPlatform(platform).build(); | |
| verify(messageOutput).write(expected); // output will reflect default properties | |
| } | |
| private void setUpExecutionContext(RemoteOptions remoteOptions) throws Exception { | |
| when(actionExecutionContext.getContext(eq(SpawnCache.class))).thenReturn(SpawnCache.NO_CACHE); | |
| when(actionExecutionContext.getExecRoot()).thenReturn(execRoot); | |
| when(actionExecutionContext.getContext(eq(SpawnLogContext.class))) | |
| .thenReturn(new SpawnLogContext(execRoot, messageOutput, remoteOptions)); | |
| when(spawnRunner.execAsync(any(Spawn.class), any(SpawnExecutionContext.class))) | |
| .thenReturn( | |
| FutureSpawn.immediate( | |
| new SpawnResult.Builder() | |
| .setStatus(Status.NON_ZERO_EXIT) | |
| .setExitCode(23) | |
| .setRunnerName("runner") | |
| .build())); | |
| when(actionExecutionContext.getMetadataProvider()).thenReturn(mock(MetadataProvider.class)); | |
| } | |
| /** Returns a SpawnExec pre-populated with values for a default spawn */ | |
| private static SpawnExec.Builder defaultSpawnExecBuilder(String cmd) { | |
| return SpawnExec.newBuilder() | |
| .addCommandArgs(cmd) | |
| .setRemotable(true) | |
| .setCacheable(true) | |
| .setProgressMessage("progress message") | |
| .setMnemonic("Mnemonic") | |
| .setRunner("runner") | |
| .setStatus("NON_ZERO_EXIT") | |
| .setExitCode(23); | |
| } | |
| } |