Skip to content
Open
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
20 changes: 16 additions & 4 deletions src/test/py/bazel/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -408,16 +408,28 @@ py_test(
],
)

py_library(
name = "remote_repo_contents_cache_test_base",
testonly = 1,
srcs = ["bzlmod/remote_repo_contents_cache_test_base.py"],
deps = [":test_base"],
)

py_test(
name = "remote_repo_contents_cache_test",
size = "large",
srcs = ["bzlmod/remote_repo_contents_cache_test.py"],
shard_count = 2,
tags = ["requires-network"],
deps = [
":bzlmod_test_utils",
":test_base",
],
deps = [":remote_repo_contents_cache_test_base"],
)

py_test(
name = "remote_repo_contents_cache_rewinding_test",
size = "large",
srcs = ["bzlmod/remote_repo_contents_cache_rewinding_test.py"],
tags = ["requires-network"],
deps = [":remote_repo_contents_cache_test_base"],
)

py_test(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright 2026 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.

import os
import re
from absl.testing import absltest
from src.test.py.bazel.bzlmod import remote_repo_contents_cache_test_base


class RemoteRepoContentsCacheRewindingTest(
remote_repo_contents_cache_test_base.RemoteRepoContentsCacheTestBase
):
"""Tests recovery of repo files lost from the remote cache."""

def _setupRepoWithSubpackage(self):
self.ScratchFile(
'MODULE.bazel',
[
'repo = use_repo_rule("//:repo.bzl", "repo")',
'repo(name = "my_repo")',
],
)

self.ScratchFile('BUILD.bazel')
self.ScratchFile(
'repo.bzl',
[
'def _repo_impl(rctx):',
(
' rctx.file("BUILD", "filegroup(name=\'root\','
" srcs=['root.txt'])\")"
),
' rctx.file("root.txt", "root")',
(
' rctx.file("sub/BUILD", "filegroup(name=\'sub\','
" srcs=['sub.txt'])\")"
),
' rctx.file("sub/sub.txt", "sub")',
' print("JUST FETCHED")',
' return rctx.repo_metadata(reproducible=True)',
'repo = repository_rule(_repo_impl)',
],
)

return self.RepoDir('my_repo')

def testLostRemoteFile_build(self):
# Create a repo with two BUILD files (one in a subpackage), build a target
# from one to cause it to be cached, then build that target again after
# expunging to verify it is cached.
# Then, restart the worker and build a target in the other build file.
repo_dir = self._setupRepoWithSubpackage()

# First fetch: not cached
_, _, stderr = self.RunBazel(['build', '@my_repo//:root'])
self.assertIn('JUST FETCHED', '\n'.join(stderr))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))

# After expunging: cached
self.RunBazel(['clean', '--expunge'])
_, _, stderr = self.RunBazel(['build', '@my_repo//:root'])
self.assertNotIn('JUST FETCHED', '\n'.join(stderr))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))

# Lose all remote files.
self.ClearRemoteCache()

# Build the other target: fails due to the lost input
_, _, stderr = self.RunBazel(['build', '@my_repo//sub:sub'])
# First restart recovers @my_repo, the next one recovers @platforms.
self.assertEqual(
2,
stderr.count(
'Found transient remote cache error, retrying the build...'
),
)
canonical_repo_name = repo_dir[repo_dir.rfind('/') + 1 :]
stderr = '\n'.join(stderr)
self.assertRegex(
stderr,
'external/%s/sub/BUILD with digest .*/.* no longer available in the'
' remote cache'
% re.escape(canonical_repo_name),
)
self.assertIn('JUST FETCHED', stderr)
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))

# After expunging again: cached
self.RunBazel(['clean', '--expunge'])
_, _, stderr = self.RunBazel(['build', '@my_repo//sub:sub'])
self.assertNotIn('JUST FETCHED', '\n'.join(stderr))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))
if __name__ == '__main__':
absltest.main()
128 changes: 4 additions & 124 deletions src/test/py/bazel/bzlmod/remote_repo_contents_cache_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@
# pylint: disable=g-long-ternary
# pylint: disable=g-bad-todo

import json
import os
import re
import tempfile
from absl.testing import absltest
from src.test.py.bazel import test_base
from src.test.py.bazel.bzlmod import remote_repo_contents_cache_test_base

# Whether repos containing symlinks that point out of the repo can be added to
# the remote repo contents cache. If False, such repos are refetched instead of
Expand All @@ -30,39 +28,9 @@
CROSS_REPO_SYMLINKS_CACHEABLE = False


class RemoteRepoContentsCacheTest(test_base.TestBase):

def setUp(self):
test_base.TestBase.setUp(self)
self._worker_port = self.StartRemoteWorker()
self.ScratchFile(
'.bazelrc',
[
'startup --experimental_remote_repo_contents_cache',
# Only use the remote repo contents cache.
'common --repo_contents_cache=',
'common --remote_cache=grpc://localhost:' + str(self._worker_port),
'common --auth_enabled=false',
'common --remote_timeout=3600s',
'common --verbose_failures',
],
)

def tearDown(self):
test_base.TestBase.tearDown(self)
self.StopRemoteWorker()

