#!/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__ = 'salcianu@google.com (Alex Salcianu)'


import string
import StringIO
import sys
import xml.dom.minidom
import xml.sax.saxutils
import gflags_googletest as googletest
import gflags
from flags_modules_for_testing import module_bar


class _MakeXMLSafeTest(googletest.TestCase):

  def _Check(self, s, expected_output):
    self.assertEqual(gflags._MakeXMLSafe(s), expected_output)

  def testMakeXMLSafe(self):
    self._Check('plain text', 'plain text')
    self._Check('(x < y) && (a >= b)',
                '(x &lt; y) &amp;&amp; (a &gt;= 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 &amp; 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<list_separator>%s</list_separator>\n' %
               (indent, repr(sep_char)))
  return result


class WriteFlagHelpInXMLFormatTest(googletest.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 gflags.FLAGS.  Each
    # test registers one flag with this FlagValues.
    self.fv = gflags.FlagValues()

  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):
    gflags.DEFINE_integer('index', 17, 'An integer flag', flag_values=self.fv)
    expected_output_pattern = (
        ' <flag>\n'
        '   <file>module.name</file>\n'
        '   <name>index</name>\n'
        '   <meaning>An integer flag</meaning>\n'
        '   <default>17</default>\n'
        '   <current>%d</current>\n'
        '   <type>int</type>\n'
        ' </flag>\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):
    gflags.DEFINE_integer('nb_iters', 17, 'An integer flag',
                         lower_bound=5, upper_bound=27,
                         flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <key>yes</key>\n'
        '   <file>module.name</file>\n'
        '   <name>nb_iters</name>\n'
        '   <meaning>An integer flag</meaning>\n'
        '   <default>17</default>\n'
        '   <current>17</current>\n'
        '   <type>int</type>\n'
        '   <lower_bound>5</lower_bound>\n'
        '   <upper_bound>27</upper_bound>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('nb_iters', 'module.name',
                             expected_output, is_key=True)

  def testFlagHelpInXML_String(self):
    gflags.DEFINE_string('file_path', '/path/to/my/dir', 'A test string flag.',
                        flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <file>simple_module</file>\n'
        '   <name>file_path</name>\n'
        '   <meaning>A test string flag.</meaning>\n'
        '   <default>/path/to/my/dir</default>\n'
        '   <current>/path/to/my/dir</current>\n'
        '   <type>string</type>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('file_path', 'simple_module',
                             expected_output)

  def testFlagHelpInXML_StringWithXMLIllegalChars(self):
    gflags.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 = (
        ' <flag>\n'
        '   <file>simple_module</file>\n'
        '   <name>file_path</name>\n'
        '   <meaning>A test string flag.</meaning>\n'
        '   <default>/path/to/my/dir</default>\n'
        '   <current>/path/to/my/dir</current>\n'
        '   <type>string</type>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('file_path', 'simple_module',
                             expected_output)

  def testFlagHelpInXML_Boolean(self):
    gflags.DEFINE_boolean('use_hack', False, 'Use performance hack',
                         flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <key>yes</key>\n'
        '   <file>a_module</file>\n'
        '   <name>use_hack</name>\n'
        '   <meaning>Use performance hack</meaning>\n'
        '   <default>false</default>\n'
        '   <current>false</current>\n'
        '   <type>bool</type>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('use_hack', 'a_module',
                             expected_output, is_key=True)

  def testFlagHelpInXML_Enum(self):
    gflags.DEFINE_enum('cc_version', 'stable', ['stable', 'experimental'],
                      'Compiler version to use.', flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <file>tool</file>\n'
        '   <name>cc_version</name>\n'
        '   <meaning>&lt;stable|experimental&gt;: '
        'Compiler version to use.</meaning>\n'
        '   <default>stable</default>\n'
        '   <current>stable</current>\n'
        '   <type>string enum</type>\n'
        '   <enum_value>stable</enum_value>\n'
        '   <enum_value>experimental</enum_value>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('cc_version', 'tool', expected_output)

  def testFlagHelpInXML_CommaSeparatedList(self):
    gflags.DEFINE_list('files', 'a.cc,a.h,archive/old.zip',
                      'Files to process.', flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <file>tool</file>\n'
        '   <name>files</name>\n'
        '   <meaning>Files to process.</meaning>\n'
        '   <default>a.cc,a.h,archive/old.zip</default>\n'
        '   <current>[\'a.cc\', \'a.h\', \'archive/old.zip\']</current>\n'
        '   <type>comma separated list of strings</type>\n'
        '   <list_separator>\',\'</list_separator>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('files', 'tool', expected_output)

  def testListAsDefaultArgument_CommaSeparatedList(self):
    gflags.DEFINE_list('allow_users', ['alice', 'bob'],
                      'Users with access.', flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <file>tool</file>\n'
        '   <name>allow_users</name>\n'
        '   <meaning>Users with access.</meaning>\n'
        '   <default>alice,bob</default>\n'
        '   <current>[\'alice\', \'bob\']</current>\n'
        '   <type>comma separated list of strings</type>\n'
        '   <list_separator>\',\'</list_separator>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('allow_users', 'tool', expected_output)

  def testFlagHelpInXML_SpaceSeparatedList(self):
    gflags.DEFINE_spaceseplist('dirs', 'src libs bin',
                              'Directories to search.', flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <file>tool</file>\n'
        '   <name>dirs</name>\n'
        '   <meaning>Directories to search.</meaning>\n'
        '   <default>src libs bin</default>\n'
        '   <current>[\'src\', \'libs\', \'bin\']</current>\n'
        '   <type>whitespace separated list of strings</type>\n'
        'LIST_SEPARATORS'
        ' </flag>\n').replace('LIST_SEPARATORS',
                              _ListSeparatorsInXMLFormat(string.whitespace,
                                                         indent='   '))
    self._CheckFlagHelpInXML('dirs', 'tool', expected_output)

  def testFlagHelpInXML_MultiString(self):
    gflags.DEFINE_multistring('to_delete', ['a.cc', 'b.h'],
                             'Files to delete', flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <file>tool</file>\n'
        '   <name>to_delete</name>\n'
        '   <meaning>Files to delete;\n    '
        'repeat this option to specify a list of values</meaning>\n'
        '   <default>[\'a.cc\', \'b.h\']</default>\n'
        '   <current>[\'a.cc\', \'b.h\']</current>\n'
        '   <type>multi string</type>\n'
        ' </flag>\n')
    self._CheckFlagHelpInXML('to_delete', 'tool', expected_output)

  def testFlagHelpInXML_MultiInt(self):
    gflags.DEFINE_multi_int('cols', [5, 7, 23],
                           'Columns to select', flag_values=self.fv)
    expected_output = (
        ' <flag>\n'
        '   <file>tool</file>\n'
        '   <name>cols</name>\n'
        '   <meaning>Columns to select;\n    '
        'repeat this option to specify a list of values</meaning>\n'
        '   <default>[5, 7, 23]</default>\n'
        '   <current>[5, 7, 23]</current>\n'
        '   <type>multi int</type>\n'
        ' </flag>\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 = """\
<?xml version="1.0"?>
<AllFlags>
  <program>gflags_helpxml_test.py</program>
  <usage>%(usage_doc)s</usage>
"""

EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE = """\
  <flag>
    <key>yes</key>
    <file>%(main_module_name)s</file>
    <name>allow_users</name>
    <meaning>Users with access.</meaning>
    <default>alice,bob</default>
    <current>['alice', 'bob']</current>
    <type>comma separated list of strings</type>
    <list_separator>','</list_separator>
  </flag>
  <flag>
    <key>yes</key>
    <file>%(main_module_name)s</file>
    <name>cc_version</name>
    <meaning>&lt;stable|experimental&gt;: Compiler version to use.</meaning>
    <default>stable</default>
    <current>stable</current>
    <type>string enum</type>
    <enum_value>stable</enum_value>
    <enum_value>experimental</enum_value>