chromium/content/test/gpu/gpu_tests/test_expectations_unittest.py

# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import inspect
import os
import re
from typing import List, Optional, Type
import unittest
import unittest.mock as mock

import gpu_project_config

from gpu_tests import common_typing as ct
from gpu_tests import gpu_helper
from gpu_tests import gpu_integration_test
from gpu_tests import pixel_integration_test
from gpu_tests import pixel_test_pages
from gpu_tests import trace_integration_test as trace_it
from gpu_tests import trace_test_pages
from gpu_tests import webgl1_conformance_integration_test as webgl1_cit
from gpu_tests import webgl2_conformance_integration_test as webgl2_cit
from gpu_tests import webgl_test_util

from py_utils import discover
from py_utils import tempfile_ext

from typ import expectations_parser
from typ import json_results


VALID_BUG_REGEXES = [
    re.compile(r'crbug\.com\/\d+'),
    re.compile(r'crbug\.com\/angleproject\/\d+'),
    re.compile(r'crbug\.com\/dawn\/\d+'),
    re.compile(r'crbug\.com\/swiftshader\/\d+'),
    re.compile(r'crbug\.com\/tint\/\d+'),
    re.compile(r'skbug\.com\/\d+'),
]

ResultType = json_results.ResultType

INTEL_DRIVER_VERSION_SCHEMA = """
The version format of Intel graphics driver is AA.BB.CC.DDDD (legacy schema)
and AA.BB.CCC.DDDD (new schema).

AA.BB: You are free to specify the real number here, but they are meaningless
when comparing two version numbers. Usually it's okay to leave it to "0.0".

CC or CCC: It's meaningful to indicate different branches. Different CC means
different branch, while all CCCs share the same branch.

DDDD: It's always meaningful.
"""


def check_intel_driver_version(version: str) -> bool:
  ver_list = version.split('.')
  if len(ver_list) != 4:
    return False
  for ver in ver_list:
    if not ver.isdigit():
      return False
  return True


def _ExtractUnitTestTestExpectations(file_name: str) -> List[str]:
  file_name = os.path.join(
      os.path.dirname(os.path.abspath(__file__)), '..', 'unittest_data',
      'test_expectations', file_name)
  test_expectations_list = []
  with open(file_name, 'r') as test_data:
    test_expectations = ''
    reach_end = False
    while not reach_end:
      line = test_data.readline()
      if line:
        line = line.strip()
        if line:
          test_expectations += line + '\n'
          continue
      else:
        reach_end = True

      if test_expectations:
        test_expectations_list.append(test_expectations)
        test_expectations = ''

  assert test_expectations_list
  return test_expectations_list


def CheckTestExpectationsAreForExistingTests(
    unittest_testcase: unittest.TestCase,
    test_class: Type[gpu_integration_test.GpuIntegrationTest],
    mock_options: mock.MagicMock,
    test_names: Optional[List[str]] = None) -> None:
  test_names = test_names or [
      args[0] for args in test_class.GenerateTestCases__RunGpuTest(mock_options)
  ]
  expectations_file = test_class.ExpectationsFiles()[0]
  with open(expectations_file, 'r') as f:
    test_expectations = expectations_parser.TestExpectations()
    test_expectations.parse_tagged_list(f.read(), f.name)
    broke_expectations = '\n'.join([
        "\t- {0}:{1}: Expectation with pattern '{2}' does not match"
        ' any tests in the {3} test suite'.format(f.name, exp.lineno, exp.test,
                                                  test_class.Name())
        for exp in test_expectations.check_for_broken_expectations(test_names)
    ])
    unittest_testcase.assertEqual(
        broke_expectations, '',
        'The following expectations were found to not apply to any tests in '
        'the %s test suite:\n%s' % (test_class.Name(), broke_expectations))


def CheckTestExpectationPatternsForConflicts(
    expectations: str, file_name: str,
    tag_conflict_checker: ct.TagConflictChecker) -> str:
  test_expectations = expectations_parser.TestExpectations()
  _, errors = test_expectations.parse_tagged_list(
      expectations, file_name=file_name, tags_conflict=tag_conflict_checker)
  return errors


