2
0

test_validate.py 7.2 KB

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