| #!/usr/bin/env python |
| # 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 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 < 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.assertEquals(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): |
| 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'} |
| |
| 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) |
| calls = [mock.call('fake/dir/index.html'), |
| mock.call('fake/dir/_files/1.html'), |
| mock.call('fake/dir/_files/0.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( |
| 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] |
| with mock.patch('__builtin__.open', mock_open): |
| return func(**kwargs), mock_open |
| |
| |
| if __name__ == '__main__': |
| # Suppress logging messages. |
| unittest.main(buffer=True) |