| #!/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.""" |
| |
| 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() |