// Copyright 2017 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.packages;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.devtools.build.lib.collect.nestedset.Order.STABLE_ORDER;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyNoInteractions;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.testing.EqualsTester;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.collect.nestedset.Depset;
import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
import java.util.Optional;
import net.starlark.java.eval.Dict;
import net.starlark.java.eval.EvalException;
import net.starlark.java.eval.Mutability;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkCallable;
import net.starlark.java.eval.StarlarkInt;
import net.starlark.java.eval.StarlarkList;
import net.starlark.java.eval.StarlarkSemantics;
import net.starlark.java.eval.StarlarkThread;
import net.starlark.java.eval.Tuple;
import net.starlark.java.syntax.Location;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link StarlarkProvider}. */
@RunWith(JUnit4.class)
public final class StarlarkProviderTest {

  @Test
  public void unexportedProvider_accessors() {
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build();
    assertThat(provider.isExported()).isFalse();
    assertThat(provider.getName()).isEqualTo("<no name>");
    assertThat(provider.getPrintableName()).isEqualTo("<no name>");
    assertThat(provider.createRawConstructor().getName()).isEqualTo("<raw constructor>");
    assertThat(provider.getErrorMessageForUnknownField("foo"))
        .isEqualTo("'struct' value has no field or method 'foo'");
    assertThat(provider.isImmutable()).isFalse();
    assertThat(Starlark.repr(provider)).isEqualTo("<provider>");
    assertThrows(IllegalStateException.class, provider::getKey);
  }

