oslo.config/oslo_config/tests/test_generator.py
Sean McGinnis 66b3da9689
Use unittest.mock instead of third party mock
Now that we no longer support py27, we can use the standard library
unittest.mock module instead of the third party mock lib.

Change-Id: I2f8067eeb2aec62f098bfbb3770a67f1876ce7a2
Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
2020-03-31 14:40:09 -05:00

1877 lines
62 KiB
Python

# Copyright 2014 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import io
import sys
import textwrap
from unittest import mock
import fixtures
from oslotest import base
import tempfile
import testscenarios
from oslo_config import cfg
from oslo_config import fixture as config_fixture
from oslo_config import generator
from oslo_config import types
import yaml
load_tests = testscenarios.load_tests_apply_scenarios
def custom_type(a):
"""Something that acts like a type, but isn't known"""
return a
def build_formatter(output_file, **kwargs):
conf = cfg.ConfigOpts()
conf.register_opts(generator._generator_opts)
for k, v in kwargs.items():
conf.set_override(k, v)
return generator._OptFormatter(conf, output_file=output_file)
class GeneratorTestCase(base.BaseTestCase):
groups = {
'group1': cfg.OptGroup(name='group1',
help='Lorem ipsum dolor sit amet, consectetur '
'adipisicing elit, sed do eiusmod tempor '
'incididunt ut labore et dolore magna '
'aliqua. Ut enim ad minim veniam, quis '
'nostrud exercitation ullamco laboris '
'nisi ut aliquip ex ea commodo '
'consequat. Duis aute irure dolor in.'),
'group2': cfg.OptGroup(name='group2', title='Group 2'),
'foo': cfg.OptGroup(name='foo', title='Foo Title', help='foo help'),
}
opts = {
'foo': cfg.StrOpt('foo', help='foo option'),
'bar': cfg.StrOpt('bar', help='bar option'),
'foo-bar': cfg.StrOpt('foo-bar', help='foobar'),
'no_help': cfg.StrOpt('no_help'),
'long_help': cfg.StrOpt('long_help',
help='Lorem ipsum dolor sit amet, consectetur '
'adipisicing elit, sed do eiusmod tempor '
'incididunt ut labore et dolore magna '
'aliqua. Ut enim ad minim veniam, quis '
'nostrud exercitation ullamco laboris '
'nisi ut aliquip ex ea commodo '
'consequat. Duis aute irure dolor in '
'reprehenderit in voluptate velit esse '
'cillum dolore eu fugiat nulla '
'pariatur. Excepteur sint occaecat '
'cupidatat non proident, sunt in culpa '
'qui officia deserunt mollit anim id est '
'laborum.'),
'long_help_pre': cfg.StrOpt('long_help_pre',
help='This is a very long help text which '
'is preformatted with line breaks. '
'It should break when it is too long '
'but also keep the specified line '
'breaks. This makes it possible to '
'create lists with items:\n'
'\n'
'* item 1\n'
'* item 2\n'
'\n'
'and should increase the '
'readability.'),
'choices_opt': cfg.StrOpt('choices_opt',
default='a',
choices=(None, '', 'a', 'b', 'c'),
help='a string with choices'),
'deprecated_opt_without_deprecated_group': cfg.StrOpt(
'bar', deprecated_name='foobar', help='deprecated'),
'deprecated_for_removal_opt': cfg.StrOpt(
'bar', deprecated_for_removal=True, help='deprecated for removal'),
'deprecated_reason_opt': cfg.BoolOpt(
'turn_off_stove',
default=False,
deprecated_for_removal=True,
deprecated_reason='This was supposed to work but it really, '
'really did not. Always buy house insurance.',
help='DEPRECATED: Turn off stove'),
'deprecated_opt_with_deprecated_since': cfg.BoolOpt(
'tune_in',
deprecated_for_removal=True,
deprecated_since='13.0'),
'deprecated_opt_with_deprecated_group': cfg.StrOpt(
'bar', deprecated_name='foobar', deprecated_group='group1',
help='deprecated'),
'opt_with_DeprecatedOpt': cfg.BoolOpt(
'foo-bar',
help='Opt with DeprecatedOpt',
deprecated_opts=[cfg.DeprecatedOpt('foo-bar',
group='deprecated')]),
# Unknown Opt default must be a string
'unknown_type': cfg.Opt('unknown_opt',
default='123',
help='unknown',
type=types.String(type_name='unknown type')),
'str_opt': cfg.StrOpt('str_opt',
default='foo bar',
help='a string'),
'str_opt_sample_default': cfg.StrOpt('str_opt',
default='fooishbar',
help='a string'),
'str_opt_with_space': cfg.StrOpt('str_opt',
default=' foo bar ',
help='a string with spaces'),
'str_opt_multiline': cfg.StrOpt('str_opt',
default='foo\nbar\nbaz',
help='a string with newlines'),
'bool_opt': cfg.BoolOpt('bool_opt',
default=False,
help='a boolean'),
'int_opt': cfg.IntOpt('int_opt',
default=10,
min=1,
max=20,
help='an integer'),
'int_opt_min_0': cfg.IntOpt('int_opt_min_0',
default=10,
min=0,
max=20,
help='an integer'),
'int_opt_max_0': cfg.IntOpt('int_opt_max_0',
default=-1,
max=0,
help='an integer'),
'float_opt': cfg.FloatOpt('float_opt',
default=0.1,
help='a float'),
'list_opt': cfg.ListOpt('list_opt',
default=['1', '2', '3'],
help='a list'),
'list_opt_with_bounds': cfg.ListOpt('list_opt_with_bounds',
default=['1', '2', '3'],
help='a list',
bounds=True),
'list_opt_single': cfg.ListOpt('list_opt_single',
default='1',
help='a list'),
'list_int_opt': cfg.ListOpt('list_int_opt',
default=[1, 2, 3],
help='a list'),
'dict_opt': cfg.DictOpt('dict_opt',
default={'1': 'yes', '2': 'no'},
help='a dict'),
'ip_opt': cfg.IPOpt('ip_opt',
default='127.0.0.1',
help='an ip address'),
'port_opt': cfg.PortOpt('port_opt',
default=80,
help='a port'),
'hostname_opt': cfg.HostnameOpt('hostname_opt',
default='compute01.nova.site1',
help='a hostname'),
'uri_opt': cfg.URIOpt('uri_opt',
default='http://example.com',
help='a URI'),
'multi_opt': cfg.MultiStrOpt('multi_opt',
default=['1', '2', '3'],
help='multiple strings'),
'multi_opt_none': cfg.MultiStrOpt('multi_opt_none',
help='multiple strings'),
'multi_opt_empty': cfg.MultiStrOpt('multi_opt_empty',
default=[],
help='multiple strings'),
'multi_opt_sample_default': cfg.MultiStrOpt('multi_opt',
default=['1', '2', '3'],
sample_default=['5', '6'],
help='multiple strings'),
'string_type_with_bad_default': cfg.Opt('string_type_with_bad_default',
help='string with bad default',
default=4096),
'native_str_type': cfg.Opt('native_str_type',
help='native help',
type=str),
'native_int_type': cfg.Opt('native_int_type',
help='native help',
type=int),
'native_float_type': cfg.Opt('native_float_type',
help='native help',
type=float),
'custom_type': cfg.Opt('custom_type',
help='custom help',
type=custom_type),
'custom_type_name': cfg.Opt('custom_opt_type',
type=types.Integer(type_name='port'
' number'),
default=5511,
help='this is a port'),
}
content_scenarios = [
('empty',
dict(opts=[], expected='''[DEFAULT]
''')),
('single_namespace',
dict(opts=[('test', [(None, [opts['foo']])])],
expected='''[DEFAULT]
#
# From test
#
# foo option (string value)
#foo = <None>
''')),
('multiple_namespaces',
dict(opts=[('test', [(None, [opts['foo']])]),
('other', [(None, [opts['bar']])])],
expected='''[DEFAULT]
#
# From other
#
# bar option (string value)
#bar = <None>
#
# From test
#
# foo option (string value)
#foo = <None>
''')),
('group',
dict(opts=[('test', [(groups['group1'], [opts['foo']])])],
expected='''[DEFAULT]
[group1]
# Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
# eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
# ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
# aliquip ex ea commodo consequat. Duis aute irure dolor in.
#
# From test
#
# foo option (string value)
#foo = <None>
''')),
('empty_group',
dict(opts=[('test', [(groups['group1'], [])])],
expected='''[DEFAULT]
''')),
('multiple_groups',
dict(opts=[('test', [(groups['group1'], [opts['foo']]),
(groups['group2'], [opts['bar']])])],
expected='''[DEFAULT]
[group1]
# Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
# eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
# ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
# aliquip ex ea commodo consequat. Duis aute irure dolor in.
#
# From test
#
# foo option (string value)
#foo = <None>
[group2]
#
# From test
#
# bar option (string value)
#bar = <None>
''')),
('group_in_multiple_namespaces',
dict(opts=[('test', [(groups['group1'], [opts['foo']])]),
('other', [(groups['group1'], [opts['bar']])])],
expected='''[DEFAULT]
[group1]
# Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
# eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
# ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
# aliquip ex ea commodo consequat. Duis aute irure dolor in.
#
# From other
#
# bar option (string value)
#bar = <None>
#
# From test
#
# foo option (string value)
#foo = <None>
''')),
('hyphenated_name',
dict(opts=[('test', [(None, [opts['foo-bar']])])],
expected='''[DEFAULT]
#
# From test
#
# foobar (string value)
#foo_bar = <None>
''')),
('no_help',
dict(opts=[('test', [(None, [opts['no_help']])])],
log_warning=('"%s" is missing a help string', 'no_help'),
expected='''[DEFAULT]
#
# From test
#
# (string value)
#no_help = <None>
''')),
('long_help',
dict(opts=[('test', [(None, [opts['long_help']])])],
expected='''[DEFAULT]
#
# From test
#
# Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do
# eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
# ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
# aliquip ex ea commodo consequat. Duis aute irure dolor in
# reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
# pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
# culpa qui officia deserunt mollit anim id est laborum. (string
# value)
#long_help = <None>
''')),
('long_help_wrap_at_40',
dict(opts=[('test', [(None, [opts['long_help']])])],
wrap_width=40,
expected='''[DEFAULT]
#
# From test
#
# Lorem ipsum dolor sit amet,
# consectetur adipisicing elit, sed do
# eiusmod tempor incididunt ut labore et
# dolore magna aliqua. Ut enim ad minim
# veniam, quis nostrud exercitation
# ullamco laboris nisi ut aliquip ex ea
# commodo consequat. Duis aute irure
# dolor in reprehenderit in voluptate
# velit esse cillum dolore eu fugiat
# nulla pariatur. Excepteur sint
# occaecat cupidatat non proident, sunt
# in culpa qui officia deserunt mollit
# anim id est laborum. (string value)
#long_help = <None>
''')),
('long_help_no_wrapping',
dict(opts=[('test', [(None, [opts['long_help']])])],
wrap_width=0,
expected='''[DEFAULT]
#
# From test
#
''' # noqa
'# Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod '
'tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, '
'quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo '
'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse '
'cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat '
'non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. '
'(string value)'
'''
#long_help = <None>
''')),
('long_help_with_preformatting',
dict(opts=[('test', [(None, [opts['long_help_pre']])])],
wrap_width=70,
expected='''[DEFAULT]
#
# From test
#
# This is a very long help text which is preformatted with line
# breaks. It should break when it is too long but also keep the
# specified line breaks. This makes it possible to create lists with
# items:
#
# * item 1
# * item 2
#
# and should increase the readability. (string value)
#long_help_pre = <None>
''')),
('choices_opt',
dict(opts=[('test', [(None, [opts['choices_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a string with choices (string value)
# Possible values:
# <None> - <No description provided>
# '' - <No description provided>
# a - <No description provided>
# b - <No description provided>
# c - <No description provided>
#choices_opt = a
''')),
('deprecated opt without deprecated group',
dict(opts=[('test',
[(groups['foo'],
[opts['deprecated_opt_without_deprecated_group']])])],
expected='''[DEFAULT]
[foo]
# foo help
#
# From test
#
# deprecated (string value)
# Deprecated group/name - [foo]/foobar
#bar = <None>
''')),
('deprecated_for_removal',
dict(opts=[('test', [(groups['foo'],
[opts['deprecated_for_removal_opt']])])],
expected='''[DEFAULT]
[foo]
# foo help
#
# From test
#
# DEPRECATED: deprecated for removal (string value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
#bar = <None>
''')),
('deprecated_reason',
dict(opts=[('test', [(groups['foo'],
[opts['deprecated_reason_opt']])])],
expected='''[DEFAULT]
[foo]
# foo help
#
# From test
#
# DEPRECATED: Turn off stove (boolean value)
# This option is deprecated for removal.
# Its value may be silently ignored in the future.
# Reason: This was supposed to work but it really, really did not.
# Always buy house insurance.
#turn_off_stove = false
''')),
('deprecated_opt_with_deprecated_group',
dict(opts=[('test',
[(groups['foo'],
[opts['deprecated_opt_with_deprecated_group']])])],
expected='''[DEFAULT]
[foo]
# foo help
#
# From test
#
# deprecated (string value)
# Deprecated group/name - [group1]/foobar
#bar = <None>
''')),
('unknown_type',
dict(opts=[('test', [(None, [opts['unknown_type']])])],
expected='''[DEFAULT]
#
# From test
#
# unknown (unknown type)
#unknown_opt = 123
''')),
('str_opt',
dict(opts=[('test', [(None, [opts['str_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a string (string value)
#str_opt = foo bar
''')),
('str_opt_with_space',
dict(opts=[('test', [(None, [opts['str_opt_with_space']])])],
expected='''[DEFAULT]
#
# From test
#
# a string with spaces (string value)
#str_opt = " foo bar "
''')),
('str_opt_multiline',
dict(opts=[('test', [(None, [opts['str_opt_multiline']])])],
expected='''[DEFAULT]
#
# From test
#
# a string with newlines (string value)
#str_opt = foo
# bar
# baz
''')),
('bool_opt',
dict(opts=[('test', [(None, [opts['bool_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a boolean (boolean value)
#bool_opt = false
''')),
('int_opt',
dict(opts=[('test', [(None, [opts['int_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# an integer (integer value)
# Minimum value: 1
# Maximum value: 20
#int_opt = 10
''')),
('int_opt_min_0',
dict(opts=[('test', [(None, [opts['int_opt_min_0']])])],
expected='''[DEFAULT]
#
# From test
#
# an integer (integer value)
# Minimum value: 0
# Maximum value: 20
#int_opt_min_0 = 10
''')),
('int_opt_max_0',
dict(opts=[('test', [(None, [opts['int_opt_max_0']])])],
expected='''[DEFAULT]
#
# From test
#
# an integer (integer value)
# Maximum value: 0
#int_opt_max_0 = -1
''')),
('float_opt',
dict(opts=[('test', [(None, [opts['float_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a float (floating point value)
#float_opt = 0.1
''')),
('list_opt',
dict(opts=[('test', [(None, [opts['list_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a list (list value)
#list_opt = 1,2,3
''')),
('list_opt_with_bounds',
dict(opts=[('test', [(None, [opts['list_opt_with_bounds']])])],
expected='''[DEFAULT]
#
# From test
#
# a list (list value)
#list_opt_with_bounds = [1,2,3]
''')),
('list_opt_single',
dict(opts=[('test', [(None, [opts['list_opt_single']])])],
expected='''[DEFAULT]
#
# From test
#
# a list (list value)
#list_opt_single = 1
''')),
('list_int_opt',
dict(opts=[('test', [(None, [opts['list_int_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a list (list value)
#list_int_opt = 1,2,3
''')),
('dict_opt',
dict(opts=[('test', [(None, [opts['dict_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a dict (dict value)
#dict_opt = 1:yes,2:no
''')),
('ip_opt',
dict(opts=[('test', [(None, [opts['ip_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# an ip address (IP address value)
#ip_opt = 127.0.0.1
''')),
('port_opt',
dict(opts=[('test', [(None, [opts['port_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a port (port value)
# Minimum value: 0
# Maximum value: 65535
#port_opt = 80
''')),
('hostname_opt',
dict(opts=[('test', [(None, [opts['hostname_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# a hostname (hostname value)
#hostname_opt = compute01.nova.site1
''')),
('multi_opt',
dict(opts=[('test', [(None, [opts['multi_opt']])])],
expected='''[DEFAULT]
#
# From test
#
# multiple strings (multi valued)
#multi_opt = 1
#multi_opt = 2
#multi_opt = 3
''')),
('multi_opt_none',
dict(opts=[('test', [(None, [opts['multi_opt_none']])])],
expected='''[DEFAULT]
#
# From test
#
# multiple strings (multi valued)
#multi_opt_none =
''')),
('multi_opt_empty',
dict(opts=[('test', [(None, [opts['multi_opt_empty']])])],
expected='''[DEFAULT]
#
# From test
#
# multiple strings (multi valued)
#multi_opt_empty =
''')),
('str_opt_sample_default',
dict(opts=[('test', [(None, [opts['str_opt_sample_default']])])],
expected='''[DEFAULT]
#
# From test
#
# a string (string value)
#str_opt = fooishbar
''')),
('native_str_type',
dict(opts=[('test', [(None, [opts['native_str_type']])])],
expected='''[DEFAULT]
#
# From test
#
# native help (string value)
#native_str_type = <None>
''')),
('native_int_type',
dict(opts=[('test', [(None, [opts['native_int_type']])])],
expected='''[DEFAULT]
#
# From test
#
# native help (integer value)
#native_int_type = <None>
''')),
('native_float_type',
dict(opts=[('test', [(None, [opts['native_float_type']])])],
expected='''[DEFAULT]
#
# From test
#
# native help (floating point value)
#native_float_type = <None>
''')),
('multi_opt_sample_default',
dict(opts=[('test', [(None, [opts['multi_opt_sample_default']])])],
expected='''[DEFAULT]
#
# From test
#
# multiple strings (multi valued)
#
# This option has a sample default set, which means that
# its actual default value may vary from the one documented
# below.
#multi_opt = 5
#multi_opt = 6
''')),
('custom_type_name',
dict(opts=[('test', [(None, [opts['custom_type_name']])])],
expected='''[DEFAULT]
#
# From test
#
# this is a port (port number)
#custom_opt_type = 5511
''')),
('custom_type',
dict(opts=[('test', [(None, [opts['custom_type']])])],
expected='''[DEFAULT]
#
# From test
#
# custom help (unknown value)
#custom_type = <None>
''')),
('string_type_with_bad_default',
dict(opts=[('test', [(None,
[opts['string_type_with_bad_default']])])],
expected='''[DEFAULT]
#
# From test
#
# string with bad default (string value)
#string_type_with_bad_default = 4096
''')),
('str_opt_str_group',
dict(opts=[('test', [('foo',
[opts['str_opt']]),
(groups['foo'],
[opts['int_opt']])]),
('foo', [('foo',
[opts['bool_opt']])])],
expected='''[DEFAULT]
[foo]
# foo help
#
# From foo
#
# a boolean (boolean value)
#bool_opt = false
#
# From test
#
# a string (string value)
#str_opt = foo bar
#
# From test
#
# an integer (integer value)
# Minimum value: 1
# Maximum value: 20
#int_opt = 10
''')),
('opt_str_opt_group',
dict(opts=[('test', [(groups['foo'],
[opts['int_opt']]),
('foo',
[opts['str_opt']])]),
('foo', [(groups['foo'],
[opts['bool_opt']])])],
expected='''[DEFAULT]
[foo]
# foo help
#
# From foo
#
# a boolean (boolean value)
#bool_opt = false
#
# From test
#
# an integer (integer value)
# Minimum value: 1
# Maximum value: 20
#int_opt = 10
#
# From test
#
# a string (string value)
#str_opt = foo bar
''')),
('opt_with_DeprecatedOpt',
dict(opts=[('test', [(None, [opts['opt_with_DeprecatedOpt']])])],
expected='''[DEFAULT]
#
# From test
#
# Opt with DeprecatedOpt (boolean value)
# Deprecated group/name - [deprecated]/foo_bar
#foo_bar = <None>
''')),
]
output_file_scenarios = [
('stdout',
dict(stdout=True, output_file=None)),
('output_file',
dict(output_file='sample.conf', stdout=False)),
]
@classmethod
def generate_scenarios(cls):
cls.scenarios = testscenarios.multiply_scenarios(
cls.content_scenarios,
cls.output_file_scenarios)
def setUp(self):
super(GeneratorTestCase, self).setUp()
self.conf = cfg.ConfigOpts()
self.config_fixture = config_fixture.Config(self.conf)
self.config = self.config_fixture.config
self.useFixture(self.config_fixture)
self.tempdir = self.useFixture(fixtures.TempDir())
def _capture_stream(self, stream_name):
self.useFixture(fixtures.MonkeyPatch("sys.%s" % stream_name,
io.StringIO()))
return getattr(sys, stream_name)
def _capture_stdout(self):
return self._capture_stream('stdout')
@mock.patch.object(generator, '_get_raw_opts_loaders')
@mock.patch.object(generator, 'LOG')
def test_generate(self, mock_log, raw_opts_loader):
generator.register_cli_opts(self.conf)
namespaces = [i[0] for i in self.opts]
self.config(namespace=namespaces)
for group in self.groups.values():
self.conf.register_group(group)
wrap_width = getattr(self, 'wrap_width', None)
if wrap_width is not None:
self.config(wrap_width=wrap_width)
if self.stdout:
stdout = self._capture_stdout()
else:
output_file = self.tempdir.join(self.output_file)
self.config(output_file=output_file)
# We have a static data structure matching what should be
# returned by _list_opts() but we're mocking out a lower level
# function that needs to return a namespace and a callable to
# return options from that namespace. We have to pass opts to
# the lambda to cache a reference to the name because the list
# comprehension changes the thing pointed to by the name each
# time through the loop.
raw_opts_loader.return_value = [
(ns, lambda opts=opts: opts)
for ns, opts in self.opts
]
generator.generate(self.conf)
if self.stdout:
self.assertEqual(self.expected, stdout.getvalue())
else:
with open(output_file, 'r') as f:
actual = f.read()
self.assertEqual(self.expected, actual)
log_warning = getattr(self, 'log_warning', None)
if log_warning is not None:
mock_log.warning.assert_called_once_with(*log_warning)
else:
self.assertFalse(mock_log.warning.called)
class GeneratorFileHandlingTestCase(base.BaseTestCase):
def setUp(self):
super(GeneratorFileHandlingTestCase, self).setUp()
self.conf = cfg.ConfigOpts()
self.config_fixture = config_fixture.Config(self.conf)
self.config = self.config_fixture.config
@mock.patch.object(generator, '_get_groups')
@mock.patch.object(generator, '_list_opts')
def test_close_generated_file(self, a, b):
generator.register_cli_opts(self.conf)
self.config(output_file='somefile')
m = mock.mock_open()
m.close = mock.Mock()
with mock.patch.object(generator, 'open', m, create=True):
generator.generate(self.conf, output_file=None)
m().close.assert_called_once()
@mock.patch.object(generator, '_get_groups')
@mock.patch.object(generator, '_list_opts')
def test_not_close_external_file(self, a, b):
generator.register_cli_opts(self.conf)
self.config()
m = mock.Mock()
generator.generate(self.conf, output_file=m)
m().close.assert_not_called()
class DriverOptionTestCase(base.BaseTestCase):
def setUp(self):
super(DriverOptionTestCase, self).setUp()
self.conf = cfg.ConfigOpts()
self.config_fixture = config_fixture.Config(self.conf)
self.config = self.config_fixture.config
self.useFixture(self.config_fixture)
@mock.patch.object(generator, '_get_driver_opts_loaders')
@mock.patch.object(generator, '_get_raw_opts_loaders')
@mock.patch.object(generator, 'LOG')
def test_driver_option(self, mock_log, raw_opts_loader,
driver_opts_loader):
group = cfg.OptGroup(
name='test_group',
title='Test Group',
driver_option='foo',
)
regular_opts = [
cfg.MultiStrOpt('foo', help='foo option'),
cfg.StrOpt('bar', help='bar option'),
]
driver_opts = {
'd1': [
cfg.StrOpt('d1-foo', help='foo option'),
],
'd2': [
cfg.StrOpt('d2-foo', help='foo option'),
],
}
# We have a static data structure matching what should be
# returned by _list_opts() but we're mocking out a lower level
# function that needs to return a namespace and a callable to
# return options from that namespace. We have to pass opts to
# the lambda to cache a reference to the name because the list
# comprehension changes the thing pointed to by the name each
# time through the loop.
raw_opts_loader.return_value = [
('testing', lambda: [(group, regular_opts)]),
]
driver_opts_loader.return_value = [
('testing', lambda: driver_opts),
]
# Initialize the generator to produce YAML output to a buffer.
generator.register_cli_opts(self.conf)
self.config(namespace=['test_generator'], format_='yaml')
stdout = io.StringIO()
# Generate the output and parse it back to a data structure.
generator.generate(self.conf, output_file=stdout)
body = stdout.getvalue()
actual = yaml.safe_load(body)
test_section = actual['options']['test_group']
self.assertEqual('foo', test_section['driver_option'])
found_option_names = [
o['name']
for o in test_section['opts']
]
self.assertEqual(
['foo', 'bar', 'd1-foo', 'd2-foo'],
found_option_names
)
self.assertEqual(
{'d1': ['d1-foo'], 'd2': ['d2-foo']},
test_section['driver_opts'],
)
GENERATOR_OPTS = {'format_': 'yaml',
'minimal': False,
'namespace': ['test'],
'output_file': None,
'summarize': False,
'wrap_width': 70,
'config_source': []}
class MachineReadableGeneratorTestCase(base.BaseTestCase):
all_opts = GeneratorTestCase.opts
all_groups = GeneratorTestCase.groups
content_scenarios = [
('single_namespace',
dict(opts=[('test', [(None, [all_opts['foo']])])],
expected={'deprecated_options': {},
'generator_options': GENERATOR_OPTS,
'options': {
'DEFAULT': {
'driver_option': '',
'driver_opts': {},
'dynamic_group_owner': '',
'help': '',
'standard_opts': ['foo'],
'opts': [{'advanced': False,
'choices': [],
'default': None,
'deprecated_for_removal': False,
'deprecated_opts': [],
'deprecated_reason': None,
'deprecated_since': None,
'dest': 'foo',
'help': 'foo option',
'max': None,
'metavar': None,
'min': None,
'mutable': False,
'name': 'foo',
'namespace': 'test',
'positional': False,
'required': False,
'sample_default': None,
'secret': False,
'short': None,
'type': 'string value'}]}}})),
('long_help',
dict(opts=[('test', [(None, [all_opts['long_help']])])],
expected={'deprecated_options': {},
'generator_options': GENERATOR_OPTS,
'options': {
'DEFAULT': {
'driver_option': '',
'driver_opts': {},
'dynamic_group_owner': '',
'help': '',
'standard_opts': ['long_help'],
'opts': [{'advanced': False,
'choices': [],
'default': None,
'deprecated_for_removal': False,
'deprecated_opts': [],
'deprecated_reason': None,
'deprecated_since': None,
'dest': 'long_help',
'help': all_opts['long_help'].help,
'max': None,
'metavar': None,
'min': None,
'mutable': False,
'name': 'long_help',
'namespace': 'test',
'positional': False,
'required': False,
'sample_default': None,
'secret': False,
'short': None,
'type': 'string value'}]}}})),
('long_help_pre',
dict(opts=[('test', [(None, [all_opts['long_help_pre']])])],
expected={'deprecated_options': {},
'generator_options': GENERATOR_OPTS,
'options': {
'DEFAULT': {
'driver_option': '',
'driver_opts': {},
'dynamic_group_owner': '',
'help': '',
'standard_opts': ['long_help_pre'],
'opts': [{'advanced': False,
'choices': [],
'default': None,
'deprecated_for_removal': False,
'deprecated_opts': [],
'deprecated_reason': None,
'deprecated_since': None,
'dest': 'long_help_pre',
'help':
all_opts['long_help_pre'].help,
'max': None,
'metavar': None,
'min': None,
'mutable': False,
'name': 'long_help_pre',
'namespace': 'test',
'positional': False,
'required': False,
'sample_default': None,
'secret': False,
'short': None,
'type': 'string value'}]}}})),
('opt_with_DeprecatedOpt',
dict(opts=[('test', [(None, [all_opts['opt_with_DeprecatedOpt']])])],
expected={
'deprecated_options': {
'deprecated': [{'name': 'foo_bar',
'replacement_group': 'DEFAULT',
'replacement_name': 'foo-bar'}]},
'generator_options': GENERATOR_OPTS,
'options': {
'DEFAULT': {
'driver_option': '',
'driver_opts': {},
'dynamic_group_owner': '',
'help': '',
'standard_opts': ['foo-bar'],
'opts': [{
'advanced': False,
'choices': [],
'default': None,
'deprecated_for_removal': False,
'deprecated_opts': [{'group': 'deprecated',
'name': 'foo_bar'}],
'deprecated_reason': None,
'deprecated_since': None,
'dest': 'foo_bar',
'help':
all_opts['opt_with_DeprecatedOpt'].help,
'max': None,
'metavar': None,
'min': None,
'mutable': False,
'name': 'foo-bar',
'namespace': 'test',
'positional': False,
'required': False,
'sample_default': None,
'secret': False,
'short': None,
'type': 'boolean value'}]}}})),
('choices_opt',
dict(opts=[('test', [(None, [all_opts['choices_opt']])])],
expected={'deprecated_options': {},
'generator_options': GENERATOR_OPTS,
'options': {
'DEFAULT': {
'driver_option': '',
'driver_opts': {},
'dynamic_group_owner': '',
'help': '',
'standard_opts': ['choices_opt'],
'opts': [{'advanced': False,
'choices': [
(None, None),
('', None),
('a', None),
('b', None),
('c', None)
],
'default': 'a',
'deprecated_for_removal': False,
'deprecated_opts': [],
'deprecated_reason': None,
'deprecated_since': None,
'dest': 'choices_opt',
'help': all_opts['choices_opt'].help,
'max': None,
'metavar': None,
'min': None,
'mutable': False,
'name': 'choices_opt',
'namespace': 'test',
'positional': False,
'required': False,
'sample_default': None,
'secret': False,
'short': None,
'type': 'string value'}]}}})),
('int_opt',
dict(opts=[('test', [(None, [all_opts['int_opt']])])],
expected={'deprecated_options': {},
'generator_options': GENERATOR_OPTS,
'options': {
'DEFAULT': {
'driver_option': '',
'driver_opts': {},
'dynamic_group_owner': '',
'help': '',
'standard_opts': ['int_opt'],
'opts': [{'advanced': False,
'choices': [],
'default': 10,
'deprecated_for_removal': False,
'deprecated_opts': [],
'deprecated_reason': None,
'deprecated_since': None,
'dest': 'int_opt',
'help': all_opts['int_opt'].help,
'max': 20,
'metavar': None,
'min': 1,
'mutable': False,
'name': 'int_opt',
'namespace': 'test',
'positional': False,
'required': False,
'sample_default': None,
'secret': False,
'short': None,
'type': 'integer value'}]}}})),
('group_help',
dict(opts=[('test', [(all_groups['group1'], [all_opts['foo']])])],
expected={'deprecated_options': {},
'generator_options': GENERATOR_OPTS,
'options': {
'DEFAULT': {
# 'driver_option': '',
# 'driver_opts': [],
# 'dynamic_group_owner': '',
'help': '',
'standard_opts': [],
'opts': []
},
'group1': {
'driver_option': '',
'driver_opts': {},
'dynamic_group_owner': '',
'help': all_groups['group1'].help,
'standard_opts': ['foo'],
'opts': [{'advanced': False,
'choices': [],
'default': None,
'deprecated_for_removal': False,
'deprecated_opts': [],
'deprecated_reason': None,
'deprecated_since': None,
'dest': 'foo',
'help': all_opts['foo'].help,
'max': None,
'metavar': None,
'min': None,
'mutable': False,
'name': 'foo',
'namespace': 'test',
'positional': False,
'required': False,
'sample_default': None,
'secret': False,
'short': None,
'type': 'string value'}]}}})),
]
def setUp(self):
super(MachineReadableGeneratorTestCase, self).setUp()
self.conf = cfg.ConfigOpts()
self.config_fixture = config_fixture.Config(self.conf)
self.config = self.config_fixture.config
self.useFixture(self.config_fixture)
@classmethod
def generate_scenarios(cls):
cls.scenarios = testscenarios.multiply_scenarios(
cls.content_scenarios)
@mock.patch.object(generator, '_get_raw_opts_loaders')
def test_generate(self, raw_opts_loader):
generator.register_cli_opts(self.conf)
namespaces = [i[0] for i in self.opts]
self.config(namespace=namespaces, format_='yaml')
# We have a static data structure matching what should be
# returned by _list_opts() but we're mocking out a lower level
# function that needs to return a namespace and a callable to
# return options from that namespace. We have to pass opts to
# the lambda to cache a reference to the name because the list
# comprehension changes the thing pointed to by the name each
# time through the loop.
raw_opts_loader.return_value = [
(ns, lambda opts=opts: opts)
for ns, opts in self.opts
]
test_groups = generator._get_groups(
generator._list_opts(self.conf.namespace))
self.assertEqual(self.expected,
generator._generate_machine_readable_data(test_groups,
self.conf))
class IgnoreDoublesTestCase(base.BaseTestCase):
opts = [cfg.StrOpt('foo', help='foo option'),
cfg.StrOpt('bar', help='bar option'),
cfg.StrOpt('foo_bar', help='foobar'),
cfg.StrOpt('str_opt', help='a string'),
cfg.BoolOpt('bool_opt', help='a boolean'),
cfg.IntOpt('int_opt', help='an integer')]
def test_cleanup_opts_default(self):
o = [("namespace1", [
("group1", self.opts)])]
self.assertEqual(o, generator._cleanup_opts(o))
def test_cleanup_opts_dup_opt(self):
o = [("namespace1", [
("group1", self.opts + [self.opts[0]])])]
e = [("namespace1", [
("group1", self.opts)])]
self.assertEqual(e, generator._cleanup_opts(o))
def test_cleanup_opts_dup_groups_opt(self):
o = [("namespace1", [
("group1", self.opts + [self.opts[1]]),
("group2", self.opts),
("group3", self.opts + [self.opts[2]])])]
e = [("namespace1", [
("group1", self.opts),
("group2", self.opts),
("group3", self.opts)])]
self.assertEqual(e, generator._cleanup_opts(o))
def test_cleanup_opts_dup_mixed_case_groups_opt(self):
o = [("namespace1", [
("default", self.opts),
("Default", self.opts + [self.opts[1]]),
("DEFAULT", self.opts + [self.opts[2]]),
("group1", self.opts + [self.opts[1]]),
("Group1", self.opts),
("GROUP1", self.opts + [self.opts[2]])])]
e = [("namespace1", [
("DEFAULT", self.opts),
("group1", self.opts)])]
self.assertEqual(e, generator._cleanup_opts(o))
def test_cleanup_opts_dup_namespace_groups_opts(self):
o = [("namespace1", [
("group1", self.opts + [self.opts[1]]),
("group2", self.opts)]),
("namespace2", [
("group1", self.opts + [self.opts[2]]),
("group2", self.opts)])]
e = [("namespace1", [
("group1", self.opts),
("group2", self.opts)]),
("namespace2", [
("group1", self.opts),
("group2", self.opts)])]
self.assertEqual(e, generator._cleanup_opts(o))
@mock.patch.object(generator, '_get_raw_opts_loaders')
def test_list_ignores_doubles(self, raw_opts_loaders):
config_opts = [
(None, [cfg.StrOpt('foo'), cfg.StrOpt('bar')]),
]
# These are the very same config options, but read twice.
# This is possible if one misconfigures the entry point for the
# sample config generator.
raw_opts_loaders.return_value = [
('namespace', lambda: config_opts),
('namespace', lambda: config_opts),
]
slurped_opts = 0
for _, listing in generator._list_opts(['namespace']):
for _, opts in listing:
slurped_opts += len(opts)
self.assertEqual(2, slurped_opts)
class GeneratorAdditionalTestCase(base.BaseTestCase):
opts = [cfg.StrOpt('foo', help='foo option', default='fred'),
cfg.StrOpt('bar', help='bar option'),
cfg.StrOpt('foo_bar', help='foobar'),
cfg.StrOpt('str_opt', help='a string'),
cfg.BoolOpt('bool_opt', help='a boolean'),
cfg.IntOpt('int_opt', help='an integer')]
def test_get_groups_empty_ns(self):
groups = generator._get_groups([])
self.assertEqual({'DEFAULT': {'object': None, 'namespaces': []}},
groups)
def test_get_groups_single_ns(self):
config = [("namespace1", [
("beta", self.opts),
("alpha", self.opts)])]
groups = generator._get_groups(config)
self.assertEqual(['DEFAULT', 'alpha', 'beta'], sorted(groups))
def test_get_groups_multiple_ns(self):
config = [("namespace1", [
("beta", self.opts),
("alpha", self.opts)]),
("namespace2", [
("gamma", self.opts),
("alpha", self.opts)])]
groups = generator._get_groups(config)
self.assertEqual(['DEFAULT', 'alpha', 'beta', 'gamma'], sorted(groups))
def test_output_opts_empty_default(self):
config = [("namespace1", [
("alpha", [])])]
groups = generator._get_groups(config)
fd, tmp_file = tempfile.mkstemp()
with open(tmp_file, 'w+') as f:
formatter = build_formatter(f)
generator._output_opts(formatter, 'DEFAULT', groups.pop('DEFAULT'))
expected = '''[DEFAULT]
'''
with open(tmp_file, 'r') as f:
actual = f.read()
self.assertEqual(expected, actual)
def test_output_opts_group(self):
config = [("namespace1", [
("alpha", [self.opts[0]])])]
groups = generator._get_groups(config)
fd, tmp_file = tempfile.mkstemp()
with open(tmp_file, 'w+') as f:
formatter = build_formatter(f)
generator._output_opts(formatter, 'alpha', groups.pop('alpha'))
expected = '''[alpha]
#
# From namespace1
#
# foo option (string value)
#foo = fred
'''
with open(tmp_file, 'r') as f:
actual = f.read()
self.assertEqual(expected, actual)
def _test_output_default_list_opt_with_string_value(self, default):
opt = cfg.ListOpt('list_opt', help='a list', default=default)
config = [("namespace1", [
("alpha", [opt])])]
groups = generator._get_groups(config)
fd, tmp_file = tempfile.mkstemp()
f = open(tmp_file, 'w+')
formatter = build_formatter(f)
expected = '''[alpha]
#
# From namespace1
#
# a list (list value)
#list_opt = %(default)s
''' % {'default': default}
generator._output_opts(formatter, 'alpha', groups.pop('alpha'))
f.close()
content = open(tmp_file).read()
self.assertEqual(expected, content)
def test_output_default_list_opt_with_string_value_multiple_entries(self):
self._test_output_default_list_opt_with_string_value('foo,bar')
def test_output_default_list_opt_with_string_value_single_entry(self):
self._test_output_default_list_opt_with_string_value('foo')
class GeneratorMutableOptionTestCase(base.BaseTestCase):
def test_include_message(self):
out = io.StringIO()
opt = cfg.StrOpt('foo', help='foo option', mutable=True)
gen = build_formatter(out)
gen.format(opt, 'group1')
result = out.getvalue()
self.assertIn(
'This option can be changed without restarting.',
result,
)
def test_do_not_include_message(self):
out = io.StringIO()
opt = cfg.StrOpt('foo', help='foo option', mutable=False)
gen = build_formatter(out)
gen.format(opt, 'group1')
result = out.getvalue()
self.assertNotIn(
'This option can be changed without restarting.',
result,
)
class GeneratorRaiseErrorTestCase(base.BaseTestCase):
def test_generator_raises_error(self):
"""Verifies that errors from extension manager are not suppressed."""
class FakeException(Exception):
pass
class FakeEP(object):
def __init__(self):
self.name = 'callback_is_expected'
self.require = self.resolve
self.load = self.resolve
def resolve(self, *args, **kwargs):
raise FakeException()
fake_ep = FakeEP()
self.conf = cfg.ConfigOpts()
self.conf.register_opts(generator._generator_opts)
self.conf.set_default('namespace', [fake_ep.name])
fake_eps = mock.Mock(return_value=[fake_ep])
with mock.patch('pkg_resources.iter_entry_points', fake_eps):
self.assertRaises(FakeException, generator.generate, self.conf)
def test_generator_call_with_no_arguments_raises_system_exit(self):
testargs = ['oslo-config-generator']
with mock.patch('sys.argv', testargs):
self.assertRaises(SystemExit, generator.main, [])
class ChangeDefaultsTestCase(base.BaseTestCase):
@mock.patch.object(generator, '_get_opt_default_updaters')
@mock.patch.object(generator, '_get_raw_opts_loaders')
def test_no_modifiers_registered(self, raw_opts_loaders, get_updaters):
orig_opt = cfg.StrOpt('foo', default='bar')
raw_opts_loaders.return_value = [
('namespace', lambda: [(None, [orig_opt])]),
]
get_updaters.return_value = []
opts = generator._list_opts(['namespace'])
# NOTE(dhellmann): Who designed this data structure?
the_opt = opts[0][1][0][1][0]
self.assertEqual('bar', the_opt.default)
self.assertIs(orig_opt, the_opt)
@mock.patch.object(generator, '_get_opt_default_updaters')
@mock.patch.object(generator, '_get_raw_opts_loaders')
def test_change_default(self, raw_opts_loaders, get_updaters):
orig_opt = cfg.StrOpt('foo', default='bar')
raw_opts_loaders.return_value = [
('namespace', lambda: [(None, [orig_opt])]),
]
def updater():
cfg.set_defaults([orig_opt], foo='blah')
get_updaters.return_value = [updater]
opts = generator._list_opts(['namespace'])
# NOTE(dhellmann): Who designed this data structure?
the_opt = opts[0][1][0][1][0]
self.assertEqual('blah', the_opt.default)
self.assertIs(orig_opt, the_opt)
class RequiredOptionTestCase(base.BaseTestCase):
opts = [cfg.StrOpt('foo', help='foo option', default='fred'),
cfg.StrOpt('bar', help='bar option', required=True),
cfg.StrOpt('foo_bar', help='foobar'),
cfg.StrOpt('bars', help='bars foo', required=True)]
def test_required_option_order_single_ns(self):
config = [("namespace1", [
("alpha", self.opts)])]
groups = generator._get_groups(config)
fd, tmp_file = tempfile.mkstemp()
with open(tmp_file, 'w+') as f:
formatter = build_formatter(f, minimal=True)
generator._output_opts(formatter,
'alpha',
groups.pop('alpha'))
expected = '''[alpha]
#
# From namespace1
#
# bar option (string value)
bar = <None>
# bars foo (string value)
bars = <None>
'''
with open(tmp_file, 'r') as f:
actual = f.read()
self.assertEqual(expected, actual)
class SummarizedOptionsTestCase(base.BaseTestCase):
"""Validate 'summarize' config option.
The 'summarize' switch ensures only summaries of each configuration
option are output.
"""
opts = [
cfg.StrOpt(
'foo',
default='fred',
help="""This is the summary line for a config option.
I can provide a lot more detail here, but I may not want to bloat my
config file. In this scenario, I can use the 'summarize' config option
to ensure only a summary of the option is output to the config file.
However, the Sphinx-generated documentation, hosted online, remains
unchanged.
Hopefully this works.
"""),
cfg.StrOpt(
'bar',
required=True,
help="""This is a less carefully formatted configuration
option, where the author has not broken their description into a brief
summary line and larger description. Watch this person's commit
messages!""")]
def test_summarized_option_order_single_ns(self):
config = [('namespace1', [('alpha', self.opts)])]
groups = generator._get_groups(config)
fd, tmp_file = tempfile.mkstemp()
with open(tmp_file, 'w+') as f:
formatter = build_formatter(f, summarize=True)
generator._output_opts(formatter,
'alpha',
groups.pop('alpha'))
expected = '''[alpha]
#
# From namespace1
#
# This is the summary line for a config option. For more information,
# refer to the documentation. (string value)
#foo = fred
# This is a less carefully formatted configuration
# option, where the author has not broken their description into a
# brief
# summary line and larger description. Watch this person's commit
# messages! (string value)
#bar = <None>
'''
with open(tmp_file, 'r') as f:
actual = f.read()
self.assertEqual(expected, actual)
class AdvancedOptionsTestCase(base.BaseTestCase):
opts = [cfg.StrOpt('foo', help='foo option', default='fred'),
cfg.StrOpt('bar', help='bar option', advanced=True),
cfg.StrOpt('foo_bar', help='foobar'),
cfg.BoolOpt('bars', help='bars foo', default=True, advanced=True)]
def test_advanced_option_order_single_ns(self):
config = [("namespace1", [
("alpha", self.opts)])]
groups = generator._get_groups(config)
fd, tmp_file = tempfile.mkstemp()
with open(tmp_file, 'w+') as f:
formatter = build_formatter(f)
generator._output_opts(formatter, 'alpha', groups.pop('alpha'))
expected = '''[alpha]
#
# From namespace1
#
# foo option (string value)
#foo = fred
# foobar (string value)
#foo_bar = <None>
# bar option (string value)
# Advanced Option: intended for advanced users and not used
# by the majority of users, and might have a significant
# effect on stability and/or performance.
#bar = <None>
# bars foo (boolean value)
# Advanced Option: intended for advanced users and not used
# by the majority of users, and might have a significant
# effect on stability and/or performance.
#bars = true
'''
with open(tmp_file, 'r') as f:
actual = f.read()
self.assertEqual(expected, actual)
class HostAddressTestCase(base.BaseTestCase):
opts = [cfg.HostAddressOpt('foo', help='foo option', default='0.0.0.0')]
def test_host_address(self):
config = [("namespace", [("alpha", self.opts)])]
groups = generator._get_groups(config)
out = io.StringIO()
formatter = build_formatter(out)
generator._output_opts(formatter, 'alpha', groups.pop('alpha'))
result = out.getvalue()
expected = textwrap.dedent('''
[alpha]
#
# From namespace
#
# foo option (host address value)
#foo = 0.0.0.0
''').lstrip()
self.assertEqual(expected, result)
GeneratorTestCase.generate_scenarios()
MachineReadableGeneratorTestCase.generate_scenarios()