def RepoDir(self, repo_name, cwd=None):
_, stdout, _ = self.RunBazel(['info', 'output_base'], cwd=cwd)
self.assertLen(stdout, 1)
output_base = stdout[0].strip()

_, stdout, _ = self.RunBazel(['mod', 'dump_repo_mapping', ''], cwd=cwd)
self.assertLen(stdout, 1)
mapping = json.loads(stdout[0])
canonical_repo_name = mapping[repo_name]

return output_base + '/external/' + canonical_repo_name
class RemoteRepoContentsCacheTest(
remote_repo_contents_cache_test_base.RemoteRepoContentsCacheTestBase
):

def testCachedAfterCleanExpunge(self):
self.ScratchFile(
Expand Down Expand Up @@ -1889,94 +1857,6 @@ def testRepoExternalSymlinkWithNativeTargetRepoLocalAction(self):
if CROSS_REPO_SYMLINKS_CACHEABLE:
self.assertFalse(os.path.exists(os.path.join(my_repo_dir, 'BUILD')))

def testLostRemoteFile_build(self):
# Create a repo with two BUILD files (one in a subpackage), build a target
# from one to cause it to be cached, then build that target again after
# expunging to verify it is cached.
# Then, restart the worker and build a target in the other build file.
self.ScratchFile(
'MODULE.bazel',
[
'repo = use_repo_rule("//:repo.bzl", "repo")',
'repo(name = "my_repo")',
],
)

self.ScratchFile('BUILD.bazel')
self.ScratchFile(
'repo.bzl',
[
'def _repo_impl(rctx):',
(
' rctx.file("BUILD", "filegroup(name=\'root\','
" srcs=['root.txt'])\")"
),
' rctx.file("root.txt", "root")',
(
' rctx.file("sub/BUILD", "filegroup(name=\'sub\','
" srcs=['sub.txt'])\")"
),
' rctx.file("sub/sub.txt", "sub")',
' print("JUST FETCHED")',
' return rctx.repo_metadata(reproducible=True)',
'repo = repository_rule(_repo_impl)',
],
)

repo_dir = self.RepoDir('my_repo')

# First fetch: not cached
_, _, stderr = self.RunBazel(['build', '@my_repo//:root'])
self.assertIn('JUST FETCHED', '\n'.join(stderr))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))

# After expunging: cached
self.RunBazel(['clean', '--expunge'])
_, _, stderr = self.RunBazel(['build', '@my_repo//:root'])
self.assertNotIn('JUST FETCHED', '\n'.join(stderr))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))

# Lose all remote files.
self.ClearRemoteCache()

# Build the other target: fails due to the lost input
_, _, stderr = self.RunBazel(['build', '@my_repo//sub:sub'])
# First restart recovers @my_repo, the next one recovers @platforms.
self.assertEqual(
2,
stderr.count(
'Found transient remote cache error, retrying the build...'
),
)
canonical_repo_name = repo_dir[repo_dir.rfind('/') + 1 :]
stderr = '\n'.join(stderr)
self.assertRegex(
stderr,
'external/%s/sub/BUILD with digest .*/.* no longer available in the'
' remote cache'
% re.escape(canonical_repo_name),
)
self.assertIn('JUST FETCHED', stderr)
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))

# After expunging again: cached
self.RunBazel(['clean', '--expunge'])
_, _, stderr = self.RunBazel(['build', '@my_repo//sub:sub'])
self.assertNotIn('JUST FETCHED', '\n'.join(stderr))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'BUILD')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'root.txt')))
self.assertFalse(os.path.exists(os.path.join(repo_dir, 'sub/BUILD')))
self.assertTrue(os.path.exists(os.path.join(repo_dir, 'sub/sub.txt')))

def testMemoryPressureRestartDuringCachedFetch(self):
# Regression test for a cached repo fetch that is interrupted by memory
# pressure (Skyframe drops the fetch's WorkerSkyKeyComputeState, which
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2026 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.

import json
from src.test.py.bazel import test_base


class RemoteRepoContentsCacheTestBase(test_base.TestBase):
"""Common setup for tests of the remote repo contents cache."""

def setUp(self):
test_base.TestBase.setUp(self)
self._worker_port = self.StartRemoteWorker()
self.ScratchFile('.bazelrc', self.BazelrcLines())

def tearDown(self):
test_base.TestBase.tearDown(self)
self.StopRemoteWorker()

def BazelrcLines(self):
"""Returns the lines of the .bazelrc shared by all tests."""
return [
'startup --experimental_remote_repo_contents_cache',
# Only use the remote repo contents cache.
'common --repo_contents_cache=',
'common --remote_cache=grpc://localhost:' + str(self._worker_port),
'common --auth_enabled=false',
'common --remote_timeout=3600s',
'common --verbose_failures',
]

def RepoDir(self, repo_name, cwd=None):
_, stdout, _ = self.RunBazel(['info', 'output_base'], cwd=cwd)
self.assertLen(stdout, 1)
output_base = stdout[0].strip()

_, stdout, _ = self.RunBazel(['mod', 'dump_repo_mapping', ''], cwd=cwd)
self.assertLen(stdout, 1)
mapping = json.loads(stdout[0])
canonical_repo_name = mapping[repo_name]

return output_base + '/external/' + canonical_repo_name