def _FindTestCases() -> List[Type[gpu_integration_test.GpuIntegrationTest]]:
  test_cases = []
  for start_dir in gpu_project_config.CONFIG.start_dirs:
    # Note we deliberately only scan the integration tests as a
    # workaround for http://crbug.com/1195465 .
    modules_to_classes = discover.DiscoverClasses(
        start_dir,
        gpu_project_config.CONFIG.top_level_dir,
        base_class=gpu_integration_test.GpuIntegrationTest,
        pattern='*_integration_test.py')
    test_cases.extend(modules_to_classes.values())
  return test_cases


class GpuTestExpectationsValidation(unittest.TestCase):
  maxDiff = None

  def testNoConflictsInGpuTestExpectations(self) -> None:
    errors = ''
    for test_case in _FindTestCases():
      if 'gpu_tests.gpu_integration_test_unittest' not in test_case.__module__:
        webgl_version = 1
        if issubclass(test_case, webgl2_cit.WebGL2ConformanceIntegrationTest):
          webgl_version = 2
        _ = list(
            test_case.GenerateTestCases__RunGpuTest(
                gpu_helper.GetMockArgs(webgl_version=('%d.0.0' %
                                                      webgl_version))))
        if test_case.ExpectationsFiles():
          with open(test_case.ExpectationsFiles()[0]) as f:
            errors += CheckTestExpectationPatternsForConflicts(
                f.read(), os.path.basename(f.name),
                test_case.GetTagConflictChecker())
    self.assertEqual(errors, '')

  def testExpectationsFilesCanBeParsed(self) -> None:
    for test_case in _FindTestCases():
      if 'gpu_tests.gpu_integration_test_unittest' not in test_case.__module__:
        webgl_version = 1
        if issubclass(test_case, webgl2_cit.WebGL2ConformanceIntegrationTest):
          webgl_version = 2
        _ = list(
            test_case.GenerateTestCases__RunGpuTest(
                gpu_helper.GetMockArgs(webgl_version=('%d.0.0' %
                                                      webgl_version))))
        if test_case.ExpectationsFiles():
          with open(test_case.ExpectationsFiles()[0]) as f:
            test_expectations = expectations_parser.TestExpectations()
            ret, err = test_expectations.parse_tagged_list(f.read(), f.name)
            self.assertEqual(
                ret, 0,
                'Error parsing %s:\n\t%s' % (os.path.basename(f.name), err))

  def testWebglTestPathsExist(self) -> None:
    def _CheckWebglConformanceTestPathIsValid(pattern: str) -> None:
      if not 'WebglExtension_' in pattern:
        full_path = os.path.normpath(
            os.path.join(webgl_test_util.conformance_path, pattern))
        self.assertTrue(os.path.exists(full_path),
                        '%s does not exist' % full_path)

    webgl_test_classes = (
        webgl1_cit.WebGL1ConformanceIntegrationTest,
        webgl2_cit.WebGL2ConformanceIntegrationTest,
    )
    for webgl_version in range(1, 3):
      webgl_test_class = webgl_test_classes[webgl_version - 1]
      _ = list(
          webgl_test_class.GenerateTestCases__RunGpuTest(
              gpu_helper.GetMockArgs(webgl_version='%d.0.0' % webgl_version)))
      with open(webgl_test_class.ExpectationsFiles()[0], 'r') as f:
        expectations = expectations_parser.TestExpectations()
        expectations.parse_tagged_list(f.read())
        for pattern, _ in expectations.individual_exps.items():
          _CheckWebglConformanceTestPathIsValid(pattern)

  # Pixel tests are handled separately since some tests are only generated on
  # certain platforms.
  def testForBrokenPixelTestExpectations(self) -> None:
    pixel_test_names = []
    for _, method in inspect.getmembers(pixel_test_pages.PixelTestPages,
                                        predicate=inspect.isfunction):
      pixel_test_names.extend([
          p.name for p in method(
              pixel_integration_test.PixelIntegrationTest.test_base_name)
      ])
    CheckTestExpectationsAreForExistingTests(
        self, pixel_integration_test.PixelIntegrationTest,
        gpu_helper.GetMockArgs(), pixel_test_names)

  # Trace tests are handled separately since some tests are only generated on
  # certain platforms.
  def testForBrokenTraceTestExpectations(self):
    generator_functions = []
    for _, method in inspect.getmembers(trace_test_pages.TraceTestPages,
                                        predicate=inspect.isfunction):
      generator_functions.append(method)

    trace_test_names = []
    # This results in a set of tests which is a superset of actual tests that
    # can be generated.
    for prefix in trace_it.TraceIntegrationTest.known_test_prefixes:
      for method in generator_functions:
        trace_test_names.extend([p.name for p in method(prefix)])
    CheckTestExpectationsAreForExistingTests(self,
                                             trace_it.TraceIntegrationTest,
                                             gpu_helper.GetMockArgs(),
                                             trace_test_names)

  # WebGL tests are handled separately since test case generation varies
  # depending on inputs.
  def testForBrokenWebGlTestExpectations(self) -> None:
    webgl_test_classes_and_versions = (
        (webgl1_cit.WebGL1ConformanceIntegrationTest, '1.0.4'),
        (webgl2_cit.WebGL2ConformanceIntegrationTest, '2.0.1'),
    )
    for webgl_test_class, webgl_version in webgl_test_classes_and_versions:
      args = gpu_helper.GetMockArgs(webgl_version=webgl_version)
      test_names = [
          test[0]
          for test in webgl_test_class.GenerateTestCases__RunGpuTest(args)
      ]
      CheckTestExpectationsAreForExistingTests(self, webgl_test_class, args,
                                               test_names)

  def testForBrokenGpuTestExpectations(self) -> None:
    options = gpu_helper.GetMockArgs()
    for test_case in _FindTestCases():
      if 'gpu_tests.gpu_integration_test_unittest' not in test_case.__module__:
        # Pixel, trace, and WebGL are handled in dedicated unittests.
        if (test_case.Name() not in ('pixel', 'trace_test',
                                     'webgl1_conformance', 'webgl2_conformance')
            and test_case.ExpectationsFiles()):
          CheckTestExpectationsAreForExistingTests(self, test_case, options)

  def testExpectationBugValidity(self) -> None:
    expectation_dir = os.path.join(os.path.dirname(__file__),
                                   'test_expectations')
    for expectation_file in (f for f in os.listdir(expectation_dir)
                             if f.endswith('.txt')):
      with open(os.path.join(expectation_dir, expectation_file)) as f:
        content = f.read()
      list_parser = expectations_parser.TaggedTestListParser(content)
      for expectation in list_parser.expectations:
        reason = expectation.reason
        if not reason:
          continue
        if not any(r.match(reason) for r in VALID_BUG_REGEXES):
          self.fail('Bug string "%s" in expectation file %s is either not in a '
                    'recognized format or references an unknown project.' %
                    (reason, expectation_file))

  def testWebglTestExpectationsForDriverTags(self) -> None:
    webgl_test_classes = (
        webgl1_cit.WebGL1ConformanceIntegrationTest,
        webgl2_cit.WebGL2ConformanceIntegrationTest,
    )
    expectations_driver_tags = set()
    for webgl_version in range(1, 3):
      webgl_conformance_test_class = webgl_test_classes[webgl_version - 1]
      _ = list(
          webgl_conformance_test_class.GenerateTestCases__RunGpuTest(
              gpu_helper.GetMockArgs(webgl_version=('%d.0.0' % webgl_version))))
      with open(webgl_conformance_test_class.ExpectationsFiles()[0], 'r') as f:
        parser = expectations_parser.TestExpectations()
        parser.parse_tagged_list(f.read(), f.name)
        driver_tag_set = set()
        for tag_set in parser.tag_sets:
          if gpu_helper.MatchDriverTag(list(tag_set)[0]):
            for tag in tag_set:
              match = gpu_helper.MatchDriverTag(tag)
              self.assertIsNotNone(match)
              if match.group(1) == 'intel':
                self.assertTrue(check_intel_driver_version(match.group(3)))

            self.assertSetEqual(driver_tag_set, set())
            driver_tag_set = tag_set
          else:
            for tag in tag_set:
              self.assertIsNone(gpu_helper.MatchDriverTag(tag))
        expectations_driver_tags |= driver_tag_set

    self.assertEqual(gpu_helper.ExpectationsDriverTags(),
                     expectations_driver_tags)


