#!/usr/bin/env vpython3
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# pylint: disable=protected-access

import sys
import unittest
from xml.etree import ElementTree

import emma_coverage_stats

import mock  # pylint: disable=import-error

EMPTY_COVERAGE_STATS_DICT = {
  'files': {},
  'patch': {
    'incremental': {
      'covered': 0, 'total': 0
    }
  }
}


class _EmmaHtmlParserTest(unittest.TestCase):
  """Tests for _EmmaHtmlParser.

  Uses modified EMMA report HTML that contains only the subset of tags needed
  for test verification.
  """

  def setUp(self):
    self.emma_dir = 'fake/dir/'
    self.parser = emma_coverage_stats._EmmaHtmlParser(self.emma_dir)
    self.simple_html = '<TR><TD CLASS="p">Test HTML</TD></TR>'
    self.index_html = (
      '<HTML>'
        '<BODY>'
          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
          '<TABLE CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
          '<TABLE CLASS="it" CELLSPACING="0">'
          '</TABLE>'
          '<TABLE CELLSPACING="0" WIDTH="100%">'
            '<TR>'
              '<TH CLASS="f">name</TH>'
              '<TH>class, %</TH>'
              '<TH>method, %</TH>'
              '<TH>block, %</TH>'
              '<TH>line, %</TH>'
            '</TR>'
            '<TR CLASS="o">'
              '<TD><A HREF="_files/0.html"'
              '>org.chromium.chrome.browser</A></TD>'
              '<TD CLASS="h">0%   (0/3)</TD>'
            '</TR>'
            '<TR>'
              '<TD><A HREF="_files/1.html"'
              '>org.chromium.chrome.browser.tabmodel</A></TD>'
              '<TD CLASS="h">0%   (0/8)</TD>'
            '</TR>'
          '</TABLE>'
          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
        '</BODY>'
      '</HTML>'
    )
    self.package_1_class_list_html = (
      '<HTML>'
        '<BODY>'
          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
          '<TABLE CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
          '<TABLE CELLSPACING="0" WIDTH="100%">'
            '<TR>'
              '<TH CLASS="f">name</TH>'
              '<TH>class, %</TH>'
              '<TH>method, %</TH>'
              '<TH>block, %</TH>'
              '<TH>line, %</TH>'
            '</TR>'
            '<TR CLASS="o">'
              '<TD><A HREF="1e.html">IntentHelper.java</A></TD>'
              '<TD CLASS="h">0%   (0/3)</TD>'
              '<TD CLASS="h">0%   (0/9)</TD>'
              '<TD CLASS="h">0%   (0/97)</TD>'
              '<TD CLASS="h">0%   (0/26)</TD>'
            '</TR>'
          '</TABLE>'
          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
        '</BODY>'
      '</HTML>'
    )
    self.package_2_class_list_html = (
      '<HTML>'
        '<BODY>'
          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
          '<TABLE CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
          '<TABLE CELLSPACING="0" WIDTH="100%">'
            '<TR>'
              '<TH CLASS="f">name</TH>'
              '<TH>class, %</TH>'
              '<TH>method, %</TH>'
              '<TH>block, %</TH>'
              '<TH>line, %</TH>'
            '</TR>'
            '<TR CLASS="o">'
              '<TD><A HREF="1f.html">ContentSetting.java</A></TD>'
              '<TD CLASS="h">0%   (0/1)</TD>'
            '</TR>'
            '<TR>'
              '<TD><A HREF="20.html">DevToolsServer.java</A></TD>'
            '</TR>'
            '<TR CLASS="o">'
              '<TD><A HREF="21.html">FileProviderHelper.java</A></TD>'
            '</TR>'
            '<TR>'
              '<TD><A HREF="22.html">ContextualMenuBar.java</A></TD>'
            '</TR>'
            '<TR CLASS="o">'
              '<TD><A HREF="23.html">AccessibilityUtil.java</A></TD>'
            '</TR>'
            '<TR>'
              '<TD><A HREF="24.html">NavigationPopup.java</A></TD>'
            '</TR>'
          '</TABLE>'
          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
          '</TABLE>'
        '</BODY>'
      '</HTML>'
    )
    self.partially_covered_tr_html = (
      '<TR CLASS="p">'
        '<TD CLASS="l" TITLE="78% line coverage (7 out of 9)">108</TD>'
        '<TD TITLE="78% line coverage (7 out of 9 instructions)">'
          'if (index &lt; 0 || index = mSelectors.size()) index = 0;</TD>'
      '</TR>'
    )
    self.covered_tr_html = (
      '<TR CLASS="c">'
        '<TD CLASS="l">110</TD>'
        '<TD>        if (mSelectors.get(index) != null) {</TD>'
      '</TR>'
    )
    self.not_executable_tr_html = (
      '<TR>'
        '<TD CLASS="l">109</TD>'
        '<TD> </TD>'
      '</TR>'
    )
    self.tr_with_extra_a_tag = (
      '<TR CLASS="z">'
        '<TD CLASS="l">'
          '<A name="1f">54</A>'
        '</TD>'
        '<TD>            }</TD>'
      '</TR>'
    )

  def testInit(self):
    emma_dir = self.emma_dir
    parser = emma_coverage_stats._EmmaHtmlParser(emma_dir)
    self.assertEqual(parser._base_dir, emma_dir)
    self.assertEqual(parser._emma_files_path, 'fake/dir/_files')
    self.assertEqual(parser._index_path, 'fake/dir/index.html')

  def testFindElements_basic(self):
    read_values = [self.simple_html]
    found, _ = MockOpenForFunction(self.parser._FindElements, read_values,
                                   file_path='fake', xpath_selector='.//TD')
    self.assertIs(type(found), list)
    self.assertIs(type(found[0]), ElementTree.Element)
    self.assertEqual(found[0].text, 'Test HTML')

  def testFindElements_multipleElements(self):
    multiple_trs = self.not_executable_tr_html + self.covered_tr_html
    read_values = ['<div>' + multiple_trs + '</div>']
    found, _ = MockOpenForFunction(self.parser._FindElements, read_values,
                                   file_path='fake', xpath_selector='.//TR')
    self.assertEqual(2, len(found))

  def testFindElements_noMatch(self):
    read_values = [self.simple_html]
    found, _ = MockOpenForFunction(self.parser._FindElements, read_values,
                                   file_path='fake', xpath_selector='.//TR')
    self.assertEqual(found, [])

  def testFindElements_badFilePath(self):
    with self.assertRaises(IOError):
      with mock.patch('os.path.exists', return_value=False):
        self.parser._FindElements('fake', xpath_selector='//tr')

  def testGetPackageNameToEmmaFileDict_basic(self):
    if sys.version_info.major < 3:
      expected_dict = {
          'org.chromium.chrome.browser.AccessibilityUtil.java':
          'fake/dir/_files/23.html',
          'org.chromium.chrome.browser.ContextualMenuBar.java':
          'fake/dir/_files/22.html',
          'org.chromium.chrome.browser.tabmodel.IntentHelper.java':
          'fake/dir/_files/1e.html',
          'org.chromium.chrome.browser.ContentSetting.java':
          'fake/dir/_files/1f.html',
          'org.chromium.chrome.browser.DevToolsServer.java':
          'fake/dir/_files/20.html',
          'org.chromium.chrome.browser.NavigationPopup.java':
          'fake/dir/_files/24.html',
          'org.chromium.chrome.browser.FileProviderHelper.java':
          'fake/dir/_files/21.html'
      }
    else:
      expected_dict = {
          'org.chromium.chrome.browser.IntentHelper.java':
          'fake/dir/_files/1e.html',
          'org.chromium.chrome.browser.tabmodel.AccessibilityUtil.java':
          'fake/dir/_files/23.html',
          'org.chromium.chrome.browser.tabmodel.ContextualMenuBar.java':
          'fake/dir/_files/22.html',
          'org.chromium.chrome.browser.tabmodel.ContentSetting.java':
          'fake/dir/_files/1f.html',
          'org.chromium.chrome.browser.tabmodel.DevToolsServer.java':
          'fake/dir/_files/20.html',
          'org.chromium.chrome.browser.tabmodel.NavigationPopup.java':
          'fake/dir/_files/24.html',
          'org.chromium.chrome.browser.tabmodel.FileProviderHelper.java':
          'fake/dir/_files/21.html'
      }
    read_values = [self.index_html, self.package_1_class_list_html,
                   self.package_2_class_list_html]
    return_dict, mock_open = MockOpenForFunction(
        self.parser.GetPackageNameToEmmaFileDict, read_values)

    self.assertDictEqual(return_dict, expected_dict)
    self.assertEqual(mock_open.call_count, 3)
    if sys.version_info.major < 3:
      calls = [
          mock.call('fake/dir/index.html'),
          mock.call('fake/dir/_files/1.html'),
          mock.call('fake/dir/_files/0.html')
      ]
    else:
      calls = [
          mock.call('fake/dir/index.html'),
          mock.call('fake/dir/_files/0.html'),
          mock.call('fake/dir/_files/1.html')
      ]
    mock_open.assert_has_calls(calls)

  def testGetPackageNameToEmmaFileDict_noPackageElements(self):
    self.parser._FindElements = mock.Mock(return_value=[])
    return_dict = self.parser.GetPackageNameToEmmaFileDict()
    self.assertDictEqual({}, return_dict)

  def testGetLineCoverage_status_basic(self):
    line_coverage = self.GetLineCoverageWithFakeElements([self.covered_tr_html])
    self.assertEqual(line_coverage[0].covered_status,
                     emma_coverage_stats.COVERED)

  def testGetLineCoverage_status_statusMissing(self):
    line_coverage = self.GetLineCoverageWithFakeElements(
        [self.not_executable_tr_html])
    self.assertEqual(line_coverage[0].covered_status,
                     emma_coverage_stats.NOT_EXECUTABLE)

  def testGetLineCoverage_fractionalCoverage_basic(self):
    line_coverage = self.GetLineCoverageWithFakeElements([self.covered_tr_html])
    self.assertEqual(line_coverage[0].fractional_line_coverage, 1.0)

  def testGetLineCoverage_fractionalCoverage_partial(self):
    line_coverage = self.GetLineCoverageWithFakeElements(
        [self.partially_covered_tr_html])
    self.assertEqual(line_coverage[0].fractional_line_coverage, 0.78)

  def testGetLineCoverage_lineno_basic(self):
    line_coverage = self.GetLineCoverageWithFakeElements([self.covered_tr_html])
    self.assertEqual(line_coverage[0].lineno, 110)

  def testGetLineCoverage_lineno_withAlternativeHtml(self):
    line_coverage = self.GetLineCoverageWithFakeElements(
        [self.tr_with_extra_a_tag])
    self.assertEqual(line_coverage[0].lineno, 54)

  def testGetLineCoverage_source(self):
    self.parser._FindElements = mock.Mock(
        return_value=[ElementTree.fromstring(self.covered_tr_html)])
    line_coverage = self.parser.GetLineCoverage('fake_path')
    self.assertEqual(line_coverage[0].source,
                     '        if (mSelectors.get(index) != null) {')

  def testGetLineCoverage_multipleElements(self):
    line_coverage = self.GetLineCoverageWithFakeElements(
        [self.covered_tr_html, self.partially_covered_tr_html,
         self.tr_with_extra_a_tag])
    self.assertEqual(len(line_coverage), 3)

  def GetLineCoverageWithFakeElements(self, html_elements):
    """Wraps GetLineCoverage so mock HTML can easily be used.

    Args:
      html_elements: List of strings each representing an HTML element.

    Returns:
      A list of LineCoverage objects.
    """
    elements = [ElementTree.fromstring(string) for string in html_elements]
    with mock.patch('emma_coverage_stats._EmmaHtmlParser._FindElements',
                    return_value=elements):
      return self.parser.GetLineCoverage('fake_path')


