# 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()