Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions sphinxdocs/.bazelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bazel-bin
bazel-out
bazel-testlogs
bazel-sphinxdocs
8 changes: 8 additions & 0 deletions sphinxdocs/.bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,13 @@ common --experimental_repository_downloader_retries=10

common --incompatible_python_disallow_native_rules
common --incompatible_no_implicit_file_export
# Pass host PATH to build and test actions so bazel_from_env (used by rules_bazel_integration_test
# for local 'self' testing) can locate the user's bazel binary without disabling strict action env.
common --action_env=PATH
common --test_env=PATH

build --lockfile_mode=update

common:fast-tests --build_tests_only=true
common:fast-tests --build_tag_filters=-large,-enormous,-integration-test
common:fast-tests --test_tag_filters=-large,-enormous,-integration-test
1 change: 1 addition & 0 deletions sphinxdocs/.bazelrc.deleted_packages
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
common --deleted_packages=integration_tests/bcr
common --deleted_packages=integration_tests/persistent_worker/workspace
16 changes: 16 additions & 0 deletions sphinxdocs/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package(default_visibility = ["//visibility:public"])

filegroup(
name = "distribution",
srcs = glob(
["**/*"],
exclude = [
"bazel-*/**",
"integration_tests/**",
"tests/**",
],
) + [
"//sphinxdocs:distribution",
],
visibility = ["//visibility:public"],
)
18 changes: 18 additions & 0 deletions sphinxdocs/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,21 @@ dev_pip.parse(
requirements_lock = "@rules_python//docs:requirements.txt",
)
use_repo(dev_pip, "dev_pip")

bazel_dep(name = "rules_bazel_integration_test", version = "0.37.1", dev_dependency = True)

bazel_binaries = use_extension(
"@rules_bazel_integration_test//:extensions.bzl",
"bazel_binaries",
dev_dependency = True,
)
bazel_binaries.local(
name = "self",
path = "integration_tests/bazel_from_env",
)
use_repo(
bazel_binaries,
"bazel_binaries",
"bazel_binaries_bazelisk",
"build_bazel_bazel_self",
)
8 changes: 8 additions & 0 deletions sphinxdocs/integration_tests/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("@rules_python//python:py_library.bzl", "py_library")

package(default_visibility = ["//visibility:public"])

py_library(
name = "runner_lib",
srcs = ["runner.py"],
)
6 changes: 6 additions & 0 deletions sphinxdocs/integration_tests/bazel_from_env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
#
# A simple wrapper so rules_bazel_integration_test can use the
# bazel version inherited from the environment.

bazel "$@"
68 changes: 68 additions & 0 deletions sphinxdocs/integration_tests/integration_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Helpers for running bazel-in-bazel integration tests for sphinxdocs."""

load(
"@rules_bazel_integration_test//bazel_integration_test:defs.bzl",
"bazel_integration_test",
"integration_test_utils",
)
load("@rules_python//python:py_test.bzl", "py_test")

def _test_runner(*, name, bazel_version, py_main, py_deps):
test_runner = "{}_bazel_{}_py_runner".format(name, bazel_version)
py_test(
name = test_runner,
srcs = [py_main],
main = py_main,
deps = ["//integration_tests:runner_lib"] + py_deps,
tags = ["manual"],
)
return test_runner

def sphinxdocs_integration_test(
name,
workspace_path = "workspace",
tags = None,
py_main = None,
py_deps = None,
bazel_versions = None,
**kwargs):
"""Runs a bazel-in-bazel integration test for sphinxdocs.

