367
584
'/home/bogus/.cache')
370
class TestIniConfig(tests.TestCase):
587
class TestXDGConfigDir(tests.TestCaseInTempDir):
588
# must be in temp dir because config tests for the existence of the bazaar
589
# subdirectory of $XDG_CONFIG_HOME
592
if sys.platform in ('darwin', 'win32'):
593
raise tests.TestNotApplicable(
594
'XDG config dir not used on this platform')
595
super(TestXDGConfigDir, self).setUp()
596
self.overrideEnv('HOME', self.test_home_dir)
597
# BZR_HOME overrides everything we want to test so unset it.
598
self.overrideEnv('BZR_HOME', None)
600
def test_xdg_config_dir_exists(self):
601
"""When ~/.config/bazaar exists, use it as the config dir."""
602
newdir = osutils.pathjoin(self.test_home_dir, '.config', 'bazaar')
604
self.assertEqual(config.config_dir(), newdir)
606
def test_xdg_config_home(self):
607
"""When XDG_CONFIG_HOME is set, use it."""
608
xdgconfigdir = osutils.pathjoin(self.test_home_dir, 'xdgconfig')
609
self.overrideEnv('XDG_CONFIG_HOME', xdgconfigdir)
610
newdir = osutils.pathjoin(xdgconfigdir, 'bazaar')
612
self.assertEqual(config.config_dir(), newdir)
615
class TestIniConfig(tests.TestCaseInTempDir):
372
617
def make_config_parser(self, s):
373
conf = config.IniBasedConfig(None)
374
parser = conf._get_parser(file=StringIO(s.encode('utf-8')))
618
conf = config.IniBasedConfig.from_string(s)
619
return conf, conf._get_parser()
378
622
class TestIniConfigBuilding(TestIniConfig):
380
624
def test_contructs(self):
381
my_config = config.IniBasedConfig("nothing")
625
my_config = config.IniBasedConfig()
383
627
def test_from_fp(self):
384
config_file = StringIO(sample_config_text.encode('utf-8'))
385
my_config = config.IniBasedConfig(None)
387
isinstance(my_config._get_parser(file=config_file),
388
configobj.ConfigObj))
628
my_config = config.IniBasedConfig.from_string(sample_config_text)
629
self.assertIsInstance(my_config._get_parser(), configobj.ConfigObj)
390
631
def test_cached(self):
632
my_config = config.IniBasedConfig.from_string(sample_config_text)
633
parser = my_config._get_parser()
634
self.assertTrue(my_config._get_parser() is parser)
636
def _dummy_chown(self, path, uid, gid):
637
self.path, self.uid, self.gid = path, uid, gid
639
def test_ini_config_ownership(self):
640
"""Ensure that chown is happening during _write_config_file"""
641
self.requireFeature(features.chown_feature)
642
self.overrideAttr(os, 'chown', self._dummy_chown)
643
self.path = self.uid = self.gid = None
644
conf = config.IniBasedConfig(file_name='./foo.conf')
645
conf._write_config_file()
646
self.assertEquals(self.path, './foo.conf')
647
self.assertTrue(isinstance(self.uid, int))
648
self.assertTrue(isinstance(self.gid, int))
650
def test_get_filename_parameter_is_deprecated_(self):
651
conf = self.callDeprecated([
652
'IniBasedConfig.__init__(get_filename) was deprecated in 2.3.'
653
' Use file_name instead.'],
654
config.IniBasedConfig, lambda: 'ini.conf')
655
self.assertEqual('ini.conf', conf.file_name)
657
def test_get_parser_file_parameter_is_deprecated_(self):
391
658
config_file = StringIO(sample_config_text.encode('utf-8'))
392
my_config = config.IniBasedConfig(None)
393
parser = my_config._get_parser(file=config_file)
394
self.failUnless(my_config._get_parser() is parser)
659
conf = config.IniBasedConfig.from_string(sample_config_text)
660
conf = self.callDeprecated([
661
'IniBasedConfig._get_parser(file=xxx) was deprecated in 2.3.'
662
' Use IniBasedConfig(_content=xxx) instead.'],
663
conf._get_parser, file=config_file)
666
class TestIniConfigSaving(tests.TestCaseInTempDir):
668
def test_cant_save_without_a_file_name(self):
669
conf = config.IniBasedConfig()
670
self.assertRaises(AssertionError, conf._write_config_file)
672
def test_saved_with_content(self):
673
content = 'foo = bar\n'
674
conf = config.IniBasedConfig.from_string(
675
content, file_name='./test.conf', save=True)
676
self.assertFileEqual(content, 'test.conf')
679
class TestIniConfigOptionExpansionDefaultValue(tests.TestCaseInTempDir):
680
"""What is the default value of expand for config options.
682
This is an opt-in beta feature used to evaluate whether or not option
683
references can appear in dangerous place raising exceptions, disapearing
684
(and as such corrupting data) or if it's safe to activate the option by
687
Note that these tests relies on config._expand_default_value being already
688
overwritten in the parent class setUp.
692
super(TestIniConfigOptionExpansionDefaultValue, self).setUp()
696
self.warnings.append(args[0] % args[1:])
697
self.overrideAttr(trace, 'warning', warning)
699
def get_config(self, expand):
700
c = config.GlobalConfig.from_string('bzr.config.expand=%s' % (expand,),
704
def assertExpandIs(self, expected):
705
actual = config._get_expand_default_value()
706
#self.config.get_user_option_as_bool('bzr.config.expand')
707
self.assertEquals(expected, actual)
709
def test_default_is_None(self):
710
self.assertEquals(None, config._expand_default_value)
712
def test_default_is_False_even_if_None(self):
713
self.config = self.get_config(None)
714
self.assertExpandIs(False)
716
def test_default_is_False_even_if_invalid(self):
717
self.config = self.get_config('<your choice>')
718
self.assertExpandIs(False)
720
# Huh ? My choice is False ? Thanks, always happy to hear that :D
721
# Wait, you've been warned !
722
self.assertLength(1, self.warnings)
724
'Value "<your choice>" is not a boolean for "bzr.config.expand"',
727
def test_default_is_True(self):
728
self.config = self.get_config(True)
729
self.assertExpandIs(True)
731
def test_default_is_False(self):
732
self.config = self.get_config(False)
733
self.assertExpandIs(False)
736
class TestIniConfigOptionExpansion(tests.TestCase):
737
"""Test option expansion from the IniConfig level.
739
What we really want here is to test the Config level, but the class being
740
abstract as far as storing values is concerned, this can't be done
743
# FIXME: This should be rewritten when all configs share a storage
744
# implementation -- vila 2011-02-18
746
def get_config(self, string=None):
749
c = config.IniBasedConfig.from_string(string)
752
def assertExpansion(self, expected, conf, string, env=None):
753
self.assertEquals(expected, conf.expand_options(string, env))
755
def test_no_expansion(self):
756
c = self.get_config('')
757
self.assertExpansion('foo', c, 'foo')
759
def test_env_adding_options(self):
760
c = self.get_config('')
761
self.assertExpansion('bar', c, '{foo}', {'foo': 'bar'})
763
def test_env_overriding_options(self):
764
c = self.get_config('foo=baz')
765
self.assertExpansion('bar', c, '{foo}', {'foo': 'bar'})
767
def test_simple_ref(self):
768
c = self.get_config('foo=xxx')
769
self.assertExpansion('xxx', c, '{foo}')
771
def test_unknown_ref(self):
772
c = self.get_config('')
773
self.assertRaises(errors.ExpandingUnknownOption,
774
c.expand_options, '{foo}')
776
def test_indirect_ref(self):
777
c = self.get_config('''
781
self.assertExpansion('xxx', c, '{bar}')
783
def test_embedded_ref(self):
784
c = self.get_config('''
788
self.assertExpansion('xxx', c, '{{bar}}')
790
def test_simple_loop(self):
791
c = self.get_config('foo={foo}')
792
self.assertRaises(errors.OptionExpansionLoop, c.expand_options, '{foo}')
794
def test_indirect_loop(self):
795
c = self.get_config('''
799
e = self.assertRaises(errors.OptionExpansionLoop,
800
c.expand_options, '{foo}')
801
self.assertEquals('foo->bar->baz', e.refs)
802
self.assertEquals('{foo}', e.string)
805
conf = self.get_config('''
809
list={foo},{bar},{baz}
811
self.assertEquals(['start', 'middle', 'end'],
812
conf.get_user_option('list', expand=True))
814
def test_cascading_list(self):
815
conf = self.get_config('''
821
self.assertEquals(['start', 'middle', 'end'],
822
conf.get_user_option('list', expand=True))
824
def test_pathological_hidden_list(self):
825
conf = self.get_config('''
831
hidden={start}{middle}{end}
833
# Nope, it's either a string or a list, and the list wins as soon as a
834
# ',' appears, so the string concatenation never occur.
835
self.assertEquals(['{foo', '}', '{', 'bar}'],
836
conf.get_user_option('hidden', expand=True))
839
class TestLocationConfigOptionExpansion(tests.TestCaseInTempDir):
841
def get_config(self, location, string=None):
844
# Since we don't save the config we won't strictly require to inherit
845
# from TestCaseInTempDir, but an error occurs so quickly...
846
c = config.LocationConfig.from_string(string, location)
849
def test_dont_cross_unrelated_section(self):
850
c = self.get_config('/another/branch/path','''
855
[/another/branch/path]
858
self.assertRaises(errors.ExpandingUnknownOption,
859
c.get_user_option, 'bar', expand=True)
861
def test_cross_related_sections(self):
862
c = self.get_config('/project/branch/path','''
866
[/project/branch/path]
869
self.assertEquals('quux', c.get_user_option('bar', expand=True))
872
class TestIniBaseConfigOnDisk(tests.TestCaseInTempDir):
874
def test_cannot_reload_without_name(self):
875
conf = config.IniBasedConfig.from_string(sample_config_text)
876
self.assertRaises(AssertionError, conf.reload)
878
def test_reload_see_new_value(self):
879
c1 = config.IniBasedConfig.from_string('editor=vim\n',
880
file_name='./test/conf')
881
c1._write_config_file()
882
c2 = config.IniBasedConfig.from_string('editor=emacs\n',
883
file_name='./test/conf')
884
c2._write_config_file()
885
self.assertEqual('vim', c1.get_user_option('editor'))
886
self.assertEqual('emacs', c2.get_user_option('editor'))
887
# Make sure we get the Right value
889
self.assertEqual('emacs', c1.get_user_option('editor'))
892
class TestLockableConfig(tests.TestCaseInTempDir):
894
scenarios = lockable_config_scenarios()
899
config_section = None
902
super(TestLockableConfig, self).setUp()
903
self._content = '[%s]\none=1\ntwo=2\n' % (self.config_section,)
904
self.config = self.create_config(self._content)
906
def get_existing_config(self):
907
return self.config_class(*self.config_args)
909
def create_config(self, content):
910
kwargs = dict(save=True)
911
c = self.config_class.from_string(content, *self.config_args, **kwargs)
914
def test_simple_read_access(self):
915
self.assertEquals('1', self.config.get_user_option('one'))
917
def test_simple_write_access(self):
918
self.config.set_user_option('one', 'one')
919
self.assertEquals('one', self.config.get_user_option('one'))
921
def test_listen_to_the_last_speaker(self):
923
c2 = self.get_existing_config()
924
c1.set_user_option('one', 'ONE')
925
c2.set_user_option('two', 'TWO')
926
self.assertEquals('ONE', c1.get_user_option('one'))
927
self.assertEquals('TWO', c2.get_user_option('two'))
928
# The second update respect the first one
929
self.assertEquals('ONE', c2.get_user_option('one'))
931
def test_last_speaker_wins(self):
932
# If the same config is not shared, the same variable modified twice
933
# can only see a single result.
935
c2 = self.get_existing_config()
936
c1.set_user_option('one', 'c1')
937
c2.set_user_option('one', 'c2')
938
self.assertEquals('c2', c2._get_user_option('one'))
939
# The first modification is still available until another refresh
941
self.assertEquals('c1', c1._get_user_option('one'))
942
c1.set_user_option('two', 'done')
943
self.assertEquals('c2', c1._get_user_option('one'))
945
def test_writes_are_serialized(self):
947
c2 = self.get_existing_config()
949
# We spawn a thread that will pause *during* the write
950
before_writing = threading.Event()
951
after_writing = threading.Event()
952
writing_done = threading.Event()
953
c1_orig = c1._write_config_file
954
def c1_write_config_file():
957
# The lock is held. We wait for the main thread to decide when to
960
c1._write_config_file = c1_write_config_file
962
c1.set_user_option('one', 'c1')
964
t1 = threading.Thread(target=c1_set_option)
965
# Collect the thread after the test
966
self.addCleanup(t1.join)
967
# Be ready to unblock the thread if the test goes wrong
968
self.addCleanup(after_writing.set)
970
before_writing.wait()
971
self.assertTrue(c1._lock.is_held)
972
self.assertRaises(errors.LockContention,
973
c2.set_user_option, 'one', 'c2')
974
self.assertEquals('c1', c1.get_user_option('one'))
975
# Let the lock be released
978
c2.set_user_option('one', 'c2')
979
self.assertEquals('c2', c2.get_user_option('one'))
981
def test_read_while_writing(self):
983
# We spawn a thread that will pause *during* the write
984
ready_to_write = threading.Event()
985
do_writing = threading.Event()
986
writing_done = threading.Event()
987
c1_orig = c1._write_config_file
988
def c1_write_config_file():
990
# The lock is held. We wait for the main thread to decide when to
995
c1._write_config_file = c1_write_config_file
997
c1.set_user_option('one', 'c1')
998
t1 = threading.Thread(target=c1_set_option)
999
# Collect the thread after the test
1000
self.addCleanup(t1.join)
1001
# Be ready to unblock the thread if the test goes wrong
1002
self.addCleanup(do_writing.set)
1004
# Ensure the thread is ready to write
1005
ready_to_write.wait()
1006
self.assertTrue(c1._lock.is_held)
1007
self.assertEquals('c1', c1.get_user_option('one'))
1008
# If we read during the write, we get the old value
1009
c2 = self.get_existing_config()
1010
self.assertEquals('1', c2.get_user_option('one'))
1011
# Let the writing occur and ensure it occurred
1014
# Now we get the updated value
1015
c3 = self.get_existing_config()
1016
self.assertEquals('c1', c3.get_user_option('one'))
397
1019
class TestGetUserOptionAs(TestIniConfig):
1312
2026
self.assertIs(None, bzrdir_config.get_default_stack_on())
2029
class TestOldConfigHooks(tests.TestCaseWithTransport):
2032
super(TestOldConfigHooks, self).setUp()
2033
create_configs_with_file_option(self)
2035
def assertGetHook(self, conf, name, value):
2039
config.OldConfigHooks.install_named_hook('get', hook, None)
2041
config.OldConfigHooks.uninstall_named_hook, 'get', None)
2042
self.assertLength(0, calls)
2043
actual_value = conf.get_user_option(name)
2044
self.assertEquals(value, actual_value)
2045
self.assertLength(1, calls)
2046
self.assertEquals((conf, name, value), calls[0])
2048
def test_get_hook_bazaar(self):
2049
self.assertGetHook(self.bazaar_config, 'file', 'bazaar')
2051
def test_get_hook_locations(self):
2052
self.assertGetHook(self.locations_config, 'file', 'locations')
2054
def test_get_hook_branch(self):
2055
# Since locations masks branch, we define a different option
2056
self.branch_config.set_user_option('file2', 'branch')
2057
self.assertGetHook(self.branch_config, 'file2', 'branch')
2059
def assertSetHook(self, conf, name, value):
2063
config.OldConfigHooks.install_named_hook('set', hook, None)
2065
config.OldConfigHooks.uninstall_named_hook, 'set', None)
2066
self.assertLength(0, calls)
2067
conf.set_user_option(name, value)
2068
self.assertLength(1, calls)
2069
# We can't assert the conf object below as different configs use
2070
# different means to implement set_user_option and we care only about
2072
self.assertEquals((name, value), calls[0][1:])
2074
def test_set_hook_bazaar(self):
2075
self.assertSetHook(self.bazaar_config, 'foo', 'bazaar')
2077
def test_set_hook_locations(self):
2078
self.assertSetHook(self.locations_config, 'foo', 'locations')
2080
def test_set_hook_branch(self):
2081
self.assertSetHook(self.branch_config, 'foo', 'branch')
2083
def assertRemoveHook(self, conf, name, section_name=None):
2087
config.OldConfigHooks.install_named_hook('remove', hook, None)
2089
config.OldConfigHooks.uninstall_named_hook, 'remove', None)
2090
self.assertLength(0, calls)
2091
conf.remove_user_option(name, section_name)
2092
self.assertLength(1, calls)
2093
# We can't assert the conf object below as different configs use
2094
# different means to implement remove_user_option and we care only about
2096
self.assertEquals((name,), calls[0][1:])
2098
def test_remove_hook_bazaar(self):
2099
self.assertRemoveHook(self.bazaar_config, 'file')
2101
def test_remove_hook_locations(self):
2102
self.assertRemoveHook(self.locations_config, 'file',
2103
self.locations_config.location)
2105
def test_remove_hook_branch(self):
2106
self.assertRemoveHook(self.branch_config, 'file')
2108
def assertLoadHook(self, name, conf_class, *conf_args):
2112
config.OldConfigHooks.install_named_hook('load', hook, None)
2114
config.OldConfigHooks.uninstall_named_hook, 'load', None)
2115
self.assertLength(0, calls)
2117
conf = conf_class(*conf_args)
2118
# Access an option to trigger a load
2119
conf.get_user_option(name)
2120
self.assertLength(1, calls)
2121
# Since we can't assert about conf, we just use the number of calls ;-/
2123
def test_load_hook_bazaar(self):
2124
self.assertLoadHook('file', config.GlobalConfig)
2126
def test_load_hook_locations(self):
2127
self.assertLoadHook('file', config.LocationConfig, self.tree.basedir)
2129
def test_load_hook_branch(self):
2130
self.assertLoadHook('file', config.BranchConfig, self.tree.branch)
2132
def assertSaveHook(self, conf):
2136
config.OldConfigHooks.install_named_hook('save', hook, None)
2138
config.OldConfigHooks.uninstall_named_hook, 'save', None)
2139
self.assertLength(0, calls)
2140
# Setting an option triggers a save
2141
conf.set_user_option('foo', 'bar')
2142
self.assertLength(1, calls)
2143
# Since we can't assert about conf, we just use the number of calls ;-/
2145
def test_save_hook_bazaar(self):
2146
self.assertSaveHook(self.bazaar_config)
2148
def test_save_hook_locations(self):
2149
self.assertSaveHook(self.locations_config)
2151
def test_save_hook_branch(self):
2152
self.assertSaveHook(self.branch_config)
2155
class TestOldConfigHooksForRemote(tests.TestCaseWithTransport):
2156
"""Tests config hooks for remote configs.
2158
No tests for the remove hook as this is not implemented there.
2162
super(TestOldConfigHooksForRemote, self).setUp()
2163
self.transport_server = test_server.SmartTCPServer_for_testing
2164
create_configs_with_file_option(self)
2166
def assertGetHook(self, conf, name, value):
2170
config.OldConfigHooks.install_named_hook('get', hook, None)
2172
config.OldConfigHooks.uninstall_named_hook, 'get', None)
2173
self.assertLength(0, calls)
2174
actual_value = conf.get_option(name)
2175
self.assertEquals(value, actual_value)
2176
self.assertLength(1, calls)
2177
self.assertEquals((conf, name, value), calls[0])
2179
def test_get_hook_remote_branch(self):
2180
remote_branch = branch.Branch.open(self.get_url('tree'))
2181
self.assertGetHook(remote_branch._get_config(), 'file', 'branch')
2183
def test_get_hook_remote_bzrdir(self):
2184
remote_bzrdir = bzrdir.BzrDir.open(self.get_url('tree'))
2185
conf = remote_bzrdir._get_config()
2186
conf.set_option('remotedir', 'file')
2187
self.assertGetHook(conf, 'file', 'remotedir')
2189
def assertSetHook(self, conf, name, value):
2193
config.OldConfigHooks.install_named_hook('set', hook, None)
2195
config.OldConfigHooks.uninstall_named_hook, 'set', None)
2196
self.assertLength(0, calls)
2197
conf.set_option(value, name)
2198
self.assertLength(1, calls)
2199
# We can't assert the conf object below as different configs use
2200
# different means to implement set_user_option and we care only about
2202
self.assertEquals((name, value), calls[0][1:])
2204
def test_set_hook_remote_branch(self):
2205
remote_branch = branch.Branch.open(self.get_url('tree'))
2206
self.addCleanup(remote_branch.lock_write().unlock)
2207
self.assertSetHook(remote_branch._get_config(), 'file', 'remote')
2209
def test_set_hook_remote_bzrdir(self):
2210
remote_branch = branch.Branch.open(self.get_url('tree'))
2211
self.addCleanup(remote_branch.lock_write().unlock)
2212
remote_bzrdir = bzrdir.BzrDir.open(self.get_url('tree'))
2213
self.assertSetHook(remote_bzrdir._get_config(), 'file', 'remotedir')
2215
def assertLoadHook(self, expected_nb_calls, name, conf_class, *conf_args):
2219
config.OldConfigHooks.install_named_hook('load', hook, None)
2221
config.OldConfigHooks.uninstall_named_hook, 'load', None)
2222
self.assertLength(0, calls)
2224
conf = conf_class(*conf_args)
2225
# Access an option to trigger a load
2226
conf.get_option(name)
2227
self.assertLength(expected_nb_calls, calls)
2228
# Since we can't assert about conf, we just use the number of calls ;-/
2230
def test_load_hook_remote_branch(self):
2231
remote_branch = branch.Branch.open(self.get_url('tree'))
2232
self.assertLoadHook(1, 'file', remote.RemoteBranchConfig, remote_branch)
2234
def test_load_hook_remote_bzrdir(self):
2235
remote_bzrdir = bzrdir.BzrDir.open(self.get_url('tree'))
2236
# The config file doesn't exist, set an option to force its creation
2237
conf = remote_bzrdir._get_config()
2238
conf.set_option('remotedir', 'file')
2239
# We get one call for the server and one call for the client, this is
2240
# caused by the differences in implementations betwen
2241
# SmartServerBzrDirRequestConfigFile (in smart/bzrdir.py) and
2242
# SmartServerBranchGetConfigFile (in smart/branch.py)
2243
self.assertLoadHook(2 ,'file', remote.RemoteBzrDirConfig, remote_bzrdir)
2245
def assertSaveHook(self, conf):
2249
config.OldConfigHooks.install_named_hook('save', hook, None)
2251
config.OldConfigHooks.uninstall_named_hook, 'save', None)
2252
self.assertLength(0, calls)
2253
# Setting an option triggers a save
2254
conf.set_option('foo', 'bar')
2255
self.assertLength(1, calls)
2256
# Since we can't assert about conf, we just use the number of calls ;-/
2258
def test_save_hook_remote_branch(self):
2259
remote_branch = branch.Branch.open(self.get_url('tree'))
2260
self.addCleanup(remote_branch.lock_write().unlock)
2261
self.assertSaveHook(remote_branch._get_config())
2263
def test_save_hook_remote_bzrdir(self):
2264
remote_branch = branch.Branch.open(self.get_url('tree'))
2265
self.addCleanup(remote_branch.lock_write().unlock)
2266
remote_bzrdir = bzrdir.BzrDir.open(self.get_url('tree'))
2267
self.assertSaveHook(remote_bzrdir._get_config())
2270
class TestOption(tests.TestCase):
2272
def test_default_value(self):
2273
opt = config.Option('foo', default='bar')
2274
self.assertEquals('bar', opt.get_default())
2276
def test_default_value_from_env(self):
2277
opt = config.Option('foo', default='bar', default_from_env=['FOO'])
2278
self.overrideEnv('FOO', 'quux')
2279
# Env variable provides a default taking over the option one
2280
self.assertEquals('quux', opt.get_default())
2282
def test_first_default_value_from_env_wins(self):
2283
opt = config.Option('foo', default='bar',
2284
default_from_env=['NO_VALUE', 'FOO', 'BAZ'])
2285
self.overrideEnv('FOO', 'foo')
2286
self.overrideEnv('BAZ', 'baz')
2287
# The first env var set wins
2288
self.assertEquals('foo', opt.get_default())
2290
def test_not_supported_list_default_value(self):
2291
self.assertRaises(AssertionError, config.Option, 'foo', default=[1])
2293
def test_not_supported_object_default_value(self):
2294
self.assertRaises(AssertionError, config.Option, 'foo',
2298
class TestOptionConverterMixin(object):
2300
def assertConverted(self, expected, opt, value):
2301
self.assertEquals(expected, opt.convert_from_unicode(value))
2303
def assertWarns(self, opt, value):
2306
warnings.append(args[0] % args[1:])
2307
self.overrideAttr(trace, 'warning', warning)
2308
self.assertEquals(None, opt.convert_from_unicode(value))
2309
self.assertLength(1, warnings)
2311
'Value "%s" is not valid for "%s"' % (value, opt.name),
2314
def assertErrors(self, opt, value):
2315
self.assertRaises(errors.ConfigOptionValueError,
2316
opt.convert_from_unicode, value)
2318
def assertConvertInvalid(self, opt, invalid_value):
2320
self.assertEquals(None, opt.convert_from_unicode(invalid_value))
2321
opt.invalid = 'warning'
2322
self.assertWarns(opt, invalid_value)
2323
opt.invalid = 'error'
2324
self.assertErrors(opt, invalid_value)
2327
class TestOptionWithBooleanConverter(tests.TestCase, TestOptionConverterMixin):
2329
def get_option(self):
2330
return config.Option('foo', help='A boolean.',
2331
from_unicode=config.bool_from_store)
2333
def test_convert_invalid(self):
2334
opt = self.get_option()
2335
# A string that is not recognized as a boolean
2336
self.assertConvertInvalid(opt, u'invalid-boolean')
2337
# A list of strings is never recognized as a boolean
2338
self.assertConvertInvalid(opt, [u'not', u'a', u'boolean'])
2340
def test_convert_valid(self):
2341
opt = self.get_option()
2342
self.assertConverted(True, opt, u'True')
2343
self.assertConverted(True, opt, u'1')
2344
self.assertConverted(False, opt, u'False')
2347
class TestOptionWithIntegerConverter(tests.TestCase, TestOptionConverterMixin):
2349
def get_option(self):
2350
return config.Option('foo', help='An integer.',
2351
from_unicode=config.int_from_store)
2353
def test_convert_invalid(self):
2354
opt = self.get_option()
2355
# A string that is not recognized as an integer
2356
self.assertConvertInvalid(opt, u'forty-two')
2357
# A list of strings is never recognized as an integer
2358
self.assertConvertInvalid(opt, [u'a', u'list'])
2360
def test_convert_valid(self):
2361
opt = self.get_option()
2362
self.assertConverted(16, opt, u'16')
2364
class TestOptionWithListConverter(tests.TestCase, TestOptionConverterMixin):
2366
def get_option(self):
2367
return config.Option('foo', help='A list.',
2368
from_unicode=config.list_from_store)
2370
def test_convert_invalid(self):
2371
# No string is invalid as all forms can be converted to a list
2374
def test_convert_valid(self):
2375
opt = self.get_option()
2376
# An empty string is an empty list
2377
self.assertConverted([], opt, '') # Using a bare str() just in case
2378
self.assertConverted([], opt, u'')
2380
self.assertConverted([u'True'], opt, u'True')
2382
self.assertConverted([u'42'], opt, u'42')
2384
self.assertConverted([u'bar'], opt, u'bar')
2385
# A list remains a list (configObj will turn a string containing commas
2386
# into a list, but that's not what we're testing here)
2387
self.assertConverted([u'foo', u'1', u'True'],
2388
opt, [u'foo', u'1', u'True'])
2391
class TestOptionConverterMixin(object):
2393
def assertConverted(self, expected, opt, value):
2394
self.assertEquals(expected, opt.convert_from_unicode(value))
2396
def assertWarns(self, opt, value):
2399
warnings.append(args[0] % args[1:])
2400
self.overrideAttr(trace, 'warning', warning)
2401
self.assertEquals(None, opt.convert_from_unicode(value))
2402
self.assertLength(1, warnings)
2404
'Value "%s" is not valid for "%s"' % (value, opt.name),
2407
def assertErrors(self, opt, value):
2408
self.assertRaises(errors.ConfigOptionValueError,
2409
opt.convert_from_unicode, value)
2411
def assertConvertInvalid(self, opt, invalid_value):
2413
self.assertEquals(None, opt.convert_from_unicode(invalid_value))
2414
opt.invalid = 'warning'
2415
self.assertWarns(opt, invalid_value)
2416
opt.invalid = 'error'
2417
self.assertErrors(opt, invalid_value)
2420
class TestOptionWithBooleanConverter(tests.TestCase, TestOptionConverterMixin):
2422
def get_option(self):
2423
return config.Option('foo', help='A boolean.',
2424
from_unicode=config.bool_from_store)
2426
def test_convert_invalid(self):
2427
opt = self.get_option()
2428
# A string that is not recognized as a boolean
2429
self.assertConvertInvalid(opt, u'invalid-boolean')
2430
# A list of strings is never recognized as a boolean
2431
self.assertConvertInvalid(opt, [u'not', u'a', u'boolean'])
2433
def test_convert_valid(self):
2434
opt = self.get_option()
2435
self.assertConverted(True, opt, u'True')
2436
self.assertConverted(True, opt, u'1')
2437
self.assertConverted(False, opt, u'False')
2440
class TestOptionWithIntegerConverter(tests.TestCase, TestOptionConverterMixin):
2442
def get_option(self):
2443
return config.Option('foo', help='An integer.',
2444
from_unicode=config.int_from_store)
2446
def test_convert_invalid(self):
2447
opt = self.get_option()
2448
# A string that is not recognized as an integer
2449
self.assertConvertInvalid(opt, u'forty-two')
2450
# A list of strings is never recognized as an integer
2451
self.assertConvertInvalid(opt, [u'a', u'list'])
2453
def test_convert_valid(self):
2454
opt = self.get_option()
2455
self.assertConverted(16, opt, u'16')
2458
class TestOptionWithListConverter(tests.TestCase, TestOptionConverterMixin):
2460
def get_option(self):
2461
return config.Option('foo', help='A list.',
2462
from_unicode=config.list_from_store)
2464
def test_convert_invalid(self):
2465
# No string is invalid as all forms can be converted to a list
2468
def test_convert_valid(self):
2469
opt = self.get_option()
2470
# An empty string is an empty list
2471
self.assertConverted([], opt, '') # Using a bare str() just in case
2472
self.assertConverted([], opt, u'')
2474
self.assertConverted([u'True'], opt, u'True')
2476
self.assertConverted([u'42'], opt, u'42')
2478
self.assertConverted([u'bar'], opt, u'bar')
2479
# A list remains a list (configObj will turn a string containing commas
2480
# into a list, but that's not what we're testing here)
2481
self.assertConverted([u'foo', u'1', u'True'],
2482
opt, [u'foo', u'1', u'True'])
2485
class TestOptionRegistry(tests.TestCase):
2488
super(TestOptionRegistry, self).setUp()
2489
# Always start with an empty registry
2490
self.overrideAttr(config, 'option_registry', config.OptionRegistry())
2491
self.registry = config.option_registry
2493
def test_register(self):
2494
opt = config.Option('foo')
2495
self.registry.register(opt)
2496
self.assertIs(opt, self.registry.get('foo'))
2498
def test_registered_help(self):
2499
opt = config.Option('foo', help='A simple option')
2500
self.registry.register(opt)
2501
self.assertEquals('A simple option', self.registry.get_help('foo'))
2503
lazy_option = config.Option('lazy_foo', help='Lazy help')
2505
def test_register_lazy(self):
2506
self.registry.register_lazy('lazy_foo', self.__module__,
2507
'TestOptionRegistry.lazy_option')
2508
self.assertIs(self.lazy_option, self.registry.get('lazy_foo'))
2510
def test_registered_lazy_help(self):
2511
self.registry.register_lazy('lazy_foo', self.__module__,
2512
'TestOptionRegistry.lazy_option')
2513
self.assertEquals('Lazy help', self.registry.get_help('lazy_foo'))
2516
class TestRegisteredOptions(tests.TestCase):
2517
"""All registered options should verify some constraints."""
2519
scenarios = [(key, {'option_name': key, 'option': option}) for key, option
2520
in config.option_registry.iteritems()]
2523
super(TestRegisteredOptions, self).setUp()
2524
self.registry = config.option_registry
2526
def test_proper_name(self):
2527
# An option should be registered under its own name, this can't be
2528
# checked at registration time for the lazy ones.
2529
self.assertEquals(self.option_name, self.option.name)
2531
def test_help_is_set(self):
2532
option_help = self.registry.get_help(self.option_name)
2533
self.assertNotEquals(None, option_help)
2534
# Come on, think about the user, he really wants to know what the
2536
self.assertIsNot(None, option_help)
2537
self.assertNotEquals('', option_help)
2540
class TestSection(tests.TestCase):
2542
# FIXME: Parametrize so that all sections produced by Stores run these
2543
# tests -- vila 2011-04-01
2545
def test_get_a_value(self):
2546
a_dict = dict(foo='bar')
2547
section = config.Section('myID', a_dict)
2548
self.assertEquals('bar', section.get('foo'))
2550
def test_get_unknown_option(self):
2552
section = config.Section(None, a_dict)
2553
self.assertEquals('out of thin air',
2554
section.get('foo', 'out of thin air'))
2556
def test_options_is_shared(self):
2558
section = config.Section(None, a_dict)
2559
self.assertIs(a_dict, section.options)
2562
class TestMutableSection(tests.TestCase):
2564
# FIXME: Parametrize so that all sections (including os.environ and the
2565
# ones produced by Stores) run these tests -- vila 2011-04-01
2568
a_dict = dict(foo='bar')
2569
section = config.MutableSection('myID', a_dict)
2570
section.set('foo', 'new_value')
2571
self.assertEquals('new_value', section.get('foo'))
2572
# The change appears in the shared section
2573
self.assertEquals('new_value', a_dict.get('foo'))
2574
# We keep track of the change
2575
self.assertTrue('foo' in section.orig)
2576
self.assertEquals('bar', section.orig.get('foo'))
2578
def test_set_preserve_original_once(self):
2579
a_dict = dict(foo='bar')
2580
section = config.MutableSection('myID', a_dict)
2581
section.set('foo', 'first_value')
2582
section.set('foo', 'second_value')
2583
# We keep track of the original value
2584
self.assertTrue('foo' in section.orig)
2585
self.assertEquals('bar', section.orig.get('foo'))
2587
def test_remove(self):
2588
a_dict = dict(foo='bar')
2589
section = config.MutableSection('myID', a_dict)
2590
section.remove('foo')
2591
# We get None for unknown options via the default value
2592
self.assertEquals(None, section.get('foo'))
2593
# Or we just get the default value
2594
self.assertEquals('unknown', section.get('foo', 'unknown'))
2595
self.assertFalse('foo' in section.options)
2596
# We keep track of the deletion
2597
self.assertTrue('foo' in section.orig)
2598
self.assertEquals('bar', section.orig.get('foo'))
2600
def test_remove_new_option(self):
2602
section = config.MutableSection('myID', a_dict)
2603
section.set('foo', 'bar')
2604
section.remove('foo')
2605
self.assertFalse('foo' in section.options)
2606
# The option didn't exist initially so it we need to keep track of it
2607
# with a special value
2608
self.assertTrue('foo' in section.orig)
2609
self.assertEquals(config._NewlyCreatedOption, section.orig['foo'])
2612
class TestStore(tests.TestCaseWithTransport):
2614
def assertSectionContent(self, expected, section):
2615
"""Assert that some options have the proper values in a section."""
2616
expected_name, expected_options = expected
2617
self.assertEquals(expected_name, section.id)
2620
dict([(k, section.get(k)) for k in expected_options.keys()]))
2623
class TestReadonlyStore(TestStore):
2625
scenarios = [(key, {'get_store': builder}) for key, builder
2626
in config.test_store_builder_registry.iteritems()]
2629
super(TestReadonlyStore, self).setUp()
2631
def test_building_delays_load(self):
2632
store = self.get_store(self)
2633
self.assertEquals(False, store.is_loaded())
2634
store._load_from_string('')
2635
self.assertEquals(True, store.is_loaded())
2637
def test_get_no_sections_for_empty(self):
2638
store = self.get_store(self)
2639
store._load_from_string('')
2640
self.assertEquals([], list(store.get_sections()))
2642
def test_get_default_section(self):
2643
store = self.get_store(self)
2644
store._load_from_string('foo=bar')
2645
sections = list(store.get_sections())
2646
self.assertLength(1, sections)
2647
self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
2649
def test_get_named_section(self):
2650
store = self.get_store(self)
2651
store._load_from_string('[baz]\nfoo=bar')
2652
sections = list(store.get_sections())
2653
self.assertLength(1, sections)
2654
self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
2656
def test_load_from_string_fails_for_non_empty_store(self):
2657
store = self.get_store(self)
2658
store._load_from_string('foo=bar')
2659
self.assertRaises(AssertionError, store._load_from_string, 'bar=baz')
2662
class TestIniFileStoreContent(tests.TestCaseWithTransport):
2663
"""Simulate loading a config store without content of various encodings.
2665
All files produced by bzr are in utf8 content.
2667
Users may modify them manually and end up with a file that can't be
2668
loaded. We need to issue proper error messages in this case.
2671
invalid_utf8_char = '\xff'
2673
def test_load_utf8(self):
2674
"""Ensure we can load an utf8-encoded file."""
2675
t = self.get_transport()
2676
# From http://pad.lv/799212
2677
unicode_user = u'b\N{Euro Sign}ar'
2678
unicode_content = u'user=%s' % (unicode_user,)
2679
utf8_content = unicode_content.encode('utf8')
2680
# Store the raw content in the config file
2681
t.put_bytes('foo.conf', utf8_content)
2682
store = config.IniFileStore(t, 'foo.conf')
2684
stack = config.Stack([store.get_sections], store)
2685
self.assertEquals(unicode_user, stack.get('user'))
2687
def test_load_non_ascii(self):
2688
"""Ensure we display a proper error on non-ascii, non utf-8 content."""
2689
t = self.get_transport()
2690
t.put_bytes('foo.conf', 'user=foo\n#%s\n' % (self.invalid_utf8_char,))
2691
store = config.IniFileStore(t, 'foo.conf')
2692
self.assertRaises(errors.ConfigContentError, store.load)
2694
def test_load_erroneous_content(self):
2695
"""Ensure we display a proper error on content that can't be parsed."""
2696
t = self.get_transport()
2697
t.put_bytes('foo.conf', '[open_section\n')
2698
store = config.IniFileStore(t, 'foo.conf')
2699
self.assertRaises(errors.ParseConfigError, store.load)
2701
def test_load_permission_denied(self):
2702
"""Ensure we get warned when trying to load an inaccessible file."""
2705
warnings.append(args[0] % args[1:])
2706
self.overrideAttr(trace, 'warning', warning)
2708
t = self.get_transport()
2710
def get_bytes(relpath):
2711
raise errors.PermissionDenied(relpath, "")
2712
t.get_bytes = get_bytes
2713
store = config.IniFileStore(t, 'foo.conf')
2714
self.assertRaises(errors.PermissionDenied, store.load)
2717
[u'Permission denied while trying to load configuration store %s.'
2718
% store.external_url()])
2721
class TestIniConfigContent(tests.TestCaseWithTransport):
2722
"""Simulate loading a IniBasedConfig without content of various encodings.
2724
All files produced by bzr are in utf8 content.
2726
Users may modify them manually and end up with a file that can't be
2727
loaded. We need to issue proper error messages in this case.
2730
invalid_utf8_char = '\xff'
2732
def test_load_utf8(self):
2733
"""Ensure we can load an utf8-encoded file."""
2734
# From http://pad.lv/799212
2735
unicode_user = u'b\N{Euro Sign}ar'
2736
unicode_content = u'user=%s' % (unicode_user,)
2737
utf8_content = unicode_content.encode('utf8')
2738
# Store the raw content in the config file
2739
with open('foo.conf', 'wb') as f:
2740
f.write(utf8_content)
2741
conf = config.IniBasedConfig(file_name='foo.conf')
2742
self.assertEquals(unicode_user, conf.get_user_option('user'))
2744
def test_load_badly_encoded_content(self):
2745
"""Ensure we display a proper error on non-ascii, non utf-8 content."""
2746
with open('foo.conf', 'wb') as f:
2747
f.write('user=foo\n#%s\n' % (self.invalid_utf8_char,))
2748
conf = config.IniBasedConfig(file_name='foo.conf')
2749
self.assertRaises(errors.ConfigContentError, conf._get_parser)
2751
def test_load_erroneous_content(self):
2752
"""Ensure we display a proper error on content that can't be parsed."""
2753
with open('foo.conf', 'wb') as f:
2754
f.write('[open_section\n')
2755
conf = config.IniBasedConfig(file_name='foo.conf')
2756
self.assertRaises(errors.ParseConfigError, conf._get_parser)
2759
class TestMutableStore(TestStore):
2761
scenarios = [(key, {'store_id': key, 'get_store': builder}) for key, builder
2762
in config.test_store_builder_registry.iteritems()]
2765
super(TestMutableStore, self).setUp()
2766
self.transport = self.get_transport()
2768
def has_store(self, store):
2769
store_basename = urlutils.relative_url(self.transport.external_url(),
2770
store.external_url())
2771
return self.transport.has(store_basename)
2773
def test_save_empty_creates_no_file(self):
2774
# FIXME: There should be a better way than relying on the test
2775
# parametrization to identify branch.conf -- vila 2011-0526
2776
if self.store_id in ('branch', 'remote_branch'):
2777
raise tests.TestNotApplicable(
2778
'branch.conf is *always* created when a branch is initialized')
2779
store = self.get_store(self)
2781
self.assertEquals(False, self.has_store(store))
2783
def test_save_emptied_succeeds(self):
2784
store = self.get_store(self)
2785
store._load_from_string('foo=bar\n')
2786
section = store.get_mutable_section(None)
2787
section.remove('foo')
2789
self.assertEquals(True, self.has_store(store))
2790
modified_store = self.get_store(self)
2791
sections = list(modified_store.get_sections())
2792
self.assertLength(0, sections)
2794
def test_save_with_content_succeeds(self):
2795
# FIXME: There should be a better way than relying on the test
2796
# parametrization to identify branch.conf -- vila 2011-0526
2797
if self.store_id in ('branch', 'remote_branch'):
2798
raise tests.TestNotApplicable(
2799
'branch.conf is *always* created when a branch is initialized')
2800
store = self.get_store(self)
2801
store._load_from_string('foo=bar\n')
2802
self.assertEquals(False, self.has_store(store))
2804
self.assertEquals(True, self.has_store(store))
2805
modified_store = self.get_store(self)
2806
sections = list(modified_store.get_sections())
2807
self.assertLength(1, sections)
2808
self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
2810
def test_set_option_in_empty_store(self):
2811
store = self.get_store(self)
2812
section = store.get_mutable_section(None)
2813
section.set('foo', 'bar')
2815
modified_store = self.get_store(self)
2816
sections = list(modified_store.get_sections())
2817
self.assertLength(1, sections)
2818
self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
2820
def test_set_option_in_default_section(self):
2821
store = self.get_store(self)
2822
store._load_from_string('')
2823
section = store.get_mutable_section(None)
2824
section.set('foo', 'bar')
2826
modified_store = self.get_store(self)
2827
sections = list(modified_store.get_sections())
2828
self.assertLength(1, sections)
2829
self.assertSectionContent((None, {'foo': 'bar'}), sections[0])
2831
def test_set_option_in_named_section(self):
2832
store = self.get_store(self)
2833
store._load_from_string('')
2834
section = store.get_mutable_section('baz')
2835
section.set('foo', 'bar')
2837
modified_store = self.get_store(self)
2838
sections = list(modified_store.get_sections())
2839
self.assertLength(1, sections)
2840
self.assertSectionContent(('baz', {'foo': 'bar'}), sections[0])
2842
def test_load_hook(self):
2843
# We first needs to ensure that the store exists
2844
store = self.get_store(self)
2845
section = store.get_mutable_section('baz')
2846
section.set('foo', 'bar')
2848
# Now we can try to load it
2849
store = self.get_store(self)
2853
config.ConfigHooks.install_named_hook('load', hook, None)
2854
self.assertLength(0, calls)
2856
self.assertLength(1, calls)
2857
self.assertEquals((store,), calls[0])
2859
def test_save_hook(self):
2863
config.ConfigHooks.install_named_hook('save', hook, None)
2864
self.assertLength(0, calls)
2865
store = self.get_store(self)
2866
section = store.get_mutable_section('baz')
2867
section.set('foo', 'bar')
2869
self.assertLength(1, calls)
2870
self.assertEquals((store,), calls[0])
2873
class TestIniFileStore(TestStore):
2875
def test_loading_unknown_file_fails(self):
2876
store = config.IniFileStore(self.get_transport(), 'I-do-not-exist')
2877
self.assertRaises(errors.NoSuchFile, store.load)
2879
def test_invalid_content(self):
2880
store = config.IniFileStore(self.get_transport(), 'foo.conf', )
2881
self.assertEquals(False, store.is_loaded())
2882
exc = self.assertRaises(
2883
errors.ParseConfigError, store._load_from_string,
2884
'this is invalid !')
2885
self.assertEndsWith(exc.filename, 'foo.conf')
2886
# And the load failed
2887
self.assertEquals(False, store.is_loaded())
2889
def test_get_embedded_sections(self):
2890
# A more complicated example (which also shows that section names and
2891
# option names share the same name space...)
2892
# FIXME: This should be fixed by forbidding dicts as values ?
2893
# -- vila 2011-04-05
2894
store = config.IniFileStore(self.get_transport(), 'foo.conf', )
2895
store._load_from_string('''
2899
foo_in_DEFAULT=foo_DEFAULT
2907
sections = list(store.get_sections())
2908
self.assertLength(4, sections)
2909
# The default section has no name.
2910
# List values are provided as lists
2911
self.assertSectionContent((None, {'foo': 'bar', 'l': ['1', '2']}),
2913
self.assertSectionContent(
2914
('DEFAULT', {'foo_in_DEFAULT': 'foo_DEFAULT'}), sections[1])
2915
self.assertSectionContent(
2916
('bar', {'foo_in_bar': 'barbar'}), sections[2])
2917
# sub sections are provided as embedded dicts.
2918
self.assertSectionContent(
2919
('baz', {'foo_in_baz': 'barbaz', 'qux': {'foo_in_qux': 'quux'}}),
2923
class TestLockableIniFileStore(TestStore):
2925
def test_create_store_in_created_dir(self):
2926
self.assertPathDoesNotExist('dir')
2927
t = self.get_transport('dir/subdir')
2928
store = config.LockableIniFileStore(t, 'foo.conf')
2929
store.get_mutable_section(None).set('foo', 'bar')
2931
self.assertPathExists('dir/subdir')
2934
class TestConcurrentStoreUpdates(TestStore):
2935
"""Test that Stores properly handle conccurent updates.
2937
New Store implementation may fail some of these tests but until such
2938
implementations exist it's hard to properly filter them from the scenarios
2939
applied here. If you encounter such a case, contact the bzr devs.
2942
scenarios = [(key, {'get_stack': builder}) for key, builder
2943
in config.test_stack_builder_registry.iteritems()]
2946
super(TestConcurrentStoreUpdates, self).setUp()
2947
self._content = 'one=1\ntwo=2\n'
2948
self.stack = self.get_stack(self)
2949
if not isinstance(self.stack, config._CompatibleStack):
2950
raise tests.TestNotApplicable(
2951
'%s is not meant to be compatible with the old config design'
2953
self.stack.store._load_from_string(self._content)
2955
self.stack.store.save()
2957
def test_simple_read_access(self):
2958
self.assertEquals('1', self.stack.get('one'))
2960
def test_simple_write_access(self):
2961
self.stack.set('one', 'one')
2962
self.assertEquals('one', self.stack.get('one'))
2964
def test_listen_to_the_last_speaker(self):
2966
c2 = self.get_stack(self)
2967
c1.set('one', 'ONE')
2968
c2.set('two', 'TWO')
2969
self.assertEquals('ONE', c1.get('one'))
2970
self.assertEquals('TWO', c2.get('two'))
2971
# The second update respect the first one
2972
self.assertEquals('ONE', c2.get('one'))
2974
def test_last_speaker_wins(self):
2975
# If the same config is not shared, the same variable modified twice
2976
# can only see a single result.
2978
c2 = self.get_stack(self)
2981
self.assertEquals('c2', c2.get('one'))
2982
# The first modification is still available until another refresh
2984
self.assertEquals('c1', c1.get('one'))
2985
c1.set('two', 'done')
2986
self.assertEquals('c2', c1.get('one'))
2988
def test_writes_are_serialized(self):
2990
c2 = self.get_stack(self)
2992
# We spawn a thread that will pause *during* the config saving.
2993
before_writing = threading.Event()
2994
after_writing = threading.Event()
2995
writing_done = threading.Event()
2996
c1_save_without_locking_orig = c1.store.save_without_locking
2997
def c1_save_without_locking():
2998
before_writing.set()
2999
c1_save_without_locking_orig()
3000
# The lock is held. We wait for the main thread to decide when to
3002
after_writing.wait()
3003
c1.store.save_without_locking = c1_save_without_locking
3007
t1 = threading.Thread(target=c1_set)
3008
# Collect the thread after the test
3009
self.addCleanup(t1.join)
3010
# Be ready to unblock the thread if the test goes wrong
3011
self.addCleanup(after_writing.set)
3013
before_writing.wait()
3014
self.assertRaises(errors.LockContention,
3015
c2.set, 'one', 'c2')
3016
self.assertEquals('c1', c1.get('one'))
3017
# Let the lock be released
3021
self.assertEquals('c2', c2.get('one'))
3023
def test_read_while_writing(self):
3025
# We spawn a thread that will pause *during* the write
3026
ready_to_write = threading.Event()
3027
do_writing = threading.Event()
3028
writing_done = threading.Event()
3029
# We override the _save implementation so we know the store is locked
3030
c1_save_without_locking_orig = c1.store.save_without_locking
3031
def c1_save_without_locking():
3032
ready_to_write.set()
3033
# The lock is held. We wait for the main thread to decide when to
3036
c1_save_without_locking_orig()
3038
c1.store.save_without_locking = c1_save_without_locking
3041
t1 = threading.Thread(target=c1_set)
3042
# Collect the thread after the test
3043
self.addCleanup(t1.join)
3044
# Be ready to unblock the thread if the test goes wrong
3045
self.addCleanup(do_writing.set)
3047
# Ensure the thread is ready to write
3048
ready_to_write.wait()
3049
self.assertEquals('c1', c1.get('one'))
3050
# If we read during the write, we get the old value
3051
c2 = self.get_stack(self)
3052
self.assertEquals('1', c2.get('one'))
3053
# Let the writing occur and ensure it occurred
3056
# Now we get the updated value
3057
c3 = self.get_stack(self)
3058
self.assertEquals('c1', c3.get('one'))
3060
# FIXME: It may be worth looking into removing the lock dir when it's not
3061
# needed anymore and look at possible fallouts for concurrent lockers. This
3062
# will matter if/when we use config files outside of bazaar directories
3063
# (.bazaar or .bzr) -- vila 20110-04-11
3066
class TestSectionMatcher(TestStore):
3068
scenarios = [('location', {'matcher': config.LocationMatcher})]
3070
def get_store(self, file_name):
3071
return config.IniFileStore(self.get_readonly_transport(), file_name)
3073
def test_no_matches_for_empty_stores(self):
3074
store = self.get_store('foo.conf')
3075
store._load_from_string('')
3076
matcher = self.matcher(store, '/bar')
3077
self.assertEquals([], list(matcher.get_sections()))
3079
def test_build_doesnt_load_store(self):
3080
store = self.get_store('foo.conf')
3081
matcher = self.matcher(store, '/bar')
3082
self.assertFalse(store.is_loaded())
3085
class TestLocationSection(tests.TestCase):
3087
def get_section(self, options, extra_path):
3088
section = config.Section('foo', options)
3089
# We don't care about the length so we use '0'
3090
return config.LocationSection(section, 0, extra_path)
3092
def test_simple_option(self):
3093
section = self.get_section({'foo': 'bar'}, '')
3094
self.assertEquals('bar', section.get('foo'))
3096
def test_option_with_extra_path(self):
3097
section = self.get_section({'foo': 'bar', 'foo:policy': 'appendpath'},
3099
self.assertEquals('bar/baz', section.get('foo'))
3101
def test_invalid_policy(self):
3102
section = self.get_section({'foo': 'bar', 'foo:policy': 'die'},
3104
# invalid policies are ignored
3105
self.assertEquals('bar', section.get('foo'))
3108
class TestLocationMatcher(TestStore):
3110
def get_store(self, file_name):
3111
return config.IniFileStore(self.get_readonly_transport(), file_name)
3113
def test_unrelated_section_excluded(self):
3114
store = self.get_store('foo.conf')
3115
store._load_from_string('''
3123
section=/foo/bar/baz
3127
self.assertEquals(['/foo', '/foo/baz', '/foo/bar', '/foo/bar/baz',
3129
[section.id for section in store.get_sections()])
3130
matcher = config.LocationMatcher(store, '/foo/bar/quux')
3131
sections = list(matcher.get_sections())
3132
self.assertEquals([3, 2],
3133
[section.length for section in sections])
3134
self.assertEquals(['/foo/bar', '/foo'],
3135
[section.id for section in sections])
3136
self.assertEquals(['quux', 'bar/quux'],
3137
[section.extra_path for section in sections])
3139
def test_more_specific_sections_first(self):
3140
store = self.get_store('foo.conf')
3141
store._load_from_string('''
3147
self.assertEquals(['/foo', '/foo/bar'],
3148
[section.id for section in store.get_sections()])
3149
matcher = config.LocationMatcher(store, '/foo/bar/baz')
3150
sections = list(matcher.get_sections())
3151
self.assertEquals([3, 2],
3152
[section.length for section in sections])
3153
self.assertEquals(['/foo/bar', '/foo'],
3154
[section.id for section in sections])
3155
self.assertEquals(['baz', 'bar/baz'],
3156
[section.extra_path for section in sections])
3158
def test_appendpath_in_no_name_section(self):
3159
# It's a bit weird to allow appendpath in a no-name section, but
3160
# someone may found a use for it
3161
store = self.get_store('foo.conf')
3162
store._load_from_string('''
3164
foo:policy = appendpath
3166
matcher = config.LocationMatcher(store, 'dir/subdir')
3167
sections = list(matcher.get_sections())
3168
self.assertLength(1, sections)
3169
self.assertEquals('bar/dir/subdir', sections[0].get('foo'))
3171
def test_file_urls_are_normalized(self):
3172
store = self.get_store('foo.conf')
3173
if sys.platform == 'win32':
3174
expected_url = 'file:///C:/dir/subdir'
3175
expected_location = 'C:/dir/subdir'
3177
expected_url = 'file:///dir/subdir'
3178
expected_location = '/dir/subdir'
3179
matcher = config.LocationMatcher(store, expected_url)
3180
self.assertEquals(expected_location, matcher.location)
3183
class TestStackGet(tests.TestCase):
3185
# FIXME: This should be parametrized for all known Stack or dedicated
3186
# paramerized tests created to avoid bloating -- vila 2011-03-31
3188
def overrideOptionRegistry(self):
3189
self.overrideAttr(config, 'option_registry', config.OptionRegistry())
3191
def test_single_config_get(self):
3192
conf = dict(foo='bar')
3193
conf_stack = config.Stack([conf])
3194
self.assertEquals('bar', conf_stack.get('foo'))
3196
def test_get_with_registered_default_value(self):
3197
conf_stack = config.Stack([dict()])
3198
opt = config.Option('foo', default='bar')
3199
self.overrideOptionRegistry()
3200
config.option_registry.register('foo', opt)
3201
self.assertEquals('bar', conf_stack.get('foo'))
3203
def test_get_without_registered_default_value(self):
3204
conf_stack = config.Stack([dict()])
3205
opt = config.Option('foo')
3206
self.overrideOptionRegistry()
3207
config.option_registry.register('foo', opt)
3208
self.assertEquals(None, conf_stack.get('foo'))
3210
def test_get_without_default_value_for_not_registered(self):
3211
conf_stack = config.Stack([dict()])
3212
opt = config.Option('foo')
3213
self.overrideOptionRegistry()
3214
self.assertEquals(None, conf_stack.get('foo'))
3216
def test_get_first_definition(self):
3217
conf1 = dict(foo='bar')
3218
conf2 = dict(foo='baz')
3219
conf_stack = config.Stack([conf1, conf2])
3220
self.assertEquals('bar', conf_stack.get('foo'))
3222
def test_get_embedded_definition(self):
3223
conf1 = dict(yy='12')
3224
conf2 = config.Stack([dict(xx='42'), dict(foo='baz')])
3225
conf_stack = config.Stack([conf1, conf2])
3226
self.assertEquals('baz', conf_stack.get('foo'))
3228
def test_get_for_empty_section_callable(self):
3229
conf_stack = config.Stack([lambda : []])
3230
self.assertEquals(None, conf_stack.get('foo'))
3232
def test_get_for_broken_callable(self):
3233
# Trying to use and invalid callable raises an exception on first use
3234
conf_stack = config.Stack([lambda : object()])
3235
self.assertRaises(TypeError, conf_stack.get, 'foo')
3238
class TestStackWithTransport(tests.TestCaseWithTransport):
3240
scenarios = [(key, {'get_stack': builder}) for key, builder
3241
in config.test_stack_builder_registry.iteritems()]
3244
class TestConcreteStacks(TestStackWithTransport):
3246
def test_build_stack(self):
3247
# Just a smoke test to help debug builders
3248
stack = self.get_stack(self)
3251
class TestStackGet(TestStackWithTransport):
3254
super(TestStackGet, self).setUp()
3255
self.conf = self.get_stack(self)
3257
def test_get_for_empty_stack(self):
3258
self.assertEquals(None, self.conf.get('foo'))
3260
def test_get_hook(self):
3261
self.conf.store._load_from_string('foo=bar')
3265
config.ConfigHooks.install_named_hook('get', hook, None)
3266
self.assertLength(0, calls)
3267
value = self.conf.get('foo')
3268
self.assertEquals('bar', value)
3269
self.assertLength(1, calls)
3270
self.assertEquals((self.conf, 'foo', 'bar'), calls[0])
3273
class TestStackGetWithConverter(TestStackGet):
3276
super(TestStackGetWithConverter, self).setUp()
3277
self.overrideAttr(config, 'option_registry', config.OptionRegistry())
3278
self.registry = config.option_registry
3280
def register_bool_option(self, name, default=None, default_from_env=None):
3281
b = config.Option(name, help='A boolean.',
3282
default=default, default_from_env=default_from_env,
3283
from_unicode=config.bool_from_store)
3284
self.registry.register(b)
3286
def test_get_default_bool_None(self):
3287
self.register_bool_option('foo')
3288
self.assertEquals(None, self.conf.get('foo'))
3290
def test_get_default_bool_True(self):
3291
self.register_bool_option('foo', u'True')
3292
self.assertEquals(True, self.conf.get('foo'))
3294
def test_get_default_bool_False(self):
3295
self.register_bool_option('foo', False)
3296
self.assertEquals(False, self.conf.get('foo'))
3298
def test_get_default_bool_False_as_string(self):
3299
self.register_bool_option('foo', u'False')
3300
self.assertEquals(False, self.conf.get('foo'))
3302
def test_get_default_bool_from_env_converted(self):
3303
self.register_bool_option('foo', u'True', default_from_env=['FOO'])
3304
self.overrideEnv('FOO', 'False')
3305
self.assertEquals(False, self.conf.get('foo'))
3307
def test_get_default_bool_when_conversion_fails(self):
3308
self.register_bool_option('foo', default='True')
3309
self.conf.store._load_from_string('foo=invalid boolean')
3310
self.assertEquals(True, self.conf.get('foo'))
3312
def register_integer_option(self, name,
3313
default=None, default_from_env=None):
3314
i = config.Option(name, help='An integer.',
3315
default=default, default_from_env=default_from_env,
3316
from_unicode=config.int_from_store)
3317
self.registry.register(i)
3319
def test_get_default_integer_None(self):
3320
self.register_integer_option('foo')
3321
self.assertEquals(None, self.conf.get('foo'))
3323
def test_get_default_integer(self):
3324
self.register_integer_option('foo', 42)
3325
self.assertEquals(42, self.conf.get('foo'))
3327
def test_get_default_integer_as_string(self):
3328
self.register_integer_option('foo', u'42')
3329
self.assertEquals(42, self.conf.get('foo'))
3331
def test_get_default_integer_from_env(self):
3332
self.register_integer_option('foo', default_from_env=['FOO'])
3333
self.overrideEnv('FOO', '18')
3334
self.assertEquals(18, self.conf.get('foo'))
3336
def test_get_default_integer_when_conversion_fails(self):
3337
self.register_integer_option('foo', default='12')
3338
self.conf.store._load_from_string('foo=invalid integer')
3339
self.assertEquals(12, self.conf.get('foo'))
3341
def register_list_option(self, name, default=None, default_from_env=None):
3342
l = config.Option(name, help='A list.',
3343
default=default, default_from_env=default_from_env,
3344
from_unicode=config.list_from_store)
3345
self.registry.register(l)
3347
def test_get_default_list_None(self):
3348
self.register_list_option('foo')
3349
self.assertEquals(None, self.conf.get('foo'))
3351
def test_get_default_list_empty(self):
3352
self.register_list_option('foo', '')
3353
self.assertEquals([], self.conf.get('foo'))
3355
def test_get_default_list_from_env(self):
3356
self.register_list_option('foo', default_from_env=['FOO'])
3357
self.overrideEnv('FOO', '')
3358
self.assertEquals([], self.conf.get('foo'))
3360
def test_get_with_list_converter_no_item(self):
3361
self.register_list_option('foo', None)
3362
self.conf.store._load_from_string('foo=,')
3363
self.assertEquals([], self.conf.get('foo'))
3365
def test_get_with_list_converter_many_items(self):
3366
self.register_list_option('foo', None)
3367
self.conf.store._load_from_string('foo=m,o,r,e')
3368
self.assertEquals(['m', 'o', 'r', 'e'], self.conf.get('foo'))
3371
class TestStackExpandOptions(tests.TestCaseWithTransport):
3374
super(TestStackExpandOptions, self).setUp()
3375
self.overrideAttr(config, 'option_registry', config.OptionRegistry())
3376
self.registry = config.option_registry
3377
self.conf = build_branch_stack(self)
3379
def assertExpansion(self, expected, string, env=None):
3380
self.assertEquals(expected, self.conf.expand_options(string, env))
3382
def test_no_expansion(self):
3383
self.assertExpansion('foo', 'foo')
3385
def test_expand_default_value(self):
3386
self.conf.store._load_from_string('bar=baz')
3387
self.registry.register(config.Option('foo', default=u'{bar}'))
3388
self.assertEquals('baz', self.conf.get('foo', expand=True))
3390
def test_expand_default_from_env(self):
3391
self.conf.store._load_from_string('bar=baz')
3392
self.registry.register(config.Option('foo', default_from_env=['FOO']))
3393
self.overrideEnv('FOO', '{bar}')
3394
self.assertEquals('baz', self.conf.get('foo', expand=True))
3396
def test_expand_default_on_failed_conversion(self):
3397
self.conf.store._load_from_string('baz=bogus\nbar=42\nfoo={baz}')
3398
self.registry.register(
3399
config.Option('foo', default=u'{bar}',
3400
from_unicode=config.int_from_store))
3401
self.assertEquals(42, self.conf.get('foo', expand=True))
3403
def test_env_adding_options(self):
3404
self.assertExpansion('bar', '{foo}', {'foo': 'bar'})
3406
def test_env_overriding_options(self):
3407
self.conf.store._load_from_string('foo=baz')
3408
self.assertExpansion('bar', '{foo}', {'foo': 'bar'})
3410
def test_simple_ref(self):
3411
self.conf.store._load_from_string('foo=xxx')
3412
self.assertExpansion('xxx', '{foo}')
3414
def test_unknown_ref(self):
3415
self.assertRaises(errors.ExpandingUnknownOption,
3416
self.conf.expand_options, '{foo}')
3418
def test_indirect_ref(self):
3419
self.conf.store._load_from_string('''
3423
self.assertExpansion('xxx', '{bar}')
3425
def test_embedded_ref(self):
3426
self.conf.store._load_from_string('''
3430
self.assertExpansion('xxx', '{{bar}}')
3432
def test_simple_loop(self):
3433
self.conf.store._load_from_string('foo={foo}')
3434
self.assertRaises(errors.OptionExpansionLoop,
3435
self.conf.expand_options, '{foo}')
3437
def test_indirect_loop(self):
3438
self.conf.store._load_from_string('''
3442
e = self.assertRaises(errors.OptionExpansionLoop,
3443
self.conf.expand_options, '{foo}')
3444
self.assertEquals('foo->bar->baz', e.refs)
3445
self.assertEquals('{foo}', e.string)
3447
def test_list(self):
3448
self.conf.store._load_from_string('''
3452
list={foo},{bar},{baz}
3454
self.assertEquals(['start', 'middle', 'end'],
3455
self.conf.get('list', expand=True))
3457
def test_cascading_list(self):
3458
self.conf.store._load_from_string('''
3464
self.assertEquals(['start', 'middle', 'end'],
3465
self.conf.get('list', expand=True))
3467
def test_pathologically_hidden_list(self):
3468
self.conf.store._load_from_string('''
3474
hidden={start}{middle}{end}
3476
# Nope, it's either a string or a list, and the list wins as soon as a
3477
# ',' appears, so the string concatenation never occur.
3478
self.assertEquals(['{foo', '}', '{', 'bar}'],
3479
self.conf.get('hidden', expand=True))
3482
class TestStackCrossSectionsExpand(tests.TestCaseWithTransport):
3485
super(TestStackCrossSectionsExpand, self).setUp()
3487
def get_config(self, location, string):
3490
# Since we don't save the config we won't strictly require to inherit
3491
# from TestCaseInTempDir, but an error occurs so quickly...
3492
c = config.LocationStack(location)
3493
c.store._load_from_string(string)
3496
def test_dont_cross_unrelated_section(self):
3497
c = self.get_config('/another/branch/path','''
3502
[/another/branch/path]
3505
self.assertRaises(errors.ExpandingUnknownOption,
3506
c.get, 'bar', expand=True)
3508
def test_cross_related_sections(self):
3509
c = self.get_config('/project/branch/path','''
3513
[/project/branch/path]
3516
self.assertEquals('quux', c.get('bar', expand=True))
3519
class TestStackSet(TestStackWithTransport):
3521
def test_simple_set(self):
3522
conf = self.get_stack(self)
3523
conf.store._load_from_string('foo=bar')
3524
self.assertEquals('bar', conf.get('foo'))
3525
conf.set('foo', 'baz')
3526
# Did we get it back ?
3527
self.assertEquals('baz', conf.get('foo'))
3529
def test_set_creates_a_new_section(self):
3530
conf = self.get_stack(self)
3531
conf.set('foo', 'baz')
3532
self.assertEquals, 'baz', conf.get('foo')
3534
def test_set_hook(self):
3538
config.ConfigHooks.install_named_hook('set', hook, None)
3539
self.assertLength(0, calls)
3540
conf = self.get_stack(self)
3541
conf.set('foo', 'bar')
3542
self.assertLength(1, calls)
3543
self.assertEquals((conf, 'foo', 'bar'), calls[0])
3546
class TestStackRemove(TestStackWithTransport):
3548
def test_remove_existing(self):
3549
conf = self.get_stack(self)
3550
conf.store._load_from_string('foo=bar')
3551
self.assertEquals('bar', conf.get('foo'))
3553
# Did we get it back ?
3554
self.assertEquals(None, conf.get('foo'))
3556
def test_remove_unknown(self):
3557
conf = self.get_stack(self)
3558
self.assertRaises(KeyError, conf.remove, 'I_do_not_exist')
3560
def test_remove_hook(self):
3564
config.ConfigHooks.install_named_hook('remove', hook, None)
3565
self.assertLength(0, calls)
3566
conf = self.get_stack(self)
3567
conf.store._load_from_string('foo=bar')
3569
self.assertLength(1, calls)
3570
self.assertEquals((conf, 'foo'), calls[0])
3573
class TestConfigGetOptions(tests.TestCaseWithTransport, TestOptionsMixin):
3576
super(TestConfigGetOptions, self).setUp()
3577
create_configs(self)
3579
def test_no_variable(self):
3580
# Using branch should query branch, locations and bazaar
3581
self.assertOptions([], self.branch_config)
3583
def test_option_in_bazaar(self):
3584
self.bazaar_config.set_user_option('file', 'bazaar')
3585
self.assertOptions([('file', 'bazaar', 'DEFAULT', 'bazaar')],
3588
def test_option_in_locations(self):
3589
self.locations_config.set_user_option('file', 'locations')
3591
[('file', 'locations', self.tree.basedir, 'locations')],
3592
self.locations_config)
3594
def test_option_in_branch(self):
3595
self.branch_config.set_user_option('file', 'branch')
3596
self.assertOptions([('file', 'branch', 'DEFAULT', 'branch')],
3599
def test_option_in_bazaar_and_branch(self):
3600
self.bazaar_config.set_user_option('file', 'bazaar')
3601
self.branch_config.set_user_option('file', 'branch')
3602
self.assertOptions([('file', 'branch', 'DEFAULT', 'branch'),
3603
('file', 'bazaar', 'DEFAULT', 'bazaar'),],
3606
def test_option_in_branch_and_locations(self):
3607
# Hmm, locations override branch :-/
3608
self.locations_config.set_user_option('file', 'locations')
3609
self.branch_config.set_user_option('file', 'branch')
3611
[('file', 'locations', self.tree.basedir, 'locations'),
3612
('file', 'branch', 'DEFAULT', 'branch'),],
3615
def test_option_in_bazaar_locations_and_branch(self):
3616
self.bazaar_config.set_user_option('file', 'bazaar')
3617
self.locations_config.set_user_option('file', 'locations')
3618
self.branch_config.set_user_option('file', 'branch')
3620
[('file', 'locations', self.tree.basedir, 'locations'),
3621
('file', 'branch', 'DEFAULT', 'branch'),
3622
('file', 'bazaar', 'DEFAULT', 'bazaar'),],
3626
class TestConfigRemoveOption(tests.TestCaseWithTransport, TestOptionsMixin):
3629
super(TestConfigRemoveOption, self).setUp()
3630
create_configs_with_file_option(self)
3632
def test_remove_in_locations(self):
3633
self.locations_config.remove_user_option('file', self.tree.basedir)
3635
[('file', 'branch', 'DEFAULT', 'branch'),
3636
('file', 'bazaar', 'DEFAULT', 'bazaar'),],
3639
def test_remove_in_branch(self):
3640
self.branch_config.remove_user_option('file')
3642
[('file', 'locations', self.tree.basedir, 'locations'),
3643
('file', 'bazaar', 'DEFAULT', 'bazaar'),],
3646
def test_remove_in_bazaar(self):
3647
self.bazaar_config.remove_user_option('file')
3649
[('file', 'locations', self.tree.basedir, 'locations'),
3650
('file', 'branch', 'DEFAULT', 'branch'),],
3654
class TestConfigGetSections(tests.TestCaseWithTransport):
3657
super(TestConfigGetSections, self).setUp()
3658
create_configs(self)
3660
def assertSectionNames(self, expected, conf, name=None):
3661
"""Check which sections are returned for a given config.
3663
If fallback configurations exist their sections can be included.
3665
:param expected: A list of section names.
3667
:param conf: The configuration that will be queried.
3669
:param name: An optional section name that will be passed to
3672
sections = list(conf._get_sections(name))
3673
self.assertLength(len(expected), sections)
3674
self.assertEqual(expected, [name for name, _, _ in sections])
3676
def test_bazaar_default_section(self):
3677
self.assertSectionNames(['DEFAULT'], self.bazaar_config)
3679
def test_locations_default_section(self):
3680
# No sections are defined in an empty file
3681
self.assertSectionNames([], self.locations_config)
3683
def test_locations_named_section(self):
3684
self.locations_config.set_user_option('file', 'locations')
3685
self.assertSectionNames([self.tree.basedir], self.locations_config)
3687
def test_locations_matching_sections(self):
3688
loc_config = self.locations_config
3689
loc_config.set_user_option('file', 'locations')
3690
# We need to cheat a bit here to create an option in sections above and
3691
# below the 'location' one.
3692
parser = loc_config._get_parser()
3693
# locations.cong deals with '/' ignoring native os.sep
3694
location_names = self.tree.basedir.split('/')
3695
parent = '/'.join(location_names[:-1])
3696
child = '/'.join(location_names + ['child'])
3698
parser[parent]['file'] = 'parent'
3700
parser[child]['file'] = 'child'
3701
self.assertSectionNames([self.tree.basedir, parent], loc_config)
3703
def test_branch_data_default_section(self):
3704
self.assertSectionNames([None],
3705
self.branch_config._get_branch_data_config())
3707
def test_branch_default_sections(self):
3708
# No sections are defined in an empty locations file
3709
self.assertSectionNames([None, 'DEFAULT'],
3711
# Unless we define an option
3712
self.branch_config._get_location_config().set_user_option(
3713
'file', 'locations')
3714
self.assertSectionNames([self.tree.basedir, None, 'DEFAULT'],
3717
def test_bazaar_named_section(self):
3718
# We need to cheat as the API doesn't give direct access to sections
3719
# other than DEFAULT.
3720
self.bazaar_config.set_alias('bazaar', 'bzr')
3721
self.assertSectionNames(['ALIASES'], self.bazaar_config, 'ALIASES')
1315
3724
class TestAuthenticationConfigFile(tests.TestCase):
1316
3725
"""Test the authentication.conf file matching"""