test_validate.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import io
  2. import string
  3. import sys
  4. import pytest
  5. from flexmock import flexmock
  6. from borgmatic.config import validate as module
  7. def test_schema_filename_returns_plausible_path():
  8. schema_path = module.schema_filename()
  9. assert schema_path.endswith('/schema.yaml')
  10. def mock_config_and_schema(config_yaml, schema_yaml=None):
  11. '''
  12. Set up mocks for the given config config YAML string and the schema YAML string, or the default
  13. schema if no schema is provided. The idea is that that the code under test consumes these mocks
  14. when parsing the configuration.
  15. '''
  16. config_stream = io.StringIO(config_yaml)
  17. config_stream.name = 'config.yaml'
  18. if schema_yaml is None:
  19. schema_stream = open(module.schema_filename())
  20. else:
  21. schema_stream = io.StringIO(schema_yaml)
  22. schema_stream.name = 'schema.yaml'
  23. builtins = flexmock(sys.modules['builtins'])
  24. flexmock(module.os).should_receive('getcwd').and_return('/tmp')
  25. flexmock(module.os.path).should_receive('isabs').and_return(False)
  26. flexmock(module.os.path).should_receive('exists').and_return(True)
  27. builtins.should_receive('open').with_args('/tmp/config.yaml').and_return(config_stream)
  28. builtins.should_receive('open').with_args('/tmp/schema.yaml').and_return(schema_stream)
  29. def test_parse_configuration_transforms_file_into_mapping():
  30. mock_config_and_schema(
  31. '''
  32. source_directories:
  33. - /home
  34. - /etc
  35. repositories:
  36. - path: hostname.borg
  37. keep_minutely: 60
  38. keep_hourly: 24
  39. keep_daily: 7
  40. checks:
  41. - name: repository
  42. - name: archives
  43. '''
  44. )
  45. config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  46. assert config == {
  47. 'source_directories': ['/home', '/etc'],
  48. 'repositories': [{'path': 'hostname.borg'}],
  49. 'keep_daily': 7,
  50. 'keep_hourly': 24,
  51. 'keep_minutely': 60,
  52. 'checks': [{'name': 'repository'}, {'name': 'archives'}],
  53. }
  54. assert logs == []
  55. def test_parse_configuration_passes_through_quoted_punctuation():
  56. escaped_punctuation = string.punctuation.replace('\\', r'\\').replace('"', r'\"')
  57. mock_config_and_schema(
  58. f'''
  59. source_directories:
  60. - "/home/{escaped_punctuation}"
  61. repositories:
  62. - path: test.borg
  63. '''
  64. )
  65. config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  66. assert config == {
  67. 'source_directories': [f'/home/{string.punctuation}'],
  68. 'repositories': [{'path': 'test.borg'}],
  69. }
  70. assert logs == []
  71. def test_parse_configuration_with_schema_lacking_examples_does_not_raise():
  72. mock_config_and_schema(
  73. '''
  74. source_directories:
  75. - /home
  76. repositories:
  77. - path: hostname.borg
  78. ''',
  79. '''
  80. map:
  81. source_directories:
  82. required: true
  83. seq:
  84. - type: scalar
  85. repositories:
  86. required: true
  87. seq:
  88. - type: scalar
  89. ''',
  90. )
  91. module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  92. def test_parse_configuration_inlines_include_inside_deprecated_section():
  93. mock_config_and_schema(
  94. '''
  95. source_directories:
  96. - /home
  97. repositories:
  98. - path: hostname.borg
  99. retention:
  100. !include include.yaml
  101. '''
  102. )
  103. builtins = flexmock(sys.modules['builtins'])
  104. include_file = io.StringIO(
  105. '''
  106. keep_daily: 7
  107. keep_hourly: 24
  108. '''
  109. )
  110. include_file.name = 'include.yaml'
  111. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  112. config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  113. assert config == {
  114. 'source_directories': ['/home'],
  115. 'repositories': [{'path': 'hostname.borg'}],
  116. 'keep_daily': 7,
  117. 'keep_hourly': 24,
  118. }
  119. assert len(logs) == 1
  120. def test_parse_configuration_merges_include():
  121. mock_config_and_schema(
  122. '''
  123. source_directories:
  124. - /home
  125. repositories:
  126. - path: hostname.borg
  127. keep_daily: 1
  128. <<: !include include.yaml
  129. '''
  130. )
  131. builtins = flexmock(sys.modules['builtins'])
  132. include_file = io.StringIO(
  133. '''
  134. keep_daily: 7
  135. keep_hourly: 24
  136. '''
  137. )
  138. include_file.name = 'include.yaml'
  139. builtins.should_receive('open').with_args('/tmp/include.yaml').and_return(include_file)
  140. config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  141. assert config == {
  142. 'source_directories': ['/home'],
  143. 'repositories': [{'path': 'hostname.borg'}],
  144. 'keep_daily': 1,
  145. 'keep_hourly': 24,
  146. }
  147. assert logs == []
  148. def test_parse_configuration_raises_for_missing_config_file():
  149. with pytest.raises(FileNotFoundError):
  150. module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  151. def test_parse_configuration_raises_for_missing_schema_file():
  152. mock_config_and_schema('')
  153. builtins = flexmock(sys.modules['builtins'])
  154. builtins.should_receive('open').with_args('/tmp/schema.yaml').and_raise(FileNotFoundError)
  155. with pytest.raises(FileNotFoundError):
  156. module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  157. def test_parse_configuration_raises_for_syntax_error():
  158. mock_config_and_schema('foo:\nbar')
  159. with pytest.raises(ValueError):
  160. module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  161. def test_parse_configuration_raises_for_validation_error():
  162. mock_config_and_schema(
  163. '''
  164. source_directories: yes
  165. repositories:
  166. - path: hostname.borg
  167. '''
  168. )
  169. with pytest.raises(module.Validation_error):
  170. module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  171. def test_parse_configuration_applies_overrides():
  172. mock_config_and_schema(
  173. '''
  174. source_directories:
  175. - /home
  176. repositories:
  177. - path: hostname.borg
  178. local_path: borg1
  179. '''
  180. )
  181. config, logs = module.parse_configuration(
  182. '/tmp/config.yaml', '/tmp/schema.yaml', overrides=['location.local_path=borg2']
  183. )
  184. assert config == {
  185. 'source_directories': ['/home'],
  186. 'repositories': [{'path': 'hostname.borg'}],
  187. 'local_path': 'borg2',
  188. }
  189. assert logs == []
  190. def test_parse_configuration_applies_normalization():
  191. mock_config_and_schema(
  192. '''
  193. location:
  194. source_directories:
  195. - /home
  196. repositories:
  197. - path: hostname.borg
  198. exclude_if_present: .nobackup
  199. '''
  200. )
  201. config, logs = module.parse_configuration('/tmp/config.yaml', '/tmp/schema.yaml')
  202. assert config == {
  203. 'source_directories': ['/home'],
  204. 'repositories': [{'path': 'hostname.borg'}],
  205. 'exclude_if_present': ['.nobackup'],
  206. }
  207. assert logs