class _EmmaCoverageStatsTest(unittest.TestCase):
  """Tests for _EmmaCoverageStats."""

  def setUp(self):
    self.good_source_to_emma = {
      '/path/to/1/File1.java': '/emma/1.html',
      '/path/2/File2.java': '/emma/2.html',
      '/path/2/File3.java': '/emma/3.html'
    }
    self.line_coverage = [
        emma_coverage_stats.LineCoverage(
            1, '', emma_coverage_stats.COVERED, 1.0),
        emma_coverage_stats.LineCoverage(
            2, '', emma_coverage_stats.COVERED, 1.0),
        emma_coverage_stats.LineCoverage(
            3, '', emma_coverage_stats.NOT_EXECUTABLE, 1.0),
        emma_coverage_stats.LineCoverage(
            4, '', emma_coverage_stats.NOT_COVERED, 1.0),
        emma_coverage_stats.LineCoverage(
            5, '', emma_coverage_stats.PARTIALLY_COVERED, 0.85),
        emma_coverage_stats.LineCoverage(
            6, '', emma_coverage_stats.PARTIALLY_COVERED, 0.20)
    ]
    self.lines_for_coverage = [1, 3, 5, 6]
    with mock.patch('emma_coverage_stats._EmmaHtmlParser._FindElements',
                    return_value=[]):
      self.simple_coverage = emma_coverage_stats._EmmaCoverageStats(
          'fake_dir', {})

  def testInit(self):
    coverage_stats = self.simple_coverage
    self.assertIsInstance(coverage_stats._emma_parser,
                          emma_coverage_stats._EmmaHtmlParser)
    self.assertIsInstance(coverage_stats._source_to_emma, dict)

  def testNeedsCoverage_withExistingJavaFile(self):
    test_file = '/path/to/file/File.java'
    with mock.patch('os.path.exists', return_value=True):
      self.assertTrue(
          emma_coverage_stats._EmmaCoverageStats.NeedsCoverage(test_file))

  def testNeedsCoverage_withNonJavaFile(self):
    test_file = '/path/to/file/File.c'
    with mock.patch('os.path.exists', return_value=True):
      self.assertFalse(
          emma_coverage_stats._EmmaCoverageStats.NeedsCoverage(test_file))

  def testNeedsCoverage_fileDoesNotExist(self):
    test_file = '/path/to/file/File.java'
    with mock.patch('os.path.exists', return_value=False):
      self.assertFalse(
          emma_coverage_stats._EmmaCoverageStats.NeedsCoverage(test_file))

  def testGetPackageNameFromFile_basic(self):
    test_file_text = """// Test Copyright
    package org.chromium.chrome.browser;
    import android.graphics.RectF;"""
    result_package, _ = MockOpenForFunction(
        emma_coverage_stats._EmmaCoverageStats.GetPackageNameFromFile,
        [test_file_text], file_path='/path/to/file/File.java')
    self.assertEqual(result_package, 'org.chromium.chrome.browser.File.java')

  def testGetPackageNameFromFile_noPackageStatement(self):
    result_package, _ = MockOpenForFunction(
        emma_coverage_stats._EmmaCoverageStats.GetPackageNameFromFile,
        ['not a package statement'], file_path='/path/to/file/File.java')
    self.assertIsNone(result_package)

  def testGetSummaryStatsForLines_basic(self):
    covered, total = self.simple_coverage.GetSummaryStatsForLines(
        self.line_coverage)
    self.assertEqual(covered, 3.05)
    self.assertEqual(total, 5)

  def testGetSourceFileToEmmaFileDict(self):
    package_names = {
      '/path/to/1/File1.java': 'org.fake.one.File1.java',
      '/path/2/File2.java': 'org.fake.File2.java',
      '/path/2/File3.java': 'org.fake.File3.java'
    }
    package_to_emma = {
      'org.fake.one.File1.java': '/emma/1.html',
      'org.fake.File2.java': '/emma/2.html',
      'org.fake.File3.java': '/emma/3.html'
    }
    with mock.patch('os.path.exists', return_value=True):
      coverage_stats = self.simple_coverage
      coverage_stats._emma_parser.GetPackageNameToEmmaFileDict = mock.MagicMock(
          return_value=package_to_emma)
      coverage_stats.GetPackageNameFromFile = lambda x: package_names[x]
      result_dict = coverage_stats._GetSourceFileToEmmaFileDict(
          list(package_names.keys()))
    self.assertDictEqual(result_dict, self.good_source_to_emma)

  def testGetCoverageDictForFile(self):
    line_coverage = self.line_coverage
    self.simple_coverage._emma_parser.GetLineCoverage = lambda x: line_coverage
    self.simple_coverage._source_to_emma = {'/fake/src': 'fake/emma'}
    lines = self.lines_for_coverage
    expected_dict = {
      'absolute': {
        'covered': 3.05,
        'total': 5
      },
      'incremental': {
        'covered': 2.05,
        'total': 3
      },
      'source': [
        {
          'line': line_coverage[0].source,
          'coverage': line_coverage[0].covered_status,
          'changed': True,
          'fractional_coverage': line_coverage[0].fractional_line_coverage,
        },
        {
          'line': line_coverage[1].source,
          'coverage': line_coverage[1].covered_status,
          'changed': False,
          'fractional_coverage': line_coverage[1].fractional_line_coverage,
        },
        {
          'line': line_coverage[2].source,
          'coverage': line_coverage[2].covered_status,
          'changed': True,
          'fractional_coverage': line_coverage[2].fractional_line_coverage,
        },
        {
          'line': line_coverage[3].source,
          'coverage': line_coverage[3].covered_status,
          'changed': False,
          'fractional_coverage': line_coverage[3].fractional_line_coverage,
        },
        {
          'line': line_coverage[4].source,
          'coverage': line_coverage[4].covered_status,
          'changed': True,
          'fractional_coverage': line_coverage[4].fractional_line_coverage,
        },
        {
          'line': line_coverage[5].source,
          'coverage': line_coverage[5].covered_status,
          'changed': True,
          'fractional_coverage': line_coverage[5].fractional_line_coverage,
        }
      ]
    }
    result_dict = self.simple_coverage.GetCoverageDictForFile(
        '/fake/src', lines)
    self.assertDictEqual(result_dict, expected_dict)

  def testGetCoverageDictForFile_emptyCoverage(self):
    expected_dict = {
      'absolute': {'covered': 0, 'total': 0},
      'incremental': {'covered': 0, 'total': 0},
      'source': []
    }
    self.simple_coverage._emma_parser.GetLineCoverage = lambda x: []
    self.simple_coverage._source_to_emma = {'fake_dir': 'fake/emma'}
    result_dict = self.simple_coverage.GetCoverageDictForFile('fake_dir', {})
    self.assertDictEqual(result_dict, expected_dict)

  def testGetCoverageDictForFile_missingCoverage(self):
    self.simple_coverage._source_to_emma = {}
    result_dict = self.simple_coverage.GetCoverageDictForFile('fake_file', {})
    self.assertIsNone(result_dict)

  def testGetCoverageDict_basic(self):
    files_for_coverage = {
      '/path/to/1/File1.java': [1, 3, 4],
      '/path/2/File2.java': [1, 2]
    }
    self.simple_coverage._source_to_emma = {
      '/path/to/1/File1.java': 'emma_1',
      '/path/2/File2.java': 'emma_2'
    }
    coverage_info = {
      'emma_1': [
        emma_coverage_stats.LineCoverage(
            1, '', emma_coverage_stats.COVERED, 1.0),
        emma_coverage_stats.LineCoverage(
            2, '', emma_coverage_stats.PARTIALLY_COVERED, 0.5),
        emma_coverage_stats.LineCoverage(
            3, '', emma_coverage_stats.NOT_EXECUTABLE, 1.0),
        emma_coverage_stats.LineCoverage(
            4, '', emma_coverage_stats.COVERED, 1.0)
      ],
      'emma_2': [
        emma_coverage_stats.LineCoverage(
            1, '', emma_coverage_stats.NOT_COVERED, 1.0),
        emma_coverage_stats.LineCoverage(
            2, '', emma_coverage_stats.COVERED, 1.0)
      ]
    }
    expected_dict = {
      'files': {
        '/path/2/File2.java': {
          'absolute': {'covered': 1, 'total': 2},
          'incremental': {'covered': 1, 'total': 2},
          'source': [{'changed': True, 'coverage': 0,
                      'line': '', 'fractional_coverage': 1.0},
                     {'changed': True, 'coverage': 1,
                      'line': '', 'fractional_coverage': 1.0}]
        },
        '/path/to/1/File1.java': {
          'absolute': {'covered': 2.5, 'total': 3},
          'incremental': {'covered': 2, 'total': 2},
          'source': [{'changed': True, 'coverage': 1,
                      'line': '', 'fractional_coverage': 1.0},
                     {'changed': False, 'coverage': 2,
                      'line': '', 'fractional_coverage': 0.5},
                     {'changed': True, 'coverage': -1,
                      'line': '', 'fractional_coverage': 1.0},
                     {'changed': True, 'coverage': 1,
                      'line': '', 'fractional_coverage': 1.0}]
        }
      },
      'patch': {'incremental': {'covered': 3, 'total': 4}}
    }
    # Return the relevant coverage info for each file.
    self.simple_coverage._emma_parser.GetLineCoverage = (
        lambda x: coverage_info[x])
    result_dict = self.simple_coverage.GetCoverageDict(files_for_coverage)
    self.assertDictEqual(result_dict, expected_dict)

  def testGetCoverageDict_noCoverage(self):
    result_dict = self.simple_coverage.GetCoverageDict({})
    self.assertDictEqual(result_dict, EMPTY_COVERAGE_STATS_DICT)


