repository_test.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import logging
  2. import os
  3. import sys
  4. from typing import Optional
  5. import pytest
  6. from ..checksums import xxh64
  7. from ..helpers import Location
  8. from ..helpers import IntegrityError
  9. from ..platformflags import is_win32
  10. from ..remote import RemoteRepository, InvalidRPCMethod, PathNotAllowed
  11. from ..repository import Repository, MAX_DATA_SIZE
  12. from ..repoobj import RepoObj
  13. from .hashindex_test import H
  14. @pytest.fixture()
  15. def repository(tmp_path):
  16. repository_location = os.fspath(tmp_path / "repository")
  17. yield Repository(repository_location, exclusive=True, create=True)
  18. @pytest.fixture()
  19. def remote_repository(tmp_path):
  20. if is_win32:
  21. pytest.skip("Remote repository does not yet work on Windows.")
  22. repository_location = Location("ssh://__testsuite__/" + os.fspath(tmp_path / "repository"))
  23. yield RemoteRepository(repository_location, exclusive=True, create=True)
  24. def pytest_generate_tests(metafunc):
  25. # Generates tests that run on both local and remote repos
  26. if "repo_fixtures" in metafunc.fixturenames:
  27. metafunc.parametrize("repo_fixtures", ["repository", "remote_repository"])
  28. def get_repository_from_fixture(repo_fixtures, request):
  29. # returns the repo object from the fixture for tests that run on both local and remote repos
  30. return request.getfixturevalue(repo_fixtures)
  31. def reopen(repository, exclusive: Optional[bool] = True, create=False):
  32. if isinstance(repository, Repository):
  33. if repository.opened:
  34. raise RuntimeError("Repo must be closed before a reopen. Cannot support nested repository contexts.")
  35. return Repository(repository._location, exclusive=exclusive, create=create)
  36. if isinstance(repository, RemoteRepository):
  37. if repository.p is not None or repository.sock is not None:
  38. raise RuntimeError("Remote repo must be closed before a reopen. Cannot support nested repository contexts.")
  39. return RemoteRepository(repository.location, exclusive=exclusive, create=create)
  40. raise TypeError(
  41. f"Invalid argument type. Expected 'Repository' or 'RemoteRepository', received '{type(repository).__name__}'."
  42. )
  43. def fchunk(data, meta=b""):
  44. # format chunk: create a raw chunk that has valid RepoObj layout, but does not use encryption or compression.
  45. hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data))
  46. assert isinstance(data, bytes)
  47. chunk = hdr + meta + data
  48. return chunk
  49. def pchunk(chunk):
  50. # parse chunk: parse data and meta from a raw chunk made by fchunk
  51. hdr_size = RepoObj.obj_header.size
  52. hdr = chunk[:hdr_size]
  53. meta_size, data_size = RepoObj.obj_header.unpack(hdr)[0:2]
  54. meta = chunk[hdr_size : hdr_size + meta_size]
  55. data = chunk[hdr_size + meta_size : hdr_size + meta_size + data_size]
  56. return data, meta
  57. def pdchunk(chunk):
  58. # parse only data from a raw chunk made by fchunk
  59. return pchunk(chunk)[0]
  60. def test_basic_operations(repo_fixtures, request):
  61. with get_repository_from_fixture(repo_fixtures, request) as repository:
  62. for x in range(100):
  63. repository.put(H(x), fchunk(b"SOMEDATA"))
  64. key50 = H(50)
  65. assert pdchunk(repository.get(key50)) == b"SOMEDATA"
  66. repository.delete(key50)
  67. with pytest.raises(Repository.ObjectNotFound):
  68. repository.get(key50)
  69. with reopen(repository) as repository:
  70. with pytest.raises(Repository.ObjectNotFound):
  71. repository.get(key50)
  72. for x in range(100):
  73. if x == 50:
  74. continue
  75. assert pdchunk(repository.get(H(x))) == b"SOMEDATA"
  76. def test_read_data(repo_fixtures, request):
  77. with get_repository_from_fixture(repo_fixtures, request) as repository:
  78. meta, data = b"meta", b"data"
  79. hdr = RepoObj.obj_header.pack(len(meta), len(data), xxh64(meta), xxh64(data))
  80. chunk_complete = hdr + meta + data
  81. chunk_short = hdr + meta
  82. repository.put(H(0), chunk_complete)
  83. assert repository.get(H(0)) == chunk_complete
  84. assert repository.get(H(0), read_data=True) == chunk_complete
  85. assert repository.get(H(0), read_data=False) == chunk_short
  86. def test_consistency(repo_fixtures, request):
  87. with get_repository_from_fixture(repo_fixtures, request) as repository:
  88. repository.put(H(0), fchunk(b"foo"))
  89. assert pdchunk(repository.get(H(0))) == b"foo"
  90. repository.put(H(0), fchunk(b"foo2"))
  91. assert pdchunk(repository.get(H(0))) == b"foo2"
  92. repository.put(H(0), fchunk(b"bar"))
  93. assert pdchunk(repository.get(H(0))) == b"bar"
  94. repository.delete(H(0))
  95. with pytest.raises(Repository.ObjectNotFound):
  96. repository.get(H(0))
  97. def test_list(repo_fixtures, request):
  98. with get_repository_from_fixture(repo_fixtures, request) as repository:
  99. for x in range(100):
  100. repository.put(H(x), fchunk(b"SOMEDATA"))
  101. repo_list = repository.list()
  102. assert len(repo_list) == 100
  103. first_half = repository.list(limit=50)
  104. assert len(first_half) == 50
  105. assert first_half == repo_list[:50]
  106. second_half = repository.list(marker=first_half[-1][0])
  107. assert len(second_half) == 50
  108. assert second_half == repo_list[50:]
  109. assert len(repository.list(limit=50)) == 50
  110. def test_max_data_size(repo_fixtures, request):
  111. with get_repository_from_fixture(repo_fixtures, request) as repository:
  112. max_data = b"x" * (MAX_DATA_SIZE - RepoObj.obj_header.size)
  113. repository.put(H(0), fchunk(max_data))
  114. assert pdchunk(repository.get(H(0))) == max_data
  115. with pytest.raises(IntegrityError):
  116. repository.put(H(1), fchunk(max_data + b"x"))
  117. def check(repository, repo_path, repair=False, status=True):
  118. assert repository.check(repair=repair) == status
  119. # Make sure no tmp files are left behind
  120. tmp_files = [name for name in os.listdir(repo_path) if "tmp" in name]
  121. assert tmp_files == [], "Found tmp files"
  122. def _get_mock_args():
  123. class MockArgs:
  124. remote_path = "borg"
  125. umask = 0o077
  126. debug_topics = []
  127. rsh = None
  128. def __contains__(self, item):
  129. # to behave like argparse.Namespace
  130. return hasattr(self, item)
  131. return MockArgs()
  132. def test_remote_invalid_rpc(remote_repository):
  133. with remote_repository:
  134. with pytest.raises(InvalidRPCMethod):
  135. remote_repository.call("__init__", {})
  136. def test_remote_rpc_exception_transport(remote_repository):
  137. with remote_repository:
  138. s1 = "test string"
  139. try:
  140. remote_repository.call("inject_exception", {"kind": "DoesNotExist"})
  141. except Repository.DoesNotExist as e:
  142. assert len(e.args) == 1
  143. assert e.args[0] == remote_repository.location.processed
  144. try:
  145. remote_repository.call("inject_exception", {"kind": "AlreadyExists"})
  146. except Repository.AlreadyExists as e:
  147. assert len(e.args) == 1
  148. assert e.args[0] == remote_repository.location.processed
  149. try:
  150. remote_repository.call("inject_exception", {"kind": "CheckNeeded"})
  151. except Repository.CheckNeeded as e:
  152. assert len(e.args) == 1
  153. assert e.args[0] == remote_repository.location.processed
  154. try:
  155. remote_repository.call("inject_exception", {"kind": "IntegrityError"})
  156. except IntegrityError as e:
  157. assert len(e.args) == 1
  158. assert e.args[0] == s1
  159. try:
  160. remote_repository.call("inject_exception", {"kind": "PathNotAllowed"})
  161. except PathNotAllowed as e:
  162. assert len(e.args) == 1
  163. assert e.args[0] == "foo"
  164. try:
  165. remote_repository.call("inject_exception", {"kind": "ObjectNotFound"})
  166. except Repository.ObjectNotFound as e:
  167. assert len(e.args) == 2
  168. assert e.args[0] == s1
  169. assert e.args[1] == remote_repository.location.processed
  170. try:
  171. remote_repository.call("inject_exception", {"kind": "InvalidRPCMethod"})
  172. except InvalidRPCMethod as e:
  173. assert len(e.args) == 1
  174. assert e.args[0] == s1
  175. try:
  176. remote_repository.call("inject_exception", {"kind": "divide"})
  177. except RemoteRepository.RPCError as e:
  178. assert e.unpacked
  179. assert e.get_message() == "ZeroDivisionError: integer division or modulo by zero\n"
  180. assert e.exception_class == "ZeroDivisionError"
  181. assert len(e.exception_full) > 0
  182. def test_remote_ssh_cmd(remote_repository):
  183. with remote_repository:
  184. args = _get_mock_args()
  185. remote_repository._args = args
  186. assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "example.com"]
  187. assert remote_repository.ssh_cmd(Location("ssh://user@example.com/foo")) == ["ssh", "user@example.com"]
  188. assert remote_repository.ssh_cmd(Location("ssh://user@example.com:1234/foo")) == [
  189. "ssh",
  190. "-p",
  191. "1234",
  192. "user@example.com",
  193. ]
  194. os.environ["BORG_RSH"] = "ssh --foo"
  195. assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "--foo", "example.com"]
  196. def test_remote_borg_cmd(remote_repository):
  197. with remote_repository:
  198. assert remote_repository.borg_cmd(None, testing=True) == [sys.executable, "-m", "borg", "serve"]
  199. args = _get_mock_args()
  200. # XXX without next line we get spurious test fails when using pytest-xdist, root cause unknown:
  201. logging.getLogger().setLevel(logging.INFO)
  202. # note: test logger is on info log level, so --info gets added automagically
  203. assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"]
  204. args.remote_path = "borg-0.28.2"
  205. assert remote_repository.borg_cmd(args, testing=False) == ["borg-0.28.2", "serve", "--info"]
  206. args.debug_topics = ["something_client_side", "repository_compaction"]
  207. assert remote_repository.borg_cmd(args, testing=False) == [
  208. "borg-0.28.2",
  209. "serve",
  210. "--info",
  211. "--debug-topic=borg.debug.repository_compaction",
  212. ]
  213. args = _get_mock_args()
  214. assert remote_repository.borg_cmd(args, testing=False) == ["borg", "serve", "--info"]
  215. args.rsh = "ssh -i foo"
  216. remote_repository._args = args
  217. assert remote_repository.ssh_cmd(Location("ssh://example.com/foo")) == ["ssh", "-i", "foo", "example.com"]