Skip to content

Commit 04f5798

Browse files
authored
tests: add integration test for build_python_zip (bazel-contrib#2045)
This is a more comprehensive regression test for verifying `--build_python_zip` is actually working (bazel-contrib#1840) This also creates a small framework to make it easier to write integration tests that need to customize the environment bazel runs in and check the output of bazel itself. I figure this will be helpful for writing simple verification tests for repository/bzlmod phase logic (i.e. set the debug env vars and grep the output). While we should avoid heavy usage of these bazel-in-bazel tests, a bit of grepping logs would go a long way for covering edge cases that examples don't cover.
1 parent 1d0c9a7 commit 04f5798

File tree

10 files changed

+263
-3
lines changed

10 files changed

+263
-3
lines changed

.bazelrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
# (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it)
55
# To update these lines, execute
66
# `bazel run @rules_bazel_integration_test//tools:update_deleted_packages`
7-
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
8-
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,gazelle,gazelle/manifest,gazelle/manifest/generate,gazelle/manifest/hasher,gazelle/manifest/test,gazelle/modules_mapping,gazelle/python,gazelle/python/private,gazelle/pythonconfig,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
7+
build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
8+
query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/patches,examples/bzlmod/py_proto_library,examples/bzlmod/py_proto_library/example.com/another_proto,examples/bzlmod/py_proto_library/example.com/proto,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,examples/py_proto_library/example.com/another_proto,examples/py_proto_library/example.com/proto,tests/integration/compile_pip_requirements,tests/integration/compile_pip_requirements_test_from_external_repo,tests/integration/custom_commands,tests/integration/ignore_root_user_error,tests/integration/ignore_root_user_error/submodule,tests/integration/pip_parse,tests/integration/pip_parse/empty,tests/integration/py_cc_toolchain_registered
99

1010
test --test_output=errors
1111

tests/integration/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
load("@rules_bazel_integration_test//bazel_integration_test:defs.bzl", "default_test_runner")
16+
load("//python:py_library.bzl", "py_library")
1617
load(":integration_test.bzl", "rules_python_integration_test")
1718

1819
licenses(["notice"])
@@ -102,3 +103,14 @@ rules_python_integration_test(
102103
bzlmod = False,
103104
workspace_path = "py_cc_toolchain_registered",
104105
)
106+
107+
rules_python_integration_test(
108+
name = "custom_commands_test",
109+
py_main = "custom_commands_test.py",
110+
)
111+
112+
py_library(
113+
name = "runner_lib",
114+
srcs = ["runner.py"],
115+
imports = ["../../"],
116+
)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
load("@rules_python//python:py_binary.bzl", "py_binary")
16+
17+
py_binary(
18+
name = "bin",
19+
srcs = ["bin.py"],
20+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
module(name = "module_under_test")
16+
17+
bazel_dep(name = "rules_python", version = "0.0.0")
18+
local_path_override(
19+
module_name = "rules_python",
20+
path = "../../..",
21+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
local_repository(
2+
name = "rules_python",
3+
path = "../../..",
4+
)
5+
6+
load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains")
7+
8+
py_repositories()
9+
10+
python_register_toolchains(
11+
name = "python_3_11",
12+
python_version = "3.11",
13+
)

tests/integration/custom_commands/WORKSPACE.bzlmod

Whitespace-only changes.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
print("Hello, world")
16+
print(__file__)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
import unittest
17+
18+
from tests.integration import runner
19+
20+
21+
class CustomCommandsTest(runner.TestCase):
22+
# Regression test for https://github.com/bazelbuild/rules_python/issues/1840
23+
def test_run_build_python_zip_false(self):
24+
result = self.run_bazel("run", "--build_python_zip=false", "//:bin")
25+
self.assert_result_matches(result, "bazel-out")
26+
27+
28+
if __name__ == "__main__":
29+
# Enabling this makes the runner log subprocesses as the test goes along.
30+
# logging.basicConfig(level = "INFO")
31+
unittest.main()

tests/integration/integration_test.bzl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ load(
1919
"bazel_integration_tests",
2020
"integration_test_utils",
2121
)
22+
load("//python:py_test.bzl", "py_test")
2223

2324
def rules_python_integration_test(
2425
name,
2526
workspace_path = None,
2627
bzlmod = True,
2728
gazelle_plugin = False,
2829
tags = None,
30+
py_main = None,
2931
**kwargs):
3032
"""Runs a bazel-in-bazel integration test.
3133
@@ -37,10 +39,24 @@ def rules_python_integration_test(
3739
disable bzlmod.
3840
gazelle_plugin: Whether the test uses the gazelle plugin.
3941
tags: Test tags.
42+
py_main: Optional `.py` file to run tests using. When specified, a
43+
python based test runner is used, and this source file is the main
44+
entry point and responsible for executing tests.
4045
**kwargs: Passed to the upstream `bazel_integration_tests` rule.
4146
"""
4247
workspace_path = workspace_path or name.removesuffix("_test")
43-
if bzlmod:
48+
if py_main:
49+
test_runner = name + "_py_runner"
50+
py_test(
51+
name = test_runner,
52+
srcs = [py_main],
53+
main = py_main,
54+
deps = [":runner_lib"],
55+
# Hide from ... patterns; should only be run as part
56+
# of the bazel integration test
57+
tags = ["manual"],
58+
)
59+
elif bzlmod:
4460
if gazelle_plugin:
4561
test_runner = "//tests/integration:test_runner_gazelle_plugin"
4662
else:

tests/integration/runner.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Copyright 2024 The Bazel Authors. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
import os
17+
import os.path
18+
import pathlib
19+
import re
20+
import shlex
21+
import subprocess
22+
import unittest
23+
24+
_logger = logging.getLogger(__name__)
25+
26+
class ExecuteError(Exception):
27+
def __init__(self, result):
28+
self.result = result
29+
def __str__(self):
30+
return self.result.describe()
31+
32+
class ExecuteResult:
33+
def __init__(
34+
self,
35+
args: list[str],
36+
env: dict[str, str],
37+
cwd: pathlib.Path,
38+
proc_result: subprocess.CompletedProcess,
39+
):
40+
self.args = args
41+
self.env = env
42+
self.cwd = cwd
43+
self.exit_code = proc_result.returncode
44+
self.stdout = proc_result.stdout
45+
self.stderr = proc_result.stderr
46+
47+
def describe(self) -> str:
48+
env_lines = [
49+
" " + shlex.quote(f"{key}={value}")
50+
for key, value in sorted(self.env.items())
51+
]
52+
env = " \\\n".join(env_lines)
53+
args = shlex.join(self.args)
54+
maybe_stdout_nl = "" if self.stdout.endswith("\n") else "\n"
55+
maybe_stderr_nl = "" if self.stderr.endswith("\n") else "\n"
56+
return f"""\
57+
COMMAND:
58+
cd {self.cwd} && \\
59+
env \\
60+
{env} \\
61+
{args}
62+
RESULT: exit_code: {self.exit_code}
63+
===== STDOUT START =====
64+
{self.stdout}{maybe_stdout_nl}===== STDOUT END =====
65+
===== STDERR START =====
66+
{self.stderr}{maybe_stderr_nl}===== STDERR END =====
67+
"""
68+
69+
70+
class TestCase(unittest.TestCase):
71+
def setUp(self):
72+
super().setUp()
73+
self.repo_root = pathlib.Path(os.environ["BIT_WORKSPACE_DIR"])
74+
self.bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"])
75+
outer_test_tmpdir = pathlib.Path(os.environ["TEST_TMPDIR"])
76+
self.test_tmp_dir = outer_test_tmpdir / "bit_test_tmp"
77+
# Put the global tmp not under the test tmp to better match how a real
78+
# execution has entirely different directories for these.
79+
self.tmp_dir = outer_test_tmpdir / "bit_tmp"
80+
self.bazel_env = {
81+
"PATH": os.environ["PATH"],
82+
"TEST_TMPDIR": str(self.test_tmp_dir),
83+
"TMP": str(self.tmp_dir),
84+
# For some reason, this is necessary for Bazel 6.4 to work.
85+
# If not present, it can't find some bash helpers in @bazel_tools
86+
"RUNFILES_DIR": os.environ["TEST_SRCDIR"]
87+
}
88+
89+
def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult:
90+
"""Run a bazel invocation.
91+
92+
Args:
93+
*args: The args to pass to bazel; the leading `bazel` command is
94+
added automatically
95+
check: True if the execution must succeed, False if failure
96+
should raise an error.
97+
Returns:
98+
An `ExecuteResult` from running Bazel
99+
"""
100+
args = [str(self.bazel), *args]
101+
env = self.bazel_env
102+
_logger.info("executing: %s", shlex.join(args))
103+
cwd = self.repo_root
104+
proc_result = subprocess.run(
105+
args=args,
106+
text=True,
107+
capture_output=True,
108+
cwd=cwd,
109+
env=env,
110+
check=False,
111+
)
112+
exec_result = ExecuteResult(args, env, cwd, proc_result)
113+
if check and exec_result.exit_code:
114+
raise ExecuteError(exec_result)
115+
else:
116+
return exec_result
117+
118+
def assert_result_matches(self, result: ExecuteResult, regex: str) -> None:
119+
"""Assert stdout/stderr of an invocation matches a regex.
120+
121+
Args:
122+
result: ExecuteResult from `run_bazel` whose stdout/stderr will
123+
be checked.
124+
regex: Pattern to match, using `re.search` semantics.
125+
"""
126+
if not re.search(regex, result.stdout + result.stderr):
127+
self.fail(
128+
"Bazel output did not match expected pattern\n"
129+
+ f"expected pattern: {regex}\n"
130+
+ f"invocation details:\n{result.describe()}"
131+
)

0 commit comments

Comments
 (0)