# 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.

"""Helper script for logging_functional_test."""

import logging as std_logging
import logging.config as std_logging_config
import os
import sys
import threading
import time
import timeit
from unittest import mock

from absl import app
from absl import flags
from absl import logging

FLAGS = flags.FLAGS


class VerboseDel:
  """Dummy class to test __del__ running."""

  def __init__(self, msg):
    self._msg = msg

  def __del__(self):
    sys.stderr.write(self._msg)
    sys.stderr.flush()


def _test_do_logging():
  """Do some log operations."""
  logging.vlog(3, 'This line is VLOG level 3')
  logging.vlog(2, 'This line is VLOG level 2')
  logging.log(2, 'This line is log level 2')
  if logging.vlog_is_on(2):
    logging.log(1, 'VLOG level 1, but only if VLOG level 2 is active')

  logging.vlog(1, 'This line is VLOG level 1')
  logging.log(1, 'This line is log level 1')
  logging.debug('This line is DEBUG')

  logging.vlog(0, 'This line is VLOG level 0')
  logging.log(0, 'This line is log level 0')
  logging.info('Interesting Stuff\0')
  logging.info('Interesting Stuff with Arguments: %d', 42)
  logging.info('%(a)s Stuff with %(b)s',
               {'a': 'Interesting', 'b': 'Dictionary'})

  with mock.patch.object(timeit, 'default_timer') as mock_timer:
    mock_timer.return_value = 0
    while timeit.default_timer() < 9:
      logging.log_every_n_seconds(logging.INFO, 'This should appear 5 times.',
                                  2)
      mock_timer.return_value = mock_timer() + .2

  for i in range(1, 5):
    logging.log_first_n(logging.INFO, 'Info first %d of %d', 2, i, 2)
    logging.log_every_n(logging.INFO, 'Info %d (every %d)', 3, i, 3)

  for i in range(1, 5):
    logging.log_first_n(
        logging.INFO,
        'Callstack aware info first %d of %d',
        2,
        i,
        2,
        use_call_stack=True,
    )
    logging.log_every_n(
        logging.INFO,
        'Callstack aware info %d (every %d)',
        3,
        i,
        3,
        use_call_stack=True,
    )
  # We call this twice on the same line to make sure the call stack is
  # distinguishing function calls on the same line.
  _ = [_test_do_logging_subfunc(), _test_do_logging_subfunc()]

  logging.vlog(-1, 'This line is VLOG level -1')
  logging.log(-1, 'This line is log level -1')
  logging.warning('Worrying Stuff')
  for i in range(1, 5):
    logging.log_first_n(logging.WARNING, 'Warn first %d of %d', 2, i, 2)
    logging.log_every_n(logging.WARNING, 'Warn %d (every %d)', 3, i, 3)

  logging.vlog(-2, 'This line is VLOG level -2')
  logging.log(-2, 'This line is log level -2')
  try:
    raise OSError('Fake Error')
  except OSError:
    saved_exc_info = sys.exc_info()
    logging.exception('An Exception %s')
    logging.exception('Once more, %(reason)s', {'reason': 'just because'})
    logging.error('Exception 2 %s', exc_info=True)
    logging.error('Non-exception', exc_info=False)

  try:
    sys.exc_clear()
  except AttributeError:
    # No sys.exc_clear() in Python 3, but this will clear sys.exc_info() too.
    pass

  logging.error('Exception %s', '3', exc_info=saved_exc_info)
  logging.error('No traceback', exc_info=saved_exc_info[:2] + (None,))

  logging.error('Alarming Stuff')
  for i in range(1, 5):
    logging.log_first_n(logging.ERROR, 'Error first %d of %d', 2, i, 2)
    logging.log_every_n(logging.ERROR, 'Error %d (every %d)', 3, i, 3)
  logging.flush()


def _test_do_logging_subfunc():
  for i in range(1, 5):
    logging.log_first_n(
        logging.INFO,
        'Callstack aware info first %d of %d',
        2,
        i,
        2,
        use_call_stack=True,
    )
    logging.log_every_n(
        logging.INFO,
        'Callstack aware info %d (every %d)',
        3,
        i,
        3,
        use_call_stack=True,
    )


def _test_fatal_main_thread_only():
  """Test logging.fatal from main thread, no other threads running."""
  v = VerboseDel('fatal_main_thread_only main del called\n')
  try:
    logging.fatal('fatal_main_thread_only message')
  finally:
    del v


def _test_fatal_with_other_threads():
  """Test logging.fatal from main thread, other threads running."""

  lock = threading.Lock()
  lock.acquire()

  def sleep_forever(lock=lock):
    v = VerboseDel('fatal_with_other_threads non-main del called\n')
    try:
      lock.release()
      while True:
        time.sleep(10000)
    finally:
      del v

  v = VerboseDel('fatal_with_other_threads main del called\n')
  try:
    # Start new thread
    t = threading.Thread(target=sleep_forever)
    t.start()

    # Wait for other thread
    lock.acquire()
    lock.release()

    # Die
    logging.fatal('fatal_with_other_threads message')
    while True:
      time.sleep(10000)
  finally:
    del v


def _test_fatal_non_main_thread():
  """Test logging.fatal from non main thread."""

  lock = threading.Lock()
  lock.acquire()

  def die_soon(lock=lock):
    v = VerboseDel('fatal_non_main_thread non-main del called\n')
    try:
      # Wait for signal from other thread
      lock.acquire()
      lock.release()
      logging.fatal('fatal_non_main_thread message')
      while True:
        time.sleep(10000)
    finally:
      del v

  v = VerboseDel('fatal_non_main_thread main del called\n')
  try:
    # Start new thread
    t = threading.Thread(target=die_soon)
    t.start()

    # Signal other thread
    lock.release()

    # Wait for it to die
    while True:
      time.sleep(10000)
  finally:
    del v


