| // Copyright 2020 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 com.google.common.collect.Iterables; |
| import com.google.common.collect.Sets; |
| import com.google.devtools.build.lib.actions.Action; |
| import com.google.devtools.build.lib.actions.ActionAnalysisMetadata; |
| import com.google.devtools.build.lib.actions.ActionGraph; |
| import com.google.devtools.build.lib.actions.ActionGraphVisitor; |
| import com.google.devtools.build.lib.actions.ActionKeyContext; |
| import com.google.devtools.build.lib.actions.Artifact; |
| import com.google.devtools.build.lib.actions.extra.ExtraActionInfo; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.ExtraActionArtifactsProvider; |
| import com.google.devtools.build.lib.analysis.ViewCreationFailedException; |
| import com.google.devtools.build.lib.analysis.extra.ExtraActionSpec; |
| import com.google.devtools.build.lib.buildtool.util.GoogleBuildIntegrationTestCase; |
| import com.google.devtools.build.lib.collect.nestedset.NestedSet; |
| import com.google.devtools.build.lib.rules.java.JavaInfo; |
| import com.google.devtools.build.lib.rules.java.JavaRuleOutputJarsProvider; |
| import com.google.devtools.build.lib.testutil.Suite; |
| import com.google.devtools.build.lib.testutil.TestSpec; |
| import com.google.devtools.build.lib.util.Fingerprint; |
| import com.google.devtools.build.lib.vfs.Path; |
| import com.google.devtools.common.options.OptionsParsingException; |
| import java.io.InputStream; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Set; |
| import org.junit.Assert; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Tests the the action_listener/extra_action feature. |
| * (--experimental_action_listener blaze option) |
| */ |
| @TestSpec(size = Suite.MEDIUM_TESTS) |
| @RunWith(JUnit4.class) |
| public class ActionListenerIntegrationTest extends GoogleBuildIntegrationTestCase { |
| protected final ActionKeyContext actionKeyContext = new ActionKeyContext(); |
| |
| private Map<ConfiguredTarget, Iterable<Artifact.DerivedArtifact>> getExtraArtifactMap() { |
| Map<ConfiguredTarget, Iterable<Artifact.DerivedArtifact>> result = new LinkedHashMap<>(); |
| for (ConfiguredTarget configuredTarget : getAllConfiguredTargets()) { |
| ExtraActionArtifactsProvider provider = configuredTarget.getProvider( |
| ExtraActionArtifactsProvider.class); |
| if (provider != null && !provider.getExtraActionArtifacts().isEmpty()) { |
| result.put(configuredTarget, provider.getExtraActionArtifacts().toList()); |
| } |
| } |
| return result; |
| } |
| |
| private NestedSet<Artifact.DerivedArtifact> getExtraActionArtifacts(ConfiguredTarget target) { |
| return target.getProvider(ExtraActionArtifactsProvider.class).getExtraActionArtifacts(); |
| } |
| |
| private void assertExtraActionOutputForJavaLibraryRule(String rule, String extraAction, |
| boolean shouldBePresent, boolean shouldDependOnOutput) throws Exception { |
| final String extraActionPath = extraAction.substring(2).replace(':', '/'); |
| final String rulePackage = rule.substring(2, rule.indexOf(':')); |
| final String extraActionOutputRoot = |
| "extra_actions/" + extraActionPath + "/" + rulePackage + "/"; |
| |
| final ConfiguredTarget javalib = getConfiguredTarget(rule); |
| |
| assertThat(javalib).isNotNull(); |
| |
| NestedSet<Artifact.DerivedArtifact> extraArtifacts = getExtraActionArtifacts(javalib); |
| assertThat(extraArtifacts).isNotNull(); |
| |
| final Set<ActionAnalysisMetadata> actions = new HashSet<>(); |
| |
| class JavacFinderVisitor extends ActionGraphVisitor { |
| public JavacFinderVisitor(ActionGraph actionGraph) { |
| super(actionGraph); |
| } |
| |
| @Override |
| protected boolean shouldVisit(ActionAnalysisMetadata action) { |
| return action.getOwner().getLabel().equals(javalib.getLabel()); |
| } |
| |
| @Override |
| protected void visitAction(ActionAnalysisMetadata action) { |
| if (action.getMnemonic().equals("Javac")) { |
| actions.add(action); |
| } |
| } |
| } |
| |
| JavacFinderVisitor visitor = new JavacFinderVisitor(getActionGraph()); |
| |
| Set<Artifact> outputs = |
| Sets.newHashSet( |
| Iterables.concat( |
| JavaInfo.getProvider(JavaRuleOutputJarsProvider.class, javalib) |
| .getAllSrcOutputJars(), |
| JavaInfo.getProvider(JavaRuleOutputJarsProvider.class, javalib) |
| .getAllClassOutputJars())); |
| outputs.addAll(getFilesToBuild(javalib).toList()); |
| visitor.visitWhiteNodes(outputs); |
| |
| assertThat(actions).isNotEmpty(); |
| |
| assertThat(extraArtifacts.toList()).hasSize(2 * actions.size()); |
| |
| for (ActionAnalysisMetadata action : actions) { |
| boolean hasProtoArtifact = false; |
| boolean hasTestArtifact = false; |
| |
| String ownerDigest = |
| new Fingerprint().addString(action.getOwner().getLabel().toString()).hexDigestAndReset(); |
| |
| String actionId = |
| ExtraActionSpec.getActionId(actionKeyContext, action.getOwner(), (Action) action); |
| String testArtifactPath = extraActionOutputRoot + ownerDigest + "_" + actionId + ".tst"; |
| String protoArtifactPath = extraActionOutputRoot + actionId + ".xa"; |
| |
| for (Artifact extraArtifact : extraArtifacts.toList()) { |
| Path path = extraArtifact.getPath(); |
| if (path.toString().endsWith(protoArtifactPath)) { |
| hasProtoArtifact = true; |
| if (shouldBePresent) { |
| ExtraActionInfo.Builder builder = ExtraActionInfo.newBuilder(); |
| InputStream inputStream = path.getInputStream(); |
| builder.mergeFrom(inputStream); |
| ExtraActionInfo info = builder.build(); |
| assertThat(info.getOwner()).isEqualTo(rule); |
| } |
| continue; |
| } |
| ActionAnalysisMetadata artifactOwningExtraAction = getActionGraph() |
| .getGeneratingAction(extraArtifact); |
| |
| assertThat(artifactOwningExtraAction).isNotNull(); |
| |
| Set<Artifact> extraActionInputs = artifactOwningExtraAction.getInputs().toSet(); |
| Set<Artifact> actionOutputs = Sets.newHashSet(action.getOutputs()); |
| if (shouldDependOnOutput) { |
| // If the extra_action has require_action_output set, all of the outputs of the |
| // shadowed action should be part of the extra_action's inputs. |
| assertThat(extraActionInputs).containsAtLeastElementsIn(actionOutputs); |
| } else { |
| // If the extra_action doesn't have requires_action_output set, none of the outputs of the |
| // shadowed action should be part of the extra_action's inputs. |
| assertThat(Sets.intersection(extraActionInputs, actionOutputs)).isEmpty(); |
| } |
| |
| assertThat(path.exists()).isEqualTo(shouldBePresent); |
| |
| if (path.toString().endsWith(testArtifactPath)) { |
| hasTestArtifact = true; |
| if (shouldBePresent) { |
| String contents = readContentAsLatin1String(extraArtifact); |
| String[] lines = contents.split("\n"); |
| assertThat(lines).isNotEmpty(); |
| String firstLine = lines[0]; |
| |
| assertThat(firstLine).endsWith(protoArtifactPath); |
| } |
| } |
| } |
| assertThat(hasProtoArtifact).isTrue(); |
| assertThat(hasTestArtifact).isTrue(); |
| } |
| } |
| |
| @Test |
| public void testBasicActionListener() throws Exception { |
| write("nobuild/BUILD", |
| "java_library(name= 'javalib',", |
| " srcs=[])", |
| "extra_action(name = 'baz',", |
| " out_templates = ['$(OWNER_LABEL_DIGEST)_$(ACTION_ID).tst'],", |
| " cmd = " + |
| " 'echo $(EXTRA_ACTION_FILE)>$(output $(OWNER_LABEL_DIGEST)" + |
| "_$(ACTION_ID).tst)')", |
| "action_listener(name = 'bar',", |
| " mnemonics = ['Javac'],", |
| " extra_actions = [':baz'])"); |
| |
| addOptions("--experimental_action_listener=//nobuild:bar"); |
| |
| buildTarget("//nobuild:javalib"); |
| |
| Map<ConfiguredTarget, Iterable<Artifact.DerivedArtifact>> extraArtifactsMap = |
| getExtraArtifactMap(); |
| assertThat(extraArtifactsMap).hasSize(1); |
| |
| assertExtraActionOutputForJavaLibraryRule("//nobuild:javalib", "//nobuild:baz", true, false); |
| } |
| |
| @Test |
| public void testActionListenerThatRequiresActionOutputs() throws Exception { |
| write("nobuild/BUILD", |
| "java_library(name= 'javalib',", |
| " srcs=[])", |
| "extra_action(name = 'baz',", |
| " out_templates = ['$(OWNER_LABEL_DIGEST)_$(ACTION_ID).tst'],", |
| " requires_action_output = 1,", |
| " cmd = " + |
| " 'echo $(EXTRA_ACTION_FILE)>$(output $(OWNER_LABEL_DIGEST)" + |
| "_$(ACTION_ID).tst)')", |
| "action_listener(name = 'bar',", |
| " mnemonics = ['Javac'],", |
| " extra_actions = [':baz'])"); |
| |
| addOptions("--experimental_action_listener=//nobuild:bar"); |
| |
| buildTarget("//nobuild:javalib"); |
| |
| Map<ConfiguredTarget, Iterable<Artifact.DerivedArtifact>> extraArtifactsMap = |
| getExtraArtifactMap(); |
| assertThat(extraArtifactsMap).hasSize(1); |
| |
| assertExtraActionOutputForJavaLibraryRule("//nobuild:javalib", "//nobuild:baz", true, true); |
| } |
| |
| @Test |
| public void testFilteredActionListener() throws Exception { |
| write("filtered/BUILD", |
| "java_library(name= 'a',", |
| " srcs=[])", |
| "java_library(name= 'b',", |
| " exports=[':a'])", |
| "extra_action(name = 'baz',", |
| " out_templates = ['$(OWNER_LABEL_DIGEST)_$(ACTION_ID).tst'],", |
| " cmd = " + |
| " 'echo $(EXTRA_ACTION_FILE)>$(output $(OWNER_LABEL_DIGEST)" + |
| "_$(ACTION_ID).tst)')", |
| "action_listener(name = 'bar',", |
| " mnemonics = ['Javac'],", |
| " extra_actions = [':baz'])"); |
| |
| addOptions("--experimental_action_listener=//filtered:bar", |
| "--experimental_extra_action_filter=.*\\:a"); |
| |
| buildTarget("//filtered:b"); |
| |
| Map<ConfiguredTarget, Iterable<Artifact.DerivedArtifact>> extraArtifactsMap = |
| getExtraArtifactMap(); |
| assertThat(extraArtifactsMap).hasSize(2); |
| |
| assertExtraActionOutputForJavaLibraryRule("//filtered:a", "//filtered:baz", true, false); |
| assertExtraActionOutputForJavaLibraryRule("//filtered:b", "//filtered:baz", false, false); |
| } |
| |
| @Test |
| public void testTopLevelOnlyActionListener() throws Exception { |
| write("filtered/BUILD", |
| "java_library(name= 'a',", |
| " srcs=[])", |
| "java_library(name= 'b',", |
| " exports=[':a'])", |
| "extra_action(name = 'baz',", |
| " out_templates = ['$(OWNER_LABEL_DIGEST)_$(ACTION_ID).tst'],", |
| " cmd = " + |
| " 'echo $(EXTRA_ACTION_FILE)>$(output $(OWNER_LABEL_DIGEST)" + |
| "_$(ACTION_ID).tst)')", |
| "action_listener(name = 'bar',", |
| " mnemonics = ['Javac'],", |
| " extra_actions = [':baz'])"); |
| |
| addOptions("--experimental_action_listener=//filtered:bar", |
| "--experimental_extra_action_top_level_only"); |
| |
| buildTarget("//filtered:b"); |
| |
| Map<ConfiguredTarget, Iterable<Artifact.DerivedArtifact>> extraArtifactsMap = |
| getExtraArtifactMap(); |
| assertThat(extraArtifactsMap).hasSize(2); |
| |
| assertExtraActionOutputForJavaLibraryRule("//filtered:a", "//filtered:baz", false, false); |
| assertExtraActionOutputForJavaLibraryRule("//filtered:b", "//filtered:baz", true, false); |
| } |
| |
| @Test |
| public void testCcTestActionListener() throws Exception { |
| write("nobuild/main.cc", |
| "int main() { return 0; }"); |
| write("nobuild/BUILD", |
| "cc_test(name= 'cctest',", |
| " srcs=['main.cc'])", |
| "extra_action(name = 'baz',", |
| " out_templates = ['$(ACTION_ID).tst'],", |
| " cmd = " + |
| " 'echo $(EXTRA_ACTION_FILE)>$(output $(ACTION_ID).tst)')", |
| "action_listener(name = 'bar',", |
| " mnemonics = ['CppCompile'],", |
| " extra_actions = [':baz'])"); |
| |
| addOptions("--experimental_action_listener=//nobuild:bar"); |
| |
| buildTarget("//nobuild:cctest"); |
| final ConfiguredTarget cctest = getConfiguredTarget("//nobuild:cctest"); |
| |
| assertThat(cctest).isNotNull(); |
| |
| NestedSet<Artifact.DerivedArtifact> extraArtifacts = getExtraActionArtifacts(cctest); |
| assertThat(extraArtifacts).isNotNull(); |
| |
| final Set<ActionAnalysisMetadata> actions = new HashSet<>(); |
| class CppCompileActionFinder extends ActionGraphVisitor { |
| |
| public CppCompileActionFinder(ActionGraph actionGraph) { |
| super(actionGraph); |
| } |
| |
| @Override |
| protected boolean shouldVisit(ActionAnalysisMetadata action) { |
| return action.getOwner().getLabel().equals(cctest.getLabel()); |
| } |
| |
| @Override |
| protected void visitAction(ActionAnalysisMetadata action) { |
| if (action.getMnemonic().equals("CppCompile")) { |
| actions.add(action); |
| } |
| } |
| } |
| |
| CppCompileActionFinder visitor = new CppCompileActionFinder(getActionGraph()); |
| |
| Set<Artifact> outputs = new HashSet<>(); |
| outputs.addAll(getFilesToBuild(cctest).toList()); |
| visitor.visitWhiteNodes(outputs); |
| |
| assertThat(actions).isNotEmpty(); |
| assertThat(extraArtifacts.toList()).hasSize(2 * actions.size()); |
| |
| for (ActionAnalysisMetadata action : actions) { |
| boolean hasProtoArtifact = false; |
| boolean hasTestArtifact = false; |
| String actionId = |
| ExtraActionSpec.getActionId(actionKeyContext, action.getOwner(), (Action) action); |
| |
| String testArtifactPath = "extra_actions/nobuild/baz/nobuild/" + actionId + ".tst"; |
| String protoArtifactPath = "extra_actions/nobuild/baz/nobuild/" + actionId + ".xa"; |
| |
| for (Artifact extraArtifact : extraArtifacts.toList()) { |
| Path path = extraArtifact.getPath(); |
| assertThat(path.exists()).isTrue(); |
| |
| if (path.toString().endsWith(testArtifactPath)) { |
| hasTestArtifact = true; |
| |
| String contents = readContentAsLatin1String(extraArtifact); |
| String[] lines = contents.split("\n"); |
| assertThat(lines).isNotEmpty(); |
| String firstLine = lines[0]; |
| |
| assertThat(firstLine).endsWith(protoArtifactPath); |
| } |
| if (path.toString().endsWith(protoArtifactPath)) { |
| hasProtoArtifact = true; |
| |
| ExtraActionInfo.Builder builder = ExtraActionInfo.newBuilder(); |
| InputStream inputStream = path.getInputStream(); |
| builder.mergeFrom(inputStream); |
| ExtraActionInfo info = builder.build(); |
| assertThat(info.getOwner()).isEqualTo("//nobuild:cctest"); |
| } |
| } |
| assertThat(hasProtoArtifact).isTrue(); |
| assertThat(hasTestArtifact).isTrue(); |
| } |
| } |
| |
| |
| @Test |
| public void testActionListenerNotEnabled() throws Exception { |
| write("nobuild/BUILD", |
| "java_library(name= 'javalib',", |
| " srcs=[])", |
| "extra_action(name = 'baz',", |
| " out_templates = ['$(ACTION_ID).tst'],", |
| " cmd = " + |
| "'echo $(EXTRA_ACTION_FILE)>$(output $(ACTION_ID).tst)')", |
| "action_listener(name = 'bar',", |
| " mnemonics = ['Javac'],", |
| " extra_actions = [':baz'])"); |
| |
| buildTarget("//nobuild:javalib"); |
| ConfiguredTarget javalib = getConfiguredTarget("//nobuild:javalib"); |
| |
| assertThat(javalib).isNotNull(); |
| |
| Map<ConfiguredTarget, Iterable<Artifact.DerivedArtifact>> extraArtifactsMap = |
| getExtraArtifactMap(); |
| assertThat(extraArtifactsMap).isEmpty(); |
| } |
| |
| @Test |
| public void testBuildActionListener() throws Exception { |
| write("nobuild/BUILD", |
| "extra_action(name = 'action',", |
| " cmd = '')", |
| "action_listener(name = 'listener',", |
| " mnemonics = ['Foo'],", |
| " extra_actions = [':action'])"); |
| buildTarget("//nobuild:listener"); |
| // Confirm target exists. |
| getExistingConfiguredTarget("//nobuild:listener"); |
| } |
| |
| @Test |
| public void testNotActionListenerLabel() throws Exception { |
| write("nobuild/BUILD", |
| "java_library(name= 'javalib1',", |
| " srcs=[])", |
| "java_library(name= 'javalib2',", |
| " srcs=[])"); |
| addOptions("--experimental_action_listener=//nobuild:javalib1"); |
| try { |
| buildTarget("//nobuild:javalib2"); |
| Assert.fail("expected failure"); |
| } catch (ViewCreationFailedException expected) { |
| assertThat(expected) |
| .hasMessageThat() |
| .contains( |
| String.format("Analysis of target '%s' failed; build aborted", "//nobuild:javalib2")); |
| } |
| } |
| |
| @Test |
| public void testInvalidActionListenerLabel() throws Exception { |
| write("nobuild/BUILD", |
| "java_library(name= 'javalib',", |
| " srcs=[])"); |
| try { |
| createOptionsParser().parse("--experimental_action_listener='this is \\not\\ a valid label'"); |
| Assert.fail("expected failure"); |
| } catch (OptionsParsingException ope) { |
| assertThat(ope) |
| .hasMessageThat() |
| .isEqualTo( |
| String.format( |
| "While parsing option %s='%s': invalid package name ''%s'': " |
| + "package names may contain " |
| + "A-Z, a-z, 0-9, or any of ' !\"#$%%&'()*+,-./;<=>?[]^_`{|}~' " |
| + "(most 7-bit ascii characters except 0-31, 127, ':', or '\\')", |
| "--experimental_action_listener", |
| "this is \\not\\ a valid label", |
| "this is \\not\\ a valid label")); |
| } |
| } |
| |
| /** |
| * Ensure outputs checked for uniqueness. |
| */ |
| @Test |
| public void testNonUniqueOutputs() throws Exception { |
| write("nobuild/BUILD", |
| "java_library(name= 'javalib',", |
| " srcs=[])", |
| "extra_action(name = 'baz',", |
| " out_templates = ['test.tst'],", |
| " cmd = 'echo $(output test.tst)')", |
| "action_listener(name = 'bar',", |
| " mnemonics = ['Javac', 'JavaSourceJar'],", |
| " extra_actions = [':baz'])"); |
| |
| addOptions("--experimental_action_listener=//nobuild:bar"); |
| |
| try { |
| buildTarget("//nobuild:javalib"); |
| Assert.fail("expected failure"); |
| } catch (ViewCreationFailedException vcfe) { |
| assertThat(vcfe) |
| .hasMessageThat() |
| .contains( |
| String.format("Analysis of target '%s' failed; build aborted", "//nobuild:javalib")); |
| } |
| } |
| } |