Refactor some py_runtime code

Moved its configured target factory, provider, and test to the ...lib/rules/python directory instead of ...lib/bazel/rules/python. Simplified the test. Also moved BazelPythonConfigurationTest into the bazel dir.

The `files` attribute of py_runtime is no longer mandatory. Since a non-empty value is not permitted when `interpreter_path` is used, it makes more sense to omit it rather than explicitly set it to empty.

The error messages and rule logic are simplified. The provider now has an invariant that either interpreter is null or else interpreterPath and files are null; this will be clarified in a follow-up CL that exposes PyRuntimeProvider to Starlark.

Work toward #7375.

PiperOrigin-RevId: 234174755
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index fa0c6af..1615493 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -1141,6 +1141,7 @@
         "//src/main/java/com/google/devtools/common/options",
         "//src/main/protobuf:crosstool_config_java_proto",
         "//src/main/protobuf:extra_actions_base_java_proto",
+        "//third_party:auto_value",
         "//third_party:guava",
         "//third_party:jsr305",
         "//third_party/protobuf:protobuf_java",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntime.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntime.java
deleted file mode 100644
index 4c96dbe..0000000
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntime.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// 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.bazel.rules.python;
-
-import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
-import com.google.devtools.build.lib.analysis.ConfiguredTarget;
-import com.google.devtools.build.lib.analysis.PrerequisiteArtifacts;
-import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
-import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory;
-import com.google.devtools.build.lib.analysis.RuleContext;
-import com.google.devtools.build.lib.analysis.RunfilesProvider;
-import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
-import com.google.devtools.build.lib.collect.nestedset.NestedSet;
-import com.google.devtools.build.lib.collect.nestedset.NestedSetBuilder;
-import com.google.devtools.build.lib.syntax.Type;
-import com.google.devtools.build.lib.vfs.PathFragment;
-
-/**
- * Implementation for the {@code py_runtime} rule.
- */
-public final class BazelPyRuntime implements RuleConfiguredTargetFactory {
-
-  @Override
-  public ConfiguredTarget create(RuleContext ruleContext)
-      throws InterruptedException, RuleErrorException, ActionConflictException {
-    NestedSet<Artifact> files =
-        PrerequisiteArtifacts.nestedSet(ruleContext, "files", Mode.TARGET);
-    Artifact interpreter =
-        ruleContext.getPrerequisiteArtifact("interpreter", Mode.TARGET);
-    String interpreterPath =
-        ruleContext.attributes().get("interpreter_path", Type.STRING);
-
-    NestedSet<Artifact> all = NestedSetBuilder.<Artifact>stableOrder()
-        .addTransitive(files)
-        .build();
-
-    if (interpreter != null && !interpreterPath.isEmpty()) {
-      ruleContext.ruleError("interpreter and interpreter_path cannot be set at the same time.");
-    }
-
-    if (interpreter == null && interpreterPath.isEmpty()) {
-      ruleContext.ruleError("interpreter and interpreter_path cannot be empty at the same time.");
-    }
-
-    if (!interpreterPath.isEmpty() && !PathFragment.create(interpreterPath).isAbsolute()) {
-      ruleContext.attributeError("interpreter_path", "must be an absolute path.");
-    }
-
-    if (!interpreterPath.isEmpty() && !files.isEmpty()) {
-      ruleContext.ruleError("interpreter with an absolute path requires files to be empty.");
-    }
-
-    if (ruleContext.hasErrors()) {
-      return null;
-    }
-
-    BazelPyRuntimeProvider provider = BazelPyRuntimeProvider
-        .create(files, interpreter, interpreterPath);
-
-    return new RuleConfiguredTargetBuilder(ruleContext)
-        .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
-        .setFilesToBuild(all)
-        .addProvider(BazelPyRuntimeProvider.class, provider)
-        .build();
-  }
-
-}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeProvider.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeProvider.java
deleted file mode 100644
index e94afad..0000000
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeProvider.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// 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.bazel.rules.python;
-
-import com.google.auto.value.AutoValue;
-import com.google.devtools.build.lib.actions.Artifact;
-import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
-import com.google.devtools.build.lib.collect.nestedset.NestedSet;
-import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
-import javax.annotation.Nullable;
-
-/** Information about the Python runtime used by the <code>py_*</code> rules. */
-@AutoValue
-@Immutable
-public abstract class BazelPyRuntimeProvider implements TransitiveInfoProvider {
-  public static BazelPyRuntimeProvider create(
-      NestedSet<Artifact> files,
-      @Nullable Artifact interpreter,
-      @Nullable String interpreterPath) {
-
-    return new AutoValue_BazelPyRuntimeProvider(files, interpreter, interpreterPath);
-  }
-
-  public abstract NestedSet<Artifact> files();
-
-  @Nullable
-  public abstract Artifact interpreter();
-
-  @Nullable
-  public abstract String interpreterPath();
-
-}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeRule.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeRule.java
index 37cf1f6..44e064f 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeRule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeRule.java
@@ -24,10 +24,11 @@
 import com.google.devtools.build.lib.analysis.RuleDefinition;
 import com.google.devtools.build.lib.analysis.RuleDefinitionEnvironment;
 import com.google.devtools.build.lib.packages.RuleClass;
