1# -*- coding: utf-8 -*- 2# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 3# See https://llvm.org/LICENSE.txt for license information. 4# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 5 6import json 7import libear 8import libscanbuild.report as sut 9import unittest 10import os 11import os.path 12 13 14def run_bug_parse(content): 15 with libear.TemporaryDirectory() as tmpdir: 16 file_name = os.path.join(tmpdir, 'test.html') 17 with open(file_name, 'w') as handle: 18 handle.writelines(content) 19 for bug in sut.parse_bug_html(file_name): 20 return bug 21 22 23def run_crash_parse(content, preproc): 24 with libear.TemporaryDirectory() as tmpdir: 25 file_name = os.path.join(tmpdir, preproc + '.info.txt') 26 with open(file_name, 'w') as handle: 27 handle.writelines(content) 28 return sut.parse_crash(file_name) 29 30 31class ParseFileTest(unittest.TestCase): 32 33 def test_parse_bug(self): 34 content = [ 35 "some header\n", 36 "<!-- BUGDESC Division by zero -->\n", 37 "<!-- BUGTYPE Division by zero -->\n", 38 "<!-- BUGCATEGORY Logic error -->\n", 39 "<!-- BUGFILE xx -->\n", 40 "<!-- BUGLINE 5 -->\n", 41 "<!-- BUGCOLUMN 22 -->\n", 42 "<!-- BUGPATHLENGTH 4 -->\n", 43 "<!-- BUGMETAEND -->\n", 44 "<!-- REPORTHEADER -->\n", 45 "some tails\n"] 46 result = run_bug_parse(content) 47 self.assertEqual(result['bug_category'], 'Logic error') 48 self.assertEqual(result['bug_path_length'], 4) 49 self.assertEqual(result['bug_line'], 5) 50 self.assertEqual(result['bug_description'], 'Division by zero') 51 self.assertEqual(result['bug_type'], 'Division by zero') 52 self.assertEqual(result['bug_file'], 'xx') 53 54 def test_parse_bug_empty(self): 55 content = [] 56 result = run_bug_parse(content) 57 self.assertEqual(result['bug_category'], 'Other') 58 self.assertEqual(result['bug_path_length'], 1) 59 self.assertEqual(result['bug_line'], 0) 60 61 def test_parse_crash(self): 62 content = [ 63 "/some/path/file.c\n", 64 "Some very serious Error\n", 65 "bla\n", 66 "bla-bla\n"] 67 result = run_crash_parse(content, 'file.i') 68 self.assertEqual(result['source'], content[0].rstrip()) 69 self.assertEqual(result['problem'], content[1].rstrip()) 70 self.assertEqual(os.path.basename(result['file']), 71 'file.i') 72 self.assertEqual(os.path.basename(result['info']), 73 'file.i.info.txt') 74 self.assertEqual(os.path.basename(result['stderr']), 75 'file.i.stderr.txt') 76 77 def test_parse_real_crash(self): 78 import libscanbuild.analyze as sut2 79 import re 80 with libear.TemporaryDirectory() as tmpdir: 81 filename = os.path.join(tmpdir, 'test.c') 82 with open(filename, 'w') as handle: 83 handle.write('int main() { return 0') 84 # produce failure report 85 opts = { 86 'clang': 'clang', 87 'directory': os.getcwd(), 88 'flags': [], 89 'file': filename, 90 'output_dir': tmpdir, 91 'language': 'c', 92 'error_type': 'other_error', 93 'error_output': 'some output', 94 'exit_code': 13 95 } 96 sut2.report_failure(opts) 97 # find the info file 98 pp_file = None 99 for root, _, files in os.walk(tmpdir): 100 keys = [os.path.join(root, name) for name in files] 101 for key in keys: 102 if re.match(r'^(.*/)+clang(.*)\.i$', key): 103 pp_file = key 104 self.assertIsNot(pp_file, None) 105 # read the failure report back 106 result = sut.parse_crash(pp_file + '.info.txt') 107 self.assertEqual(result['source'], filename) 108 self.assertEqual(result['problem'], 'Other Error') 109 self.assertEqual(result['file'], pp_file) 110 self.assertEqual(result['info'], pp_file + '.info.txt') 111 self.assertEqual(result['stderr'], pp_file + '.stderr.txt') 112 113 114class ReportMethodTest(unittest.TestCase): 115 116 def test_chop(self): 117 self.assertEqual('file', sut.chop('/prefix', '/prefix/file')) 118 self.assertEqual('file', sut.chop('/prefix/', '/prefix/file')) 119 self.assertEqual('lib/file', sut.chop('/prefix/', '/prefix/lib/file')) 120 self.assertEqual('/prefix/file', sut.chop('', '/prefix/file')) 121 122 def test_chop_when_cwd(self): 123 self.assertEqual('../src/file', sut.chop('/cwd', '/src/file')) 124 self.assertEqual('../src/file', sut.chop('/prefix/cwd', 125 '/prefix/src/file')) 126 127 128class GetPrefixFromCompilationDatabaseTest(unittest.TestCase): 129 130 def test_with_different_filenames(self): 131 self.assertEqual( 132 sut.commonprefix(['/tmp/a.c', '/tmp/b.c']), '/tmp') 133 134 def test_with_different_dirnames(self): 135 self.assertEqual( 136 sut.commonprefix(['/tmp/abs/a.c', '/tmp/ack/b.c']), '/tmp') 137 138 def test_no_common_prefix(self): 139 self.assertEqual( 140 sut.commonprefix(['/tmp/abs/a.c', '/usr/ack/b.c']), '/') 141 142 def test_with_single_file(self): 143 self.assertEqual( 144 sut.commonprefix(['/tmp/a.c']), '/tmp') 145 146 def test_empty(self): 147 self.assertEqual( 148 sut.commonprefix([]), '') 149 150class MergeSarifTest(unittest.TestCase): 151 152 def test_merging_sarif(self): 153 sarif1 = { 154 '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json', 155 'runs': [ 156 { 157 'artifacts': [ 158 { 159 'length': 100, 160 'location': { 161 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py' 162 }, 163 'mimeType': 'text/plain', 164 'roles': [ 165 'resultFile' 166 ] 167 } 168 ], 169 'columnKind': 'unicodeCodePoints', 170 'results': [ 171 { 172 'codeFlows': [ 173 { 174 'threadFlows': [ 175 { 176 'locations': [ 177 { 178 'importance': 'important', 179 'location': { 180 'message': { 181 'text': 'test message 1' 182 }, 183 'physicalLocation': { 184 'artifactLocation': { 185 'index': 0, 186 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py' 187 }, 188 'region': { 189 'endColumn': 5, 190 'startColumn': 1, 191 'startLine': 2 192 } 193 } 194 } 195 } 196 ] 197 } 198 ] 199 } 200 ] 201 }, 202 { 203 'codeFlows': [ 204 { 205 'threadFlows': [ 206 { 207 'locations': [ 208 { 209 'importance': 'important', 210 'location': { 211 'message': { 212 'text': 'test message 2' 213 }, 214 'physicalLocation': { 215 'artifactLocation': { 216 'index': 0, 217 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py' 218 }, 219 'region': { 220 'endColumn': 23, 221 'startColumn': 9, 222 'startLine': 10 223 } 224 } 225 } 226 } 227 ] 228 } 229 ] 230 } 231 ] 232 } 233 ], 234 'tool': { 235 'driver': { 236 'fullName': 'clang static analyzer', 237 'language': 'en-US', 238 'name': 'clang', 239 'rules': [ 240 { 241 'fullDescription': { 242 'text': 'test rule for merge sarif test' 243 }, 244 'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py', 245 'id': 'testId', 246 'name': 'testName' 247 } 248 ], 249 'version': 'test clang' 250 } 251 } 252 } 253 ], 254 'version': '2.1.0' 255 } 256 sarif2 = { 257 '$schema': 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json', 258 'runs': [ 259 { 260 'artifacts': [ 261 { 262 'length': 1523, 263 'location': { 264 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py' 265 }, 266 'mimeType': 'text/plain', 267 'roles': [ 268 'resultFile' 269 ] 270 } 271 ], 272 'columnKind': 'unicodeCodePoints', 273 'results': [ 274 { 275 'codeFlows': [ 276 { 277 'threadFlows': [ 278 { 279 'locations': [ 280 { 281 'importance': 'important', 282 'location': { 283 'message': { 284 'text': 'test message 3' 285 }, 286 'physicalLocation': { 287 'artifactLocation': { 288 'index': 0, 289 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py' 290 }, 291 'region': { 292 'endColumn': 99, 293 'startColumn': 99, 294 'startLine': 17 295 } 296 } 297 } 298 } 299 ] 300 } 301 ] 302 } 303 ] 304 }, 305 { 306 'codeFlows': [ 307 { 308 'threadFlows': [ 309 { 310 'locations': [ 311 { 312 'importance': 'important', 313 'location': { 314 'message': { 315 'text': 'test message 4' 316 }, 317 'physicalLocation': { 318 'artifactLocation': { 319 'index': 0, 320 'uri': '//clang/tools/scan-build-py/tests/unit/test_report.py' 321 }, 322 'region': { 323 'endColumn': 305, 324 'startColumn': 304, 325 'startLine': 1 326 } 327 } 328 } 329 } 330 ] 331 } 332 ] 333 } 334 ] 335 } 336 ], 337 'tool': { 338 'driver': { 339 'fullName': 'clang static analyzer', 340 'language': 'en-US', 341 'name': 'clang', 342 'rules': [ 343 { 344 'fullDescription': { 345 'text': 'test rule for merge sarif test' 346 }, 347 'helpUrl': '//clang/tools/scan-build-py/tests/unit/test_report.py', 348 'id': 'testId', 349 'name': 'testName' 350 } 351 ], 352 'version': 'test clang' 353 } 354 } 355 } 356 ], 357 'version': '2.1.0' 358 } 359 360 contents = [sarif1, sarif2] 361 with libear.TemporaryDirectory() as tmpdir: 362 for idx, content in enumerate(contents): 363 file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx)) 364 with open(file_name, 'w') as handle: 365 json.dump(content, handle) 366 367 sut.merge_sarif_files(tmpdir, sort_files=True) 368 369 self.assertIn('results-merged.sarif', os.listdir(tmpdir)) 370 with open(os.path.join(tmpdir, 'results-merged.sarif')) as f: 371 merged = json.load(f) 372 self.assertEqual(len(merged['runs']), 2) 373 self.assertEqual(len(merged['runs'][0]['results']), 2) 374 self.assertEqual(len(merged['runs'][1]['results']), 2) 375 376 expected = sarif1 377 for run in sarif2['runs']: 378 expected['runs'].append(run) 379 380 self.assertEqual(merged, expected) 381 382 def test_merge_updates_embedded_link(self): 383 sarif1 = { 384 'runs': [ 385 { 386 'results': [ 387 { 388 'codeFlows': [ 389 { 390 'message': { 391 'text': 'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)' 392 }, 393 'threadFlows': [ 394 { 395 'message': { 396 'text': 'test message 1-2 [link](sarif:/runs/1/results/0)' 397 } 398 } 399 ] 400 } 401 ] 402 } 403 ] 404 }, 405 { 406 'results': [ 407 { 408 'codeFlows': [ 409 { 410 'message': { 411 'text': 'test message 2-1 [link](sarif:/runs/0/results/0)' 412 }, 413 'threadFlows': [ 414 { 415 'message': { 416 'text': 'test message 2-2 [link](sarif:/runs/0/results/0)' 417 } 418 } 419 ] 420 } 421 ] 422 } 423 ] 424 } 425 ] 426 } 427 sarif2 = { 428 'runs': [ 429 { 430 'results': [ 431 { 432 'codeFlows': [ 433 { 434 'message': { 435 'text': 'test message 3-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)' 436 }, 437 'threadFlows': [ 438 { 439 'message': { 440 'text': 'test message 3-2 [link](sarif:/runs/1/results/0)' 441 } 442 } 443 ] 444 } 445 ] 446 } 447 ], 448 }, 449 { 450 'results': [ 451 { 452 'codeFlows': [ 453 { 454 'message': { 455 'text': 'test message 4-1 [link](sarif:/runs/0/results/0)' 456 }, 457 'threadFlows': [ 458 { 459 'message': { 460 'text': 'test message 4-2 [link](sarif:/runs/0/results/0)' 461 } 462 } 463 ] 464 } 465 ] 466 } 467 ] 468 } 469 ] 470 } 471 sarif3 = { 472 'runs': [ 473 { 474 'results': [ 475 { 476 'codeFlows': [ 477 { 478 'message': { 479 'text': 'test message 5-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)' 480 }, 481 'threadFlows': [ 482 { 483 'message': { 484 'text': 'test message 5-2 [link](sarif:/runs/1/results/0)' 485 } 486 } 487 ] 488 } 489 ] 490 } 491 ], 492 }, 493 { 494 'results': [ 495 { 496 'codeFlows': [ 497 { 498 'message': { 499 'text': 'test message 6-1 [link](sarif:/runs/0/results/0)' 500 }, 501 'threadFlows': [ 502 { 503 'message': { 504 'text': 'test message 6-2 [link](sarif:/runs/0/results/0)' 505 } 506 } 507 ] 508 } 509 ] 510 } 511 ] 512 } 513 ] 514 } 515 516 contents = [sarif1, sarif2, sarif3] 517 518 with libear.TemporaryDirectory() as tmpdir: 519 for idx, content in enumerate(contents): 520 file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx)) 521 with open(file_name, 'w') as handle: 522 json.dump(content, handle) 523 524 sut.merge_sarif_files(tmpdir, sort_files=True) 525 526 self.assertIn('results-merged.sarif', os.listdir(tmpdir)) 527 with open(os.path.join(tmpdir, 'results-merged.sarif')) as f: 528 merged = json.load(f) 529 self.assertEqual(len(merged['runs']), 6) 530 531 code_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['message']['text'] for x in range(6)] 532 thread_flows = [merged['runs'][x]['results'][0]['codeFlows'][0]['threadFlows'][0]['message']['text'] for x in range(6)] 533 534 # The run index should be updated for the second and third sets of runs 535 self.assertEqual(code_flows, 536 [ 537 'test message 1-1 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/1/results/0)', 538 'test message 2-1 [link](sarif:/runs/0/results/0)', 539 'test message 3-1 [link](sarif:/runs/3/results/0) [link2](sarif:/runs/3/results/0)', 540 'test message 4-1 [link](sarif:/runs/2/results/0)', 541 'test message 5-1 [link](sarif:/runs/5/results/0) [link2](sarif:/runs/5/results/0)', 542 'test message 6-1 [link](sarif:/runs/4/results/0)' 543 ]) 544 self.assertEquals(thread_flows, 545 [ 546 'test message 1-2 [link](sarif:/runs/1/results/0)', 547 'test message 2-2 [link](sarif:/runs/0/results/0)', 548 'test message 3-2 [link](sarif:/runs/3/results/0)', 549 'test message 4-2 [link](sarif:/runs/2/results/0)', 550 'test message 5-2 [link](sarif:/runs/5/results/0)', 551 'test message 6-2 [link](sarif:/runs/4/results/0)' 552 ]) 553 554 def test_overflow_run_count(self): 555 sarif1 = { 556 'runs': [ 557 {'results': [{ 558 'message': {'text': 'run 1-0 [link](sarif:/runs/1/results/0)'} 559 }]}, 560 {'results': [{ 561 'message': {'text': 'run 1-1 [link](sarif:/runs/2/results/0)'} 562 }]}, 563 {'results': [{ 564 'message': {'text': 'run 1-2 [link](sarif:/runs/3/results/0)'} 565 }]}, 566 {'results': [{ 567 'message': {'text': 'run 1-3 [link](sarif:/runs/4/results/0)'} 568 }]}, 569 {'results': [{ 570 'message': {'text': 'run 1-4 [link](sarif:/runs/5/results/0)'} 571 }]}, 572 {'results': [{ 573 'message': {'text': 'run 1-5 [link](sarif:/runs/6/results/0)'} 574 }]}, 575 {'results': [{ 576 'message': {'text': 'run 1-6 [link](sarif:/runs/7/results/0)'} 577 }]}, 578 {'results': [{ 579 'message': {'text': 'run 1-7 [link](sarif:/runs/8/results/0)'} 580 }]}, 581 {'results': [{ 582 'message': {'text': 'run 1-8 [link](sarif:/runs/9/results/0)'} 583 }]}, 584 {'results': [{ 585 'message': {'text': 'run 1-9 [link](sarif:/runs/0/results/0)'} 586 }]} 587 ] 588 } 589 sarif2 = { 590 'runs': [ 591 {'results': [{ 592 'message': {'text': 'run 2-0 [link](sarif:/runs/1/results/0) [link2](sarif:/runs/2/results/0)'} 593 }]}, 594 {'results': [{ 595 'message': {'text': 'run 2-1 [link](sarif:/runs/2/results/0)'} 596 }]}, 597 {'results': [{ 598 'message': {'text': 'run 2-2 [link](sarif:/runs/3/results/0)'} 599 }]}, 600 {'results': [{ 601 'message': {'text': 'run 2-3 [link](sarif:/runs/4/results/0)'} 602 }]}, 603 {'results': [{ 604 'message': {'text': 'run 2-4 [link](sarif:/runs/5/results/0)'} 605 }]}, 606 {'results': [{ 607 'message': {'text': 'run 2-5 [link](sarif:/runs/6/results/0)'} 608 }]}, 609 {'results': [{ 610 'message': {'text': 'run 2-6 [link](sarif:/runs/7/results/0)'} 611 }]}, 612 {'results': [{ 613 'message': {'text': 'run 2-7 [link](sarif:/runs/8/results/0)'} 614 }]}, 615 {'results': [{ 616 'message': {'text': 'run 2-8 [link](sarif:/runs/9/results/0)'} 617 }]}, 618 {'results': [{ 619 'message': {'text': 'run 2-9 [link](sarif:/runs/0/results/0)'} 620 }]} 621 ] 622 } 623 624 contents = [sarif1, sarif2] 625 with libear.TemporaryDirectory() as tmpdir: 626 for idx, content in enumerate(contents): 627 file_name = os.path.join(tmpdir, 'results-{}.sarif'.format(idx)) 628 with open(file_name, 'w') as handle: 629 json.dump(content, handle) 630 631 sut.merge_sarif_files(tmpdir, sort_files=True) 632 633 self.assertIn('results-merged.sarif', os.listdir(tmpdir)) 634 with open(os.path.join(tmpdir, 'results-merged.sarif')) as f: 635 merged = json.load(f) 636 self.assertEqual(len(merged['runs']), 20) 637 638 messages = [merged['runs'][x]['results'][0]['message']['text'] for x in range(20)] 639 self.assertEqual(messages, 640 [ 641 'run 1-0 [link](sarif:/runs/1/results/0)', 642 'run 1-1 [link](sarif:/runs/2/results/0)', 643 'run 1-2 [link](sarif:/runs/3/results/0)', 644 'run 1-3 [link](sarif:/runs/4/results/0)', 645 'run 1-4 [link](sarif:/runs/5/results/0)', 646 'run 1-5 [link](sarif:/runs/6/results/0)', 647 'run 1-6 [link](sarif:/runs/7/results/0)', 648 'run 1-7 [link](sarif:/runs/8/results/0)', 649 'run 1-8 [link](sarif:/runs/9/results/0)', 650 'run 1-9 [link](sarif:/runs/0/results/0)', 651 'run 2-0 [link](sarif:/runs/11/results/0) [link2](sarif:/runs/12/results/0)', 652 'run 2-1 [link](sarif:/runs/12/results/0)', 653 'run 2-2 [link](sarif:/runs/13/results/0)', 654 'run 2-3 [link](sarif:/runs/14/results/0)', 655 'run 2-4 [link](sarif:/runs/15/results/0)', 656 'run 2-5 [link](sarif:/runs/16/results/0)', 657 'run 2-6 [link](sarif:/runs/17/results/0)', 658 'run 2-7 [link](sarif:/runs/18/results/0)', 659 'run 2-8 [link](sarif:/runs/19/results/0)', 660 'run 2-9 [link](sarif:/runs/10/results/0)' 661 ]) 662