// Copyright 2022 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.rules.proto;

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.build.lib.actions.util.ActionsTestUtil.prettyArtifactNames;

import com.google.common.truth.Correspondence;
import com.google.devtools.build.lib.actions.ResourceSet;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.analysis.FileProvider;
import com.google.devtools.build.lib.analysis.actions.SpawnAction;
import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.packages.StarlarkInfo;
import com.google.devtools.build.lib.packages.StarlarkProvider;
import com.google.devtools.build.lib.packages.StarlarkProviderIdentifier;
import com.google.devtools.build.lib.packages.util.MockProtoSupport;
import com.google.devtools.build.lib.testutil.TestConstants;
import com.google.devtools.build.lib.util.OS;
import com.google.testing.junit.testparameterinjector.TestParameterInjector;
import com.google.testing.junit.testparameterinjector.TestParameters;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

/** Unit test for proto_common module. */
@RunWith(TestParameterInjector.class)
public class BazelProtoCommonTest extends BuildViewTestCase {
  private static final Correspondence<String, String> MATCHES_REGEX =
      Correspondence.from((a, b) -> Pattern.matches(b, a), "matches");

  private static final StarlarkProviderIdentifier boolProviderId =
      StarlarkProviderIdentifier.forKey(
          new StarlarkProvider.Key(
              Label.parseCanonicalUnchecked("//foo:should_generate.bzl"), "BoolProvider"));

