// 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.analysis.test;

import static com.google.common.truth.Truth.assertThat;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.eventbus.EventBus;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.analysis.AnalysisResult;
import com.google.devtools.build.lib.analysis.ConfiguredAspect;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.TransitiveInfoCollection;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.packages.StarlarkProvider;
import com.google.devtools.build.lib.packages.StructImpl;
import com.google.devtools.build.lib.packages.TestTimeout;
import com.google.devtools.build.lib.testutil.TestConstants;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** {@link com.google.devtools.build.lib.analysis.test.TestActionBuilder} tests. */
@RunWith(JUnit4.class)
public class TestActionBuilderTest extends BuildViewTestCase {

  @Before
  public final void createBuildFile() throws Exception {
    analysisMock.pySupport().setup(mockToolsConfig);

    scratch.file(
        "tests/BUILD",
        "py_test(name = 'small_test_1',",
        "        srcs = ['small_test_1.py'],",
        "        data = [':xUnit'],",
        "        size = 'small',",
        "        tags = ['tag1'])",
        "",
        "sh_test(name = 'small_test_2',",
        "        srcs = ['small_test_2.sh'],",
        "        size = 'small',",
        "        tags = ['tag2'])",
        "",
        "sh_test(name = 'large_test_1',",
        "        srcs = ['large_test_1.sh'],",
        "        data = [':xUnit'],",
        "        size = 'large',",
        "        tags = ['tag1'])",
        "",
        "py_binary(name = 'notest',",
        "        srcs = ['notest.py'])",
        "cc_library(name = 'xUnit')",
        "",
        "test_suite(name = 'smallTests', tags=['small'])");
  }

  private void assertSharded(ConfiguredTarget testRule, int expectSharding) {
    ImmutableList<Artifact.DerivedArtifact> testStatusList = getTestStatusArtifacts(testRule);
    if (expectSharding == 0) {
      Artifact testResult = Iterables.getOnlyElement(testStatusList);
      TestRunnerAction action = (TestRunnerAction) getGeneratingAction(testResult);
      assertThat(action.isSharded()).isFalse();
      assertThat(action.getExecutionSettings().getTotalShards()).isSameInstanceAs(0);
      assertThat(action.getShardNum()).isSameInstanceAs(0);
      return;
    }

    int totalShards = testStatusList.size();
    Set<Integer> shardNumbers = new HashSet<>();
    for (Artifact testResult : testStatusList) {
      TestRunnerAction action = (TestRunnerAction) getGeneratingAction(testResult);
      assertThat(action.isSharded()).isTrue();
      assertThat(action.getExecutionSettings().getTotalShards()).isSameInstanceAs(totalShards);
      assertThat(action.getTestLog().getExecPath().getPathString())
          .endsWith(
              String.format("shard_%d_of_%d/test.log", action.getShardNum() + 1, totalShards));
      shardNumbers.add(action.getShardNum());
    }
    assertThat(shardNumbers).isEqualTo(sequenceSet(0, totalShards));
    assertThat(shardNumbers).hasSize(expectSharding);
  }

  private static Set<Integer> sequenceSet(int start, int end) {
    Preconditions.checkArgument(end > start);
    Set<Integer> seqSet = new HashSet<>();
    for (int i = start; i < end; i++) {
      seqSet.add(i);
    }
    return seqSet;
  }

  private void writeJavaTests() throws IOException {
    scratch.file(
        "javatests/jt/BUILD",
        "java_test(name = 'RGT',",
        "          srcs = ['RGT.java'])",
        "java_test(name = 'RGT_none',",
        "          shard_count = 0,",
        "          srcs = ['RGT.java'])",
        "java_test(name = 'RGT_many',",
        "          shard_count = 33,",
        "          srcs = ['RGT.java'])",
        "java_test(name = 'RGT_small',",
        "          srcs = ['RGT.java'],",
        "          size = 'small')",
        "",
        "java_test(name = 'NoRunner',",
        "          main_class = 'NoTestRunnerTest.java',",
        "          use_testrunner = 0,",
        "          srcs = ['NoTestRunnerTest.java'])",
        "");
  }

