test_arguments.py 42 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262
  1. import collections
  2. import pytest
  3. from flexmock import flexmock
  4. from borgmatic.commands import arguments as module
  5. def test_get_subaction_parsers_with_no_subactions_returns_empty_result():
  6. assert module.get_subaction_parsers(flexmock(_subparsers=None)) == {}
  7. def test_get_subaction_parsers_with_subactions_returns_one_entry_per_subaction():
  8. foo_parser = flexmock()
  9. bar_parser = flexmock()
  10. baz_parser = flexmock()
  11. assert module.get_subaction_parsers(
  12. flexmock(
  13. _subparsers=flexmock(
  14. _group_actions=(
  15. flexmock(choices={'foo': foo_parser, 'bar': bar_parser}),
  16. flexmock(choices={'baz': baz_parser}),
  17. )
  18. )
  19. )
  20. ) == {'foo': foo_parser, 'bar': bar_parser, 'baz': baz_parser}
  21. def test_get_subactions_for_actions_with_no_subactions_returns_empty_result():
  22. assert module.get_subactions_for_actions({'action': flexmock(_subparsers=None)}) == {}
  23. def test_get_subactions_for_actions_with_subactions_returns_one_entry_per_action():
  24. assert module.get_subactions_for_actions(
  25. {
  26. 'action': flexmock(
  27. _subparsers=flexmock(
  28. _group_actions=(
  29. flexmock(choices={'foo': flexmock(), 'bar': flexmock()}),
  30. flexmock(choices={'baz': flexmock()}),
  31. )
  32. )
  33. ),
  34. 'other': flexmock(
  35. _subparsers=flexmock(_group_actions=(flexmock(choices={'quux': flexmock()}),))
  36. ),
  37. }
  38. ) == {'action': ('foo', 'bar', 'baz'), 'other': ('quux',)}
  39. def test_omit_values_colliding_with_action_names_drops_action_names_that_have_been_parsed_as_values():
  40. assert module.omit_values_colliding_with_action_names(
  41. ('check', '--only', 'extract', '--some-list', 'borg'),
  42. {'check': flexmock(only='extract', some_list=['borg'])},
  43. ) == ('check', '--only', '--some-list')
  44. def test_omit_values_colliding_twice_with_action_names_drops_action_names_that_have_been_parsed_as_values():
  45. assert module.omit_values_colliding_with_action_names(
  46. ('config', 'bootstrap', '--local-path', '--remote-path', 'borg'),
  47. {'bootstrap': flexmock(local_path='borg', remote_path='borg')},
  48. ) == ('config', 'bootstrap', '--local-path', '--remote-path')
  49. def test_parse_and_record_action_arguments_without_action_name_leaves_arguments_untouched():
  50. unparsed_arguments = ('--foo', '--bar')
  51. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  52. unparsed_arguments
  53. )
  54. assert (
  55. module.parse_and_record_action_arguments(
  56. unparsed_arguments, flexmock(), flexmock(), 'action'
  57. )
  58. == unparsed_arguments
  59. )
  60. def test_parse_and_record_action_arguments_updates_parsed_arguments_and_returns_remaining():
  61. unparsed_arguments = ('action', '--foo', '--bar', '--verbosity', '1')
  62. other_parsed_arguments = flexmock()
  63. parsed_arguments = {'other': other_parsed_arguments}
  64. action_parsed_arguments = flexmock()
  65. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  66. unparsed_arguments
  67. )
  68. action_parser = flexmock()
  69. flexmock(action_parser).should_receive('parse_known_args').and_return(
  70. action_parsed_arguments, ('action', '--verbosity', '1')
  71. )
  72. assert module.parse_and_record_action_arguments(
  73. unparsed_arguments, parsed_arguments, action_parser, 'action'
  74. ) == ('--verbosity', '1')
  75. assert parsed_arguments == {'other': other_parsed_arguments, 'action': action_parsed_arguments}
  76. def test_parse_and_record_action_arguments_with_alias_updates_canonical_parsed_arguments():
  77. unparsed_arguments = ('action', '--foo', '--bar', '--verbosity', '1')
  78. other_parsed_arguments = flexmock()
  79. parsed_arguments = {'other': other_parsed_arguments}
  80. action_parsed_arguments = flexmock()
  81. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  82. unparsed_arguments
  83. )
  84. action_parser = flexmock()
  85. flexmock(action_parser).should_receive('parse_known_args').and_return(
  86. action_parsed_arguments, ('action', '--verbosity', '1')
  87. )
  88. assert module.parse_and_record_action_arguments(
  89. unparsed_arguments, parsed_arguments, action_parser, 'action', canonical_name='doit'
  90. ) == ('--verbosity', '1')
  91. assert parsed_arguments == {'other': other_parsed_arguments, 'doit': action_parsed_arguments}
  92. def test_parse_and_record_action_arguments_with_borg_action_consumes_arguments_after_action_name():
  93. unparsed_arguments = ('--verbosity', '1', 'borg', 'list')
  94. parsed_arguments = {}
  95. borg_parsed_arguments = flexmock(options=flexmock())
  96. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  97. unparsed_arguments
  98. )
  99. borg_parser = flexmock()
  100. flexmock(borg_parser).should_receive('parse_known_args').and_return(
  101. borg_parsed_arguments, ('--verbosity', '1', 'borg', 'list')
  102. )
  103. assert module.parse_and_record_action_arguments(
  104. unparsed_arguments,
  105. parsed_arguments,
  106. borg_parser,
  107. 'borg',
  108. ) == ('--verbosity', '1')
  109. assert parsed_arguments == {'borg': borg_parsed_arguments}
  110. assert borg_parsed_arguments.options == ('list',)
  111. @pytest.mark.parametrize(
  112. 'argument, expected',
  113. [
  114. ('--foo', True),
  115. ('foo', False),
  116. (33, False),
  117. ],
  118. )
  119. def test_argument_is_flag_only_for_string_starting_with_double_dash(argument, expected):
  120. assert module.argument_is_flag(argument) == expected
  121. @pytest.mark.parametrize(
  122. 'arguments, expected',
  123. [
  124. # Ending with a valueless flag.
  125. (
  126. ('--foo', '--bar', 33, '--baz'),
  127. (
  128. ('--foo',),
  129. ('--bar', 33),
  130. ('--baz',),
  131. ),
  132. ),
  133. # Ending with a flag and its corresponding value.
  134. (
  135. ('--foo', '--bar', 33, '--baz', '--quux', 'thing'),
  136. (('--foo',), ('--bar', 33), ('--baz',), ('--quux', 'thing')),
  137. ),
  138. # Starting with an action name.
  139. (
  140. ('check', '--foo', '--bar', 33, '--baz'),
  141. (
  142. ('check',),
  143. ('--foo',),
  144. ('--bar', 33),
  145. ('--baz',),
  146. ),
  147. ),
  148. # Action name that one could mistake for a flag value.
  149. (('--progress', 'list'), (('--progress',), ('list',))),
  150. # No arguments.
  151. ((), ()),
  152. ],
  153. )
  154. def test_group_arguments_with_values_returns_flags_with_corresponding_values(arguments, expected):
  155. flexmock(module).should_receive('argument_is_flag').with_args('--foo').and_return(True)
  156. flexmock(module).should_receive('argument_is_flag').with_args('--bar').and_return(True)
  157. flexmock(module).should_receive('argument_is_flag').with_args('--baz').and_return(True)
  158. flexmock(module).should_receive('argument_is_flag').with_args('--quux').and_return(True)
  159. flexmock(module).should_receive('argument_is_flag').with_args('--progress').and_return(True)
  160. flexmock(module).should_receive('argument_is_flag').with_args(33).and_return(False)
  161. flexmock(module).should_receive('argument_is_flag').with_args('thing').and_return(False)
  162. flexmock(module).should_receive('argument_is_flag').with_args('check').and_return(False)
  163. flexmock(module).should_receive('argument_is_flag').with_args('list').and_return(False)
  164. assert module.group_arguments_with_values(arguments) == expected
  165. @pytest.mark.parametrize(
  166. 'arguments, grouped_arguments, expected',
  167. [
  168. # An unparsable flag remaining from each parsed action.
  169. (
  170. (
  171. ('--latest', 'archive', 'prune', 'extract', 'list', '--flag'),
  172. ('--latest', 'archive', 'check', 'extract', 'list', '--flag'),
  173. ('prune', 'check', 'list', '--flag'),
  174. ('prune', 'check', 'extract', '--flag'),
  175. ),
  176. (
  177. (
  178. ('--latest',),
  179. ('archive',),
  180. ('prune',),
  181. ('extract',),
  182. ('list',),
  183. ('--flag',),
  184. ),
  185. (
  186. ('--latest',),
  187. ('archive',),
  188. ('check',),
  189. ('extract',),
  190. ('list',),
  191. ('--flag',),
  192. ),
  193. (('prune',), ('check',), ('list',), ('--flag',)),
  194. (('prune',), ('check',), ('extract',), ('--flag',)),
  195. ),
  196. ('--flag',),
  197. ),
  198. # No unparsable flags remaining.
  199. (
  200. (
  201. ('--archive', 'archive', 'prune', 'extract', 'list'),
  202. ('--archive', 'archive', 'check', 'extract', 'list'),
  203. ('prune', 'check', 'list'),
  204. ('prune', 'check', 'extract'),
  205. ),
  206. (
  207. (
  208. (
  209. '--archive',
  210. 'archive',
  211. ),
  212. ('prune',),
  213. ('extract',),
  214. ('list',),
  215. ),
  216. (
  217. (
  218. '--archive',
  219. 'archive',
  220. ),
  221. ('check',),
  222. ('extract',),
  223. ('list',),
  224. ),
  225. (('prune',), ('check',), ('list',)),
  226. (('prune',), ('check',), ('extract',)),
  227. ),
  228. (),
  229. ),
  230. # No unparsable flags remaining, but some values in common.
  231. (
  232. (
  233. ('--verbosity', '5', 'archive', 'prune', 'extract', 'list'),
  234. ('--last', '5', 'archive', 'check', 'extract', 'list'),
  235. ('prune', 'check', 'list', '--last', '5'),
  236. ('prune', 'check', '--verbosity', '5', 'extract'),
  237. ),
  238. (
  239. (('--verbosity', '5'), ('archive',), ('prune',), ('extract',), ('list',)),
  240. (
  241. (
  242. '--last',
  243. '5',
  244. ),
  245. ('archive',),
  246. ('check',),
  247. ('extract',),
  248. ('list',),
  249. ),
  250. (('prune',), ('check',), ('list',), ('--last', '5')),
  251. (
  252. ('prune',),
  253. ('check',),
  254. (
  255. '--verbosity',
  256. '5',
  257. ),
  258. ('extract',),
  259. ),
  260. ),
  261. (),
  262. ),
  263. # No flags.
  264. ((), (), ()),
  265. ],
  266. )
  267. def test_get_unparsable_arguments_returns_remaining_arguments_that_no_action_can_parse(
  268. arguments, grouped_arguments, expected
  269. ):
  270. for action_arguments, grouped_action_arguments in zip(arguments, grouped_arguments):
  271. flexmock(module).should_receive('group_arguments_with_values').with_args(
  272. action_arguments
  273. ).and_return(grouped_action_arguments)
  274. assert module.get_unparsable_arguments(arguments) == expected
  275. def test_parse_arguments_for_actions_consumes_action_arguments_after_action_name():
  276. action_namespace = flexmock(foo=True)
  277. remaining = flexmock()
  278. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  279. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  280. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  281. {action: action_namespace}
  282. )
  283. or remaining
  284. )
  285. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  286. action_parsers = {'action': flexmock(), 'other': flexmock()}
  287. global_namespace = flexmock(config_paths=[])
  288. global_parser = flexmock()
  289. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  290. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  291. ('action', '--foo', 'true'), action_parsers, global_parser
  292. )
  293. assert arguments == {'global': global_namespace, 'action': action_namespace}
  294. assert remaining_action_arguments == (remaining, ())
  295. def test_parse_arguments_for_actions_consumes_action_arguments_with_alias():
  296. action_namespace = flexmock(foo=True)
  297. remaining = flexmock()
  298. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  299. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  300. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  301. {canonical or action: action_namespace}
  302. )
  303. or remaining
  304. )
  305. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  306. action_parsers = {
  307. 'action': flexmock(),
  308. '-a': flexmock(),
  309. 'other': flexmock(),
  310. '-o': flexmock(),
  311. }
  312. global_namespace = flexmock(config_paths=[])
  313. global_parser = flexmock()
  314. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  315. flexmock(module).ACTION_ALIASES = {'action': ['-a'], 'other': ['-o']}
  316. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  317. ('-a', '--foo', 'true'), action_parsers, global_parser
  318. )
  319. assert arguments == {'global': global_namespace, 'action': action_namespace}
  320. assert remaining_action_arguments == (remaining, ())
  321. def test_parse_arguments_for_actions_consumes_multiple_action_arguments():
  322. action_namespace = flexmock(foo=True)
  323. other_namespace = flexmock(bar=3)
  324. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  325. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  326. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  327. {action: action_namespace if action == 'action' else other_namespace}
  328. )
  329. or ()
  330. ).and_return(('other', '--bar', '3')).and_return('action', '--foo', 'true')
  331. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  332. action_parsers = {
  333. 'action': flexmock(),
  334. 'other': flexmock(),
  335. }
  336. global_namespace = flexmock(config_paths=[])
  337. global_parser = flexmock()
  338. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  339. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  340. ('action', '--foo', 'true', 'other', '--bar', '3'), action_parsers, global_parser
  341. )
  342. assert arguments == {
  343. 'global': global_namespace,
  344. 'action': action_namespace,
  345. 'other': other_namespace,
  346. }
  347. assert remaining_action_arguments == ((), (), ())
  348. def test_parse_arguments_for_actions_respects_command_line_action_ordering():
  349. other_namespace = flexmock()
  350. action_namespace = flexmock(foo=True)
  351. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  352. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  353. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  354. {action: other_namespace if action == 'other' else action_namespace}
  355. )
  356. or ()
  357. ).and_return(('action',)).and_return(('other', '--foo', 'true'))
  358. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  359. action_parsers = {
  360. 'action': flexmock(),
  361. 'other': flexmock(),
  362. }
  363. global_namespace = flexmock(config_paths=[])
  364. global_parser = flexmock()
  365. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  366. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  367. ('other', '--foo', 'true', 'action'), action_parsers, global_parser
  368. )
  369. assert arguments == collections.OrderedDict(
  370. [('other', other_namespace), ('action', action_namespace), ('global', global_namespace)]
  371. )
  372. assert remaining_action_arguments == ((), (), ())
  373. def test_parse_arguments_for_actions_applies_default_action_parsers():
  374. global_namespace = flexmock(config_paths=[])
  375. namespaces = {
  376. 'global': global_namespace,
  377. 'prune': flexmock(),
  378. 'compact': flexmock(),
  379. 'create': flexmock(progress=True),
  380. 'check': flexmock(),
  381. }
  382. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  383. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  384. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  385. {action: namespaces.get(action)}
  386. )
  387. or ()
  388. ).and_return(())
  389. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  390. action_parsers = {
  391. 'prune': flexmock(),
  392. 'compact': flexmock(),
  393. 'create': flexmock(),
  394. 'check': flexmock(),
  395. 'other': flexmock(),
  396. }
  397. global_parser = flexmock()
  398. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  399. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  400. ('--progress'), action_parsers, global_parser
  401. )
  402. assert arguments == namespaces
  403. assert remaining_action_arguments == ((), (), (), (), ())
  404. def test_parse_arguments_for_actions_consumes_global_arguments():
  405. action_namespace = flexmock()
  406. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  407. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  408. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  409. {action: action_namespace}
  410. )
  411. or ('--verbosity', 'lots')
  412. )
  413. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  414. action_parsers = {
  415. 'action': flexmock(),
  416. 'other': flexmock(),
  417. }
  418. global_namespace = flexmock(config_paths=[])
  419. global_parser = flexmock()
  420. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  421. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  422. ('action', '--verbosity', 'lots'), action_parsers, global_parser
  423. )
  424. assert arguments == {'global': global_namespace, 'action': action_namespace}
  425. assert remaining_action_arguments == (('--verbosity', 'lots'), ())
  426. def test_parse_arguments_for_actions_passes_through_unknown_arguments_before_action_name():
  427. action_namespace = flexmock()
  428. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  429. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  430. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  431. {action: action_namespace}
  432. )
  433. or ('--wtf', 'yes')
  434. )
  435. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  436. action_parsers = {
  437. 'action': flexmock(),
  438. 'other': flexmock(),
  439. }
  440. global_namespace = flexmock(config_paths=[])
  441. global_parser = flexmock()
  442. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  443. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  444. ('--wtf', 'yes', 'action'), action_parsers, global_parser
  445. )
  446. assert arguments == {'global': global_namespace, 'action': action_namespace}
  447. assert remaining_action_arguments == (('--wtf', 'yes'), ())
  448. def test_parse_arguments_for_actions_passes_through_unknown_arguments_after_action_name():
  449. action_namespace = flexmock()
  450. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  451. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  452. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  453. {action: action_namespace}
  454. )
  455. or ('--wtf', 'yes')
  456. )
  457. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  458. action_parsers = {
  459. 'action': flexmock(),
  460. 'other': flexmock(),
  461. }
  462. global_namespace = flexmock(config_paths=[])
  463. global_parser = flexmock()
  464. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  465. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  466. ('action', '--wtf', 'yes'), action_parsers, global_parser
  467. )
  468. assert arguments == {'global': global_namespace, 'action': action_namespace}
  469. assert remaining_action_arguments == (('--wtf', 'yes'), ())
  470. def test_parse_arguments_for_actions_with_borg_action_skips_other_action_parsers():
  471. action_namespace = flexmock(options=[])
  472. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  473. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  474. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  475. {action: action_namespace}
  476. )
  477. or ()
  478. ).and_return(())
  479. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  480. action_parsers = {
  481. 'borg': flexmock(),
  482. 'list': flexmock(),
  483. }
  484. global_namespace = flexmock(config_paths=[])
  485. global_parser = flexmock()
  486. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  487. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  488. ('borg', 'list'), action_parsers, global_parser
  489. )
  490. assert arguments == {'global': global_namespace, 'borg': action_namespace}
  491. assert remaining_action_arguments == ((), ())
  492. def test_parse_arguments_for_actions_raises_error_when_no_action_is_specified():
  493. flexmock(module).should_receive('get_subaction_parsers').and_return({'bootstrap': [flexmock()]})
  494. flexmock(module).should_receive('parse_and_record_action_arguments').and_return(flexmock())
  495. flexmock(module).should_receive('get_subactions_for_actions').and_return(
  496. {'config': ['bootstrap']}
  497. )
  498. action_parsers = {'config': flexmock()}
  499. global_parser = flexmock()
  500. global_parser.should_receive('parse_known_args').and_return((flexmock(), ()))
  501. with pytest.raises(ValueError):
  502. module.parse_arguments_for_actions(('config',), action_parsers, global_parser)
  503. def test_make_argument_description_without_description_bails():
  504. assert (
  505. module.make_argument_description(
  506. schema={
  507. 'description': None,
  508. 'type': 'not yours',
  509. },
  510. flag_name='flag',
  511. )
  512. is None
  513. )
  514. def test_make_argument_description_with_object_adds_example():
  515. buffer = flexmock()
  516. buffer.should_receive('getvalue').and_return('{foo: example}')
  517. flexmock(module.io).should_receive('StringIO').and_return(buffer)
  518. yaml = flexmock()
  519. yaml.should_receive('dump')
  520. flexmock(module.ruamel.yaml).should_receive('YAML').and_return(yaml)
  521. assert (
  522. module.make_argument_description(
  523. schema={
  524. 'description': 'Thing.',
  525. 'type': 'object',
  526. 'example': {'foo': 'example'},
  527. },
  528. flag_name='flag',
  529. )
  530. == 'Thing. Example value: "{foo: example}"'
  531. )
  532. def test_make_argument_description_with_object_skips_missing_example():
  533. flexmock(module.ruamel.yaml).should_receive('YAML').never()
  534. assert (
  535. module.make_argument_description(
  536. schema={
  537. 'description': 'Thing.',
  538. 'type': 'object',
  539. },
  540. flag_name='flag',
  541. )
  542. == 'Thing.'
  543. )
  544. def test_make_argument_description_with_array_adds_example():
  545. buffer = flexmock()
  546. buffer.should_receive('getvalue').and_return('[example]')
  547. flexmock(module.io).should_receive('StringIO').and_return(buffer)
  548. yaml = flexmock()
  549. yaml.should_receive('dump')
  550. flexmock(module.ruamel.yaml).should_receive('YAML').and_return(yaml)
  551. assert (
  552. module.make_argument_description(
  553. schema={
  554. 'description': 'Thing.',
  555. 'type': 'array',
  556. 'example': ['example'],
  557. },
  558. flag_name='flag',
  559. )
  560. == 'Thing. Example value: "[example]"'
  561. )
  562. def test_make_argument_description_with_array_skips_missing_example():
  563. flexmock(module.ruamel.yaml).should_receive('YAML').never()
  564. assert (
  565. module.make_argument_description(
  566. schema={
  567. 'description': 'Thing.',
  568. 'type': 'array',
  569. },
  570. flag_name='flag',
  571. )
  572. == 'Thing.'
  573. )
  574. def test_make_argument_description_with_array_index_in_flag_name_adds_to_description():
  575. assert 'list element' in module.make_argument_description(
  576. schema={
  577. 'description': 'Thing.',
  578. 'type': 'something',
  579. },
  580. flag_name='flag[0]',
  581. )
  582. def test_make_argument_description_escapes_percent_character():
  583. assert (
  584. module.make_argument_description(
  585. schema={
  586. 'description': '% Thing.',
  587. 'type': 'something',
  588. },
  589. flag_name='flag',
  590. )
  591. == '%% Thing.'
  592. )
  593. def test_add_array_element_arguments_without_array_index_bails():
  594. arguments_group = flexmock()
  595. arguments_group.should_receive('add_argument').never()
  596. module.add_array_element_arguments(
  597. arguments_group=arguments_group,
  598. unparsed_arguments=(),
  599. flag_name='foo',
  600. )
  601. def test_add_array_element_arguments_with_help_flag_bails():
  602. arguments_group = flexmock()
  603. arguments_group.should_receive('add_argument').never()
  604. module.add_array_element_arguments(
  605. arguments_group=arguments_group,
  606. unparsed_arguments=('--foo', '--help', '--bar'),
  607. flag_name='foo[0]',
  608. )
  609. def test_add_array_element_arguments_without_any_flags_bails():
  610. arguments_group = flexmock()
  611. arguments_group.should_receive('add_argument').never()
  612. module.add_array_element_arguments(
  613. arguments_group=arguments_group,
  614. unparsed_arguments=(),
  615. flag_name='foo[0]',
  616. )
  617. # Use this instead of a flexmock because it's not easy to check the type() of a flexmock instance.
  618. Group_action = collections.namedtuple(
  619. 'Group_action',
  620. (
  621. 'option_strings',
  622. 'choices',
  623. 'default',
  624. 'nargs',
  625. 'required',
  626. 'type',
  627. ),
  628. defaults=(
  629. flexmock(),
  630. flexmock(),
  631. flexmock(),
  632. flexmock(),
  633. flexmock(),
  634. ),
  635. )
  636. def test_add_array_element_arguments_without_array_index_flags_bails():
  637. arguments_group = flexmock(
  638. _group_actions=(
  639. Group_action(
  640. option_strings=('--foo[0].val',),
  641. ),
  642. ),
  643. _registries={'action': {'store_stuff': Group_action}},
  644. )
  645. arguments_group.should_receive('add_argument').never()
  646. module.add_array_element_arguments(
  647. arguments_group=arguments_group,
  648. unparsed_arguments=('--foo', '--bar'),
  649. flag_name='foo[0].val',
  650. )
  651. def test_add_array_element_arguments_with_non_matching_array_index_flags_bails():
  652. arguments_group = flexmock(
  653. _group_actions=(
  654. Group_action(
  655. option_strings=('--foo[0].val',),
  656. ),
  657. ),
  658. _registries={'action': {'store_stuff': Group_action}},
  659. )
  660. arguments_group.should_receive('add_argument').never()
  661. module.add_array_element_arguments(
  662. arguments_group=arguments_group,
  663. unparsed_arguments=('--foo', '--bar[25].val', 'barval'),
  664. flag_name='foo[0].val',
  665. )
  666. def test_add_array_element_arguments_with_identical_array_index_flag_bails():
  667. arguments_group = flexmock(
  668. _group_actions=(
  669. Group_action(
  670. option_strings=('--foo[0].val',),
  671. ),
  672. ),
  673. _registries={'action': {'store_stuff': Group_action}},
  674. )
  675. arguments_group.should_receive('add_argument').never()
  676. module.add_array_element_arguments(
  677. arguments_group=arguments_group,
  678. unparsed_arguments=('--foo[0].val', 'fooval', '--bar'),
  679. flag_name='foo[0].val',
  680. )
  681. def test_add_array_element_arguments_without_action_type_in_registry_bails():
  682. arguments_group = flexmock(
  683. _group_actions=(
  684. Group_action(
  685. option_strings=('--foo[0].val',),
  686. choices=flexmock(),
  687. default=flexmock(),
  688. nargs=flexmock(),
  689. required=flexmock(),
  690. type=flexmock(),
  691. ),
  692. ),
  693. _registries={'action': {'store_stuff': bool}},
  694. )
  695. arguments_group.should_receive('add_argument').never()
  696. module.add_array_element_arguments(
  697. arguments_group=arguments_group,
  698. unparsed_arguments=('--foo[25].val', 'fooval', '--bar[1].val', 'barval'),
  699. flag_name='foo[0].val',
  700. )
  701. def test_add_array_element_arguments_adds_arguments_for_array_index_flags():
  702. arguments_group = flexmock(
  703. _group_actions=(
  704. Group_action(
  705. option_strings=('--foo[0].val',),
  706. choices=flexmock(),
  707. default=flexmock(),
  708. nargs=flexmock(),
  709. required=flexmock(),
  710. type=flexmock(),
  711. ),
  712. ),
  713. _registries={'action': {'store_stuff': Group_action}},
  714. )
  715. arguments_group.should_receive('add_argument').with_args(
  716. '--foo[25].val',
  717. action='store_stuff',
  718. choices=object,
  719. default=object,
  720. dest='foo[25].val',
  721. nargs=object,
  722. required=object,
  723. type=object,
  724. ).once()
  725. module.add_array_element_arguments(
  726. arguments_group=arguments_group,
  727. unparsed_arguments=('--foo[25].val', 'fooval', '--bar[1].val', 'barval'),
  728. flag_name='foo[0].val',
  729. )
  730. def test_add_array_element_arguments_adds_arguments_for_array_index_flags_with_equals_sign():
  731. arguments_group = flexmock(
  732. _group_actions=(
  733. Group_action(
  734. option_strings=('--foo[0].val',),
  735. choices=flexmock(),
  736. default=flexmock(),
  737. nargs=flexmock(),
  738. required=flexmock(),
  739. type=flexmock(),
  740. ),
  741. ),
  742. _registries={'action': {'store_stuff': Group_action}},
  743. )
  744. arguments_group.should_receive('add_argument').with_args(
  745. '--foo[25].val',
  746. action='store_stuff',
  747. choices=object,
  748. default=object,
  749. dest='foo[25].val',
  750. nargs=object,
  751. required=object,
  752. type=object,
  753. ).once()
  754. module.add_array_element_arguments(
  755. arguments_group=arguments_group,
  756. unparsed_arguments=('--foo[25].val=fooval', '--bar[1].val=barval'),
  757. flag_name='foo[0].val',
  758. )
  759. def test_add_array_element_arguments_adds_arguments_for_array_index_flags_with_dashes():
  760. arguments_group = flexmock(
  761. _group_actions=(
  762. Group_action(
  763. option_strings=('--foo[0].val-and-stuff',),
  764. choices=flexmock(),
  765. default=flexmock(),
  766. nargs=flexmock(),
  767. required=flexmock(),
  768. type=flexmock(),
  769. ),
  770. ),
  771. _registries={'action': {'store_stuff': Group_action}},
  772. )
  773. arguments_group.should_receive('add_argument').with_args(
  774. '--foo[25].val-and-stuff',
  775. action='store_stuff',
  776. choices=object,
  777. default=object,
  778. dest='foo[25].val-and-stuff',
  779. nargs=object,
  780. required=object,
  781. type=object,
  782. ).once()
  783. module.add_array_element_arguments(
  784. arguments_group=arguments_group,
  785. unparsed_arguments=('--foo[25].val-and-stuff', 'fooval', '--bar[1].val', 'barval'),
  786. flag_name='foo[0].val_and_stuff',
  787. )
  788. def test_add_arguments_from_schema_with_non_dict_schema_bails():
  789. arguments_group = flexmock()
  790. flexmock(module).should_receive('make_argument_description').never()
  791. flexmock(module.borgmatic.config.schema).should_receive('parse_type').never()
  792. arguments_group.should_receive('add_argument').never()
  793. module.add_arguments_from_schema(
  794. arguments_group=arguments_group, schema='foo', unparsed_arguments=()
  795. )
  796. def test_add_arguments_from_schema_with_nested_object_adds_flag_for_each_option():
  797. arguments_group = flexmock()
  798. flexmock(module).should_receive('make_argument_description').and_return('help 1').and_return(
  799. 'help 2'
  800. )
  801. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(
  802. int
  803. ).and_return(str)
  804. arguments_group.should_receive('add_argument').with_args(
  805. '--foo.bar',
  806. type=int,
  807. metavar='BAR',
  808. help='help 1',
  809. ).once()
  810. arguments_group.should_receive('add_argument').with_args(
  811. '--foo.baz',
  812. type=str,
  813. metavar='BAZ',
  814. help='help 2',
  815. ).once()
  816. flexmock(module).should_receive('add_array_element_arguments')
  817. module.add_arguments_from_schema(
  818. arguments_group=arguments_group,
  819. schema={
  820. 'type': 'object',
  821. 'properties': {
  822. 'foo': {
  823. 'type': 'object',
  824. 'properties': {
  825. 'bar': {'type': 'integer'},
  826. 'baz': {'type': 'str'},
  827. },
  828. }
  829. },
  830. },
  831. unparsed_arguments=(),
  832. )
  833. def test_add_arguments_from_schema_uses_first_non_null_type_from_multi_type_object():
  834. arguments_group = flexmock()
  835. flexmock(module).should_receive('make_argument_description').and_return('help 1')
  836. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(int)
  837. arguments_group.should_receive('add_argument').with_args(
  838. '--foo.bar',
  839. type=int,
  840. metavar='BAR',
  841. help='help 1',
  842. ).once()
  843. flexmock(module).should_receive('add_array_element_arguments')
  844. module.add_arguments_from_schema(
  845. arguments_group=arguments_group,
  846. schema={
  847. 'type': 'object',
  848. 'properties': {
  849. 'foo': {
  850. 'type': ['null', 'object', 'boolean'],
  851. 'properties': {
  852. 'bar': {'type': 'integer'},
  853. },
  854. }
  855. },
  856. },
  857. unparsed_arguments=(),
  858. )
  859. def test_add_arguments_from_schema_with_empty_multi_type_raises():
  860. arguments_group = flexmock()
  861. flexmock(module).should_receive('make_argument_description').and_return('help 1')
  862. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(int)
  863. arguments_group.should_receive('add_argument').never()
  864. flexmock(module).should_receive('add_array_element_arguments').never()
  865. with pytest.raises(ValueError):
  866. module.add_arguments_from_schema(
  867. arguments_group=arguments_group,
  868. schema={
  869. 'type': 'object',
  870. 'properties': {
  871. 'foo': {
  872. 'type': [],
  873. 'properties': {
  874. 'bar': {'type': 'integer'},
  875. },
  876. }
  877. },
  878. },
  879. unparsed_arguments=(),
  880. )
  881. def test_add_arguments_from_schema_with_propertyless_option_adds_flag():
  882. arguments_group = flexmock()
  883. flexmock(module).should_receive('make_argument_description').and_return('help')
  884. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(str)
  885. arguments_group.should_receive('add_argument').with_args(
  886. '--foo',
  887. type=str,
  888. metavar='FOO',
  889. help='help',
  890. ).once()
  891. flexmock(module).should_receive('add_array_element_arguments')
  892. module.add_arguments_from_schema(
  893. arguments_group=arguments_group,
  894. schema={
  895. 'type': 'object',
  896. 'properties': {
  897. 'foo': {
  898. 'type': 'object',
  899. }
  900. },
  901. },
  902. unparsed_arguments=(),
  903. )
  904. def test_add_arguments_from_schema_with_array_adds_flag():
  905. arguments_group = flexmock()
  906. flexmock(module).should_receive('make_argument_description').and_return('help')
  907. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(str)
  908. arguments_group.should_receive('add_argument').with_args(
  909. '--foo',
  910. type=str,
  911. metavar='FOO',
  912. help='help',
  913. ).once()
  914. flexmock(module).should_receive('add_array_element_arguments')
  915. module.add_arguments_from_schema(
  916. arguments_group=arguments_group,
  917. schema={
  918. 'type': 'object',
  919. 'properties': {
  920. 'foo': {
  921. 'type': 'array',
  922. 'items': {
  923. 'type': 'integer',
  924. },
  925. }
  926. },
  927. },
  928. unparsed_arguments=(),
  929. )
  930. def test_add_arguments_from_schema_with_array_and_nested_object_adds_multiple_flags():
  931. arguments_group = flexmock()
  932. flexmock(module).should_receive('make_argument_description').and_return('help 1').and_return(
  933. 'help 2'
  934. )
  935. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(
  936. int
  937. ).and_return(str)
  938. arguments_group.should_receive('add_argument').with_args(
  939. '--foo[0].bar',
  940. type=int,
  941. metavar='BAR',
  942. help='help 1',
  943. ).once()
  944. arguments_group.should_receive('add_argument').with_args(
  945. '--foo',
  946. type=str,
  947. metavar='FOO',
  948. help='help 2',
  949. ).once()
  950. flexmock(module).should_receive('add_array_element_arguments')
  951. flexmock(module).should_receive('add_array_element_arguments').with_args(
  952. arguments_group=arguments_group,
  953. unparsed_arguments=(),
  954. flag_name='foo[0].bar',
  955. ).once()
  956. module.add_arguments_from_schema(
  957. arguments_group=arguments_group,
  958. schema={
  959. 'type': 'object',
  960. 'properties': {
  961. 'foo': {
  962. 'type': 'array',
  963. 'items': {
  964. 'type': 'object',
  965. 'properties': {
  966. 'bar': {
  967. 'type': 'integer',
  968. }
  969. },
  970. },
  971. }
  972. },
  973. },
  974. unparsed_arguments=(),
  975. )
  976. def test_add_arguments_from_schema_with_default_false_boolean_adds_valueless_flag():
  977. arguments_group = flexmock()
  978. flexmock(module).should_receive('make_argument_description').and_return('help')
  979. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(bool)
  980. arguments_group.should_receive('add_argument').with_args(
  981. '--foo',
  982. action='store_true',
  983. default=None,
  984. help='help',
  985. ).once()
  986. flexmock(module).should_receive('add_array_element_arguments')
  987. module.add_arguments_from_schema(
  988. arguments_group=arguments_group,
  989. schema={
  990. 'type': 'object',
  991. 'properties': {
  992. 'foo': {
  993. 'type': 'boolean',
  994. 'default': False,
  995. }
  996. },
  997. },
  998. unparsed_arguments=(),
  999. )
  1000. def test_add_arguments_from_schema_with_default_true_boolean_adds_value_flag():
  1001. arguments_group = flexmock()
  1002. flexmock(module).should_receive('make_argument_description').and_return('help')
  1003. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(bool)
  1004. arguments_group.should_receive('add_argument').with_args(
  1005. '--foo',
  1006. type=bool,
  1007. metavar='FOO',
  1008. help='help',
  1009. ).once()
  1010. flexmock(module).should_receive('add_array_element_arguments')
  1011. module.add_arguments_from_schema(
  1012. arguments_group=arguments_group,
  1013. schema={
  1014. 'type': 'object',
  1015. 'properties': {
  1016. 'foo': {
  1017. 'type': 'boolean',
  1018. 'default': True,
  1019. }
  1020. },
  1021. },
  1022. unparsed_arguments=(),
  1023. )
  1024. def test_add_arguments_from_schema_with_defaultless_boolean_adds_value_flag():
  1025. arguments_group = flexmock()
  1026. flexmock(module).should_receive('make_argument_description').and_return('help')
  1027. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(bool)
  1028. arguments_group.should_receive('add_argument').with_args(
  1029. '--foo',
  1030. type=bool,
  1031. metavar='FOO',
  1032. help='help',
  1033. ).once()
  1034. flexmock(module).should_receive('add_array_element_arguments')
  1035. module.add_arguments_from_schema(
  1036. arguments_group=arguments_group,
  1037. schema={
  1038. 'type': 'object',
  1039. 'properties': {
  1040. 'foo': {
  1041. 'type': 'boolean',
  1042. }
  1043. },
  1044. },
  1045. unparsed_arguments=(),
  1046. )
  1047. def test_add_arguments_from_schema_skips_omitted_flag_name():
  1048. arguments_group = flexmock()
  1049. flexmock(module).should_receive('make_argument_description').and_return('help')
  1050. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(str)
  1051. arguments_group.should_receive('add_argument').with_args(
  1052. '--match-archives',
  1053. type=object,
  1054. metavar=object,
  1055. help=object,
  1056. ).never()
  1057. arguments_group.should_receive('add_argument').with_args(
  1058. '--foo',
  1059. type=str,
  1060. metavar='FOO',
  1061. help='help',
  1062. ).once()
  1063. flexmock(module).should_receive('add_array_element_arguments')
  1064. module.add_arguments_from_schema(
  1065. arguments_group=arguments_group,
  1066. schema={
  1067. 'type': 'object',
  1068. 'properties': {
  1069. 'match_archives': {
  1070. 'type': 'string',
  1071. },
  1072. 'foo': {
  1073. 'type': 'string',
  1074. },
  1075. },
  1076. },
  1077. unparsed_arguments=(),
  1078. )
  1079. def test_add_arguments_from_schema_rewrites_option_name_to_flag_name():
  1080. arguments_group = flexmock()
  1081. flexmock(module).should_receive('make_argument_description').and_return('help')
  1082. flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(str)
  1083. arguments_group.should_receive('add_argument').with_args(
  1084. '--foo-and-stuff',
  1085. type=str,
  1086. metavar='FOO_AND_STUFF',
  1087. help='help',
  1088. ).once()
  1089. flexmock(module).should_receive('add_array_element_arguments')
  1090. module.add_arguments_from_schema(
  1091. arguments_group=arguments_group,
  1092. schema={
  1093. 'type': 'object',
  1094. 'properties': {
  1095. 'foo_and_stuff': {
  1096. 'type': 'string',
  1097. },
  1098. },
  1099. },
  1100. unparsed_arguments=(),
  1101. )