test_arguments.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  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_parse_and_record_action_arguments_without_action_name_leaves_arguments_untouched():
  45. unparsed_arguments = ('--foo', '--bar')
  46. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  47. unparsed_arguments
  48. )
  49. assert (
  50. module.parse_and_record_action_arguments(
  51. unparsed_arguments, flexmock(), flexmock(), 'action'
  52. )
  53. == unparsed_arguments
  54. )
  55. def test_parse_and_record_action_arguments_updates_parsed_arguments_and_returns_remaining():
  56. unparsed_arguments = ('action', '--foo', '--bar', '--verbosity', '1')
  57. other_parsed_arguments = flexmock()
  58. parsed_arguments = {'other': other_parsed_arguments}
  59. action_parsed_arguments = flexmock()
  60. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  61. unparsed_arguments
  62. )
  63. action_parser = flexmock()
  64. flexmock(action_parser).should_receive('parse_known_args').and_return(
  65. action_parsed_arguments, ('action', '--verbosity', '1')
  66. )
  67. assert module.parse_and_record_action_arguments(
  68. unparsed_arguments, parsed_arguments, action_parser, 'action'
  69. ) == ('--verbosity', '1')
  70. assert parsed_arguments == {'other': other_parsed_arguments, 'action': action_parsed_arguments}
  71. def test_parse_and_record_action_arguments_with_alias_updates_canonical_parsed_arguments():
  72. unparsed_arguments = ('action', '--foo', '--bar', '--verbosity', '1')
  73. other_parsed_arguments = flexmock()
  74. parsed_arguments = {'other': other_parsed_arguments}
  75. action_parsed_arguments = flexmock()
  76. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  77. unparsed_arguments
  78. )
  79. action_parser = flexmock()
  80. flexmock(action_parser).should_receive('parse_known_args').and_return(
  81. action_parsed_arguments, ('action', '--verbosity', '1')
  82. )
  83. assert module.parse_and_record_action_arguments(
  84. unparsed_arguments, parsed_arguments, action_parser, 'action', canonical_name='doit'
  85. ) == ('--verbosity', '1')
  86. assert parsed_arguments == {'other': other_parsed_arguments, 'doit': action_parsed_arguments}
  87. def test_parse_and_record_action_arguments_with_borg_action_consumes_arguments_after_action_name():
  88. unparsed_arguments = ('--verbosity', '1', 'borg', 'list')
  89. parsed_arguments = {}
  90. borg_parsed_arguments = flexmock(options=flexmock())
  91. flexmock(module).should_receive('omit_values_colliding_with_action_names').and_return(
  92. unparsed_arguments
  93. )
  94. borg_parser = flexmock()
  95. flexmock(borg_parser).should_receive('parse_known_args').and_return(
  96. borg_parsed_arguments, ('--verbosity', '1', 'borg', 'list')
  97. )
  98. assert module.parse_and_record_action_arguments(
  99. unparsed_arguments,
  100. parsed_arguments,
  101. borg_parser,
  102. 'borg',
  103. ) == ('--verbosity', '1')
  104. assert parsed_arguments == {'borg': borg_parsed_arguments}
  105. assert borg_parsed_arguments.options == ('list',)
  106. @pytest.mark.parametrize(
  107. 'argument, expected',
  108. [
  109. ('--foo', True),
  110. ('foo', False),
  111. (33, False),
  112. ],
  113. )
  114. def test_argument_is_flag_only_for_string_starting_with_double_dash(argument, expected):
  115. assert module.argument_is_flag(argument) == expected
  116. @pytest.mark.parametrize(
  117. 'arguments, expected',
  118. [
  119. # Ending with a valueless flag.
  120. (
  121. ('--foo', '--bar', 33, '--baz'),
  122. (
  123. ('--foo',),
  124. ('--bar', 33),
  125. ('--baz',),
  126. ),
  127. ),
  128. # Ending with a flag and its corresponding value.
  129. (
  130. ('--foo', '--bar', 33, '--baz', '--quux', 'thing'),
  131. (('--foo',), ('--bar', 33), ('--baz',), ('--quux', 'thing')),
  132. ),
  133. # Starting with an action name.
  134. (
  135. ('check', '--foo', '--bar', 33, '--baz'),
  136. (
  137. ('check',),
  138. ('--foo',),
  139. ('--bar', 33),
  140. ('--baz',),
  141. ),
  142. ),
  143. # Action name that one could mistake for a flag value.
  144. (('--progress', 'list'), (('--progress',), ('list',))),
  145. # No arguments.
  146. ((), ()),
  147. ],
  148. )
  149. def test_group_arguments_with_values_returns_flags_with_corresponding_values(arguments, expected):
  150. flexmock(module).should_receive('argument_is_flag').with_args('--foo').and_return(True)
  151. flexmock(module).should_receive('argument_is_flag').with_args('--bar').and_return(True)
  152. flexmock(module).should_receive('argument_is_flag').with_args('--baz').and_return(True)
  153. flexmock(module).should_receive('argument_is_flag').with_args('--quux').and_return(True)
  154. flexmock(module).should_receive('argument_is_flag').with_args('--progress').and_return(True)
  155. flexmock(module).should_receive('argument_is_flag').with_args(33).and_return(False)
  156. flexmock(module).should_receive('argument_is_flag').with_args('thing').and_return(False)
  157. flexmock(module).should_receive('argument_is_flag').with_args('check').and_return(False)
  158. flexmock(module).should_receive('argument_is_flag').with_args('list').and_return(False)
  159. assert module.group_arguments_with_values(arguments) == expected
  160. @pytest.mark.parametrize(
  161. 'arguments, grouped_arguments, expected',
  162. [
  163. # An unparsable flag remaining from each parsed action.
  164. (
  165. (
  166. ('--latest', 'archive', 'prune', 'extract', 'list', '--flag'),
  167. ('--latest', 'archive', 'check', 'extract', 'list', '--flag'),
  168. ('prune', 'check', 'list', '--flag'),
  169. ('prune', 'check', 'extract', '--flag'),
  170. ),
  171. (
  172. (
  173. ('--latest',),
  174. ('archive',),
  175. ('prune',),
  176. ('extract',),
  177. ('list',),
  178. ('--flag',),
  179. ),
  180. (
  181. ('--latest',),
  182. ('archive',),
  183. ('check',),
  184. ('extract',),
  185. ('list',),
  186. ('--flag',),
  187. ),
  188. (('prune',), ('check',), ('list',), ('--flag',)),
  189. (('prune',), ('check',), ('extract',), ('--flag',)),
  190. ),
  191. ('--flag',),
  192. ),
  193. # No unparsable flags remaining.
  194. (
  195. (
  196. ('--archive', 'archive', 'prune', 'extract', 'list'),
  197. ('--archive', 'archive', 'check', 'extract', 'list'),
  198. ('prune', 'check', 'list'),
  199. ('prune', 'check', 'extract'),
  200. ),
  201. (
  202. (
  203. (
  204. '--archive',
  205. 'archive',
  206. ),
  207. ('prune',),
  208. ('extract',),
  209. ('list',),
  210. ),
  211. (
  212. (
  213. '--archive',
  214. 'archive',
  215. ),
  216. ('check',),
  217. ('extract',),
  218. ('list',),
  219. ),
  220. (('prune',), ('check',), ('list',)),
  221. (('prune',), ('check',), ('extract',)),
  222. ),
  223. (),
  224. ),
  225. # No unparsable flags remaining, but some values in common.
  226. (
  227. (
  228. ('--verbosity', '5', 'archive', 'prune', 'extract', 'list'),
  229. ('--last', '5', 'archive', 'check', 'extract', 'list'),
  230. ('prune', 'check', 'list', '--last', '5'),
  231. ('prune', 'check', '--verbosity', '5', 'extract'),
  232. ),
  233. (
  234. (('--verbosity', '5'), ('archive',), ('prune',), ('extract',), ('list',)),
  235. (
  236. (
  237. '--last',
  238. '5',
  239. ),
  240. ('archive',),
  241. ('check',),
  242. ('extract',),
  243. ('list',),
  244. ),
  245. (('prune',), ('check',), ('list',), ('--last', '5')),
  246. (
  247. ('prune',),
  248. ('check',),
  249. (
  250. '--verbosity',
  251. '5',
  252. ),
  253. ('extract',),
  254. ),
  255. ),
  256. (),
  257. ),
  258. # No flags.
  259. ((), (), ()),
  260. ],
  261. )
  262. def test_get_unparsable_arguments_returns_remaining_arguments_that_no_action_can_parse(
  263. arguments, grouped_arguments, expected
  264. ):
  265. for action_arguments, grouped_action_arguments in zip(arguments, grouped_arguments):
  266. flexmock(module).should_receive('group_arguments_with_values').with_args(
  267. action_arguments
  268. ).and_return(grouped_action_arguments)
  269. assert module.get_unparsable_arguments(arguments) == expected
  270. def test_parse_arguments_for_actions_consumes_action_arguments_after_action_name():
  271. action_namespace = flexmock(foo=True)
  272. remaining = flexmock()
  273. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  274. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  275. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  276. {action: action_namespace}
  277. )
  278. or remaining
  279. )
  280. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  281. action_parsers = {'action': flexmock(), 'other': flexmock()}
  282. global_namespace = flexmock(config_paths=[])
  283. global_parser = flexmock()
  284. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  285. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  286. ('action', '--foo', 'true'), action_parsers, global_parser
  287. )
  288. assert arguments == {'global': global_namespace, 'action': action_namespace}
  289. assert remaining_action_arguments == (remaining, ())
  290. def test_parse_arguments_for_actions_consumes_action_arguments_with_alias():
  291. action_namespace = flexmock(foo=True)
  292. remaining = flexmock()
  293. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  294. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  295. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  296. {canonical or action: action_namespace}
  297. )
  298. or remaining
  299. )
  300. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  301. action_parsers = {
  302. 'action': flexmock(),
  303. '-a': flexmock(),
  304. 'other': flexmock(),
  305. '-o': flexmock(),
  306. }
  307. global_namespace = flexmock(config_paths=[])
  308. global_parser = flexmock()
  309. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  310. flexmock(module).ACTION_ALIASES = {'action': ['-a'], 'other': ['-o']}
  311. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  312. ('-a', '--foo', 'true'), action_parsers, global_parser
  313. )
  314. assert arguments == {'global': global_namespace, 'action': action_namespace}
  315. assert remaining_action_arguments == (remaining, ())
  316. def test_parse_arguments_for_actions_consumes_multiple_action_arguments():
  317. action_namespace = flexmock(foo=True)
  318. other_namespace = flexmock(bar=3)
  319. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  320. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  321. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  322. {action: action_namespace if action == 'action' else other_namespace}
  323. )
  324. or ()
  325. ).and_return(('other', '--bar', '3')).and_return('action', '--foo', 'true')
  326. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  327. action_parsers = {
  328. 'action': flexmock(),
  329. 'other': flexmock(),
  330. }
  331. global_namespace = flexmock(config_paths=[])
  332. global_parser = flexmock()
  333. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  334. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  335. ('action', '--foo', 'true', 'other', '--bar', '3'), action_parsers, global_parser
  336. )
  337. assert arguments == {
  338. 'global': global_namespace,
  339. 'action': action_namespace,
  340. 'other': other_namespace,
  341. }
  342. assert remaining_action_arguments == ((), (), ())
  343. def test_parse_arguments_for_actions_respects_command_line_action_ordering():
  344. other_namespace = flexmock()
  345. action_namespace = flexmock(foo=True)
  346. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  347. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  348. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  349. {action: other_namespace if action == 'other' else action_namespace}
  350. )
  351. or ()
  352. ).and_return(('action',)).and_return(('other', '--foo', 'true'))
  353. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  354. action_parsers = {
  355. 'action': flexmock(),
  356. 'other': flexmock(),
  357. }
  358. global_namespace = flexmock(config_paths=[])
  359. global_parser = flexmock()
  360. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  361. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  362. ('other', '--foo', 'true', 'action'), action_parsers, global_parser
  363. )
  364. assert arguments == collections.OrderedDict(
  365. [('other', other_namespace), ('action', action_namespace), ('global', global_namespace)]
  366. )
  367. assert remaining_action_arguments == ((), (), ())
  368. def test_parse_arguments_for_actions_applies_default_action_parsers():
  369. global_namespace = flexmock(config_paths=[])
  370. namespaces = {
  371. 'global': global_namespace,
  372. 'prune': flexmock(),
  373. 'compact': flexmock(),
  374. 'create': flexmock(progress=True),
  375. 'check': flexmock(),
  376. }
  377. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  378. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  379. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  380. {action: namespaces.get(action)}
  381. )
  382. or ()
  383. ).and_return(())
  384. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  385. action_parsers = {
  386. 'prune': flexmock(),
  387. 'compact': flexmock(),
  388. 'create': flexmock(),
  389. 'check': flexmock(),
  390. 'other': flexmock(),
  391. }
  392. global_parser = flexmock()
  393. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  394. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  395. ('--progress'), action_parsers, global_parser
  396. )
  397. assert arguments == namespaces
  398. assert remaining_action_arguments == ((), (), (), (), ())
  399. def test_parse_arguments_for_actions_consumes_global_arguments():
  400. action_namespace = flexmock()
  401. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  402. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  403. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  404. {action: action_namespace}
  405. )
  406. or ('--verbosity', 'lots')
  407. )
  408. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  409. action_parsers = {
  410. 'action': flexmock(),
  411. 'other': flexmock(),
  412. }
  413. global_namespace = flexmock(config_paths=[])
  414. global_parser = flexmock()
  415. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  416. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  417. ('action', '--verbosity', 'lots'), action_parsers, global_parser
  418. )
  419. assert arguments == {'global': global_namespace, 'action': action_namespace}
  420. assert remaining_action_arguments == (('--verbosity', 'lots'), ())
  421. def test_parse_arguments_for_actions_passes_through_unknown_arguments_before_action_name():
  422. action_namespace = flexmock()
  423. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  424. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  425. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  426. {action: action_namespace}
  427. )
  428. or ('--wtf', 'yes')
  429. )
  430. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  431. action_parsers = {
  432. 'action': flexmock(),
  433. 'other': flexmock(),
  434. }
  435. global_namespace = flexmock(config_paths=[])
  436. global_parser = flexmock()
  437. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  438. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  439. ('--wtf', 'yes', 'action'), action_parsers, global_parser
  440. )
  441. assert arguments == {'global': global_namespace, 'action': action_namespace}
  442. assert remaining_action_arguments == (('--wtf', 'yes'), ())
  443. def test_parse_arguments_for_actions_passes_through_unknown_arguments_after_action_name():
  444. action_namespace = flexmock()
  445. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  446. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  447. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  448. {action: action_namespace}
  449. )
  450. or ('--wtf', 'yes')
  451. )
  452. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  453. action_parsers = {
  454. 'action': flexmock(),
  455. 'other': flexmock(),
  456. }
  457. global_namespace = flexmock(config_paths=[])
  458. global_parser = flexmock()
  459. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  460. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  461. ('action', '--wtf', 'yes'), action_parsers, global_parser
  462. )
  463. assert arguments == {'global': global_namespace, 'action': action_namespace}
  464. assert remaining_action_arguments == (('--wtf', 'yes'), ())
  465. def test_parse_arguments_for_actions_with_borg_action_skips_other_action_parsers():
  466. action_namespace = flexmock(options=[])
  467. flexmock(module).should_receive('get_subaction_parsers').and_return({})
  468. flexmock(module).should_receive('parse_and_record_action_arguments').replace_with(
  469. lambda unparsed, parsed, parser, action, canonical=None: parsed.update(
  470. {action: action_namespace}
  471. )
  472. or ()
  473. ).and_return(())
  474. flexmock(module).should_receive('get_subactions_for_actions').and_return({})
  475. action_parsers = {
  476. 'borg': flexmock(),
  477. 'list': flexmock(),
  478. }
  479. global_namespace = flexmock(config_paths=[])
  480. global_parser = flexmock()
  481. global_parser.should_receive('parse_known_args').and_return((global_namespace, ()))
  482. arguments, remaining_action_arguments = module.parse_arguments_for_actions(
  483. ('borg', 'list'), action_parsers, global_parser
  484. )
  485. assert arguments == {'global': global_namespace, 'borg': action_namespace}
  486. assert remaining_action_arguments == ((), ())
  487. def test_parse_arguments_for_actions_raises_error_when_no_action_is_specified():
  488. flexmock(module).should_receive('get_subaction_parsers').and_return({'bootstrap': [flexmock()]})
  489. flexmock(module).should_receive('parse_and_record_action_arguments').and_return(flexmock())
  490. flexmock(module).should_receive('get_subactions_for_actions').and_return(
  491. {'config': ['bootstrap']}
  492. )
  493. action_parsers = {'config': flexmock()}
  494. global_parser = flexmock()
  495. global_parser.should_receive('parse_known_args').and_return((flexmock(), ()))
  496. with pytest.raises(ValueError):
  497. module.parse_arguments_for_actions(('config',), action_parsers, global_parser)