# Copyright 2017 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Test of logging behavior before app.run(), aka flag and logging init()."""

import contextlib
import io
import os
import re
import sys
import tempfile
from unittest import mock

from absl import logging
from absl.testing import absltest

logging.get_verbosity()  # Access --verbosity before flag parsing.
# Access --logtostderr before flag parsing.
logging.get_absl_handler().use_absl_log_file()


class Error(Exception):
  pass


@contextlib.contextmanager
def captured_stderr_filename():
  """Captures stderr and writes them to a temporary file.

  This uses os.dup/os.dup2 to redirect the stderr fd for capturing standard
  error of logging at import-time. We cannot mock sys.stderr because on the
  first log call, a default log handler writing to the mock sys.stderr is
  registered, and it will never be removed and subsequent logs go to the mock
  in addition to the real stder.

  Yields:
    The filename of captured stderr.
  """
  stderr_capture_file_fd, stderr_capture_file_name = tempfile.mkstemp()
  original_stderr_fd = os.dup(sys.stderr.fileno())
  os.dup2(stderr_capture_file_fd, sys.stderr.fileno())
  try:
    yield stderr_capture_file_name
  finally:
    os.close(stderr_capture_file_fd)
    os.dup2(original_stderr_fd, sys.stderr.fileno())


# Pre-initialization (aka "import" / __main__ time) test.
with captured_stderr_filename() as before_set_verbosity_filename:
  # Warnings and above go to stderr.
  logging.debug('Debug message at parse time.')
  logging.info('Info message at parse time.')
  logging.error('Error message at parse time.')
  logging.warning('Warning message at parse time.')
  try:
    raise Error('Exception reason.')
  except Error:
    logging.exception('Exception message at parse time.')


logging.set_verbosity(logging.ERROR)
with captured_stderr_filename() as after_set_verbosity_filename:
  # Verbosity is set to ERROR, errors and above go to stderr.
  logging.debug('Debug message at parse time.')
  logging.info('Info message at parse time.')
  logging.warning('Warning message at parse time.')
  logging.error('Error message at parse time.')


class LoggingInitWarningTest(absltest.TestCase):

  def test_captured_pre_init_warnings(self):
    with open(before_set_verbosity_filename) as stderr_capture_file:
      captured_stderr = stderr_capture_file.read()
    self.assertNotIn('Debug message at parse time.', captured_stderr)
    self.assertNotIn('Info message at parse time.', captured_stderr)

    traceback_re = re.compile(
        r'\nTraceback \(most recent call last\):.*?Error: Exception reason.',
        re.MULTILINE | re.DOTALL)
    if not traceback_re.search(captured_stderr):
      self.fail(
          'Cannot find traceback message from logging.exception '
          'in stderr:\n{}'.format(captured_stderr))
    # Remove the traceback so the rest of the stderr is deterministic.
    captured_stderr = traceback_re.sub('', captured_stderr)
    captured_stderr_lines = captured_stderr.splitlines()
    self.assertLen(captured_stderr_lines, 3)
    self.assertIn('Error message at parse time.', captured_stderr_lines[0])
    self.assertIn('Warning message at parse time.', captured_stderr_lines[1])
    self.assertIn('Exception message at parse time.', captured_stderr_lines[2])

  def test_set_verbosity_pre_init(self):
    with open(after_set_verbosity_filename) as stderr_capture_file:
      captured_stderr = stderr_capture_file.read()
    captured_stderr_lines = captured_stderr.splitlines()

    self.assertNotIn('Debug message at parse time.', captured_stderr)
    self.assertNotIn('Info message at parse time.', captured_stderr)
    self.assertNotIn('Warning message at parse time.', captured_stderr)
    self.assertLen(captured_stderr_lines, 1)
    self.assertIn('Error message at parse time.', captured_stderr_lines[0])

  def test_no_more_warnings(self):
    fake_stderr_type = io.BytesIO if bytes is str else io.StringIO
    with mock.patch('sys.stderr', new=fake_stderr_type()) as mock_stderr:
      self.assertMultiLineEqual('', mock_stderr.getvalue())
      logging.warning('Hello. hello. hello. Is there anybody out there?')
      self.assertNotIn('Logging before flag parsing goes to stderr',
                       mock_stderr.getvalue())
    logging.info('A major purpose of this executable is merely not to crash.')


if __name__ == '__main__':
  absltest.main()  # This calls the app.run() init equivalent.