class TestGpuTestExpectationsValidators(unittest.TestCase):

  def setUp(self):
    self.conflict_checker = (
        gpu_integration_test.GpuIntegrationTest.GetTagConflictChecker())

  def testConflictInTestExpectationsWithGpuDriverTags(self) -> None:
    failed_test_expectations = _ExtractUnitTestTestExpectations(
        'failed_test_expectations_with_driver_tags.txt')
    self.assertTrue(
        all(
            CheckTestExpectationPatternsForConflicts(
                test_expectations, 'test.txt', self.conflict_checker)
            for test_expectations in failed_test_expectations))

  def testNoConflictInTestExpectationsWithGpuDriverTags(self) -> None:
    passed_test_expectations = _ExtractUnitTestTestExpectations(
        'passed_test_expectations_with_driver_tags.txt')
    for test_expectations in passed_test_expectations:
      errors = CheckTestExpectationPatternsForConflicts(test_expectations,
                                                        'test.txt',
                                                        self.conflict_checker)
      self.assertFalse(errors)

  def testConflictsBetweenAngleAndNonAngleConfigurations(self) -> None:
    test_expectations = """
    # tags: [ android ]
    # tags: [ android-nexus-5x ]
    # tags: [ opengles ]
    # results: [ RetryOnFailure Skip ]
    [ android android-nexus-5x ] a/b/c/d [ RetryOnFailure ]
    [ android opengles ] a/b/c/d [ Skip ]
    """
    errors = CheckTestExpectationPatternsForConflicts(test_expectations,
                                                      'test.txt',
                                                      self.conflict_checker)
    self.assertTrue(errors)

  def testConflictBetweenTestExpectationsWithOsNameAndOSVersionTags(self
                                                                    ) -> None:
    test_expectations = """# tags: [ mac win linux win10 ]
    # tags: [ intel amd nvidia ]
    # tags: [ debug release ]
    # results: [ Failure Skip ]
    [ intel win10 ] a/b/c/d [ Failure ]
    [ intel win debug ] a/b/c/d [ Skip ]
    """
    errors = CheckTestExpectationPatternsForConflicts(test_expectations,
                                                      'test.txt',
                                                      self.conflict_checker)
    self.assertTrue(errors)

  def testNoConflictBetweenOsVersionTags(self) -> None:
    test_expectations = """# tags: [ mac win linux xp win7 ]
    # tags: [ intel amd nvidia ]
    # tags: [ debug release ]
    # results: [ Failure Skip ]
    [ intel win7 ] a/b/c/d [ Failure ]
    [ intel xp debug ] a/b/c/d [ Skip ]
    """
    errors = CheckTestExpectationPatternsForConflicts(test_expectations,
                                                      'test.txt',
                                                      self.conflict_checker)
    self.assertFalse(errors)

  def testConflictBetweenGpuVendorAndGpuDeviceIdTags(self) -> None:
    test_expectations = """# tags: [ mac win linux xp win7 ]
    # tags: [ intel amd nvidia nvidia-0x1cb3 nvidia-0x2184 ]
    # tags: [ debug release ]
    # results: [ Failure Skip ]
    [ nvidia-0x1cb3 ] a/b/c/d [ Failure ]
    [ nvidia debug ] a/b/c/d [ Skip ]
    """
    errors = CheckTestExpectationPatternsForConflicts(test_expectations,
                                                      'test.txt',
                                                      self.conflict_checker)
    self.assertTrue(errors)

  def testNoConflictBetweenGpuDeviceIdTags(self) -> None:
    test_expectations = """# tags: [ mac win linux xp win7 ]
    # tags: [ intel amd nvidia nvidia-0x01 nvidia-0x02 ]
    # tags: [ debug release ]
    # results: [ Failure Skip ]
    [ nvidia-0x01 win7 ] a/b/c/d [ Failure ]
    [ nvidia-0x02 win7 debug ] a/b/c/d [ Skip ]
    [ nvidia win debug ] a/b/c/* [ Skip ]
    """
    errors = CheckTestExpectationPatternsForConflicts(test_expectations,
                                                      'test.txt',
                                                      self.conflict_checker)
    self.assertFalse(errors)

  def testFoundBrokenExpectations(self) -> None:
    test_expectations = ('# tags: [ mac ]\n'
                         '# results: [ Failure ]\n'
                         '[ mac ] a/b/d [ Failure ]\n'
                         'a/c/* [ Failure ]\n')
    options = gpu_helper.GetMockArgs()
    test_class = gpu_integration_test.GpuIntegrationTest
    with tempfile_ext.NamedTemporaryFile(mode='w') as expectations_file,    \
         mock.patch.object(
             test_class, 'GenerateTestCases__RunGpuTest',
             return_value=[('a/b/c', ())]),                                 \
         mock.patch.object(
             test_class,
             'ExpectationsFiles', return_value=[expectations_file.name]):
      expectations_file.write(test_expectations)
      expectations_file.close()
      with self.assertRaises(AssertionError) as context:
        CheckTestExpectationsAreForExistingTests(self, test_class, options)
      self.assertIn(
          'The following expectations were found to not apply'
          ' to any tests in the GpuIntegrationTest test suite',
          str(context.exception))
      self.assertIn(
          "4: Expectation with pattern 'a/c/*' does not match"
          ' any tests in the GpuIntegrationTest test suite',
          str(context.exception))
      self.assertIn(
          "3: Expectation with pattern 'a/b/d' does not match"
          ' any tests in the GpuIntegrationTest test suite',
          str(context.exception))


  def testDriverVersionComparision(self) -> None:
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000', 'eq',
                                             '24.20.100.7000'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100', 'ne',
                                             '24.20.100.7000'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000', 'gt',
                                             '24.20.100'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000a', 'gt',
                                             '24.20.100.7000'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000', 'lt',
                                             '24.20.100.7001'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000', 'lt',
                                             '24.20.200.6000'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000', 'lt',
                                             '25.30.100.6000', 'linux',
                                             'intel'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000', 'gt',
                                             '25.30.100.6000', 'win', 'intel'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.101.6000', 'gt',
                                             '25.30.100.7000', 'win', 'intel'))
    self.assertFalse(
        gpu_helper.EvaluateVersionComparison('24.20.99.7000', 'gt',
                                             '24.20.100.7000', 'win', 'intel'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.99.7000', 'lt',
                                             '24.20.100.7000', 'win', 'intel'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.99.7000', 'ne',
                                             '24.20.100.7000', 'win', 'intel'))
    self.assertFalse(
        gpu_helper.EvaluateVersionComparison('24.20.100', 'lt',
                                             '24.20.100.7000', 'win', 'intel'))
    self.assertFalse(
        gpu_helper.EvaluateVersionComparison('24.20.100', 'gt',
                                             '24.20.100.7000', 'win', 'intel'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100', 'ne',
                                             '24.20.100.7000', 'win', 'intel'))
    self.assertTrue(
        gpu_helper.EvaluateVersionComparison('24.20.100.7000', 'eq',
                                             '25.20.100.7000', 'win', 'intel'))


if __name__ == '__main__':
  unittest.main(verbosity=2)