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.
140 lines
5.0 KiB
Python
140 lines
5.0 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.
|
|
"""Tools for interacting with ResultDB"""
|
|
|
|
import json
|
|
import posixpath
|
|
import requests
|
|
|
|
import bs4
|
|
from mcp.server import fastmcp
|
|
import telemetry
|
|
|
|
import common
|
|
|
|
tracer = telemetry.get_tracer(__name__)
|
|
|
|
RESULTDB_SERVER = 'results.api.luci.app'
|
|
|
|
|
|
async def get_non_exonerated_unexpected_results_from_build(
|
|
ctx: fastmcp.Context,
|
|
build_id: str,
|
|
):
|
|
"""Gets test results from the specified build.
|
|
|
|
The returned results will only be for unexpected results which were not
|
|
exonerated, i.e. unexpected test failures.
|
|
|
|
Args:
|
|
build_id: The Buildbucket ID of the build to retrieve results for. This
|
|
should only be the numerical ID, i.e. not prefixed with 'build-'.
|
|
|
|
Returns:
|
|
The stdout of the prpc command which should be a JSON string for a
|
|
luci.resultdb.v1.ResultDB.QueryTestResultsResponse proto. See
|
|
https://source.chromium.org/chromium/infra/infra_superproject/+/main:infra/go/src/go.chromium.org/luci/resultdb/proto/v1/resultdb.proto
|
|
for more details.
|
|
"""
|
|
with tracer.start_as_current_span(
|
|
'chromium.mcp.get_non_exonerated_unexpected_results_from_build'):
|
|
if not build_id.isnumeric():
|
|
raise ValueError(
|
|
f'Provided build_id {build_id} contains non-numeric characters')
|
|
request = {
|
|
'invocations': [
|
|
f'invocations/build-{build_id}',
|
|
],
|
|
'predicate': {
|
|
'expectancy': 'VARIANTS_WITH_UNEXPECTED_RESULTS',
|
|
'exclude_exonerated': True,
|
|
},
|
|
'read_mask': ('name,resultId,variant,status,statusV2,duration,'
|
|
'failureReason,summary_html'),
|
|
}
|
|
response = await common.run_prpc_call(
|
|
ctx, RESULTDB_SERVER, 'luci.resultdb.v1.ResultDB.QueryTestResults',
|
|
request)
|
|
return response
|
|
|
|
|
|
async def expand_summary_html(
|
|
ctx: fastmcp.Context,
|
|
result_name: str,
|
|
summary_html: str,
|
|
) -> str:
|
|
"""Expands the given summary HTML with referenced artifact content.
|
|
|
|
The summaryHtml field included in ResultDB test results often references
|
|
artifacts with `text-artifact` tags which contain the bulk of the useful
|
|
information. These references are automatically expanded in Milo, but need
|
|
to be manually expanded if accessing ResultDB directly.
|
|
|
|
Args:
|
|
result_name: The name of the result whose summary is being expanded.
|
|
This corresponds to the `name` field of a ResultDB result. Any URL
|
|
encoding must be left as-is.
|
|
summary_html: The summary HTML to expand. Corresponds to the
|
|
`summaryHtml` field of a ResultDB result.
|
|
|
|
Returns:
|
|
A copy of |summary_html| with any `text-artifact` tags replaced with
|
|
the contents of the artifacts they reference.
|
|
"""
|
|
with tracer.start_as_current_span('chromium.mcp.expand_summary_html'):
|
|
soup = bs4.BeautifulSoup(summary_html, 'html.parser')
|
|
for tag in soup.find_all('text-artifact'):
|
|
artifact_id = tag.get('artifact-id')
|
|
if not artifact_id:
|
|
continue
|
|
artifact_content = await get_test_level_text_artifact(
|
|
ctx, result_name, artifact_id)
|
|
tag.replace_with(artifact_content)
|
|
return str(soup)
|
|
|
|
|
|
async def get_test_level_text_artifact(
|
|
ctx: fastmcp.Context,
|
|
result_name: str,
|
|
artifact_id: str,
|
|
) -> str:
|
|
"""Retrieves the content for the specified test result level text artifact.
|
|
|
|
Since the expected content type is text, this cannot be used for retrieving
|
|
binary artifacts.
|
|
|
|
Test result level artifacts are associated with a single test (i.e. one
|
|
test case), which is different from an invocation level artifact (i.e.
|
|
for an entire Swarming task).
|
|
|
|
Args:
|
|
result_name: The name of the result whose artifact is being retrieved.
|
|
This corresponds to the `name` field of a ResultDB result. Any URL
|
|
encoding must be left as-is.
|
|
artifact_id: The ID of the artifact being retrieved. When combined with
|
|
|result_name|, this uniquely identifies an artifact within ResultDB.
|
|
|
|
Returns:
|
|
A string containing the contents of the specified artifact.
|
|
"""
|
|
with tracer.start_as_current_span(
|
|
'chromium.mcp.get_test_level_text_artifact'):
|
|
artifact_name = posixpath.join(result_name, 'artifacts', artifact_id)
|
|
request = {
|
|
'name': artifact_name,
|
|
}
|
|
prpc_response = await common.run_prpc_call(
|
|
ctx, RESULTDB_SERVER, 'luci.resultdb.v1.ResultDB.GetArtifact',
|
|
request)
|
|
response = json.loads(prpc_response)
|
|
|
|
content_type = response['contentType']
|
|
if 'text/plain' not in content_type:
|
|
raise ValueError(
|
|
f'Expected text artifact, got content type {content_type}')
|
|
|
|
r = requests.get(response['fetchUrl'])
|
|
r.raise_for_status()
|
|
return r.text
|