1# Copyright 2017 The Abseil Authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Unit tests for the XML-format help generated by the flags.py module."""
16
17import enum
18import io
19import os
20import string
21import sys
22import xml.dom.minidom
23import xml.sax.saxutils
24
25from absl import flags
26from absl.flags import _helpers
27from absl.flags.tests import module_bar
28from absl.testing import absltest
29
30
31class CreateXMLDOMElement(absltest.TestCase):
32
33  def _check(self, name, value, expected_output):
34    doc = xml.dom.minidom.Document()
35    node = _helpers.create_xml_dom_element(doc, name, value)
36    output = node.toprettyxml('  ', encoding='utf-8')
37    self.assertEqual(expected_output, output)
38
39  def test_create_xml_dom_element(self):
40    self._check('tag', '', b'<tag></tag>\n')
41    self._check('tag', 'plain text', b'<tag>plain text</tag>\n')
42    self._check('tag', '(x < y) && (a >= b)',
43                b'<tag>(x &lt; y) &amp;&amp; (a &gt;= b)</tag>\n')
44
45    # If the value is bytes with invalid unicode:
46    bytes_with_invalid_unicodes = b'\x81\xff'
47    # In python 3 the string representation is "b'\x81\xff'" so they are kept
48    # as "b'\x81\xff'".
49    self._check('tag', bytes_with_invalid_unicodes,
50                b"<tag>b'\\x81\\xff'</tag>\n")
51
52    # Some unicode chars are illegal in xml
53    # (http://www.w3.org/TR/REC-xml/#charsets):
54    self._check('tag', u'\x0b\x02\x08\ufffe', b'<tag></tag>\n')
55
56    # Valid unicode will be encoded:
57    self._check('tag', u'\xff', b'<tag>\xc3\xbf</tag>\n')
58
59
60def _list_separators_in_xmlformat(separators, indent=''):
61  """Generates XML encoding of a list of list separators.
62
63  Args:
64    separators: A list of list separators.  Usually, this should be a
65      string whose characters are the valid list separators, e.g., ','
66      means that both comma (',') and space (' ') are valid list
67      separators.
68    indent: A string that is added at the beginning of each generated
69      XML element.
70
71  Returns:
72    A string.
73  """
74  result = ''
75  separators = list(separators)
76  separators.sort()
77  for sep_char in separators:
78    result += ('%s<list_separator>%s</list_separator>\n' %
79               (indent, repr(sep_char)))
80  return result
81
82
83class FlagCreateXMLDOMElement(absltest.TestCase):
84  """Test the create_xml_dom_element method for a single flag at a time.
85
86  There is one test* method for each kind of DEFINE_* declaration.
87  """
88
89  def setUp(self):
90    # self.fv is a FlagValues object, just like flags.FLAGS.  Each
91    # test registers one flag with this FlagValues.
92    self.fv = flags.FlagValues()
93
94  def _check_flag_help_in_xml(self, flag_name, module_name,
95                              expected_output, is_key=False):
96    flag_obj = self.fv[flag_name]
97    doc = xml.dom.minidom.Document()
98    element = flag_obj._create_xml_dom_element(doc, module_name, is_key=is_key)
99    output = element.toprettyxml(indent='  ')
100    self.assertMultiLineEqual(expected_output, output)
101
102  def test_flag_help_in_xml_int(self):
103    flags.DEFINE_integer('index', 17, 'An integer flag', flag_values=self.fv)
104    expected_output_pattern = (
105        '<flag>\n'
106        '  <file>module.name</file>\n'
107        '  <name>index</name>\n'
108        '  <meaning>An integer flag</meaning>\n'
109        '  <default>17</default>\n'
110        '  <current>%d</current>\n'
111        '  <type>int</type>\n'
112        '</flag>\n')
113    self._check_flag_help_in_xml('index', 'module.name',
114                                 expected_output_pattern % 17)
115    # Check that the output is correct even when the current value of
116    # a flag is different from the default one.
117    self.fv['index'].value = 20
118    self._check_flag_help_in_xml('index', 'module.name',
119                                 expected_output_pattern % 20)
120
121  def test_flag_help_in_xml_int_with_bounds(self):
122    flags.DEFINE_integer('nb_iters', 17, 'An integer flag',
123                         lower_bound=5, upper_bound=27,
124                         flag_values=self.fv)
125    expected_output = (
126        '<flag>\n'
127        '  <key>yes</key>\n'
128        '  <file>module.name</file>\n'
129        '  <name>nb_iters</name>\n'
130        '  <meaning>An integer flag</meaning>\n'
131        '  <default>17</default>\n'
132        '  <current>17</current>\n'
133        '  <type>int</type>\n'
134        '  <lower_bound>5</lower_bound>\n'
135        '  <upper_bound>27</upper_bound>\n'
136        '</flag>\n')
137    self._check_flag_help_in_xml('nb_iters', 'module.name', expected_output,
138                                 is_key=True)
139
140  def test_flag_help_in_xml_string(self):
141    flags.DEFINE_string('file_path', '/path/to/my/dir', 'A test string flag.',
142                        flag_values=self.fv)
143    expected_output = (
144        '<flag>\n'
145        '  <file>simple_module</file>\n'
146        '  <name>file_path</name>\n'
147        '  <meaning>A test string flag.</meaning>\n'
148        '  <default>/path/to/my/dir</default>\n'
149        '  <current>/path/to/my/dir</current>\n'
150        '  <type>string</type>\n'
151        '</flag>\n')
152    self._check_flag_help_in_xml('file_path', 'simple_module', expected_output)
153
154  def test_flag_help_in_xml_string_with_xmlillegal_chars(self):
155    flags.DEFINE_string('file_path', '/path/to/\x08my/dir',
156                        'A test string flag.', flag_values=self.fv)
157    # '\x08' is not a legal character in XML 1.0 documents.  Our
158    # current code purges such characters from the generated XML.
159    expected_output = (
160        '<flag>\n'
161        '  <file>simple_module</file>\n'
162        '  <name>file_path</name>\n'
163        '  <meaning>A test string flag.</meaning>\n'
164        '  <default>/path/to/my/dir</default>\n'
165        '  <current>/path/to/my/dir</current>\n'
166        '  <type>string</type>\n'
167        '</flag>\n')
168    self._check_flag_help_in_xml('file_path', 'simple_module', expected_output)
169
170  def test_flag_help_in_xml_boolean(self):
171    flags.DEFINE_boolean('use_gpu', False, 'Use gpu for performance.',
172                         flag_values=self.fv)
173    expected_output = (
174        '<flag>\n'
175        '  <key>yes</key>\n'
176        '  <file>a_module</file>\n'
177        '  <name>use_gpu</name>\n'
178        '  <meaning>Use gpu for performance.</meaning>\n'
179        '  <default>false</default>\n'
180        '  <current>false</current>\n'
181        '  <type>bool</type>\n'
182        '</flag>\n')
183    self._check_flag_help_in_xml('use_gpu', 'a_module', expected_output,
184                                 is_key=True)
185
186  def test_flag_help_in_xml_enum(self):
187    flags.DEFINE_enum('cc_version', 'stable', ['stable', 'experimental'],
188                      'Compiler version to use.', flag_values=self.fv)
189    expected_output = (
190        '<flag>\n'
191        '  <file>tool</file>\n'
192        '  <name>cc_version</name>\n'
193        '  <meaning>&lt;stable|experimental&gt;: '
194        'Compiler version to use.</meaning>\n'
195        '  <default>stable</default>\n'
196        '  <current>stable</current>\n'
197        '  <type>string enum</type>\n'
198        '  <enum_value>stable</enum_value>\n'
199        '  <enum_value>experimental</enum_value>\n'
200        '</flag>\n')
201    self._check_flag_help_in_xml('cc_version', 'tool', expected_output)
202
203  def test_flag_help_in_xml_enum_class(self):
204    class Version(enum.Enum):
205      STABLE = 0
206      EXPERIMENTAL = 1
207
208    flags.DEFINE_enum_class('cc_version', 'STABLE', Version,
209                            'Compiler version to use.', flag_values=self.fv)
210    expected_output = ('<flag>\n'
211                       '  <file>tool</file>\n'
212                       '  <name>cc_version</name>\n'
213                       '  <meaning>&lt;stable|experimental&gt;: '
214                       'Compiler version to use.</meaning>\n'
215                       '  <default>stable</default>\n'
216                       '  <current>Version.STABLE</current>\n'
217                       '  <type>enum class</type>\n'
218                       '  <enum_value>STABLE</enum_value>\n'
219                       '  <enum_value>EXPERIMENTAL</enum_value>\n'
220                       '</flag>\n')
221    self._check_flag_help_in_xml('cc_version', 'tool', expected_output)
222
223  def test_flag_help_in_xml_comma_separated_list(self):
224    flags.DEFINE_list('files', 'a.cc,a.h,archive/old.zip',
225                      'Files to process.', flag_values=self.fv)
226    expected_output = (
227        '<flag>\n'
228        '  <file>tool</file>\n'
229        '  <name>files</name>\n'
230        '  <meaning>Files to process.</meaning>\n'
231        '  <default>a.cc,a.h,archive/old.zip</default>\n'
232        '  <current>[\'a.cc\', \'a.h\', \'archive/old.zip\']</current>\n'
233        '  <type>comma separated list of strings</type>\n'
234        '  <list_separator>\',\'</list_separator>\n'
235        '</flag>\n')
236    self._check_flag_help_in_xml('files', 'tool', expected_output)
237
238  def test_list_as_default_argument_comma_separated_list(self):
239    flags.DEFINE_list('allow_users', ['alice', 'bob'],
240                      'Users with access.', flag_values=self.fv)
241    expected_output = (
242        '<flag>\n'
243        '  <file>tool</file>\n'
244        '  <name>allow_users</name>\n'
245        '  <meaning>Users with access.</meaning>\n'
246        '  <default>alice,bob</default>\n'
247        '  <current>[\'alice\', \'bob\']</current>\n'
248        '  <type>comma separated list of strings</type>\n'
249        '  <list_separator>\',\'</list_separator>\n'
250        '</flag>\n')
251    self._check_flag_help_in_xml('allow_users', 'tool', expected_output)
252
253  def test_none_as_default_arguments_comma_separated_list(self):
254    flags.DEFINE_list('allow_users', None,
255                      'Users with access.', flag_values=self.fv)
256    expected_output = (
257        '<flag>\n'
258        '  <file>tool</file>\n'
259        '  <name>allow_users</name>\n'
260        '  <meaning>Users with access.</meaning>\n'
261        '  <default></default>\n'
262        '  <current>None</current>\n'
263        '  <type>comma separated list of strings</type>\n'
264        '  <list_separator>\',\'</list_separator>\n'
265        '</flag>\n')
266    self._check_flag_help_in_xml('allow_users', 'tool', expected_output)
267
268  def test_flag_help_in_xml_space_separated_list(self):
269    flags.DEFINE_spaceseplist('dirs', 'src libs bin',
270                              'Directories to search.', flag_values=self.fv)
271    expected_separators = sorted(string.whitespace)
272    expected_output = (
273        '<flag>\n'
274        '  <file>tool</file>\n'
275        '  <name>dirs</name>\n'
276        '  <meaning>Directories to search.</meaning>\n'
277        '  <default>src libs bin</default>\n'
278        '  <current>[\'src\', \'libs\', \'bin\']</current>\n'
279        '  <type>whitespace separated list of strings</type>\n'
280        'LIST_SEPARATORS'
281        '</flag>\n').replace('LIST_SEPARATORS',
282                             _list_separators_in_xmlformat(expected_separators,
283                                                           indent='  '))
284    self._check_flag_help_in_xml('dirs', 'tool', expected_output)
285
286  def test_flag_help_in_xml_space_separated_list_with_comma_compat(self):
287    flags.DEFINE_spaceseplist('dirs', 'src libs,bin',
288                              'Directories to search.', comma_compat=True,
289                              flag_values=self.fv)
290    expected_separators = sorted(string.whitespace + ',')
291    expected_output = (
292        '<flag>\n'
293        '  <file>tool</file>\n'
294        '  <name>dirs</name>\n'
295        '  <meaning>Directories to search.</meaning>\n'
296        '  <default>src libs bin</default>\n'
297        '  <current>[\'src\', \'libs\', \'bin\']</current>\n'
298        '  <type>whitespace or comma separated list of strings</type>\n'
299        'LIST_SEPARATORS'
300        '</flag>\n').replace('LIST_SEPARATORS',
301                             _list_separators_in_xmlformat(expected_separators,
302                                                           indent='  '))
303    self._check_flag_help_in_xml('dirs', 'tool', expected_output)
304
305  def test_flag_help_in_xml_multi_string(self):
306    flags.DEFINE_multi_string('to_delete', ['a.cc', 'b.h'],
307                              'Files to delete', flag_values=self.fv)
308    expected_output = (
309        '<flag>\n'
310        '  <file>tool</file>\n'
311        '  <name>to_delete</name>\n'
312        '  <meaning>Files to delete;\n'
313        '    repeat this option to specify a list of values</meaning>\n'
314        '  <default>[\'a.cc\', \'b.h\']</default>\n'
315        '  <current>[\'a.cc\', \'b.h\']</current>\n'
316        '  <type>multi string</type>\n'
317        '</flag>\n')
318    self._check_flag_help_in_xml('to_delete', 'tool', expected_output)
319
320  def test_flag_help_in_xml_multi_int(self):
321    flags.DEFINE_multi_integer('cols', [5, 7, 23],
322                               'Columns to select', flag_values=self.fv)
323    expected_output = (
324        '<flag>\n'
325        '  <file>tool</file>\n'
326        '  <name>cols</name>\n'
327        '  <meaning>Columns to select;\n    '
328        'repeat this option to specify a list of values</meaning>\n'
329        '  <default>[5, 7, 23]</default>\n'
330        '  <current>[5, 7, 23]</current>\n'
331        '  <type>multi int</type>\n'
332        '</flag>\n')
333    self._check_flag_help_in_xml('cols', 'tool', expected_output)
334
335  def test_flag_help_in_xml_multi_enum(self):
336    flags.DEFINE_multi_enum('flavours', ['APPLE', 'BANANA'],
337                            ['APPLE', 'BANANA', 'CHERRY'],
338                            'Compilation flavour.', flag_values=self.fv)
339    expected_output = (
340        '<flag>\n'
341        '  <file>tool</file>\n'
342        '  <name>flavours</name>\n'
343        '  <meaning>&lt;APPLE|BANANA|CHERRY&gt;: Compilation flavour.;\n'
344        '    repeat this option to specify a list of values</meaning>\n'
345        '  <default>[\'APPLE\', \'BANANA\']</default>\n'
346        '  <current>[\'APPLE\', \'BANANA\']</current>\n'
347        '  <type>multi string enum</type>\n'
348        '  <enum_value>APPLE</enum_value>\n'
349        '  <enum_value>BANANA</enum_value>\n'
350        '  <enum_value>CHERRY</enum_value>\n'
351        '</flag>\n')
352    self._check_flag_help_in_xml('flavours', 'tool', expected_output)
353
354  def test_flag_help_in_xml_multi_enum_class_singleton_default(self):
355    class Fruit(enum.Enum):
356      ORANGE = 0
357      BANANA = 1
358
359    flags.DEFINE_multi_enum_class('fruit', ['ORANGE'],
360                                  Fruit,
361                                  'The fruit flag.', flag_values=self.fv)
362    expected_output = (
363        '<flag>\n'
364        '  <file>tool</file>\n'
365        '  <name>fruit</name>\n'
366        '  <meaning>&lt;orange|banana&gt;: The fruit flag.;\n'
367        '    repeat this option to specify a list of values</meaning>\n'
368        '  <default>orange</default>\n'
369        '  <current>orange</current>\n'
370        '  <type>multi enum class</type>\n'
371        '  <enum_value>ORANGE</enum_value>\n'
372        '  <enum_value>BANANA</enum_value>\n'
373        '</flag>\n')
374    self._check_flag_help_in_xml('fruit', 'tool', expected_output)
375
376  def test_flag_help_in_xml_multi_enum_class_list_default(self):
377    class Fruit(enum.Enum):
378      ORANGE = 0
379      BANANA = 1
380
381    flags.DEFINE_multi_enum_class('fruit', ['ORANGE', 'BANANA'],
382                                  Fruit,
383                                  'The fruit flag.', flag_values=self.fv)
384    expected_output = (
385        '<flag>\n'
386        '  <file>tool</file>\n'
387        '  <name>fruit</name>\n'
388        '  <meaning>&lt;orange|banana&gt;: The fruit flag.;\n'
389        '    repeat this option to specify a list of values</meaning>\n'
390        '  <default>orange,banana</default>\n'
391        '  <current>orange,banana</current>\n'
392        '  <type>multi enum class</type>\n'
393        '  <enum_value>ORANGE</enum_value>\n'
394        '  <enum_value>BANANA</enum_value>\n'
395        '</flag>\n')
396    self._check_flag_help_in_xml('fruit', 'tool', expected_output)
397
398# The next EXPECTED_HELP_XML_* constants are parts of a template for
399# the expected XML output from WriteHelpInXMLFormatTest below.  When
400# we assemble these parts into a single big string, we'll take into
401# account the ordering between the name of the main module and the
402# name of module_bar.  Next, we'll fill in the docstring for this
403# module (%(usage_doc)s), the name of the main module
404# (%(main_module_name)s) and the name of the module module_bar
405# (%(module_bar_name)s).  See WriteHelpInXMLFormatTest below.
406EXPECTED_HELP_XML_START = """\
407<?xml version="1.0" encoding="utf-8"?>
408<AllFlags>
409  <program>%(basename_of_argv0)s</program>
410  <usage>%(usage_doc)s</usage>
411"""
412
413EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE = """\
414  <flag>
415    <key>yes</key>
416    <file>%(main_module_name)s</file>
417    <name>allow_users</name>
418    <meaning>Users with access.</meaning>
419    <default>alice,bob</default>
420    <current>['alice', 'bob']</current>
421    <type>comma separated list of strings</type>
422    <list_separator>','</list_separator>
423  </flag>
424  <flag>
425    <key>yes</key>
426    <file>%(main_module_name)s</file>
427    <name>cc_version</name>
428    <meaning>&lt;stable|experimental&gt;: Compiler version to use.</meaning>
429    <default>stable</default>
430    <current>stable</current>
431    <type>string enum</type>
432    <enum_value>stable</enum_value>
433    <enum_value>experimental</enum_value>
434  </flag>
435  <flag>
436    <key>yes</key>
437    <file>%(main_module_name)s</file>
438    <name>cols</name>
439    <meaning>Columns to select;
440    repeat this option to specify a list of values</meaning>
441    <default>[5, 7, 23]</default>
442    <current>[5, 7, 23]</current>
443    <type>multi int</type>
444  </flag>
445  <flag>
446    <key>yes</key>
447    <file>%(main_module_name)s</file>
448    <name>dirs</name>
449    <meaning>Directories to create.</meaning>
450    <default>src libs bins</default>
451    <current>['src', 'libs', 'bins']</current>
452    <type>whitespace separated list of strings</type>
453%(whitespace_separators)s  </flag>
454  <flag>
455    <key>yes</key>
456    <file>%(main_module_name)s</file>
457    <name>file_path</name>
458    <meaning>A test string flag.</meaning>
459    <default>/path/to/my/dir</default>
460    <current>/path/to/my/dir</current>
461    <type>string</type>
462  </flag>
463  <flag>
464    <key>yes</key>
465    <file>%(main_module_name)s</file>
466    <name>files</name>
467    <meaning>Files to process.</meaning>
468    <default>a.cc,a.h,archive/old.zip</default>
469    <current>['a.cc', 'a.h', 'archive/old.zip']</current>
470    <type>comma separated list of strings</type>
471    <list_separator>\',\'</list_separator>
472  </flag>
473  <flag>
474    <key>yes</key>
475    <file>%(main_module_name)s</file>
476    <name>flavours</name>
477    <meaning>&lt;APPLE|BANANA|CHERRY&gt;: Compilation flavour.;
478    repeat this option to specify a list of values</meaning>
479    <default>['APPLE', 'BANANA']</default>
480    <current>['APPLE', 'BANANA']</current>
481    <type>multi string enum</type>
482    <enum_value>APPLE</enum_value>
483    <enum_value>BANANA</enum_value>
484    <enum_value>CHERRY</enum_value>
485  </flag>
486  <flag>
487    <key>yes</key>
488    <file>%(main_module_name)s</file>
489    <name>index</name>
490    <meaning>An integer flag</meaning>
491    <default>17</default>
492    <current>17</current>
493    <type>int</type>
494  </flag>
495  <flag>
496    <key>yes</key>
497    <file>%(main_module_name)s</file>
498    <name>nb_iters</name>
499    <meaning>An integer flag</meaning>
500    <default>17</default>
501    <current>17</current>
502    <type>int</type>
503    <lower_bound>5</lower_bound>
504    <upper_bound>27</upper_bound>
505  </flag>
506  <flag>
507    <key>yes</key>
508    <file>%(main_module_name)s</file>
509    <name>to_delete</name>
510    <meaning>Files to delete;
511    repeat this option to specify a list of values</meaning>
512    <default>['a.cc', 'b.h']</default>
513    <current>['a.cc', 'b.h']</current>
514    <type>multi string</type>
515  </flag>
516  <flag>
517    <key>yes</key>
518    <file>%(main_module_name)s</file>
519    <name>use_gpu</name>
520    <meaning>Use gpu for performance.</meaning>
521    <default>false</default>
522    <current>false</current>
523    <type>bool</type>
524  </flag>
525"""
526
527EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR = """\
528  <flag>
529    <file>%(module_bar_name)s</file>
530    <name>tmod_bar_t</name>
531    <meaning>Sample int flag.</meaning>
532    <default>4</default>
533    <current>4</current>
534    <type>int</type>
535  </flag>
536  <flag>
537    <key>yes</key>
538    <file>%(module_bar_name)s</file>
539    <name>tmod_bar_u</name>
540    <meaning>Sample int flag.</meaning>
541    <default>5</default>
542    <current>5</current>
543    <type>int</type>
544  </flag>
545  <flag>
546    <file>%(module_bar_name)s</file>
547    <name>tmod_bar_v</name>
548    <meaning>Sample int flag.</meaning>
549    <default>6</default>
550    <current>6</current>
551    <type>int</type>
552  </flag>
553  <flag>
554    <file>%(module_bar_name)s</file>
555    <name>tmod_bar_x</name>
556    <meaning>Boolean flag.</meaning>
557    <default>true</default>
558    <current>true</current>
559    <type>bool</type>
560  </flag>
561  <flag>
562    <file>%(module_bar_name)s</file>
563    <name>tmod_bar_y</name>
564    <meaning>String flag.</meaning>
565    <default>default</default>
566    <current>default</current>
567    <type>string</type>
568  </flag>
569  <flag>
570    <key>yes</key>
571    <file>%(module_bar_name)s</file>
572    <name>tmod_bar_z</name>
573    <meaning>Another boolean flag from module bar.</meaning>
574    <default>false</default>
575    <current>false</current>
576    <type>bool</type>
577  </flag>
578"""
579
580EXPECTED_HELP_XML_END = """\
581</AllFlags>
582"""
583
584
585class WriteHelpInXMLFormatTest(absltest.TestCase):
586  """Big test of FlagValues.write_help_in_xml_format, with several flags."""
587
588  def test_write_help_in_xmlformat(self):
589    fv = flags.FlagValues()
590    # Since these flags are defined by the top module, they are all key.
591    flags.DEFINE_integer('index', 17, 'An integer flag', flag_values=fv)
592    flags.DEFINE_integer('nb_iters', 17, 'An integer flag',
593                         lower_bound=5, upper_bound=27, flag_values=fv)
594    flags.DEFINE_string('file_path', '/path/to/my/dir', 'A test string flag.',
595                        flag_values=fv)
596    flags.DEFINE_boolean('use_gpu', False, 'Use gpu for performance.',
597                         flag_values=fv)
598    flags.DEFINE_enum('cc_version', 'stable', ['stable', 'experimental'],
599                      'Compiler version to use.', flag_values=fv)
600    flags.DEFINE_list('files', 'a.cc,a.h,archive/old.zip',
601                      'Files to process.', flag_values=fv)
602    flags.DEFINE_list('allow_users', ['alice', 'bob'],
603                      'Users with access.', flag_values=fv)
604    flags.DEFINE_spaceseplist('dirs', 'src libs bins',
605                              'Directories to create.', flag_values=fv)
606    flags.DEFINE_multi_string('to_delete', ['a.cc', 'b.h'],
607                              'Files to delete', flag_values=fv)
608    flags.DEFINE_multi_integer('cols', [5, 7, 23],
609                               'Columns to select', flag_values=fv)
610    flags.DEFINE_multi_enum('flavours', ['APPLE', 'BANANA'],
611                            ['APPLE', 'BANANA', 'CHERRY'],
612                            'Compilation flavour.', flag_values=fv)
613    # Define a few flags in a different module.
614    module_bar.define_flags(flag_values=fv)
615    # And declare only a few of them to be key.  This way, we have
616    # different kinds of flags, defined in different modules, and not
617    # all of them are key flags.
618    flags.declare_key_flag('tmod_bar_z', flag_values=fv)
619    flags.declare_key_flag('tmod_bar_u', flag_values=fv)
620
621    # Generate flag help in XML format in the StringIO sio.
622    sio = io.StringIO()
623    fv.write_help_in_xml_format(sio)
624
625    # Check that we got the expected result.
626    expected_output_template = EXPECTED_HELP_XML_START
627    main_module_name = sys.argv[0]
628    module_bar_name = module_bar.__name__
629
630    if main_module_name < module_bar_name:
631      expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE
632      expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR
633    else:
634      expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR
635      expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE
636
637    expected_output_template += EXPECTED_HELP_XML_END
638
639    # XML representation of the whitespace list separators.
640    whitespace_separators = _list_separators_in_xmlformat(string.whitespace,
641                                                          indent='    ')
642    expected_output = (
643        expected_output_template %
644        {'basename_of_argv0': os.path.basename(sys.argv[0]),
645         'usage_doc': sys.modules['__main__'].__doc__,
646         'main_module_name': main_module_name,
647         'module_bar_name': module_bar_name,
648         'whitespace_separators': whitespace_separators})
649
650    actual_output = sio.getvalue()
651    self.assertMultiLineEqual(expected_output, actual_output)
652
653    # Also check that our result is valid XML.  minidom.parseString
654    # throws an xml.parsers.expat.ExpatError in case of an error.
655    xml.dom.minidom.parseString(actual_output)
656
657
658if __name__ == '__main__':
659  absltest.main()
660