# Copyright (C) 2016-2023 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 . # # 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)