#!/usr/bin/env vpython3
# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unittests for test_result_util.py."""

import collections
import copy
import mock
import unittest

import test_result_util
from test_result_util import TestResult, TestStatus, ResultCollection
import test_runner_test

FAKE_TEST_LOC = {'repo': 'https://test', 'fileName': '//test.cc'}
PASSED_RESULT = TestResult(
  'passed/test', TestStatus.PASS, duration=1233, test_log='Logs'
)
PASSED_RESULT_WITH_LOC = TestResult(
  'passed/test',
  TestStatus.PASS,
  duration=1233,
  test_log='Logs',
  test_loc=FAKE_TEST_LOC,
)
FAILED_RESULT = TestResult(
  'failed/test', TestStatus.FAIL, duration=1233, test_log='line1\nline2'
)
FAILED_RESULT_DUPLICATE = TestResult(
  'failed/test', TestStatus.FAIL, test_log='line3\nline4'
)
DISABLED_RESULT = TestResult(
  'disabled/test',
  TestStatus.SKIP,
  expected_status=TestStatus.SKIP,
  attachments={'name': '/path/to/name'},
)
UNEXPECTED_SKIPPED_RESULT = TestResult(
  'unexpected/skipped_test', TestStatus.SKIP
)
CRASHED_RESULT = TestResult('crashed/test', TestStatus.CRASH)
FLAKY_PASS_RESULT = TestResult('flaky/test', TestStatus.PASS)
FLAKY_FAIL_RESULT = TestResult(
  'flaky/test', TestStatus.FAIL, test_log='line1\nline2'
)
ABORTED_RESULT = TestResult('aborted/test', TestStatus.ABORT)


class UtilTest(test_runner_test.TestCase):
  """Tests util methods in test_result_util module."""

  def test_validate_kwargs(self):
    """Tests _validate_kwargs."""
    with self.assertRaises(AssertionError) as context:
      TestResult('name', TestStatus.PASS, unknown='foo')
    expected_message = "Invalid keyword argument(s) in"
    self.assertTrue(expected_message in str(context.exception))
    with self.assertRaises(AssertionError) as context:
      ResultCollection(test_log='foo')
    expected_message = "Invalid keyword argument(s) in"
    self.assertTrue(expected_message in str(context.exception))

  def test_validate_test_status(self):
    """Tests exception raised from validation."""
    with self.assertRaises(TypeError) as context:
      test_result_util._validate_test_status('TIMEOUT')
    expected_message = 'Invalid test status: TIMEOUT. Should be one of'
    self.assertTrue(expected_message in str(context.exception))

  def test_to_standard_json_literal(self):
    """Tests _to_standard_json_literal."""
    status = test_result_util._to_standard_json_literal(TestStatus.FAIL)
    self.assertEqual(status, 'FAIL')
    status = test_result_util._to_standard_json_literal(TestStatus.ABORT)
    self.assertEqual(status, 'TIMEOUT')


class TestResultTest(test_runner_test.TestCase):
  """Tests TestResult class APIs."""

  def test_init(self):
    """Tests class initialization."""
    test_result = PASSED_RESULT
    self.assertEqual(test_result.name, 'passed/test')
    self.assertEqual(test_result.status, TestStatus.PASS)
    self.assertEqual(test_result.expected_status, TestStatus.PASS)
    self.assertEqual(test_result.test_log, 'Logs')

  def test_compose_result_sink_tags(self):
    """Tests _compose_result_sink_tags."""
    disabled_test_tags = [
      ('test_name', 'disabled/test'),
      ('disabled_test', 'true'),
    ]
    unexpected_skip_test_tags = [
      ('test_name', 'unexpected/skipped_test'),
      ('disabled_test', 'false'),
    ]
    not_skip_test_tags = [('test_name', 'passed/test')]

    not_skip_test_result = PASSED_RESULT
    self.assertEqual(
      not_skip_test_tags, not_skip_test_result._compose_result_sink_tags()
    )

    disabled_test_result = DISABLED_RESULT
    self.assertEqual(
      disabled_test_tags, disabled_test_result._compose_result_sink_tags()
    )

    unexpected_skip_test_result = UNEXPECTED_SKIPPED_RESULT
    self.assertEqual(
      unexpected_skip_test_tags,
      unexpected_skip_test_result._compose_result_sink_tags(),
    )

  @mock.patch('result_sink_util.ResultSinkClient.post')
  def test_report_to_result_sink(self, mock_post):
    disabled_test_result = DISABLED_RESULT
    client = mock.MagicMock()
    disabled_test_result.report_to_result_sink(client)
    client.post.assert_called_with(
      'disabled/test',
      'SKIP',
      True,
      duration=None,
      test_log='',
      tags=[('test_name', 'disabled/test'), ('disabled_test', 'true')],
      test_loc=None,
      file_artifacts={'name': '/path/to/name'},
    )
    # Duplicate calls will only report once.
    disabled_test_result.report_to_result_sink(client)
    self.assertEqual(client.post.call_count, 1)
    disabled_test_result.report_to_result_sink(client)
    self.assertEqual(client.post.call_count, 1)

    faileded_result = FAILED_RESULT
    client = mock.MagicMock()
    faileded_result.report_to_result_sink(client)
    client.post.assert_called_with(
      'failed/test',
      'FAIL',
      False,
      duration=1233,
      file_artifacts={},
      tags=[('test_name', 'failed/test')],
      test_loc=None,
      test_log='line1\nline2',
    )

    passed_result = PASSED_RESULT_WITH_LOC
    client = mock.MagicMock()
    passed_result.report_to_result_sink(client)
    client.post.assert_called_with(
      'passed/test',
      'PASS',
      True,
      duration=1233,
      file_artifacts={},
      tags=[('test_name', 'passed/test')],
      test_loc=FAKE_TEST_LOC,
      test_log='Logs',
    )


