|
|
# Copyright (C) 2016-2024 RhodeCode GmbH
|
|
|
#
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
# it under the terms of the GNU Affero General Public License, version 3
|
|
|
# (only), as published by the Free Software Foundation.
|
|
|
#
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
# GNU General Public License for more details.
|
|
|
#
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
#
|
|
|
# This program is dual-licensed. If you wish to learn more about the
|
|
|
# RhodeCode Enterprise Edition, including its added features, Support services,
|
|
|
# and proprietary license terms, please see https://rhodecode.com/licenses/
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
from mock import call, patch
|
|
|
|
|
|
from rhodecode.lib.vcs.backends.base import Reference
|
|
|
|
|
|
|
|
|
class TestMercurialRemoteRepoInvalidation(object):
|
|
|
"""
|
|
|
If the VCSServer is running with multiple processes or/and instances.
|
|
|
Operations on repositories are potentially handled by different processes
|
|
|
in a random fashion. The mercurial repository objects used in the VCSServer
|
|
|
are caching the commits of the repo. Therefore we have to invalidate the
|
|
|
VCSServer caching of these objects after a writing operation.
|
|
|
"""
|
|
|
|
|
|
# Default reference used as a dummy during tests.
|
|
|
default_ref = Reference("branch", "default", None)
|
|
|
|
|
|
# Methods of vcsserver.hg.HgRemote that are "writing" operations.
|
|
|
writing_methods = [
|
|
|
"bookmark",
|
|
|
"commit",
|
|
|
"merge",
|
|
|
"pull",
|
|
|
"pull_cmd",
|
|
|
"rebase",
|
|
|
"strip",
|
|
|
"tag",
|
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
"method_name, method_args",
|
|
|
[
|
|
|
("_local_merge", [default_ref, None, None, None, default_ref]),
|
|
|
("_local_pull", ["", default_ref]),
|
|
|
("bookmark", [None]),
|
|
|
("pull", ["", default_ref]),
|
|
|
("remove_tag", ["mytag", None]),
|
|
|
("strip", [None]),
|
|
|
("tag", ["newtag", None]),
|
|
|
],
|
|
|
)
|
|
|
def test_method_invokes_invalidate_on_remote_repo(self, method_name, method_args, backend_hg):
|
|
|
"""
|
|
|
Check that the listed methods are invalidating the VCSServer cache
|
|
|
after invoking a writing method of their remote repository object.
|
|
|
"""
|
|
|
tags = {"mytag": "mytag-id"}
|
|
|
|
|
|
def add_tag(name, raw_id, *args, **kwds):
|
|
|
tags[name] = raw_id
|
|
|
|
|
|
repo = backend_hg.repo.scm_instance()
|
|
|
|
|
|
with patch.object(repo, "_remote") as remote:
|
|
|
repo.tags = tags
|
|
|
remote.lookup.return_value = ("commit-id", "commit-idx")
|
|
|
remote.tags.return_value = tags
|
|
|
remote._get_tags.return_value = tags
|
|
|
remote.is_empty.return_value = False
|
|
|
remote.tag.side_effect = add_tag
|
|
|
|
|
|
# Invoke method.
|
|
|
method = getattr(repo, method_name)
|
|
|
method(*method_args)
|
|
|
|
|
|
# Assert that every "writing" method is followed by an invocation
|
|
|
# of the cache invalidation method.
|
|
|
for counter, method_call in enumerate(remote.method_calls):
|
|
|
call_name = method_call[0]
|
|
|
if call_name in self.writing_methods:
|
|
|
next_call = remote.method_calls[counter + 1]
|
|
|
assert next_call == call.invalidate_vcs_cache()
|
|
|
|
|
|
def _prepare_shadow_repo(self, pull_request):
|
|
|
"""
|
|
|
Helper that creates a shadow repo that can be used to reproduce the
|
|
|
CommitDoesNotExistError when pulling in from target and source
|
|
|
references.
|
|
|
"""
|
|
|
from rhodecode.model.pull_request import PullRequestModel
|
|
|
|
|
|
repo_id = pull_request.target_repo.repo_id
|
|
|
target_vcs = pull_request.target_repo.scm_instance()
|
|
|
target_ref = pull_request.target_ref_parts
|
|
|
source_ref = pull_request.source_ref_parts
|
|
|
|
|
|
# Create shadow repository.
|
|
|
pr = PullRequestModel()
|
|
|
workspace_id = pr._workspace_id(pull_request)
|
|
|
shadow_repository_path = target_vcs._maybe_prepare_merge_workspace(
|
|
|
repo_id, workspace_id, target_ref, source_ref
|
|
|
)
|
|
|
shadow_repo = target_vcs.get_shadow_instance(shadow_repository_path, cache=True)
|
|
|
|
|
|
# This will populate the cache of the mercurial repository object
|
|
|
# inside of the VCSServer.
|
|
|
shadow_repo.get_commit()
|
|
|
|
|
|
return shadow_repo, source_ref, target_ref
|
|
|
|
|
|
@pytest.mark.backends("hg")
|
|
|
def test_commit_does_not_exist_error_happens(self, pr_util, app):
|
|
|
"""
|
|
|
This test is somewhat special. It does not really test the system
|
|
|
instead it is more or less a precondition for the
|
|
|
"test_commit_does_not_exist_error_does_not_happen". It deactivates the
|
|
|
cache invalidation and asserts that the error occurs.
|
|
|
"""
|
|
|
from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
|
|
|
|
|
|
pull_request = pr_util.create_pull_request()
|
|
|
|
|
|
target_vcs = pull_request.target_repo.scm_instance()
|
|
|
source_vcs = pull_request.source_repo.scm_instance()
|
|
|
shadow_repo, source_ref, target_ref = self._prepare_shadow_repo(pull_request)
|
|
|
|
|
|
initial_cache_uid = shadow_repo._remote._wire["context"]
|
|
|
initial_commit_ids = shadow_repo._remote.get_all_commit_ids("visible")
|
|
|
|
|
|
# Pull from target and source references but without invalidation of
|
|
|
# RemoteRepo objects and without VCSServer caching of mercurial repository objects.
|
|
|
with patch.object(shadow_repo._remote, "invalidate_vcs_cache"):
|
|
|
# NOTE: Do not use patch.dict() to disable the cache because it
|
|
|
# restores the WHOLE dict and not only the patched keys.
|
|
|
shadow_repo._remote._wire["cache"] = False
|
|
|
shadow_repo._local_pull(target_vcs.path, target_ref)
|
|
|
shadow_repo._local_pull(source_vcs.path, source_ref)
|
|
|
shadow_repo._remote._wire["cache"] = True
|
|
|
|
|
|
# Try to lookup the target_ref in shadow repo. This should work because
|
|
|
# test_repo_maker_uses_session_for_instance_methods
|
|
|
# the shadow repo is a clone of the target and always contains all off
|
|
|
# it's commits in the initial cache.
|
|
|
shadow_repo.get_commit(target_ref.commit_id)
|
|
|
|
|
|
# we ensure that call context has not changed, this is what
|
|
|
# `invalidate_vcs_cache` does
|
|
|
assert initial_cache_uid == shadow_repo._remote._wire["context"]
|
|
|
|
|
|
# If we try to lookup all commits.
|
|
|
# repo commit cache doesn't get invalidated. (Due to patched
|
|
|
# invalidation and caching above).
|
|
|
assert initial_commit_ids == shadow_repo._remote.get_all_commit_ids("visible")
|
|
|
|
|
|
@pytest.mark.backends("hg")
|
|
|
def test_commit_does_not_exist_error_does_not_happen(self, pr_util, app):
|
|
|
"""
|
|
|
This test simulates a pull request merge in which the pull operations
|
|
|
are handled by a different VCSServer process than all other operations.
|
|
|
Without correct cache invalidation this leads to an error when
|
|
|
retrieving the pulled commits afterwards.
|
|
|
"""
|
|
|
|
|
|
pull_request = pr_util.create_pull_request()
|
|
|
target_vcs = pull_request.target_repo.scm_instance()
|
|
|
source_vcs = pull_request.source_repo.scm_instance()
|
|
|
shadow_repo, source_ref, target_ref = self._prepare_shadow_repo(pull_request)
|
|
|
|
|
|
# Pull from target and source references without without VCSServer
|
|
|
# caching of mercurial repository objects but with active invalidation
|
|
|
# of RemoteRepo objects.
|
|
|
# NOTE: Do not use patch.dict() to disable the cache because it
|
|
|
# restores the WHOLE dict and not only the patched keys.
|
|
|
shadow_repo._remote._wire["cache"] = False
|
|
|
shadow_repo._local_pull(target_vcs.path, target_ref)
|
|
|
shadow_repo._local_pull(source_vcs.path, source_ref)
|
|
|
shadow_repo._remote._wire["cache"] = True
|
|
|
|
|
|
# Try to lookup the target and source references in shadow repo. This
|
|
|
# should work because the RemoteRepo object gets invalidated during the
|
|
|
# above pull operations.
|
|
|
shadow_repo.get_commit(target_ref.commit_id)
|
|
|
shadow_repo.get_commit(source_ref.commit_id)
|
|
|
|