ci: add bazel_integration_test (#338)
This runs a py_test with a copy of bazel as a data dep.
It glob()s up the sources for each example and runs nested bazel test on them.
This detects whether the examples are fully working and self-contained.
Follow-up step is to replace the rules_python.tgz with a HEAD version so we detect
breakages.
diff --git a/.bazelrc b/.bazelrc
index e8874a9..2b60f35 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -1,8 +1,9 @@
# For bazel-in-bazel testing
-# Trick bazel into treating BUILD files under examples/* and e2e/* as being regular files
+# Trick bazel into treating BUILD files under examples/* as being regular files
# This lets us glob() up all the files inside the examples to make them inputs to tests
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
-# To update these lines, run this command:
-# sed -i.bak "/^[^#].*--deleted_packages/s#=.*#=$(find examples/*/* \( -name BUILD -or -name BUILD.bazel \) | xargs -n 1 dirname | paste -sd, -)#" .bazelrc && rm .bazelrc.bak
+# To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh
build --deleted_packages=examples/pip/boto,examples/pip/extras,examples/pip/helloworld
query --deleted_packages=examples/pip/boto,examples/pip/extras,examples/pip/helloworld
+
+test --test_output=errors
diff --git a/examples/BUILD b/examples/BUILD
index 426ddc0..18377eb 100644
--- a/examples/BUILD
+++ b/examples/BUILD
@@ -11,6 +11,13 @@
# 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.
+load("//tools/bazel_integration_test:bazel_integration_test.bzl", "bazel_integration_test")
+
package(default_visibility = ["//visibility:public"])
licenses(["notice"]) # Apache 2.0
+
+bazel_integration_test(
+ name = "pip_example",
+ timeout = "long",
+)
diff --git a/examples/pip/.bazelrc b/examples/pip/.bazelrc
new file mode 100644
index 0000000..0dca511
--- /dev/null
+++ b/examples/pip/.bazelrc
@@ -0,0 +1,2 @@
+# Watch Bazel run, and only run one at a time
+test --test_output=streamed
diff --git a/examples/pip/boto/BUILD b/examples/pip/boto/BUILD
index c5ac4fc..7bd91d9 100644
--- a/examples/pip/boto/BUILD
+++ b/examples/pip/boto/BUILD
@@ -22,8 +22,10 @@
py_test(
name = "boto_test",
srcs = ["boto_test.py"],
+ python_version = "PY2",
deps = [
requirement("boto3"),
+ requirement("pip"),
# six is a transitive dependency via python-dateutil. Explicitly depend
# on it to work around issue #70; see issue #98.
requirement("six"),
diff --git a/examples/pip/helloworld/helloworld_test.py b/examples/pip/helloworld/helloworld_test.py
index ca9f6b1..da6ac77 100644
--- a/examples/pip/helloworld/helloworld_test.py
+++ b/examples/pip/helloworld/helloworld_test.py
@@ -12,10 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import helloworld
import unittest
-from examples.helloworld import helloworld
-
class HelloWorldTest(unittest.TestCase):
diff --git a/internal_deps.bzl b/internal_deps.bzl
index 281efd8..15fe0a5 100644
--- a/internal_deps.bzl
+++ b/internal_deps.bzl
@@ -110,3 +110,13 @@
name = "piptool_deps",
requirements = "@rules_python//python:requirements.txt",
)
+
+ maybe(
+ http_archive,
+ name = "build_bazel_integration_testing",
+ urls = [
+ "https://github.com/bazelbuild/bazel-integration-testing/archive/165440b2dbda885f8d1ccb8d0f417e6cf8c54f17.zip",
+ ],
+ strip_prefix = "bazel-integration-testing-165440b2dbda885f8d1ccb8d0f417e6cf8c54f17",
+ sha256 = "2401b1369ef44cc42f91dc94443ef491208dbd06da1e1e10b702d8c189f098e3",
+ )
diff --git a/internal_setup.bzl b/internal_setup.bzl
index 90c74f2..646b86c 100644
--- a/internal_setup.bzl
+++ b/internal_setup.bzl
@@ -1,13 +1,20 @@
"""Setup for rules_python tests and tools."""
+load("@build_bazel_integration_testing//tools:repositories.bzl", "bazel_binaries")
+
# Requirements for building our piptool.
load(
"@piptool_deps//:requirements.bzl",
_piptool_install = "pip_install",
)
+load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS")
+
def rules_python_internal_setup():
"""Setup for rules_python tests and tools."""
# Requirements for building our piptool.
_piptool_install()
+
+ # Depend on the Bazel binaries for running bazel-in-bazel tests
+ bazel_binaries(versions = SUPPORTED_BAZEL_VERSIONS)
diff --git a/tools/bazel_integration_test/BUILD b/tools/bazel_integration_test/BUILD
new file mode 100644
index 0000000..10566c4
--- /dev/null
+++ b/tools/bazel_integration_test/BUILD
@@ -0,0 +1 @@
+exports_files(["test_runner.py"])
diff --git a/tools/bazel_integration_test/bazel_integration_test.bzl b/tools/bazel_integration_test/bazel_integration_test.bzl
new file mode 100644
index 0000000..b23360b
--- /dev/null
+++ b/tools/bazel_integration_test/bazel_integration_test.bzl
@@ -0,0 +1,111 @@
+"Define a rule for running bazel test under Bazel"
+
+load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS")
+load("//python:defs.bzl", "py_test")
+
+BAZEL_BINARY = "@build_bazel_bazel_%s//:bazel_binary" % SUPPORTED_BAZEL_VERSIONS[0].replace(".", "_")
+
+_ATTRS = {
+ "bazel_binary": attr.label(
+ default = BAZEL_BINARY,
+ doc = """The bazel binary files to test against.
+
+It is assumed by the test runner that the bazel binary is found at label_workspace/bazel (wksp/bazel.exe on Windows)""",
+ ),
+ "bazel_commands": attr.string_list(
+ default = ["info", "test ..."],
+ doc = """The list of bazel commands to run. Defaults to `["info", "test ..."]`.
+
+Note that if a command contains a bare `--` argument, the --test_arg passed to Bazel will appear before it.
+""",
+ ),
+ "workspace_files": attr.label(
+ doc = """A filegroup of all files in the workspace-under-test necessary to run the test.""",
+ ),
+}
+
+# Avoid using non-normalized paths (workspace/../other_workspace/path)
+def _to_manifest_path(ctx, file):
+ if file.short_path.startswith("../"):
+ return file.short_path[3:]
+ else:
+ return ctx.workspace_name + "/" + file.short_path
+
+def _config_impl(ctx):
+ if len(SUPPORTED_BAZEL_VERSIONS) > 1:
+ fail("""
+ bazel_integration_test doesn't support multiple Bazel versions to test against yet.
+ """)
+ if len(ctx.files.workspace_files) == 0:
+ fail("""
+No files were found to run under integration testing. See comment in /.bazelrc.
+You probably need to run
+ tools/bazel_integration_test/update_deleted_packages.sh
+""")
+
+ # Serialize configuration file for test runner
+ config = ctx.actions.declare_file("%s.json" % ctx.attr.name)
+ ctx.actions.write(
+ output = config,
+ content = """
+{{
+ "workspaceRoot": "{TMPL_workspace_root}",
+ "bazelBinaryWorkspace": "{TMPL_bazel_binary_workspace}",
+ "bazelCommands": [ {TMPL_bazel_commands} ]
+}}
+""".format(
+ TMPL_workspace_root = ctx.files.workspace_files[0].dirname,
+ TMPL_bazel_binary_workspace = ctx.attr.bazel_binary.label.workspace_name,
+ TMPL_bazel_commands = ", ".join(["\"%s\"" % s for s in ctx.attr.bazel_commands]),
+ ),
+ )
+
+ return [DefaultInfo(
+ files = depset([config]),
+ runfiles = ctx.runfiles(files = [config]),
+ )]
+
+_config = rule(
+ implementation = _config_impl,
+ doc = "Configures an integration test that runs a specified version of bazel against an external workspace.",
+ attrs = _ATTRS,
+)
+
+def bazel_integration_test(name, **kwargs):
+ """Wrapper macro to set default srcs and run a py_test with config
+
+ Args:
+ name: name of the resulting py_test
+ **kwargs: additional attributes like timeout and visibility
+ """
+ # By default, we assume sources for "pip_example" are in examples/pip/**/*
+ dirname = name[:-len("_example")]
+ native.filegroup(
+ name = "_%s_sources" % name,
+ srcs = native.glob(
+ ["%s/**/*" % dirname],
+ exclude = ["%s/bazel-*/**" % dirname],
+ ),
+ )
+ workspace_files = kwargs.pop("workspace_files", "_%s_sources" % name)
+
+ _config(
+ name = "_%s_config" % name,
+ workspace_files = workspace_files,
+ )
+
+ py_test(
+ name = name,
+ srcs = [Label("//tools/bazel_integration_test:test_runner.py")],
+ main = "test_runner.py",
+ args = [native.package_name() + "/_%s_config.json" % name],
+ deps = [Label("//python/runfiles")],
+ data = [
+ BAZEL_BINARY,
+ "_%s_config" % name,
+ workspace_files,
+ ],
+ **kwargs,
+ )
+
+
\ No newline at end of file
diff --git a/tools/bazel_integration_test/test_runner.py b/tools/bazel_integration_test/test_runner.py
new file mode 100644
index 0000000..46ba734
--- /dev/null
+++ b/tools/bazel_integration_test/test_runner.py
@@ -0,0 +1,45 @@
+from pathlib import Path
+import json
+import os
+import platform
+from subprocess import Popen
+import sys
+
+from rules_python.python.runfiles import runfiles
+
+def main(conf_file):
+ with open(conf_file) as j:
+ config = json.load(j)
+ r = runfiles.Create()
+
+ isWindows = platform.system() == 'Windows'
+ bazelBinary = r.Rlocation(os.path.join(config['bazelBinaryWorkspace'], 'bazel.exe' if isWindows else 'bazel'))
+
+ workspacePath = config['workspaceRoot']
+ # Canonicalize bazel external/some_repo/foo
+ if workspacePath.startswith('external/'):
+ workspacePath = '..' + workspacePath[len('external'):]
+
+ for command in config['bazelCommands']:
+ bazel_args = command.split(' ')
+ try:
+ doubleHyphenPos = bazel_args.index('--')
+ print("patch that in ", doubleHyphenPos)
+ except ValueError:
+ pass
+
+
+ # Bazel's wrapper script needs this or you get
+ # 2020/07/13 21:58:11 could not get the user's cache directory: $HOME is not defined
+ os.environ['HOME'] = str(Path.home())
+
+ bazel_args.insert(0, bazelBinary)
+ bazel_process = Popen(bazel_args, cwd = workspacePath)
+ bazel_process.wait()
+ if bazel_process.returncode != 0:
+ # Test failure in Bazel is exit 3
+ # https://github.com/bazelbuild/bazel/blob/486206012a664ecb20bdb196a681efc9a9825049/src/main/java/com/google/devtools/build/lib/util/ExitCode.java#L44
+ sys.exit(3)
+
+if __name__ == '__main__':
+ main(sys.argv[1])
diff --git a/tools/bazel_integration_test/update_deleted_packages.sh b/tools/bazel_integration_test/update_deleted_packages.sh
new file mode 100755
index 0000000..95a8500
--- /dev/null
+++ b/tools/bazel_integration_test/update_deleted_packages.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+# For integration tests, we want to be able to glob() up the sources inside a nested package
+# See explanation in .bazelrc
+
+set -eux
+
+DIR="$(dirname $0)/../.."
+# The sed -i.bak pattern is compatible between macos and linux
+sed -i.bak "/^[^#].*--deleted_packages/s#=.*#=$(\
+ find examples/*/* \( -name BUILD -or -name BUILD.bazel \) | xargs -n 1 dirname | paste -sd, -\
+)#" $DIR/.bazelrc && rm .bazelrc.bak
diff --git a/version.bzl b/version.bzl
index b9e2216..00d16b5 100644
--- a/version.bzl
+++ b/version.bzl
@@ -14,3 +14,20 @@
"""The version of rules_python."""
version = "0.0.2"
+
+# Currently used Bazel version. This version is what the rules here are tested
+# against.
+# This version should be updated together with the version of the Bazel
+# in .bazelversion.
+# TODO(alexeagle): assert this is the case in a test
+BAZEL_VERSION = "3.3.1"
+
+# Versions of Bazel which users should be able to use.
+# Ensures we don't break backwards-compatibility,
+# accidentally forcing users to update their LTS-supported bazel.
+# These are the versions used when testing nested workspaces with
+# bazel_integration_test.
+SUPPORTED_BAZEL_VERSIONS = [
+ # TODO: add LTS versions of bazel like 1.0.0, 2.0.0
+ BAZEL_VERSION,
+]