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.
259 lines
9.5 KiB
Python
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()
|