| #!/usr/bin/env vpython3 | |
| # Copyright 2026 The Chromium Authors | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import io | |
| import json | |
| import os | |
| import pathlib | |
| import subprocess | |
| import sys | |
| import textwrap | |
| import unittest | |
| from unittest import mock | |
| from pyfakefs import fake_filesystem_unittest | |
| import ci_only_validator | |
| class TestCIOnlyValidator(fake_filesystem_unittest.TestCase): | |
| def setUp(self): | |
| # Resolve the path on the real filesystem first before pyfakefs is active. | |
| self.real_src_root = ( | |
| pathlib.Path(ci_only_validator.__file__).resolve().parents[2] | |
| ) | |
| self.setUpPyfakefs() | |
| self.fs.create_dir(self.real_src_root) | |
| # Set up global mocking for subprocess.run. | |
| self.subprocess_run_patcher = mock.patch('subprocess.run') | |
| self.mock_run = self.subprocess_run_patcher.start() | |
| self.addCleanup(self.subprocess_run_patcher.stop) | |
| def test_discover_trybots_for_test_suite(self): | |
| self.mock_run.return_value = mock.MagicMock( | |
| returncode=0, | |
| stdout=textwrap.dedent("""\ | |
| infra/config/generated/builders/try/android-code-coverage/targets/chromium.coverage.json | |
| infra/config/generated/builders/try/android-x86-code-coverage/targets/chromium.coverage.json | |
| """), | |
| ) | |
| src_root = pathlib.Path('/mock/src') | |
| entries = ci_only_validator.discover_trybots_for_test_suite( | |
| src_root, 'chrome_junit_tests' | |
| ) | |
| self.assertEqual( | |
| entries, | |
| [ | |
| ci_only_validator.TrybotEntry( | |
| 'android-code-coverage', 'chromium.coverage' | |
| ), | |
| ci_only_validator.TrybotEntry( | |
| 'android-x86-code-coverage', 'chromium.coverage' | |
| ), | |
| ], | |
| ) | |
| def test_discover_trybots_for_test_suite_error(self): | |
| self.mock_run.return_value = mock.MagicMock(returncode=1) | |
| src_root = pathlib.Path('/mock/src') | |
| entries = ci_only_validator.discover_trybots_for_test_suite( | |
| src_root, 'chrome_junit_tests' | |
| ) | |
| self.assertEqual(entries, []) | |
| def test_discover_trybots_for_test_suite_with_revision(self): | |
| self.mock_run.return_value = mock.MagicMock( | |
| returncode=0, | |
| stdout=textwrap.dedent("""\ | |
| myrev:infra/config/generated/builders/try/android-code-coverage/targets/chromium.coverage.json | |
| """), | |
| ) | |
| src_root = pathlib.Path('/mock/src') | |
| entries = ci_only_validator.discover_trybots_for_test_suite( | |
| src_root, 'chrome_junit_tests', 'myrev' | |
| ) | |
| self.assertEqual( | |
| entries, | |
| [ | |
| ci_only_validator.TrybotEntry( | |
| 'android-code-coverage', 'chromium.coverage' | |
| ) | |
| ], | |
| ) | |
| def test_read_local_success(self): | |
| src_root = pathlib.Path('/mock/src') | |
| dummy_file = src_root / 'dummy_path' | |
| self.fs.create_file(dummy_file, contents='file_content') | |
| content = ci_only_validator.read_local(src_root, 'dummy_path') | |
| self.assertEqual(content, 'file_content') | |
| def test_read_local_not_found(self): | |
| src_root = pathlib.Path('/mock/src') | |
| content = ci_only_validator.read_local(src_root, 'non_existent_file') | |
| self.assertIsNone(content) | |
| def test_read_local_os_error(self): | |
| src_root = pathlib.Path('/mock/src') | |
| dummy_file = src_root / 'dummy_path' | |
| # Create an unreadable file to raise OSError/PermissionError natively. | |
| self.fs.create_file(dummy_file, contents='') | |
| os.chmod(dummy_file, 0o000) | |
| # Redirect stderr to suppress prints in test output | |
| with mock.patch('sys.stderr', new=io.StringIO()): | |
| content = ci_only_validator.read_local(src_root, 'dummy_path') | |
| self.assertIsNone(content) | |
| def test_read_git_success(self): | |
| self.mock_run.return_value = mock.MagicMock( | |
| returncode=0, stdout='git_file_content' | |
| ) | |
| src_root = pathlib.Path('/mock/src') | |
| content = ci_only_validator.read_git(src_root, 'myrev', 'dummy_path') | |
| self.assertEqual(content, 'git_file_content') | |
| def test_read_git_error(self): | |
| self.mock_run.return_value = mock.MagicMock(returncode=1) | |
| src_root = pathlib.Path('/mock/src') | |
| content = ci_only_validator.read_git(src_root, 'myrev', 'dummy_path') | |
| self.assertIsNone(content) | |
| def test_check_coverage_enabled_true(self): | |
| mock_read = mock.MagicMock( | |
| return_value=json.dumps({'gn_args': {'use_clang_coverage': True}}) | |
| ) | |
| enabled = ci_only_validator.check_coverage_enabled( | |
| mock_read, 'android-code-coverage' | |
| ) | |
| self.assertTrue(enabled) | |
| def test_check_coverage_enabled_false(self): | |
| mock_read = mock.MagicMock( | |
| return_value=json.dumps({'gn_args': {'use_clang_coverage': False}}) | |
| ) | |
| enabled = ci_only_validator.check_coverage_enabled( | |
| mock_read, 'android-code-coverage' | |
| ) | |
| self.assertFalse(enabled) | |
| def test_check_coverage_enabled_none_content(self): | |
| mock_read = mock.MagicMock(return_value=None) | |
| enabled = ci_only_validator.check_coverage_enabled( | |
| mock_read, 'android-code-coverage' | |
| ) | |
| self.assertFalse(enabled) | |
| def test_check_coverage_enabled_invalid_json(self): | |
| mock_read = mock.MagicMock(return_value='{invalid_json') | |
| enabled = ci_only_validator.check_coverage_enabled( | |
| mock_read, 'android-code-coverage' | |
| ) | |
| self.assertFalse(enabled) | |
| def test_check_trybot_coverage_config_ci_only_true(self): | |
| targets_json = { | |
| 'android-code-coverage': { | |
| 'isolated_scripts': [{'name': 'chrome_junit_tests', 'ci_only': True}] | |
| } | |
| } | |
| mock_read = mock.MagicMock(return_value=json.dumps(targets_json)) | |
| results = ci_only_validator.check_trybot_coverage_config( | |
| mock_read, | |
| 'android-code-coverage', | |
| 'chromium.coverage', | |
| 'chrome_junit_tests', | |
| ) | |
| self.assertEqual( | |
| results, | |
| [ci_only_validator.BuilderConfig('android-code-coverage', True)], | |
| ) | |
| def test_check_trybot_coverage_config_ci_only_false(self): | |
| targets_json = { | |
| 'android-code-coverage': { | |
| 'isolated_scripts': [{'name': 'chrome_junit_tests', 'ci_only': False}] | |
| } | |
| } | |
| mock_read = mock.MagicMock(return_value=json.dumps(targets_json)) | |
| results = ci_only_validator.check_trybot_coverage_config( | |
| mock_read, | |
| 'android-code-coverage', | |
| 'chromium.coverage', | |
| 'chrome_junit_tests', | |
| ) | |
| self.assertEqual( | |
| results, | |
| [ci_only_validator.BuilderConfig('android-code-coverage', False)], | |
| ) | |
| def test_check_trybot_coverage_config_ci_only_omitted(self): | |
| # Tests that when 'ci_only' is omitted entirely, it defaults to False. | |
| targets_json = { | |
| 'android-code-coverage': { | |
| 'isolated_scripts': [{'name': 'chrome_junit_tests'}] | |
| } | |
| } | |
| mock_read = mock.MagicMock(return_value=json.dumps(targets_json)) | |
| results = ci_only_validator.check_trybot_coverage_config( | |
| mock_read, | |
| 'android-code-coverage', | |
| 'chromium.coverage', | |
| 'chrome_junit_tests', | |
| ) | |
| self.assertEqual( | |
| results, | |
| [ci_only_validator.BuilderConfig('android-code-coverage', False)], | |
| ) | |
| def test_check_trybot_coverage_config_none_content(self): | |
| mock_read = mock.MagicMock(return_value=None) | |
| results = ci_only_validator.check_trybot_coverage_config( | |
| mock_read, | |
| 'android-code-coverage', | |
| 'chromium.coverage', | |
| 'chrome_junit_tests', | |
| ) | |
| self.assertEqual(results, []) | |
| def test_check_trybot_coverage_config_invalid_json(self): | |
| mock_read = mock.MagicMock(return_value='{invalid_json') | |
| # Redirect stderr to suppress prints in test output | |
| with mock.patch('sys.stderr', new=io.StringIO()): | |
| results = ci_only_validator.check_trybot_coverage_config( | |
| mock_read, | |
| 'android-code-coverage', | |
| 'chromium.coverage', | |
| 'chrome_junit_tests', | |
| ) | |
| self.assertEqual(results, []) | |
| @mock.patch('ci_only_validator.discover_trybots_for_test_suite') | |
| def test_main_success_disk(self, mock_discover): | |
| mock_discover.return_value = [ | |
| ci_only_validator.TrybotEntry( | |
| 'android-code-coverage', 'chromium.coverage' | |
| ) | |
| ] | |
| targets_json = { | |
| 'android-code-coverage': { | |
| 'isolated_scripts': [{'name': 'chrome_junit_tests', 'ci_only': True}] | |
| } | |
| } | |
| # Create the files on the fake filesystem relative to real_src_root | |
| targets_file = ( | |
| self.real_src_root | |
| / 'infra/config/generated/builders/try/android-code-coverage/targets/' | |
| 'chromium.coverage.json' | |
| ) | |
| self.fs.create_file(targets_file, contents=json.dumps(targets_json)) | |
| gn_args_file = ( | |
| self.real_src_root | |
| / 'infra/config/generated/builders/try/android-code-coverage/' | |
| 'gn-args.json' | |
| ) | |
| self.fs.create_file( | |
| gn_args_file, | |
| contents=json.dumps({'gn_args': {'use_clang_coverage': True}}), | |
| ) | |
| test_args = ['ci_only_validator.py', 'chrome_junit_tests'] | |
| with ( | |
| mock.patch('sys.argv', test_args), | |
| mock.patch('sys.stdout', new=io.StringIO()) as mock_stdout, | |
| ): | |
| ci_only_validator.main() | |
| output = mock_stdout.getvalue() | |
| self.assertIn("Checking test suite 'chrome_junit_tests'", output) | |
| self.assertIn('android-code-coverage', output) | |
| self.assertIn('ci_only = True', output) | |
| self.assertIn('Coverage: Enabled', output) | |
| @mock.patch('ci_only_validator.discover_trybots_for_test_suite') | |
| def test_main_success_git(self, mock_discover): | |
| mock_discover.return_value = [ | |
| ci_only_validator.TrybotEntry( | |
| 'android-code-coverage', 'chromium.coverage' | |
| ) | |
| ] | |
| targets_json = { | |
| 'android-code-coverage': { | |
| 'isolated_scripts': [{'name': 'chrome_junit_tests', 'ci_only': False}] | |
| } | |
| } | |
| # Setup mock subprocess runs for git show | |
| def mock_run_side_effect(args, **kwargs): | |
| # git rev-parse --verify myrev | |
| if 'rev-parse' in args: | |
| return mock.MagicMock(returncode=0) | |
| # git show myrev:path/to/targets/chromium.coverage.json | |
| if 'show' in args: | |
| rel_path = args[2].split(':', 1)[1] | |
| if 'targets' in rel_path: | |
| return mock.MagicMock(returncode=0, stdout=json.dumps(targets_json)) | |
| if 'gn-args.json' in rel_path: | |
| return mock.MagicMock( | |
| returncode=0, | |
| stdout=json.dumps({'gn_args': {'use_jacoco_coverage': True}}), | |
| ) | |
| return mock.MagicMock(returncode=1) | |
| self.mock_run.side_effect = mock_run_side_effect | |
| test_args = [ | |
| 'ci_only_validator.py', | |
| 'chrome_junit_tests', | |
| '--revision', | |
| 'myrev', | |
| ] | |
| with ( | |
| mock.patch('sys.argv', test_args), | |
| mock.patch('sys.stdout', new=io.StringIO()) as mock_stdout, | |
| ): | |
| ci_only_validator.main() | |
| output = mock_stdout.getvalue() | |
| self.assertIn( | |
| "Checking test suite 'chrome_junit_tests' across 1 configured" | |
| ' trybots at revision myrev...', | |
| output, | |
| ) | |
| self.assertIn('android-code-coverage', output) | |
| self.assertIn('ci_only = False', output) | |
| self.assertIn('Coverage: Enabled', output) | |
| def test_main_src_root_not_exists(self): | |
| test_args = ['ci_only_validator.py', 'chrome_junit_tests'] | |
| with ( | |
| mock.patch('pathlib.Path.exists', return_value=False), | |
| mock.patch('sys.argv', test_args), | |
| mock.patch('sys.stderr', new=io.StringIO()) as mock_stderr, | |
| ): | |
| with self.assertRaises(SystemExit) as cm: | |
| ci_only_validator.main() | |
| self.assertEqual(cm.exception.code, 1) | |
| self.assertIn('Error: Src path', mock_stderr.getvalue()) | |
| def test_main_invalid_revision(self): | |
| self.mock_run.return_value = mock.MagicMock(returncode=1) | |
| test_args = [ | |
| 'ci_only_validator.py', | |
| 'chrome_junit_tests', | |
| '--revision', | |
| 'invalidrev', | |
| ] | |
| with ( | |
| mock.patch('sys.argv', test_args), | |
| mock.patch('sys.stderr', new=io.StringIO()) as mock_stderr, | |
| ): | |
| with self.assertRaises(SystemExit) as cm: | |
| ci_only_validator.main() | |
| self.assertEqual(cm.exception.code, 1) | |
| self.assertIn('Error: Invalid revision', mock_stderr.getvalue()) | |
| @mock.patch('ci_only_validator.discover_trybots_for_test_suite') | |
| def test_main_no_trybots(self, mock_discover): | |
| mock_discover.return_value = [] | |
| test_args = ['ci_only_validator.py', 'chrome_junit_tests'] | |
| with ( | |
| mock.patch('sys.argv', test_args), | |
| mock.patch('sys.stderr', new=io.StringIO()) as mock_stderr, | |
| ): | |
| with self.assertRaises(SystemExit) as cm: | |
| ci_only_validator.main() | |
| self.assertEqual(cm.exception.code, 0) | |
| self.assertIn("Warning: Test suite", mock_stderr.getvalue()) | |
| class TestCIOnlyValidatorExecutable(unittest.TestCase): | |
| def test_main_executable_run(self): | |
| import runpy | |
| script_path = pathlib.Path(ci_only_validator.__file__).resolve() | |
| test_args = ['ci_only_validator.py', '--help'] | |
| with ( | |
| mock.patch('sys.argv', test_args), | |
| mock.patch('sys.stdout', new=io.StringIO()) as mock_stdout, | |
| ): | |
| try: | |
| runpy.run_path(str(script_path), run_name='__main__') | |
| except SystemExit as e: | |
| self.assertEqual(e.code, 0) | |
| self.assertIn( | |
| 'Check if a test suite is barred by ci_only', mock_stdout.getvalue() | |
| ) | |
| if __name__ == '__main__': | |
| unittest.main() |