blob: 5d6b07c71d64a834728171f7852e13fb489090e4 [file] [log] [blame]
Avi Drissmandfd880852022-09-15 20:11:091# Copyright 2021 The Chromium Authors
Owen Rodleyfc72e9f2021-10-29 02:06:282# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Functions for querying ResultDB, via the "rdb rpc" subcommand."""
5
6import datetime
7import subprocess
8import json
9import re
10from typing import Optional, Tuple
11
12import errors
13
14
Quang Minh Tuan Nguyenbd81b212022-09-21 04:19:3615def get_test_metadata(invocation, test_regex: str) -> Tuple[str, str]:
Owen Rodleyfc72e9f2021-10-29 02:06:2816 """Fetch test metadata from ResultDB.
17
18 Args:
Quang Minh Tuan Nguyenbd81b212022-09-21 04:19:3619 invocation: the invocation to fetch the test
20 test_regex: The regex to match the test id
Owen Rodleyfc72e9f2021-10-29 02:06:2821 Returns:
22 A tuple of (test name, filename). The test name will for example have the
23 form SuitName.TestName for GTest tests. The filename is the location in the
24 source tree where this test is defined.
25 """
Quang Minh Tuan Nguyenbd81b212022-09-21 04:19:3626 test_results = query_test_result(invocation=invocation, test_regex=test_regex)
27 if 'testResults' not in test_results:
Owen Rodleyfc72e9f2021-10-29 02:06:2828 raise errors.UserError(
Quang Minh Tuan Nguyenbd81b212022-09-21 04:19:3629 f"ResultDB couldn't query for invocation: {invocation}")
Owen Rodleyfc72e9f2021-10-29 02:06:2830
Quang Minh Tuan Nguyenbd81b212022-09-21 04:19:3631 if len(test_results["testResults"]) == 0:
32 raise errors.UserError(
33 f"ResultDB couldn't find test result for test regex {test_regex}")
Owen Rodleyfc72e9f2021-10-29 02:06:2834
Quang Minh Tuan Nguyenbd81b212022-09-21 04:19:3635 result = test_results["testResults"][0]
Owen Rodleyfc72e9f2021-10-29 02:06:2836 try:
37 name = result['testMetadata']['name']
38 loc = result['testMetadata']['location']
39 repo, filename = loc['repo'], loc['fileName']
40 except KeyError as e:
41 raise errors.InternalError(
42 f"Malformed GetTestResult response: no key {e}") from e
43
Owen Rodleyfc72e9f2021-10-29 02:06:2844 if repo != 'https://chromium.googlesource.com/chromium/src':
45 raise errors.UserError(
46 f"Test is in repo '{repo}', this tool can only disable tests in " +
47 "chromium/chromium/src")
48
49 return name, filename
50
51
52def get_test_result_history(test_id: str, page_size: int) -> dict:
53 """Make a GetTestResultHistory RPC call to ResultDB.
54
55 Args:
56 test_id: The full test ID to query. This can be a regex.
57 page_size: The number of results to return within the first response.
58
59 Returns:
60 The GetTestResultHistoryResponse message, in dict form.
61 """
62
63 now = datetime.datetime.now(datetime.timezone.utc)
64 request = {
65 'realm': 'chromium:ci',
66 'testIdRegexp': test_id,
67 'timeRange': {
68 'earliest': (now - datetime.timedelta(hours=6)).isoformat(),
69 'latest': now.isoformat(),
70 },
71 'pageSize': page_size,
72 }
73
74 return rdb_rpc('GetTestResultHistory', request)
75
76
77def get_test_result(test_name: str) -> dict:
78 """Make a GetTestResult RPC call to ResultDB.
79
80 Args:
81 test_name: The name of the test result to query. This specifies a result for
82 a particular test, within a particular test run. As returned by
83 GetTestResultHistory.
84
85 Returns:
86 The TestResult message, in dict form.
87 """
88
89 return rdb_rpc('GetTestResult', {
90 'name': test_name,
91 })
92
93
Quang Minh Tuan Nguyenbd81b212022-09-21 04:19:3694def query_test_result(invocation: str, test_regex: str):
95 """Make a QueryTestResults RPC call to ResultDB.
96
97 Args:
98 invocation_name: the name of the invocation to query for test results
99 test_regex: The test regex to filter
100
101 Returns:
102 The QueryTestResults response message, in dict form.
103 """
104 request = {
105 'invocations': [invocation],
106 'readMask': {
107 'paths': ['test_id', 'test_metadata'],
108 },
109 'pageSize': 1000,
110 'predicate': {
111 'testIdRegexp': test_regex,
112 },
113 }
114 return rdb_rpc('QueryTestResults', request)
115
116
Owen Rodleyfc72e9f2021-10-29 02:06:28117# Used for caching RPC responses, for development purposes.
118CANNED_RESPONSE_FILE: Optional[str] = None
119
120
121def rdb_rpc(method: str, request: dict) -> dict:
122 """Call the given RPC method, with the given request.
123
124 Args:
125 method: The method to call. Must be within luci.resultdb.v1.ResultDB.
126 request: The request, in dict format.
127
128 Returns:
129 The response from ResultDB, in dict format.
130 """
131
132 if CANNED_RESPONSE_FILE is not None:
133 try:
134 with open(CANNED_RESPONSE_FILE, 'r') as f:
135 canned_responses = json.load(f)
136 except Exception:
137 canned_responses = {}
138
139 # HACK: Strip out timestamps when caching the request. GetTestResultHistory
140 # includes timestamps based on the current time, which will bust the cache.
141 # But for e2e testing we just want to cache the result the first time and
142 # then keep using it.
143 if 'timeRange' in request:
144 key_request = dict(request)
145 del key_request['timeRange']
146 else:
147 key_request = request
148
149 key = f'{method}/{json.dumps(key_request)}'
150 if (response_json := canned_responses.get(key, None)) is not None:
151 return json.loads(response_json)
152
153 p = subprocess.Popen(['rdb', 'rpc', 'luci.resultdb.v1.ResultDB', method],
154 stdin=subprocess.PIPE,
155 stdout=subprocess.PIPE,
156 stderr=subprocess.PIPE,
157 text=True)
158
159 stdout, stderr = p.communicate(json.dumps(request))
160 if p.returncode != 0:
Owen Rodleye79c4022022-01-21 04:22:38161 # rdb doesn't return unique status codes for different errors, so we have to
162 # just match on the output.
163 if 'interactive login is required' in stderr:
164 raise errors.UserError(
165 "Authentication is required to fetch test metadata.\n" +
166 "Please run:\n\trdb auth-login\nand try again")
167
Owen Rodleyfc72e9f2021-10-29 02:06:28168 raise Exception(f'rdb rpc {method} failed with: {stderr}')
169
170 if CANNED_RESPONSE_FILE:
171 canned_responses[key] = stdout
172 with open(CANNED_RESPONSE_FILE, 'w') as f:
173 json.dump(canned_responses, f)
174
175 return json.loads(stdout)