+import com.google.devtools.build.lib.rules.python.PyRuntime;
 import com.google.devtools.build.lib.rules.python.PythonConfiguration;
 import com.google.devtools.build.lib.util.FileTypeSet;
 
-/** Rule definition for {@code python_runtime} */
+/** Rule definition for {@code py_runtime} */
 public final class BazelPyRuntimeRule implements RuleDefinition {
 
   @Override
@@ -38,24 +39,19 @@
         /* <!-- #BLAZE_RULE(py_runtime).ATTRIBUTE(files) -->
         The set of files comprising this Python runtime.
         <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
-        .add(attr("files", LABEL_LIST)
-            .allowedFileTypes(FileTypeSet.ANY_FILE)
-            .mandatory())
+        .add(attr("files", LABEL_LIST).allowedFileTypes(FileTypeSet.ANY_FILE))
 
         /* <!-- #BLAZE_RULE(py_runtime).ATTRIBUTE(interpreter) -->
         The Python interpreter used in this runtime. Binary rules will be executed using this
         binary.
         <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
-        .add(attr("interpreter", LABEL)
-            .allowedFileTypes(FileTypeSet.ANY_FILE)
-            .singleArtifact())
+        .add(attr("interpreter", LABEL).allowedFileTypes(FileTypeSet.ANY_FILE).singleArtifact())
 
         /* <!-- #BLAZE_RULE(py_runtime).ATTRIBUTE(interpreter_path) -->
         The absolute path of a Python interpreter. This attribute and interpreter attribute cannot
         be set at the same time.
         <!-- #END_BLAZE_RULE.ATTRIBUTE --> */
         .add(attr("interpreter_path", STRING))
-
         .add(attr("output_licenses", LICENSE))
         .build();
   }
@@ -65,7 +61,7 @@
     return Metadata.builder()
         .name("py_runtime")
         .ancestors(BaseRuleClasses.BaseRule.class)
-        .factoryClass(BazelPyRuntime.class)
+        .factoryClass(PyRuntime.class)
         .build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
index b13d844..a7e0a28 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonSemantics.java
@@ -44,6 +44,7 @@
 import com.google.devtools.build.lib.rules.cpp.CcInfo;
 import com.google.devtools.build.lib.rules.python.PyCcLinkParamsProvider;
 import com.google.devtools.build.lib.rules.python.PyCommon;
+import com.google.devtools.build.lib.rules.python.PyRuntimeProvider;
 import com.google.devtools.build.lib.rules.python.PythonConfiguration;
 import com.google.devtools.build.lib.rules.python.PythonSemantics;
 import com.google.devtools.build.lib.util.FileTypeSet;