def _test_critical_from_non_absl_logger():
  """Test CRITICAL logs from non-absl loggers."""

  std_logging.critical('A critical message')


def _test_register_frame_to_skip():
  """Test skipping frames for line number reporting."""

  def _getline():

    def _getline_inner():
      return logging.get_absl_logger().findCaller()[1]

    return _getline_inner()

  # Check register_frame_to_skip function to see if log frame skipping works.
  line1 = _getline()
  line2 = _getline()
  logging.get_absl_logger().register_frame_to_skip(__file__, '_getline')
  line3 = _getline()
  # Both should be line number of the _getline_inner() call.
  assert (line1 == line2), (line1, line2)
  # line3 should be a line number in this function.
  assert (line2 != line3), (line2, line3)


def _test_flush():
  """Test flush in various difficult cases."""
  # Flush, but one of the logfiles is closed
  log_filename = os.path.join(FLAGS.log_dir, 'a_thread_with_logfile.txt')
  with open(log_filename, 'w') as log_file:
    logging.get_absl_handler().python_handler.stream = log_file
  logging.flush()


def _test_stderrthreshold():
  """Tests modifying --stderrthreshold after flag parsing will work."""

  def log_things():
    logging.debug('FLAGS.stderrthreshold=%s, debug log', FLAGS.stderrthreshold)
    logging.info('FLAGS.stderrthreshold=%s, info log', FLAGS.stderrthreshold)
    logging.warning('FLAGS.stderrthreshold=%s, warning log',
                    FLAGS.stderrthreshold)
    logging.error('FLAGS.stderrthreshold=%s, error log', FLAGS.stderrthreshold)

  FLAGS.stderrthreshold = 'debug'
  log_things()
  FLAGS.stderrthreshold = 'info'
  log_things()
  FLAGS.stderrthreshold = 'warning'
  log_things()
  FLAGS.stderrthreshold = 'error'
  log_things()


def _test_std_logging():
  """Tests logs from std logging."""
  std_logging.debug('std debug log')
  std_logging.info('std info log')
  std_logging.warning('std warning log')
  std_logging.error('std error log')


def _test_bad_exc_info():
  """Tests when a bad exc_info valud is provided."""
  logging.info('Bad exc_info', exc_info=(None, None))


def _test_none_exc_info():
  """Tests when exc_info is requested but not available."""
  # Clear exc_info first.
  try:
    sys.exc_clear()
  except AttributeError:
    # No sys.exc_clear() in Python 3, but this will clear sys.exc_info() too.
    pass
  logging.info('None exc_info', exc_info=True)


def _test_unicode():
  """Tests unicode handling."""

  test_names = []

  def log(name, msg, *args):
    """Logs the message, and ensures the same name is not logged again."""
    assert name not in test_names, ('test_unicode expects unique names to work,'
                                    ' found existing name {}').format(name)
    test_names.append(name)

    # Add line separators so that tests can verify the output for each log
    # message.
    sys.stderr.write(f'-- begin {name} --\n')
    logging.info(msg, *args)
    sys.stderr.write(f'-- end {name} --\n')

  log('unicode', 'G\u00eete: Ch\u00e2tonnaye')
  log('unicode % unicode', 'G\u00eete: %s', 'Ch\u00e2tonnaye')
  log('bytes % bytes', 'G\u00eete: %s'.encode(), 'Ch\u00e2tonnaye'.encode())
  log('unicode % bytes', 'G\u00eete: %s', 'Ch\u00e2tonnaye'.encode())
  log('bytes % unicode', 'G\u00eete: %s'.encode(), 'Ch\u00e2tonnaye')
  log(
      'unicode % iso8859-15',
      'G\u00eete: %s',
      'Ch\u00e2tonnaye'.encode('iso-8859-15'),
  )
  log('str % exception', 'exception: %s', Exception('Ch\u00e2tonnaye'))


def _test_log_if_exc_info():
  """Tests exc_info forwarding for log_first_n, log_every_n, etc."""
  try:
    raise OSError('Fake Error')
  except OSError:
    logging.log_first_n(
        logging.ERROR, 'no exc_info log_first_n', 1, exc_info=False
    )
    logging.log_first_n(logging.ERROR, 'exc_info log_first_n', 1, exc_info=True)
    logging.log_every_n(
        logging.ERROR, 'no exc_info log_every_n', 1, exc_info=False
    )
    logging.log_every_n(logging.ERROR, 'exc_info log_every_n', 1, exc_info=True)
    logging.log_every_n_seconds(
        logging.ERROR, 'no exc_info log_every_n_seconds', 0, exc_info=False
    )
    logging.log_every_n_seconds(
        logging.ERROR, 'exc_info log_every_n_seconds', 0, exc_info=True
    )


def main(argv):
  del argv  # Unused.

  test_name = os.environ.get('TEST_NAME', None)
  test_fn = globals().get('_test_%s' % test_name)
  if test_fn is None:
    raise AssertionError('TEST_NAME must be set to a valid value')
  # Flush so previous messages are written to file before we switch to a new
  # file with use_absl_log_file.
  logging.flush()
  if os.environ.get('USE_ABSL_LOG_FILE') == '1':
    logging.get_absl_handler().use_absl_log_file('absl_log_file', FLAGS.log_dir)

  test_fn()


if __name__ == '__main__':
  sys.argv[0] = 'py_argv_0'
  if os.environ.get('CALL_DICT_CONFIG') == '1':
    std_logging_config.dictConfig({'version': 1})
  app.run(main)
