1#!/usr/bin/python
2# Copyright 2015 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6# pylint: disable=protected-access
7
8import unittest
9from xml.etree import ElementTree
10
11import emma_coverage_stats
12from pylib.constants import host_paths
13
14with host_paths.SysPath(host_paths.PYMOCK_PATH):
15  import mock  # pylint: disable=import-error
16
17EMPTY_COVERAGE_STATS_DICT = {
18  'files': {},
19  'patch': {
20    'incremental': {
21      'covered': 0, 'total': 0
22    }
23  }
24}
25
26
27class _EmmaHtmlParserTest(unittest.TestCase):
28  """Tests for _EmmaHtmlParser.
29
30  Uses modified EMMA report HTML that contains only the subset of tags needed
31  for test verification.
32  """
33
34  def setUp(self):
35    self.emma_dir = 'fake/dir/'
36    self.parser = emma_coverage_stats._EmmaHtmlParser(self.emma_dir)
37    self.simple_html = '<TR><TD CLASS="p">Test HTML</TD></TR>'
38    self.index_html = (
39      '<HTML>'
40        '<BODY>'
41          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
42          '</TABLE>'
43          '<TABLE CELLSPACING="0" WIDTH="100%">'
44          '</TABLE>'
45          '<TABLE CLASS="it" CELLSPACING="0">'
46          '</TABLE>'
47          '<TABLE CELLSPACING="0" WIDTH="100%">'
48            '<TR>'
49              '<TH CLASS="f">name</TH>'
50              '<TH>class, %</TH>'
51              '<TH>method, %</TH>'
52              '<TH>block, %</TH>'
53              '<TH>line, %</TH>'
54            '</TR>'
55            '<TR CLASS="o">'
56              '<TD><A HREF="_files/0.html"'
57              '>org.chromium.chrome.browser</A></TD>'
58              '<TD CLASS="h">0%   (0/3)</TD>'
59            '</TR>'
60            '<TR>'
61              '<TD><A HREF="_files/1.html"'
62              '>org.chromium.chrome.browser.tabmodel</A></TD>'
63              '<TD CLASS="h">0%   (0/8)</TD>'
64            '</TR>'
65          '</TABLE>'
66          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
67          '</TABLE>'
68        '</BODY>'
69      '</HTML>'
70    )
71    self.package_1_class_list_html = (
72      '<HTML>'
73        '<BODY>'
74          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
75          '</TABLE>'
76          '<TABLE CELLSPACING="0" WIDTH="100%">'
77          '</TABLE>'
78          '<TABLE CELLSPACING="0" WIDTH="100%">'
79            '<TR>'
80              '<TH CLASS="f">name</TH>'
81              '<TH>class, %</TH>'
82              '<TH>method, %</TH>'
83              '<TH>block, %</TH>'
84              '<TH>line, %</TH>'
85            '</TR>'
86            '<TR CLASS="o">'
87              '<TD><A HREF="1e.html">IntentHelper.java</A></TD>'
88              '<TD CLASS="h">0%   (0/3)</TD>'
89              '<TD CLASS="h">0%   (0/9)</TD>'
90              '<TD CLASS="h">0%   (0/97)</TD>'
91              '<TD CLASS="h">0%   (0/26)</TD>'
92            '</TR>'
93          '</TABLE>'
94          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
95          '</TABLE>'
96        '</BODY>'
97      '</HTML>'
98    )
99    self.package_2_class_list_html = (
100      '<HTML>'
101        '<BODY>'
102          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
103          '</TABLE>'
104          '<TABLE CELLSPACING="0" WIDTH="100%">'
105          '</TABLE>'
106          '<TABLE CELLSPACING="0" WIDTH="100%">'
107            '<TR>'
108              '<TH CLASS="f">name</TH>'
109              '<TH>class, %</TH>'
110              '<TH>method, %</TH>'
111              '<TH>block, %</TH>'
112              '<TH>line, %</TH>'
113            '</TR>'
114            '<TR CLASS="o">'
115              '<TD><A HREF="1f.html">ContentSetting.java</A></TD>'
116              '<TD CLASS="h">0%   (0/1)</TD>'
117            '</TR>'
118            '<TR>'
119              '<TD><A HREF="20.html">DevToolsServer.java</A></TD>'
120            '</TR>'
121            '<TR CLASS="o">'
122              '<TD><A HREF="21.html">FileProviderHelper.java</A></TD>'
123            '</TR>'
124            '<TR>'
125              '<TD><A HREF="22.html">ContextualMenuBar.java</A></TD>'
126            '</TR>'
127            '<TR CLASS="o">'
128              '<TD><A HREF="23.html">AccessibilityUtil.java</A></TD>'
129            '</TR>'
130            '<TR>'
131              '<TD><A HREF="24.html">NavigationPopup.java</A></TD>'
132            '</TR>'
133          '</TABLE>'
134          '<TABLE CLASS="hdft" CELLSPACING="0" WIDTH="100%">'
135          '</TABLE>'
136        '</BODY>'
137      '</HTML>'
138    )
139    self.partially_covered_tr_html = (
140      '<TR CLASS="p">'
141        '<TD CLASS="l" TITLE="78% line coverage (7 out of 9)">108</TD>'
142        '<TD TITLE="78% line coverage (7 out of 9 instructions)">'
143          'if (index &lt; 0 || index = mSelectors.size()) index = 0;</TD>'
144      '</TR>'
145    )
146    self.covered_tr_html = (
147      '<TR CLASS="c">'
148        '<TD CLASS="l">110</TD>'
149        '<TD>        if (mSelectors.get(index) != null) {</TD>'
150      '</TR>'
151    )
152    self.not_executable_tr_html = (
153      '<TR>'
154        '<TD CLASS="l">109</TD>'
155        '<TD> </TD>'
156      '</TR>'
157    )
158    self.tr_with_extra_a_tag = (
159      '<TR CLASS="z">'
160        '<TD CLASS="l">'
161          '<A name="1f">54</A>'
162        '</TD>'
163        '<TD>            }</TD>'
164      '</TR>'
165    )
166
167  def testInit(self):
168    emma_dir = self.emma_dir
169    parser = emma_coverage_stats._EmmaHtmlParser(emma_dir)
170    self.assertEqual(parser._base_dir, emma_dir)
171    self.assertEqual(parser._emma_files_path, 'fake/dir/_files')
172    self.assertEqual(parser._index_path, 'fake/dir/index.html')
173
174  def testFindElements_basic(self):
175    read_values = [self.simple_html]
176    found, _ = MockOpenForFunction(self.parser._FindElements, read_values,
177                                   file_path='fake', xpath_selector='.//TD')
178    self.assertIs(type(found), list)
179    self.assertIs(type(found[0]), ElementTree.Element)
180    self.assertEqual(found[0].text, 'Test HTML')
181
182  def testFindElements_multipleElements(self):
183    multiple_trs = self.not_executable_tr_html + self.covered_tr_html
184    read_values = ['<div>' + multiple_trs + '</div>']
185    found, _ = MockOpenForFunction(self.parser._FindElements, read_values,
186                                   file_path='fake', xpath_selector='.//TR')
187    self.assertEquals(2, len(found))
188
189  def testFindElements_noMatch(self):
190    read_values = [self.simple_html]
191    found, _ = MockOpenForFunction(self.parser._FindElements, read_values,
192                                   file_path='fake', xpath_selector='.//TR')
193    self.assertEqual(found, [])
194
195  def testFindElements_badFilePath(self):
196    with self.assertRaises(IOError):
197      with mock.patch('os.path.exists', return_value=False):
198        self.parser._FindElements('fake', xpath_selector='//tr')
199
200  def testGetPackageNameToEmmaFileDict_basic(self):
201    expected_dict = {
202      'org.chromium.chrome.browser.AccessibilityUtil.java':
203      'fake/dir/_files/23.html',
204      'org.chromium.chrome.browser.ContextualMenuBar.java':
205      'fake/dir/_files/22.html',
206      'org.chromium.chrome.browser.tabmodel.IntentHelper.java':
207      'fake/dir/_files/1e.html',
208      'org.chromium.chrome.browser.ContentSetting.java':
209      'fake/dir/_files/1f.html',
210      'org.chromium.chrome.browser.DevToolsServer.java':
211      'fake/dir/_files/20.html',
212      'org.chromium.chrome.browser.NavigationPopup.java':
213      'fake/dir/_files/24.html',
214      'org.chromium.chrome.browser.FileProviderHelper.java':
215      'fake/dir/_files/21.html'}
216
217    read_values = [self.index_html, self.package_1_class_list_html,
218                   self.package_2_class_list_html]
219    return_dict, mock_open = MockOpenForFunction(
220        self.parser.GetPackageNameToEmmaFileDict, read_values)
221
222    self.assertDictEqual(return_dict, expected_dict)
223    self.assertEqual(mock_open.call_count, 3)
224    calls = [mock.call('fake/dir/index.html'),
225             mock.call('fake/dir/_files/1.html'),
226             mock.call('fake/dir/_files/0.html')]
227    mock_open.assert_has_calls(calls)
228
229  def testGetPackageNameToEmmaFileDict_noPackageElements(self):
230    self.parser._FindElements = mock.Mock(return_value=[])
231    return_dict = self.parser.GetPackageNameToEmmaFileDict()
232    self.assertDictEqual({}, return_dict)
233
234  def testGetLineCoverage_status_basic(self):
235    line_coverage = self.GetLineCoverageWithFakeElements([self.covered_tr_html])
236    self.assertEqual(line_coverage[0].covered_status,
237                     emma_coverage_stats.COVERED)
238
239  def testGetLineCoverage_status_statusMissing(self):
240    line_coverage = self.GetLineCoverageWithFakeElements(
241        [self.not_executable_tr_html])
242    self.assertEqual(line_coverage[0].covered_status,
243                     emma_coverage_stats.NOT_EXECUTABLE)
244
245  def testGetLineCoverage_fractionalCoverage_basic(self):
246    line_coverage = self.GetLineCoverageWithFakeElements([self.covered_tr_html])
247    self.assertEqual(line_coverage[0].fractional_line_coverage, 1.0)
248
249  def testGetLineCoverage_fractionalCoverage_partial(self):
250    line_coverage = self.GetLineCoverageWithFakeElements(
251        [self.partially_covered_tr_html])
252    self.assertEqual(line_coverage[0].fractional_line_coverage, 0.78)
253
254  def testGetLineCoverage_lineno_basic(self):
255    line_coverage = self.GetLineCoverageWithFakeElements([self.covered_tr_html])
256    self.assertEqual(line_coverage[0].lineno, 110)
257
258  def testGetLineCoverage_lineno_withAlternativeHtml(self):
259    line_coverage = self.GetLineCoverageWithFakeElements(
260        [self.tr_with_extra_a_tag])
261    self.assertEqual(line_coverage[0].lineno, 54)
262
263  def testGetLineCoverage_source(self):
264    self.parser._FindElements = mock.Mock(
265        return_value=[ElementTree.fromstring(self.covered_tr_html)])
266    line_coverage = self.parser.GetLineCoverage('fake_path')
267    self.assertEqual(line_coverage[0].source,
268                     '        if (mSelectors.get(index) != null) {')
269
270  def testGetLineCoverage_multipleElements(self):
271    line_coverage = self.GetLineCoverageWithFakeElements(
272        [self.covered_tr_html, self.partially_covered_tr_html,
273         self.tr_with_extra_a_tag])
274    self.assertEqual(len(line_coverage), 3)
275
276  def GetLineCoverageWithFakeElements(self, html_elements):
277    """Wraps GetLineCoverage so mock HTML can easily be used.
278
279    Args:
280      html_elements: List of strings each representing an HTML element.
281
282    Returns:
283      A list of LineCoverage objects.
284    """
285    elements = [ElementTree.fromstring(string) for string in html_elements]
286    with mock.patch('emma_coverage_stats._EmmaHtmlParser._FindElements',
287                    return_value=elements):
288      return self.parser.GetLineCoverage('fake_path')
289
290
291class _EmmaCoverageStatsTest(unittest.TestCase):
292  """Tests for _EmmaCoverageStats."""
293
294  def setUp(self):
295    self.good_source_to_emma = {
296      '/path/to/1/File1.java': '/emma/1.html',
297      '/path/2/File2.java': '/emma/2.html',
298      '/path/2/File3.java': '/emma/3.html'
299    }
300    self.line_coverage = [
301        emma_coverage_stats.LineCoverage(
302            1, '', emma_coverage_stats.COVERED, 1.0),
303        emma_coverage_stats.LineCoverage(
304            2, '', emma_coverage_stats.COVERED, 1.0),
305        emma_coverage_stats.LineCoverage(
306            3, '', emma_coverage_stats.NOT_EXECUTABLE, 1.0),
307        emma_coverage_stats.LineCoverage(
308            4, '', emma_coverage_stats.NOT_COVERED, 1.0),
309        emma_coverage_stats.LineCoverage(
310            5, '', emma_coverage_stats.PARTIALLY_COVERED, 0.85),
311        emma_coverage_stats.LineCoverage(
312            6, '', emma_coverage_stats.PARTIALLY_COVERED, 0.20)
313    ]
314    self.lines_for_coverage = [1, 3, 5, 6]
315    with mock.patch('emma_coverage_stats._EmmaHtmlParser._FindElements',
316                    return_value=[]):
317      self.simple_coverage = emma_coverage_stats._EmmaCoverageStats(
318          'fake_dir', {})
319
320  def testInit(self):
321    coverage_stats = self.simple_coverage
322    self.assertIsInstance(coverage_stats._emma_parser,
323                          emma_coverage_stats._EmmaHtmlParser)
324    self.assertIsInstance(coverage_stats._source_to_emma, dict)
325
326  def testNeedsCoverage_withExistingJavaFile(self):
327    test_file = '/path/to/file/File.java'
328    with mock.patch('os.path.exists', return_value=True):
329      self.assertTrue(
330          emma_coverage_stats._EmmaCoverageStats.NeedsCoverage(test_file))
331
332  def testNeedsCoverage_withNonJavaFile(self):
333    test_file = '/path/to/file/File.c'
334    with mock.patch('os.path.exists', return_value=True):
335      self.assertFalse(
336          emma_coverage_stats._EmmaCoverageStats.NeedsCoverage(test_file))
337
338  def testNeedsCoverage_fileDoesNotExist(self):
339    test_file = '/path/to/file/File.java'
340    with mock.patch('os.path.exists', return_value=False):
341      self.assertFalse(
342          emma_coverage_stats._EmmaCoverageStats.NeedsCoverage(test_file))
343
344  def testGetPackageNameFromFile_basic(self):
345    test_file_text = """// Test Copyright
346    package org.chromium.chrome.browser;
347    import android.graphics.RectF;"""
348    result_package, _ = MockOpenForFunction(
349        emma_coverage_stats._EmmaCoverageStats.GetPackageNameFromFile,
350        [test_file_text], file_path='/path/to/file/File.java')
351    self.assertEqual(result_package, 'org.chromium.chrome.browser.File.java')
352
353  def testGetPackageNameFromFile_noPackageStatement(self):
354    result_package, _ = MockOpenForFunction(
355        emma_coverage_stats._EmmaCoverageStats.GetPackageNameFromFile,
356        ['not a package statement'], file_path='/path/to/file/File.java')
357    self.assertIsNone(result_package)
358
359  def testGetSummaryStatsForLines_basic(self):
360    covered, total = self.simple_coverage.GetSummaryStatsForLines(
361        self.line_coverage)
362    self.assertEqual(covered, 3.05)
363    self.assertEqual(total, 5)
364
365  def testGetSourceFileToEmmaFileDict(self):
366    package_names = {
367      '/path/to/1/File1.java': 'org.fake.one.File1.java',
368      '/path/2/File2.java': 'org.fake.File2.java',
369      '/path/2/File3.java': 'org.fake.File3.java'
370    }
371    package_to_emma = {
372      'org.fake.one.File1.java': '/emma/1.html',
373      'org.fake.File2.java': '/emma/2.html',
374      'org.fake.File3.java': '/emma/3.html'
375    }
376    with mock.patch('os.path.exists', return_value=True):
377      coverage_stats = self.simple_coverage
378      coverage_stats._emma_parser.GetPackageNameToEmmaFileDict = mock.MagicMock(
379          return_value=package_to_emma)
380      coverage_stats.GetPackageNameFromFile = lambda x: package_names[x]
381      result_dict = coverage_stats._GetSourceFileToEmmaFileDict(
382          package_names.keys())
383    self.assertDictEqual(result_dict, self.good_source_to_emma)
384
385  def testGetCoverageDictForFile(self):
386    line_coverage = self.line_coverage
387    self.simple_coverage._emma_parser.GetLineCoverage = lambda x: line_coverage
388    self.simple_coverage._source_to_emma = {'/fake/src': 'fake/emma'}
389    lines = self.lines_for_coverage
390    expected_dict = {
391      'absolute': {
392        'covered': 3.05,
393        'total': 5
394      },
395      'incremental': {
396        'covered': 2.05,
397        'total': 3
398      },
399      'source': [
400        {
401          'line': line_coverage[0].source,
402          'coverage': line_coverage[0].covered_status,
403          'changed': True,
404          'fractional_coverage': line_coverage[0].fractional_line_coverage,
405        },
406        {
407          'line': line_coverage[1].source,
408          'coverage': line_coverage[1].covered_status,
409          'changed': False,
410          'fractional_coverage': line_coverage[1].fractional_line_coverage,
411        },
412        {
413          'line': line_coverage[2].source,
414          'coverage': line_coverage[2].covered_status,
415          'changed': True,
416          'fractional_coverage': line_coverage[2].fractional_line_coverage,
417        },
418        {
419          'line': line_coverage[3].source,
420          'coverage': line_coverage[3].covered_status,
421          'changed': False,
422          'fractional_coverage': line_coverage[3].fractional_line_coverage,
423        },
424        {
425          'line': line_coverage[4].source,
426          'coverage': line_coverage[4].covered_status,
427          'changed': True,
428          'fractional_coverage': line_coverage[4].fractional_line_coverage,
429        },
430        {
431          'line': line_coverage[5].source,
432          'coverage': line_coverage[5].covered_status,
433          'changed': True,
434          'fractional_coverage': line_coverage[5].fractional_line_coverage,
435        }
436      ]
437    }
438    result_dict = self.simple_coverage.GetCoverageDictForFile(
439        '/fake/src', lines)
440    self.assertDictEqual(result_dict, expected_dict)
441
442  def testGetCoverageDictForFile_emptyCoverage(self):
443    expected_dict = {
444      'absolute': {'covered': 0, 'total': 0},
445      'incremental': {'covered': 0, 'total': 0},
446      'source': []
447    }
448    self.simple_coverage._emma_parser.GetLineCoverage = lambda x: []
449    self.simple_coverage._source_to_emma = {'fake_dir': 'fake/emma'}
450    result_dict = self.simple_coverage.GetCoverageDictForFile('fake_dir', {})
451    self.assertDictEqual(result_dict, expected_dict)
452
453  def testGetCoverageDictForFile_missingCoverage(self):
454    self.simple_coverage._source_to_emma = {}
455    result_dict = self.simple_coverage.GetCoverageDictForFile('fake_file', {})
456    self.assertIsNone(result_dict)
457
458  def testGetCoverageDict_basic(self):
459    files_for_coverage = {
460      '/path/to/1/File1.java': [1, 3, 4],
461      '/path/2/File2.java': [1, 2]
462    }
463    self.simple_coverage._source_to_emma = {
464      '/path/to/1/File1.java': 'emma_1',
465      '/path/2/File2.java': 'emma_2'
466    }
467    coverage_info = {
468      'emma_1': [
469        emma_coverage_stats.LineCoverage(
470            1, '', emma_coverage_stats.COVERED, 1.0),
471        emma_coverage_stats.LineCoverage(
472            2, '', emma_coverage_stats.PARTIALLY_COVERED, 0.5),
473        emma_coverage_stats.LineCoverage(
474            3, '', emma_coverage_stats.NOT_EXECUTABLE, 1.0),
475        emma_coverage_stats.LineCoverage(
476            4, '', emma_coverage_stats.COVERED, 1.0)
477      ],
478      'emma_2': [
479        emma_coverage_stats.LineCoverage(
480            1, '', emma_coverage_stats.NOT_COVERED, 1.0),
481        emma_coverage_stats.LineCoverage(
482            2, '', emma_coverage_stats.COVERED, 1.0)
483      ]
484    }
485    expected_dict = {
486      'files': {
487        '/path/2/File2.java': {
488          'absolute': {'covered': 1, 'total': 2},
489          'incremental': {'covered': 1, 'total': 2},
490          'source': [{'changed': True, 'coverage': 0,
491                      'line': '', 'fractional_coverage': 1.0},
492                     {'changed': True, 'coverage': 1,
493                      'line': '', 'fractional_coverage': 1.0}]
494        },
495        '/path/to/1/File1.java': {
496          'absolute': {'covered': 2.5, 'total': 3},
497          'incremental': {'covered': 2, 'total': 2},
498          'source': [{'changed': True, 'coverage': 1,
499                      'line': '', 'fractional_coverage': 1.0},
500                     {'changed': False, 'coverage': 2,
501                      'line': '', 'fractional_coverage': 0.5},
502                     {'changed': True, 'coverage': -1,
503                      'line': '', 'fractional_coverage': 1.0},
504                     {'changed': True, 'coverage': 1,
505                      'line': '', 'fractional_coverage': 1.0}]
506        }
507      },
508      'patch': {'incremental': {'covered': 3, 'total': 4}}
509    }
510    # Return the relevant coverage info for each file.
511    self.simple_coverage._emma_parser.GetLineCoverage = (
512        lambda x: coverage_info[x])
513    result_dict = self.simple_coverage.GetCoverageDict(files_for_coverage)
514    self.assertDictEqual(result_dict, expected_dict)
515
516  def testGetCoverageDict_noCoverage(self):
517    result_dict = self.simple_coverage.GetCoverageDict({})
518    self.assertDictEqual(result_dict, EMPTY_COVERAGE_STATS_DICT)
519
520
521class EmmaCoverageStatsGenerateCoverageReport(unittest.TestCase):
522  """Tests for GenerateCoverageReport."""
523
524  def testGenerateCoverageReport_missingJsonFile(self):
525    with self.assertRaises(IOError):
526      with mock.patch('os.path.exists', return_value=False):
527        emma_coverage_stats.GenerateCoverageReport('', '', '')
528
529  def testGenerateCoverageReport_invalidJsonFile(self):
530    with self.assertRaises(ValueError):
531      with mock.patch('os.path.exists', return_value=True):
532        MockOpenForFunction(emma_coverage_stats.GenerateCoverageReport, [''],
533                            line_coverage_file='', out_file_path='',
534                            coverage_dir='')
535
536
537def MockOpenForFunction(func, side_effects, **kwargs):
538  """Allows easy mock open and read for callables that open multiple files.
539
540  Will mock the python open function in a way such that each time read() is
541  called on an open file, the next element in |side_effects| is returned. This
542  makes it easier to test functions that call open() multiple times.
543
544  Args:
545    func: The callable to invoke once mock files are setup.
546    side_effects: A list of return values for each file to return once read.
547      Length of list should be equal to the number calls to open in |func|.
548    **kwargs: Keyword arguments to be passed to |func|.
549
550  Returns:
551    A tuple containing the return value of |func| and the MagicMock object used
552      to mock all calls to open respectively.
553  """
554  mock_open = mock.mock_open()
555  mock_open.side_effect = [mock.mock_open(read_data=side_effect).return_value
556                           for side_effect in side_effects]
557  with mock.patch('__builtin__.open', mock_open):
558    return func(**kwargs), mock_open
559
560
561if __name__ == '__main__':
562  # Suppress logging messages.
563  unittest.main(buffer=True)
564