From 34714d5f6b691d03269100db1914a85ca6756f2e Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone Date: Wed, 18 Nov 2020 12:42:31 -0500 Subject: [PATCH] Add everything and nothing config validation helpers --- src/allmydata/test/test_configutil.py | 101 ++++++++++++++++++++++++++ src/allmydata/util/configutil.py | 26 ++++++- 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/test_configutil.py b/src/allmydata/test/test_configutil.py index 19e5b303d..e6035c512 100644 --- a/src/allmydata/test/test_configutil.py +++ b/src/allmydata/test/test_configutil.py @@ -14,6 +14,17 @@ if PY2: from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, list, object, range, str, max, min # noqa: F401 import os.path +from configparser import ( + ConfigParser, +) + +from hypothesis import ( + given, +) +from hypothesis.strategies import ( + dictionaries, + text, +) from twisted.python.filepath import ( FilePath, @@ -23,6 +34,51 @@ from twisted.trial import unittest from allmydata.util import configutil +def arbitrary_config_dicts( + min_sections=0, + max_sections=3, + max_items_per_section=3, + max_item_length=8, + max_value_length=8, +): + """ + Build ``dict[str, dict[str, str]]`` instances populated with arbitrary + configurations. + """ + return dictionaries( + text(), + dictionaries( + text(max_size=max_item_length), + text(max_size=max_value_length), + max_size=max_items_per_section, + ), + min_size=min_sections, + max_size=max_sections, + ) + + +def to_configparser(dictconfig): + """ + Take a ``dict[str, dict[str, str]]`` and turn it into the corresponding + populated ``ConfigParser`` instance. + """ + cp = ConfigParser() + for section, items in dictconfig.items(): + cp.add_section(section) + for k, v in items.items(): + cp.set( + section, + k, + # ConfigParser has a feature that everyone knows and loves + # where it will use %-style interpolation to substitute + # values from one part of the config into another part of + # the config. Escape all our `%`s to avoid hitting this + # and complicating things. + v.replace("%", "%%"), + ) + return cp + + class ConfigUtilTests(unittest.TestCase): def setUp(self): super(ConfigUtilTests, self).setUp() @@ -166,3 +222,48 @@ enabled = false config = configutil.get_config(fname) self.assertEqual(config.get("node", "a"), "foo") self.assertEqual(config.get("node", "b"), "bar") + + @given(arbitrary_config_dicts()) + def test_everything_valid(self, cfgdict): + """ + ``validate_config`` returns ``None`` when the validator is + ``ValidConfiguration.everything()``. + """ + cfg = to_configparser(cfgdict) + self.assertIs( + configutil.validate_config( + "", + cfg, + configutil.ValidConfiguration.everything(), + ), + None, + ) + + @given(arbitrary_config_dicts(min_sections=1)) + def test_nothing_valid(self, cfgdict): + """ + ``validate_config`` raises ``UnknownConfigError`` when the validator is + ``ValidConfiguration.nothing()`` for all non-empty configurations. + """ + cfg = to_configparser(cfgdict) + with self.assertRaises(configutil.UnknownConfigError): + configutil.validate_config( + "", + cfg, + configutil.ValidConfiguration.nothing(), + ) + + def test_nothing_empty_valid(self): + """ + ``validate_config`` returns ``None`` when the validator is + ``ValidConfiguration.nothing()`` if the configuration is empty. + """ + cfg = ConfigParser() + self.assertIs( + configutil.validate_config( + "", + cfg, + configutil.ValidConfiguration.nothing(), + ), + None, + ) diff --git a/src/allmydata/util/configutil.py b/src/allmydata/util/configutil.py index d905a8e00..c85f58af3 100644 --- a/src/allmydata/util/configutil.py +++ b/src/allmydata/util/configutil.py @@ -115,10 +115,34 @@ class ValidConfiguration(object): an item name as bytes and returns True if that section, item pair is valid, False otherwise. """ - _static_valid_sections = attr.ib() + _static_valid_sections = attr.ib( + validator=attr.validators.instance_of(dict) + ) _is_valid_section = attr.ib(default=lambda section_name: False) _is_valid_item = attr.ib(default=lambda section_name, item_name: False) + @classmethod + def everything(cls): + """ + Create a validator which considers everything valid. + """ + return cls( + {}, + lambda section_name: True, + lambda section_name, item_name: True, + ) + + @classmethod + def nothing(cls): + """ + Create a validator which considers nothing valid. + """ + return cls( + {}, + lambda section_name: False, + lambda section_name, item_name: False, + ) + def is_valid_section(self, section_name): """ :return: True if the given section name is valid, False otherwise.