blob: e4e77a197be491b85e25776a37475320bcda363f [file]
#!/usr/bin/env python3
# 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.
"""Unit tests for inspect_coverage_build.py."""
# pylint: disable=line-too-long
import contextlib
import io
import os
from pathlib import Path
import tempfile
import unittest
from unittest import mock
import sys
import inspect_coverage_build
class InspectCoverageBuildTest(unittest.TestCase):
"""Tests build ID parsing, status verification, config audit, and pipeline check."""
def setUp(self) -> None:
"""Sets up test fixtures before each test method."""
inspect_coverage_build._ALL_JSON_GZ_CACHE.clear()
self.addCleanup(inspect_coverage_build._ALL_JSON_GZ_CACHE.clear)
self.run_patcher = mock.patch('subprocess.run')
self.mock_run = self.run_patcher.start()
self.addCleanup(self.run_patcher.stop)
def test_parse_build_id_numeric_id(self) -> None:
"""Verifies numeric build ID strings are returned as-is."""
self.assertEqual(
inspect_coverage_build.parse_build_id('8676997864980431489'),
'8676997864980431489',
)
def test_parse_build_id_ci_url(self) -> None:
"""Verifies numeric ID extraction from ci.chromium.org build URLs."""
url = 'https://ci.chromium.org/ui/p/chromium/builders/try/linux-rel/2773124/infra'
self.assertEqual(
inspect_coverage_build.parse_build_id(url),
'chromium/try/linux-rel/2773124',
)
def test_parse_build_id_invalid_raises(self) -> None:
"""Verifies malformed strings raise ValueError instead of passing through."""
with self.assertRaises(ValueError):
inspect_coverage_build.parse_build_id('not-a-build-id-or-url')
def test_check_build_status_terminal(self) -> None:
"""Verifies terminal SUCCESS status contains terminal flag."""
build_data = {'id': '12345', 'status': 'SUCCESS'}
result = inspect_coverage_build.check_build_status(build_data)
self.assertTrue(result['terminal'])
self.assertEqual(result['status'], 'SUCCESS')
def test_check_build_status_running(self) -> None:
"""Verifies non-terminal STARTED status returns terminal=False."""
build_data = {'id': '12345', 'status': 'STARTED'}
result = inspect_coverage_build.check_build_status(build_data)
self.assertFalse(result['terminal'])
self.assertEqual(result['status'], 'STARTED')
def test_verify_configuration_valid(self) -> None:
"""Verifies configuration audit reports valid when gclient flags are set."""
build_data = {
'input': {'properties': {'checkout_clang_coverage_tools': True}},
'steps': [{'name': 'lookup GN args', 'status': 'SUCCESS'}],
}
result = inspect_coverage_build.verify_configuration(build_data, 'cpp')
self.assertTrue(result['configuration_valid'])
self.assertEqual(result['gclient_config_mismatches'], [])
def test_verify_configuration_missing_clang_cov(self) -> None:
"""Verifies config audit flags missing checkout_clang_coverage_tools property."""
build_data = {
'input': {'properties': {}},
'steps': [{'name': 'lookup GN args', 'status': 'FAILURE'}],
}
result = inspect_coverage_build.verify_configuration(build_data, 'cpp')
self.assertFalse(result['configuration_valid'])
self.assertIn(
'Missing checkout_clang_coverage_tools in gclient config.',
result['gclient_config_mismatches'],
)
def test_verify_configuration_all_languages(self) -> None:
"""Verifies all supported languages can be checked against config map."""
build_data = {
'input': {'properties': {}},
'steps': [{'name': 'lookup GN args', 'status': 'SUCCESS'}],
}
for lang in ['cpp', 'objc', 'rust', 'java', 'js', 'ts']:
res = inspect_coverage_build.verify_configuration(build_data, lang)
self.assertIn('configuration_valid', res)
def test_verify_configuration_unsupported_language_raises(self) -> None:
"""Verifies unmapped language strings raise ValueError."""
build_data = {'input': {'properties': {}}, 'steps': []}
with self.assertRaises(ValueError):
inspect_coverage_build.verify_configuration(
build_data, 'unsupported-lang'
)
def test_verify_coverage_pipeline_success(self) -> None:
"""Verifies Phase 2 pipeline audit reports success when HTML upload succeeds."""
build_data = {
'steps': [
{
'name': 'process clang code coverage data for unit test coverage',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for unit test coverage|generate html report in 42 tests',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for unit test coverage|gsutil Upload coverage artifacts',
'status': 'SUCCESS',
},
],
'output': {
'properties': {
'gsutil_urls': {
'process clang code coverage data for unit test coverage|gsutil Upload coverage artifacts': 'gs://bucket/report'
}
}
},
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
unit_res = result['pipelines_checked']['cpp_unit']
self.assertTrue(unit_res['pipeline_success'])
self.assertEqual(unit_res['tests_processed_count'], 42)
self.assertFalse(unit_res['skipped_no_data'])
def test_verify_coverage_pipeline_skipped_no_data(self) -> None:
"""Verifies pipeline audit flags skipped_no_data and records substeps correctly."""
build_data = {
'steps': [
{
'name': 'process clang code coverage data for unit test coverage',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for unit test coverage|merge all profile files into a single .profdata',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for unit test coverage|skip processing because no data is found',
'status': 'SUCCESS',
},
],
'output': {'properties': {}},
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
unit_res = result['pipelines_checked']['cpp_unit']
self.assertFalse(unit_res['pipeline_success'])
self.assertTrue(unit_res['skipped_no_data'])
self.assertEqual(len(unit_res['child_steps']), 2)
def test_run_bb_get_success(self) -> None:
"""Verifies run_bb_get returns parsed JSON dictionary when bb succeeds."""
self.mock_run.return_value = mock.MagicMock(
returncode=0, stdout='{"id": "123", "status": "SUCCESS"}'
)
data = inspect_coverage_build.run_bb_get('123')
self.assertEqual(data['id'], '123')
self.assertEqual(data['status'], 'SUCCESS')
def test_run_bb_get_failure_raises(self) -> None:
"""Verifies run_bb_get raises RuntimeError when bb returns non-zero code."""
self.mock_run.return_value = mock.MagicMock(
returncode=1, stderr='not found'
)
with self.assertRaises(RuntimeError):
inspect_coverage_build.run_bb_get('999')
def test_verify_configuration_java(self) -> None:
"""Verifies configuration audit for Java language context."""
build_data = {
'input': {'properties': {}},
'steps': [{'name': 'lookup GN args', 'status': 'SUCCESS'}],
}
result = inspect_coverage_build.verify_configuration(build_data, 'java')
self.assertTrue(result['configuration_valid'])
def test_verify_configuration_gn_arg_mismatches(self) -> None:
"""Verifies gn_arg_mismatches are detected when input gn_args conflict with required_gn_args."""
build_data = {
'input': {
'properties': {
'gn_args': {
'use_clang_coverage': False,
'is_clang': True,
'is_debug': False,
}
}
},
'steps': [],
}
result = inspect_coverage_build.verify_configuration(build_data, 'cpp')
self.assertFalse(result['configuration_valid'])
self.assertEqual(len(result['gn_arg_mismatches']), 1)
self.assertIn(
'use_clang_coverage = False (expected True)',
result['gn_arg_mismatches'][0],
)
def test_verify_configuration_in_props_gn_arg_mismatch(self) -> None:
"""Verifies boolean input properties are checked for required GN args when gn_args dict is omitted."""
build_data = {
'input': {
'properties': {
'use_clang_coverage': False,
}
},
'steps': [],
}
result = inspect_coverage_build.verify_configuration(build_data, 'cpp')
self.assertFalse(result['configuration_valid'])
self.assertIn(
'use_clang_coverage = False (expected True)',
result['gn_arg_mismatches'][0],
)
def test_verify_configuration_orchestrator_compilator_lookup_gn_args(
self,
) -> None:
"""Verifies orchestrator builds extract lookup GN args status from compilator child steps."""
build_data = {
'input': {
'properties': {
'recipe': 'chromium_orchestrator',
}
},
'steps': [
{
'name': 'compilator steps (with patch)|lookup GN args',
'status': 'SUCCESS',
}
],
}
result = inspect_coverage_build.verify_configuration(build_data, 'cpp')
self.assertEqual(result['lookup_gn_args_step_status'], 'SUCCESS')
def test_verify_configuration_orchestrator_nested_substeps_lookup_gn_args(
self,
) -> None:
"""Verifies lookup GN args status is found when nested inside substeps/children arrays."""
build_data = {
'input': {
'properties': {
'recipe': 'chromium_orchestrator',
}
},
'steps': [
{
'name': 'compilator steps (with patch)',
'status': 'SUCCESS',
'substeps': [
{
'name': 'lookup GN args',
'status': 'SUCCESS',
}
],
}
],
}
result = inspect_coverage_build.verify_configuration(build_data, 'cpp')
self.assertEqual(result['lookup_gn_args_step_status'], 'SUCCESS')
def test_verify_coverage_pipeline_url_conversions(self) -> None:
"""Verifies URL helper conversion of gs:// paths to https:// and pantheon links."""
build_data = {
'steps': [
{
'name': 'process clang code coverage data for overall test coverage',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for overall test coverage|generate html report in 10 tests',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for overall test coverage|gsutil upload html report',
'status': 'SUCCESS',
},
],
'output': {
'properties': {
'gsutil_urls': {
'process clang code coverage data for overall test coverage': 'gs://bucket/meta',
'process clang code coverage data for overall test coverage|gsutil upload html report': 'gs://bucket/html',
'process clang code coverage data for overall test coverage|gsutil upload artifact to GS': 'gs://bucket/profdata',
}
}
},
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
overall = result['pipelines_checked']['cpp_overall']
self.assertEqual(overall['merged_profdata_gcs_url'], 'gs://bucket/profdata')
def test_verify_coverage_pipeline_detects_recipe_messages(self) -> None:
"""Verifies official recipe skip/error messages and triage actions are detected."""
build_data = {
'steps': [
{
'name': 'skip processing clang coverage data because no profile data collected',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for overall test coverage|skip processing because no data is found',
'status': 'SUCCESS',
},
]
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
overall = result['pipelines_checked']['cpp_overall']
messages = [
m['detected_condition'] for m in overall['recipe_error_messages_detected']
]
self.assertIn(
'skip processing clang coverage data because no profile data collected',
messages,
)
self.assertIn('skip processing because no data is found', messages)
def test_format_line_ranges(self) -> None:
"""Verifies format_line_ranges compresses contiguous line numbers correctly."""
self.assertEqual(
inspect_coverage_build.format_line_ranges([1, 2, 3, 5, 8, 9]),
'1-3, 5, 8-9',
)
self.assertEqual(inspect_coverage_build.format_line_ranges([]), 'None')
def test_extract_coverage_artifacts_for_files(self) -> None:
"""Verifies target file coverage metrics are extracted across pipelines."""
pipeline_report = {
'pipelines_checked': {
'cpp_unit': {
'pipeline_success': True,
'html_report_gcs_url': 'gs://bucket/report',
}
}
}
file_res = inspect_coverage_build.extract_coverage_artifacts_for_files(
pipeline_report, target_files=['chrome/browser/ui/foo.cc']
)
self.assertIn('chrome/browser/ui/foo.cc', file_res)
unit_info = file_res['chrome/browser/ui/foo.cc']['cpp_unit']
self.assertFalse(unit_info['available'])
def test_format_file_coverage_report(self) -> None:
"""Verifies readable text formatting of target file coverage breakdown."""
file_data = {
'foo.cc': {
'cpp_unit': {
'available': True,
'line_coverage': '80.0% (80/100)',
'function_coverage': '100.0% (5/5)',
'region_coverage': '75.0%',
'branch_coverage': '70.0%',
'uncovered_lines': [10, 11, 12, 25],
}
}
}
text = inspect_coverage_build.format_file_coverage_report(file_data)
self.assertIn('[Target File Coverage Extraction]', text)
self.assertIn('* File: foo.cc', text)
self.assertIn('Line Coverage: 80.0% (80/100)', text)
self.assertIn('Uncovered Lines (4): 10-12, 25', text)
def test_verify_configuration_orchestrator_delegated(self) -> None:
"""Verifies orchestrator builds delegate GN args lookup to compilator."""
build_data = {
'input': {'properties': {'recipe': 'chromium_orchestrator'}},
'steps': [],
}
result = inspect_coverage_build.verify_configuration(build_data, 'cpp')
self.assertEqual(
result['lookup_gn_args_step_status'],
inspect_coverage_build.StepStatus.DELEGATED_TO_COMPILATOR,
)
def test_verify_coverage_pipeline_all_recipe_error_flags(self) -> None:
"""Verifies detection of invalid profraw, failure flags, and skip profdata."""
build_data = {
'steps': [
{
'name': 'process clang code coverage data for overall test coverage|foo',
'status': 'FAILURE',
'output': {'properties': {'process_coverage_data_failure': True}},
},
{
'name': 'process clang code coverage data for overall test coverage|Found invalid profraw files',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for overall test coverage|skip processing because no profdata was generated',
'status': 'SUCCESS',
},
]
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
overall = result['pipelines_checked']['cpp_overall']
messages = [
m['detected_condition'] for m in overall['recipe_error_messages_detected']
]
self.assertIn('process_coverage_data_failure = True', messages)
self.assertIn('Found invalid profraw files', messages)
self.assertIn('skip processing because no profdata was generated', messages)
def test_extract_coverage_artifacts_for_files_unmocked(self) -> None:
"""Verifies real file report extraction runs fetch_and_parse or not available."""
pipeline_report = {
'pipelines_checked': {
'cpp_unit': {
'pipeline_success': False,
'html_report_gcs_url': 'gs://bucket/report',
}
}
}
file_res = inspect_coverage_build.extract_coverage_artifacts_for_files(
pipeline_report, target_files=['foo.cc']
)
self.assertFalse(file_res['foo.cc']['cpp_unit']['available'])
def test_format_file_coverage_report_not_available(self) -> None:
"""Verifies format_file_coverage_report handles unavailable pipelines."""
file_data = {'foo.cc': {'cpp_unit': {'available': False}}}
text = inspect_coverage_build.format_file_coverage_report(file_data)
self.assertIn("Pipeline 'cpp_unit': Not Available", text)
def test_format_inspection_report_comprehensive(self) -> None:
"""Verifies format_inspection_report outputs all optional sections and mismatches."""
combined_report = {
'status_check': {
'build_id': '12345',
'status': 'STARTED',
'terminal': False,
},
'config_verification': {
'configuration_valid': False,
'lookup_gn_args_step_status': 'FAILURE',
'gn_arg_mismatches': ['use_clang_coverage missing'],
'gclient_config_mismatches': ['checkout_clang_coverage_tools missing'],
},
'coverage_pipeline_verification': {
'all_pipelines_successful': False,
'pipelines_checked': {
'cpp_overall': {
'parent_step_status': 'FAILURE',
'html_report_generation_status': 'FAILURE',
'artifacts_upload_status': 'NOT_FOUND',
'tests_processed_count': 10,
'skipped_no_data': True,
'recipe_error_messages_detected': [
{'detected_condition': 'foo', 'step_name': 'bar'}
],
'child_steps': [{'name': 'substep', 'status': 'FAILURE'}],
'html_report_url': 'https://storage.cloud.google.com/foo/index.html',
'merged_profdata_gcs_url': 'gs://foo/merged.profdata',
'pipeline_success': False,
}
},
},
'target_file_coverage': {
'foo.cc': {
'cpp_overall': {
'available': True,
'line_coverage': '50%',
'function_coverage': '50%',
'region_coverage': '50%',
'branch_coverage': '50%',
'uncovered_lines': [1],
}
}
},
}
text = inspect_coverage_build.format_inspection_report(
combined_report, 'cpp'
)
self.assertIn('Build status is non-terminal (STARTED)', text)
self.assertIn('use_clang_coverage missing', text)
self.assertIn('checkout_clang_coverage_tools missing', text)
self.assertIn("DETECTED RECIPE CONDITION: 'foo'", text)
self.assertIn("SKIPPED: 'skip processing because no data is found'", text)
self.assertIn('Substep breakdown:', text)
self.assertIn('Merged Profdata GCS: gs://foo/merged.profdata', text)
def test_verify_coverage_pipeline_java_and_js(self) -> None:
"""Verifies Java and JS coverage pipelines and metadata generation steps."""
build_data = {
'steps': [
{
'name': 'process java coverage (overall)',
'status': 'SUCCESS',
},
{
'name': 'process java coverage (overall)|Generate Java coverage metadata',
'status': 'SUCCESS',
},
{
'name': 'process java coverage (overall)|gsutil Upload coverage artifacts (2)',
'status': 'SUCCESS',
},
{
'name': 'process javascript coverage (overall)',
'status': 'SUCCESS',
},
{
'name': 'process javascript coverage (overall)|Generate JavaScript coverage metadata',
'status': 'SUCCESS',
},
{
'name': 'process javascript coverage (overall)|gsutil Upload coverage artifacts',
'status': 'SUCCESS',
},
],
'output': {
'properties': {
'gsutil_urls': {
'process java coverage (overall)|gsutil Upload coverage artifacts (2)': 'gs://bucket/java_meta',
'process javascript coverage (overall)|gsutil Upload coverage artifacts': 'gs://bucket/js_meta',
}
}
},
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
java_res = result['pipelines_checked']['java_overall']
self.assertTrue(java_res['pipeline_success'])
self.assertEqual(
java_res['all_json_gz_url'], 'gs://bucket/java_meta/all.json.gz'
)
def test_verify_coverage_pipeline_upload_step_fallback(self) -> None:
"""Verifies fallback between upload step (2) and without (2) when checking status and GCS URL."""
build_data = {
'steps': [
{
'name': 'process clang code coverage data for overall test coverage',
'status': 'SUCCESS',
},
{
'name': 'process clang code coverage data for overall test coverage|generate html report in 5 tests',
'status': 'SUCCESS',
},
{
'name': 'gsutil Upload coverage artifacts',
'status': 'SUCCESS',
},
],
'output': {
'properties': {
'gsutil_urls': {
'gsutil Upload coverage artifacts': 'gs://bucket/fallback',
}
}
},
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
overall = result['pipelines_checked']['cpp_overall']
self.assertTrue(overall['pipeline_success'])
self.assertEqual(
overall['all_json_gz_url'], 'gs://bucket/fallback/all.json.gz'
)
@mock.patch('inspect_coverage_build.fetch_and_parse_json_file_coverage')
def test_extract_coverage_artifacts_for_files_json_gz(
self, mock_parse_json: mock.MagicMock
) -> None:
"""Verifies extract_coverage_artifacts_for_files uses all_json_gz_url when present."""
mock_parse_json.return_value = {
'available': True,
'line_coverage': '90.0% (90/100)',
}
pipeline_report = {
'pipelines_checked': {
'java_overall': {
'pipeline_success': True,
'all_json_gz_url': 'gs://bucket/all.json.gz',
'html_report_gcs_url': 'gs://bucket/html_report/index.html',
}
}
}
file_res = inspect_coverage_build.extract_coverage_artifacts_for_files(
pipeline_report, target_files=['foo/bar.java']
)
self.assertTrue(file_res['foo/bar.java']['java_overall']['available'])
self.assertEqual(
file_res['foo/bar.java']['java_overall']['line_coverage'],
'90.0% (90/100)',
)
mock_parse_json.assert_called_once_with(
'gs://bucket/all.json.gz', 'foo/bar.java'
)
@mock.patch('subprocess.run')
def test_fetch_and_parse_json_file_coverage_dict_summaries(
self, mock_run: mock.MagicMock
) -> None:
"""Verifies fetch_and_parse_json_file_coverage parses dict summary format and uncovered lines."""
import gzip
import json
data = {
'files': [
{
'path': 'foo/bar.java',
'lines': [
{'first': 10, 'last': 12, 'count': 0},
{'first': 13, 'last': 13, 'count': 5},
],
'summaries': {
'line': {'covered': 8, 'count': 10},
'method': {'covered': 2, 'total': 2},
},
}
]
}
with tempfile.NamedTemporaryFile(suffix='.json.gz', delete=False) as tf:
tf.write(gzip.compress(json.dumps(data).encode('utf-8')))
src_path = tf.name
try:
def fake_cp(*args, **kwargs):
dest = args[0][4]
with open(src_path, 'rb') as f_in, open(dest, 'wb') as f_out:
f_out.write(f_in.read())
return mock.MagicMock(returncode=0)
mock_run.side_effect = fake_cp
res = inspect_coverage_build.fetch_and_parse_json_file_coverage(
'gs://bucket/all.json.gz', 'bar.java'
)
self.assertTrue(res['available'])
self.assertEqual(res['line_coverage'], '8/10 (80.00%)')
self.assertEqual(res['function_coverage'], '2/2 (100.00%)')
self.assertEqual(res['uncovered_lines'], [10, 11, 12])
finally:
if os.path.exists(src_path):
os.remove(src_path)
@mock.patch('subprocess.run')
def test_fetch_and_parse_json_file_coverage_list_summaries_or_absent(
self, mock_run: mock.MagicMock
) -> None:
"""Verifies list-based summaries and missing target handling in fetch_and_parse_json_file_coverage."""
import gzip
import json
data = {
'files': [
{
'filename': 'foo/other.js',
'summaries': [{'name': 'line', 'covered': 15, 'count': 20}],
}
]
}
with tempfile.NamedTemporaryFile(suffix='.json.gz', delete=False) as tf:
tf.write(gzip.compress(json.dumps(data).encode('utf-8')))
src_path = tf.name
try:
def fake_cp(*args, **kwargs):
dest = args[0][4]
with open(src_path, 'rb') as f_in, open(dest, 'wb') as f_out:
f_out.write(f_in.read())
return mock.MagicMock(returncode=0)
mock_run.side_effect = fake_cp
res = inspect_coverage_build.fetch_and_parse_json_file_coverage(
'gs://bucket/all.json.gz', 'other.js'
)
self.assertTrue(res['available'])
self.assertEqual(res['line_coverage'], '15/20 (75.00%)')
res_absent = inspect_coverage_build.fetch_and_parse_json_file_coverage(
'gs://bucket/all.json.gz', 'missing.js'
)
self.assertFalse(res_absent['available'])
self.assertFalse(
inspect_coverage_build.fetch_and_parse_json_file_coverage(
'', 'other.js'
)['available']
)
finally:
if os.path.exists(src_path):
os.remove(src_path)
def test_verify_coverage_pipeline_upload_step_primary_match(self) -> None:
"""Verifies primary upload_step_name match when nested upload status is NOT_FOUND."""
build_data = {
'steps': [
{
'name': 'process clang code coverage data for overall test coverage',
'status': 'SUCCESS',
},
{
'name': 'gsutil Upload coverage artifacts (2)',
'status': 'SUCCESS',
},
],
'output': {'properties': {'gsutil_urls': {}}},
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
overall = result['pipelines_checked']['cpp_overall']
@mock.patch.dict(os.environ, {'INSPECT_COVERAGE_NO_NETWORK': ''})
@mock.patch('subprocess.run')
def test_verify_coverage_pipeline_gcs_fallback_when_url_missing(
self, mock_run: mock.MagicMock
) -> None:
"""Verifies gsutil ls fallback execution when all_json_gz_url is missing."""
mock_run.return_value = mock.MagicMock(
returncode=0, stdout='gs://bucket/fallback/all.json.gz\n'
)
build_data = {
'id': '123456789',
'steps': [
{
'name': 'process clang code coverage data for overall test coverage',
'status': 'SUCCESS',
},
],
'output': {'properties': {'gsutil_urls': {}}},
}
result = inspect_coverage_build.verify_coverage_pipeline(build_data)
overall = result['pipelines_checked']['cpp_overall']
self.assertEqual(
overall['all_json_gz_url'], 'gs://bucket/fallback/all.json.gz'
)
@mock.patch('subprocess.run')
def test_fetch_and_parse_json_file_coverage_uncompressed_fallback(
self, mock_run: mock.MagicMock
) -> None:
"""Verifies fallback to uncompressed json.loads when zlib and gzip decompression both fail."""
import json
data = {
'files': [
{
'path': 'foo/raw.js',
'summaries': [{'name': 'line', 'covered': 5, 'count': 5}],
}
]
}
with tempfile.NamedTemporaryFile(suffix='.json.gz', delete=False) as tf:
tf.write(json.dumps(data).encode('utf-8'))
src_path = tf.name
try:
def fake_cp(*args, **kwargs):
dest = args[0][4]
with open(src_path, 'rb') as f_in, open(dest, 'wb') as f_out:
f_out.write(f_in.read())
return mock.MagicMock(returncode=0)
mock_run.side_effect = fake_cp
res = inspect_coverage_build.fetch_and_parse_json_file_coverage(
'gs://bucket/all.json.gz', 'raw.js'
)
self.assertTrue(res['available'])
self.assertEqual(res['line_coverage'], '5/5 (100.00%)')
finally:
if os.path.exists(src_path):
os.remove(src_path)
@mock.patch('subprocess.run')
def test_fetch_and_parse_json_file_coverage_exceptions(
self, mock_run: mock.MagicMock
) -> None:
"""Verifies exception handling on corrupt JSON and os.remove failure inside fetch_and_parse."""
with tempfile.NamedTemporaryFile(suffix='.json.gz', delete=False) as tf:
tf.write(b'corrupt non-json bytes')
src_path = tf.name
try:
def fake_cp(*args, **kwargs):
dest = args[0][4]
with open(src_path, 'rb') as f_in, open(dest, 'wb') as f_out:
f_out.write(f_in.read())
return mock.MagicMock(returncode=0)
mock_run.side_effect = fake_cp
with mock.patch('os.remove', side_effect=OSError('Permission denied')):
res = inspect_coverage_build.fetch_and_parse_json_file_coverage(
'gs://bucket/all.json.gz', 'any.js'
)
self.assertFalse(res['available'])
finally:
if os.path.exists(src_path):
os.remove(src_path)
class InspectCoverageBuildMainTest(unittest.TestCase):
"""Tests CLI main() execution modes and error reporting."""
def setUp(self) -> None:
"""Initializes stdout, stderr, exit, and run_bb_get patchers for main()."""
self.stdout_buffer = io.StringIO()
self.stderr_buffer = io.StringIO()
self.stdout_context = contextlib.redirect_stdout(self.stdout_buffer)
self.stderr_context = contextlib.redirect_stderr(self.stderr_buffer)
self.stdout_context.__enter__()
self.stderr_context.__enter__()
self.addCleanup(self.stdout_context.__exit__, None, None, None)
self.addCleanup(self.stderr_context.__exit__, None, None, None)
self.exit_patcher = mock.patch('sys.exit', side_effect=SystemExit)
self.bb_patcher = mock.patch('inspect_coverage_build.run_bb_get')
self.run_patcher = mock.patch('subprocess.run')
self.mock_exit = self.exit_patcher.start()
self.mock_bb = self.bb_patcher.start()
self.mock_run = self.run_patcher.start()
self.addCleanup(self.exit_patcher.stop)
self.addCleanup(self.bb_patcher.stop)
self.addCleanup(self.run_patcher.stop)
def test_main_json_output(self) -> None:
"""Verifies main() formats and outputs JSON when --json flag is passed."""
self.mock_bb.return_value = {
'id': '8676997864980431489',
'status': 'SUCCESS',
'steps': [],
}
with mock.patch(
'sys.argv',
[
'inspect_coverage_build.py',
'--build',
'8676997864980431489',
'--language',
'cpp',
'--json',
],
):
inspect_coverage_build.main()
self.mock_bb.assert_called_once_with('8676997864980431489')
self.mock_exit.assert_not_called()
def test_main_text_output(self) -> None:
"""Verifies main() prints human-readable inspection summary by default."""
self.mock_bb.return_value = {
'id': '8676997864980431489',
'status': 'STARTED',
'steps': [
{'name': 'lookup GN args', 'status': 'SUCCESS'},
{
'name': 'process clang code coverage data for unit test coverage',
'status': 'SUCCESS',
},
],
}
with mock.patch(
'sys.argv',
[
'inspect_coverage_build.py',
'--build',
'8676997864980431489',
'--language',
'cpp',
],
):
inspect_coverage_build.main()
self.mock_bb.assert_called_once_with('8676997864980431489')
self.mock_exit.assert_not_called()
def test_main_with_files_argument(self) -> None:
"""Verifies main() extracts per-file coverage reports when --files is passed."""
self.mock_bb.return_value = {
'id': '8676997864980431489',
'status': 'SUCCESS',
'steps': [],
}
with mock.patch(
'sys.argv',
[
'inspect_coverage_build.py',
'--build',
'8676997864980431489',
'--language',
'cpp',
'--files',
'foo.cc',
],
):
inspect_coverage_build.main()
self.mock_bb.assert_called_once_with('8676997864980431489')
self.mock_exit.assert_not_called()
def test_main_exception_exits(self) -> None:
"""Verifies main() logs error and exits with status 1 on exceptions."""
self.mock_bb.side_effect = RuntimeError('API Error')
with mock.patch(
'sys.argv',
[
'inspect_coverage_build.py',
'--build',
'8676997864980431489',
'--language',
'cpp',
],
):
with self.assertRaises(SystemExit):
inspect_coverage_build.main()
self.mock_exit.assert_called_once_with(1)
if __name__ == '__main__':
unittest.main()