test_generate.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. import os
  2. import sys
  3. from io import StringIO
  4. import pytest
  5. from flexmock import flexmock
  6. from borgmatic.config import generate as module
  7. def test_insert_newline_before_comment_does_not_raise():
  8. field_name = 'foo'
  9. config = module.ruamel.yaml.comments.CommentedMap([(field_name, 33)])
  10. config.yaml_set_comment_before_after_key(key=field_name, before='Comment')
  11. module.insert_newline_before_comment(config, field_name)
  12. def test_schema_to_sample_configuration_comments_out_non_default_options():
  13. schema = {
  14. 'type': 'object',
  15. 'properties': dict(
  16. [
  17. ('field1', {'type': 'string', 'example': 'Example 1'}),
  18. ('field2', {'type': 'string', 'example': 'Example 2'}),
  19. ('source_directories', {'type': 'string', 'example': 'Example 3'}),
  20. ]
  21. ),
  22. }
  23. config = module.schema_to_sample_configuration(schema)
  24. assert config == dict(
  25. [
  26. ('field1', 'Example 1'),
  27. ('field2', 'Example 2'),
  28. ('source_directories', 'Example 3'),
  29. ]
  30. )
  31. assert 'COMMENT_OUT' in config.ca.items['field1'][1][-1]._value
  32. assert 'COMMENT_OUT' in config.ca.items['field2'][1][-1]._value
  33. assert 'source_directories' not in config.ca.items
  34. def test_schema_to_sample_configuration_comments_out_non_source_config_options():
  35. schema = {
  36. 'type': 'object',
  37. 'properties': dict(
  38. [
  39. ('field1', {'type': 'string', 'example': 'Example 1'}),
  40. ('field2', {'type': 'string', 'example': 'Example 2'}),
  41. ('field3', {'type': 'string', 'example': 'Example 3'}),
  42. ]
  43. ),
  44. }
  45. source_config = {'field3': 'value'}
  46. config = module.schema_to_sample_configuration(schema, source_config)
  47. assert config == dict(
  48. [
  49. ('field1', 'Example 1'),
  50. ('field2', 'Example 2'),
  51. ('field3', 'Example 3'),
  52. ]
  53. )
  54. assert 'COMMENT_OUT' in config.ca.items['field1'][1][-1]._value
  55. assert 'COMMENT_OUT' in config.ca.items['field2'][1][-1]._value
  56. assert 'field3' not in config.ca.items
  57. def test_schema_to_sample_configuration_comments_out_non_default_options_in_sequence_of_maps():
  58. schema = {
  59. 'type': 'array',
  60. 'items': {
  61. 'type': 'object',
  62. 'properties': dict(
  63. [
  64. ('field1', {'type': 'string', 'example': 'Example 1'}),
  65. ('field2', {'type': 'string', 'example': 'Example 2'}),
  66. ('source_directories', {'type': 'string', 'example': 'Example 3'}),
  67. ]
  68. ),
  69. },
  70. }
  71. config = module.schema_to_sample_configuration(schema)
  72. assert config == [
  73. dict(
  74. [('field1', 'Example 1'), ('field2', 'Example 2'), ('source_directories', 'Example 3')]
  75. )
  76. ]
  77. # The first field in a sequence does not get commented.
  78. assert 'field1' not in config[0].ca.items
  79. assert 'COMMENT_OUT' in config[0].ca.items['field2'][1][-1]._value
  80. assert 'source_directories' not in config[0].ca.items
  81. def test_schema_to_sample_configuration_comments_out_non_source_config_options_in_sequence_of_maps():
  82. schema = {
  83. 'type': 'array',
  84. 'items': {
  85. 'type': 'object',
  86. 'properties': dict(
  87. [
  88. ('field1', {'type': 'string', 'example': 'Example 1'}),
  89. ('field2', {'type': 'string', 'example': 'Example 2'}),
  90. ('field3', {'type': 'string', 'example': 'Example 3'}),
  91. ]
  92. ),
  93. },
  94. }
  95. source_config = [{'field3': 'value'}]
  96. config = module.schema_to_sample_configuration(schema, source_config)
  97. assert config == [
  98. dict([('field1', 'Example 1'), ('field2', 'Example 2'), ('field3', 'Example 3')])
  99. ]
  100. # The first field in a sequence does not get commented.
  101. assert 'field1' not in config[0].ca.items
  102. assert 'COMMENT_OUT' in config[0].ca.items['field2'][1][-1]._value
  103. assert 'field3' not in config[0].ca.items
  104. def test_schema_to_sample_configuration_comments_out_non_source_config_options_in_sequence_of_maps_with_different_subschemas():
  105. schema = {
  106. 'type': 'array',
  107. 'items': {
  108. 'type': 'object',
  109. 'oneOf': [
  110. {
  111. 'properties': dict(
  112. [
  113. ('field1', {'type': 'string', 'example': 'Example 1'}),
  114. ('field2', {'type': 'string', 'example': 'Example 2'}),
  115. ]
  116. )
  117. },
  118. {
  119. 'properties': dict(
  120. [
  121. ('field2', {'type': 'string', 'example': 'Example 2'}),
  122. ('field3', {'type': 'string', 'example': 'Example 3'}),
  123. ]
  124. )
  125. },
  126. ],
  127. },
  128. }
  129. source_config = [{'field1': 'value'}, {'field3': 'value'}]
  130. config = module.schema_to_sample_configuration(schema, source_config)
  131. assert config == [
  132. dict([('field1', 'Example 1'), ('field2', 'Example 2'), ('field3', 'Example 3')])
  133. ]
  134. # The first field in a sequence does not get commented.
  135. assert 'field1' not in config[0].ca.items
  136. assert 'COMMENT_OUT' in config[0].ca.items['field2'][1][-1]._value
  137. assert 'COMMENT_OUT' in config[0].ca.items['field3'][1][-1]._value
  138. def test_comment_out_line_skips_blank_line():
  139. line = ' \n'
  140. assert module.comment_out_line(line) == line
  141. def test_comment_out_line_skips_already_commented_out_line():
  142. line = ' # foo'
  143. assert module.comment_out_line(line) == line
  144. def test_comment_out_line_comments_section_name():
  145. line = 'figgy-pudding:'
  146. assert module.comment_out_line(line) == '# ' + line
  147. def test_comment_out_line_comments_indented_option():
  148. line = ' enabled: true'
  149. assert module.comment_out_line(line) == ' # enabled: true'
  150. def test_comment_out_line_comments_twice_indented_option():
  151. line = ' - item'
  152. assert module.comment_out_line(line) == ' # - item'
  153. def test_comment_out_optional_configuration_comments_optional_config_only():
  154. # The "# COMMENT_OUT" comment is a sentinel used to express that the following key is optional.
  155. # It's stripped out of the final output.
  156. flexmock(module).comment_out_line = lambda line: '# ' + line
  157. config = '''
  158. # COMMENT_OUT
  159. foo:
  160. # COMMENT_OUT
  161. bar:
  162. - baz
  163. - quux
  164. repositories:
  165. - path: foo
  166. # COMMENT_OUT
  167. label: bar
  168. - path: baz
  169. label: quux
  170. # This comment should be kept.
  171. # COMMENT_OUT
  172. other: thing
  173. '''
  174. # flake8: noqa
  175. expected_config = '''
  176. # foo:
  177. # bar:
  178. # - baz
  179. # - quux
  180. repositories:
  181. - path: foo
  182. # label: bar
  183. - path: baz
  184. label: quux
  185. # This comment should be kept.
  186. # other: thing
  187. '''
  188. assert module.comment_out_optional_configuration(config.strip()) == expected_config.strip()
  189. def test_render_configuration_converts_configuration_to_yaml_string():
  190. yaml_string = module.render_configuration({'foo': 'bar'})
  191. assert yaml_string == 'foo: bar\n'
  192. def test_write_configuration_does_not_raise():
  193. flexmock(os.path).should_receive('exists').and_return(False)
  194. flexmock(os).should_receive('makedirs')
  195. builtins = flexmock(sys.modules['builtins'])
  196. builtins.should_receive('open').and_return(StringIO())
  197. flexmock(os).should_receive('chmod')
  198. module.write_configuration('config.yaml', 'config: yaml')
  199. def test_write_configuration_with_already_existing_file_raises():
  200. flexmock(os.path).should_receive('exists').and_return(True)
  201. with pytest.raises(FileExistsError):
  202. module.write_configuration('config.yaml', 'config: yaml')
  203. def test_write_configuration_with_already_existing_file_and_overwrite_does_not_raise():
  204. flexmock(os.path).should_receive('exists').and_return(True)
  205. module.write_configuration('/tmp/config.yaml', 'config: yaml', overwrite=True)
  206. def test_write_configuration_with_already_existing_directory_does_not_raise():
  207. flexmock(os.path).should_receive('exists').and_return(False)
  208. flexmock(os).should_receive('makedirs').and_raise(FileExistsError)
  209. builtins = flexmock(sys.modules['builtins'])
  210. builtins.should_receive('open').and_return(StringIO())
  211. flexmock(os).should_receive('chmod')
  212. module.write_configuration('config.yaml', 'config: yaml')
  213. def test_add_comments_to_configuration_sequence_of_strings_does_not_raise():
  214. config = module.ruamel.yaml.comments.CommentedSeq(['foo', 'bar'])
  215. schema = {'type': 'array', 'items': {'type': 'string'}}
  216. module.add_comments_to_configuration_sequence(config, schema)
  217. def test_add_comments_to_configuration_sequence_of_maps_does_not_raise():
  218. config = module.ruamel.yaml.comments.CommentedSeq(
  219. [module.ruamel.yaml.comments.CommentedMap([('foo', 'yo')])]
  220. )
  221. schema = {
  222. 'type': 'array',
  223. 'items': {'type': 'object', 'properties': {'foo': {'description': 'yo'}}},
  224. }
  225. module.add_comments_to_configuration_sequence(config, schema)
  226. def test_add_comments_to_configuration_sequence_of_maps_without_description_does_not_raise():
  227. config = module.ruamel.yaml.comments.CommentedSeq(
  228. [module.ruamel.yaml.comments.CommentedMap([('foo', 'yo')])]
  229. )
  230. schema = {'type': 'array', 'items': {'type': 'object', 'properties': {'foo': {}}}}
  231. module.add_comments_to_configuration_sequence(config, schema)
  232. def test_add_comments_to_configuration_comments_out_non_default_options():
  233. # Ensure that it can deal with fields both in the schema and missing from the schema.
  234. config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
  235. schema = {
  236. 'type': 'object',
  237. 'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
  238. }
  239. module.add_comments_to_configuration_object(config, schema)
  240. assert 'COMMENT_OUT' in config.ca.items['foo'][1][-1]._value
  241. assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
  242. assert 'baz' not in config.ca.items
  243. def test_add_comments_to_configuration_comments_out_non_source_config_options():
  244. # Ensure that it can deal with fields both in the schema and missing from the schema.
  245. config = module.ruamel.yaml.comments.CommentedMap(
  246. [('repositories', 33), ('bar', 44), ('baz', 55)]
  247. )
  248. schema = {
  249. 'type': 'object',
  250. 'properties': {
  251. 'repositories': {'description': 'repositories'},
  252. 'bar': {'description': 'Bar'},
  253. },
  254. }
  255. module.add_comments_to_configuration_object(config, schema)
  256. assert 'repositories' in config.ca.items
  257. assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
  258. assert 'baz' not in config.ca.items
  259. def test_add_comments_to_configuration_object_with_skip_first_field_does_not_comment_out_first_option():
  260. config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
  261. schema = {
  262. 'type': 'object',
  263. 'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
  264. }
  265. module.add_comments_to_configuration_object(config, schema, skip_first_field=True)
  266. assert 'foo' not in config.ca.items
  267. assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
  268. assert 'baz' not in config.ca.items
  269. def test_add_comments_to_configuration_object_with_skip_first_field_does_not_comment_out_first_option():
  270. config = module.ruamel.yaml.comments.CommentedMap([('foo', 33), ('bar', 44), ('baz', 55)])
  271. schema = {
  272. 'type': 'object',
  273. 'properties': {'foo': {'description': 'Foo'}, 'bar': {'description': 'Bar'}},
  274. }
  275. module.add_comments_to_configuration_object(config, schema, skip_first_field=True)
  276. assert 'foo' not in config.ca.items
  277. assert 'COMMENT_OUT' in config.ca.items['bar'][1][-1]._value
  278. assert 'baz' not in config.ca.items
  279. def test_generate_sample_configuration_does_not_raise():
  280. builtins = flexmock(sys.modules['builtins'])
  281. builtins.should_receive('open').with_args('schema.yaml').and_return('')
  282. flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
  283. flexmock(load=lambda filename: {})
  284. )
  285. flexmock(module).should_receive('schema_to_sample_configuration')
  286. flexmock(module).should_receive('merge_source_configuration_into_destination')
  287. flexmock(module).should_receive('render_configuration')
  288. flexmock(module).should_receive('comment_out_optional_configuration')
  289. flexmock(module).should_receive('write_configuration')
  290. module.generate_sample_configuration(False, None, 'dest.yaml', 'schema.yaml')
  291. def test_generate_sample_configuration_with_source_filename_omits_empty_bootstrap_field():
  292. builtins = flexmock(sys.modules['builtins'])
  293. builtins.should_receive('open').with_args('schema.yaml').and_return('')
  294. flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
  295. flexmock(load=lambda filename: {})
  296. )
  297. flexmock(module.load).should_receive('load_configuration').and_return(
  298. {'bootstrap': {}, 'foo': 'bar'}
  299. )
  300. flexmock(module.normalize).should_receive('normalize')
  301. flexmock(module).should_receive('schema_to_sample_configuration').with_args(
  302. object, {'foo': 'bar'}
  303. ).once()
  304. flexmock(module).should_receive('merge_source_configuration_into_destination')
  305. flexmock(module).should_receive('render_configuration')
  306. flexmock(module).should_receive('comment_out_optional_configuration')
  307. flexmock(module).should_receive('write_configuration')
  308. module.generate_sample_configuration(False, 'source.yaml', 'dest.yaml', 'schema.yaml')
  309. def test_generate_sample_configuration_with_source_filename_keeps_non_empty_bootstrap_field():
  310. builtins = flexmock(sys.modules['builtins'])
  311. builtins.should_receive('open').with_args('schema.yaml').and_return('')
  312. flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
  313. flexmock(load=lambda filename: {})
  314. )
  315. source_config = {'bootstrap': {'stuff': 'here'}, 'foo': 'bar'}
  316. flexmock(module.load).should_receive('load_configuration').and_return(source_config)
  317. flexmock(module.normalize).should_receive('normalize')
  318. flexmock(module).should_receive('schema_to_sample_configuration').with_args(
  319. object, source_config
  320. ).once()
  321. flexmock(module).should_receive('merge_source_configuration_into_destination')
  322. flexmock(module).should_receive('render_configuration')
  323. flexmock(module).should_receive('comment_out_optional_configuration')
  324. flexmock(module).should_receive('write_configuration')
  325. module.generate_sample_configuration(False, 'source.yaml', 'dest.yaml', 'schema.yaml')
  326. def test_generate_sample_configuration_with_dry_run_does_not_write_file():
  327. builtins = flexmock(sys.modules['builtins'])
  328. builtins.should_receive('open').with_args('schema.yaml').and_return('')
  329. flexmock(module.ruamel.yaml).should_receive('YAML').and_return(
  330. flexmock(load=lambda filename: {})
  331. )
  332. flexmock(module).should_receive('schema_to_sample_configuration')
  333. flexmock(module).should_receive('merge_source_configuration_into_destination')
  334. flexmock(module).should_receive('render_configuration')
  335. flexmock(module).should_receive('comment_out_optional_configuration')
  336. flexmock(module).should_receive('write_configuration').never()
  337. module.generate_sample_configuration(True, None, 'dest.yaml', 'schema.yaml')