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"""Tests for app.py.""" 16 17import contextlib 18import copy 19import enum 20import io 21import os 22import re 23import subprocess 24import sys 25import tempfile 26from unittest import mock 27 28from absl import app 29from absl import flags 30from absl.testing import _bazelize_command 31from absl.testing import absltest 32from absl.testing import flagsaver 33from absl.tests import app_test_helper 34 35 36FLAGS = flags.FLAGS 37 38 39_newline_regex = re.compile('(\r\n)|\r') 40 41 42@contextlib.contextmanager 43def patch_main_module_docstring(docstring): 44 old_doc = sys.modules['__main__'].__doc__ 45 sys.modules['__main__'].__doc__ = docstring 46 yield 47 sys.modules['__main__'].__doc__ = old_doc 48 49 50def _normalize_newlines(s): 51 return re.sub('(\r\n)|\r', '\n', s) 52 53 54class UnitTests(absltest.TestCase): 55 56 def test_install_exception_handler(self): 57 with self.assertRaises(TypeError): 58 app.install_exception_handler(1) 59 60 def test_usage(self): 61 with mock.patch.object( 62 sys, 'stderr', new=io.StringIO()) as mock_stderr: 63 app.usage() 64 self.assertIn(__doc__, mock_stderr.getvalue()) 65 # Assert that flags are written to stderr. 66 self.assertIn('\n --[no]helpfull:', mock_stderr.getvalue()) 67 68 def test_usage_shorthelp(self): 69 with mock.patch.object( 70 sys, 'stderr', new=io.StringIO()) as mock_stderr: 71 app.usage(shorthelp=True) 72 # Assert that flags are NOT written to stderr. 73 self.assertNotIn(' --', mock_stderr.getvalue()) 74 75 def test_usage_writeto_stderr(self): 76 with mock.patch.object( 77 sys, 'stdout', new=io.StringIO()) as mock_stdout: 78 app.usage(writeto_stdout=True) 79 self.assertIn(__doc__, mock_stdout.getvalue()) 80 81 def test_usage_detailed_error(self): 82 with mock.patch.object( 83 sys, 'stderr', new=io.StringIO()) as mock_stderr: 84 app.usage(detailed_error='BAZBAZ') 85 self.assertIn('BAZBAZ', mock_stderr.getvalue()) 86 87 def test_usage_exitcode(self): 88 with mock.patch.object(sys, 'stderr', new=sys.stderr): 89 try: 90 app.usage(exitcode=2) 91 self.fail('app.usage(exitcode=1) should raise SystemExit') 92 except SystemExit as e: 93 self.assertEqual(2, e.code) 94 95 def test_usage_expands_docstring(self): 96 with patch_main_module_docstring('Name: %s, %%s'): 97 with mock.patch.object( 98 sys, 'stderr', new=io.StringIO()) as mock_stderr: 99 app.usage() 100 self.assertIn('Name: {}, %s'.format(sys.argv[0]), 101 mock_stderr.getvalue()) 102 103 def test_usage_does_not_expand_bad_docstring(self): 104 with patch_main_module_docstring('Name: %s, %%s, %@'): 105 with mock.patch.object( 106 sys, 'stderr', new=io.StringIO()) as mock_stderr: 107 app.usage() 108 self.assertIn('Name: %s, %%s, %@', mock_stderr.getvalue()) 109 110 @flagsaver.flagsaver 111 def test_register_and_parse_flags_with_usage_exits_on_only_check_args(self): 112 done = app._register_and_parse_flags_with_usage.done 113 try: 114 app._register_and_parse_flags_with_usage.done = False 115 with self.assertRaises(SystemExit): 116 app._register_and_parse_flags_with_usage( 117 argv=['./program', '--only_check_args']) 118 finally: 119 app._register_and_parse_flags_with_usage.done = done 120 121 def test_register_and_parse_flags_with_usage_exits_on_second_run(self): 122 with self.assertRaises(SystemError): 123 app._register_and_parse_flags_with_usage() 124 125 126class FunctionalTests(absltest.TestCase): 127 """Functional tests that use runs app_test_helper.""" 128 129 helper_type = 'pure_python' 130 131 def run_helper(self, expect_success, 132 expected_stdout_substring=None, expected_stderr_substring=None, 133 arguments=(), 134 env_overrides=None): 135 env = os.environ.copy() 136 env['APP_TEST_HELPER_TYPE'] = self.helper_type 137 env['PYTHONIOENCODING'] = 'utf8' 138 if env_overrides: 139 env.update(env_overrides) 140 141 helper = 'absl/tests/app_test_helper_{}'.format(self.helper_type) 142 process = subprocess.Popen( 143 [_bazelize_command.get_executable_path(helper)] + list(arguments), 144 stdout=subprocess.PIPE, 145 stderr=subprocess.PIPE, env=env, universal_newlines=False) 146 stdout, stderr = process.communicate() 147 # In Python 2, we can't control the encoding used by universal_newline 148 # mode, which can cause UnicodeDecodeErrors when subprocess tries to 149 # convert the bytes to unicode, so we have to decode it manually. 150 stdout = _normalize_newlines(stdout.decode('utf8')) 151 stderr = _normalize_newlines(stderr.decode('utf8')) 152 153 message = (u'Command: {command}\n' 154 'Exit Code: {exitcode}\n' 155 '===== stdout =====\n{stdout}' 156 '===== stderr =====\n{stderr}' 157 '=================='.format( 158 command=' '.join([helper] + list(arguments)), 159 exitcode=process.returncode, 160 stdout=stdout or '<no output>\n', 161 stderr=stderr or '<no output>\n')) 162 if expect_success: 163 self.assertEqual(0, process.returncode, msg=message) 164 else: 165 self.assertNotEqual(0, process.returncode, msg=message) 166 167 if expected_stdout_substring: 168 self.assertIn(expected_stdout_substring, stdout, message) 169 if expected_stderr_substring: 170 self.assertIn(expected_stderr_substring, stderr, message) 171 172 return process.returncode, stdout, stderr 173 174 def test_help(self): 175 _, _, stderr = self.run_helper( 176 False, 177 arguments=['--help'], 178 expected_stdout_substring=app_test_helper.__doc__) 179 self.assertNotIn('--', stderr) 180 181 def test_helpfull_basic(self): 182 self.run_helper( 183 False, 184 arguments=['--helpfull'], 185 # --logtostderr is from absl.logging module. 186 expected_stdout_substring='--[no]logtostderr') 187 188 def test_helpfull_unicode_flag_help(self): 189 _, stdout, _ = self.run_helper( 190 False, 191 arguments=['--helpfull'], 192 expected_stdout_substring='str_flag_with_unicode_args') 193 194 self.assertIn(u'smile:\U0001F604', stdout) 195 196 self.assertIn(u'thumb:\U0001F44D', stdout) 197 198 def test_helpshort(self): 199 _, _, stderr = self.run_helper( 200 False, 201 arguments=['--helpshort'], 202 expected_stdout_substring=app_test_helper.__doc__) 203 self.assertNotIn('--', stderr) 204 205 def test_custom_main(self): 206 self.run_helper( 207 True, 208 env_overrides={'APP_TEST_CUSTOM_MAIN_FUNC': 'custom_main'}, 209 expected_stdout_substring='Function called: custom_main.') 210 211 def test_custom_argv(self): 212 self.run_helper( 213 True, 214 expected_stdout_substring='argv: ./program pos_arg1', 215 env_overrides={ 216 'APP_TEST_CUSTOM_ARGV': './program --noraise_exception pos_arg1', 217 'APP_TEST_PRINT_ARGV': '1', 218 }) 219 220 def test_gwq_status_file_on_exception(self): 221 if self.helper_type == 'pure_python': 222 # Pure python binary does not write to GWQ Status. 223 return 224 225 tmpdir = tempfile.mkdtemp(dir=absltest.TEST_TMPDIR.value) 226 self.run_helper( 227 False, 228 arguments=['--raise_exception'], 229 env_overrides={'GOOGLE_STATUS_DIR': tmpdir}) 230 with open(os.path.join(tmpdir, 'STATUS')) as status_file: 231 self.assertIn('MyException:', status_file.read()) 232 233 def test_faulthandler_dumps_stack_on_sigsegv(self): 234 return_code, _, _ = self.run_helper( 235 False, 236 expected_stderr_substring='app_test_helper.py", line', 237 arguments=['--faulthandler_sigsegv']) 238 # sigsegv returns 3 on Windows, and -11 on LINUX/macOS. 239 expected_return_code = 3 if os.name == 'nt' else -11 240 self.assertEqual(expected_return_code, return_code) 241 242 def test_top_level_exception(self): 243 self.run_helper( 244 False, 245 arguments=['--raise_exception'], 246 expected_stderr_substring='MyException') 247 248 def test_only_check_args(self): 249 self.run_helper( 250 True, 251 arguments=['--only_check_args', '--raise_exception']) 252 253 def test_only_check_args_failure(self): 254 self.run_helper( 255 False, 256 arguments=['--only_check_args', '--banana'], 257 expected_stderr_substring='FATAL Flags parsing error') 258 259 def test_usage_error(self): 260 exitcode, _, _ = self.run_helper( 261 False, 262 arguments=['--raise_usage_error'], 263 expected_stderr_substring=app_test_helper.__doc__) 264 self.assertEqual(1, exitcode) 265 266 def test_usage_error_exitcode(self): 267 exitcode, _, _ = self.run_helper( 268 False, 269 arguments=['--raise_usage_error', '--usage_error_exitcode=88'], 270 expected_stderr_substring=app_test_helper.__doc__) 271 self.assertEqual(88, exitcode) 272 273 def test_exception_handler(self): 274 exception_handler_messages = ( 275 'MyExceptionHandler: first\nMyExceptionHandler: second\n') 276 self.run_helper( 277 False, 278 arguments=['--raise_exception'], 279 expected_stdout_substring=exception_handler_messages) 280 281 def test_exception_handler_not_called(self): 282 _, _, stdout = self.run_helper(True) 283 self.assertNotIn('MyExceptionHandler', stdout) 284 285 def test_print_init_callbacks(self): 286 _, stdout, _ = self.run_helper( 287 expect_success=True, arguments=['--print_init_callbacks']) 288 self.assertIn('before app.run', stdout) 289 self.assertIn('during real_main', stdout) 290 291 292class FlagDeepCopyTest(absltest.TestCase): 293 """Make sure absl flags are copy.deepcopy() compatible.""" 294 295 def test_deepcopyable(self): 296 copy.deepcopy(FLAGS) 297 # Nothing to assert 298 299 300class FlagValuesExternalizationTest(absltest.TestCase): 301 """Test to make sure FLAGS can be serialized out and parsed back in.""" 302 303 @flagsaver.flagsaver 304 def test_nohelp_doesnt_show_help(self): 305 with self.assertRaisesWithPredicateMatch(SystemExit, 306 lambda e: e.code == 1): 307 app.run( 308 len, 309 argv=[ 310 './program', '--nohelp', '--helpshort=false', '--helpfull=0', 311 '--helpxml=f' 312 ]) 313 314 @flagsaver.flagsaver 315 def test_serialize_roundtrip(self): 316 # Use the global 'FLAGS' as the source, to ensure all the framework defined 317 # flags will go through the round trip process. 318 flags.DEFINE_string('testflag', 'testval', 'help', flag_values=FLAGS) 319 320 flags.DEFINE_multi_enum('test_multi_enum_flag', 321 ['x', 'y'], ['x', 'y', 'z'], 322 'Multi enum help.', 323 flag_values=FLAGS) 324 325 class Fruit(enum.Enum): 326 APPLE = 1 327 ORANGE = 2 328 TOMATO = 3 329 flags.DEFINE_multi_enum_class('test_multi_enum_class_flag', 330 ['APPLE', 'TOMATO'], Fruit, 331 'Fruit help.', 332 flag_values=FLAGS) 333 334 new_flag_values = flags.FlagValues() 335 new_flag_values.append_flag_values(FLAGS) 336 337 FLAGS.testflag = 'roundtrip_me' 338 FLAGS.test_multi_enum_flag = ['y', 'z'] 339 FLAGS.test_multi_enum_class_flag = [Fruit.ORANGE, Fruit.APPLE] 340 argv = ['binary_name'] + FLAGS.flags_into_string().splitlines() 341 342 self.assertNotEqual(new_flag_values['testflag'], FLAGS.testflag) 343 self.assertNotEqual(new_flag_values['test_multi_enum_flag'], 344 FLAGS.test_multi_enum_flag) 345 self.assertNotEqual(new_flag_values['test_multi_enum_class_flag'], 346 FLAGS.test_multi_enum_class_flag) 347 new_flag_values(argv) 348 self.assertEqual(new_flag_values.testflag, FLAGS.testflag) 349 self.assertEqual(new_flag_values.test_multi_enum_flag, 350 FLAGS.test_multi_enum_flag) 351 self.assertEqual(new_flag_values.test_multi_enum_class_flag, 352 FLAGS.test_multi_enum_class_flag) 353 del FLAGS.testflag 354 del FLAGS.test_multi_enum_flag 355 del FLAGS.test_multi_enum_class_flag 356 357 358if __name__ == '__main__': 359 absltest.main() 360