Handle --flagfile by converting to .ini style

Add code to translate flagfiles into .ini config files so that we can
deprecate and, eventually remove, --flagfile.

By using the flagfile compat code, we no longer need to process the
contents of flagfiles as CLI options and we can reduce the number
of CLI options to a sane set:

  --verbose
  --logdir
  --logfile
  --use_syslog
  --use_stderr
  --connection_type
  --sql_connection
  --api_paste_config
  --state_path
  --lock_path
  --fake_network
  --fake_rabbit

This, in turn, means we can remove the evil hacks which we needed in
order to register CLI options after the initial parsing of the command
line.

Change-Id: I9e24008fa634d7c8378b253c1f7a6d2169076086
This commit is contained in:
Mark McLoughlin
2012-02-03 00:50:58 +00:00
parent 67307bb7fe
commit 393e28bd3d
6 changed files with 465 additions and 139 deletions

15
nova/compat/__init__.py Normal file
View File

@@ -0,0 +1,15 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 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.

182
nova/compat/flagfile.py Normal file
View File

@@ -0,0 +1,182 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 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 contextlib
import os
import shutil
import tempfile
'''
Compatibility code for handling the deprecated --flagfile option.
gflags style configuration files are deprecated and will be removed in future.
The code in this module transles --flagfile options into --config-file and can
be removed when support for --flagfile is removed.
'''
def _get_flagfile(argp):
'''Parse the filename from a --flagfile argument.
The current and next arguments are passed as a 2 item list. If the
flagfile filename is in the next argument, the two arguments are
joined into the first item while the second item is set to None.
'''
i = argp[0].find('-flagfile')
if i < 0:
return None
# Accept -flagfile or -flagfile
if i != 0 and (i != 1 or argp[0][i] != '-'):
return None
i += len('-flagfile')
if i == len(argp[0]): # Accept [-]-flagfile foo
argp[0] += '=' + argp[1]
argp[1] = None
if argp[0][i] != '=': # Accept [-]-flagfile=foo
return None
return argp[0][i + 1:]
def _open_file_for_reading(path):
'''Helper method which test code may stub out.'''
return open(path, 'r')
def _open_fd_for_writing(fd, _path):
'''Helper method which test code may stub out.'''
return os.fdopen(fd, 'w')
def _read_lines(flagfile):
'''Read a flag file, returning all lines with comments stripped.'''
with _open_file_for_reading(flagfile) as f:
lines = f.readlines()
ret = []
for l in lines:
if l.isspace() or l.startswith('#') or l.startswith('//'):
continue
ret.append(l.strip())
return ret
def _read_flagfile(arg, next_arg, tempdir=None):
'''Convert a --flagfile argument to --config-file.
If the supplied argument is a --flagfile argument, read the contents
of the file and convert it to a .ini format config file. Return a
--config-file argument with the converted file.
If the flag file contains more --flagfile arguments, multiple
--config-file arguments will be returned.
The returned argument list may also contain None values which should
be filtered out later.
'''
argp = [arg, next_arg]
flagfile = _get_flagfile(argp)
if not flagfile:
return argp
args = _read_lines(flagfile)
#
# We're recursing here to convert any --flagfile arguments
# read from this flagfile into --config-file arguments
#
# We don't actually include those --config-file arguments
# in the generated config file; instead we include all those
# --config-file args in the final command line
#
args = _iterate_args(args, _read_flagfile, tempdir=tempdir)
config_file_args = []
(fd, tmpconf) = tempfile.mkstemp(suffix='.conf', dir=tempdir)
with _open_fd_for_writing(fd, tmpconf) as f:
f.write('[DEFAULT]\n')
for arg in args:
if arg.startswith('--config-file='):
config_file_args.append(arg)
continue
if '=' in arg:
f.write(arg[2:] + '\n')
elif arg[2:].startswith('no'):
f.write(arg[4:] + '=false\n')
else:
f.write(arg[2:] + '=true\n')
return ['--config-file=' + tmpconf] + argp[1:] + config_file_args
def _iterate_args(args, iterator, **kwargs):
'''Run an iterator function on the supplied args list.
The iterator is passed the current arg and next arg and returns a
list of args. The returned args replace the suppied args in the
resulting args list.
The iterator will be passed None for the next arg when processing
the last arg.
'''
args.append(None)
ret = []
for i in range(len(args)):
if args[i] is None: # last item, or consumed file name
continue
modified = iterator(args[i], args[i + 1], **kwargs)
args[i], args[i + 1] = modified[:2]
ret.extend(modified[:1] + modified[2:]) # don't append next arg
return filter(None, ret)
def handle_flagfiles(args, tempdir=None):
'''Replace --flagfile arguments with --config-file arguments.
Replace any --flagfile argument in the supplied list with a --config-file
argument containing a temporary config file with the contents of the flag
file translated to .ini format.
The tempdir argument is a directory which will be used to create temporary
files.
'''
return _iterate_args(args[:], _read_flagfile, tempdir=tempdir)
@contextlib.contextmanager
def handle_flagfiles_managed(args):
'''A context manager for handle_flagfiles() which removes temp files.
For use with the 'with' statement, i.e.
with handle_flagfiles_managed(args) as args:
# Do stuff
# Any temporary fils have been removed
'''
tempdir = tempfile.mkdtemp(prefix='nova-conf-')
try:
yield handle_flagfiles(args, tempdir=tempdir)
finally:
shutil.rmtree(tempdir)

