Add managed_directories attribute to workspace() function.

This is only a part of the incrementally updated user-owned directory feature; that is why the parsed value is not yet used in computations.

- Under --experimental_allow_incremental_repository_updates flag.
- Parse results are put into WorkspaceFileValue map field.

PiperOrigin-RevId: 244819268
diff --git a/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java b/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
index 08eeec2..7e2df6f 100644
--- a/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
+++ b/src/test/java/com/google/devtools/build/lib/packages/SkylarkSemanticsConsistencyTest.java
@@ -118,6 +118,7 @@
   private static StarlarkSemanticsOptions buildRandomOptions(Random rand) throws Exception {
     return parseOptions(
         // <== Add new options here in alphabetic order ==>
+        "--experimental_allow_incremental_repository_updates=" + rand.nextBoolean(),
         "--experimental_build_setting_api=" + rand.nextBoolean(),
         "--experimental_cc_skylark_api_enabled_packages="
             + rand.nextDouble()
@@ -172,6 +173,7 @@
   private static StarlarkSemantics buildRandomSemantics(Random rand) {
     return StarlarkSemantics.builder()
         // <== Add new options here in alphabetic order ==>
+        .experimentalAllowIncrementalRepositoryUpdates(rand.nextBoolean())
         .experimentalBuildSettingApi(rand.nextBoolean())
         .experimentalCcSkylarkApiEnabledPackages(
             ImmutableList.of(String.valueOf(rand.nextDouble()), String.valueOf(rand.nextDouble())))
diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java
index e8754f7..a69e001 100644
--- a/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/skyframe/WorkspaceFileFunctionTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.actions.FileStateValue;
@@ -32,6 +33,7 @@
 import com.google.devtools.build.lib.packages.PackageFactory.EnvironmentExtension;
 import com.google.devtools.build.lib.packages.Rule;
 import com.google.devtools.build.lib.packages.WorkspaceFileValue;
+import com.google.devtools.build.lib.packages.WorkspaceFileValue.WorkspaceFileKey;
 import com.google.devtools.build.lib.rules.repository.RepositoryDelegatorFunction;
 import com.google.devtools.build.lib.skyframe.SequencedSkyframeExecutor.WorkspaceFileHeaderListener;
 import com.google.devtools.build.lib.skyframe.util.SkyframeExecutorTestUtils;
@@ -44,6 +46,7 @@
 import com.google.devtools.build.lib.vfs.Root;
 import com.google.devtools.build.lib.vfs.RootedPath;
 import com.google.devtools.build.skyframe.EvaluationResult;
+import com.google.devtools.build.skyframe.Injectable;
 import com.google.devtools.build.skyframe.SkyFunction;
 import com.google.devtools.build.skyframe.SkyFunctionName;
 import com.google.devtools.build.skyframe.SkyKey;
@@ -306,6 +309,104 @@
   }
 
   @Test
+  public void testManagedDirectories() throws Exception {
+    PrecomputedValue precomputedValue =
+        (PrecomputedValue)
+            getEnv().getValue(PrecomputedValue.STARLARK_SEMANTICS.getKeyForTesting());
+    StarlarkSemantics semantics =
+        (StarlarkSemantics) Preconditions.checkNotNull(precomputedValue).get();
+    Injectable injectable = getSkyframeExecutor().injectable();
+    try {
+      StarlarkSemantics semanticsWithManagedDirectories =
+          StarlarkSemantics.builderWithDefaults()
+              .experimentalAllowIncrementalRepositoryUpdates(true)
+              .build();
+      PrecomputedValue.STARLARK_SEMANTICS.set(injectable, semanticsWithManagedDirectories);
+
+      WorkspaceFileValue workspaceFileValue =
+          parseWorkspaceFileValue(
+              "workspace(",
+              "  name = 'rr',",
+              "  managed_directories = {'@repo1': ['dir1', 'dir2'], '@repo2': ['dir3/dir1/..']}",
+              ")");
+      ImmutableMap<PathFragment, RepositoryName> managedDirectories =
+          workspaceFileValue.getManagedDirectories();
+      assertThat(managedDirectories).isNotNull();
+      assertThat(managedDirectories).hasSize(3);
+      assertThat(managedDirectories)
+          .containsExactly(
+              PathFragment.create("dir1"), RepositoryName.create("@repo1"),
+              PathFragment.create("dir2"), RepositoryName.create("@repo1"),
+              PathFragment.create("dir3"), RepositoryName.create("@repo2"));
+
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': 'dir1', '@repo2': ['dir3']}",
+          "managed_directories attribute value should be of the type attr.string_list_dict(),"
+              + " mapping repository name to the list of managed directories.");
+
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['dir1'], '@repo2': ['dir1']}",
+          "managed_directories attribute should not contain multiple (or duplicate) repository"
+              + " mappings for the same directory ('dir1').");
+
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['']}", "Expected managed directory path to be non-empty string.");
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['/abc']}",
+          "Expected managed directory path ('/abc') to be relative to the workspace root.");
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['../abc']}",
+          "Expected managed directory path ('../abc') to be under the workspace root.");
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['a/b', 'a/b']}",
+          "managed_directories attribute should not contain multiple (or duplicate)"
+              + " repository mappings for the same directory ('a/b').");
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': [], '@repo1': [] }", "Duplicated key \"@repo1\" when creating dictionary");
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['a/b'], '@repo2': ['a/b/c/..'] }",
+          "managed_directories attribute should not contain multiple (or duplicate)"
+              + " repository mappings for the same directory ('a/b/c/..').");
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['a'], '@repo2': ['a/b'] }",
+          "managed_directories attribute value can not contain nested mappings."
+              + " 'a/b' is a descendant of 'a'.");
+      assertManagedDirectoriesParsingError(
+          "{'@repo1': ['a/b'], '@repo2': ['a'] }",
+          "managed_directories attribute value can not contain nested mappings."
+              + " 'a/b' is a descendant of 'a'.");
+
+      assertManagedDirectoriesParsingError(
+          "{'repo1': []}",
+          "Cannot parse repository name 'repo1'. Repository name should start with '@'.");
+    } finally {
+      PrecomputedValue.STARLARK_SEMANTICS.set(injectable, semantics);
+    }
+  }
+
+  private void assertManagedDirectoriesParsingError(
+      String managedDirectoriesValue, String expectedError)
+      throws IOException, InterruptedException {
+    WorkspaceFileValue workspaceFileValue =
+        parseWorkspaceFileValue(
+            "workspace(",
+            "  name = 'rr',",
+            "  managed_directories = " + managedDirectoriesValue,
+            ")");
+    Package pkg = workspaceFileValue.getPackage();
+    assertThat(pkg.containsErrors()).isTrue();
+    MoreAsserts.assertContainsEvent(pkg.getEvents(), expectedError);
+  }
+
+  private WorkspaceFileValue parseWorkspaceFileValue(String... lines)
+      throws IOException, InterruptedException {
+    RootedPath workspaceFile = createWorkspaceFile(lines);
+    WorkspaceFileKey key = WorkspaceFileValue.key(workspaceFile);
+    EvaluationResult<WorkspaceFileValue> result = eval(key);
+    return result.get(key);
+  }
+
+  @Test
   public void testInvalidRepo() throws Exception {
     RootedPath workspacePath = createWorkspaceFile("workspace(name = 'foo$')");
     PackageValue value =