# 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 = ('

Failure

') 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'

Failure

{artifact_content}') async def test_expand_summary_html_no_artifacts(self): result_name = 'invocations/build-123/tests/test_id/results/result_id' summary_html = '

Just a regular summary.

' 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 = '

Failure

' expanded_html = await resultdb.expand_summary_html( self.mock_context, result_name, summary_html) mock_get_artifact.assert_not_called() self.assertEqual(expanded_html, '

Failure

') 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()