  @Before
  public final void setup() throws Exception {
    MockProtoSupport.setupWorkspace(scratch);
    invalidatePackages();

    MockProtoSupport.setup(mockToolsConfig);

    scratch.file(
        "third_party/x/BUILD",
        "licenses(['unencumbered'])",
        "cc_binary(name = 'plugin', srcs = ['plugin.cc'])",
        "cc_library(name = 'runtime', srcs = ['runtime.cc'])",
        "filegroup(name = 'descriptors', srcs = ['metadata.proto', 'descriptor.proto'])",
        "filegroup(name = 'any', srcs = ['any.proto'])",
        "filegroup(name = 'something', srcs = ['something.proto'])",
        "proto_library(name = 'mixed', srcs = [':descriptors', ':something'])",
        "proto_library(name = 'denied', srcs = [':descriptors', ':any'])");
    scratch.file(
        "foo/BUILD",
        TestConstants.LOAD_PROTO_LANG_TOOLCHAIN,
        "proto_lang_toolchain(",
        "    name = 'toolchain',",
        "    command_line = '--java_out=param1,param2:$(OUT)',",
        "    plugin_format_flag = '--plugin=%s',",
        "    plugin = '//third_party/x:plugin',",
        "    runtime = '//third_party/x:runtime',",
        "    blacklisted_protos = ['//third_party/x:denied'],",
        "    progress_message = 'Progress Message %{label}',",
        "    mnemonic = 'MyMnemonic',",
        "    allowlist_different_package ="
            + " '//tools/allowlists/proto_library_allowlists:lang_proto_library_allowed_in_different_package'",
        ")",
        "proto_lang_toolchain(",
        "    name = 'toolchain_noplugin',",
        "    command_line = '--java_out=param1,param2:$(OUT)',",
        "    runtime = '//third_party/x:runtime',",
        "    blacklisted_protos = ['//third_party/x:denied'],",
        "    progress_message = 'Progress Message %{label}',",
        "    mnemonic = 'MyMnemonic',",
        ")");

    mockToolsConfig.overwrite(
        "tools/allowlists/proto_library_allowlists/BUILD",
        "package_group(",
        "    name='lang_proto_library_allowed_in_different_package',",
        "    packages=['//...', '-//test/...'],",
        ")");

    scratch.file(
        "foo/generate.bzl",
        "def _resource_set_callback(os, inputs_size):",
        "   return {'memory': 25 + 0.15 * inputs_size, 'cpu': 1}",
        "def _impl(ctx):",
        "  outfile = ctx.actions.declare_file('out')",
        "  kwargs = {}",
        "  if ctx.attr.plugin_output == 'single':",
        "    kwargs['plugin_output'] = outfile.path",
        "  elif ctx.attr.plugin_output == 'multiple':",
        "    kwargs['plugin_output'] = ctx.genfiles_dir.path",
        "  elif ctx.attr.plugin_output == 'wrong':",
        "    kwargs['plugin_output'] = ctx.genfiles_dir.path + '///'",
        "  if ctx.attr.additional_args:",
        "    additional_args = ctx.actions.args()",
        "    additional_args.add_all(ctx.attr.additional_args)",
        "    kwargs['additional_args'] = additional_args",
        "  if ctx.files.additional_tools:",
        "    kwargs['additional_tools'] = ctx.files.additional_tools",
        "  if ctx.files.additional_inputs:",
        "    kwargs['additional_inputs'] = depset(ctx.files.additional_inputs)",
        "  if ctx.attr.use_resource_set:",
        "    kwargs['resource_set'] = _resource_set_callback",
        "  if ctx.attr.progress_message:",
        "    kwargs['experimental_progress_message'] = ctx.attr.progress_message",
        "  proto_common_do_not_use.compile(",
        "    ctx.actions,",
        "    ctx.attr.proto_dep[ProtoInfo],",
        "    ctx.attr.toolchain[proto_common_do_not_use.ProtoLangToolchainInfo],",
        "    [outfile],",
        "    **kwargs)",
        "  return [DefaultInfo(files = depset([outfile]))]",
        "generate_rule = rule(_impl,",
        "  attrs = {",
        "     'proto_dep': attr.label(),",
        "     'plugin_output': attr.string(),",
        "     'toolchain': attr.label(default = '//foo:toolchain'),",
        "     'additional_args': attr.string_list(),",
        "     'additional_tools': attr.label_list(cfg = 'exec'),",
        "     'additional_inputs': attr.label_list(allow_files = True),",
        "     'use_resource_set': attr.bool(),",
        "     'progress_message': attr.string(),",
        "  })");

    scratch.file(
        "foo/should_generate.bzl",
        "BoolProvider = provider()",
        "def _impl(ctx):",
        "  result = proto_common_do_not_use.experimental_should_generate_code(",
        "    ctx.attr.proto_dep[ProtoInfo],",
        "    ctx.attr.toolchain[proto_common_do_not_use.ProtoLangToolchainInfo],",
        "    'MyRule',",
        "    ctx.attr.proto_dep.label)",
        "  return [BoolProvider(value = result)]",
        "should_generate_rule = rule(_impl,",
        "  attrs = {",
        "     'proto_dep': attr.label(),",
        "     'toolchain': attr.label(default = '//foo:toolchain'),",
        "  })");

    scratch.file(
        "foo/declare_generated_files.bzl",
        "def _impl(ctx):",
        "  files = proto_common_do_not_use.declare_generated_files(",
        "    ctx.actions,",
        "    ctx.attr.proto_dep[ProtoInfo],",
        "    ctx.attr.extension,",
        "    (lambda s: s.replace('-','_').replace('.','/')) if ctx.attr.python_names else None)",
        "  for f in files:",
        "    ctx.actions.write(f, '')",
        "  return [DefaultInfo(files = depset(files))]",
        "declare_generated_files = rule(_impl,",
        "  attrs = {",
        "     'proto_dep': attr.label(),",
        "     'extension': attr.string(),",
        "     'python_names': attr.bool(default = False),",
        "  })");

    scratch.file(
        "foo/check_collocated.bzl",
        "def _impl(ctx):",
        "  proto_common_do_not_use.check_collocated(",
        "    ctx.label,",
        "    ctx.attr.proto_dep[ProtoInfo],",
        "    ctx.attr.toolchain[proto_common_do_not_use.ProtoLangToolchainInfo])",
        "  return None",
        "check_collocated = rule(_impl,",
        "  attrs = {",
        "     'proto_dep': attr.label(),",
        "     'toolchain': attr.label(default = '//foo:toolchain'),",
        "  })");
  }

