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