You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

235 lines
9.3 KiB

2 years ago
  1. import json
  2. import unittest
  3. from argparse import ArgumentParser
  4. from contextlib import contextmanager
  5. from pprint import pprint
  6. from typing import Dict, List
  7. from unittest.mock import MagicMock, patch
  8. import sys
  9. import shlex
  10. from wx._core import CommandEvent
  11. from gooey import GooeyParser
  12. from python_bindings.coms import decode_payload, deserialize_inbound
  13. from python_bindings.dynamics import patch_argument, check_value
  14. from gooey.python_bindings import control
  15. from gooey.python_bindings.parameters import gooey_params
  16. from gooey.gui import state as s
  17. from gooey.python_bindings.schema import validate_public_state
  18. from python_bindings.types import FormField
  19. from tests.harness import instrumentGooey
  20. from gooey.tests import *
  21. def custom_type(x):
  22. if x == '1234':
  23. return x
  24. else:
  25. raise Exception('KABOOM!')
  26. class TestControl(unittest.TestCase):
  27. def tearDown(self):
  28. """
  29. Undoes the monkey patching after every tests
  30. """
  31. if hasattr(ArgumentParser, 'original_parse_args'):
  32. ArgumentParser.parse_args = ArgumentParser.original_parse_args
  33. def test_validate_form(self):
  34. """
  35. Testing the major validation cases we support.
  36. """
  37. writer = MagicMock()
  38. exit = MagicMock()
  39. monkey_patch = control.validate_form(gooey_params(), write=writer, exit=exit)
  40. ArgumentParser.original_parse_args = ArgumentParser.parse_args
  41. ArgumentParser.parse_args = monkey_patch
  42. parser = GooeyParser()
  43. # examples:
  44. # ERROR: mismatched builtin type
  45. parser.add_argument('a', type=int, gooey_options={'initial_value': 'not-an-int'})
  46. # ERROR: mismatched custom type
  47. parser.add_argument('b', type=custom_type, gooey_options={'initial_value': 'not-a-float'})
  48. # ERROR: missing required positional arg
  49. parser.add_argument('c')
  50. # ERROR: missing required 'optional' arg
  51. parser.add_argument('--oc', required=True)
  52. # VALID: This is one of the bizarre cases which are possible
  53. # but don't make much sense. It should pass through as valid
  54. # because there's no way for us to send a 'not present optional value'
  55. parser.add_argument('--bo', action='store_true', required=True)
  56. # ERROR: a required mutex group, with no args supplied.
  57. # Should flag all as missing.
  58. group = parser.add_mutually_exclusive_group(required=True)
  59. group.add_argument('--gp1-a', type=str)
  60. group.add_argument('--gp1-b', type=str)
  61. # ERROR: required mutex group with a default option but nothing
  62. # selected will still fail
  63. group2 = parser.add_mutually_exclusive_group(required=True)
  64. group2.add_argument('--gp2-a', type=str)
  65. group2.add_argument('--gp2-b', type=str, default='Heeeeyyyyy')
  66. # VALID: now, same as above, but now the option is actually enabled via
  67. # the initial selection. No error.
  68. group3 = parser.add_mutually_exclusive_group(required=True, gooey_options={'initial_selection': 1})
  69. group3.add_argument('--gp3-a', type=str)
  70. group3.add_argument('--gp3-b', type=str, default='Heeeeyyyyy')
  71. # VALID: optional mutex.
  72. group4 = parser.add_mutually_exclusive_group()
  73. group4.add_argument('--gp4-a', type=str)
  74. group4.add_argument('--gp4-b', type=str)
  75. # VALID: arg present and type satisfied
  76. parser.add_argument('ga', type=str, gooey_options={'initial_value': 'whatever'})
  77. # VALID: arg present and custom type satisfied
  78. parser.add_argument('gb', type=custom_type, gooey_options={'initial_value': '1234'})
  79. # VALID: optional
  80. parser.add_argument('--gc')
  81. # now we're adding the same
  82. with instrumentGooey(parser, target='test') as (app, frame, gapp):
  83. # we start off with no errors
  84. self.assertFalse(s.has_errors(gapp.fullState()))
  85. # now we feed our form-validation
  86. cmd = s.buildFormValidationCmd(gapp.fullState())
  87. asdf = shlex.split(cmd)[1:]
  88. parser.parse_args(shlex.split(cmd)[1:])
  89. assert writer.called
  90. assert exit.called
  91. result = deserialize_inbound(writer.call_args[0][0].encode('utf-8'), 'utf-8')
  92. # Host->Gooey communication is all done over the PublicGooeyState schema
  93. # as such, we coarsely validate it's shape here
  94. validate_public_state(result)
  95. # manually merging the two states back together
  96. nextState = s.mergeExternalState(gapp.fullState(), result)
  97. # and now we find that we have errors!
  98. self.assertTrue(s.has_errors(nextState))
  99. items = s.activeFormState(nextState)
  100. self.assertIn('invalid literal', get_by_id(items, 'a')['error'])
  101. self.assertIn('KABOOM!', get_by_id(items, 'b')['error'])
  102. self.assertIn('required', get_by_id(items, 'c')['error'])
  103. self.assertIn('required', get_by_id(items, 'oc')['error'])
  104. for item in get_by_id(items, 'group_gp1_a_gp1_b')['options']:
  105. self.assertIsNotNone(item['error'])
  106. for item in get_by_id(items, 'group_gp2_a_gp2_b')['options']:
  107. self.assertIsNotNone(item['error'])
  108. for item in get_by_id(items, 'group_gp3_a_gp3_b')['options']:
  109. self.assertIsNone(item['error'])
  110. # should be None, since this one was entirely optional
  111. for item in get_by_id(items, 'group_gp4_a_gp4_b')['options']:
  112. self.assertIsNone(item['error'])
  113. self.assertIsNone(get_by_id(items, 'bo')['error'])
  114. self.assertIsNone(get_by_id(items, 'ga')['error'])
  115. self.assertIsNone(get_by_id(items, 'gb')['error'])
  116. self.assertIsNone(get_by_id(items, 'gc')['error'])
  117. def test_subparsers(self):
  118. """
  119. Making sure that subparsers are handled correctly and
  120. all validations still work as expected.
  121. """
  122. writer = MagicMock()
  123. exit = MagicMock()
  124. monkey_patch = control.validate_form(gooey_params(), write=writer, exit=exit)
  125. ArgumentParser.original_parse_args = ArgumentParser.parse_args
  126. ArgumentParser.parse_args = monkey_patch
  127. def build_parser():
  128. # we build a new parser for each subtest
  129. # since we monkey patch the hell out of it
  130. # each time
  131. parser = GooeyParser()
  132. subs = parser.add_subparsers()
  133. foo = subs.add_parser('foo')
  134. foo.add_argument('a')
  135. foo.add_argument('b')
  136. foo.add_argument('p')
  137. bar = subs.add_parser('bar')
  138. bar.add_argument('a')
  139. bar.add_argument('b')
  140. bar.add_argument('z')
  141. return parser
  142. parser = build_parser()
  143. with instrumentGooey(parser, target='test') as (app, frame, gapp):
  144. with self.subTest('first subparser'):
  145. # we start off with no errors
  146. self.assertFalse(s.has_errors(gapp.fullState()))
  147. cmd = s.buildFormValidationCmd(gapp.fullState())
  148. parser.parse_args(shlex.split(cmd)[1:])
  149. assert writer.called
  150. assert exit.called
  151. result = deserialize_inbound(writer.call_args[0][0].encode('utf-8'), 'utf-8')
  152. nextState = s.mergeExternalState(gapp.fullState(), result)
  153. # by default, the subparser defined first, 'foo', is selected.
  154. self.assertIn('foo', nextState['forms'])
  155. # and we should find its attributes
  156. expected = {'a', 'b', 'p'}
  157. actual = {x['id'] for x in nextState['forms']['foo']}
  158. self.assertEqual(expected, actual)
  159. parser = build_parser()
  160. with instrumentGooey(parser, target='test') as (app, frame, gapp):
  161. with self.subTest('Second subparser'):
  162. # mocking a 'selection change' event to select
  163. # the second subparser
  164. event = MagicMock()
  165. event.Selection = 1
  166. gapp.handleSelectAction(event)
  167. # Flushing our events by running the main loop
  168. wx.CallLater(1, app.ExitMainLoop)
  169. app.MainLoop()
  170. cmd = s.buildFormValidationCmd(gapp.fullState())
  171. parser.parse_args(shlex.split(cmd)[1:])
  172. assert writer.called
  173. assert exit.called
  174. result = deserialize_inbound(writer.call_args[0][0].encode('utf-8'), 'utf-8')
  175. nextState = s.mergeExternalState(gapp.fullState(), result)
  176. # Now our second subparer, 'bar', should be present.
  177. self.assertIn('bar', nextState['forms'])
  178. # and we should find its attributes
  179. expected = {'a', 'b', 'z'}
  180. actual = {x['id'] for x in nextState['forms']['bar']}
  181. self.assertEqual(expected, actual)
  182. def test_ignore_gooey(self):
  183. parser = GooeyParser()
  184. subs = parser.add_subparsers()
  185. foo = subs.add_parser('foo')
  186. foo.add_argument('a')
  187. foo.add_argument('b')
  188. foo.add_argument('p')
  189. bar = subs.add_parser('bar')
  190. bar.add_argument('a')
  191. bar.add_argument('b')
  192. bar.add_argument('z')
  193. control.bypass_gooey(gooey_params())(parser)
  194. def get_by_id(items: List[FormField], id: str):
  195. return [x for x in items if x['id'] == id][0]