helpers.py 43 KB

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