github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/results-processor/wptreport_test.py (about) 1 # Copyright 2018 The WPT Dashboard Project. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import gzip 6 import io 7 import json 8 import os 9 import shutil 10 import tempfile 11 import unittest 12 13 from wptreport import ( 14 ConflictingDataError, 15 InsufficientDataError, 16 InvalidJSONError, 17 MissingMetadataError, 18 WPTReport, 19 prepare_labels, 20 normalize_product 21 ) 22 23 24 class WPTReportTest(unittest.TestCase): 25 def setUp(self): 26 self.tmp_dir = tempfile.mkdtemp() 27 28 def tearDown(self): 29 shutil.rmtree(self.tmp_dir) 30 31 def test_write_json(self): 32 obj = {'results': [{'test': 'foo'}]} 33 tmp_path = os.path.join(self.tmp_dir, 'test.json') 34 with open(tmp_path, 'wb') as f: 35 WPTReport.write_json(f, obj) 36 with open(tmp_path, 'rt') as f: 37 round_trip = json.load(f) 38 self.assertDictEqual(obj, round_trip) 39 40 def test_write_gzip_json(self): 41 # This case also covers the Unicode testing of write_json(). 42 obj = {'results': [{ 43 'test': 'ABC~‾¥≈¤・・•∙·☼★星🌟星★☼·∙•・・¤≈¥‾~XYZ', 44 'message': None, 45 'status': 'PASS' 46 }]} 47 tmp_path = os.path.join(self.tmp_dir, 'foo', 'bar.json.gz') 48 WPTReport.write_gzip_json(tmp_path, obj) 49 with open(tmp_path, 'rb') as f: 50 with gzip.GzipFile(fileobj=f, mode='rb') as gf: 51 with io.TextIOWrapper(gf, encoding='utf-8') as tf: 52 round_trip = json.load(tf) 53 self.assertDictEqual(obj, round_trip) 54 55 def test_load_json(self): 56 tmp_path = os.path.join(self.tmp_dir, 'test.json') 57 with open(tmp_path, 'wt') as f: 58 f.write('{"results": [{"test": "foo"}]}') 59 r = WPTReport() 60 with open(tmp_path, 'rb') as f: 61 r.load_json(f) 62 self.assertEqual(len(r.results), 1) 63 # This is the sha1sum of the string written above. 64 self.assertEqual(r.hashsum(), 65 'afa59408e1797c7091d7e89de5561612f7da440d') 66 67 def test_load_json_empty_report(self): 68 tmp_path = os.path.join(self.tmp_dir, 'test.json') 69 with open(tmp_path, 'wt') as f: 70 f.write('{}') 71 r = WPTReport() 72 with open(tmp_path, 'rb') as f: 73 with self.assertRaises(InsufficientDataError): 74 r.load_json(f) 75 76 def test_load_json_invalid_json(self): 77 tmp_path = os.path.join(self.tmp_dir, 'test.json') 78 with open(tmp_path, 'wt') as f: 79 f.write('{[') 80 r = WPTReport() 81 with open(tmp_path, 'rb') as f: 82 with self.assertRaises(InvalidJSONError): 83 r.load_json(f) 84 85 def test_load_json_multiple_chunks(self): 86 tmp_path = os.path.join(self.tmp_dir, 'test.json') 87 r = WPTReport() 88 89 with open(tmp_path, 'wt') as f: 90 f.write('{"results": [{"test1": "foo"}]}\n') 91 with open(tmp_path, 'rb') as f: 92 r.load_json(f) 93 94 with open(tmp_path, 'wt') as f: 95 f.write('{"results": [{"test2": "bar"}]}\n') 96 with open(tmp_path, 'rb') as f: 97 r.load_json(f) 98 99 self.assertEqual(len(r.results), 2) 100 # This is the sha1sum of the two strings above concatenated. 101 self.assertEqual(r.hashsum(), 102 '3aa5e332b892025bc6c301e6578ae0d54375351d') 103 104 def test_load_json_multiple_chunks_metadata(self): 105 tmp_path = os.path.join(self.tmp_dir, 'test.json') 106 r = WPTReport() 107 108 # Load a report with no metadata first to test the handling of None. 109 with open(tmp_path, 'wt') as f: 110 f.write('{"results": [{"test": "foo"}]}\n') 111 with open(tmp_path, 'rb') as f: 112 r.load_json(f) 113 114 with open(tmp_path, 'wt') as f: 115 json.dump({ 116 'results': [{'test1': 'foo'}], 117 'run_info': {'product': 'firefox', 'os': 'linux'}, 118 'time_start': 100, 119 'time_end': 200, 120 }, f) 121 with open(tmp_path, 'rb') as f: 122 r.load_json(f) 123 124 with open(tmp_path, 'wt') as f: 125 json.dump({ 126 'results': [{'test2': 'bar'}], 127 'run_info': {'product': 'firefox', 'browser_version': '59.0'}, 128 'time_start': 10, 129 'time_end': 500, 130 }, f) 131 with open(tmp_path, 'rb') as f: 132 r.load_json(f) 133 134 self.assertEqual(len(r.results), 3) 135 # run_info should be the union of all run_info. 136 self.assertDictEqual(r.run_info, { 137 'product': 'firefox', 138 'browser_version': '59.0', 139 'os': 'linux' 140 }) 141 # The smallest time_start should be kept. 142 self.assertEqual(r._report['time_start'], 10) 143 # The largest time_end should be kept. 144 self.assertEqual(r._report['time_end'], 500) 145 146 def test_load_json_multiple_chunks_conflicting_data(self): 147 tmp_path = os.path.join(self.tmp_dir, 'test.json') 148 r = WPTReport() 149 with open(tmp_path, 'wt') as f: 150 json.dump({ 151 'results': [{'test1': 'foo'}], 152 'run_info': { 153 'revision': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 154 'product': 'firefox', 155 'browser_version': '59', 156 }, 157 }, f) 158 with open(tmp_path, 'rb') as f: 159 r.load_json(f) 160 161 with open(tmp_path, 'wt') as f: 162 json.dump({ 163 'results': [{'test2': 'bar'}], 164 'run_info': { 165 'revision': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 166 'product': 'chrome', 167 'browser_version': '70', 168 }, 169 }, f) 170 with open(tmp_path, 'rb') as f: 171 reg = r"product: \[chrome, firefox\], browser_version: \[70, 59\]" 172 with self.assertRaisesRegex(ConflictingDataError, reg): 173 r.load_json(f) 174 175 # Fields without conflict should be preserved. 176 self.assertEqual(r.run_info['revision'], 177 '0bdaaf9c1622ca49eb140381af1ece6d8001c934') 178 # Conflicting fields should be set to None. 179 self.assertIsNone(r.run_info['product']) 180 self.assertIsNone(r.run_info['browser_version']) 181 182 def test_load_json_multiple_chunks_ignored_conflicting_data(self): 183 tmp_path = os.path.join(self.tmp_dir, 'test.json') 184 r = WPTReport() 185 with open(tmp_path, 'wt') as f: 186 json.dump({ 187 'results': [{'test1': 'foo'}], 188 'run_info': { 189 'browser_build_id': '1', 190 'browser_changeset': 'r1', 191 'version': 'v1', 192 'os_build': 'b1', 193 }, 194 }, f) 195 with open(tmp_path, 'rb') as f: 196 r.load_json(f) 197 198 with open(tmp_path, 'wt') as f: 199 json.dump({ 200 'results': [{'test2': 'bar'}], 201 'run_info': { 202 'browser_build_id': '2', 203 'browser_changeset': 'r2', 204 'version': 'v2', 205 'os_build': 'b2', 206 }, 207 }, f) 208 with open(tmp_path, 'rb') as f: 209 r.load_json(f) 210 self.assertIsNone(r.run_info['browser_build_id']) 211 self.assertIsNone(r.run_info['browser_changeset']) 212 self.assertIsNone(r.run_info['version']) 213 self.assertIsNone(r.run_info['os_build']) 214 215 def test_load_gzip_json(self): 216 # This case also covers the Unicode testing of load_json(). 217 obj = { 218 'results': [{ 219 'test': 'ABC~‾¥≈¤・・•∙·☼★星🌟星★☼·∙•・・¤≈¥‾~XYZ', 220 'message': None, 221 'status': 'PASS' 222 }], 223 'run_info': {}, 224 } 225 json_s = json.dumps(obj, ensure_ascii=False) 226 tmp_path = os.path.join(self.tmp_dir, 'test.json.gz') 227 with open(tmp_path, 'wb') as f: 228 gzip_file = gzip.GzipFile(fileobj=f, mode='wb') 229 gzip_file.write(json_s.encode('utf-8')) 230 gzip_file.close() 231 232 r = WPTReport() 233 with open(tmp_path, 'rb') as f: 234 r.load_gzip_json(f) 235 self.assertDictEqual(r._report, obj) 236 237 def test_summarize(self): 238 r = WPTReport() 239 r._report = {'results': [ 240 { 241 'test': '/js/with-statement.html', 242 'status': 'OK', 243 'message': None, 244 'subtests': [ 245 {'status': 'PASS', 'message': None, 'name': 'first'}, 246 {'status': 'FAIL', 'message': 'bad', 'name': 'second'} 247 ] 248 }, 249 { 250 'test': '/js/isNaN.html', 251 'status': 'OK', 252 'message': None, 253 'subtests': [ 254 {'status': 'PASS', 'message': None, 'name': 'first'}, 255 {'status': 'FAIL', 'message': 'bad', 'name': 'second'}, 256 {'status': 'PASS', 'message': None, 'name': 'third'} 257 ] 258 } 259 ]} 260 self.assertEqual(r.summarize(), { 261 '/js/with-statement.html': {'s': 'O', 'c': [1, 2]}, 262 '/js/isNaN.html': {'s': 'O', 'c': [2, 3]} 263 }) 264 265 def test_summarize_zero_results(self): 266 r = WPTReport() 267 # Do not throw! 268 r.summarize() 269 270 def test_summarize_duplicate_results(self): 271 r = WPTReport() 272 r._report = {'results': [ 273 { 274 'test': '/js/with-statement.html', 275 'status': 'OK', 276 'message': None, 277 'subtests': [ 278 {'status': 'PASS', 'message': None, 'name': 'first'}, 279 {'status': 'FAIL', 'message': 'bad', 'name': 'second'} 280 ] 281 }, 282 { 283 'test': '/js/with-statement.html', 284 'status': 'OK', 285 'message': None, 286 'subtests': [ 287 {'status': 'PASS', 'message': None, 'name': 'first'}, 288 {'status': 'FAIL', 'message': 'bad', 'name': 'second'}, 289 {'status': 'FAIL', 'message': 'bad', 'name': 'third'}, 290 {'status': 'FAIL', 'message': 'bad', 'name': 'fourth'} 291 ] 292 } 293 ]} 294 with self.assertRaises(ConflictingDataError): 295 r.summarize() 296 297 def test_summarize_whitespaces(self): 298 r = WPTReport() 299 r._report = {'results': [ 300 { 301 'test': ' /ref/reftest.html', 302 'status': 'PASS', 303 'message': None, 304 'subtests': [] 305 }, 306 { 307 'test': '/ref/reftest-fail.html\n', 308 'status': 'FAIL', 309 'message': None, 310 'subtests': [] 311 } 312 ]} 313 self.assertEqual(r.summarize(), { 314 '/ref/reftest.html': {'s': 'P', 'c': [0, 0]}, 315 '/ref/reftest-fail.html': {'s': 'F', 'c': [0, 0]} 316 }) 317 318 def test_each_result(self): 319 expected_results = [ 320 { 321 'test': '/js/with-statement.html', 322 'status': 'OK', 323 'message': None, 324 'subtests': [ 325 {'status': 'PASS', 'message': None, 'name': 'first'}, 326 {'status': 'FAIL', 'message': 'bad', 'name': 'second'} 327 ] 328 }, 329 { 330 'test': '/js/isNaN.html', 331 'status': 'OK', 332 'message': None, 333 'subtests': [ 334 {'status': 'PASS', 'message': None, 'name': 'first'}, 335 {'status': 'FAIL', 'message': 'bad', 'name': 'second'}, 336 {'status': 'PASS', 'message': None, 'name': 'third'} 337 ] 338 }, 339 { 340 'test': '/js/do-while-statement.html', 341 'status': 'OK', 342 'message': None, 343 'subtests': [ 344 {'status': 'PASS', 'message': None, 'name': 'first'} 345 ] 346 }, 347 { 348 'test': '/js/symbol-unscopables.html', 349 'status': 'TIMEOUT', 350 'message': None, 351 'subtests': [] 352 }, 353 { 354 'test': '/js/void-statement.html', 355 'status': 'OK', 356 'message': None, 357 'subtests': [ 358 {'status': 'PASS', 'message': None, 'name': 'first'}, 359 {'status': 'FAIL', 'message': 'bad', 'name': 'second'}, 360 {'status': 'FAIL', 'message': 'bad', 'name': 'third'}, 361 {'status': 'FAIL', 'message': 'bad', 'name': 'fourth'} 362 ] 363 } 364 ] 365 r = WPTReport() 366 r._report = {'results': expected_results} 367 self.assertListEqual(list(r.each_result()), expected_results) 368 369 def test_populate_upload_directory(self): 370 # This also tests write_summary() and write_result_directory(). 371 revision = '0bdaaf9c1622ca49eb140381af1ece6d8001c934' 372 r = WPTReport() 373 r._report = { 374 'results': [ 375 { 376 'test': '/foo/bar.html', 377 'status': 'PASS', 378 'message': None, 379 'subtests': [] 380 }, 381 # Whitespaces need to be trimmed from the test name. 382 { 383 'test': ' /foo/fail.html\n', 384 'status': 'FAIL', 385 'message': None, 386 'subtests': [] 387 } 388 ], 389 'run_info': { 390 'revision': revision, 391 'product': 'firefox', 392 'browser_version': '59.0', 393 'os': 'linux' 394 } 395 } 396 r.hashsum = lambda: '0123456789' 397 r.populate_upload_directory(output_dir=self.tmp_dir) 398 399 self.assertTrue(os.path.isfile(os.path.join( 400 self.tmp_dir, revision, 401 'firefox-59.0-linux-0123456789-summary_v2.json.gz' 402 ))) 403 self.assertTrue(os.path.isfile(os.path.join( 404 self.tmp_dir, revision, 405 'firefox-59.0-linux-0123456789', 'foo', 'bar.html' 406 ))) 407 self.assertTrue(os.path.isfile(os.path.join( 408 self.tmp_dir, revision, 409 'firefox-59.0-linux-0123456789', 'foo', 'fail.html' 410 ))) 411 412 def test_update_metadata(self): 413 r = WPTReport() 414 r.update_metadata( 415 revision='0bdaaf9c1622ca49eb140381af1ece6d8001c934', 416 browser_name='firefox', 417 browser_version='59.0', 418 os_name='linux', 419 os_version='4.4' 420 ) 421 self.assertDictEqual(r.run_info, { 422 'revision': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 423 'product': 'firefox', 424 'browser_version': '59.0', 425 'os': 'linux', 426 'os_version': '4.4' 427 }) 428 429 def test_test_run_metadata(self): 430 r = WPTReport() 431 r._report = { 432 'run_info': { 433 'revision': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 434 'product': 'firefox', 435 'browser_version': '59.0', 436 'os': 'linux' 437 } 438 } 439 self.assertDictEqual(r.test_run_metadata, { 440 'browser_name': 'firefox', 441 'browser_version': '59.0', 442 'os_name': 'linux', 443 'revision': '0bdaaf9c16', 444 'full_revision_hash': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 445 }) 446 447 def test_test_run_metadata_missing_required_fields(self): 448 r = WPTReport() 449 r._report = { 450 'run_info': { 451 'product': 'firefox', 452 'os': 'linux' 453 } 454 } 455 with self.assertRaises(MissingMetadataError): 456 r.test_run_metadata 457 458 def test_test_run_metadata_optional_fields(self): 459 r = WPTReport() 460 r._report = { 461 'run_info': { 462 'revision': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 463 'product': 'firefox', 464 'browser_version': '59.0', 465 'os': 'windows', 466 'os_version': '10' 467 }, 468 'time_start': 1529606394218, 469 'time_end': 1529611429000, 470 } 471 self.assertDictEqual(r.test_run_metadata, { 472 'browser_name': 'firefox', 473 'browser_version': '59.0', 474 'os_name': 'windows', 475 'os_version': '10', 476 'revision': '0bdaaf9c16', 477 'full_revision_hash': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 478 'time_start': '2018-06-21T18:39:54.218000+00:00', 479 'time_end': '2018-06-21T20:03:49+00:00', 480 }) 481 482 def test_product_id(self): 483 r = WPTReport() 484 r._report = { 485 'run_info': { 486 'product': 'firefox', 487 'browser_version': '59.0', 488 'os': 'linux', 489 } 490 } 491 r.hashsum = lambda: 'afa59408e1797c7091d7e89de5561612f7da440d' 492 self.assertEqual(r.product_id(), 'firefox-59.0-linux-afa59408e1') 493 494 r._report['run_info']['os_version'] = '4.4' 495 self.assertEqual(r.product_id(separator='_'), 496 'firefox_59.0_linux_4.4_afa59408e1') 497 498 def test_product_id_sanitize(self): 499 r = WPTReport() 500 r._report = { 501 'run_info': { 502 'product': 'chrome!', 503 'browser_version': '1.2.3 dev-1', 504 'os': 'linux', 505 } 506 } 507 r.hashsum = lambda: 'afa59408e1797c7091d7e89de5561612f7da440d' 508 self.assertEqual(r.product_id(separator='-', sanitize=True), 509 'chrome_-1.2.3_dev-1-linux-afa59408e1') 510 511 def test_sha_product_path(self): 512 r = WPTReport() 513 r._report = { 514 'run_info': { 515 'revision': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 516 'product': 'firefox', 517 'browser_version': '59.0', 518 'os': 'linux' 519 } 520 } 521 r.hashsum = lambda: 'afa59408e1797c7091d7e89de5561612f7da440d' 522 self.assertEqual(r.sha_product_path, 523 '0bdaaf9c1622ca49eb140381af1ece6d8001c934/' 524 'firefox-59.0-linux-afa59408e1') 525 526 def test_sha_summary_path(self): 527 r = WPTReport() 528 r._report = { 529 'run_info': { 530 'revision': '0bdaaf9c1622ca49eb140381af1ece6d8001c934', 531 'product': 'firefox', 532 'browser_version': '59.0', 533 'os': 'linux' 534 } 535 } 536 r.hashsum = lambda: 'afa59408e1797c7091d7e89de5561612f7da440d' 537 self.assertEqual(r.sha_summary_path, 538 '0bdaaf9c1622ca49eb140381af1ece6d8001c934/' 539 'firefox-59.0-linux-afa59408e1-summary_v2.json.gz') 540 541 def test_normalize_version(self): 542 r = WPTReport() 543 r._report = {'run_info': { 544 'browser_version': 'Technology Preview (Release 67, 13607.1.9.0.1)' 545 }} 546 r.normalize_version() 547 self.assertEqual(r.run_info['browser_version'], '67 preview') 548 549 def test_normalize_version_missing_version(self): 550 r = WPTReport() 551 r._report = {'run_info': {}} 552 r.normalize_version() 553 # Do not throw! 554 self.assertIsNone(r.run_info.get('browser_version')) 555 556 557 class HelpersTest(unittest.TestCase): 558 def test_prepare_labels_from_empty_str(self): 559 r = WPTReport() 560 r.update_metadata(browser_name='firefox') 561 self.assertSetEqual( 562 prepare_labels(r, '', 'blade-runner'), 563 {'blade-runner', 'firefox', 'stable'} 564 ) 565 566 def test_prepare_labels_from_custom_labels(self): 567 r = WPTReport() 568 r.update_metadata(browser_name='firefox') 569 self.assertSetEqual( 570 prepare_labels(r, 'foo,bar', 'blade-runner'), 571 {'bar', 'blade-runner', 'firefox', 'foo', 'stable'} 572 ) 573 574 def test_prepare_labels_from_experimental_label(self): 575 r = WPTReport() 576 r.update_metadata(browser_name='firefox') 577 self.assertSetEqual( 578 prepare_labels(r, 'experimental', 'blade-runner'), 579 {'blade-runner', 'experimental', 'firefox'} 580 ) 581 582 def test_prepare_labels_from_stable_label(self): 583 r = WPTReport() 584 r.update_metadata(browser_name='firefox') 585 self.assertSetEqual( 586 prepare_labels(r, 'stable', 'blade-runner'), 587 {'blade-runner', 'firefox', 'stable'} 588 ) 589 590 def test_prepare_labels_from_browser_channel(self): 591 # Chrome Dev 592 r = WPTReport() 593 r._report = { 594 'run_info': { 595 'product': 'chrome', 596 'browser_channel': 'dev', 597 } 598 } 599 self.assertSetEqual( 600 prepare_labels(r, '', 'blade-runner'), 601 {'blade-runner', 'dev', 'chrome'} 602 ) 603 604 # Chrome Canary 605 r._report['run_info']['browser_channel'] = 'canary' 606 self.assertSetEqual( 607 prepare_labels(r, '', 'blade-runner'), 608 {'blade-runner', 'canary', 'experimental', 'chrome'} 609 ) 610 611 # Chrome Nightly 612 r._report['run_info']['browser_channel'] = 'nightly' 613 self.assertSetEqual( 614 prepare_labels(r, '', 'blade-runner'), 615 {'blade-runner', 'nightly', 'chrome'} 616 ) 617 618 # WebKitGTK Nightly 619 r._report['run_info']['product'] = 'webkitgtk_minibrowser' 620 self.assertSetEqual( 621 prepare_labels(r, '', 'blade-runner'), 622 {'blade-runner', 'nightly', 'experimental', 623 'webkitgtk_minibrowser'} 624 ) 625 626 # Firefox Nightly 627 r._report['run_info']['product'] = 'firefox' 628 self.assertSetEqual( 629 prepare_labels(r, '', 'blade-runner'), 630 {'blade-runner', 'nightly', 'experimental', 'firefox'} 631 ) 632 633 # Firefox Beta 634 r._report['run_info']['browser_channel'] = 'beta' 635 self.assertSetEqual( 636 prepare_labels(r, '', 'blade-runner'), 637 {'blade-runner', 'beta', 'firefox'} 638 ) 639 640 def test_normalize_product_edge(self): 641 r = WPTReport() 642 r._report = { 643 'run_info': { 644 'product': 'edge', 645 } 646 } 647 self.assertSetEqual( 648 normalize_product(r), 649 {'edge', 'edgechromium'} 650 ) 651 self.assertEqual( 652 r.run_info['product'], 653 'edge' 654 ) 655 656 def test_normalize_product_edgechromium(self): 657 r = WPTReport() 658 r._report = { 659 'run_info': { 660 'product': 'edgechromium', 661 } 662 } 663 self.assertSetEqual( 664 normalize_product(r), 665 {'edge', 'edgechromium'} 666 ) 667 self.assertEqual( 668 r.run_info['product'], 669 'edge' 670 ) 671 672 def test_normalize_product_webkitgtk_minibrowser(self): 673 r = WPTReport() 674 r._report = { 675 'run_info': { 676 'product': 'webkitgtk_minibrowser', 677 } 678 } 679 self.assertSetEqual( 680 normalize_product(r), 681 {'webkitgtk', 'minibrowser'} 682 ) 683 self.assertEqual( 684 r.run_info['product'], 685 'webkitgtk' 686 ) 687 688 def test_normalize_product_noop(self): 689 r = WPTReport() 690 r._report = { 691 'run_info': { 692 'product': 'firefox', 693 } 694 } 695 self.assertSetEqual( 696 normalize_product(r), 697 set() 698 ) 699 self.assertEqual( 700 r.run_info['product'], 701 'firefox' 702 )