blob: fb386bfd71e9314d36eb0f132ddb0565b80daf59 [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.
"""Unit tests for fetch_coverage_metrics.py."""
import base64
import contextlib
import io
import json
import sys
import textwrap
import unittest
from unittest import mock
import requests
from pyfakefs import fake_filesystem_unittest
import fetch_coverage_metrics as fcm
class FetchCoverageMetricsTest(fake_filesystem_unittest.TestCase):
"""Tests percentage calculation, network helpers, and main CLI workflow."""
def setUp(self) -> None:
"""Sets up virtual filesystem and network mocks for test cases."""
self.setUpPyfakefs()
self.get_patcher = mock.patch('requests.get')
self.mock_get = self.get_patcher.start()
self.addCleanup(self.get_patcher.stop)
self.make_patcher = mock.patch('fetch_coverage_metrics.make_request')
self.mock_make = self.make_patcher.start()
self.addCleanup(self.make_patcher.stop)
self.lines_patcher = mock.patch(
'fetch_coverage_metrics.fetch_gerrit_file_lines')
self.mock_lines = self.lines_patcher.start()
self.addCleanup(self.lines_patcher.stop)
def test_parse_percentage_dict(self) -> None:
"""Verifies ratio dict percentage calculation."""
val = {'covered': 4, 'total': 5}
self.assertEqual(fcm.parse_percentage(val), 80.0)
def test_parse_percentage_dict_zero_total(self) -> None:
"""Verifies zero total ratio dict defaults to 100%."""
val = {'covered': 0, 'total': 0}
self.assertEqual(fcm.parse_percentage(val), 100.0)
def test_parse_percentage_scalar(self) -> None:
"""Verifies numeric scalar percentage parsing."""
self.assertEqual(fcm.parse_percentage(65.5), 65.5)
self.assertEqual(fcm.parse_percentage('42.0'), 42.0)
def test_parse_percentage_invalid(self) -> None:
"""Verifies invalid values return None."""
self.assertIsNone(fcm.parse_percentage(None))
self.assertIsNone(fcm.parse_percentage('not-a-num'))
def test_make_request_success(self) -> None:
"""Verifies make_request sends Authorization header."""
self.make_patcher.stop()
mock_resp = mock.MagicMock()
mock_resp.content = b'{"data": {}}'
self.mock_get.return_value = mock_resp
res = fcm.make_request('https://example.com/api', 'my-token')
self.assertEqual(res, b'{"data": {}}')
self.mock_get.assert_called_once_with(
'https://example.com/api',
headers={'Authorization': 'Bearer my-token'},
timeout=30)
def test_make_request_http_error(self) -> None:
"""Verifies RequestException is caught and logged."""
self.make_patcher.stop()
self.mock_get.side_effect = requests.RequestException('404 not found')
buf = io.StringIO()
with contextlib.redirect_stderr(buf):
res = fcm.make_request('https://example.com/api')
self.assertIsNone(res)
self.assertIn('Error fetching', buf.getvalue())
def test_fetch_gerrit_file_lines_success(self) -> None:
"""Verifies base64 decoding of Gerrit file content."""
self.lines_patcher.stop()
raw_b64 = base64.b64encode(b'line 1\nline 2')
self.mock_make.return_value = raw_b64
lines = fcm.fetch_gerrit_file_lines('host', 1, 1, 'path/foo.cc', 'tok')
self.assertEqual(lines, {'1': 'line 1', '2': 'line 2'})
self.mock_make.assert_called_once()
def test_fetch_gerrit_file_lines_no_data(self) -> None:
"""Verifies missing request data returns empty dict."""
self.lines_patcher.stop()
self.mock_make.return_value = None
lines = fcm.fetch_gerrit_file_lines('host', 1, 1, 'path/foo.cc')
self.assertEqual(lines, {})
def test_fetch_gerrit_file_lines_decode_error(self) -> None:
"""Verifies base64 decoding errors return empty dict and log."""
self.lines_patcher.stop()
self.mock_make.return_value = b'!!!not_base64!!!'
buf = io.StringIO()
with contextlib.redirect_stderr(buf):
lines = fcm.fetch_gerrit_file_lines('host', 1, 1, 'path/foo.cc')
self.assertEqual(lines, {})
self.assertIn('Error decoding file', buf.getvalue())
def test_main_success_stdout(self) -> None:
"""Verifies full CLI workflow printing JSON to stdout."""
pct_json = {
'data': {
'files': [
{
'path': 'partial_cov.cc',
'absolute_coverage': {
'covered': 85,
'total': 100
},
},
{
'path': 'high_cov.cc',
'absolute_coverage': {
'covered': 10,
'total': 10
},
},
]
}
}
lines_json = {
'data': {
'files': [{
'path':
'partial_cov.cc',
'lines': [
{
'line': 10,
'count': 0
},
{
'line': 20,
'count': 5
},
],
}]
}
}
self.mock_make.side_effect = [
json.dumps(pct_json).encode('utf-8'),
json.dumps(lines_json).encode('utf-8'),
]
self.mock_lines.return_value = {'10': 'uncovered();', '20': 'cov();'}
test_args = [
'fcm.py', '--host', 'h', '--project', 'p', '--change', '123',
'--patchset', '1'
]
out_buf = io.StringIO()
with mock.patch.object(sys, 'argv', test_args):
with contextlib.redirect_stdout(out_buf):
fcm.main()
expected_json = textwrap.dedent("""\
{
"partial_cov.cc": {
"metrics": {
"absolute_coverage": 85.0
},
"low_coverage_type": [
"absolute_coverage"
],
"uncovered_lines": {
"10": "uncovered();"
}
},
"high_cov.cc": {
"metrics": {
"absolute_coverage": 100.0
},
"low_coverage_type": [],
"uncovered_lines": {}
}
}""")
self.assertEqual(out_buf.getvalue().strip(), expected_json)
def test_main_success_file_output(self) -> None:
"""Verifies CLI workflow saving report to disk via pathlib."""
pct_json = {'data': {'files': {'foo.java': {'absolute_coverage': 10.0}}}}
lines_json = {'data': {'files': {'foo.java': {'lines': []}}}}
self.mock_make.side_effect = [
json.dumps(pct_json).encode('utf-8'),
json.dumps(lines_json).encode('utf-8'),
]
self.mock_lines.return_value = {}
out_path = '/mock/out/report.json'
self.fs.create_dir('/mock/out')
test_args = [
'fcm.py', '--host', 'h', '--project', 'p', '--change', '1',
'--patchset', '1', '--output', out_path
]
with mock.patch.object(sys, 'argv', test_args):
fcm.main()
saved = self.fs.get_object(out_path).contents
self.assertIn('"foo.java"', saved)
def test_main_files_filtering(self) -> None:
"""Verifies CLI --files argument filters out non-matching files."""
pct_json = {
'data': {
'files': {
'foo.java': {'absolute_coverage': 10.0},
'bar.cc': {'absolute_coverage': 20.0},
}
}
}
lines_json = {'data': {'files': {'foo.java': {'lines': []}}}}
self.mock_make.side_effect = [
json.dumps(pct_json).encode('utf-8'),
json.dumps(lines_json).encode('utf-8'),
]
self.mock_lines.return_value = {}
test_args = [
'fcm.py', '--host', 'h', '--project', 'p', '--change', '1',
'--patchset', '1', '--files', 'foo.java'
]
out_buf = io.StringIO()
with mock.patch.object(sys, 'argv', test_args):
with contextlib.redirect_stdout(out_buf):
fcm.main()
res = json.loads(out_buf.getvalue())
self.assertIn('foo.java', res)
self.assertNotIn('bar.cc', res)
def test_main_percentages_fetch_failure(self) -> None:
"""Verifies exit(1) when percentages fetch returns None."""
self.mock_make.return_value = None
test_args = [
'fcm.py', '--host', 'h', '--project', 'p', '--change', '1',
'--patchset', '1'
]
err_buf = io.StringIO()
with mock.patch.object(sys, 'argv', test_args):
with contextlib.redirect_stderr(err_buf):
with self.assertRaises(SystemExit) as cm:
fcm.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn('Failed to fetch', err_buf.getvalue())
def test_main_percentages_json_failure(self) -> None:
"""Verifies exit(1) when percentages JSON parsing fails."""
self.mock_make.return_value = b'not_json'
test_args = [
'fcm.py', '--host', 'h', '--project', 'p', '--change', '1',
'--patchset', '1'
]
err_buf = io.StringIO()
with mock.patch.object(sys, 'argv', test_args):
with contextlib.redirect_stderr(err_buf):
with self.assertRaises(SystemExit) as cm:
fcm.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn('Failed to parse', err_buf.getvalue())
def test_main_lines_query_error(self) -> None:
"""Verifies lines query exception logs to stderr and continues."""
pct_json = {'data': {'files': {'bar.cc': {'absolute_coverage': 0.0}}}}
self.mock_make.side_effect = [
json.dumps(pct_json).encode('utf-8'),
b'bad_lines_json',
]
test_args = [
'fcm.py', '--host', 'h', '--project', 'p', '--change', '1',
'--patchset', '1'
]
err_buf = io.StringIO()
out_buf = io.StringIO()
with mock.patch.object(sys, 'argv', test_args):
with contextlib.redirect_stderr(err_buf):
with contextlib.redirect_stdout(out_buf):
fcm.main()
self.assertIn('Error processing lines', err_buf.getvalue())
self.assertIn('"uncovered_lines": {}', out_buf.getvalue())
if __name__ == '__main__':
unittest.main()