|  | // Copyright 2021 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.skyframe; | 
|  |  | 
|  | import static com.google.common.truth.Truth.assertThat; | 
|  | import static org.mockito.ArgumentMatchers.any; | 
|  | import static org.mockito.ArgumentMatchers.anyBoolean; | 
|  | import static org.mockito.Mockito.mock; | 
|  | import static org.mockito.Mockito.when; | 
|  |  | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | import com.google.devtools.build.lib.actions.Artifact; | 
|  | import com.google.devtools.build.lib.actions.BuildFailedException; | 
|  | import com.google.devtools.build.lib.analysis.FileProvider; | 
|  | import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; | 
|  | import com.google.devtools.build.lib.events.EventKind; | 
|  | import com.google.devtools.build.lib.events.util.EventCollectionApparatus; | 
|  | import com.google.devtools.build.lib.runtime.BlazeModule; | 
|  | import com.google.devtools.build.lib.runtime.BlazeRuntime; | 
|  | import com.google.devtools.build.lib.testutil.MoreAsserts; | 
|  | import com.google.devtools.build.lib.util.AbruptExitException; | 
|  | import com.google.devtools.build.lib.vfs.ModifiedFileSet; | 
|  | import com.google.devtools.build.lib.vfs.OutputService; | 
|  | import com.google.devtools.build.lib.vfs.OutputService.ActionFileSystemType; | 
|  | import com.google.testing.junit.testparameterinjector.TestParameter; | 
|  | import com.google.testing.junit.testparameterinjector.TestParameterInjector; | 
|  | import com.google.testing.junit.testparameterinjector.TestParameters; | 
|  | import java.io.IOException; | 
|  | import org.junit.Before; | 
|  | import org.junit.Test; | 
|  | import org.junit.runner.RunWith; | 
|  |  | 
|  | /** | 
|  | * Integration test for action invalidation based on output modifications returned by {@link | 
|  | * OutputService#startBuild}. | 
|  | */ | 
|  | @RunWith(TestParameterInjector.class) | 
|  | public final class OutputsInvalidationIntegrationTest extends BuildIntegrationTestCase { | 
|  |  | 
|  | private final OutputService outputService = mock(OutputService.class); | 
|  |  | 
|  | @Before | 
|  | public void prepareOutputServiceMock() | 
|  | throws BuildFailedException, AbruptExitException, InterruptedException, IOException { | 
|  | when(outputService.actionFileSystemType()).thenReturn(ActionFileSystemType.DISABLED); | 
|  | when(outputService.startBuild(any(), any(), anyBoolean())) | 
|  | .thenReturn(ModifiedFileSet.EVERYTHING_MODIFIED); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception { | 
|  | return super.getRuntimeBuilder() | 
|  | .addBlazeModule( | 
|  | new BlazeModule() { | 
|  | @Override | 
|  | public OutputService getOutputService() { | 
|  | return outputService; | 
|  | } | 
|  | }); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | protected EventCollectionApparatus createEvents() { | 
|  | return new EventCollectionApparatus(ImmutableSet.of(EventKind.FINISH)); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void nothingModified_doesntInvalidateAnyActions(@TestParameter boolean deleteOutput) | 
|  | throws Exception { | 
|  | write("foo/BUILD", "genrule(name='foo', outs=['foo.out'], cmd='touch $@')"); | 
|  | buildTarget("//foo"); | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | if (deleteOutput) { | 
|  | delete(getOnlyOutput("//foo")); | 
|  | } | 
|  |  | 
|  | when(outputService.startBuild(any(), any(), anyBoolean())) | 
|  | .thenReturn(ModifiedFileSet.NOTHING_MODIFIED); | 
|  | events.collector().clear(); | 
|  | buildTarget("//foo"); | 
|  |  | 
|  | MoreAsserts.assertDoesNotContainEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | } | 
|  |  | 
|  | private enum ReportedModification { | 
|  | EVERYTHING_MODIFIED { | 
|  | @Override | 
|  | ModifiedFileSet modifiedFileSet(Artifact artifact) { | 
|  | return ModifiedFileSet.EVERYTHING_MODIFIED; | 
|  | } | 
|  | }, | 
|  | EVERYTHING_DELETED { | 
|  | @Override | 
|  | ModifiedFileSet modifiedFileSet(Artifact artifact) { | 
|  | return ModifiedFileSet.EVERYTHING_DELETED; | 
|  | } | 
|  | }, | 
|  | SINGLE_FILE { | 
|  | @Override | 
|  | ModifiedFileSet modifiedFileSet(Artifact artifact) { | 
|  | return ModifiedFileSet.builder().modify(artifact.getExecPath()).build(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | abstract ModifiedFileSet modifiedFileSet(Artifact artifact); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void identicalOutputs_doesntInvalidateAnyActions( | 
|  | @TestParameter ReportedModification modification) throws Exception { | 
|  | write("foo/BUILD", "genrule(name='foo', outs=['foo.out'], cmd='touch $@')"); | 
|  | buildTarget("//foo"); | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  |  | 
|  | when(outputService.startBuild(any(), any(), anyBoolean())) | 
|  | .thenReturn(modification.modifiedFileSet(getOnlyOutput("//foo"))); | 
|  | events.collector().clear(); | 
|  | buildTarget("//foo"); | 
|  |  | 
|  | MoreAsserts.assertDoesNotContainEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void noCheckOutputFiles_ignoresModifiedFiles( | 
|  | @TestParameter ReportedModification modification) throws Exception { | 
|  | addOptions("--experimental_check_output_files"); | 
|  | write("foo/BUILD", "genrule(name='foo', outs=['foo.out'], cmd='touch $@')"); | 
|  | buildTarget("//foo"); | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  |  | 
|  | when(outputService.startBuild(any(), any(), anyBoolean())) | 
|  | .thenReturn(modification.modifiedFileSet(getOnlyOutput("//foo"))); | 
|  | events.collector().clear(); | 
|  | buildTarget("//foo"); | 
|  |  | 
|  | MoreAsserts.assertDoesNotContainEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | } | 
|  |  | 
|  | @TestParameters({ | 
|  | "{everythingDeleted: false, checkOutputFiles: true}", | 
|  | "{everythingDeleted: true, checkOutputFiles: false}", | 
|  | "{everythingDeleted: true, checkOutputFiles: true}", | 
|  | }) | 
|  | @Test | 
|  | public void everythingModified_invalidatesAllActions( | 
|  | boolean everythingDeleted, boolean checkOutputFiles) throws Exception { | 
|  | addOptions("--experimental_check_output_files=" + checkOutputFiles); | 
|  | write("foo/BUILD", "genrule(name='foo', outs=['foo.out'], cmd='touch $@')"); | 
|  | buildTarget("//foo"); | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | delete(getOnlyOutput("//foo")); | 
|  |  | 
|  | when(outputService.startBuild(any(), any(), anyBoolean())) | 
|  | .thenReturn( | 
|  | everythingDeleted | 
|  | ? ModifiedFileSet.EVERYTHING_DELETED | 
|  | : ModifiedFileSet.EVERYTHING_MODIFIED); | 
|  | events.collector().clear(); | 
|  | buildTarget("//foo"); | 
|  |  | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | } | 
|  |  | 
|  | @Test | 
|  | public void outputFileModified_invalidatesOnlyAffectedAction() throws Exception { | 
|  | write( | 
|  | "foo/BUILD", | 
|  | "genrule(name='foo', outs=['foo.out'], cmd='touch $@')", | 
|  | "genrule(name='bar', outs=['bar.out'], cmd='touch $@')"); | 
|  | buildTarget("//foo:all"); | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:bar"); | 
|  | Artifact fooOut = getOnlyOutput("//foo"); | 
|  | delete(fooOut); | 
|  |  | 
|  | when(outputService.startBuild(any(), any(), anyBoolean())).thenReturn(modifiedFileSet(fooOut)); | 
|  | events.collector().clear(); | 
|  | buildTarget("//foo:all"); | 
|  |  | 
|  | MoreAsserts.assertContainsEvent(events.collector(), "Executing genrule //foo:foo"); | 
|  | MoreAsserts.assertDoesNotContainEvent(events.collector(), "Executing genrule //foo:bar"); | 
|  | } | 
|  |  | 
|  | private static void delete(Artifact artifact) throws IOException { | 
|  | assertThat(artifact.getPath().delete()).isTrue(); | 
|  | } | 
|  |  | 
|  | private Artifact getOnlyOutput(String label) throws Exception { | 
|  | return getConfiguredTarget(label) | 
|  | .getProvider(FileProvider.class) | 
|  | .getFilesToBuild() | 
|  | .getSingleton(); | 
|  | } | 
|  |  | 
|  | private ModifiedFileSet modifiedFileSet(Artifact... artifacts) { | 
|  | ModifiedFileSet.Builder modifiedFileSet = ModifiedFileSet.builder(); | 
|  | for (Artifact artifact : artifacts) { | 
|  | modifiedFileSet.modify(artifact.getExecPath()); | 
|  | } | 
|  | return modifiedFileSet.build(); | 
|  | } | 
|  | } |