blob: 0c739a526f039f710115e6e67fd9341592ac08fa [file] [log] [blame]
# Copyright 2025 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 sys
import tempfile
import unittest
from unittest import mock
import PRESUBMIT
class MockInputApi:
def __init__(self):
self.os_path = os.path
self.change = mock.Mock()
@staticmethod
def PresubmitLocalPath():
return os.path.dirname(__file__)
class MockOutputApi:
@staticmethod
def PresubmitError(message, items=None):
# Using a tuple to make it easy to compare.
return ('Error', message, items)
@staticmethod
def PresubmitResult(message, items=None):
# Using a tuple to make it easy to compare.
return ('Result', message, items)
class UndeclaredFeaturesTest(unittest.TestCase):
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.repo_root = self.temp_dir.name
self.mock_input_api = MockInputApi()
self.mock_input_api.change.RepositoryRoot.return_value = self.repo_root
self.mock_output_api = MockOutputApi()
def tearDown(self):
self.temp_dir.cleanup()
# Ensure find_features is reloaded for the next test or test suite
# by removing it from the module cache. This prevents test state from
# leaking.
if 'find_features' in sys.modules:
del sys.modules['find_features']
def _create_file_in_repo(self, path, content):
full_path = os.path.join(self.repo_root, path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
with open(full_path, 'w', encoding='utf-8') as f:
f.write(content)
def testAllFeaturesOnChangedLinesAreDeclared(self):
# Declare a variety of features using both 2- and 3-argument macros.
self._create_file_in_repo(
'components/feature_a.cc',
'BASE_FEATURE(FeatureA, base::FEATURE_ENABLED_BY_DEFAULT);')
self._create_file_in_repo(
'chrome/feature_b.cc', 'BASE_FEATURE(kFeatureB, "FeatureB", '
'base::FEATURE_ENABLED_BY_DEFAULT);')
self._create_file_in_repo(
'components/feature_c.cc', 'BASE_FEATURE(kFeatureC, "FeatureC",\n'
' base::FEATURE_ENABLED_BY_DEFAULT);')
json_data = {
'Study1': [{
'experiments': [{
'name': 'group1',
'enable_features': ['FeatureA', 'FeatureB'],
}]
}],
'Study2_Unaffected': [{
'experiments': [{
'name': 'group2',
'enable_features': ['UndeclaredFeature'],
}]
}]
}
# The check should only trigger on features in changed lines.
# FeatureA and FeatureB are declared, so this should pass.
# UndeclaredFeature is not on a changed line, so it's ignored.
changed_lines = [(1, '"enable_features": ["FeatureA", "FeatureB"]')]
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, json_data,
changed_lines)
self.assertEqual(result, [])
def testUndeclaredFeatureOnChangedLine(self):
self._create_file_in_repo(
'components/feature_a.cc',
'BASE_FEATURE(FeatureA, base::FEATURE_ENABLED_BY_DEFAULT);')
json_data = {
'Study1': [{
'experiments': [{
'name': 'group1',
'enable_features': ['FeatureA', 'UndeclaredFeature']
}]
}]
}
changed_lines = [(1, '"enable_features": ["UndeclaredFeature"]')]
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, json_data,
changed_lines)
self.assertEqual(len(result), 1)
self.assertEqual(result[0][0], 'Result')
self.assertIn('UndeclaredFeature', str(result[0]))
self.assertIn('Study1', str(result[0]))
def testFeatureWithMacroParametersOnDifferentLines(self):
self._create_file_in_repo(
'components/feature_d.cc', 'BASE_FEATURE(kFeatureD,\n'
' "FeatureD",\n'
' base::FEATURE_ENABLED_BY_DEFAULT);')
json_data = {
'Study1': [{
'experiments': [{
'name': 'group1',
'enable_features': ['FeatureD']
}]
}]
}
changed_lines = [(1, '"enable_features": ["FeatureD"]')]
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, json_data,
changed_lines)
self.assertEqual(result, [])
def testOldTwoParameterFeatureWithMacroParametersOnDifferentLines(self):
self._create_file_in_repo(
'components/feature_e.cc', 'BASE_FEATURE(FeatureE,\n'
' base::FEATURE_ENABLED_BY_DEFAULT);')
json_data = {
'Study1': [{
'experiments': [{
'name': 'group1',
'enable_features': ['FeatureE']
}]
}]
}
changed_lines = [(1, '"enable_features": ["FeatureE"]')]
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, json_data,
changed_lines)
self.assertEqual(result, [])
def testTwoParameterMacro(self):
self._create_file_in_repo(
'components/feature_g.cc',
'BASE_FEATURE(kFeatureG, base::FEATURE_ENABLED_BY_DEFAULT);')
json_data = {
'Study1': [{
'experiments': [{
'name': 'group1',
'enable_features': ['FeatureG']
}]
}]
}
changed_lines = [(1, '"enable_features": ["FeatureG"]')]
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, json_data,
changed_lines)
self.assertEqual(result, [])
def testTwoParameterMacroWithParametersOnDifferentLines(self):
self._create_file_in_repo(
'components/feature_h.cc', 'BASE_FEATURE(kFeatureH,\n'
' base::FEATURE_ENABLED_BY_DEFAULT);')
json_data = {
'Study1': [{
'experiments': [{
'name': 'group1',
'enable_features': ['FeatureH']
}]
}]
}
changed_lines = [(1, '"enable_features": ["FeatureH"]')]
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, json_data,
changed_lines)
self.assertEqual(result, [])
def testFeatureWithPreprocessorDirectives(self):
self._create_file_in_repo(
'components/feature_f.cc', 'BASE_FEATURE(kFeatureF,\n'
' "FeatureF",\n'
'#if BUILDFLAG(IS_APPLE) || BUILDFLAG(IS_WIN)\n'
' base::FEATURE_ENABLED_BY_DEFAULT\n'
'#else\n'
' base::FEATURE_DISABLED_BY_DEFAULT\n'
'#endif\n'
');')
json_data = {
'Study1': [{
'experiments': [{
'name': 'group1',
'enable_features': ['FeatureF']
}]
}]
}
changed_lines = [(1, '"enable_features": ["FeatureF"]')]
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, json_data,
changed_lines)
self.assertEqual(result, [])
def testNoFeaturesDeclaredInRepo(self):
# No files created, so no features will be found, which is an error
# condition.
result = PRESUBMIT.CheckUndeclaredFeatures(self.mock_input_api,
self.mock_output_api, {}, [])
self.assertEqual(len(result), 1)
self.assertEqual(result[0][0], 'Error')
self.assertIn('unable to find any declared flags', result[0][1])
if __name__ == '__main__':
unittest.main()