  @Test
  public void exportedProvider_accessors() throws Exception {
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonical("//foo:bar.bzl"), "prov");
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setExported(key).build();
    assertThat(provider.isExported()).isTrue();
    assertThat(provider.getName()).isEqualTo("prov");
    assertThat(provider.getPrintableName()).isEqualTo("prov");
    assertThat(provider.createRawConstructor().getName()).isEqualTo("<raw constructor for prov>");
    assertThat(provider.getErrorMessageForUnknownField("foo"))
        .isEqualTo("'prov' value has no field or method 'foo'");
    assertThat(provider.isImmutable()).isTrue();
    assertThat(Starlark.repr(provider)).isEqualTo("<provider>");
    assertThat(provider.getKey()).isEqualTo(key);
  }

  @Test
  public void basicInstantiation() throws Exception {
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build();
    StarlarkInfo infoFromNormalConstructor = instantiateWithA1B2C3(provider);
    assertHasExactlyValuesA1B2C3(infoFromNormalConstructor);
    assertThat(infoFromNormalConstructor.getProvider()).isEqualTo(provider);

    StarlarkInfo infoFromRawConstructor = instantiateWithA1B2C3(provider.createRawConstructor());
    assertHasExactlyValuesA1B2C3(infoFromRawConstructor);
    assertThat(infoFromRawConstructor.getProvider()).isEqualTo(provider);

    assertThat(infoFromNormalConstructor).isEqualTo(infoFromRawConstructor);
  }

  @Test
  public void instantiationWithInit() throws Exception {
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setInit(initBC).build();
    StarlarkInfo infoFromNormalConstructor = instantiateWithA1(provider);
    assertHasExactlyValuesA1B2C3(infoFromNormalConstructor);
    assertThat(infoFromNormalConstructor.getProvider()).isEqualTo(provider);
  }

  @Test
  public void instantiationWithInitSignatureMismatch_fails() throws Exception {
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setInit(initBC).build();
    EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider));
    assertThat(e).hasMessageThat().contains("expected a single `a` argument");
  }

  @Test
  public void instantiationWithInitReturnTypeMismatch_fails() throws Exception {
    StarlarkCallable initWithInvalidReturnType =
        new StarlarkCallable() {
          @Override
          public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs) {
            return "invalid";
          }

          @Override
          public String getName() {
            return "initWithInvalidReturnType";
          }

          @Override
          public Location getLocation() {
            return Location.BUILTIN;
          }
        };

    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN).setInit(initWithInvalidReturnType).build();
    EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider));
    assertThat(e)
        .hasMessageThat()
        .contains("got string for 'return value of provider init()', want dict");
  }

  @Test
  public void instantiationWithFailingInit_fails() throws Exception {
    StarlarkCallable failingInit =
        new StarlarkCallable() {
          @Override
          public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs)
              throws EvalException {
            throw Starlark.errorf("failingInit fails");
          }

          @Override
          public String getName() {
            return "failingInit";
          }

          @Override
          public Location getLocation() {
            return Location.BUILTIN;
          }
        };

    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN).setInit(failingInit).build();
    EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider));
    assertThat(e).hasMessageThat().contains("failingInit fails");
  }

  @Test
  public void rawConstructorBypassesInit() throws Exception {
    StarlarkCallable init = mock(StarlarkCallable.class, "init");
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).setInit(init).build();
    StarlarkInfo infoFromRawConstructor = instantiateWithA1B2C3(provider.createRawConstructor());
    assertHasExactlyValuesA1B2C3(infoFromRawConstructor);
    assertThat(infoFromRawConstructor.getProvider()).isEqualTo(provider);
    verifyNoInteractions(init);
  }

  @Test
  public void basicInstantiationWithSchemaWithSomeFieldsUnset() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a", "b", "c"))
            .build();
    StarlarkInfo infoFromNormalConstructor = instantiateWithA1(provider);
    assertHasExactlyValuesA1(infoFromNormalConstructor);
    StarlarkInfo infoFromRawConstructor = instantiateWithA1(provider.createRawConstructor());
    assertHasExactlyValuesA1(infoFromRawConstructor);
  }

  @Test
  public void basicInstantiationWithSchemaWithAllFieldsSet() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a", "b", "c"))
            .build();
    StarlarkInfo infoFromNormalConstructor = instantiateWithA1B2C3(provider);
    assertHasExactlyValuesA1B2C3(infoFromNormalConstructor);
    StarlarkInfo infoFromRawConstructor = instantiateWithA1B2C3(provider.createRawConstructor());
    assertHasExactlyValuesA1B2C3(infoFromRawConstructor);
  }

  @Test
  public void basicInstantiationWithDocumentedSchema() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableMap.of("a", "Parameter a", "b", "Parameter b", "c", "Parameter c"))
            .build();
    StarlarkInfo infoFromNormalConstructor = instantiateWithA1(provider);
    assertHasExactlyValuesA1(infoFromNormalConstructor);
    StarlarkInfo infoFromRawConstructor = instantiateWithA1B2C3(provider.createRawConstructor());
    assertHasExactlyValuesA1B2C3(infoFromRawConstructor);
  }

  @Test
  public void schemaDisallowsUnexpectedFields() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN).setSchema(ImmutableList.of("a", "b")).build();
    EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider));
    assertThat(e)
        .hasMessageThat()
        .contains("got unexpected field 'c' in call to instantiate provider");
  }

  @Test
  public void documentedSchemaDisallowsUnexpectedFields() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableMap.of("a", "Parameter a", "b", "Parameter b"))
            .build();
    EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1B2C3(provider));
    assertThat(e)
        .hasMessageThat()
        .contains("got unexpected field 'c' in call to instantiate provider");
  }

  @Test
  public void schemaEnforcedOnRawConstructor() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN).setSchema(ImmutableList.of("a", "b")).build();
    EvalException e =
        assertThrows(
            EvalException.class, () -> instantiateWithA1B2C3(provider.createRawConstructor()));
    assertThat(e)
        .hasMessageThat()
        .contains("got unexpected field 'c' in call to instantiate provider");
  }

  @Test
  public void schemaEnforcedOnInit() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a", "b"))
            .setInit(initBC)
            .build();
    EvalException e = assertThrows(EvalException.class, () -> instantiateWithA1(provider));
    assertThat(e)
        .hasMessageThat()
        .contains("got unexpected field 'c' in call to instantiate provider");
  }

  @Test
  public void documentedProvider_getDocumentation() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN).setDocumentation("My doc string").build();
    assertThat(provider.getDocumentation()).hasValue("My doc string");
  }

  @Test
  public void undocumentedProvider_getDocumentation() throws Exception {
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build();
    assertThat(provider.getDocumentation()).isEmpty();
  }

  @Test
  public void schemalessProvider_getFields() throws Exception {
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build();
    assertThat(provider.getFields()).isNull();
  }

  @Test
  public void schemalessProvider_getSchema() throws Exception {
    StarlarkProvider provider = StarlarkProvider.builder(Location.BUILTIN).build();
    assertThat(provider.getSchema()).isNull();
  }

  @Test
  public void providerWithUndocumentedSchema_getFields() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            // Note fields in setSchema() call below are not alphabetized to simulate
            // non-alphabetized field order in a provider declaration in Starlark code.
            .setSchema(ImmutableList.of("c", "a", "b"))
            .build();
    assertThat(provider.getFields()).containsExactly("a", "b", "c").inOrder();
  }

  @Test
  public void providerWithUndocumentedSchema_getSchema() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            // Note fields in setSchema() call below are not alphabetized to simulate
            // non-alphabetized field order in a provider declaration in Starlark code.
            .setSchema(ImmutableList.of("c", "a", "b"))
            .build();
    assertThat(provider.getSchema())
        .containsExactly("c", Optional.empty(), "a", Optional.empty(), "b", Optional.empty())
        .inOrder();
  }

  @Test
  public void providerWithDocumentedSchema_getFields() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            // Note fields in setSchema() call below are not alphabetized to simulate
            // non-alphabetized field order in a provider declaration in Starlark code.
            .setSchema(ImmutableMap.of("c", "Parameter c", "a", "Parameter a", "b", "Parameter b"))
            .build();
    assertThat(provider.getFields()).containsExactly("a", "b", "c").inOrder();
  }

  @Test
  public void providerWithDocumentedSchema_getSchema() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            // Note fields in setSchema() call below are not alphabetized to simulate
            // non-alphabetized field order in a provider declaration in Starlark code.
            .setSchema(ImmutableMap.of("c", "Parameter c", "a", "Parameter a", "b", "Parameter b"))
            .build();
    assertThat(provider.getSchema())
        .containsExactly(
            "c",
            Optional.of("Parameter c"),
            "a",
            Optional.of("Parameter a"),
            "b",
            Optional.of("Parameter b"))
        .inOrder();
  }

  /**
   * Tests the safe storage and retrieval of depsets, which may be optimized to nested sets in the
   * internal representation.
   */
  @Test
  public void schemafulProvider_withDepset() throws Exception {
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN).setSchema(ImmutableList.of("field")).build();
    StarlarkInfo instance1;
    StarlarkInfo instance2;
    StarlarkInfo instance3;
    StarlarkInfo instance4;
    StarlarkInfo instance5;
    StarlarkInfo instance6;
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      // Instantiates provider with values of different types all in the same field.
      // Instance with an empty depset of string
      instance1 =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of(
                      "field", Depset.of(String.class, NestedSetBuilder.emptySet(STABLE_ORDER))));
      // Instance with a non-empty depset of string
      instance2 =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of(
                      "field",
                      Depset.of(String.class, NestedSetBuilder.create(STABLE_ORDER, "foo"))));
      // Instance with a non-empty depset of int
      instance3 =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of(
                      "field",
                      Depset.of(
                          StarlarkInt.class,
                          NestedSetBuilder.create(STABLE_ORDER, StarlarkInt.of(1)))));
      // Instance with a string (not a depset)
      instance4 =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of("field", "foo"));
      // Instance with a None
      instance5 =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of("field", Starlark.NONE));
      // Instance with the field not set
      instance6 =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of());
    }

    assertThat(instance1.getValue("field")).isInstanceOf(Depset.class);
    assertThat(((Depset) instance1.getValue("field")).isEmpty()).isTrue();
    assertThat(instance2.getValue("field")).isInstanceOf(Depset.class);
    assertThat(((Depset) instance2.getValue("field")).getElementClass()).isEqualTo(String.class);
    assertThat(((Depset) instance2.getValue("field")).toList()).containsExactly("foo");
    assertThat(instance3.getValue("field")).isInstanceOf(Depset.class);
    assertThat(((Depset) instance3.getValue("field")).getElementClass())
        .isEqualTo(StarlarkInt.class);
    assertThat(((Depset) instance3.getValue("field")).toList()).containsExactly(StarlarkInt.of(1));
    assertThat(instance4.getValue("field")).isEqualTo("foo");
    assertThat(instance5.getValue("field")).isEqualTo(Starlark.NONE);
    assertThat(instance6.getValue("field")).isNull();
  }

  @Test
  public void schemafulProvider_mutable() throws Exception {
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonical("//foo:bar.bzl"), "prov");
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a"))
            .setExported(key)
            .build();
    StarlarkInfo instance;
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      instance =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of("a", StarlarkList.of(mu, "x")));
      @SuppressWarnings("unchecked")
      StarlarkList<String> list = (StarlarkList<String>) instance.getValue("a");

      list.addElement("y"); // verifies the fields of the provider instance are mutable
      assertThat(instance.isImmutable()).isFalse();
    }

    @SuppressWarnings("unchecked")
    StarlarkList<String> list = (StarlarkList<String>) instance.getValue("a");
    assertThat((Iterable<?>) list).containsExactly("x", "y");
  }

  @Test
  public void schemafulProvider_immutable() throws Exception {
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonical("//foo:bar.bzl"), "prov");
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a"))
            .setExported(key)
            .build();
    StarlarkInfo instance;
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      instance =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of("a", StarlarkList.of(mu, "x")));
    }

    assertThat(instance.isImmutable()).isTrue();
    @SuppressWarnings("unchecked")
    StarlarkList<String> list = (StarlarkList<String>) instance.getValue("a");
    assertThat((Iterable<?>) list).containsExactly("x");
    // verifies the fields of the frozen provider instance are immutable
    EvalException e = assertThrows(EvalException.class, () -> list.addElement("y"));
    assertThat(e).hasMessageThat().contains("trying to mutate a frozen list value");
  }

  @Test
  public void schemafulProviderWithDepset_isImmutable() throws Exception {
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonical("//foo:bar.bzl"), "prov");
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a"))
            .setExported(key)
            .build();
    StarlarkInfo instance;
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      instance =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of(
                      "a", Depset.of(String.class, NestedSetBuilder.create(STABLE_ORDER, "foo"))));

      assertThat(instance.isImmutable()).isTrue();
    }
  }

  @Test
  public void schemafulProviderWithDepset_becomesImmutable() throws Exception {
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonical("//foo:bar.bzl"), "prov");
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a", "b"))
            .setExported(key)
            .build();
    StarlarkInfo instance;
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      instance =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of(
                      "a",
                      Depset.of(String.class, NestedSetBuilder.create(STABLE_ORDER, "foo")),
                      "b",
                      StarlarkList.of(mu, "x")));

      assertThat(instance.isImmutable()).isFalse();
    }

    assertThat(instance.isImmutable()).isTrue();
  }

  @Test
  public void schemafulProvider_optimisedImmutable() throws Exception {
    StarlarkProvider.Key key =
        new StarlarkProvider.Key(Label.parseCanonical("//foo:bar.bzl"), "prov");
    StarlarkProvider provider =
        StarlarkProvider.builder(Location.BUILTIN)
            .setSchema(ImmutableList.of("a"))
            .setExported(key)
            .build();
    StarlarkInfo instance;
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      instance =
          (StarlarkInfo)
              Starlark.call(
                  thread,
                  provider,
                  /* args= */ ImmutableList.of(),
                  /* kwargs= */ ImmutableMap.of("a", StarlarkList.of(mu, "x")));
    }
    instance = instance.unsafeOptimizeMemoryLayout();

    assertThat(instance.isImmutable()).isTrue();
    @SuppressWarnings("unchecked")
    StarlarkList<String> list = (StarlarkList<String>) instance.getValue("a");
    assertThat((Iterable<?>) list).containsExactly("x");

    // verifies the fields of the frozen and optimised provider instance are immutable
    EvalException e = assertThrows(EvalException.class, () -> list.addElement("y"));
    assertThat(e).hasMessageThat().contains("trying to mutate a frozen list value");
  }

  @Test
  public void providerEquals() throws Exception {
    // All permutations of differing label and differing name.
    StarlarkProvider.Key keyFooA =
        new StarlarkProvider.Key(Label.parseCanonical("//foo.bzl"), "provA");
    StarlarkProvider.Key keyFooB =
        new StarlarkProvider.Key(Label.parseCanonical("//foo.bzl"), "provB");
    StarlarkProvider.Key keyBarA =
        new StarlarkProvider.Key(Label.parseCanonical("//bar.bzl"), "provA");
    StarlarkProvider.Key keyBarB =
        new StarlarkProvider.Key(Label.parseCanonical("//bar.bzl"), "provB");

    // 1 for each key, plus a duplicate for one of the keys, plus 2 that have no key.
    StarlarkProvider provFooA1 =
        StarlarkProvider.builder(Location.BUILTIN).setExported(keyFooA).build();
    StarlarkProvider provFooA2 =
        StarlarkProvider.builder(Location.BUILTIN).setExported(keyFooA).build();
    StarlarkProvider provFooB =
        StarlarkProvider.builder(Location.BUILTIN).setExported(keyFooB).build();
    StarlarkProvider provBarA =
        StarlarkProvider.builder(Location.BUILTIN).setExported(keyBarA).build();
    StarlarkProvider provBarB =
        StarlarkProvider.builder(Location.BUILTIN).setExported(keyBarB).build();
    StarlarkProvider provUnexported1 = StarlarkProvider.builder(Location.BUILTIN).build();
    StarlarkProvider provUnexported2 = StarlarkProvider.builder(Location.BUILTIN).build();

    // For exported providers, different keys -> unequal, same key -> equal. For unexported
    // providers it comes down to object identity.
    new EqualsTester()
        .addEqualityGroup(provFooA1, provFooA2)
        .addEqualityGroup(provFooB)
        .addEqualityGroup(provBarA, provBarA) // reflexive equality (exported)
        .addEqualityGroup(provBarB)
        .addEqualityGroup(provUnexported1, provUnexported1) // reflexive equality (unexported)
        .addEqualityGroup(provUnexported2)
        .testEquals();
  }

  /** Custom init equivalent to `def initBC(a): return {a:a, b:a*2, c:a*3}` */
  private static final StarlarkCallable initBC =
      new StarlarkCallable() {
        @Override
        public Object call(StarlarkThread thread, Tuple args, Dict<String, Object> kwargs)
            throws EvalException {
          if (!args.isEmpty()) {
            throw Starlark.errorf("unexpected positional arguments");
          }
          if (kwargs.size() != 1 || !kwargs.containsKey("a")) {
            throw Starlark.errorf("expected a single `a` argument");
          }
          StarlarkInt a = (StarlarkInt) kwargs.get("a");
          return Dict.builder()
              .put("a", a)
              .put("b", StarlarkInt.of(a.toIntUnchecked() * 2))
              .put("c", StarlarkInt.of(a.toIntUnchecked() * 3))
              .build(Mutability.IMMUTABLE);
        }

        @Override
        public String getName() {
          return "initBC";
        }

        @Override
        public Location getLocation() {
          return Location.BUILTIN;
        }
      };

  /** Instantiates a {@link StarlarkInfo} with fields a=1 (and nothing else). */
  private static StarlarkInfo instantiateWithA1(StarlarkCallable provider) throws Exception {
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      Object result =
          Starlark.call(
              thread,
              provider,
              /*args=*/ ImmutableList.of(),
              /*kwargs=*/ ImmutableMap.of("a", StarlarkInt.of(1)));
      assertThat(result).isInstanceOf(StarlarkInfo.class);
      return (StarlarkInfo) result;
    }
  }

  /** Instantiates a {@link StarlarkInfo} with fields a=1, b=2, c=3 (and nothing else). */
  private static StarlarkInfo instantiateWithA1B2C3(StarlarkCallable provider) throws Exception {
    try (Mutability mu = Mutability.create()) {
      StarlarkThread thread = new StarlarkThread(mu, StarlarkSemantics.DEFAULT);
      Object result =
          Starlark.call(
              thread,
              provider,
              /*args=*/ ImmutableList.of(),
              /*kwargs=*/ ImmutableMap.of(
                  "a", StarlarkInt.of(1), "b", StarlarkInt.of(2), "c", StarlarkInt.of(3)));
      assertThat(result).isInstanceOf(StarlarkInfo.class);
      return (StarlarkInfo) result;
    }
  }

  /** Asserts that a {@link StarlarkInfo} has field a=1 (and nothing else). */
  private static void assertHasExactlyValuesA1(StarlarkInfo info) throws Exception {
    assertThat(info.getFieldNames()).containsExactly("a");
    assertThat(info.getValue("a")).isEqualTo(StarlarkInt.of(1));
  }

  /** Asserts that a {@link StarlarkInfo} has fields a=1, b=2, c=3 (and nothing else). */
  private static void assertHasExactlyValuesA1B2C3(StarlarkInfo info) throws Exception {
    assertThat(info.getFieldNames()).containsExactly("a", "b", "c");
    assertThat(info.getValue("a")).isEqualTo(StarlarkInt.of(1));
    assertThat(info.getValue("b")).isEqualTo(StarlarkInt.of(2));
    assertThat(info.getValue("c")).isEqualTo(StarlarkInt.of(3));
  }
}