Args:
name: Name of the test.
workspace_path: The directory name of the sub-workspace.
tags: Test tags.
py_main: Main Python test runner script.
py_deps: Dependencies for py_main.
bazel_versions: List of bazel versions to test.
**kwargs: Passed to bazel_integration_test.
"""
workspace_files = integration_test_utils.glob_workspace_files(workspace_path)
native.filegroup(
name = name + "_workspace_files",
srcs = workspace_files + [
"//:distribution",
],
)
kwargs.setdefault("size", "large")
for bazel_version in bazel_versions or ["self"]:
test_runner = _test_runner(
name = name,
bazel_version = bazel_version,
py_main = py_main,
py_deps = py_deps or [],
)
bazel_integration_test(
name = "{}_bazel_{}".format(name, bazel_version),
workspace_path = workspace_path,
test_runner = test_runner,
bazel_version = bazel_version,
workspace_files = [name + "_workspace_files"],
tags = (tags or []) + [
"exclusive",
"no-sandbox",
"no-remote-exec",
"integration-test",
],
**kwargs
)
8 changes: 8 additions & 0 deletions sphinxdocs/integration_tests/persistent_worker/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
load("//integration_tests:integration_test.bzl", "sphinxdocs_integration_test")

package(default_visibility = ["//visibility:public"])

sphinxdocs_integration_test(
name = "persistent_worker_test",
py_main = "persistent_worker_test.py",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import unittest

from integration_tests import runner


class PersistentWorkerTest(runner.TestCase):
def _check_index_html(self, text: str, should_exist: bool):
index_html = (
self.repo_root / "bazel-bin" / "docs" / "_build" / "html" / "index.html"
)
if not index_html.exists():
self.fail(f"Could not find index.html at {index_html}")
content = index_html.read_text()
if should_exist:
self.assertIn(
text,
content,
f"Expected '{text}' in index.html after build, but not found.",
)
else:
self.assertNotIn(
text,
content,
f"Expected '{text}' NOT to be in index.html after build, but found it.",
)

def test_incremental_add_and_remove_files(self):
# 1. Initial build
result = self.run_bazel("build", "//:docs")
self.assert_result_matches(result, "bazel-bin")
index_html = (
self.repo_root / "bazel-bin" / "docs" / "_build" / "html" / "index.html"
)
self.assertTrue(
index_html.exists(), "index.html should exist after initial build"
)

# 2. Add a new markdown file and verify it is included across incremental build
page2_md = self.repo_root / "page2.md"
page2_md.write_text("# Page 2\n\nThis is a newly added page.\n")
result = self.run_bazel("build", "//:docs")
self.assert_result_matches(result, "bazel-bin")
self._check_index_html("page2.html", should_exist=True)

# 3. Remove the added markdown file and verify the persistent worker cleans up
# stale source files and invalidates toctrees without errors or warnings.
page2_md.unlink()
result = self.run_bazel("build", "//:docs")
self.assert_result_matches(result, "bazel-bin")
self._check_index_html("page2.html", should_exist=False)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Disable disk caching and remote action caching so Bazel is forced to dispatch builds
# directly to the running persistent worker instead of restoring pre-built HTML cache results across test steps.
common --disk_cache=
build --noremote_accept_cached
common --http_timeout_scaling=10.0
common --experimental_repository_downloader_retries=10
common --experimental_downloader_config=downloader_config.cfg
common --lockfile_mode=off
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("@sphinxdocs//sphinxdocs:sphinx.bzl", "sphinx_build_binary", "sphinx_docs")

sphinx_docs(
name = "docs",
srcs = glob(["*.md"]),
config = "conf.py",
formats = ["html"],
sphinx = ":sphinx-build",
)

sphinx_build_binary(
name = "sphinx-build",
deps = [
"@dev_pip//myst_parser",
"@dev_pip//sphinx",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module(version = "0.0.0")

bazel_dep(name = "sphinxdocs", version = "0.0.0")
local_path_override(
module_name = "sphinxdocs",
path = "../../..",
)

bazel_dep(name = "rules_python", version = "1.8.5")

dev_pip = use_extension(
"@rules_python//python/extensions:pip.bzl",
"pip",
dev_dependency = True,
)
dev_pip.parse(
hub_name = "dev_pip",
python_version = "3.11",
requirements_lock = "@rules_python//docs:requirements.txt",
)
use_repo(dev_pip, "dev_pip")

bazel_dep(name = "bazel_skylib", version = "1.8.2")
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Bzlmod-enabled workspace; this empty WORKSPACE file satisfies rules_bazel_integration_test checks.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extensions = ["myst_parser"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Try GitHub first (primary)
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) github.com/bazel-contrib/bazel_features/$1
rewrite ^github\.com/bazel-contrib/rules_go/(.*) github.com/bazel-contrib/rules_go/$1
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) github.com/bazelbuild/bazel-skylib/$1
rewrite ^github\.com/bazelbuild/platforms/(.*) github.com/bazelbuild/platforms/$1
rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) github.com/bazelbuild/rules_kotlin/$1
rewrite ^github\.com/bazelbuild/rules_shell/(.*) github.com/bazelbuild/rules_shell/$1
rewrite ^github\.com/bazelbuild/rules_java/(.*) github.com/bazelbuild/rules_java/$1
rewrite ^github\.com/bazelbuild/stardoc/(.*) github.com/bazelbuild/stardoc/$1


# Fall back to mirror (secondary)
# Tracking upstream BCR mirror addition: https://github.com/bazelbuild/platforms/issues/139
rewrite ^github\.com/bazel-contrib/bazel_features/(.*) mirror.bazel.build/github.com/bazel-contrib/bazel_features/$1
rewrite ^github\.com/bazel-contrib/rules_go/(.*) mirror.bazel.build/github.com/bazel-contrib/rules_go/$1
rewrite ^github\.com/bazelbuild/bazel-skylib/(.*) mirror.bazel.build/github.com/bazelbuild/bazel-skylib/$1
rewrite ^github\.com/bazelbuild/platforms/(.*) mirror.bazel.build/github.com/bazelbuild/platforms/$1
rewrite ^github\.com/bazelbuild/rules_kotlin/(.*) mirror.bazel.build/github.com/bazelbuild/rules_kotlin/$1
rewrite ^github\.com/bazelbuild/rules_shell/(.*) mirror.bazel.build/github.com/bazelbuild/rules_shell/$1
rewrite ^github\.com/bazelbuild/rules_java/(.*) mirror.bazel.build/github.com/bazelbuild/rules_java/$1
rewrite ^github\.com/bazelbuild/stardoc/(.*) mirror.bazel.build/github.com/bazelbuild/stardoc/$1
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Test Documentation

```{toctree}
:glob: true
*
```
99 changes: 99 additions & 0 deletions sphinxdocs/integration_tests/runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import logging
import os
import os.path
import pathlib
import re
import shlex
import subprocess
import unittest

_logger = logging.getLogger(__name__)


class ExecuteError(Exception):
def __init__(self, result):
self.result = result

def __str__(self):
return self.result.describe()


class ExecuteResult:
def __init__(
self,
args: list[str],
env: dict[str, str],
cwd: pathlib.Path,
proc_result: subprocess.CompletedProcess,
):
self.args = args
self.env = env
self.cwd = cwd
self.exit_code = proc_result.returncode
self.stdout = proc_result.stdout
self.stderr = proc_result.stderr

def describe(self) -> str:
env_lines = [
" " + shlex.quote(f"{key}={value}")
for key, value in sorted(self.env.items())
]
env = " \\\n".join(env_lines)
args = shlex.join(self.args)
maybe_stdout_nl = "" if self.stdout.endswith("\n") else "\n"
maybe_stderr_nl = "" if self.stderr.endswith("\n") else "\n"
return f"""\
COMMAND:
cd {self.cwd} && \\
env \\
{env} \\
{args}
RESULT: exit_code: {self.exit_code}
===== STDOUT START =====
{self.stdout}{maybe_stdout_nl}===== STDOUT END =====
===== STDERR START =====
{self.stderr}{maybe_stderr_nl}===== STDERR END =====
"""


class TestCase(unittest.TestCase):
def setUp(self):
super().setUp()
self.repo_root = pathlib.Path(os.environ["BIT_WORKSPACE_DIR"])
self.bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"])
outer_test_tmpdir = pathlib.Path(os.environ["TEST_TMPDIR"])
self.test_tmp_dir = outer_test_tmpdir / "bit_test_tmp"
self.tmp_dir = outer_test_tmpdir / "bit_tmp"
self.bazel_env = {
"PATH": os.environ["PATH"],
"TEST_TMPDIR": str(self.test_tmp_dir),
"TMP": str(self.tmp_dir),
"RUNFILES_DIR": os.environ["TEST_SRCDIR"],
}

def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult:
args = [str(self.bazel), *args]
env = self.bazel_env
_logger.info("executing: %s", shlex.join(args))
cwd = self.repo_root
proc_result = subprocess.run(
args=args,
text=True,
capture_output=True,
cwd=cwd,
env=env,
check=False,
)
exec_result = ExecuteResult(args, env, cwd, proc_result)
if check and exec_result.exit_code:
raise ExecuteError(exec_result)
else:
return exec_result

def assert_result_matches(self, result: ExecuteResult, regex: str) -> None:
if not re.search(regex, result.stdout + result.stderr):
self.fail(
"Bazel output did not match expected pattern\n"
+ f"expected pattern: {regex}\n"
+ f"invocation details:\n{result.describe()}"
)
Loading