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"""Test of logging behavior before app.run(), aka flag and logging init()."""
16
17import contextlib
18import io
19import os
20import re
21import sys
22import tempfile
23from unittest import mock
24
25from absl import logging
26from absl.testing import absltest
27
28logging.get_verbosity()  # Access --verbosity before flag parsing.
29# Access --logtostderr before flag parsing.
30logging.get_absl_handler().use_absl_log_file()
31
32
33class Error(Exception):
34  pass
35
36
37@contextlib.contextmanager
38def captured_stderr_filename():
39  """Captures stderr and writes them to a temporary file.
40
41  This uses os.dup/os.dup2 to redirect the stderr fd for capturing standard
42  error of logging at import-time. We cannot mock sys.stderr because on the
43  first log call, a default log handler writing to the mock sys.stderr is
44  registered, and it will never be removed and subsequent logs go to the mock
45  in addition to the real stder.
46
47  Yields:
48    The filename of captured stderr.
49  """
50  stderr_capture_file_fd, stderr_capture_file_name = tempfile.mkstemp()
51  original_stderr_fd = os.dup(sys.stderr.fileno())
52  os.dup2(stderr_capture_file_fd, sys.stderr.fileno())
53  try:
54    yield stderr_capture_file_name
55  finally:
56    os.close(stderr_capture_file_fd)
57    os.dup2(original_stderr_fd, sys.stderr.fileno())
58
59
60# Pre-initialization (aka "import" / __main__ time) test.
61with captured_stderr_filename() as before_set_verbosity_filename:
62  # Warnings and above go to stderr.
63  logging.debug('Debug message at parse time.')
64  logging.info('Info message at parse time.')
65  logging.error('Error message at parse time.')
66  logging.warning('Warning message at parse time.')
67  try:
68    raise Error('Exception reason.')
69  except Error:
70    logging.exception('Exception message at parse time.')
71
72
73logging.set_verbosity(logging.ERROR)
74with captured_stderr_filename() as after_set_verbosity_filename:
75  # Verbosity is set to ERROR, errors and above go to stderr.
76  logging.debug('Debug message at parse time.')
77  logging.info('Info message at parse time.')
78  logging.warning('Warning message at parse time.')
79  logging.error('Error message at parse time.')
80
81
82class LoggingInitWarningTest(absltest.TestCase):
83
84  def test_captured_pre_init_warnings(self):
85    with open(before_set_verbosity_filename) as stderr_capture_file:
86      captured_stderr = stderr_capture_file.read()
87    self.assertNotIn('Debug message at parse time.', captured_stderr)
88    self.assertNotIn('Info message at parse time.', captured_stderr)
89
90    traceback_re = re.compile(
91        r'\nTraceback \(most recent call last\):.*?Error: Exception reason.',
92        re.MULTILINE | re.DOTALL)
93    if not traceback_re.search(captured_stderr):
94      self.fail(
95          'Cannot find traceback message from logging.exception '
96          'in stderr:\n{}'.format(captured_stderr))
97    # Remove the traceback so the rest of the stderr is deterministic.
98    captured_stderr = traceback_re.sub('', captured_stderr)
99    captured_stderr_lines = captured_stderr.splitlines()
100    self.assertLen(captured_stderr_lines, 3)
101    self.assertIn('Error message at parse time.', captured_stderr_lines[0])
102    self.assertIn('Warning message at parse time.', captured_stderr_lines[1])
103    self.assertIn('Exception message at parse time.', captured_stderr_lines[2])
104
105  def test_set_verbosity_pre_init(self):
106    with open(after_set_verbosity_filename) as stderr_capture_file:
107      captured_stderr = stderr_capture_file.read()
108    captured_stderr_lines = captured_stderr.splitlines()
109
110    self.assertNotIn('Debug message at parse time.', captured_stderr)
111    self.assertNotIn('Info message at parse time.', captured_stderr)
112    self.assertNotIn('Warning message at parse time.', captured_stderr)
113    self.assertLen(captured_stderr_lines, 1)
114    self.assertIn('Error message at parse time.', captured_stderr_lines[0])
115
116  def test_no_more_warnings(self):
117    fake_stderr_type = io.BytesIO if bytes is str else io.StringIO
118    with mock.patch('sys.stderr', new=fake_stderr_type()) as mock_stderr:
119      self.assertMultiLineEqual('', mock_stderr.getvalue())
120      logging.warning('Hello. hello. hello. Is there anybody out there?')
121      self.assertNotIn('Logging before flag parsing goes to stderr',
122                       mock_stderr.getvalue())
123    logging.info('A major purpose of this executable is merely not to crash.')
124
125
126if __name__ == '__main__':
127  absltest.main()  # This calls the app.run() init equivalent.
128