  @Test
  public void testSharding() throws Exception {
    useConfiguration("--test_sharding_strategy=explicit");

    assertSharded(getConfiguredTarget("//tests:small_test_1"), 0);
    assertSharded(getConfiguredTarget("//tests:large_test_1"), 0);

    writeJavaTests();
    assertSharded(getConfiguredTarget("//javatests/jt:NoRunner"), 0);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT"), 0);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_small"), 0);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_none"), 0);

    // Has an explicit "shard_count" attribute.
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_many"), 33);
  }

  @Test
  public void testShardingDisabled() throws Exception {
    useConfiguration("--test_sharding_strategy=disabled");

    assertSharded(getConfiguredTarget("//tests:small_test_1"), 0);
    assertSharded(getConfiguredTarget("//tests:large_test_1"), 0);

    writeJavaTests();
    assertSharded(getConfiguredTarget("//javatests/jt:NoRunner"), 0);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT"), 0);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_small"), 0);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_none"), 0);

    // Has an explicit "shard_count" attribute.
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_many"), 0);
  }

  @Test
  public void testShardingForced() throws Exception {
    useConfiguration("--test_sharding_strategy=forced=5");

    assertSharded(getConfiguredTarget("//tests:small_test_1"), 5);
    assertSharded(getConfiguredTarget("//tests:large_test_1"), 5);

    writeJavaTests();
    assertSharded(getConfiguredTarget("//javatests/jt:NoRunner"), 5);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT"), 5);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_small"), 5);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_none"), 5);
    assertSharded(getConfiguredTarget("//javatests/jt:RGT_many"), 5);
  }

  @Test
  public void testFlakyAttributeValidation() throws Exception {
    scratch.file("flaky/BUILD",
        "sh_test(name = 'good_test',",
        "        srcs = ['a.sh'])",
        "sh_test(name = 'flaky_test',",
        "        srcs = ['a.sh'],",
        "        flaky = 1)");
    Artifact testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:good_test"));
    assertThat(testStatus).isNotNull();
    TestRunnerAction action = (TestRunnerAction) getGeneratingAction(testStatus);
    assertThat(action.getTestProperties().isFlaky()).isFalse();

    testStatus = Iterables.getOnlyElement(getTestStatusArtifacts("//flaky:flaky_test"));
    assertThat(testStatus).isNotNull();
    action = (TestRunnerAction) getGeneratingAction(testStatus);
    assertThat(action.getTestProperties().isFlaky()).isTrue();
  }

  @Test
  public void testIllegalBooleanFlakySetting() throws Exception {
    checkError("flaky", "bad_test",
        "boolean is not one of [0, 1]",
        "sh_test(name = 'bad_test',",
        "        srcs = ['a.sh'],",
        "        flaky = 2)");
  }

  @Test
  public void testRunsPerTest() throws Exception {
    useConfiguration("--runs_per_test=2");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//tests:small_test_1");
    assertThat(testStatusList).hasSize(2);
    Artifact testStatus1 = testStatusList.get(0);
    Artifact testStatus2 = testStatusList.get(1);
    assertThat(testStatus1).isNotNull();
    assertThat(testStatus2).isNotNull();
    assertThat(testStatus2).isNotSameInstanceAs(testStatus1);
    assertThat(getGeneratingAction(testStatus2))
        .isNotSameInstanceAs(getGeneratingAction(testStatus1));
    assertThat(testStatus1.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_1_of_2/test");
    assertThat(testStatus2.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_2_of_2/test");
  }

  @Test
  public void testRunsPerTestCanBeOverridden() throws Exception {
    useConfiguration("--runs_per_test=1", "--runs_per_test=2");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//tests:small_test_1");
    assertThat(testStatusList).hasSize(2);
    Artifact testStatus1 = testStatusList.get(0);
    Artifact testStatus2 = testStatusList.get(1);
    assertThat(testStatus1).isNotNull();
    assertThat(testStatus2).isNotNull();
    assertThat(testStatus2).isNotSameInstanceAs(testStatus1);
    assertThat(getGeneratingAction(testStatus2))
        .isNotSameInstanceAs(getGeneratingAction(testStatus1));
    assertThat(testStatus1.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_1_of_2/test");
    assertThat(testStatus2.getRootRelativePath().getPathString())
        .contains("tests/small_test_1/run_2_of_2/test");
  }

  /**
   * Test that test rules always construct with a standard timeout, either
   * inferred from size or explicitly set by attribute.
   */
  @Test
  public void testTestTimeoutFlagOverridesTimeoutDefaultsValues() throws Exception {
    scratch.file("javatests/timeouts/BUILD",
        "java_test(name = 'small_no_timeout',",
        "          srcs = [],",
        "          size = 'small')",
        "java_test(name = 'small_with_timeout',",
        "          srcs = [],",
        "          size = 'small',",
        "          timeout = 'long')");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//javatests/timeouts:small_no_timeout");
    TestRunnerAction testAction = (TestRunnerAction)
        getGeneratingAction(Iterables.get(testStatusList, 0));
    Integer timeout = testAction.getTestProperties().getTimeout().getTimeoutSeconds();
    assertThat(timeout).isEqualTo(TestTimeout.SHORT.getTimeoutSeconds());

    testStatusList = getTestStatusArtifacts("//javatests/timeouts:small_with_timeout");
    testAction = (TestRunnerAction) getGeneratingAction(Iterables.get(testStatusList, 0));
    timeout = testAction.getTestProperties().getTimeout().getTimeoutSeconds();
    assertThat(timeout).isEqualTo(TestTimeout.LONG.getTimeoutSeconds());
  }

  @Test
  public void testRunsPerTestWithSharding() throws Exception {
    useConfiguration("--runs_per_test=2");
    scratch.file(
        "javatests/jt/BUILD",
        "java_test(name = 'RGT',",
        "          shard_count = 10,",
        "          srcs = ['RGT.java'])");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//javatests/jt:RGT");
    assertThat(testStatusList).hasSize(20);
    Artifact testStatus1 = testStatusList.get(0);
    Artifact testStatus10 = testStatusList.get(9);
    Artifact testStatus11 = testStatusList.get(10);
    assertThat(testStatus1).isNotNull();
    assertThat(testStatus10).isNotNull();
    assertThat(testStatus11).isNotNull();
    assertThat(testStatus1.getRootRelativePath().getPathString())
        .contains("javatests/jt/RGT/shard_1_of_10_run_1_of_2/test");
    assertThat(testStatus10.getRootRelativePath().getPathString())
        .contains("javatests/jt/RGT/shard_5_of_10_run_2_of_2/test");
    assertThat(testStatus11.getRootRelativePath().getPathString())
        .contains("javatests/jt/RGT/shard_6_of_10_run_1_of_2/test");
  }

  @Test
  public void testAspectOverNonExpandingTestSuitesVisitsImplicitTests() throws Exception {
    scratch.file(
        "BUILD",
        "sh_test(name = 'test_a',",
        "        srcs = [':a.sh'])",
        "",
        "sh_test(name = 'test_b',",
        "        srcs = [':b.sh'])",
        "",
        "test_suite(name = 'suite'",
        ")");
    writeLabelCollectionAspect();

    useLoadingOptions("--noexpand_test_suites");
    AnalysisResult analysisResult =
        update(
            ImmutableList.of("//:suite"),
            ImmutableList.of("//:aspect.bzl%a"),
            /* keepGoing= */ false,
            /* loadingPhaseThreads= */ 1,
            /* doAnalysis= */ true,
            new EventBus());
    ConfiguredAspect aspectValue =
        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonicalUnchecked("//:aspect.bzl"), "StructImpl");
    StructImpl info = (StructImpl) aspectValue.get(key);
    assertThat(((Depset) info.getValue("labels")).getSet(String.class).toList())
        .containsExactly("@@//:suite", "@@//:test_a", "@@//:test_b");
  }

  @Test
  public void testAspectOverNonExpandingTestSuitesVisitsExplicitTests() throws Exception {
    scratch.file(
        "BUILD",
        "sh_test(name = 'test_a',",
        "        srcs = [':a.sh'])",
        "",
        "sh_test(name = 'test_b',",
        "        srcs = [':b.sh'])",
        "",
        "test_suite(name = 'suite',",
        "           tests = [':test_b']",
        ")");
    writeLabelCollectionAspect();

    useLoadingOptions("--noexpand_test_suites");
    AnalysisResult analysisResult =
        update(
            ImmutableList.of("//:suite"),
            ImmutableList.of("//:aspect.bzl%a"),
            /* keepGoing= */ false,
            /* loadingPhaseThreads= */ 1,
            /* doAnalysis= */ true,
            new EventBus());
    ConfiguredAspect aspectValue =
        Iterables.getOnlyElement(analysisResult.getAspectsMap().values());
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonicalUnchecked("//:aspect.bzl"), "StructImpl");
    StructImpl info = (StructImpl) aspectValue.get(key);
    assertThat(((Depset) info.getValue("labels")).getSet(String.class).toList())
        .containsExactly("@@//:suite", "@@//:test_b");
  }

  @Test
  public void testAspectOverExpandingTestSuitesDoesNotVisitSuite() throws Exception {
    scratch.file(
        "BUILD",
        "sh_test(name = 'test_a',",
        "        srcs = [':a.sh'])",
        "",
        "sh_test(name = 'test_b',",
        "        srcs = [':b.sh'])",
        "",
        "test_suite(name = 'suite',",
        ")");
    writeLabelCollectionAspect();

    AnalysisResult analysisResult =
        update(
            ImmutableList.of("//:suite"),
            ImmutableList.of("//:aspect.bzl%a"),
            /* keepGoing= */ false,
            /* loadingPhaseThreads= */ 1,
            /* doAnalysis= */ true,
            new EventBus());
    final StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonicalUnchecked("//:aspect.bzl"), "StructImpl");

    List<String> labels = new ArrayList<>();
    for (ConfiguredAspect a : analysisResult.getAspectsMap().values()) {
      StructImpl info = (StructImpl) a.get(key);
      labels.addAll(((Depset) info.getValue("labels")).getSet(String.class).toList());
    }
    assertThat(labels).containsExactly("@@//:test_a", "@@//:test_b");
  }

  private void writeLabelCollectionAspect() throws IOException {
    scratch.file(
        "aspect.bzl",
        "StructImpl = provider(fields = ['labels'])",
        "def _impl(target,ctx):",
        "    print(target.label)",
        "    transitive = []",
        "    if hasattr(ctx.rule.attr, 'tests'):",
        "      transitive += [dep[StructImpl].labels for dep in ctx.rule.attr.tests]",
        "    if hasattr(ctx.rule.attr, '_implicit_tests'):",
        "      transitive += [dep[StructImpl].labels for dep in ctx.rule.attr._implicit_tests]",
        "    return [StructImpl(labels = depset([str(target.label)], transitive = transitive))]",
        "",
        "a = aspect(_impl, attr_aspects = ['tests', '_implicit_tests'])");
  }

  /**
   * Regression test for bug {@link "http://b/2644860"}.
   */
  @Test
  public void testIllegalTestSizeAttributeDoesNotCrashTestSuite() throws Exception {
    checkError("bad_size", "illegal_size_test",
        "In rule 'illegal_size_test', size 'bad' is not a valid size",
        "sh_test(name = 'illegal_size_test',",
        "        srcs = ['illegal.sh'],",
        "        size = 'bad')",
        "test_suite(name = 'everything')");
  }

  /**
   * Regression test for bug {@link "http://b/2644860"} but with an illegal Timeout.
   */
  @Test
  public void testIllegalTestTimeoutAttributeDoesNotCrashTestSuite() throws Exception {
    checkError("bad_timeout", "illegal_timeout_test",
        "In rule 'illegal_timeout_test', timeout 'unreasonable' is not a valid timeout",
        "sh_test(name = 'illegal_timeout_test',",
        "        srcs = ['illegal.sh'],",
        "        timeout = 'unreasonable')",
        "test_suite(name = 'everything')");
  }

  /**
   * Overriding the exec group from within the test affects the way exec properties are selected.
   */
  @Test
  public void testOverrideExecGroup() throws Exception {
    scratch.file(
        "some_test.bzl",
        "def _some_test_impl(ctx):",
        "    script = ctx.actions.declare_file(ctx.attr.name + '.sh')",
        "    ctx.actions.write(script, 'shell script goes here', is_executable = True)",
        "    return [",
        "        DefaultInfo(executable = script),",
        "        testing.ExecutionInfo({}, exec_group = 'custom_group'),",
        "    ]",
        "",
        "some_test = rule(",
        "    implementation = _some_test_impl,",
        "    exec_groups = {'custom_group': exec_group()},",
        "    test = True,",
        ")");
    scratch.file(
        "BUILD",
        "load(':some_test.bzl', 'some_test')",
        "some_test(",
        "    name = 'custom_exec_group_test',",
        "    exec_properties = {'test.key': 'bad', 'custom_group.key': 'good'},",
        ")");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//:custom_exec_group_test");
    TestRunnerAction testAction = (TestRunnerAction) getGeneratingAction(testStatusList.get(0));
    ImmutableMap<String, String> executionInfo = testAction.getExecutionInfo();
    assertThat(executionInfo).containsExactly("key", "good");
  }

  /**
   * Overriding the exec group from within the test with --use_target_platform_for_tests.
   *
   * <p>This is the same test as testOverrideExecGroup with --use_target_platform_for_tests and a
   * target platform.
   */
  @Test
  public void testOverrideTestExecGroup() throws Exception {
    useConfiguration("--use_target_platform_for_tests=true", "--platforms=//:linux_aarch64");
    scratch.file(
        "some_test.bzl",
        "def _some_test_impl(ctx):",
        "    script = ctx.actions.declare_file(ctx.attr.name + '.sh')",
        "    ctx.actions.write(script, 'shell script goes here', is_executable = True)",
        "    return [",
        "        DefaultInfo(executable = script),",
        "        testing.ExecutionInfo({}, exec_group = 'custom_group'),",
        "    ]",
        "",
        "some_test = rule(",
        "    implementation = _some_test_impl,",
        "    exec_groups = {'custom_group': exec_group()},",
        "    test = True,",
        ")");
    scratch.file(
        "BUILD",
        "load(':some_test.bzl', 'some_test')",
        "platform(",
        "    name = 'linux_aarch64',",
        "    constraint_values = [",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "os:linux',",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "cpu:aarch64',",
        "    ],",
        ")",
        "some_test(",
        "    name = 'custom_exec_group_test',",
        "    exec_properties = {'test.key': 'bad', 'custom_group.key': 'good'},",
        ")");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//:custom_exec_group_test");
    TestRunnerAction testAction = (TestRunnerAction) getGeneratingAction(testStatusList.get(0));
    ImmutableMap<String, String> executionInfo = testAction.getExecutionInfo();
    assertThat(executionInfo).containsExactly("key", "good");
  }

  /** Adding exec_properties from the platform with --use_target_platform_for_tests. */
  @Test
  public void testTargetTestExecGroup() throws Exception {
    useConfiguration(
        "--use_target_platform_for_tests=true",
        "--platforms=//:linux_aarch64",
        "--host_platform=//:linux_x86");
    scratch.file(
        "some_test.bzl",
        "def _some_test_impl(ctx):",
        "    script = ctx.actions.declare_file(ctx.attr.name + '.sh')",
        "    ctx.actions.write(script, 'shell script goes here', is_executable = True)",
        "    return [",
        "        DefaultInfo(executable = script),",
        "    ]",
        "",
        "some_test = rule(",
        "    implementation = _some_test_impl,",
        "    test = True,",
        ")");
    scratch.file(
        "BUILD",
        "load(':some_test.bzl', 'some_test')",
        "platform(",
        "    name = 'linux_x86',",
        "    constraint_values = [",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "os:linux',",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "cpu:x86_64',",
        "    ],",
        "    exec_properties = {'keyhost': 'bad'},",
        ")",
        "platform(",
        "    name = 'linux_aarch64',",
        "    constraint_values = [",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "os:linux',",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "cpu:aarch64',",
        "    ],",
        "    exec_properties = {'key2': 'good'},",
        ")",
        "some_test(",
        "    name = 'exec_group_test',",
        "    exec_properties = {'key': 'bad'},",
        ")");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//:exec_group_test");
    TestRunnerAction testAction = (TestRunnerAction) getGeneratingAction(testStatusList.get(0));
    assertThat(testAction.getExecutionPlatform().label().getName()).isEqualTo("linux_aarch64");

    ImmutableMap<String, String> executionInfo = testAction.getExecutionInfo();
    assertThat(executionInfo).containsExactly("key2", "good");
  }

  /** Adding test specific exec_properties with --use_target_platform_for_tests. */
  @Test
  @Ignore("https://github.com/bazelbuild/bazel/issues/17466")
  public void testTargetTestExecGroupInheritance() throws Exception {
    useConfiguration(
        "--use_target_platform_for_tests=true",
        "--platforms=//:linux_aarch64",
        "--host_platform=//:linux_x86");
    scratch.file(
        "some_test.bzl",
        "def _some_test_impl(ctx):",
        "    script = ctx.actions.declare_file(ctx.attr.name + '.sh')",
        "    ctx.actions.write(script, 'shell script goes here', is_executable = True)",
        "    return [",
        "        DefaultInfo(executable = script),",
        "    ]",
        "",
        "some_test = rule(",
        "    implementation = _some_test_impl,",
        "    test = True,",
        ")");
    scratch.file(
        "BUILD",
        "load(':some_test.bzl', 'some_test')",
        "platform(",
        "    name = 'linux_x86',",
        "    constraint_values = [",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "os:linux',",
        "       '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "cpu:x86_64',",
        "    ],",
        "    exec_properties = {'keyhost': 'bad'},",
        ")",
        "platform(",
        "    name = 'linux_aarch64',",
        "    constraint_values = [",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "os:linux',",
        "        '" + TestConstants.CONSTRAINTS_PACKAGE_ROOT + "cpu:aarch64',",
        "    ],",
        "    exec_properties = {'key2': 'good'},",
        ")",
        "some_test(",
        "    name = 'exec_group_test',",
        "    exec_properties = {'test.key': 'good', 'key': 'bad'},",
        ")");
    ImmutableList<Artifact.DerivedArtifact> testStatusList =
        getTestStatusArtifacts("//:exec_group_test");
    TestRunnerAction testAction = (TestRunnerAction) getGeneratingAction(testStatusList.get(0));
    assertThat(testAction.getExecutionPlatform().label().getName()).isEqualTo("linux_aarch64");

    ImmutableMap<String, String> executionInfo = testAction.getExecutionInfo();
    assertThat(executionInfo).containsExactly("key2", "good", "key", "good");
  }

  @Test
  public void testNonExecutableCoverageReportGenerator() throws Exception {
    useConfiguration(
        "--coverage_report_generator=//bad_gen:bad_cov_gen", "--collect_code_coverage");
    checkError(
        "bad_gen",
        "some_test",
        "--coverage_report_generator does not refer to an executable target",
        "sh_library(name = 'bad_cov_gen')",
        "cc_test(name = 'some_test')");
  }

  private ImmutableList<Artifact.DerivedArtifact> getTestStatusArtifacts(String label)
      throws Exception {
    ConfiguredTarget target = getConfiguredTarget(label);
    return target.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts();
  }

  private ImmutableList<Artifact.DerivedArtifact> getTestStatusArtifacts(
      TransitiveInfoCollection target) {
    return target.getProvider(TestProvider.class).getTestParams().getTestStatusArtifacts();
  }
}
