You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
depot_tools/mcp/resultdb_test.py

259 lines
9.5 KiB
Python

# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Tests for resultdb tools."""
import json
import os
import pathlib
import subprocess
import sys
import unittest
from unittest import mock
sys.path.insert(
0,
os.path.abspath(
pathlib.Path(__file__).resolve().parent.parent.joinpath(
pathlib.Path('infra_lib'))))
import requests
import resultdb
class GetNonExoneratedUnexpectedResultsFromBuildTest(
unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.mock_context = mock.AsyncMock()
self.mock_context.info = mock.AsyncMock()
@mock.patch('subprocess.run')
async def test_get_non_exonerated_unexpected_results_from_build_success(
self, mock_subprocess_run):
build_id = '12345'
expected_output = '{"testResults": []}'
mock_subprocess_run.return_value = subprocess.CompletedProcess(
args=[],
returncode=0,
stdout=expected_output,
stderr='',
)
output = await (
resultdb.get_non_exonerated_unexpected_results_from_build(
self.mock_context,
build_id,
))
self.assertEqual(output, expected_output)
expected_command = [
'prpc',
'call',
'results.api.luci.app',
'luci.resultdb.v1.ResultDB.QueryTestResults',
]
expected_request = {
'invocations': [
'invocations/build-12345',
],
'predicate': {
'expectancy': 'VARIANTS_WITH_UNEXPECTED_RESULTS',
'exclude_exonerated': True,
},
'read_mask': ('name,resultId,variant,status,statusV2,duration,'
'failureReason,summary_html'),
}
mock_subprocess_run.assert_called_once_with(
expected_command,
capture_output=True,
input=json.dumps(expected_request),
check=True,
text=True,
)
@mock.patch('subprocess.run')
async def test_get_non_exonerated_unexpected_results_from_build_exception(
self, mock_subprocess_run):
build_id = '12345'
mock_subprocess_run.side_effect = Exception('PRPC call failed')
with self.assertRaisesRegex(Exception, 'PRPC call failed'):
await resultdb.get_non_exonerated_unexpected_results_from_build(
self.mock_context, build_id)
async def test_get_non_exonerated_unexpected_results_from_build_invalid_id(
self):
with self.assertRaisesRegex(
ValueError,
'Provided build_id b-12345 contains non-numeric characters'):
await resultdb.get_non_exonerated_unexpected_results_from_build(
self.mock_context, 'b-12345')
class ExpandSummaryHtmlTest(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.mock_context = mock.AsyncMock()
@mock.patch('resultdb.get_test_level_text_artifact',
new_callable=mock.AsyncMock)
async def test_expand_summary_html_success(self, mock_get_artifact):
result_name = 'invocations/build-123/tests/test_id/results/result_id'
summary_html = ('<p>Failure</p><text-artifact '
'artifact-id="artifact1"></text-artifact>')
artifact_content = 'Detailed failure log.'
mock_get_artifact.return_value = artifact_content
expanded_html = await resultdb.expand_summary_html(
self.mock_context, result_name, summary_html)
mock_get_artifact.assert_called_once_with(self.mock_context,
result_name, 'artifact1')
self.assertEqual(expanded_html, f'<p>Failure</p>{artifact_content}')
async def test_expand_summary_html_no_artifacts(self):
result_name = 'invocations/build-123/tests/test_id/results/result_id'
summary_html = '<p>Just a regular summary.</p>'
expanded_html = await resultdb.expand_summary_html(
self.mock_context, result_name, summary_html)
self.assertEqual(expanded_html, summary_html)
@mock.patch('resultdb.get_test_level_text_artifact',
new_callable=mock.AsyncMock)
async def test_expand_summary_html_missing_artifact_id(
self, mock_get_artifact):
result_name = 'invocations/build-123/tests/test_id/results/result_id'
summary_html = '<p>Failure</p><text-artifact></text-artifact>'
expanded_html = await resultdb.expand_summary_html(
self.mock_context, result_name, summary_html)
mock_get_artifact.assert_not_called()
self.assertEqual(expanded_html,
'<p>Failure</p><text-artifact></text-artifact>')
class GetTestLevelTextArtifactTest(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.mock_context = mock.AsyncMock()
self.mock_context.info = mock.AsyncMock()
@mock.patch('requests.get')
@mock.patch('subprocess.run')
async def test_get_test_level_text_artifact_success(self,
mock_subprocess_run,
mock_requests_get):
result_name = 'invocations/build-123/tests/test_id/results/result_id'
artifact_id = 'artifact1'
fetch_url = 'https://example.com/artifact1'
artifact_content = 'This is the artifact content.'
mock_subprocess_run.return_value = subprocess.CompletedProcess(
args=[],
returncode=0,
stdout=json.dumps({
'fetchUrl': fetch_url,
'contentType': 'text/plain'
}),
stderr='',
)
mock_response = mock.MagicMock()
mock_response.text = artifact_content
mock_response.raise_for_status = mock.MagicMock()
mock_requests_get.return_value = mock_response
content = await resultdb.get_test_level_text_artifact(
self.mock_context, result_name, artifact_id)
self.assertEqual(content, artifact_content)
expected_command = [
'prpc',
'call',
'results.api.luci.app',
'luci.resultdb.v1.ResultDB.GetArtifact',
]
expected_artifact_name = (
'invocations/build-123/tests/test_id/results/result_id/artifacts/'
'artifact1')
expected_request = {'name': expected_artifact_name}
mock_subprocess_run.assert_called_once_with(
expected_command,
capture_output=True,
input=json.dumps(expected_request),
check=True,
text=True,
)
mock_requests_get.assert_called_once_with(fetch_url)
mock_response.raise_for_status.assert_called_once()
@mock.patch('subprocess.run')
async def test_get_test_level_text_artifact_prpc_exception(
self, mock_subprocess_run):
result_name = 'invocations/build-123/tests/test_id/results/result_id'
artifact_id = 'artifact1'
mock_subprocess_run.side_effect = Exception('PRPC call failed')
with self.assertRaisesRegex(Exception, 'PRPC call failed'):
await resultdb.get_test_level_text_artifact(self.mock_context,
result_name,
artifact_id)
@mock.patch('subprocess.run')
async def test_get_test_level_text_artifact_wrong_content_type(
self, mock_subprocess_run):
result_name = 'invocations/build-123/tests/test_id/results/result_id'
artifact_id = 'artifact1'
fetch_url = 'https://example.com/artifact1'
mock_subprocess_run.return_value = subprocess.CompletedProcess(
args=[],
returncode=0,
stdout=json.dumps({
'fetchUrl': fetch_url,
'contentType': 'application/octet-stream'
}),
stderr='',
)
with self.assertRaisesRegex(ValueError,
('Expected text artifact, got content type '
'application/octet-stream')):
await resultdb.get_test_level_text_artifact(self.mock_context,
result_name,
artifact_id)
@mock.patch('requests.get')
@mock.patch('subprocess.run')
async def test_get_test_level_text_artifact_fetch_fails(
self, mock_subprocess_run, mock_requests_get):
result_name = 'invocations/build-123/tests/test_id/results/result_id'
artifact_id = 'artifact1'
fetch_url = 'https://example.com/artifact1'
mock_subprocess_run.return_value = subprocess.CompletedProcess(
args=[],
returncode=0,
stdout=json.dumps({
'fetchUrl': fetch_url,
'contentType': 'text/plain'
}),
stderr='',
)
mock_response = mock.MagicMock()
mock_response.raise_for_status.side_effect = (
requests.exceptions.HTTPError('404 Not Found'))
mock_requests_get.return_value = mock_response
with self.assertRaises(requests.exceptions.HTTPError):
await resultdb.get_test_level_text_artifact(self.mock_context,
result_name,
artifact_id)
if __name__ == '__main__':
unittest.main()