chromium/components/sync/PRESUBMIT_test.py

#!/usr/bin/env python
# Copyright 2016 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import os
import re
import sys
import unittest

import PRESUBMIT

sys.path.append(
  os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from PRESUBMIT_test_mocks import MockChange, MockOutputApi

class MockCannedChecks(object):
  def CheckChangeLintsClean(self, input_api, output_api, source_filter,
                            lint_filters, verbose_level):
    return []


class MockInputApi(object):
  """ Mocked input api for unit testing of presubmit.
  This lets us mock things like file system operations and changed files.
  """
  def __init__(self):
    self.canned_checks = MockCannedChecks()
    self.re = re
    self.os_path = os.path
    self.files = []
    self.is_committing = False

  def AffectedFiles(self):
    return self.files

  def AffectedSourceFiles(self):
    return self.files

  def ReadFile(self, f):
    """ Returns the mock contents of f if they've been defined.
    """
    for api_file in self.files:
        if api_file.LocalPath() == f:
            return api_file.NewContents()


class MockFile(object):
  """Mock file object so that presubmit can act invoke file system operations.
  """
  def __init__(self, local_path, new_contents):
    self._local_path = local_path
    self._new_contents = new_contents
    self._changed_contents = ([(i + 1, l) for i, l in enumerate(new_contents)])

  def ChangedContents(self):
    return self._changed_contents

  def NewContents(self):
    return self._new_contents

  def LocalPath(self):
    return self._local_path

  def AbsoluteLocalPath(self):
    return self._local_path


# Format string used as the contents of a mock sync.proto in order to
# test presubmit parsing of EntitySpecifics definition in that file.
MOCK_PROTOFILE_CONTENTS = ('\n'
  'message EntitySpecifics {\n'
  '  //comment\n'
  '\n'
  '  oneof specifics_variant {\n'
  '    AutofillSpecifics autofill = 123;\n'
  '    AppSpecifics app = 456;\n'
  '    AppSettingSpecifics app_setting = 789;\n'
  '    ExtensionSettingSpecifics extension_setting = 910;\n'
  '    //comment\n'
  '  }\n'
  '}\n'
  )


# Format string used as the contents of a mock data_type.cc
# in order to test presubmit parsing of the DataTypeInfoMap in that file.
MOCK_DATATYPE_CONTENTS =('\n'
  'const DataTypeInfo kDataTypeInfoMap[] = {\n'
  '// Some comment \n'
  '{APP_SETTINGS, "APP_SETTING", "app_settings", "App settings",\n'
  'sync_pb::EntitySpecifics::kAppSettingFieldNumber, 13},\n'
  '%s\n'
  '};\n')


class DataTypeInfoChangeTest(unittest.TestCase):
  """Unit testing class that contains tests for sync/PRESUBMIT.py.
  """
  def test_ValidChangeMultiLine(self):
    results = self._testChange('{APPS, "APP", "apps", "Apps",\n'
     'sync_pb::EntitySpecifics::kAppFieldNumber, 12},')
    self.assertEqual(0, len(results))

  def testValidChangeToleratesPluralization(self):
    results = self._testChange('{APPS, "APP", "apps", "App",\n'
      'sync_pb::EntitySpecifics::kAppFieldNumber, 12},')
    self.assertEqual(0, len(results))

  def testValidChangeGrandfatheredEntry(self):
    results = self._testChange('{PROXY_TABS, "", "", "Tabs", -1, 25},')
    self.assertEqual(0, len(results))

  # TODO(crbug.com/40744701): The only remaining deprecated type doesn't satisfy
  # this test, revisit it.
  def DISABLED_testValidChangeDeprecatedEntry(self):
    results = self._testChange('{DEPRECATED_SUPERVISED_USER_ALLOWLISTS,\n'
      '"MANAGED_USER_WHITELIST",\n'
      '"managed_user_whitelists", "Managed User Whitelists",\n'
      'sync_pb::EntitySpecifics::kManagedUserWhitelistFieldNumber, 33},')
    self.assertEqual(0, len(results))

  def testInvalidChangeMismatchedNotificationType(self):
    results = self._testChange('{AUTOFILL, "AUTOFILL_WRONG", "autofill",\n'
     '"Autofill",sync_pb::EntitySpecifics::kAutofillFieldNumber, 6},')
    self.assertEqual(1, len(results))
    self.assertTrue('notification type' in results[0].message)

  def testInvalidChangeInconsistentDataType(self):
    results = self._testChange('{AUTOFILL, "AUTOFILL", "autofill",\n'
     '"Autofill Extra",sync_pb::EntitySpecifics::kAutofillFieldNumber, 6},')
    self.assertEqual(1, len(results))
    self.assertTrue('data type string' in results[0].message)

  def testInvalidChangeNotTitleCased(self):
    results = self._testChange('{AUTOFILL, "AUTOFILL", "autofill",\n'
     '"autofill",sync_pb::EntitySpecifics::kAutofillFieldNumber, 6},')
    self.assertEqual(1, len(results))
    self.assertTrue('title' in results[0].message)

  def testInvalidChangeInconsistentRootTag(self):
    results = self._testChange('{AUTOFILL, "AUTOFILL", "autofill root",\n'
     '"Autofill",sync_pb::EntitySpecifics::kAutofillFieldNumber, 6},')
    self.assertEqual(1, len(results))
    self.assertTrue('root tag' in results[0].message)

  def testInvalidChangeDuplicatedValues(self):
    results = self._testChange('{APP_SETTINGS, "APP_SETTING",\n'
      '"app_settings", "App settings",\n'
      'sync_pb::EntitySpecifics::kAppSettingFieldNumber, 13},\n')
    self.assertEqual(6, len(results))
    self.assertTrue('APP_SETTINGS' in results[0].message)

  def testBlocklistedRootTag(self):
    results = self._testChange('{EXTENSION_SETTING, "EXTENSION_SETTING",\n'
      '"_mts_schema_descriptor","Extension Setting",\n'
      'sync_pb::EntitySpecifics::kExtensionSettingFieldNumber, 6},')
    self.assertEqual(2, len(results))
    self.assertTrue('_mts_schema_descriptor' in results[0].message)
    self.assertTrue("blocklist" in results[0].message)

  def testProtoChangeWithoutVisitors(self):
    files = [
      MockFile(os.path.abspath('./protocol/entity_specifics.proto'), '')
    ]
    results = self._testChangeWithFiles(files)
    # Changing a .proto file without also updating proto_visitors.h should
    # result in a warning.
    self.assertEqual(1, len(results))
    self.assertTrue("proto_visitors.h" in results[0].message)

  def testProtoChangeWithVisitors(self):
    files = [
      MockFile(os.path.abspath('./protocol/entity_specifics.proto'), ''),
      MockFile(os.path.abspath('./protocol/proto_visitors.h'), '')
    ]
    results = self._testChangeWithFiles(files)
    # Changing .proto files along with proto_visitors.h is good.
    self.assertEqual(0, len(results))

  def testProtoVisitorsChange(self):
    files = [
      MockFile(os.path.abspath('./protocol/proto_visitors.h'), '')
    ]
    results = self._testChangeWithFiles(files)
    # Changing proto_visitors.h without changing any proto files is fine.
    self.assertEqual(0, len(results))

  def _testChangeWithFiles(self, files):
    mock_input_api = MockInputApi()
    mock_input_api.files = files
    return PRESUBMIT.CheckChangeOnCommit(mock_input_api, MockOutputApi())

  def _testChange(self, datatype_literal):
    files = [
      MockFile(os.path.abspath('./protocol/entity_specifics.proto'),
        MOCK_PROTOFILE_CONTENTS),
      MockFile(os.path.abspath('./protocol/proto_visitors.h'), ''),
      MockFile(os.path.abspath('./base/data_type.cc'),
        MOCK_DATATYPE_CONTENTS % (datatype_literal))
    ]
    return self._testChangeWithFiles(files)


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