| // Copyright 2024 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.buildtool; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static org.junit.Assert.assertThrows; |
| |
| import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase; |
| import com.google.devtools.build.lib.cmdline.TargetParsingException; |
| import com.google.devtools.build.lib.skyframe.SkyfocusState.Request; |
| import com.google.devtools.build.lib.util.AbruptExitException; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** Skyfocus integration tests */ |
| @RunWith(JUnit4.class) |
| public final class SkyfocusIntegrationTest extends BuildIntegrationTestCase { |
| |
| @Override |
| protected void setupOptions() throws Exception { |
| super.setupOptions(); |
| addOptions("--experimental_enable_skyfocus"); |
| } |
| |
| @Test |
| public void workingSet_canBeUsedWithBuildCommand() throws Exception { |
| addOptions("--experimental_working_set=hello/x.txt", "--experimental_skyfocus_dump_keys=count"); |
| write("hello/x.txt", "x"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt"], |
| outs = ["out"], |
| cmd = "cat $< > $@", |
| ) |
| """); |
| |
| buildTarget("//hello/..."); |
| assertContainsEvent("Updated working set successfully."); |
| assertContainsEvent("Focusing on"); |
| assertContainsEvent("Node count:"); |
| |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(1); |
| } |
| |
| @Test |
| public void workingSet_withFiles_correctlyRebuilds() throws Exception { |
| addOptions("--experimental_working_set=hello/x.txt"); |
| write("hello/x.txt", "x"); |
| write("hello/y.txt", "y"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| buildTarget("//hello/..."); |
| assertContents("x\ny", "//hello:target"); |
| |
| write("hello/x.txt", "x2"); |
| buildTarget("//hello/..."); |
| assertContents("x2\ny", "//hello:target"); |
| } |
| |
| @Test |
| public void workingSet_withDirs_correctlyRebuilds() throws Exception { |
| /* |
| * Setting directories in the working set works, because the rdep edges look like: |
| * |
| * FILE_STATE:[dir] -> FILE:[dir] -> FILE:[dir/BUILD], FILE:[dir/file.txt] |
| * |
| * ...and the FILE SkyKeys directly depend on their respective FILE_STATE SkyKeys, |
| * which are the nodes that are invalidated by SkyframeExecutor#handleDiffs |
| * at the start of every build, and are also kept by Skyfocus. |
| * |
| * In other words, defining a working set of directories will automatically |
| * include all the files under those directories for focusing. |
| */ |
| |
| // Define working set to be a directory, not file |
| addOptions("--experimental_working_set=hello"); |
| write("hello/x.txt", "x"); |
| write("hello/y.txt", "y"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| buildTarget("//hello/..."); |
| assertContents("x\ny", "//hello:target"); |
| |
| write("hello/x.txt", "x2"); |
| buildTarget("//hello/..."); |
| // Correctly rebuilds referenced source file |
| assertContents("x2\ny", "//hello:target"); |
| |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "y_only", |
| srcs = ["y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| buildTarget("//hello/..."); |
| // Correctly reanalyzes BUILD file |
| assertContents("y", "//hello:y_only"); |
| } |
| |
| @Test |
| public void workingSet_nestedDirs_correctlyRebuilds() throws Exception { |
| // Define working set to be the parent package |
| addOptions("--experimental_working_set=hello"); |
| write("hello/x.txt", "x"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "//hello/world:target"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| write("hello/world/y.txt", "y"); |
| write( |
| "hello/world/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| buildTarget("//hello/..."); |
| assertContents("x\ny", "//hello:target"); |
| |
| write("hello/x.txt", "x2"); |
| buildTarget("//hello/..."); |
| // Rebuilds when parent package's source file changes |
| assertContents("x2\ny", "//hello:target"); |
| |
| write("hello/world/y.txt", "y2"); |
| buildTarget("//hello/..."); |
| // Rebuilds when child package's source file changes |
| assertContents("x2\ny2", "//hello:target"); |
| } |
| |
| @Test |
| public void newNonFocusedTargets_canBeBuilt() throws Exception { |
| addOptions("--experimental_working_set=hello/x.txt"); |
| write("hello/x.txt", "x"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "g", |
| srcs = ["x.txt"], |
| outs = ["g.txt"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| genrule( |
| name = "g2", |
| srcs = ["x.txt"], |
| outs = ["g2.txt"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| genrule( |
| name = "g3", |
| outs = ["g3.txt"], |
| cmd = "touch $@", |
| ) |
| """); |
| buildTarget("//hello:g"); |
| write("hello/x.txt", "x2"); |
| buildTarget("//hello:g"); |
| buildTarget("//hello:g2"); |
| buildTarget("//hello:g3"); |
| } |
| |
| @Test |
| public void skyfocus_doesNotRun_forUnsuccessfulBuilds() throws Exception { |
| addOptions("--experimental_working_set=hello/x.txt"); |
| write("hello/x.txt", "x"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| # error |
| ) |
| """); |
| TargetParsingException e = |
| assertThrows(TargetParsingException.class, () -> buildTarget("//hello/...")); |
| |
| assertThat(e).hasMessageThat().contains("Package 'hello' contains errors"); |
| |
| assertThat(getSkyframeExecutor().getSkyfocusState().enabled()).isTrue(); |
| assertThat(getSkyframeExecutor().getSkyfocusState().request()).isNotEqualTo(Request.DO_NOTHING); |
| assertThat(getSkyframeExecutor().getSkyfocusState().verificationSet()).isEmpty(); |
| } |
| |
| @Test |
| public void editingNonWorkingSet_inSameDir_failsTheBuild() throws Exception { |
| write("hello/x.txt", "x"); |
| write("hello/y.txt", "y"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| addOptions("--experimental_working_set=hello/x.txt"); |
| buildTarget("//hello/..."); |
| |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(1); |
| |
| write("hello/y.txt", "y2"); |
| AbruptExitException e = |
| assertThrows(AbruptExitException.class, () -> buildTarget("//hello/...")); |
| assertThat(e).hasMessageThat().contains("detected changes outside of the working set"); |
| assertThat(e).hasMessageThat().contains("hello/y.txt"); |
| |
| addOptions("--experimental_working_set=hello/x.txt,hello/y.txt"); |
| buildTarget("//hello/..."); |
| assertContents("x\ny2", "//hello:target"); |
| } |
| |
| @Test |
| public void editingNonWorkingSet_throughDep_failsTheBuild() throws Exception { |
| write("hello/x.txt", "x"); |
| write("hello/y.txt", "y"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "dep", |
| srcs = ["x.txt"], |
| outs = ["dep.txt"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| genrule( |
| name = "target", |
| srcs = ["dep.txt", "y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| addOptions("--experimental_working_set=hello/y.txt"); |
| buildTarget("//hello/..."); |
| |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(1); |
| |
| write("hello/x.txt", "x2"); |
| AbruptExitException e = |
| assertThrows(AbruptExitException.class, () -> buildTarget("//hello/...")); |
| assertThat(e).hasMessageThat().contains("detected changes outside of the working set"); |
| assertThat(e).hasMessageThat().contains("hello/x.txt"); |
| |
| addOptions("--experimental_working_set=hello/x.txt,hello/y.txt"); |
| buildTarget("//hello/..."); |
| assertContents("x2\ny", "//hello:target"); |
| } |
| |
| @Test |
| public void editingNonWorkingSet_inSiblingDir_failsTheBuild() throws Exception { |
| write("hello/x.txt", "x"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "//world:target"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| write("world/y.txt", "y"); |
| write( |
| "world/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| addOptions("--experimental_working_set=hello/x.txt"); |
| buildTarget("//hello/..."); |
| |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(1); |
| |
| write("world/y.txt", "y2"); |
| AbruptExitException e = |
| assertThrows(AbruptExitException.class, () -> buildTarget("//hello/...")); |
| assertThat(e).hasMessageThat().contains("detected changes outside of the working set"); |
| assertThat(e).hasMessageThat().contains("world/y.txt"); |
| |
| addOptions("--experimental_working_set=hello/x.txt,world/y.txt"); |
| buildTarget("//hello/..."); |
| assertContents("x\ny2", "//hello:target"); |
| } |
| |
| @Test |
| public void editingNonWorkingSet_inParentDir_failsTheBuild() throws Exception { |
| write("hello/x.txt", "x"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "//hello/world:target"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| write("hello/world/y.txt", "y"); |
| write( |
| "hello/world/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| addOptions("--experimental_working_set=hello/world"); |
| buildTarget("//hello/..."); |
| |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(1); |
| |
| write("hello/x.txt", "x2"); |
| AbruptExitException e = |
| assertThrows(AbruptExitException.class, () -> buildTarget("//hello/...")); |
| assertThat(e).hasMessageThat().contains("detected changes outside of the working set"); |
| assertThat(e).hasMessageThat().contains("hello/x.txt"); |
| |
| addOptions("--experimental_working_set=hello"); |
| buildTarget("//hello/..."); |
| assertContents("x2\ny", "//hello:target"); |
| } |
| |
| @Test |
| public void workingSet_reduced_withoutReanalysis() throws Exception { |
| addOptions("--experimental_working_set=hello/x.txt,hello/y.txt"); |
| write("hello/x.txt", "x"); |
| write("hello/y.txt", "y"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| buildTarget("//hello/..."); |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(2); |
| |
| resetOptions(); |
| setupOptions(); |
| addOptions("--experimental_working_set=hello/x.txt"); |
| write("hello/x.txt", "x2"); |
| |
| buildTarget("//hello/..."); |
| assertDoesNotContainEvent("discarding analysis cache"); |
| assertContents("x2\ny", "//hello:target"); |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(1); |
| } |
| |
| @Test |
| public void workingSet_expanded_withReanalysis() throws Exception { |
| addOptions("--experimental_working_set=hello/x.txt"); |
| write("hello/x.txt", "x"); |
| write("hello/y.txt", "y"); |
| write( |
| "hello/BUILD", |
| """ |
| genrule( |
| name = "target", |
| srcs = ["x.txt", "y.txt"], |
| outs = ["out"], |
| cmd = "cat $(SRCS) > $@", |
| ) |
| """); |
| |
| buildTarget("//hello/..."); |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(1); |
| |
| resetOptions(); |
| setupOptions(); |
| addOptions("--experimental_working_set=hello/x.txt,hello/y.txt"); |
| write("hello/x.txt", "x2"); |
| |
| buildTarget("//hello/..."); |
| assertContainsEvent("discarding analysis cache"); |
| assertContents("x2\ny", "//hello:target"); |
| assertThat(getSkyframeExecutor().getSkyfocusState().workingSet()).hasSize(2); |
| } |
| } |