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          )