  /** Verifies basic usage of <code>proto_common.generate_code</code>. */
  @Test
  public void generateCode_basic() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    List<String> cmdLine = spawnAction.getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin", "-I.", "bar/A.proto")
        .inOrder();
    assertThat(spawnAction.getMnemonic()).isEqualTo("MyMnemonic");
    assertThat(spawnAction.getProgressMessage()).isEqualTo("Progress Message //bar:simple");
  }

  /** Verifies usage of proto_common.generate_code with no plugin specified by toolchain. */
  @Test
  public void generateCode_noPlugin() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto',",
        "  toolchain = '//foo:toolchain_noplugin')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    List<String> cmdLine =
        getGeneratingSpawnAction(getBinArtifact("out", target)).getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly("-I.", "bar/A.proto")
        .inOrder();
  }

  /**
   * Verifies usage of <code>proto_common.generate_code</code> with <code>plugin_output</code>
   * parameter set to file.
   */
  @Test
  public void generateCode_withPluginOutput() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto', plugin_output = 'single')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    List<String> cmdLine =
        getGeneratingSpawnAction(getBinArtifact("out", target)).getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--java_out=param1,param2:bl?azel?-out/k8-fastbuild/bin/bar/out",
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin",
            "-I.",
            "bar/A.proto")
        .inOrder();
  }

  /**
   * Verifies usage of <code>proto_common.generate_code</code> with <code>plugin_output</code>
   * parameter set to directory.
   */
  @Test
  public void generateCode_withDirectoryPluginOutput() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto', plugin_output = 'multiple')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    List<String> cmdLine =
        getGeneratingSpawnAction(getBinArtifact("out", target)).getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--java_out=param1,param2:bl?azel?-out/k8-fastbuild/bin",
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin",
            "-I.",
            "bar/A.proto")
        .inOrder();
  }

  /**
   * Verifies usage of <code>proto_common.generate_code</code> with <code>additional_args</code>
   * parameter.
   */
  @Test
  public void generateCode_additionalArgs() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto', additional_args = ['--a', '--b'])");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    List<String> cmdLine =
        getGeneratingSpawnAction(getBinArtifact("out", target)).getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--a",
            "--b",
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin",
            "-I.",
            "bar/A.proto")
        .inOrder();
  }

  /**
   * Verifies usage of <code>proto_common.generate_code</code> with <code>additional_tools</code>
   * parameter.
   */
  @Test
  public void generateCode_additionalTools() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "cc_binary(name = 'tool1', srcs = ['tool1.cc'])",
        "cc_binary(name = 'tool2', srcs = ['tool2.cc'])",
        "generate_rule(name = 'simple', proto_dep = ':proto',",
        "  additional_tools = [':tool1', ':tool2'])");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    assertThat(prettyArtifactNames(spawnAction.getTools()))
        .containsAtLeast("bar/tool1", "bar/tool2", "third_party/x/plugin");
  }

  /**
   * Verifies usage of <code>proto_common.generate_code</code> with <code>additional_tools</code>
   * parameter and no plugin on the toolchain.
   */
  @Test
  public void generateCode_additionalToolsNoPlugin() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "cc_binary(name = 'tool1', srcs = ['tool1.cc'])",
        "cc_binary(name = 'tool2', srcs = ['tool2.cc'])",
        "generate_rule(name = 'simple',",
        "  proto_dep = ':proto',",
        "  additional_tools = [':tool1', ':tool2'],",
        "  toolchain = '//foo:toolchain_noplugin',",
        ")");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    assertThat(prettyArtifactNames(spawnAction.getTools()))
        .containsAtLeast("bar/tool1", "bar/tool2");
  }

  /**
   * Verifies usage of <code>proto_common.generate_code</code> with <code>additional_inputs</code>
   * parameter.
   */
  @Test
  public void generateCode_additionalInputs() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto',",
        "  additional_inputs = [':input1.txt', ':input2.txt'])");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    assertThat(prettyArtifactNames(spawnAction.getInputs()))
        .containsAtLeast("bar/input1.txt", "bar/input2.txt");
  }

  /**
   * Verifies usage of <code>proto_common.generate_code</code> with <code>resource_set</code>
   * parameter.
   */
  @Test
  public void generateCode_resourceSet() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto', use_resource_set = True)");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    assertThat(spawnAction.getResourceSetOrBuilder().buildResourceSet(OS.DARWIN, 0))
        .isEqualTo(ResourceSet.createWithRamCpu(25, 1));
    assertThat(spawnAction.getResourceSetOrBuilder().buildResourceSet(OS.LINUX, 2))
        .isEqualTo(ResourceSet.createWithRamCpu(25.3, 1));
  }

  /** Verifies <code>--protocopts</code> are passed to command line. */
  @Test
  public void generateCode_protocOpts() throws Exception {
    useConfiguration("--protocopt=--foo", "--protocopt=--bar");
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    List<String> cmdLine = spawnAction.getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin",
            "-I.",
            "--foo",
            "--bar",
            "bar/A.proto")
        .inOrder();
  }

  /**
   * Verifies <code>proto_common.generate_code</code> correctly handles direct generated <code>
   * .proto</code> files.
   */
  @Test
  public void generateCode_directGeneratedProtos() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "genrule(name = 'generate', srcs = ['A.txt'], cmd = '', outs = ['G.proto'])",
        "proto_library(name = 'proto', srcs = ['A.proto', 'G.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    List<String> cmdLine = spawnAction.getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin",
            "-Ibl?azel?-out/k8-fastbuild/bin",
            "-I.",
            "bar/A.proto",
            "bl?azel?-out/k8-fastbuild/bin/bar/G.proto")
        .inOrder();
  }

  /**
   * Verifies <code>proto_common.generate_code</code> correctly handles in-direct generated <code>
   * .proto</code> files.
   */
  @Test
  public void generateCode_inDirectGeneratedProtos() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "genrule(name = 'generate', srcs = ['A.txt'], cmd = '', outs = ['G.proto'])",
        "proto_library(name = 'generated', srcs = ['G.proto'])",
        "proto_library(name = 'proto', srcs = ['A.proto'], deps = [':generated'])",
        "generate_rule(name = 'simple', proto_dep = ':proto')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    List<String> cmdLine = spawnAction.getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin",
            "-Ibl?azel?-out/k8-fastbuild/bin",
            "-I.",
            "bar/A.proto")
        .inOrder();
  }

  /**
   * Verifies <code>proto_common.generate_code</code> correctly handles external <code>proto_library
   * </code>-es.
   */
  @Test
  @TestParameters({
    "{sibling: false, generated: false, expectedFlags:" + " ['-Iexternal/foo']}",
    "{sibling: false, generated: true, expectedFlags:"
        + " ['-Ibl?azel?-out/k8-fastbuild/bin/external/foo']}",
    "{sibling: true, generated: false,expectedFlags:" + " ['-I../foo']}",
    "{sibling: true, generated: true, expectedFlags:" + " ['-Ibl?azel?-out/foo/k8-fastbuild/bin']}",
  })
  public void generateCode_externalProtoLibrary(
      boolean sibling, boolean generated, List<String> expectedFlags) throws Exception {
    if (sibling) {
      setBuildLanguageOptions("--experimental_sibling_repository_layout");
    }
    scratch.appendFile("WORKSPACE", "local_repository(name = 'foo', path = '/foo')");
    invalidatePackages();
    scratch.file("/foo/WORKSPACE");
    scratch.file(
        "/foo/e/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "proto_library(name='e', srcs=['E.proto'])",
        generated
            ? "genrule(name = 'generate', srcs = ['A.txt'], cmd = '', outs = ['E.proto'])"
            : "");
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'], deps = ['@foo//e:e'])",
        "generate_rule(name = 'simple', proto_dep = ':proto')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    List<String> cmdLine = spawnAction.getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin",
            expectedFlags.get(0),
            "-I.",
            "bar/A.proto")
        .inOrder();
  }

  /** Verifies <code>experimental_progress_message</code> parameters. */
  @Test
  public void generateCode_overrideProgressMessage() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:generate.bzl', 'generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "generate_rule(name = 'simple', proto_dep = ':proto', progress_message = 'My %{label}')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    SpawnAction spawnAction = getGeneratingSpawnAction(getBinArtifact("out", target));
    List<String> cmdLine = spawnAction.getRemainingArguments();
    assertThat(cmdLine)
        .comparingElementsUsing(MATCHES_REGEX)
        .containsExactly(
            "--plugin=bl?azel?-out/[^/]*-exec-[^/]*/bin/third_party/x/plugin", "-I.", "bar/A.proto")
        .inOrder();
    assertThat(spawnAction.getMnemonic()).isEqualTo("MyMnemonic");
    assertThat(spawnAction.getProgressMessage()).isEqualTo("My //bar:simple");
  }

  /** Verifies <code>proto_common.should_generate_code</code> call. */
  @Test
  public void shouldGenerateCode_basic() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:should_generate.bzl', 'should_generate_rule')",
        "proto_library(name = 'proto', srcs = ['A.proto'])",
        "should_generate_rule(name = 'simple', proto_dep = ':proto')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    StarlarkInfo boolProvider = (StarlarkInfo) target.get(boolProviderId);
    assertThat(boolProvider.getValue("value", Boolean.class)).isTrue();
  }

  /** Verifies <code>proto_common.should_generate_code</code> call. */
  @Test
  public void shouldGenerateCode_dontGenerate() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:should_generate.bzl', 'should_generate_rule')",
        "should_generate_rule(name = 'simple', proto_dep = '//third_party/x:denied')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    StarlarkInfo boolProvider = (StarlarkInfo) target.get(boolProviderId);
    assertThat(boolProvider.getValue("value", Boolean.class)).isFalse();
  }

  /** Verifies <code>proto_common.should_generate_code</code> call. */
  @Test
  public void shouldGenerateCode_mixed() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:should_generate.bzl', 'should_generate_rule')",
        "should_generate_rule(name = 'simple', proto_dep = '//third_party/x:mixed')");

    reporter.removeHandler(failFastHandler);
    getConfiguredTarget("//bar:simple");

    assertContainsEvent(
        "The 'srcs' attribute of '@//third_party/x:mixed' contains protos for which 'MyRule'"
            + " shouldn't generate code (third_party/x/metadata.proto,"
            + " third_party/x/descriptor.proto), in addition to protos for which it should"
            + " (third_party/x/something.proto).\n"
            + "Separate '@//third_party/x:mixed' into 2 proto_library rules.");
  }

  /** Verifies <code>proto_common.declare_generated_files</code> call. */
  @Test
  public void declareGenerateFiles_basic() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:declare_generated_files.bzl', 'declare_generated_files')",
        "proto_library(name = 'proto', srcs = ['A.proto', 'b/B.proto'])",
        "declare_generated_files(name = 'simple', proto_dep = ':proto', extension = '.cc')");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    assertThat(prettyArtifactNames(target.getProvider(FileProvider.class).getFilesToBuild()))
        .containsExactly("bar/A.cc", "bar/b/B.cc");
  }

  /** Verifies <code>proto_common.declare_generated_files</code> call for Python. */
  @Test
  public void declareGenerateFiles_pythonc() throws Exception {
    scratch.file(
        "bar/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "load('//foo:declare_generated_files.bzl', 'declare_generated_files')",
        "proto_library(name = 'proto', srcs = ['my-proto.gen.proto'])",
        "declare_generated_files(name = 'simple', proto_dep = ':proto', extension = '_pb2.py',",
        "  python_names = True)");

    ConfiguredTarget target = getConfiguredTarget("//bar:simple");

    assertThat(prettyArtifactNames(target.getProvider(FileProvider.class).getFilesToBuild()))
        .containsExactly("bar/my_proto/gen_pb2.py");
  }

  @Test
  public void langProtoLibrary_inDifferentPackage_allowed() throws Exception {
    scratch.file(
        "proto/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "proto_library(name = 'proto', srcs = ['A.proto'])");
    scratch.file(
        "bar/BUILD",
        "load('//foo:check_collocated.bzl', 'check_collocated')",
        "check_collocated(name = 'simple', proto_dep = '//proto:proto')");

    getConfiguredTarget("//bar:simple");

    assertNoEvents();
  }

  @Test
  public void langProtoLibrary_inDifferentPackage_fails() throws Exception {
    scratch.file(
        "proto/BUILD",
        TestConstants.LOAD_PROTO_LIBRARY,
        "proto_library(name = 'proto', srcs = ['A.proto'])");
    scratch.file(
        "test/BUILD",
        "load('//foo:check_collocated.bzl', 'check_collocated')",
        "check_collocated(name = 'simple', proto_dep = '//proto:proto')");

    reporter.removeHandler(failFastHandler);
    getConfiguredTarget("//test:simple");

    assertContainsEvent(
        "lang_proto_library '@//test:simple' may only be created in the same package as"
            + " proto_library '@//proto:proto'");
  }

  @Test
  public void langProtoLibrary_exportNotAllowed() throws Exception {
    scratch.file(
        "x/BUILD",
        "proto_library(name='foo', srcs=['foo.proto'], allow_exports = ':test')",
        "package_group(",
        "    name='test',",
        "    packages=['//allowed'],",
        ")");
    scratch.file(
        "notallowed/BUILD",
        "load('//foo:check_collocated.bzl', 'check_collocated')",
        "check_collocated(name = 'simple', proto_dep = '//x:foo')");

    reporter.removeHandler(failFastHandler);
    getConfiguredTarget("//notallowed:simple");

    assertContainsEvent(
        "lang_proto_library '@//notallowed:simple' may only be created in the same package as"
            + " proto_library '@//x:foo'");
  }

  @Test
  public void langProtoLibrary_exportAllowed() throws Exception {
    scratch.file(
        "x/BUILD",
        "proto_library(name='foo', srcs=['foo.proto'], allow_exports = ':test')",
        "package_group(",
        "    name='test',",
        "    packages=['//allowed'],",
        ")");
    scratch.file(
        "allowed/BUILD",
        "load('//foo:check_collocated.bzl', 'check_collocated')",
        "check_collocated(name = 'simple', proto_dep = '//x:foo')");

    getConfiguredTarget("//allowed:simple");

    assertNoEvents();
  }
}
