#!/usr/bin/env python # Copyright (c) 2009, Google Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Unit tests for the XML-format help generated by the gflags.py module.""" __author__ = 'Alex Salcianu' import string import StringIO import sys import unittest import xml.dom.minidom import xml.sax.saxutils # We use the name 'flags' internally in this test, for historical reasons. # Don't do this yourself! :-) Just do 'import gflags; FLAGS=gflags.FLAGS; etc' import gflags as flags # For historic reasons, we use the name module_bar instead of test_module_bar import test_module_bar as module_bar def MultiLineEqual(expected_help, help): """Returns True if expected_help == help. Otherwise returns False and logs the difference in a human-readable way. """ if help == expected_help: return True print "Error: FLAGS.MainModuleHelp() didn't return the expected result." print "Got:" print help print "[End of got]" help_lines = help.split('\n') expected_help_lines = expected_help.split('\n') num_help_lines = len(help_lines) num_expected_help_lines = len(expected_help_lines) if num_help_lines != num_expected_help_lines: print "Number of help lines = %d, expected %d" % ( num_help_lines, num_expected_help_lines) num_to_match = min(num_help_lines, num_expected_help_lines) for i in range(num_to_match): if help_lines[i] != expected_help_lines[i]: print "One discrepancy: Got:" print help_lines[i] print "Expected:" print expected_help_lines[i] break else: # If we got here, found no discrepancy, print first new line. if num_help_lines > num_expected_help_lines: print "New help line:" print help_lines[num_expected_help_lines] elif num_expected_help_lines > num_help_lines: print "Missing expected help line:" print expected_help_lines[num_help_lines] else: print "Bug in this test -- discrepancy detected but not found." return False class _MakeXMLSafeTest(unittest.TestCase): def _Check(self, s, expected_output): self.assertEqual(flags._MakeXMLSafe(s), expected_output) def testMakeXMLSafe(self): self._Check('plain text', 'plain text') self._Check('(x < y) && (a >= b)', '(x < y) && (a >= b)') # Some characters with ASCII code < 32 are illegal in XML 1.0 and # are removed by us. However, '\n', '\t', and '\r' are legal. self._Check('\x09\x0btext \x02 with\x0dsome \x08 good & bad chars', '\ttext with\rsome good & bad chars') def _ListSeparatorsInXMLFormat(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 WriteFlagHelpInXMLFormatTest(unittest.TestCase): """Test the XML-format help 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 assertMultiLineEqual(self, expected, actual): self.assert_(MultiLineEqual(expected, actual)) def _CheckFlagHelpInXML(self, flag_name, module_name, expected_output, is_key=False): # StringIO.StringIO is a file object that writes into a memory string. sio = StringIO.StringIO() flag_obj = self.fv[flag_name] flag_obj.WriteInfoInXMLFormat(sio, module_name, is_key=is_key, indent=' ') self.assertMultiLineEqual(sio.getvalue(), expected_output) sio.close() def testFlagHelpInXML_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._CheckFlagHelpInXML('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._CheckFlagHelpInXML('index', 'module.name', expected_output_pattern % 20) def testFlagHelpInXML_IntWithBounds(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._CheckFlagHelpInXML('nb_iters', 'module.name', expected_output, is_key=True) def testFlagHelpInXML_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._CheckFlagHelpInXML('file_path', 'simple_module', expected_output) def testFlagHelpInXML_StringWithXMLIllegalChars(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._CheckFlagHelpInXML('file_path', 'simple_module', expected_output) def testFlagHelpInXML_Boolean(self): flags.DEFINE_boolean('use_hack', False, 'Use performance hack', flag_values=self.fv) expected_output = ( ' \n' ' yes\n' ' a_module\n' ' use_hack\n' ' Use performance hack\n' ' false\n' ' false\n' ' bool\n' ' \n') self._CheckFlagHelpInXML('use_hack', 'a_module', expected_output, is_key=True) def testFlagHelpInXML_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._CheckFlagHelpInXML('cc_version', 'tool', expected_output) def testFlagHelpInXML_CommaSeparatedList(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._CheckFlagHelpInXML('files', 'tool', expected_output) def testFlagHelpInXML_SpaceSeparatedList(self): flags.DEFINE_spaceseplist('dirs', 'src libs bin', 'Directories to search.', flag_values=self.fv) 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', _ListSeparatorsInXMLFormat(string.whitespace, indent=' ')) self._CheckFlagHelpInXML('dirs', 'tool', expected_output) def testFlagHelpInXML_MultiString(self): flags.DEFINE_multistring('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._CheckFlagHelpInXML('to_delete', 'tool', expected_output) def testFlagHelpInXML_MultiInt(self): flags.DEFINE_multi_int('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._CheckFlagHelpInXML('cols', '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. # # NOTE: given the current implementation of _GetMainModule(), we # already know the ordering between the main module and module_bar. # However, there is no guarantee that _GetMainModule will never be # changed in the future (especially since it's far from perfect). EXPECTED_HELP_XML_START = """\ gflags_helpxml_test.py %(usage_doc)s """ EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE = """\ 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 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_hack Use performance hack 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(unittest.TestCase): """Big test of FlagValues.WriteHelpInXMLFormat, with several flags.""" def assertMultiLineEqual(self, expected, actual): self.assert_(MultiLineEqual(expected, actual)) def testWriteHelpInXMLFormat(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_hack', False, 'Use performance hack', 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_spaceseplist('dirs', 'src libs bins', 'Directories to create.', flag_values=fv) flags.DEFINE_multistring('to_delete', ['a.cc', 'b.h'], 'Files to delete', flag_values=fv) flags.DEFINE_multi_int('cols', [5, 7, 23], 'Columns to select', flag_values=fv) # Define a few flags in a different module. module_bar.DefineFlags(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 = StringIO.StringIO() fv.WriteHelpInXMLFormat(sio) # Check that we got the expected result. expected_output_template = EXPECTED_HELP_XML_START main_module_name = flags._GetMainModule() 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 = _ListSeparatorsInXMLFormat(string.whitespace, indent=' ') expected_output = ( expected_output_template % {'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(actual_output, expected_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__': unittest.main()