helpers.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. import hashlib
  2. import io
  3. import logging
  4. from time import mktime, strptime
  5. from datetime import datetime, timezone, timedelta
  6. from io import StringIO
  7. import os
  8. import pytest
  9. import sys
  10. import msgpack
  11. import msgpack.fallback
  12. import time
  13. from ..helpers import Location, format_file_size, format_timedelta, make_path_safe, clean_lines, \
  14. prune_within, prune_split, get_cache_dir, get_keys_dir, Statistics, is_slow_msgpack, \
  15. yes, TRUISH, FALSISH, DEFAULTISH, \
  16. StableDict, int_to_bigint, bigint_to_int, bin_to_hex, parse_timestamp, ChunkerParams, Chunk, \
  17. ProgressIndicatorPercent, ProgressIndicatorEndless, load_excludes, parse_pattern, \
  18. CompressionSpec, CompressionDecider1, CompressionDecider2, \
  19. PatternMatcher, RegexPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, partial_format, ChunkIteratorFileWrapper
  20. from . import BaseTestCase, environment_variable, FakeInputs
  21. class BigIntTestCase(BaseTestCase):
  22. def test_bigint(self):
  23. self.assert_equal(int_to_bigint(0), 0)
  24. self.assert_equal(int_to_bigint(2**63-1), 2**63-1)
  25. self.assert_equal(int_to_bigint(-2**63+1), -2**63+1)
  26. self.assert_equal(int_to_bigint(2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\x00')
  27. self.assert_equal(int_to_bigint(-2**63), b'\x00\x00\x00\x00\x00\x00\x00\x80\xff')
  28. self.assert_equal(bigint_to_int(int_to_bigint(-2**70)), -2**70)
  29. self.assert_equal(bigint_to_int(int_to_bigint(2**70)), 2**70)
  30. def test_bin_to_hex():
  31. assert bin_to_hex(b'') == ''
  32. assert bin_to_hex(b'\x00\x01\xff') == '0001ff'
  33. class TestLocationWithoutEnv:
  34. def test_ssh(self, monkeypatch):
  35. monkeypatch.delenv('BORG_REPO', raising=False)
  36. assert repr(Location('ssh://user@host:1234/some/path::archive')) == \
  37. "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')"
  38. assert repr(Location('ssh://user@host:1234/some/path')) == \
  39. "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
  40. def test_file(self, monkeypatch):
  41. monkeypatch.delenv('BORG_REPO', raising=False)
  42. assert repr(Location('file:///some/path::archive')) == \
  43. "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')"
  44. assert repr(Location('file:///some/path')) == \
  45. "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)"
  46. def test_scp(self, monkeypatch):
  47. monkeypatch.delenv('BORG_REPO', raising=False)
  48. assert repr(Location('user@host:/some/path::archive')) == \
  49. "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
  50. assert repr(Location('user@host:/some/path')) == \
  51. "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
  52. def test_folder(self, monkeypatch):
  53. monkeypatch.delenv('BORG_REPO', raising=False)
  54. assert repr(Location('path::archive')) == \
  55. "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')"
  56. assert repr(Location('path')) == \
  57. "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)"
  58. def test_abspath(self, monkeypatch):
  59. monkeypatch.delenv('BORG_REPO', raising=False)
  60. assert repr(Location('/some/absolute/path::archive')) == \
  61. "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
  62. assert repr(Location('/some/absolute/path')) == \
  63. "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
  64. def test_relpath(self, monkeypatch):
  65. monkeypatch.delenv('BORG_REPO', raising=False)
  66. assert repr(Location('some/relative/path::archive')) == \
  67. "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
  68. assert repr(Location('some/relative/path')) == \
  69. "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
  70. def test_underspecified(self, monkeypatch):
  71. monkeypatch.delenv('BORG_REPO', raising=False)
  72. with pytest.raises(ValueError):
  73. Location('::archive')
  74. with pytest.raises(ValueError):
  75. Location('::')
  76. with pytest.raises(ValueError):
  77. Location()
  78. def test_no_double_colon(self, monkeypatch):
  79. monkeypatch.delenv('BORG_REPO', raising=False)
  80. with pytest.raises(ValueError):
  81. Location('ssh://localhost:22/path:archive')
  82. def test_no_slashes(self, monkeypatch):
  83. monkeypatch.delenv('BORG_REPO', raising=False)
  84. with pytest.raises(ValueError):
  85. Location('/some/path/to/repo::archive_name_with/slashes/is_invalid')
  86. def test_canonical_path(self, monkeypatch):
  87. monkeypatch.delenv('BORG_REPO', raising=False)
  88. locations = ['some/path::archive', 'file://some/path::archive', 'host:some/path::archive',
  89. 'host:~user/some/path::archive', 'ssh://host/some/path::archive',
  90. 'ssh://user@host:1234/some/path::archive']
  91. for location in locations:
  92. assert Location(location).canonical_path() == \
  93. Location(Location(location).canonical_path()).canonical_path()
  94. def test_format_path(self, monkeypatch):
  95. monkeypatch.delenv('BORG_REPO', raising=False)
  96. test_pid = os.getpid()
  97. assert repr(Location('/some/path::archive{pid}')) == \
  98. "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive{}')".format(test_pid)
  99. location_time1 = Location('/some/path::archive{now:%s}')
  100. time.sleep(1.1)
  101. location_time2 = Location('/some/path::archive{now:%s}')
  102. assert location_time1.archive != location_time2.archive
  103. class TestLocationWithEnv:
  104. def test_ssh(self, monkeypatch):
  105. monkeypatch.setenv('BORG_REPO', 'ssh://user@host:1234/some/path')
  106. assert repr(Location('::archive')) == \
  107. "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive='archive')"
  108. assert repr(Location()) == \
  109. "Location(proto='ssh', user='user', host='host', port=1234, path='/some/path', archive=None)"
  110. def test_file(self, monkeypatch):
  111. monkeypatch.setenv('BORG_REPO', 'file:///some/path')
  112. assert repr(Location('::archive')) == \
  113. "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive='archive')"
  114. assert repr(Location()) == \
  115. "Location(proto='file', user=None, host=None, port=None, path='/some/path', archive=None)"
  116. def test_scp(self, monkeypatch):
  117. monkeypatch.setenv('BORG_REPO', 'user@host:/some/path')
  118. assert repr(Location('::archive')) == \
  119. "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive='archive')"
  120. assert repr(Location()) == \
  121. "Location(proto='ssh', user='user', host='host', port=None, path='/some/path', archive=None)"
  122. def test_folder(self, monkeypatch):
  123. monkeypatch.setenv('BORG_REPO', 'path')
  124. assert repr(Location('::archive')) == \
  125. "Location(proto='file', user=None, host=None, port=None, path='path', archive='archive')"
  126. assert repr(Location()) == \
  127. "Location(proto='file', user=None, host=None, port=None, path='path', archive=None)"
  128. def test_abspath(self, monkeypatch):
  129. monkeypatch.setenv('BORG_REPO', '/some/absolute/path')
  130. assert repr(Location('::archive')) == \
  131. "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive='archive')"
  132. assert repr(Location()) == \
  133. "Location(proto='file', user=None, host=None, port=None, path='/some/absolute/path', archive=None)"
  134. def test_relpath(self, monkeypatch):
  135. monkeypatch.setenv('BORG_REPO', 'some/relative/path')
  136. assert repr(Location('::archive')) == \
  137. "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive='archive')"
  138. assert repr(Location()) == \
  139. "Location(proto='file', user=None, host=None, port=None, path='some/relative/path', archive=None)"
  140. def test_no_slashes(self, monkeypatch):
  141. monkeypatch.setenv('BORG_REPO', '/some/absolute/path')
  142. with pytest.raises(ValueError):
  143. Location('::archive_name_with/slashes/is_invalid')
  144. class FormatTimedeltaTestCase(BaseTestCase):
  145. def test(self):
  146. t0 = datetime(2001, 1, 1, 10, 20, 3, 0)
  147. t1 = datetime(2001, 1, 1, 12, 20, 4, 100000)
  148. self.assert_equal(
  149. format_timedelta(t1 - t0),
  150. '2 hours 1.10 seconds'
  151. )
  152. def check_patterns(files, pattern, expected):
  153. """Utility for testing patterns.
  154. """
  155. assert all([f == os.path.normpath(f) for f in files]), "Pattern matchers expect normalized input paths"
  156. matched = [f for f in files if pattern.match(f)]
  157. assert matched == (files if expected is None else expected)
  158. @pytest.mark.parametrize("pattern, expected", [
  159. # "None" means all files, i.e. all match the given pattern
  160. ("/", None),
  161. ("/./", None),
  162. ("", []),
  163. ("/home/u", []),
  164. ("/home/user", ["/home/user/.profile", "/home/user/.bashrc"]),
  165. ("/etc", ["/etc/server/config", "/etc/server/hosts"]),
  166. ("///etc//////", ["/etc/server/config", "/etc/server/hosts"]),
  167. ("/./home//..//home/user2", ["/home/user2/.profile", "/home/user2/public_html/index.html"]),
  168. ("/srv", ["/srv/messages", "/srv/dmesg"]),
  169. ])
  170. def test_patterns_prefix(pattern, expected):
  171. files = [
  172. "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc",
  173. "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv/messages", "/srv/dmesg",
  174. ]
  175. check_patterns(files, PathPrefixPattern(pattern), expected)
  176. @pytest.mark.parametrize("pattern, expected", [
  177. # "None" means all files, i.e. all match the given pattern
  178. ("", []),
  179. ("foo", []),
  180. ("relative", ["relative/path1", "relative/two"]),
  181. ("more", ["more/relative"]),
  182. ])
  183. def test_patterns_prefix_relative(pattern, expected):
  184. files = ["relative/path1", "relative/two", "more/relative"]
  185. check_patterns(files, PathPrefixPattern(pattern), expected)
  186. @pytest.mark.parametrize("pattern, expected", [
  187. # "None" means all files, i.e. all match the given pattern
  188. ("/*", None),
  189. ("/./*", None),
  190. ("*", None),
  191. ("*/*", None),
  192. ("*///*", None),
  193. ("/home/u", []),
  194. ("/home/*",
  195. ["/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", "/home/user2/public_html/index.html",
  196. "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]),
  197. ("/home/user/*", ["/home/user/.profile", "/home/user/.bashrc"]),
  198. ("/etc/*", ["/etc/server/config", "/etc/server/hosts"]),
  199. ("*/.pr????e", ["/home/user/.profile", "/home/user2/.profile"]),
  200. ("///etc//////*", ["/etc/server/config", "/etc/server/hosts"]),
  201. ("/./home//..//home/user2/*", ["/home/user2/.profile", "/home/user2/public_html/index.html"]),
  202. ("/srv*", ["/srv/messages", "/srv/dmesg"]),
  203. ("/home/*/.thumbnails", ["/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]),
  204. ])
  205. def test_patterns_fnmatch(pattern, expected):
  206. files = [
  207. "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc",
  208. "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv/messages", "/srv/dmesg",
  209. "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails",
  210. ]
  211. check_patterns(files, FnmatchPattern(pattern), expected)
  212. @pytest.mark.parametrize("pattern, expected", [
  213. # "None" means all files, i.e. all match the given pattern
  214. ("*", None),
  215. ("**/*", None),
  216. ("/**/*", None),
  217. ("/./*", None),
  218. ("*/*", None),
  219. ("*///*", None),
  220. ("/home/u", []),
  221. ("/home/*",
  222. ["/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile", "/home/user2/public_html/index.html",
  223. "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails"]),
  224. ("/home/user/*", ["/home/user/.profile", "/home/user/.bashrc"]),
  225. ("/etc/*/*", ["/etc/server/config", "/etc/server/hosts"]),
  226. ("/etc/**/*", ["/etc/server/config", "/etc/server/hosts"]),
  227. ("/etc/**/*/*", ["/etc/server/config", "/etc/server/hosts"]),
  228. ("*/.pr????e", []),
  229. ("**/.pr????e", ["/home/user/.profile", "/home/user2/.profile"]),
  230. ("///etc//////*", ["/etc/server/config", "/etc/server/hosts"]),
  231. ("/./home//..//home/user2/", ["/home/user2/.profile", "/home/user2/public_html/index.html"]),
  232. ("/./home//..//home/user2/**/*", ["/home/user2/.profile", "/home/user2/public_html/index.html"]),
  233. ("/srv*/", ["/srv/messages", "/srv/dmesg", "/srv2/blafasel"]),
  234. ("/srv*", ["/srv", "/srv/messages", "/srv/dmesg", "/srv2", "/srv2/blafasel"]),
  235. ("/srv/*", ["/srv/messages", "/srv/dmesg"]),
  236. ("/srv2/**", ["/srv2", "/srv2/blafasel"]),
  237. ("/srv2/**/", ["/srv2/blafasel"]),
  238. ("/home/*/.thumbnails", ["/home/foo/.thumbnails"]),
  239. ("/home/*/*/.thumbnails", ["/home/foo/bar/.thumbnails"]),
  240. ])
  241. def test_patterns_shell(pattern, expected):
  242. files = [
  243. "/etc/server/config", "/etc/server/hosts", "/home", "/home/user/.profile", "/home/user/.bashrc",
  244. "/home/user2/.profile", "/home/user2/public_html/index.html", "/srv", "/srv/messages", "/srv/dmesg",
  245. "/srv2", "/srv2/blafasel", "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails",
  246. ]
  247. check_patterns(files, ShellPattern(pattern), expected)
  248. @pytest.mark.parametrize("pattern, expected", [
  249. # "None" means all files, i.e. all match the given pattern
  250. ("", None),
  251. (".*", None),
  252. ("^/", None),
  253. ("^abc$", []),
  254. ("^[^/]", []),
  255. ("^(?!/srv|/foo|/opt)",
  256. ["/home", "/home/user/.profile", "/home/user/.bashrc", "/home/user2/.profile",
  257. "/home/user2/public_html/index.html", "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails", ]),
  258. ])
  259. def test_patterns_regex(pattern, expected):
  260. files = [
  261. '/srv/data', '/foo/bar', '/home',
  262. '/home/user/.profile', '/home/user/.bashrc',
  263. '/home/user2/.profile', '/home/user2/public_html/index.html',
  264. '/opt/log/messages.txt', '/opt/log/dmesg.txt',
  265. "/home/foo/.thumbnails", "/home/foo/bar/.thumbnails",
  266. ]
  267. obj = RegexPattern(pattern)
  268. assert str(obj) == pattern
  269. assert obj.pattern == pattern
  270. check_patterns(files, obj, expected)
  271. def test_regex_pattern():
  272. # The forward slash must match the platform-specific path separator
  273. assert RegexPattern("^/$").match("/")
  274. assert RegexPattern("^/$").match(os.path.sep)
  275. assert not RegexPattern(r"^\\$").match("/")
  276. def use_normalized_unicode():
  277. return sys.platform in ("darwin",)
  278. def _make_test_patterns(pattern):
  279. return [PathPrefixPattern(pattern),
  280. FnmatchPattern(pattern),
  281. RegexPattern("^{}/foo$".format(pattern)),
  282. ShellPattern(pattern),
  283. ]
  284. @pytest.mark.parametrize("pattern", _make_test_patterns("b\N{LATIN SMALL LETTER A WITH ACUTE}"))
  285. def test_composed_unicode_pattern(pattern):
  286. assert pattern.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo")
  287. assert pattern.match("ba\N{COMBINING ACUTE ACCENT}/foo") == use_normalized_unicode()
  288. @pytest.mark.parametrize("pattern", _make_test_patterns("ba\N{COMBINING ACUTE ACCENT}"))
  289. def test_decomposed_unicode_pattern(pattern):
  290. assert pattern.match("b\N{LATIN SMALL LETTER A WITH ACUTE}/foo") == use_normalized_unicode()
  291. assert pattern.match("ba\N{COMBINING ACUTE ACCENT}/foo")
  292. @pytest.mark.parametrize("pattern", _make_test_patterns(str(b"ba\x80", "latin1")))
  293. def test_invalid_unicode_pattern(pattern):
  294. assert not pattern.match("ba/foo")
  295. assert pattern.match(str(b"ba\x80/foo", "latin1"))
  296. @pytest.mark.parametrize("lines, expected", [
  297. # "None" means all files, i.e. none excluded
  298. ([], None),
  299. (["# Comment only"], None),
  300. (["*"], []),
  301. (["# Comment",
  302. "*/something00.txt",
  303. " *whitespace* ",
  304. # Whitespace before comment
  305. " #/ws*",
  306. # Empty line
  307. "",
  308. "# EOF"],
  309. ["/more/data", "/home", " #/wsfoobar"]),
  310. (["re:.*"], []),
  311. (["re:\s"], ["/data/something00.txt", "/more/data", "/home"]),
  312. ([r"re:(.)(\1)"], ["/more/data", "/home", "\tstart/whitespace", "/whitespace/end\t"]),
  313. (["", "", "",
  314. "# This is a test with mixed pattern styles",
  315. # Case-insensitive pattern
  316. "re:(?i)BAR|ME$",
  317. "",
  318. "*whitespace*",
  319. "fm:*/something00*"],
  320. ["/more/data"]),
  321. ([r" re:^\s "], ["/data/something00.txt", "/more/data", "/home", "/whitespace/end\t"]),
  322. ([r" re:\s$ "], ["/data/something00.txt", "/more/data", "/home", " #/wsfoobar", "\tstart/whitespace"]),
  323. (["pp:./"], None),
  324. (["pp:/"], [" #/wsfoobar", "\tstart/whitespace"]),
  325. (["pp:aaabbb"], None),
  326. (["pp:/data", "pp: #/", "pp:\tstart", "pp:/whitespace"], ["/more/data", "/home"]),
  327. ])
  328. def test_patterns_from_file(tmpdir, lines, expected):
  329. files = [
  330. '/data/something00.txt', '/more/data', '/home',
  331. ' #/wsfoobar',
  332. '\tstart/whitespace',
  333. '/whitespace/end\t',
  334. ]
  335. def evaluate(filename):
  336. matcher = PatternMatcher(fallback=True)
  337. matcher.add(load_excludes(open(filename, "rt")), False)
  338. return [path for path in files if matcher.match(path)]
  339. exclfile = tmpdir.join("exclude.txt")
  340. with exclfile.open("wt") as fh:
  341. fh.write("\n".join(lines))
  342. assert evaluate(str(exclfile)) == (files if expected is None else expected)
  343. @pytest.mark.parametrize("pattern, cls", [
  344. ("", FnmatchPattern),
  345. # Default style
  346. ("*", FnmatchPattern),
  347. ("/data/*", FnmatchPattern),
  348. # fnmatch style
  349. ("fm:", FnmatchPattern),
  350. ("fm:*", FnmatchPattern),
  351. ("fm:/data/*", FnmatchPattern),
  352. ("fm:fm:/data/*", FnmatchPattern),
  353. # Regular expression
  354. ("re:", RegexPattern),
  355. ("re:.*", RegexPattern),
  356. ("re:^/something/", RegexPattern),
  357. ("re:re:^/something/", RegexPattern),
  358. # Path prefix
  359. ("pp:", PathPrefixPattern),
  360. ("pp:/", PathPrefixPattern),
  361. ("pp:/data/", PathPrefixPattern),
  362. ("pp:pp:/data/", PathPrefixPattern),
  363. # Shell-pattern style
  364. ("sh:", ShellPattern),
  365. ("sh:*", ShellPattern),
  366. ("sh:/data/*", ShellPattern),
  367. ("sh:sh:/data/*", ShellPattern),
  368. ])
  369. def test_parse_pattern(pattern, cls):
  370. assert isinstance(parse_pattern(pattern), cls)
  371. @pytest.mark.parametrize("pattern", ["aa:", "fo:*", "00:", "x1:abc"])
  372. def test_parse_pattern_error(pattern):
  373. with pytest.raises(ValueError):
  374. parse_pattern(pattern)
  375. def test_pattern_matcher():
  376. pm = PatternMatcher()
  377. assert pm.fallback is None
  378. for i in ["", "foo", "bar"]:
  379. assert pm.match(i) is None
  380. pm.add([RegexPattern("^a")], "A")
  381. pm.add([RegexPattern("^b"), RegexPattern("^z")], "B")
  382. pm.add([RegexPattern("^$")], "Empty")
  383. pm.fallback = "FileNotFound"
  384. assert pm.match("") == "Empty"
  385. assert pm.match("aaa") == "A"
  386. assert pm.match("bbb") == "B"
  387. assert pm.match("ccc") == "FileNotFound"
  388. assert pm.match("xyz") == "FileNotFound"
  389. assert pm.match("z") == "B"
  390. assert PatternMatcher(fallback="hey!").fallback == "hey!"
  391. def test_compression_specs():
  392. with pytest.raises(ValueError):
  393. CompressionSpec('')
  394. assert CompressionSpec('none') == dict(name='none')
  395. assert CompressionSpec('lz4') == dict(name='lz4')
  396. assert CompressionSpec('zlib') == dict(name='zlib', level=6)
  397. assert CompressionSpec('zlib,0') == dict(name='zlib', level=0)
  398. assert CompressionSpec('zlib,9') == dict(name='zlib', level=9)
  399. with pytest.raises(ValueError):
  400. CompressionSpec('zlib,9,invalid')
  401. assert CompressionSpec('lzma') == dict(name='lzma', level=6)
  402. assert CompressionSpec('lzma,0') == dict(name='lzma', level=0)
  403. assert CompressionSpec('lzma,9') == dict(name='lzma', level=9)
  404. with pytest.raises(ValueError):
  405. CompressionSpec('lzma,9,invalid')
  406. with pytest.raises(ValueError):
  407. CompressionSpec('invalid')
  408. def test_chunkerparams():
  409. assert ChunkerParams('19,23,21,4095') == (19, 23, 21, 4095)
  410. assert ChunkerParams('10,23,16,4095') == (10, 23, 16, 4095)
  411. with pytest.raises(ValueError):
  412. ChunkerParams('19,24,21,4095')
  413. class MakePathSafeTestCase(BaseTestCase):
  414. def test(self):
  415. self.assert_equal(make_path_safe('/foo/bar'), 'foo/bar')
  416. self.assert_equal(make_path_safe('/foo/bar'), 'foo/bar')
  417. self.assert_equal(make_path_safe('/f/bar'), 'f/bar')
  418. self.assert_equal(make_path_safe('fo/bar'), 'fo/bar')
  419. self.assert_equal(make_path_safe('../foo/bar'), 'foo/bar')
  420. self.assert_equal(make_path_safe('../../foo/bar'), 'foo/bar')
  421. self.assert_equal(make_path_safe('/'), '.')
  422. self.assert_equal(make_path_safe('/'), '.')
  423. class MockArchive:
  424. def __init__(self, ts):
  425. self.ts = ts
  426. def __repr__(self):
  427. return repr(self.ts)
  428. class PruneSplitTestCase(BaseTestCase):
  429. def test(self):
  430. def local_to_UTC(month, day):
  431. """Convert noon on the month and day in 2013 to UTC."""
  432. seconds = mktime(strptime('2013-%02d-%02d 12:00' % (month, day), '%Y-%m-%d %H:%M'))
  433. return datetime.fromtimestamp(seconds, tz=timezone.utc)
  434. def subset(lst, indices):
  435. return {lst[i] for i in indices}
  436. def dotest(test_archives, n, skip, indices):
  437. for ta in test_archives, reversed(test_archives):
  438. self.assert_equal(set(prune_split(ta, '%Y-%m', n, skip)),
  439. subset(test_archives, indices))
  440. test_pairs = [(1, 1), (2, 1), (2, 28), (3, 1), (3, 2), (3, 31), (5, 1)]
  441. test_dates = [local_to_UTC(month, day) for month, day in test_pairs]
  442. test_archives = [MockArchive(date) for date in test_dates]
  443. dotest(test_archives, 3, [], [6, 5, 2])
  444. dotest(test_archives, -1, [], [6, 5, 2, 0])
  445. dotest(test_archives, 3, [test_archives[6]], [5, 2, 0])
  446. dotest(test_archives, 3, [test_archives[5]], [6, 2, 0])
  447. dotest(test_archives, 3, [test_archives[4]], [6, 5, 2])
  448. dotest(test_archives, 0, [], [])
  449. class PruneWithinTestCase(BaseTestCase):
  450. def test(self):
  451. def subset(lst, indices):
  452. return {lst[i] for i in indices}
  453. def dotest(test_archives, within, indices):
  454. for ta in test_archives, reversed(test_archives):
  455. self.assert_equal(set(prune_within(ta, within)),
  456. subset(test_archives, indices))
  457. # 1 minute, 1.5 hours, 2.5 hours, 3.5 hours, 25 hours, 49 hours
  458. test_offsets = [60, 90*60, 150*60, 210*60, 25*60*60, 49*60*60]
  459. now = datetime.now(timezone.utc)
  460. test_dates = [now - timedelta(seconds=s) for s in test_offsets]
  461. test_archives = [MockArchive(date) for date in test_dates]
  462. dotest(test_archives, '1H', [0])
  463. dotest(test_archives, '2H', [0, 1])
  464. dotest(test_archives, '3H', [0, 1, 2])
  465. dotest(test_archives, '24H', [0, 1, 2, 3])
  466. dotest(test_archives, '26H', [0, 1, 2, 3, 4])
  467. dotest(test_archives, '2d', [0, 1, 2, 3, 4])
  468. dotest(test_archives, '50H', [0, 1, 2, 3, 4, 5])
  469. dotest(test_archives, '3d', [0, 1, 2, 3, 4, 5])
  470. dotest(test_archives, '1w', [0, 1, 2, 3, 4, 5])
  471. dotest(test_archives, '1m', [0, 1, 2, 3, 4, 5])
  472. dotest(test_archives, '1y', [0, 1, 2, 3, 4, 5])
  473. class StableDictTestCase(BaseTestCase):
  474. def test(self):
  475. d = StableDict(foo=1, bar=2, boo=3, baz=4)
  476. self.assert_equal(list(d.items()), [('bar', 2), ('baz', 4), ('boo', 3), ('foo', 1)])
  477. self.assert_equal(hashlib.md5(msgpack.packb(d)).hexdigest(), 'fc78df42cd60691b3ac3dd2a2b39903f')
  478. class TestParseTimestamp(BaseTestCase):
  479. def test(self):
  480. self.assert_equal(parse_timestamp('2015-04-19T20:25:00.226410'), datetime(2015, 4, 19, 20, 25, 0, 226410, timezone.utc))
  481. self.assert_equal(parse_timestamp('2015-04-19T20:25:00'), datetime(2015, 4, 19, 20, 25, 0, 0, timezone.utc))
  482. def test_get_cache_dir():
  483. """test that get_cache_dir respects environment"""
  484. # reset BORG_CACHE_DIR in order to test default
  485. old_env = None
  486. if os.environ.get('BORG_CACHE_DIR'):
  487. old_env = os.environ['BORG_CACHE_DIR']
  488. del(os.environ['BORG_CACHE_DIR'])
  489. assert get_cache_dir() == os.path.join(os.path.expanduser('~'), '.cache', 'borg')
  490. os.environ['XDG_CACHE_HOME'] = '/var/tmp/.cache'
  491. assert get_cache_dir() == os.path.join('/var/tmp/.cache', 'borg')
  492. os.environ['BORG_CACHE_DIR'] = '/var/tmp'
  493. assert get_cache_dir() == '/var/tmp'
  494. # reset old env
  495. if old_env is not None:
  496. os.environ['BORG_CACHE_DIR'] = old_env
  497. def test_get_keys_dir():
  498. """test that get_keys_dir respects environment"""
  499. # reset BORG_KEYS_DIR in order to test default
  500. old_env = None
  501. if os.environ.get('BORG_KEYS_DIR'):
  502. old_env = os.environ['BORG_KEYS_DIR']
  503. del(os.environ['BORG_KEYS_DIR'])
  504. assert get_keys_dir() == os.path.join(os.path.expanduser('~'), '.config', 'borg', 'keys')
  505. os.environ['XDG_CONFIG_HOME'] = '/var/tmp/.config'
  506. assert get_keys_dir() == os.path.join('/var/tmp/.config', 'borg', 'keys')
  507. os.environ['BORG_KEYS_DIR'] = '/var/tmp'
  508. assert get_keys_dir() == '/var/tmp'
  509. # reset old env
  510. if old_env is not None:
  511. os.environ['BORG_KEYS_DIR'] = old_env
  512. @pytest.fixture()
  513. def stats():
  514. stats = Statistics()
  515. stats.update(20, 10, unique=True)
  516. return stats
  517. def test_stats_basic(stats):
  518. assert stats.osize == 20
  519. assert stats.csize == stats.usize == 10
  520. stats.update(20, 10, unique=False)
  521. assert stats.osize == 40
  522. assert stats.csize == 20
  523. assert stats.usize == 10
  524. def tests_stats_progress(stats, columns=80):
  525. os.environ['COLUMNS'] = str(columns)
  526. out = StringIO()
  527. stats.show_progress(stream=out)
  528. s = '20 B O 10 B C 10 B D 0 N '
  529. buf = ' ' * (columns - len(s))
  530. assert out.getvalue() == s + buf + "\r"
  531. out = StringIO()
  532. stats.update(10**3, 0, unique=False)
  533. stats.show_progress(item={b'path': 'foo'}, final=False, stream=out)
  534. s = '1.02 kB O 10 B C 10 B D 0 N foo'
  535. buf = ' ' * (columns - len(s))
  536. assert out.getvalue() == s + buf + "\r"
  537. out = StringIO()
  538. stats.show_progress(item={b'path': 'foo'*40}, final=False, stream=out)
  539. s = '1.02 kB O 10 B C 10 B D 0 N foofoofoofoofoofoofoofo...oofoofoofoofoofoofoofoofoo'
  540. buf = ' ' * (columns - len(s))
  541. assert out.getvalue() == s + buf + "\r"
  542. def test_stats_format(stats):
  543. assert str(stats) == """\
  544. Original size Compressed size Deduplicated size
  545. This archive: 20 B 10 B 10 B"""
  546. s = "{0.osize_fmt}".format(stats)
  547. assert s == "20 B"
  548. # kind of redundant, but id is variable so we can't match reliably
  549. assert repr(stats) == '<Statistics object at {:#x} (20, 10, 10)>'.format(id(stats))
  550. def test_file_size():
  551. """test the size formatting routines"""
  552. si_size_map = {
  553. 0: '0 B', # no rounding necessary for those
  554. 1: '1 B',
  555. 142: '142 B',
  556. 999: '999 B',
  557. 1000: '1.00 kB', # rounding starts here
  558. 1001: '1.00 kB', # should be rounded away
  559. 1234: '1.23 kB', # should be rounded down
  560. 1235: '1.24 kB', # should be rounded up
  561. 1010: '1.01 kB', # rounded down as well
  562. 999990000: '999.99 MB', # rounded down
  563. 999990001: '999.99 MB', # rounded down
  564. 999995000: '1.00 GB', # rounded up to next unit
  565. 10**6: '1.00 MB', # and all the remaining units, megabytes
  566. 10**9: '1.00 GB', # gigabytes
  567. 10**12: '1.00 TB', # terabytes
  568. 10**15: '1.00 PB', # petabytes
  569. 10**18: '1.00 EB', # exabytes
  570. 10**21: '1.00 ZB', # zottabytes
  571. 10**24: '1.00 YB', # yottabytes
  572. -1: '-1 B', # negative value
  573. -1010: '-1.01 kB', # negative value with rounding
  574. }
  575. for size, fmt in si_size_map.items():
  576. assert format_file_size(size) == fmt
  577. def test_file_size_precision():
  578. assert format_file_size(1234, precision=1) == '1.2 kB' # rounded down
  579. assert format_file_size(1254, precision=1) == '1.3 kB' # rounded up
  580. assert format_file_size(999990000, precision=1) == '1.0 GB' # and not 999.9 MB or 1000.0 MB
  581. def test_file_size_sign():
  582. si_size_map = {
  583. 0: '0 B',
  584. 1: '+1 B',
  585. 1234: '+1.23 kB',
  586. -1: '-1 B',
  587. -1234: '-1.23 kB',
  588. }
  589. for size, fmt in si_size_map.items():
  590. assert format_file_size(size, sign=True) == fmt
  591. def test_is_slow_msgpack():
  592. saved_packer = msgpack.Packer
  593. try:
  594. msgpack.Packer = msgpack.fallback.Packer
  595. assert is_slow_msgpack()
  596. finally:
  597. msgpack.Packer = saved_packer
  598. # this assumes that we have fast msgpack on test platform:
  599. assert not is_slow_msgpack()
  600. def test_yes_input():
  601. inputs = list(TRUISH)
  602. input = FakeInputs(inputs)
  603. for i in inputs:
  604. assert yes(input=input)
  605. inputs = list(FALSISH)
  606. input = FakeInputs(inputs)
  607. for i in inputs:
  608. assert not yes(input=input)
  609. def test_yes_input_defaults():
  610. inputs = list(DEFAULTISH)
  611. input = FakeInputs(inputs)
  612. for i in inputs:
  613. assert yes(default=True, input=input)
  614. input = FakeInputs(inputs)
  615. for i in inputs:
  616. assert not yes(default=False, input=input)
  617. def test_yes_input_custom():
  618. input = FakeInputs(['YES', 'SURE', 'NOPE', ])
  619. assert yes(truish=('YES', ), input=input)
  620. assert yes(truish=('SURE', ), input=input)
  621. assert not yes(falsish=('NOPE', ), input=input)
  622. def test_yes_env():
  623. for value in TRUISH:
  624. with environment_variable(OVERRIDE_THIS=value):
  625. assert yes(env_var_override='OVERRIDE_THIS')
  626. for value in FALSISH:
  627. with environment_variable(OVERRIDE_THIS=value):
  628. assert not yes(env_var_override='OVERRIDE_THIS')
  629. def test_yes_env_default():
  630. for value in DEFAULTISH:
  631. with environment_variable(OVERRIDE_THIS=value):
  632. assert yes(env_var_override='OVERRIDE_THIS', default=True)
  633. with environment_variable(OVERRIDE_THIS=value):
  634. assert not yes(env_var_override='OVERRIDE_THIS', default=False)
  635. def test_yes_defaults():
  636. input = FakeInputs(['invalid', '', ' '])
  637. assert not yes(input=input) # default=False
  638. assert not yes(input=input)
  639. assert not yes(input=input)
  640. input = FakeInputs(['invalid', '', ' '])
  641. assert yes(default=True, input=input)
  642. assert yes(default=True, input=input)
  643. assert yes(default=True, input=input)
  644. input = FakeInputs([])
  645. assert yes(default=True, input=input)
  646. assert not yes(default=False, input=input)
  647. with pytest.raises(ValueError):
  648. yes(default=None)
  649. def test_yes_retry():
  650. input = FakeInputs(['foo', 'bar', TRUISH[0], ])
  651. assert yes(retry_msg='Retry: ', input=input)
  652. input = FakeInputs(['foo', 'bar', FALSISH[0], ])
  653. assert not yes(retry_msg='Retry: ', input=input)
  654. def test_yes_no_retry():
  655. input = FakeInputs(['foo', 'bar', TRUISH[0], ])
  656. assert not yes(retry=False, default=False, input=input)
  657. input = FakeInputs(['foo', 'bar', FALSISH[0], ])
  658. assert yes(retry=False, default=True, input=input)
  659. def test_yes_output(capfd):
  660. input = FakeInputs(['invalid', 'y', 'n'])
  661. assert yes(msg='intro-msg', false_msg='false-msg', true_msg='true-msg', retry_msg='retry-msg', input=input)
  662. out, err = capfd.readouterr()
  663. assert out == ''
  664. assert 'intro-msg' in err
  665. assert 'retry-msg' in err
  666. assert 'true-msg' in err
  667. assert not yes(msg='intro-msg', false_msg='false-msg', true_msg='true-msg', retry_msg='retry-msg', input=input)
  668. out, err = capfd.readouterr()
  669. assert out == ''
  670. assert 'intro-msg' in err
  671. assert 'retry-msg' not in err
  672. assert 'false-msg' in err
  673. def test_progress_percentage_multiline(capfd):
  674. pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%")
  675. pi.show(0)
  676. out, err = capfd.readouterr()
  677. assert err == ' 0%\n'
  678. pi.show(420)
  679. out, err = capfd.readouterr()
  680. assert err == ' 42%\n'
  681. pi.show(1000)
  682. out, err = capfd.readouterr()
  683. assert err == '100%\n'
  684. pi.finish()
  685. out, err = capfd.readouterr()
  686. assert err == ''
  687. def test_progress_percentage_sameline(capfd):
  688. pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=True, msg="%3.0f%%")
  689. pi.show(0)
  690. out, err = capfd.readouterr()
  691. assert err == ' 0%\r'
  692. pi.show(420)
  693. pi.show(680)
  694. out, err = capfd.readouterr()
  695. assert err == ' 42%\r 68%\r'
  696. pi.show(1000)
  697. out, err = capfd.readouterr()
  698. assert err == '100%\r'
  699. pi.finish()
  700. out, err = capfd.readouterr()
  701. assert err == ' ' * 4 + '\r'
  702. def test_progress_percentage_step(capfd):
  703. pi = ProgressIndicatorPercent(100, step=2, start=0, same_line=False, msg="%3.0f%%")
  704. pi.show()
  705. out, err = capfd.readouterr()
  706. assert err == ' 0%\n'
  707. pi.show()
  708. out, err = capfd.readouterr()
  709. assert err == '' # no output at 1% as we have step == 2
  710. pi.show()
  711. out, err = capfd.readouterr()
  712. assert err == ' 2%\n'
  713. def test_progress_percentage_quiet(capfd):
  714. logging.getLogger('borg.output.progress').setLevel(logging.WARN)
  715. pi = ProgressIndicatorPercent(1000, step=5, start=0, same_line=False, msg="%3.0f%%")
  716. pi.show(0)
  717. out, err = capfd.readouterr()
  718. assert err == ''
  719. pi.show(1000)
  720. out, err = capfd.readouterr()
  721. assert err == ''
  722. pi.finish()
  723. out, err = capfd.readouterr()
  724. assert err == ''
  725. def test_progress_endless(capfd):
  726. pi = ProgressIndicatorEndless(step=1, file=sys.stderr)
  727. pi.show()
  728. out, err = capfd.readouterr()
  729. assert err == '.'
  730. pi.show()
  731. out, err = capfd.readouterr()
  732. assert err == '.'
  733. pi.finish()
  734. out, err = capfd.readouterr()
  735. assert err == '\n'
  736. def test_progress_endless_step(capfd):
  737. pi = ProgressIndicatorEndless(step=2, file=sys.stderr)
  738. pi.show()
  739. out, err = capfd.readouterr()
  740. assert err == '' # no output here as we have step == 2
  741. pi.show()
  742. out, err = capfd.readouterr()
  743. assert err == '.'
  744. pi.show()
  745. out, err = capfd.readouterr()
  746. assert err == '' # no output here as we have step == 2
  747. pi.show()
  748. out, err = capfd.readouterr()
  749. assert err == '.'
  750. def test_partial_format():
  751. assert partial_format('{space:10}', {'space': ' '}) == ' ' * 10
  752. assert partial_format('{foobar}', {'bar': 'wrong', 'foobar': 'correct'}) == 'correct'
  753. assert partial_format('{unknown_key}', {}) == '{unknown_key}'
  754. assert partial_format('{key}{{escaped_key}}', {}) == '{key}{{escaped_key}}'
  755. assert partial_format('{{escaped_key}}', {'escaped_key': 1234}) == '{{escaped_key}}'
  756. def test_chunk_file_wrapper():
  757. cfw = ChunkIteratorFileWrapper(iter([Chunk(b'abc'), Chunk(b'def')]))
  758. assert cfw.read(2) == b'ab'
  759. assert cfw.read(50) == b'cdef'
  760. assert cfw.exhausted
  761. cfw = ChunkIteratorFileWrapper(iter([]))
  762. assert cfw.read(2) == b''
  763. assert cfw.exhausted
  764. def test_clean_lines():
  765. conf = """\
  766. #comment
  767. data1 #data1
  768. data2
  769. data3
  770. """.splitlines(keepends=True)
  771. assert list(clean_lines(conf)) == ['data1 #data1', 'data2', 'data3', ]
  772. assert list(clean_lines(conf, lstrip=False)) == ['data1 #data1', 'data2', ' data3', ]
  773. assert list(clean_lines(conf, rstrip=False)) == ['data1 #data1\n', 'data2\n', 'data3\n', ]
  774. assert list(clean_lines(conf, remove_empty=False)) == ['data1 #data1', 'data2', '', 'data3', ]
  775. assert list(clean_lines(conf, remove_comments=False)) == ['#comment', 'data1 #data1', 'data2', 'data3', ]
  776. def test_compression_decider1():
  777. default = CompressionSpec('zlib')
  778. conf = """
  779. # use super-fast lz4 compression on huge VM files in this path:
  780. lz4:/srv/vm_disks
  781. # jpeg or zip files do not compress:
  782. none:*.jpeg
  783. none:*.zip
  784. """.splitlines()
  785. cd = CompressionDecider1(default, []) # no conf, always use default
  786. assert cd.decide('/srv/vm_disks/linux')['name'] == 'zlib'
  787. assert cd.decide('test.zip')['name'] == 'zlib'
  788. assert cd.decide('test')['name'] == 'zlib'
  789. cd = CompressionDecider1(default, [conf, ])
  790. assert cd.decide('/srv/vm_disks/linux')['name'] == 'lz4'
  791. assert cd.decide('test.zip')['name'] == 'none'
  792. assert cd.decide('test')['name'] == 'zlib' # no match in conf, use default
  793. def test_compression_decider2():
  794. default = CompressionSpec('zlib')
  795. cd = CompressionDecider2(default)
  796. compr_spec, chunk = cd.decide(Chunk(None))
  797. assert compr_spec['name'] == 'zlib'
  798. compr_spec, chunk = cd.decide(Chunk(None, compress=CompressionSpec('lzma')))
  799. assert compr_spec['name'] == 'lzma'