blob: 212d4f0a1b377ecfb8a1f31b825feb57b005caf7 [file]
#!/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()