class ResultCollectionTest(test_runner_test.TestCase):
  """Tests ResultCollection class APIs."""

  def setUp(self):
    super(ResultCollectionTest, self).setUp()
    self.full_collection = ResultCollection(
      test_results=[
        PASSED_RESULT,
        FAILED_RESULT,
        FAILED_RESULT_DUPLICATE,
        DISABLED_RESULT,
        UNEXPECTED_SKIPPED_RESULT,
        CRASHED_RESULT,
        FLAKY_PASS_RESULT,
        FLAKY_FAIL_RESULT,
        ABORTED_RESULT,
      ]
    )

  def test_init(self):
    """Tests class initialization."""
    collection = ResultCollection(
      test_results=[PASSED_RESULT, DISABLED_RESULT, UNEXPECTED_SKIPPED_RESULT],
      crashed=True,
    )
    self.assertTrue(collection.crashed)
    self.assertEqual(collection.crash_message, '')
    self.assertEqual(
      collection.test_results,
      [PASSED_RESULT, DISABLED_RESULT, UNEXPECTED_SKIPPED_RESULT],
    )

  def test_add_result(self):
    """Tests add_test_result."""
    collection = ResultCollection(test_results=[FAILED_RESULT])
    collection.add_test_result(DISABLED_RESULT)
    self.assertEqual(collection.test_results, [FAILED_RESULT, DISABLED_RESULT])

  def test_add_result_collection_default(self):
    """Tests add_result_collection default (merge crash info)."""
    collection = ResultCollection(test_results=[FAILED_RESULT])
    self.assertFalse(collection.crashed)
    collection.append_crash_message('Crash1')

    crashed_collection = ResultCollection(
      test_results=[PASSED_RESULT], crashed=True
    )
    crashed_collection.append_crash_message('Crash2')

    collection.add_result_collection(crashed_collection)
    self.assertTrue(collection.crashed)
    self.assertEqual(collection.crash_message, 'Crash1\nCrash2')
    self.assertEqual(collection.test_results, [FAILED_RESULT, PASSED_RESULT])

  def test_add_result_collection_overwrite(self):
    """Tests add_result_collection overwrite."""
    collection = ResultCollection(test_results=[FAILED_RESULT], crashed=True)
    self.assertTrue(collection.crashed)
    collection.append_crash_message('Crash1')

    crashed_collection = ResultCollection(test_results=[PASSED_RESULT])

    collection.add_result_collection(crashed_collection, overwrite_crash=True)
    self.assertFalse(collection.crashed)
    self.assertEqual(collection.crash_message, '')
    self.assertEqual(collection.test_results, [FAILED_RESULT, PASSED_RESULT])

  def test_add_result_collection_ignore(self):
    """Tests add_result_collection overwrite."""
    collection = ResultCollection(test_results=[FAILED_RESULT])
    self.assertFalse(collection.crashed)

    crashed_collection = ResultCollection(
      test_results=[PASSED_RESULT], crashed=True
    )
    crashed_collection.append_crash_message('Crash2')

    collection.add_result_collection(crashed_collection, ignore_crash=True)
    self.assertFalse(collection.crashed)
    self.assertEqual(collection.crash_message, '')
    self.assertEqual(collection.test_results, [FAILED_RESULT, PASSED_RESULT])

  def test_add_results(self):
    """Tests add_results."""
    collection = ResultCollection(test_results=[PASSED_RESULT])
    collection.add_results([FAILED_RESULT, DISABLED_RESULT])
    self.assertEqual(
      collection.test_results, [PASSED_RESULT, FAILED_RESULT, DISABLED_RESULT]
    )

  def test_add_name_prefix_to_tests(self):
    """Tests add_name_prefix_to_tests."""
    passed = copy.copy(PASSED_RESULT)
    disabeld = copy.copy(DISABLED_RESULT)
    collection = ResultCollection(test_results=[passed, disabeld])
    some_prefix = 'Some/prefix'
    collection.add_name_prefix_to_tests(some_prefix)
    for test_result in collection.test_results:
      self.assertTrue(test_result.name.startswith(some_prefix))

  def test_add_test_names_status(self):
    """Tests add_test_names_status."""
    test_names = ['test1', 'test2', 'test3']
    collection = ResultCollection(test_results=[PASSED_RESULT])
    collection.add_test_names_status(test_names, TestStatus.SKIP)
    disabled_test_names = ['test4', 'test5', 'test6']
    collection.add_test_names_status(
      disabled_test_names, TestStatus.SKIP, expected_status=TestStatus.SKIP
    )
    self.assertEqual(collection.test_results[0], PASSED_RESULT)
    unexpected_skipped = collection.tests_by_expression(
      lambda t: not t.expected() and t.status == TestStatus.SKIP
    )
    self.assertEqual(unexpected_skipped, set(['test1', 'test2', 'test3']))
    self.assertEqual(
      collection.disabled_tests(), set(['test4', 'test5', 'test6'])
    )

  @mock.patch('test_result_util.TestResult.report_to_result_sink')
  @mock.patch('result_sink_util.ResultSinkClient.close')
  @mock.patch('result_sink_util.ResultSinkClient.__init__', return_value=None)
  def test_add_and_report_test_names_status(
    self, mock_sink_init, mock_sink_close, mock_report
  ):
    """Tests add_test_names_status."""
    test_names = ['test1', 'test2', 'test3']
    collection = ResultCollection(test_results=[PASSED_RESULT])
    collection.add_and_report_test_names_status(test_names, TestStatus.SKIP)
    self.assertEqual(collection.test_results[0], PASSED_RESULT)
    unexpected_skipped = collection.tests_by_expression(
      lambda t: not t.expected() and t.status == TestStatus.SKIP
    )
    self.assertEqual(unexpected_skipped, set(['test1', 'test2', 'test3']))
    self.assertEqual(1, len(mock_sink_init.mock_calls))
    self.assertEqual(3, len(mock_report.mock_calls))
    self.assertEqual(1, len(mock_sink_close.mock_calls))

  def testappend_crash_message(self):
    """Tests append_crash_message."""
    collection = ResultCollection(test_results=[PASSED_RESULT])
    collection.append_crash_message('Crash message 1.')
    self.assertEqual(collection.crash_message, 'Crash message 1.')
    collection.append_crash_message('Crash message 2.')
    self.assertEqual(
      collection.crash_message, 'Crash message 1.\nCrash message 2.'
    )

  def test_tests_by_expression(self):
    """Tests tests_by_expression."""
    collection = self.full_collection
    exp = lambda result: result.status == TestStatus.SKIP
    skipped_tests = collection.tests_by_expression(exp)
    self.assertEqual(
      skipped_tests, set(['unexpected/skipped_test', 'disabled/test'])
    )

  def test_get_spcific_tests(self):
    """Tests getting sets of tests of specific status."""
    collection = self.full_collection
    self.assertEqual(
      collection.all_test_names(),
      set(
        [
          'passed/test',
          'disabled/test',
          'failed/test',
          'unexpected/skipped_test',
          'crashed/test',
          'flaky/test',
          'aborted/test',
        ]
      ),
    )
    self.assertEqual(collection.crashed_tests(), set(['crashed/test']))
    self.assertEqual(collection.disabled_tests(), set(['disabled/test']))
    self.assertEqual(
      collection.expected_tests(),
      set(['passed/test', 'disabled/test', 'flaky/test']),
    )
    self.assertEqual(
      collection.unexpected_tests(),
      set(
        [
          'failed/test',
          'unexpected/skipped_test',
          'crashed/test',
          'flaky/test',
          'aborted/test',
        ]
      ),
    )
    self.assertEqual(
      collection.passed_tests(), set(['passed/test', 'flaky/test'])
    )
    self.assertEqual(
      collection.failed_tests(), set(['failed/test', 'flaky/test'])
    )
    self.assertEqual(collection.flaky_tests(), set(['flaky/test']))
    self.assertEqual(
      collection.never_expected_tests(),
      set(
        [
          'failed/test',
          'unexpected/skipped_test',
          'crashed/test',
          'aborted/test',
        ]
      ),
    )
    self.assertEqual(
      collection.pure_expected_tests(), set(['passed/test', 'disabled/test'])
    )

  def test_add_and_report_crash(self):
    """Tests add_and_report_crash."""
    collection = copy.copy(self.full_collection)

    collection.set_crashed_with_prefix('Prefix Line')
    self.assertEqual(collection.crash_message, 'Prefix Line\n')
    self.assertTrue(collection.crashed)

  @mock.patch('test_result_util.TestResult.report_to_result_sink')
  @mock.patch('result_sink_util.ResultSinkClient.close')
  @mock.patch('result_sink_util.ResultSinkClient.__init__', return_value=None)
  def test_report_to_result_sink(
    self, mock_sink_init, mock_sink_close, mock_report
  ):
    """Tests report_to_result_sink."""
    collection = copy.copy(self.full_collection)
    collection.report_to_result_sink()

    mock_sink_init.assert_called_once()
    self.assertEqual(len(collection.test_results), len(mock_report.mock_calls))
    mock_sink_close.assert_called()

  @mock.patch('shard_util.gtest_shard_index', return_value=0)
  @mock.patch('time.time', return_value=10000)
  def test_standard_json_output(self, *args):
    """Tests standard_json_output."""
    passed_test_value = {
      'expected': 'PASS',
      'actual': 'PASS',
      'shard': 0,
      'is_unexpected': False,
    }
    failed_test_value = {
      'expected': 'PASS',
      'actual': 'FAIL FAIL',
      'shard': 0,
      'is_unexpected': True,
    }
    disabled_test_value = {
      'expected': 'SKIP',
      'actual': 'SKIP',
      'shard': 0,
      'is_unexpected': False,
    }
    unexpected_skip_test_value = {
      'expected': 'PASS',
      'actual': 'SKIP',
      'shard': 0,
      'is_unexpected': True,
    }
    crashed_test_value = {
      'expected': 'PASS',
      'actual': 'CRASH',
      'shard': 0,
      'is_unexpected': True,
    }
    flaky_test_value = {
      'expected': 'PASS',
      'actual': 'PASS FAIL',
      'shard': 0,
      'is_unexpected': False,
      'is_flaky': True,
    }
    aborted_test_value = {
      'expected': 'PASS',
      'actual': 'TIMEOUT',
      'shard': 0,
      'is_unexpected': True,
    }
    expected_tests = collections.OrderedDict()
    expected_tests['passed/test'] = passed_test_value
    expected_tests['failed/test'] = failed_test_value
    expected_tests['disabled/test'] = disabled_test_value
    expected_tests['unexpected/skipped_test'] = unexpected_skip_test_value
    expected_tests['crashed/test'] = crashed_test_value
    expected_tests['flaky/test'] = flaky_test_value
    expected_tests['aborted/test'] = aborted_test_value
    expected_num_failures_by_type = {
      'PASS': 2,
      'FAIL': 1,
      'CRASH': 1,
      'SKIP': 2,
      'TIMEOUT': 1,
    }
    expected_json = {
      'version': 3,
      'path_delimiter': '/',
      'seconds_since_epoch': 10000,
      'interrupted': False,
      'num_failures_by_type': expected_num_failures_by_type,
      'tests': expected_tests,
    }
    self.assertEqual(
      self.full_collection.standard_json_output(path_delimiter='/'),
      expected_json,
    )

  def test_test_runner_logs(self):
    """Test test_runner_logs."""
    expected_logs = collections.OrderedDict()
    expected_logs['passed tests'] = ['passed/test']
    expected_logs['disabled tests'] = ['disabled/test']
    flaky_logs = ['Failure log of attempt 1:', 'line1', 'line2']
    failed_logs = [
      'Failure log of attempt 1:',
      'line1',
      'line2',
      'Failure log of attempt 2:',
      'line3',
      'line4',
    ]
    no_logs = ['Failure log of attempt 1:', '']
    expected_logs['flaked tests'] = {'flaky/test': flaky_logs}
    expected_logs['failed tests'] = {
      'failed/test': failed_logs,
      'crashed/test': no_logs,
      'unexpected/skipped_test': no_logs,
      'aborted/test': no_logs,
    }
    expected_logs['failed/test'] = failed_logs
    expected_logs['unexpected/skipped_test'] = no_logs
    expected_logs['flaky/test'] = flaky_logs
    expected_logs['crashed/test'] = no_logs
    expected_logs['aborted/test'] = no_logs
    generated_logs = self.full_collection.test_runner_logs()
    keys = [
      'passed tests',
      'disabled tests',
      'flaked tests',
      'failed tests',
      'failed/test',
      'unexpected/skipped_test',
      'flaky/test',
      'crashed/test',
      'aborted/test',
    ]
    for key in keys:
      self.assertEqual(generated_logs[key], expected_logs[key])


if __name__ == '__main__':
  unittest.main()