View File

@@ -32,6 +32,7 @@ import sys
import gflags
from nova.compat import flagfile
from nova.openstack.common import cfg
@@ -46,62 +47,17 @@ class FlagValues(object):
if self._update_default:
self._update_default(self.name, default)
class ErrorCatcher:
def __init__(self, orig_error):
self.orig_error = orig_error
self.reset()
def reset(self):
self._error_msg = None
def catch(self, msg):
if ": --" in msg:
self._error_msg = msg
else:
self.orig_error(msg)
def get_unknown_arg(self, args):
if not self._error_msg:
return None
# Error message is e.g. "no such option: --runtime_answer"
a = self._error_msg[self._error_msg.rindex(": --") + 2:]
return filter(lambda i: i == a or i.startswith(a + "="), args)[0]
def __init__(self):
self._conf = cfg.ConfigOpts()
self._conf.disable_interspersed_args()
self._opts = {}
self.Reset()
def _parse(self):
if self._extra is not None:
return
args = gflags.FlagValues().ReadFlagsFromFiles(self._args)
extra = None
#
# This horrendous hack allows us to stop optparse
# exiting when it encounters an unknown option
#
error_catcher = self.ErrorCatcher(self._conf._oparser.error)
self._conf._oparser.error = error_catcher.catch
try:
while True:
error_catcher.reset()
extra = self._conf(args)
unknown = error_catcher.get_unknown_arg(args)
if not unknown:
break
args.remove(unknown)
finally:
self._conf._oparser.error = error_catcher.orig_error
self._extra = extra
with flagfile.handle_flagfiles_managed(self._args) as args:
self._extra = self._conf(args)
def __call__(self, argv):
self.Reset()
@@ -152,21 +108,16 @@ class FlagValues(object):
return ret
def add_option(self, opt):
if opt.dest in self._conf:
return
self._opts[opt.dest] = opt
try:
self._conf.register_cli_opts(self._opts.values())
except cfg.ArgsAlreadyParsedError:
self._conf.reset()
self._conf.register_cli_opts(self._opts.values())
self._extra = None
self._conf.register_opt(opt)
def add_options(self, opts):
for opt in opts:
self.add_option(opt)
self._conf.register_opts(opts)
def add_cli_option(self, opt):
self._conf.register_cli_opt(opt)
def add_cli_options(self, opts):
self._conf.register_cli_opts(opts)
FLAGS = FlagValues()
@@ -195,6 +146,55 @@ def _get_my_ip():
return "127.0.0.1"
log_opts = [
cfg.BoolOpt('verbose',
default=False,
help='show debug output'),
cfg.StrOpt('logdir',
default=None,
help='output to a per-service log file in named directory'),
cfg.StrOpt('logfile',
default=None,
help='output to named file'),
cfg.BoolOpt('use_syslog',
default=False,
help='output to syslog'),
cfg.BoolOpt('use_stderr',
default=True,
help='log to standard error'),
]
core_opts = [
cfg.StrOpt('connection_type',
default=None,
help='libvirt, xenapi or fake'),
cfg.StrOpt('sql_connection',
default='sqlite:///$state_path/$sqlite_db',
help='connection string for sql database'),
cfg.StrOpt('api_paste_config',
default="api-paste.ini",
help='File name for the paste.deploy config for nova-api'),
cfg.StrOpt('state_path',
default=os.path.join(os.path.dirname(__file__), '../'),
help="Top-level directory for maintaining nova's state"),
cfg.StrOpt('lock_path',
default=os.path.join(os.path.dirname(__file__), '../'),
help='Directory for lock files'),
]
debug_opts = [
cfg.BoolOpt('fake_network',
default=False,
help='should we use fake network devices and addresses'),
cfg.BoolOpt('fake_rabbit',
default=False,
help='use a fake rabbit'),
]
FLAGS.add_cli_options(log_opts)
FLAGS.add_cli_options(core_opts)
FLAGS.add_cli_options(debug_opts)
global_opts = [
cfg.StrOpt('my_ip',
default=_get_my_ip(),
@@ -202,9 +202,6 @@ global_opts = [
cfg.ListOpt('region_list',
default=[],
help='list of region=fqdn pairs separated by commas'),
cfg.StrOpt('connection_type',
default=None,
help='libvirt, xenapi or fake'),
cfg.StrOpt('aws_access_key_id',
default='admin',
help='AWS Access ID'),
@@ -262,15 +259,6 @@ global_opts = [
cfg.StrOpt('vsa_topic',
default='vsa',
help='the topic that nova-vsa service listens on'),
cfg.BoolOpt('verbose',
default=False,
help='show debug output'),
cfg.BoolOpt('fake_rabbit',
default=False,
help='use a fake rabbit'),
cfg.BoolOpt('fake_network',
default=False,
help='should we use fake network devices and addresses'),
cfg.StrOpt('rabbit_host',
default='localhost',
help='rabbit host'),
@@ -377,15 +365,6 @@ global_opts = [
cfg.IntOpt('auth_token_ttl',
default=3600,
help='Seconds for auth tokens to linger'),
cfg.StrOpt('state_path',
default=os.path.join(os.path.dirname(__file__), '../'),
help="Top-level directory for maintaining nova's state"),
cfg.StrOpt('lock_path',
default=os.path.join(os.path.dirname(__file__), '../'),
help='Directory for lock files'),
cfg.StrOpt('logdir',
default=None,
help='output to a per-service log file in named directory'),
cfg.StrOpt('logfile_mode',
default='0644',
help='Default file mode of the logs.'),
@@ -395,9 +374,6 @@ global_opts = [
cfg.BoolOpt('sqlite_synchronous',
default=True,
help='Synchronous mode for sqlite'),
cfg.StrOpt('sql_connection',
default='sqlite:///$state_path/$sqlite_db',
help='connection string for sql database'),
cfg.IntOpt('sql_idle_timeout',
default=3600,
help='timeout for idle sql database connections'),

View File

@@ -76,18 +76,9 @@ log_opts = [
'eventlet.wsgi.server=WARN'
],
help='list of logger=LEVEL pairs'),
cfg.BoolOpt('use_syslog',
default=False,
help='output to syslog'),
cfg.BoolOpt('publish_errors',
default=False,
help='publish error events'),
cfg.StrOpt('logfile',
default=None,
help='output to named file'),
cfg.BoolOpt('use_stderr',
default=True,
help='log to standard error'),
]
FLAGS = flags.FLAGS

View File

@@ -0,0 +1,174 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 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 contextlib
import os
import shutil
import StringIO
import stubout
import textwrap
import tempfile
import unittest
import uuid
from nova.compat import flagfile
class ThatLastTwoPercentCoverageTestCase(unittest.TestCase):
def test_open_file_for_reading(self):
with flagfile._open_file_for_reading(__file__):
pass
def test_open_fd_for_writing(self):
(fd, path) = tempfile.mkstemp()
try:
with flagfile._open_fd_for_writing(fd, None):
pass
finally:
os.remove(path)
class CompatFlagfileTestCase(unittest.TestCase):
def setUp(self):
self.stubs = stubout.StubOutForTesting()
self.files = {}
self.tempdir = str(uuid.uuid4())
self.tempfiles = []
self.stubs.Set(flagfile, '_open_file_for_reading', self._fake_open)
self.stubs.Set(flagfile, '_open_fd_for_writing', self._fake_open)
self.stubs.Set(tempfile, 'mkdtemp', self._fake_mkdtemp)
self.stubs.Set(tempfile, 'mkstemp', self._fake_mkstemp)
self.stubs.Set(shutil, 'rmtree', self._fake_rmtree)
def tearDown(self):
self.stubs.UnsetAll()
def _fake_open(self, *args):
@contextlib.contextmanager
def managed_stringio(path):
if not path in self.files:
self.files[path] = ""
sio = StringIO.StringIO(textwrap.dedent(self.files[path]))
try:
yield sio
finally:
self.files[path] = sio.getvalue()
sio.close()
if len(args) == 2:
args = args[1:] # remove the fd arg for fdopen() case
return managed_stringio(args[0])
def _fake_mkstemp(self, *args, **kwargs):
self.assertTrue('dir' in kwargs)
self.assertEquals(kwargs['dir'], self.tempdir)
self.tempfiles.append(str(uuid.uuid4()))
return (None, self.tempfiles[-1])
def _fake_mkdtemp(self, *args, **kwargs):
return self.tempdir
def _fake_rmtree(self, path):
self.assertEquals(self.tempdir, path)
self.tempdir = None
def test_no_args(self):
before = []
after = flagfile.handle_flagfiles(before, tempdir=self.tempdir)
self.assertEquals(after, before)
def _do_test_empty_flagfile(self, before):
self.files['foo.flags'] = ''
after = flagfile.handle_flagfiles(before, tempdir=self.tempdir)
self.assertEquals(after, ['--config-file=' + self.tempfiles[-1]])
self.assertEquals(self.files[self.tempfiles[-1]], '[DEFAULT]\n')
def test_empty_flagfile(self):
self._do_test_empty_flagfile(['--flagfile=foo.flags'])
def test_empty_flagfile_separated(self):
self._do_test_empty_flagfile(['--flagfile', 'foo.flags'])
def test_empty_flagfile_single_hyphen(self):
self._do_test_empty_flagfile(['-flagfile=foo.flags'])
def test_empty_flagfile_single_hyphen_separated_separated(self):
self._do_test_empty_flagfile(['-flagfile', 'foo.flags'])
def test_empty_flagfile_with_other_args(self):
self.files['foo.flags'] = ''
before = [
'--foo', 'bar',
'--flagfile=foo.flags',
'--blaa=foo',
'--foo-flagfile',
'--flagfile-foo'
]
after = flagfile.handle_flagfiles(before, tempdir=self.tempdir)
self.assertEquals(after, [
'--foo', 'bar',
'--config-file=' + self.tempfiles[-1],
'--blaa=foo',
'--foo-flagfile',
'--flagfile-foo'])
self.assertEquals(self.files[self.tempfiles[-1]], '[DEFAULT]\n')
def _do_test_flagfile(self, flags, conf):
self.files['foo.flags'] = flags
before = ['--flagfile=foo.flags']
after = flagfile.handle_flagfiles(before, tempdir=self.tempdir)
self.assertEquals(after,
['--config-file=' + t
for t in reversed(self.tempfiles)])
self.assertEquals(self.files[self.tempfiles[-1]],
'[DEFAULT]\n' + conf)
def test_flagfile(self):
self._do_test_flagfile('--bar=foo', 'bar=foo\n')
def test_boolean_flag(self):
self._do_test_flagfile('--verbose', 'verbose=true\n')
def test_boolean_inverted_flag(self):
self._do_test_flagfile('--noverbose', 'verbose=false\n')
def test_flagfile_comments(self):
self._do_test_flagfile('--bar=foo\n#foo\n--foo=bar\n//bar',
'bar=foo\nfoo=bar\n')
def test_flagfile_nested(self):
self.files['bar.flags'] = '--foo=bar'
self._do_test_flagfile('--flagfile=bar.flags', '')
self.assertEquals(self.files[self.tempfiles[-2]],
'[DEFAULT]\nfoo=bar\n')
def test_flagfile_managed(self):
self.files['foo.flags'] = ''
before = ['--flagfile=foo.flags']
with flagfile.handle_flagfiles_managed(before) as after:
self.assertEquals(after, ['--config-file=' + self.tempfiles[-1]])
self.assertEquals(self.files[self.tempfiles[-1]], '[DEFAULT]\n')
self.assertTrue(self.tempdir is None)

View File

@@ -30,6 +30,17 @@ FLAGS.add_option(cfg.StrOpt('flags_unittest',
default='foo',
help='for testing purposes only'))
test_opts = [
cfg.StrOpt('string', default='default', help='desc'),
cfg.IntOpt('int', default=1, help='desc'),
cfg.BoolOpt('false', default=False, help='desc'),
cfg.BoolOpt('true', default=True, help='desc'),
]
float_opt = cfg.FloatOpt('float', default=6.66, help='desc')
multistr_opt = cfg.MultiStrOpt('multi', default=['blaa'], help='desc')
list_opt = cfg.ListOpt('list', default=['foo'], help='desc')
class FlagsTestCase(test.TestCase):
@@ -39,16 +50,7 @@ class FlagsTestCase(test.TestCase):
self.global_FLAGS = flags.FLAGS
def test_define(self):
self.assert_('string' not in self.FLAGS)
self.assert_('int' not in self.FLAGS)
self.assert_('false' not in self.FLAGS)
self.assert_('true' not in self.FLAGS)
self.FLAGS.add_option(cfg.StrOpt('string',
default='default', help='desc'))
self.FLAGS.add_option(cfg.IntOpt('int', default=1, help='desc'))
self.FLAGS.add_option(cfg.BoolOpt('false', default=False, help='desc'))
self.FLAGS.add_option(cfg.BoolOpt('true', default=True, help='desc'))
self.FLAGS.add_cli_options(test_opts)
self.assert_(self.FLAGS['string'])
self.assert_(self.FLAGS['int'])
@@ -72,12 +74,12 @@ class FlagsTestCase(test.TestCase):
self.assertEqual(self.FLAGS.true, False)
def test_define_float(self):
self.FLAGS.add_option(cfg.FloatOpt('float', default=6.66, help='desc'))
self.FLAGS.add_cli_options(test_opts)
self.FLAGS.add_option(float_opt)
self.assertEqual(self.FLAGS.float, 6.66)
def test_define_multistring(self):
self.FLAGS.add_option(cfg.MultiStrOpt('multi',
default=['blaa'], help='desc'))
self.FLAGS.add_cli_option(multistr_opt)
self.assert_(self.FLAGS['multi'])
self.assertEqual(self.FLAGS.multi, ['blaa'])
@@ -87,13 +89,8 @@ class FlagsTestCase(test.TestCase):
self.assertEqual(self.FLAGS.multi, ['foo', 'bar'])
# Re-parse to test multistring isn't append multiple times
self.FLAGS(argv + ['--unknown1', '--unknown2'])
self.assertEqual(self.FLAGS.multi, ['foo', 'bar'])
def test_define_list(self):
self.FLAGS.add_option(cfg.ListOpt('list',
default=['foo'], help='desc'))
self.FLAGS.add_cli_option(list_opt)
self.assert_(self.FLAGS['list'])
self.assertEqual(self.FLAGS.list, ['foo'])
@@ -104,11 +101,11 @@ class FlagsTestCase(test.TestCase):
self.assertEqual(self.FLAGS.list, ['a', 'b', 'c', 'd'])
def test_error(self):
self.FLAGS.add_option(cfg.IntOpt('error', default=1, help='desc'))
self.FLAGS.add_cli_option(float_opt)
self.assertEqual(self.FLAGS.error, 1)
self.assertEqual(self.FLAGS.float, 6.66)
argv = ['flags_test', '--error=foo']
argv = ['flags_test', '--float=foo']
self.assertRaises(exceptions.SystemExit, self.FLAGS, argv)
def test_declare(self):
@@ -133,30 +130,26 @@ class FlagsTestCase(test.TestCase):
def test_runtime_and_unknown_flags(self):
self.assert_('runtime_answer' not in self.global_FLAGS)
argv = ['flags_test', '--runtime_answer=60', 'extra_arg']
args = self.global_FLAGS(argv)
self.assertEqual(len(args), 2)
self.assertEqual(args[1], 'extra_arg')
self.assert_('runtime_answer' not in self.global_FLAGS)
import nova.tests.runtime_flags
self.assert_('runtime_answer' in self.global_FLAGS)
self.assertEqual(self.global_FLAGS.runtime_answer, 60)
self.assertEqual(self.global_FLAGS.runtime_answer, 54)
def test_long_vs_short_flags(self):
self.global_FLAGS.add_option(cfg.StrOpt('duplicate_answer_long',
default='val', help='desc'))
self.global_FLAGS.Reset()
self.global_FLAGS.add_cli_option(cfg.StrOpt('duplicate_answer_long',
default='val',
help='desc'))
argv = ['flags_test', '--duplicate_answer=60', 'extra_arg']
args = self.global_FLAGS(argv)
self.assert_('duplicate_answer' not in self.global_FLAGS)
self.assert_(self.global_FLAGS.duplicate_answer_long, 60)
self.global_FLAGS.add_option(cfg.IntOpt('duplicate_answer',
default=60, help='desc'))
self.global_FLAGS.Reset()
self.global_FLAGS.add_cli_option(cfg.IntOpt('duplicate_answer',
default=60,
help='desc'))
args = self.global_FLAGS(argv)
self.assertEqual(self.global_FLAGS.duplicate_answer, 60)
self.assertEqual(self.global_FLAGS.duplicate_answer_long, 'val')
@@ -182,19 +175,14 @@ class FlagsTestCase(test.TestCase):
self.assertEqual(FLAGS.FlagValuesDict()['flags_unittest'], 'foo')
def test_flagfile(self):
self.FLAGS.add_option(cfg.StrOpt('string',
default='default', help='desc'))
self.FLAGS.add_option(cfg.IntOpt('int', default=1, help='desc'))
self.FLAGS.add_option(cfg.BoolOpt('false', default=False, help='desc'))
self.FLAGS.add_option(cfg.BoolOpt('true', default=True, help='desc'))
self.FLAGS.add_option(cfg.MultiStrOpt('multi',
default=['blaa'], help='desc'))
self.FLAGS.add_options(test_opts)
self.FLAGS.add_option(multistr_opt)
(fd, path) = tempfile.mkstemp(prefix='nova', suffix='.flags')
try:
os.write(fd, '--string=foo\n--int=2\n--false\n--notrue\n')
os.write(fd, '--multi=foo\n--multi=bar\n')
os.write(fd, '--multi=foo\n') # FIXME(markmc): --multi=bar\n')
os.close(fd)
self.FLAGS(['flags_test', '--flagfile=' + path])
@@ -203,11 +191,11 @@ class FlagsTestCase(test.TestCase):
self.assertEqual(self.FLAGS.int, 2)
self.assertEqual(self.FLAGS.false, True)
self.assertEqual(self.FLAGS.true, False)
self.assertEqual(self.FLAGS.multi, ['foo', 'bar'])
self.assertEqual(self.FLAGS.multi, ['foo']) # FIXME(markmc): 'bar'
# Re-parse to test multistring isn't append multiple times
self.FLAGS(['flags_test', '--flagfile=' + path])
self.assertEqual(self.FLAGS.multi, ['foo', 'bar'])
self.assertEqual(self.FLAGS.multi, ['foo']) # FIXME(markmc): 'bar'
finally:
os.remove(path)