@@ -313,17 +314,17 @@
   }
 
   private static void addRuntime(RuleContext ruleContext, Runfiles.Builder builder) {
-    BazelPyRuntimeProvider provider = ruleContext.getPrerequisite(
-        ":py_interpreter", Mode.TARGET, BazelPyRuntimeProvider.class);
-    if (provider != null && provider.interpreter() != null) {
-      builder.addArtifact(provider.interpreter());
+    PyRuntimeProvider provider =
+        ruleContext.getPrerequisite(":py_interpreter", Mode.TARGET, PyRuntimeProvider.class);
+    if (provider != null && provider.isHermetic()) {
+      builder.addArtifact(provider.getInterpreter());
       // WARNING: we are adding the all Python runtime files here,
       // and it would fail if the filenames of them contain spaces.
       // Currently, we need to exclude them in py_runtime rules.
       // Possible files in Python runtime which contain spaces in filenames:
       // - https://github.com/pypa/setuptools/blob/master/setuptools/script%20(dev).tmpl
       // - https://github.com/pypa/setuptools/blob/master/setuptools/command/launcher%20manifest.xml
-      builder.addTransitiveArtifacts(provider.files());
+      builder.addTransitiveArtifacts(provider.getFiles());
     }
   }
 
@@ -333,20 +334,20 @@
 
     String pythonBinary;
 
-    BazelPyRuntimeProvider provider = ruleContext.getPrerequisite(
-        ":py_interpreter", Mode.TARGET, BazelPyRuntimeProvider.class);
+    PyRuntimeProvider provider =
+        ruleContext.getPrerequisite(":py_interpreter", Mode.TARGET, PyRuntimeProvider.class);
 
     if (provider != null) {
       // make use of py_runtime defined by --python_top
-      if (!provider.interpreterPath().isEmpty()) {
+      if (!provider.isHermetic()) {
         // absolute Python path in py_runtime
-        pythonBinary = provider.interpreterPath();
+        pythonBinary = provider.getInterpreterPath().getPathString();
       } else {
         // checked in Python interpreter in py_runtime
         PathFragment workspaceName =
             PathFragment.create(ruleContext.getRule().getPackage().getWorkspaceName());
         pythonBinary =
-            workspaceName.getRelative(provider.interpreter().getRunfilesPath()).getPathString();
+            workspaceName.getRelative(provider.getInterpreter().getRunfilesPath()).getPathString();
       }
     } else  {
       // make use of the Python interpreter in an absolute path
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java
new file mode 100644
index 0000000..74627e1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntime.java
@@ -0,0 +1,72 @@
+// 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.rules.python;
+
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
+import com.google.devtools.build.lib.analysis.ConfiguredTarget;
+import com.google.devtools.build.lib.analysis.PrerequisiteArtifacts;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetBuilder;
+import com.google.devtools.build.lib.analysis.RuleConfiguredTargetFactory;
+import com.google.devtools.build.lib.analysis.RuleContext;
+import com.google.devtools.build.lib.analysis.RunfilesProvider;
+import com.google.devtools.build.lib.analysis.configuredtargets.RuleConfiguredTarget.Mode;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.syntax.Type;
+import com.google.devtools.build.lib.vfs.PathFragment;
+
+/** Implementation for the {@code py_runtime} rule. */
+public final class PyRuntime implements RuleConfiguredTargetFactory {
+
+  @Override
+  public ConfiguredTarget create(RuleContext ruleContext) throws ActionConflictException {
+    NestedSet<Artifact> files =
+        PrerequisiteArtifacts.nestedSet(ruleContext, "files", Mode.TARGET);
+    Artifact interpreter = ruleContext.getPrerequisiteArtifact("interpreter", Mode.TARGET);
+    PathFragment interpreterPath =
+        PathFragment.create(ruleContext.attributes().get("interpreter_path", Type.STRING));
+
+    // Determine whether we're pointing to an in-build target (hermetic) or absolute system path
+    // (non-hermetic).
+    if ((interpreter == null) == interpreterPath.isEmpty()) {
+      ruleContext.ruleError(
+          "exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified");
+    }
+    boolean hermetic = interpreter != null;
+    // Validate attributes.
+    if (!hermetic && !files.isEmpty()) {
+      ruleContext.ruleError("if 'interpreter_path' is given then 'files' must be empty");
+    }
+    if (!hermetic && !interpreterPath.isAbsolute()) {
+      ruleContext.attributeError("interpreter_path", "must be an absolute path.");
+    }
+
+    if (ruleContext.hasErrors()) {
+      return null;
+    }
+
+    PyRuntimeProvider provider =
+        hermetic
+            ? PyRuntimeProvider.create(files, interpreter, /*interpreterPath=*/ null)
+            : PyRuntimeProvider.create(/*files=*/ null, /*interpreter=*/ null, interpreterPath);
+
+    return new RuleConfiguredTargetBuilder(ruleContext)
+        .setFilesToBuild(files)
+        .addProvider(RunfilesProvider.class, RunfilesProvider.EMPTY)
+        .addProvider(PyRuntimeProvider.class, provider)
+        .build();
+  }
+
+}
diff --git a/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeProvider.java b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeProvider.java
new file mode 100644
index 0000000..c3230c0
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/rules/python/PyRuntimeProvider.java
@@ -0,0 +1,60 @@
+// 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.rules.python;
+
+import com.google.auto.value.AutoValue;
+import com.google.devtools.build.lib.actions.Artifact;
+import com.google.devtools.build.lib.analysis.TransitiveInfoProvider;
+import com.google.devtools.build.lib.collect.nestedset.NestedSet;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import javax.annotation.Nullable;
+
+/** Information about the Python runtime used by the <code>py_*</code> rules. */
+@AutoValue
+@Immutable
+public abstract class PyRuntimeProvider implements TransitiveInfoProvider {
+
+  public static PyRuntimeProvider create(
+      @Nullable NestedSet<Artifact> files,
+      @Nullable Artifact interpreter,
+      @Nullable PathFragment interpreterPath) {
+    return new AutoValue_PyRuntimeProvider(files, interpreter, interpreterPath);
+  }
+
+  /**
+   * Returns whether this runtime is hermetic, i.e. represents an in-build interpreter as opposed to
+   * a system interpreter.
+   *
+   * <p>Hermetic runtimes have non-null values for {@link #getInterpreter} and {@link #getFiles},
+   * while non-hermetic runtimes have non-null {@link #getInterpreterPath}.
+   *
+   * <p>Note: Despite the name, it is still possible for a hermetic runtime to reference in-build
+   * files that have non-hermetic behavior. For example, {@link #getInterpreter} could reference a
+   * checked-in wrapper script that calls the system interpreter at execution time.
+   */
+  public boolean isHermetic() {
+    return getInterpreter() != null;
+  }
+
+  @Nullable
+  public abstract NestedSet<Artifact> getFiles();
+
+  @Nullable
+  public abstract Artifact getInterpreter();
+
+  @Nullable
+  public abstract PathFragment getInterpreterPath();
+}
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/BUILD b/src/test/java/com/google/devtools/build/lib/bazel/BUILD
index 6387494..b0e5f17 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/bazel/BUILD
@@ -48,7 +48,6 @@
         "//src/main/java/com/google/devtools/build/lib:core-workspace-rules",
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib:packages-internal",
-        "//src/main/java/com/google/devtools/build/lib:python-rules",
         "//src/main/java/com/google/devtools/build/lib:syntax",
         "//src/main/java/com/google/devtools/build/lib:util",
         "//src/main/java/com/google/devtools/build/lib/actions",
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeTest.java
deleted file mode 100644
index 1a7d460..0000000
--- a/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPyRuntimeTest.java
+++ /dev/null
@@ -1,163 +0,0 @@
-// 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.bazel.rules.python;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
-import com.google.devtools.build.lib.analysis.ConfiguredTarget;
-import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-
-/**
- * Unit tests for {@link BazelPyRuntime}.
- */
-@RunWith(JUnit4.class)
-public final class BazelPyRuntimeTest extends BuildViewTestCase {
-
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
-
-  @Before
-  public final void setup() throws Exception {
-    scratch.file(
-      "py/BUILD",
-        "py_runtime(",
-        "    name='py-2.7',",
-        "    files = [",
-        "        'py-2.7/bin/python',",
-        "        'py-2.7/lib/libpython2.7.a',",
-        "     ],",
-        "    interpreter='py-2.7/bin/python',",
-        ")",
-        "",
-        "py_runtime(",
-        "    name='py-3.6',",
-        "    files = [],",
-        "    interpreter_path='/opt/pyenv/versions/3.6.0/bin/python',",
-        ")",
-        "",
-        "py_runtime(",
-        "    name='err-interpreter-and-path-both-set',",
-        "    files = [],",
-        "    interpreter='py-2.7/bin/python',",
-        "    interpreter_path='/opt/pyenv/versions/3.6.0/bin/python',",
-        ")",
-        "",
-        "py_runtime(",
-        "    name='err-interpreter-and-path-both-unset',",
-        "    files = [],",
-        ")",
-        "",
-        "py_runtime(",
-        "    name='err-path-not-absolute',",
-        "    files = [],",
-        "    interpreter_path='py-2.7/bin/python',",
-        ")",
-        "",
-        "py_runtime(",
-        "    name='err-non-empty-files-with-path-absolute',",
-        "    files = [",
-        "        'py-err/bin/python',",
-        "        'py-err/lib/libpython2.7.a',",
-        "     ],",
-        "    interpreter_path='/opt/pyenv/versions/3.6.0/bin/python',",
-        ")"
-    );
-
-  }
-
-  @Test
-  public void testCheckedInPyRuntime() throws Exception {
-    useConfiguration("--python_top=//py:py-2.7");
-    ConfiguredTarget target = getConfiguredTarget("//py:py-2.7");
-
-    assertThat(
-        ActionsTestUtil.prettyArtifactNames(
-            target.getProvider(BazelPyRuntimeProvider.class).files()))
-        .containsExactly("py/py-2.7/bin/python", "py/py-2.7/lib/libpython2.7.a");
-    assertThat(
-            target.getProvider(BazelPyRuntimeProvider.class).interpreter().getExecPathString())
-        .isEqualTo("py/py-2.7/bin/python");
-    assertThat(target.getProvider(BazelPyRuntimeProvider.class).interpreterPath())
-        .isEqualTo("");
-  }
-
-  @Test
-  public void testAbsolutePathPyRuntime() throws Exception {
-    useConfiguration("--python_top=//py:py-3.6");
-    ConfiguredTarget target = getConfiguredTarget("//py:py-3.6");
-
-    assertThat(
-        ActionsTestUtil.prettyArtifactNames(
-            target.getProvider(BazelPyRuntimeProvider.class).files()))
-        .isEmpty();
-    assertThat(
-        target.getProvider(BazelPyRuntimeProvider.class).interpreter())
-        .isNull();
-    assertThat(target.getProvider(BazelPyRuntimeProvider.class).interpreterPath())
-        .isEqualTo("/opt/pyenv/versions/3.6.0/bin/python");
-  }
-
-  @Test
-  public void testErrorWithInterpreterAndPathBothSet() throws Exception {
-    useConfiguration("--python_top=//py:err-interpreter-and-path-both-set");
-    try {
-      getConfiguredTarget("//py:err-interpreter-and-path-both-set");
-    } catch (Error e) {
-      assertThat(e.getMessage())
-          .contains("interpreter and interpreter_path cannot be set at the same time.");
-    }
-  }
-
-  @Test
-  public void testErrorWithInterpreterAndPathBothUnset() throws Exception {
-    useConfiguration("--python_top=//py:err-interpreter-and-path-both-unset");
-    try {
-      getConfiguredTarget("//py:err-interpreter-and-path-both-unset");
-    } catch (Error e) {
-      assertThat(e.getMessage())
-          .contains("interpreter and interpreter_path cannot be empty at the same time.");
-    }
-  }
-
-  @Test
-  public void testErrorWithPathNotAbsolute() throws Exception {
-    useConfiguration("--python_top=//py:err-path-not-absolute");
-    try {
-      getConfiguredTarget("//py:err-path-not-absolute");
-    } catch (Error e) {
-      assertThat(e.getMessage())
-          .contains("must be an absolute path.");
-    }
-  }
-
-  @Test
-  public void testPyRuntimeWithError() throws Exception {
-    useConfiguration("--python_top=//py:err-non-empty-files-with-path-absolute");
-    try {
-      getConfiguredTarget("//py:err-non-empty-files-with-path-absolute");
-    } catch (Error e) {
-      assertThat(e.getMessage())
-          .contains("interpreter with an absolute path requires files to be empty.");
-    }
-  }
-
-}
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/BazelPythonConfigurationTest.java b/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonConfigurationTest.java
similarity index 94%
rename from src/test/java/com/google/devtools/build/lib/rules/python/BazelPythonConfigurationTest.java
rename to src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonConfigurationTest.java
index a6e4aad..cdb3223 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/BazelPythonConfigurationTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/rules/python/BazelPythonConfigurationTest.java
@@ -12,14 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.devtools.build.lib.rules.python;
+package com.google.devtools.build.lib.bazel.rules.python;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.devtools.build.lib.testutil.MoreAsserts.assertThrows;
 
 import com.google.devtools.build.lib.analysis.config.InvalidConfigurationException;
 import com.google.devtools.build.lib.analysis.util.ConfigurationTestCase;
-import com.google.devtools.build.lib.bazel.rules.python.BazelPythonConfiguration;
 import com.google.devtools.common.options.OptionsParsingException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/BUILD b/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
index 5edb30a..3df2ae7 100644
--- a/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/BUILD
@@ -12,11 +12,11 @@
 test_suite(
     name = "PythonTests",
     tests = [
-        ":BazelPythonConfigurationTest",
         ":PyBinaryConfiguredTargetTest",
         ":PyInfoTest",
         ":PyLibraryConfiguredTargetTest",
         ":PyProviderUtilsTest",
+        ":PyRuntimeTest",
         ":PyStructUtilsTest",
         ":PyTestConfiguredTargetTest",
         ":PythonConfigurationTest",
@@ -39,20 +39,6 @@
 )
 
 java_test(
-    name = "BazelPythonConfigurationTest",
-    srcs = ["BazelPythonConfigurationTest.java"],
-    deps = [
-        "//src/main/java/com/google/devtools/build/lib:bazel-rules",
-        "//src/main/java/com/google/devtools/build/lib:build-base",
-        "//src/main/java/com/google/devtools/common/options",
-        "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
-        "//src/test/java/com/google/devtools/build/lib:testutil",
-        "//third_party:junit4",
-        "//third_party:truth",
-    ],
-)
-
-java_test(
     name = "PythonConfigurationTest",
     srcs = ["PythonConfigurationTest.java"],
     deps = [
@@ -67,6 +53,19 @@
     ],
 )
 
+java_test(
+    name = "PyRuntimeTest",
+    srcs = ["PyRuntimeTest.java"],
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:bazel-rules",
+        "//src/main/java/com/google/devtools/build/lib:python-rules",
+        "//src/test/java/com/google/devtools/build/lib:actions_testutil",
+        "//src/test/java/com/google/devtools/build/lib:analysis_testutil",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
 java_library(
     name = "PyBaseTestBase",
     srcs = ["PyBaseConfiguredTargetTestBase.java"],
diff --git a/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeTest.java b/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeTest.java
new file mode 100644
index 0000000..bf3b515
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/rules/python/PyRuntimeTest.java
@@ -0,0 +1,128 @@
+// Copyright 2019 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.python;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.devtools.build.lib.actions.util.ActionsTestUtil;
+import com.google.devtools.build.lib.analysis.util.BuildViewTestCase;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@code py_runtime}. */
+@RunWith(JUnit4.class)
+public class PyRuntimeTest extends BuildViewTestCase {
+
+  @Before
+  public final void setUpPython() throws Exception {
+    analysisMock.pySupport().setup(mockToolsConfig);
+  }
+
+  @Test
+  public void hermeticRuntime() throws Exception {
+    scratch.file(
+        "pkg/BUILD",
+        "py_runtime(",
+        "    name = 'myruntime',",
+        "    files = [':myfile'],",
+        "    interpreter = ':myinterpreter',",
+        ")");
+    PyRuntimeProvider info =
+        getConfiguredTarget("//pkg:myruntime").getProvider(PyRuntimeProvider.class);
+
+    assertThat(info.isHermetic()).isTrue();
+    assertThat(info.getInterpreterPath()).isNull();
+    assertThat(info.getInterpreter().getExecPathString()).isEqualTo("pkg/myinterpreter");
+    assertThat(ActionsTestUtil.baseArtifactNames(info.getFiles())).containsExactly("myfile");
+  }
+
+  @Test
+  public void nonhermeticRuntime() throws Exception {
+    scratch.file(
+        "pkg/BUILD",
+        "py_runtime(",
+        "    name = 'myruntime',",
+        "    interpreter_path = '/system/interpreter',",
+        ")");
+    PyRuntimeProvider info =
+        getConfiguredTarget("//pkg:myruntime").getProvider(PyRuntimeProvider.class);
+
+    assertThat(info.isHermetic()).isFalse();
+    assertThat(info.getInterpreterPath().getPathString()).isEqualTo("/system/interpreter");
+    assertThat(info.getInterpreter()).isNull();
+    assertThat(info.getFiles()).isNull();
+  }
+
+  @Test
+  public void cannotUseBothInterpreterAndPath() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "pkg/BUILD",
+        "py_runtime(",
+        "    name = 'myruntime',",
+        "    interpreter = ':myinterpreter',",
+        "    interpreter_path = '/system/interpreter',",
+        ")");
+    getConfiguredTarget("//pkg:myruntime");
+
+    assertContainsEvent(
+        "exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified");
+  }
+
+  @Test
+  public void mustUseEitherInterpreterOrPath() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "pkg/BUILD", //
+        "py_runtime(",
+        "    name = 'myruntime',",
+        ")");
+    getConfiguredTarget("//pkg:myruntime");
+
+    assertContainsEvent(
+        "exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified");
+  }
+
+  @Test
+  public void interpreterPathMustBeAbsolute() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "pkg/BUILD",
+        "py_runtime(",
+        "    name = 'myruntime',",
+        "    interpreter_path = 'some/relative/path',",
+        ")");
+    getConfiguredTarget("//pkg:myruntime");
+
+    assertContainsEvent("must be an absolute path");
+  }
+
+  @Test
+  public void cannotSpecifyFilesForNonhermeticRuntime() throws Exception {
+    reporter.removeHandler(failFastHandler);
+    scratch.file(
+        "pkg/BUILD",
+        "py_runtime(",
+        "    name = 'myruntime',",
+        "    files = [':myfile'],",
+        "    interpreter_path = '/system/interpreter',",
+        ")");
+    getConfiguredTarget("//pkg:myruntime");
+
+    assertContainsEvent("if 'interpreter_path' is given then 'files' must be empty");
+  }
+}