| // 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.skyframe; |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.devtools.build.lib.analysis.testing.ToolchainCollectionSubject.assertThat; |
| import static com.google.devtools.build.lib.analysis.testing.ToolchainContextSubject.assertThat; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.Iterables; |
| import com.google.devtools.build.lib.actions.Action; |
| import com.google.devtools.build.lib.analysis.BlazeDirectories; |
| import com.google.devtools.build.lib.analysis.ConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.TargetAndConfiguration; |
| import com.google.devtools.build.lib.analysis.ToolchainCollection; |
| import com.google.devtools.build.lib.analysis.ToolchainContext; |
| import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget; |
| import com.google.devtools.build.lib.analysis.test.BaselineCoverageAction; |
| import com.google.devtools.build.lib.analysis.util.AnalysisMock; |
| import com.google.devtools.build.lib.analysis.util.AnalysisTestCase; |
| import com.google.devtools.build.lib.cmdline.Label; |
| import com.google.devtools.build.lib.packages.RuleClassProvider; |
| import com.google.devtools.build.lib.skyframe.ConfiguredTargetFunction.ComputedToolchainContexts; |
| import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils; |
| import com.google.devtools.build.skyframe.EvaluationResult; |
| import com.google.devtools.build.skyframe.SkyFunction; |
| import com.google.devtools.build.skyframe.SkyFunctionException; |
| import com.google.devtools.build.skyframe.SkyFunctionName; |
| import com.google.devtools.build.skyframe.SkyKey; |
| import com.google.devtools.build.skyframe.SkyValue; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| |
| /** |
| * Tests {@link ConfiguredTargetFunction}'s logic for determining each target toolchain context. |
| * |
| * <p>This is essentially an integration test for {@link |
| * ConfiguredTargetFunction#computeUnloadedToolchainContexts}. These methods form the core logic |
| * that figures out what a target's toolchain dependencies are. |
| * |
| * <p>{@link ConfiguredTargetFunction} is a complicated class that does a lot of things. This test |
| * focuses purely on the task of toolchain resolution. So instead of evaluating full {@link |
| * ConfiguredTargetFunction} instances, it evaluates a mock {@link SkyFunction} that just wraps the |
| * {@link ConfiguredTargetFunction#computeUnloadedToolchainContexts} part. This keeps focus tight |
| * and integration dependencies narrow. |
| * |
| * <p>We can't just call {@link ConfiguredTargetFunction#computeUnloadedToolchainContexts} directly |
| * because that method needs a {@link SkyFunction.Environment} and Blaze's test infrastructure |
| * doesn't support direct access to environments. |
| */ |
| @RunWith(JUnit4.class) |
| public final class ToolchainsForTargetsTest extends AnalysisTestCase { |
| /** Returns a {@link SkyKey} for a given <Target, BuildConfiguration> pair. */ |
| private static Key key( |
| TargetAndConfiguration targetAndConfiguration, ConfiguredTargetKey configuredTargetKey) { |
| return new AutoValue_ToolchainsForTargetsTest_Key(targetAndConfiguration, configuredTargetKey); |
| } |
| |
| /** Key class for {@link ComputeUnloadedToolchainContextsFunction}. */ |
| @AutoValue |
| abstract static class Key implements SkyKey { |
| abstract TargetAndConfiguration targetAndConfiguration(); |
| |
| abstract ConfiguredTargetKey configuredTargetKey(); |
| |
| @Override |
| public SkyFunctionName functionName() { |
| return ComputeUnloadedToolchainContextsFunction.SKYFUNCTION_NAME; |
| } |
| } |
| |
| /** |
| * Returns a {@link ToolchainCollection<UnloadedToolchainContext>} as the result of {@link |
| * ConfiguredTargetFunction#computeUnloadedToolchainContexts}. |
| */ |
| @AutoValue |
| abstract static class Value implements SkyValue { |
| abstract ToolchainCollection<UnloadedToolchainContext> getToolchainCollection(); |
| |
| static Value create(ToolchainCollection<UnloadedToolchainContext> toolchainCollection) { |
| return new AutoValue_ToolchainsForTargetsTest_Value(toolchainCollection); |
| } |
| } |
| |
| /** |
| * A mock {@link SkyFunction} that just calls {@link |
| * ConfiguredTargetFunction#computeUnloadedToolchainContexts} and returns its results. |
| */ |
| static class ComputeUnloadedToolchainContextsFunction implements SkyFunction { |
| static final SkyFunctionName SKYFUNCTION_NAME = |
| SkyFunctionName.createHermetic( |
| "CONFIGURED_TARGET_FUNCTION_COMPUTE_UNLOADED_TOOLCHAIN_CONTEXTS"); |
| |
| private final LateBoundStateProvider stateProvider; |
| |
| ComputeUnloadedToolchainContextsFunction(LateBoundStateProvider lateBoundStateProvider) { |
| this.stateProvider = lateBoundStateProvider; |
| } |
| |
| @Override |
| public SkyValue compute(SkyKey skyKey, Environment env) |
| throws ComputeUnloadedToolchainContextsException, InterruptedException { |
| try { |
| Key key = (Key) skyKey.argument(); |
| ComputedToolchainContexts result = |
| ConfiguredTargetFunction.computeUnloadedToolchainContexts( |
| env, |
| stateProvider.lateBoundRuleClassProvider(), |
| key.targetAndConfiguration(), |
| key.configuredTargetKey().getExecutionPlatformLabel()); |
| if (env.valuesMissing()) { |
| return null; |
| } |
| ToolchainCollection<UnloadedToolchainContext> toolchainCollection = |
| result.toolchainCollection; |
| return Value.create(toolchainCollection); |
| } catch (ToolchainException e) { |
| throw new ComputeUnloadedToolchainContextsException(e); |
| } |
| } |
| |
| private static class ComputeUnloadedToolchainContextsException extends SkyFunctionException { |
| ComputeUnloadedToolchainContextsException(Exception cause) { |
| super(cause, Transience.PERSISTENT); // We can generalize the transience if/when needed. |
| } |
| } |
| |
| @Override |
| public String extractTag(SkyKey skyKey) { |
| return null; |
| } |
| } |
| |
| /** |
| * Provides build state to {@link ComputeUnloadedToolchainContextsFunction}. This needs to be |
| * late-bound (i.e. we can't just pass the contents directly) because of the way {@link |
| * AnalysisTestCase} works: the {@link AnalysisMock} instance that instantiates the function gets |
| * created before the rest of the build state. See {@link AnalysisTestCase#createMocks} for |
| * details. |
| */ |
| private class LateBoundStateProvider { |
| RuleClassProvider lateBoundRuleClassProvider() { |
| return ruleClassProvider; |
| } |
| } |
| |
| /** |
| * An {@link AnalysisMock} that injects {@link ComputeUnloadedToolchainContextsFunction} into the |
| * Skyframe executor. |
| */ |
| private static final class AnalysisMockWithComputeDepsFunction extends AnalysisMock.Delegate { |
| private final LateBoundStateProvider stateProvider; |
| |
| AnalysisMockWithComputeDepsFunction(AnalysisMock parent, LateBoundStateProvider stateProvider) { |
| super(parent); |
| this.stateProvider = stateProvider; |
| } |
| |
| @Override |
| public ImmutableMap<SkyFunctionName, SkyFunction> getSkyFunctions( |
| BlazeDirectories directories) { |
| return ImmutableMap.<SkyFunctionName, SkyFunction>builder() |
| .putAll(super.getSkyFunctions(directories)) |
| .put( |
| ComputeUnloadedToolchainContextsFunction.SKYFUNCTION_NAME, |
| new ComputeUnloadedToolchainContextsFunction(stateProvider)) |
| .build(); |
| } |
| } |
| |
| @Override |
| protected AnalysisMock getAnalysisMock() { |
| return new AnalysisMockWithComputeDepsFunction( |
| super.getAnalysisMock(), new LateBoundStateProvider()); |
| } |
| |
| public ToolchainCollection<UnloadedToolchainContext> getToolchainCollection( |
| ConfiguredTarget configuredTarget, ConfiguredTargetKey configuredTargetKey) |
| throws InterruptedException { |
| String targetLabel = configuredTarget.getOriginalLabel().toString(); |
| SkyKey key = |
| key( |
| new TargetAndConfiguration(getTarget(targetLabel), getConfiguration(configuredTarget)), |
| configuredTargetKey); |
| // Analysis phase ended after the update() call in getToolchainCollection. We must re-enable |
| // analysis so we can call ConfiguredTargetFunction again without raising an error. |
| skyframeExecutor.getSkyframeBuildView().enableAnalysis(true); |
| EvaluationResult<Value> evalResult = |
| SkyframeExecutorTestUtils.evaluate(skyframeExecutor, key, /*keepGoing=*/ false, reporter); |
| // Test call has finished, to reset the state. |
| skyframeExecutor.getSkyframeBuildView().enableAnalysis(false); |
| return evalResult.get(key).getToolchainCollection(); |
| } |
| |
| public ToolchainCollection<UnloadedToolchainContext> getToolchainCollection(String targetLabel) |
| throws Exception { |
| ConfiguredTarget target = Iterables.getOnlyElement(update(targetLabel).getTargetsToBuild()); |
| return getToolchainCollection( |
| target, |
| ConfiguredTargetKey.builder() |
| .setLabel(target.getOriginalLabel()) |
| .setConfigurationKey(target.getConfigurationKey()) |
| .build()); |
| } |
| |
| @Before |
| public void createToolchains() throws Exception { |
| scratch.appendFile("WORKSPACE", "register_toolchains('//toolchains:all')"); |
| |
| scratch.file( |
| "toolchain/toolchain_def.bzl", |
| "def _impl(ctx):", |
| " toolchain = platform_common.ToolchainInfo(", |
| " data = ctx.attr.data)", |
| " return [toolchain]", |
| "test_toolchain = rule(", |
| " implementation = _impl,", |
| " attrs = {", |
| " 'data': attr.string()})"); |
| |
| scratch.file("toolchain/BUILD", "toolchain_type(name = 'test_toolchain')"); |
| |
| scratch.appendFile( |
| "toolchains/BUILD", |
| "load('//toolchain:toolchain_def.bzl', 'test_toolchain')", |
| "toolchain(", |
| " name = 'toolchain_1',", |
| " toolchain_type = '//toolchain:test_toolchain',", |
| " exec_compatible_with = [],", |
| " target_compatible_with = [],", |
| " toolchain = ':toolchain_1_impl')", |
| "test_toolchain(", |
| " name='toolchain_1_impl',", |
| " data = 'foo')", |
| "toolchain(", |
| " name = 'toolchain_2',", |
| " toolchain_type = '//toolchain:test_toolchain',", |
| " exec_compatible_with = [],", |
| " target_compatible_with = [],", |
| " toolchain = ':toolchain_2_impl')", |
| "test_toolchain(", |
| " name='toolchain_2_impl',", |
| " data = 'bar')"); |
| |
| scratch.appendFile( |
| "toolchain/rule.bzl", |
| "def _impl(ctx):", |
| " data = ctx.toolchains['//toolchain:test_toolchain'].data", |
| " return []", |
| "my_rule = rule(", |
| " implementation = _impl,", |
| " toolchains = ['//toolchain:test_toolchain'],", |
| ")"); |
| } |
| |
| // actual tests |
| @Test |
| public void basicToolchains() throws Exception { |
| scratch.file("a/BUILD", "load('//toolchain:rule.bzl', 'my_rule')", "my_rule(name = 'a')"); |
| |
| ToolchainCollection<UnloadedToolchainContext> toolchainCollection = |
| getToolchainCollection("//a"); |
| assertThat(toolchainCollection).isNotNull(); |
| assertThat(toolchainCollection).hasDefaultExecGroup(); |
| assertThat(toolchainCollection) |
| .defaultToolchainContext() |
| .hasToolchainType("//toolchain:test_toolchain"); |
| assertThat(toolchainCollection) |
| .defaultToolchainContext() |
| .hasResolvedToolchain("//toolchains:toolchain_1_impl"); |
| } |
| |
| @Test |
| public void execPlatform() throws Exception { |
| // Add some platforms and custom constraints. |
| scratch.file("platforms/BUILD", "platform(name = 'local_platform_a')"); |
| |
| // Test normal resolution, and with a per-target exec constraint. |
| scratch.file("a/BUILD", "load('//toolchain:rule.bzl', 'my_rule')", "my_rule(name = 'a')"); |
| |
| useConfiguration("--extra_execution_platforms=//platforms:local_platform_a"); |
| |
| ToolchainCollection<UnloadedToolchainContext> toolchainCollection = |
| getToolchainCollection("//a"); |
| assertThat(toolchainCollection).isNotNull(); |
| assertThat(toolchainCollection).hasDefaultExecGroup(); |
| assertThat(toolchainCollection) |
| .defaultToolchainContext() |
| // First execution platform will be used. |
| .hasExecutionPlatform("//platforms:local_platform_a"); |
| } |
| |
| @Test |
| public void execPlatform_withExecConstraint() throws Exception { |
| // Add some platforms and custom constraints. |
| scratch.file( |
| "platforms/BUILD", |
| "constraint_setting(name = 'local_setting')", |
| "constraint_value(name = 'local_value_a', constraint_setting = ':local_setting')", |
| "constraint_value(name = 'local_value_b', constraint_setting = ':local_setting')", |
| "platform(name = 'local_platform_a',", |
| " constraint_values = [':local_value_a'],", |
| ")", |
| "platform(name = 'local_platform_b',", |
| " constraint_values = [':local_value_b'],", |
| ")"); |
| |
| // Test normal resolution, and with a per-target exec constraint. |
| scratch.file( |
| "a/BUILD", |
| "load('//toolchain:rule.bzl', 'my_rule')", |
| "my_rule(name = 'a',", |
| " exec_compatible_with = ['//platforms:local_value_b'],", |
| ")"); |
| |
| useConfiguration( |
| "--extra_execution_platforms=//platforms:local_platform_a,//platforms:local_platform_b"); |
| |
| ToolchainCollection<UnloadedToolchainContext> toolchainCollection = |
| getToolchainCollection("//a"); |
| assertThat(toolchainCollection).isNotNull(); |
| assertThat(toolchainCollection).hasDefaultExecGroup(); |
| assertThat(toolchainCollection) |
| .defaultToolchainContext() |
| // Exec constraint forces the use of this exec platform. |
| .hasExecutionPlatform("//platforms:local_platform_b"); |
| } |
| |
| @Test |
| public void execGroups_named() throws Exception { |
| // Write a rule with exec groups. |
| scratch.appendFile( |
| "toolchain/exec_group_rule.bzl", |
| "def _impl(ctx):", |
| " pass", |
| "my_exec_group_rule = rule(", |
| " implementation = _impl,", |
| " exec_groups = {", |
| " 'temp': exec_group(", |
| " toolchains = ['//toolchain:test_toolchain'],", |
| " ),", |
| " },", |
| ")"); |
| |
| scratch.file( |
| "a/BUILD", |
| "load('//toolchain:exec_group_rule.bzl', 'my_exec_group_rule')", |
| "my_exec_group_rule(name = 'a')"); |
| |
| ToolchainCollection<UnloadedToolchainContext> toolchainCollection = |
| getToolchainCollection("//a"); |
| assertThat(toolchainCollection).isNotNull(); |
| assertThat(toolchainCollection).hasDefaultExecGroup(); |
| assertThat(toolchainCollection).defaultToolchainContext().toolchainTypeLabels().isEmpty(); |
| assertThat(toolchainCollection).defaultToolchainContext().resolvedToolchainLabels().isEmpty(); |
| |
| assertThat(toolchainCollection).hasExecGroup("temp"); |
| assertThat(toolchainCollection) |
| .execGroup("temp") |
| .hasToolchainType("//toolchain:test_toolchain"); |
| assertThat(toolchainCollection) |
| .execGroup("temp") |
| .hasResolvedToolchain("//toolchains:toolchain_1_impl"); |
| assertThat(toolchainCollection) |
| .execGroup("temp") |
| .hasToolchainType("//toolchain:test_toolchain"); |
| assertThat(toolchainCollection) |
| .execGroup("temp") |
| .hasResolvedToolchain("//toolchains:toolchain_1_impl"); |
| } |
| |
| @Test |
| public void execGroups_defaultAndNamed() throws Exception { |
| // Add another toolchain type. |
| scratch.appendFile( |
| "extra/BUILD", |
| "load('//toolchain:toolchain_def.bzl', 'test_toolchain')", |
| "toolchain_type(name = 'extra_toolchain')", |
| "toolchain(", |
| " name = 'toolchain',", |
| " toolchain_type = ':extra_toolchain',", |
| " exec_compatible_with = [],", |
| " target_compatible_with = [],", |
| " toolchain = ':toolchain_impl')", |
| "test_toolchain(", |
| " name='toolchain_impl',", |
| " data = 'foo')"); |
| |
| // Write a rule with exec groups. |
| scratch.appendFile( |
| "toolchain/exec_group_rule.bzl", |
| "def _impl(ctx):", |
| " pass", |
| "my_exec_group_rule = rule(", |
| " implementation = _impl,", |
| " toolchains = ['//extra:extra_toolchain'],", |
| " exec_groups = {", |
| " 'temp': exec_group(", |
| " toolchains = ['//toolchain:test_toolchain'],", |
| " ),", |
| " },", |
| ")"); |
| |
| scratch.file( |
| "a/BUILD", |
| "load('//toolchain:exec_group_rule.bzl', 'my_exec_group_rule')", |
| "my_exec_group_rule(name = 'a')"); |
| |
| useConfiguration("--extra_toolchains=//extra:toolchain"); |
| ToolchainCollection<UnloadedToolchainContext> toolchainCollection = |
| getToolchainCollection("//a"); |
| assertThat(toolchainCollection).isNotNull(); |
| assertThat(toolchainCollection).hasDefaultExecGroup(); |
| assertThat(toolchainCollection) |
| .defaultToolchainContext() |
| .hasToolchainType("//extra:extra_toolchain"); |
| assertThat(toolchainCollection) |
| .defaultToolchainContext() |
| .hasResolvedToolchain("//extra:toolchain_impl"); |
| |
| assertThat(toolchainCollection).hasExecGroup("temp"); |
| assertThat(toolchainCollection) |
| .execGroup("temp") |
| .hasToolchainType("//toolchain:test_toolchain"); |
| assertThat(toolchainCollection) |
| .execGroup("temp") |
| .hasResolvedToolchain("//toolchains:toolchain_1_impl"); |
| } |
| |
| @Test |
| public void keepParentToolchainContext() throws Exception { |
| // Add some platforms and custom constraints. |
| scratch.file( |
| "platforms/BUILD", |
| "constraint_setting(name = 'local_setting')", |
| "constraint_value(name = 'local_value_a', constraint_setting = ':local_setting')", |
| "constraint_value(name = 'local_value_b', constraint_setting = ':local_setting')", |
| "platform(name = 'local_platform_a',", |
| " constraint_values = [':local_value_a'],", |
| ")", |
| "platform(name = 'local_platform_b',", |
| " constraint_values = [':local_value_b'],", |
| ")"); |
| |
| // Test normal resolution, and with a per-target exec constraint. |
| scratch.file("a/BUILD", "load('//toolchain:rule.bzl', 'my_rule')", "my_rule(name = 'a')"); |
| |
| useConfiguration( |
| "--extra_execution_platforms=//platforms:local_platform_a,//platforms:local_platform_b"); |
| |
| ConfiguredTarget target = Iterables.getOnlyElement(update("//a").getTargetsToBuild()); |
| ToolchainCollection<UnloadedToolchainContext> toolchainCollection = |
| getToolchainCollection( |
| target, |
| ConfiguredTargetKey.builder() |
| .setLabel(target.getOriginalLabel()) |
| .setConfigurationKey(target.getConfigurationKey()) |
| .setExecutionPlatformLabel( |
| Label.parseAbsoluteUnchecked("//platforms:local_platform_b")) |
| .build()); |
| |
| assertThat(toolchainCollection).isNotNull(); |
| assertThat(toolchainCollection).hasDefaultExecGroup(); |
| |
| // This should have the same exec platform as parentToolchainKey, which is local_platform_b. |
| assertThat(toolchainCollection) |
| .defaultToolchainContext() |
| .hasExecutionPlatform("//platforms:local_platform_b"); |
| } |
| |
| /** Regression test for b/214105142, https://github.com/bazelbuild/bazel/issues/14521 */ |
| @Test |
| public void toolchainWithDifferentExecutionPlatforms_doesNotGenerateConflictingCoverageAction() |
| throws Exception { |
| scratch.file( |
| "platforms/BUILD", |
| "constraint_setting(name = 'local_setting')", |
| "constraint_value(name = 'local_value_a', constraint_setting = ':local_setting')", |
| "constraint_value(name = 'local_value_b', constraint_setting = ':local_setting')", |
| "platform(name = 'local_platform_a', constraint_values = [':local_value_a'])", |
| "platform(name = 'local_platform_b', constraint_values = [':local_value_b'])"); |
| scratch.file( |
| "a/BUILD", |
| "load('//toolchain:rule.bzl', 'my_rule')", |
| "my_rule(name='a', exec_compatible_with=['//platforms:local_value_a'])", |
| "my_rule(name='b', exec_compatible_with=['//platforms:local_value_b'])"); |
| useConfiguration( |
| "--collect_code_coverage", |
| "--extra_execution_platforms=//platforms:local_platform_a,//platforms:local_platform_b"); |
| |
| update("//a:a", "//a:b"); |
| |
| // Sanity check that a coverage action was generated for the rule itself. |
| assertHasBaselineCoverageAction("//a:a", "Writing file a/a/baseline_coverage.dat"); |
| assertHasBaselineCoverageAction("//a:b", "Writing file a/b/baseline_coverage.dat"); |
| assertThat(getActions("//toolchains:toolchain_1_impl")).isEmpty(); |
| ToolchainContext toolchainAContext = |
| getToolchainCollection("//a:a").getDefaultToolchainContext(); |
| assertThat(toolchainAContext).hasExecutionPlatform("//platforms:local_platform_a"); |
| assertThat(toolchainAContext).hasToolchainType("//toolchain:test_toolchain"); |
| assertThat(toolchainAContext).hasResolvedToolchain("//toolchains:toolchain_1_impl"); |
| ToolchainContext toolchainBContext = |
| getToolchainCollection("//a:b").getDefaultToolchainContext(); |
| assertThat(toolchainBContext).hasExecutionPlatform("//platforms:local_platform_b"); |
| assertThat(toolchainBContext).hasToolchainType("//toolchain:test_toolchain"); |
| assertThat(toolchainBContext).hasResolvedToolchain("//toolchains:toolchain_1_impl"); |
| } |
| |
| private void assertHasBaselineCoverageAction(String label, String progressMessage) |
| throws InterruptedException { |
| Action coverageAction = Iterables.getOnlyElement(getActions(label)); |
| assertThat(coverageAction).isInstanceOf(BaselineCoverageAction.class); |
| assertThat(coverageAction.getProgressMessage()).isEqualTo(progressMessage); |
| } |
| |
| private ImmutableList<Action> getActions(String label) throws InterruptedException { |
| return ((RuleConfiguredTarget) getConfiguredTarget(label)) |
| .getActions().stream().map(Action.class::cast).collect(toImmutableList()); |
| } |
| } |