# Copyright 2017 The Abseil Authors.
#
# 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.
"""Unit tests for the XML-format help generated by the flags.py module."""
import enum
import io
import os
import string
import sys
import xml.dom.minidom
import xml.sax.saxutils
from absl import flags
from absl.flags import _helpers
from absl.flags.tests import module_bar
from absl.testing import absltest
class CreateXMLDOMElement(absltest.TestCase):
def _check(self, name, value, expected_output):
doc = xml.dom.minidom.Document()
node = _helpers.create_xml_dom_element(doc, name, value)
output = node.toprettyxml(' ', encoding='utf-8')
self.assertEqual(expected_output, output)
def test_create_xml_dom_element(self):
self._check('tag', '', b'\n')
self._check('tag', 'plain text', b'plain text\n')
self._check('tag', '(x < y) && (a >= b)',
b'(x < y) && (a >= b)\n')
# If the value is bytes with invalid unicode:
bytes_with_invalid_unicodes = b'\x81\xff'
# In python 3 the string representation is "b'\x81\xff'" so they are kept
# as "b'\x81\xff'".
self._check('tag', bytes_with_invalid_unicodes,
b"b'\\x81\\xff'\n")
# Some unicode chars are illegal in xml
# (http://www.w3.org/TR/REC-xml/#charsets):
self._check('tag', u'\x0b\x02\x08\ufffe', b'\n')
# Valid unicode will be encoded:
self._check('tag', u'\xff', b'\xc3\xbf\n')
def _list_separators_in_xmlformat(separators, indent=''):
"""Generates XML encoding of a list of list separators.
Args:
separators: A list of list separators. Usually, this should be a
string whose characters are the valid list separators, e.g., ','
means that both comma (',') and space (' ') are valid list
separators.
indent: A string that is added at the beginning of each generated
XML element.
Returns:
A string.
"""
result = ''
separators = list(separators)
separators.sort()
for sep_char in separators:
result += ('%s%s\n' %
(indent, repr(sep_char)))
return result
class FlagCreateXMLDOMElement(absltest.TestCase):
"""Test the create_xml_dom_element method for a single flag at a time.
There is one test* method for each kind of DEFINE_* declaration.
"""
def setUp(self):
# self.fv is a FlagValues object, just like flags.FLAGS. Each
# test registers one flag with this FlagValues.
self.fv = flags.FlagValues()
def _check_flag_help_in_xml(self, flag_name, module_name,
expected_output, is_key=False):
flag_obj = self.fv[flag_name]
doc = xml.dom.minidom.Document()
element = flag_obj._create_xml_dom_element(doc, module_name, is_key=is_key)
output = element.toprettyxml(indent=' ')
self.assertMultiLineEqual(expected_output, output)
def test_flag_help_in_xml_int(self):
flags.DEFINE_integer('index', 17, 'An integer flag', flag_values=self.fv)
expected_output_pattern = (
'\n'
' module.name\n'
' index\n'
' An integer flag\n'
' 17\n'
' %d\n'
' int\n'
'\n')
self._check_flag_help_in_xml('index', 'module.name',
expected_output_pattern % 17)
# Check that the output is correct even when the current value of
# a flag is different from the default one.
self.fv['index'].value = 20
self._check_flag_help_in_xml('index', 'module.name',
expected_output_pattern % 20)
def test_flag_help_in_xml_int_with_bounds(self):
flags.DEFINE_integer('nb_iters', 17, 'An integer flag',
lower_bound=5, upper_bound=27,
flag_values=self.fv)
expected_output = (
'\n'
' yes\n'
' module.name\n'
' nb_iters\n'
' An integer flag\n'
' 17\n'
' 17\n'
' int\n'
' 5\n'
' 27\n'
'\n')
self._check_flag_help_in_xml('nb_iters', 'module.name', expected_output,
is_key=True)
def test_flag_help_in_xml_string(self):
flags.DEFINE_string('file_path', '/path/to/my/dir', 'A test string flag.',
flag_values=self.fv)
expected_output = (
'\n'
' simple_module\n'
' file_path\n'
' A test string flag.\n'
' /path/to/my/dir\n'
' /path/to/my/dir\n'
' string\n'
'\n')
self._check_flag_help_in_xml('file_path', 'simple_module', expected_output)
def test_flag_help_in_xml_string_with_xmlillegal_chars(self):
flags.DEFINE_string('file_path', '/path/to/\x08my/dir',
'A test string flag.', flag_values=self.fv)
# '\x08' is not a legal character in XML 1.0 documents. Our
# current code purges such characters from the generated XML.
expected_output = (
'\n'
' simple_module\n'
' file_path\n'
' A test string flag.\n'
' /path/to/my/dir\n'
' /path/to/my/dir\n'
' string\n'
'\n')
self._check_flag_help_in_xml('file_path', 'simple_module', expected_output)
def test_flag_help_in_xml_boolean(self):
flags.DEFINE_boolean('use_gpu', False, 'Use gpu for performance.',
flag_values=self.fv)
expected_output = (
'\n'
' yes\n'
' a_module\n'
' use_gpu\n'
' Use gpu for performance.\n'
' false\n'
' false\n'
' bool\n'
'\n')
self._check_flag_help_in_xml('use_gpu', 'a_module', expected_output,
is_key=True)
def test_flag_help_in_xml_enum(self):
flags.DEFINE_enum('cc_version', 'stable', ['stable', 'experimental'],
'Compiler version to use.', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' cc_version\n'
' <stable|experimental>: '
'Compiler version to use.\n'
' stable\n'
' stable\n'
' string enum\n'
' stable\n'
' experimental\n'
'\n')
self._check_flag_help_in_xml('cc_version', 'tool', expected_output)
def test_flag_help_in_xml_enum_class(self):
class Version(enum.Enum):
STABLE = 0
EXPERIMENTAL = 1
flags.DEFINE_enum_class('cc_version', 'STABLE', Version,
'Compiler version to use.', flag_values=self.fv)
expected_output = ('\n'
' tool\n'
' cc_version\n'
' <stable|experimental>: '
'Compiler version to use.\n'
' stable\n'
' Version.STABLE\n'
' enum class\n'
' STABLE\n'
' EXPERIMENTAL\n'
'\n')
self._check_flag_help_in_xml('cc_version', 'tool', expected_output)
def test_flag_help_in_xml_comma_separated_list(self):
flags.DEFINE_list('files', 'a.cc,a.h,archive/old.zip',
'Files to process.', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' files\n'
' Files to process.\n'
' a.cc,a.h,archive/old.zip\n'
' [\'a.cc\', \'a.h\', \'archive/old.zip\']\n'
' comma separated list of strings\n'
' \',\'\n'
'\n')
self._check_flag_help_in_xml('files', 'tool', expected_output)
def test_list_as_default_argument_comma_separated_list(self):
flags.DEFINE_list('allow_users', ['alice', 'bob'],
'Users with access.', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' allow_users\n'
' Users with access.\n'
' alice,bob\n'
' [\'alice\', \'bob\']\n'
' comma separated list of strings\n'
' \',\'\n'
'\n')
self._check_flag_help_in_xml('allow_users', 'tool', expected_output)
def test_none_as_default_arguments_comma_separated_list(self):
flags.DEFINE_list('allow_users', None,
'Users with access.', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' allow_users\n'
' Users with access.\n'
' \n'
' None\n'
' comma separated list of strings\n'
' \',\'\n'
'\n')
self._check_flag_help_in_xml('allow_users', 'tool', expected_output)
def test_flag_help_in_xml_space_separated_list(self):
flags.DEFINE_spaceseplist('dirs', 'src libs bin',
'Directories to search.', flag_values=self.fv)
expected_separators = sorted(string.whitespace)
expected_output = (
'\n'
' tool\n'
' dirs\n'
' Directories to search.\n'
' src libs bin\n'
' [\'src\', \'libs\', \'bin\']\n'
' whitespace separated list of strings\n'
'LIST_SEPARATORS'
'\n').replace('LIST_SEPARATORS',
_list_separators_in_xmlformat(expected_separators,
indent=' '))
self._check_flag_help_in_xml('dirs', 'tool', expected_output)
def test_flag_help_in_xml_space_separated_list_with_comma_compat(self):
flags.DEFINE_spaceseplist('dirs', 'src libs,bin',
'Directories to search.', comma_compat=True,
flag_values=self.fv)
expected_separators = sorted(string.whitespace + ',')
expected_output = (
'\n'
' tool\n'
' dirs\n'
' Directories to search.\n'
' src libs bin\n'
' [\'src\', \'libs\', \'bin\']\n'
' whitespace or comma separated list of strings\n'
'LIST_SEPARATORS'
'\n').replace('LIST_SEPARATORS',
_list_separators_in_xmlformat(expected_separators,
indent=' '))
self._check_flag_help_in_xml('dirs', 'tool', expected_output)
def test_flag_help_in_xml_multi_string(self):
flags.DEFINE_multi_string('to_delete', ['a.cc', 'b.h'],
'Files to delete', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' to_delete\n'
' Files to delete;\n'
' repeat this option to specify a list of values\n'
' [\'a.cc\', \'b.h\']\n'
' [\'a.cc\', \'b.h\']\n'
' multi string\n'
'\n')
self._check_flag_help_in_xml('to_delete', 'tool', expected_output)
def test_flag_help_in_xml_multi_int(self):
flags.DEFINE_multi_integer('cols', [5, 7, 23],
'Columns to select', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' cols\n'
' Columns to select;\n '
'repeat this option to specify a list of values\n'
' [5, 7, 23]\n'
' [5, 7, 23]\n'
' multi int\n'
'\n')
self._check_flag_help_in_xml('cols', 'tool', expected_output)
def test_flag_help_in_xml_multi_enum(self):
flags.DEFINE_multi_enum('flavours', ['APPLE', 'BANANA'],
['APPLE', 'BANANA', 'CHERRY'],
'Compilation flavour.', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' flavours\n'
' <APPLE|BANANA|CHERRY>: Compilation flavour.;\n'
' repeat this option to specify a list of values\n'
' [\'APPLE\', \'BANANA\']\n'
' [\'APPLE\', \'BANANA\']\n'
' multi string enum\n'
' APPLE\n'
' BANANA\n'
' CHERRY\n'
'\n')
self._check_flag_help_in_xml('flavours', 'tool', expected_output)
def test_flag_help_in_xml_multi_enum_class_singleton_default(self):
class Fruit(enum.Enum):
ORANGE = 0
BANANA = 1
flags.DEFINE_multi_enum_class('fruit', ['ORANGE'],
Fruit,
'The fruit flag.', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' fruit\n'
' <orange|banana>: The fruit flag.;\n'
' repeat this option to specify a list of values\n'
' orange\n'
' orange\n'
' multi enum class\n'
' ORANGE\n'
' BANANA\n'
'\n')
self._check_flag_help_in_xml('fruit', 'tool', expected_output)
def test_flag_help_in_xml_multi_enum_class_list_default(self):
class Fruit(enum.Enum):
ORANGE = 0
BANANA = 1
flags.DEFINE_multi_enum_class('fruit', ['ORANGE', 'BANANA'],
Fruit,
'The fruit flag.', flag_values=self.fv)
expected_output = (
'\n'
' tool\n'
' fruit\n'
' <orange|banana>: The fruit flag.;\n'
' repeat this option to specify a list of values\n'
' orange,banana\n'
' orange,banana\n'
' multi enum class\n'
' ORANGE\n'
' BANANA\n'
'\n')
self._check_flag_help_in_xml('fruit', 'tool', expected_output)
# The next EXPECTED_HELP_XML_* constants are parts of a template for
# the expected XML output from WriteHelpInXMLFormatTest below. When
# we assemble these parts into a single big string, we'll take into
# account the ordering between the name of the main module and the
# name of module_bar. Next, we'll fill in the docstring for this
# module (%(usage_doc)s), the name of the main module
# (%(main_module_name)s) and the name of the module module_bar
# (%(module_bar_name)s). See WriteHelpInXMLFormatTest below.
EXPECTED_HELP_XML_START = """\
%(basename_of_argv0)s
%(usage_doc)s
"""
EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE = """\
yes
%(main_module_name)s
allow_users
Users with access.
alice,bob
['alice', 'bob']
comma separated list of strings
','
yes
%(main_module_name)s
cc_version
<stable|experimental>: Compiler version to use.
stable
stable
string enum
stable
experimental
yes
%(main_module_name)s
cols
Columns to select;
repeat this option to specify a list of values
[5, 7, 23]
[5, 7, 23]
multi int
yes
%(main_module_name)s
dirs
Directories to create.
src libs bins
['src', 'libs', 'bins']
whitespace separated list of strings
%(whitespace_separators)s
yes
%(main_module_name)s
file_path
A test string flag.
/path/to/my/dir
/path/to/my/dir
string
yes
%(main_module_name)s
files
Files to process.
a.cc,a.h,archive/old.zip
['a.cc', 'a.h', 'archive/old.zip']
comma separated list of strings
\',\'
yes
%(main_module_name)s
flavours
<APPLE|BANANA|CHERRY>: Compilation flavour.;
repeat this option to specify a list of values
['APPLE', 'BANANA']
['APPLE', 'BANANA']
multi string enum
APPLE
BANANA
CHERRY
yes
%(main_module_name)s
index
An integer flag
17
17
int
yes
%(main_module_name)s
nb_iters
An integer flag
17
17
int
5
27
yes
%(main_module_name)s
to_delete
Files to delete;
repeat this option to specify a list of values
['a.cc', 'b.h']
['a.cc', 'b.h']
multi string
yes
%(main_module_name)s
use_gpu
Use gpu for performance.
false
false
bool
"""
EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR = """\
%(module_bar_name)s
tmod_bar_t
Sample int flag.
4
4
int
yes
%(module_bar_name)s
tmod_bar_u
Sample int flag.
5
5
int
%(module_bar_name)s
tmod_bar_v
Sample int flag.
6
6
int
%(module_bar_name)s
tmod_bar_x
Boolean flag.
true
true
bool
%(module_bar_name)s
tmod_bar_y
String flag.
default
default
string
yes
%(module_bar_name)s
tmod_bar_z
Another boolean flag from module bar.
false
false
bool
"""
EXPECTED_HELP_XML_END = """\
"""
class WriteHelpInXMLFormatTest(absltest.TestCase):
"""Big test of FlagValues.write_help_in_xml_format, with several flags."""
def test_write_help_in_xmlformat(self):
fv = flags.FlagValues()
# Since these flags are defined by the top module, they are all key.
flags.DEFINE_integer('index', 17, 'An integer flag', flag_values=fv)
flags.DEFINE_integer('nb_iters', 17, 'An integer flag',
lower_bound=5, upper_bound=27, flag_values=fv)
flags.DEFINE_string('file_path', '/path/to/my/dir', 'A test string flag.',
flag_values=fv)
flags.DEFINE_boolean('use_gpu', False, 'Use gpu for performance.',
flag_values=fv)
flags.DEFINE_enum('cc_version', 'stable', ['stable', 'experimental'],
'Compiler version to use.', flag_values=fv)
flags.DEFINE_list('files', 'a.cc,a.h,archive/old.zip',
'Files to process.', flag_values=fv)
flags.DEFINE_list('allow_users', ['alice', 'bob'],
'Users with access.', flag_values=fv)
flags.DEFINE_spaceseplist('dirs', 'src libs bins',
'Directories to create.', flag_values=fv)
flags.DEFINE_multi_string('to_delete', ['a.cc', 'b.h'],
'Files to delete', flag_values=fv)
flags.DEFINE_multi_integer('cols', [5, 7, 23],
'Columns to select', flag_values=fv)
flags.DEFINE_multi_enum('flavours', ['APPLE', 'BANANA'],
['APPLE', 'BANANA', 'CHERRY'],
'Compilation flavour.', flag_values=fv)
# Define a few flags in a different module.
module_bar.define_flags(flag_values=fv)
# And declare only a few of them to be key. This way, we have
# different kinds of flags, defined in different modules, and not
# all of them are key flags.
flags.declare_key_flag('tmod_bar_z', flag_values=fv)
flags.declare_key_flag('tmod_bar_u', flag_values=fv)
# Generate flag help in XML format in the StringIO sio.
sio = io.StringIO()
fv.write_help_in_xml_format(sio)
# Check that we got the expected result.
expected_output_template = EXPECTED_HELP_XML_START
main_module_name = sys.argv[0]
module_bar_name = module_bar.__name__
if main_module_name < module_bar_name:
expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE
expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR
else:
expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR
expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE
expected_output_template += EXPECTED_HELP_XML_END
# XML representation of the whitespace list separators.
whitespace_separators = _list_separators_in_xmlformat(string.whitespace,
indent=' ')
expected_output = (
expected_output_template %
{'basename_of_argv0': os.path.basename(sys.argv[0]),
'usage_doc': sys.modules['__main__'].__doc__,
'main_module_name': main_module_name,
'module_bar_name': module_bar_name,
'whitespace_separators': whitespace_separators})
actual_output = sio.getvalue()
self.assertMultiLineEqual(expected_output, actual_output)
# Also check that our result is valid XML. minidom.parseString
# throws an xml.parsers.expat.ExpatError in case of an error.
xml.dom.minidom.parseString(actual_output)
if __name__ == '__main__':
absltest.main()