class EmmaCoverageStatsGenerateCoverageReport(unittest.TestCase):
  """Tests for GenerateCoverageReport."""

  def testGenerateCoverageReport_missingJsonFile(self):
    with self.assertRaises(IOError):
      with mock.patch('os.path.exists', return_value=False):
        emma_coverage_stats.GenerateCoverageReport('', '', '')

  def testGenerateCoverageReport_invalidJsonFile(self):
    with self.assertRaises(ValueError):
      with mock.patch('os.path.exists', return_value=True):
        MockOpenForFunction(emma_coverage_stats.GenerateCoverageReport, [''],
                            line_coverage_file='', out_file_path='',
                            coverage_dir='')


def MockOpenForFunction(func, side_effects, **kwargs):
  """Allows easy mock open and read for callables that open multiple files.

  Will mock the python open function in a way such that each time read() is
  called on an open file, the next element in |side_effects| is returned. This
  makes it easier to test functions that call open() multiple times.

  Args:
    func: The callable to invoke once mock files are setup.
    side_effects: A list of return values for each file to return once read.
      Length of list should be equal to the number calls to open in |func|.
    **kwargs: Keyword arguments to be passed to |func|.

  Returns:
    A tuple containing the return value of |func| and the MagicMock object used
      to mock all calls to open respectively.
  """
  mock_open = mock.mock_open()
  mock_open.side_effect = [mock.mock_open(read_data=side_effect).return_value
                           for side_effect in side_effects]
  if sys.version_info.major < 3:
    open_builtin_path = '__builtin__.open'
  else:
    open_builtin_path = 'builtins.open'
  with mock.patch(open_builtin_path, mock_open):
    return func(**kwargs), mock_open


if __name__ == '__main__':
  # Suppress logging messages.
  unittest.main(buffer=True)
