blob: 727f084a3b454c44c4f84b1a9eca301c1b86e6bf [file] [log] [blame]
xixuan878b1eb2017-03-20 15:58:17 -07001# Copyright 2017 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Module for interacting with google APIs."""
Xixuan Wu5d6063e2017-09-05 16:15:07 -07006# pylint: disable=g-bad-import-order
Xixuan Wud55ac6e2019-03-14 10:56:39 -07007# pylint: disable=g-bad-exception-name
Xixuan Wu5d6063e2017-09-05 16:15:07 -07008
Xixuan Wuf856ff12019-05-21 14:09:38 -07009import ast
Xixuan Wu5d6063e2017-09-05 16:15:07 -070010import httplib2
Xixuan Wu55d38c52019-05-21 14:26:23 -070011import logging
Xixuan Wuf856ff12019-05-21 14:09:38 -070012import re
xixuan878b1eb2017-03-20 15:58:17 -070013
14import apiclient
Xixuan Wuf856ff12019-05-21 14:09:38 -070015import build_lib
xixuan878b1eb2017-03-20 15:58:17 -070016import constants
17import file_getter
Xixuan Wuf856ff12019-05-21 14:09:38 -070018import global_config
19import time_converter
xixuan878b1eb2017-03-20 15:58:17 -070020
21from oauth2client import service_account
22from oauth2client.contrib import appengine
23
24
25class RestClientError(Exception):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070026 """Raised when there is a general error."""
xixuan878b1eb2017-03-20 15:58:17 -070027
28
29class NoServiceRestClientError(RestClientError):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070030 """Raised when there is no ready service for a google API."""
xixuan878b1eb2017-03-20 15:58:17 -070031
32
33class BaseRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070034 """Base class of REST client for google APIs."""
xixuan878b1eb2017-03-20 15:58:17 -070035
Xixuan Wu5d6063e2017-09-05 16:15:07 -070036 def __init__(self, scopes, service_name, service_version):
37 """Initialize a REST client to connect to a google API.
xixuan878b1eb2017-03-20 15:58:17 -070038
Xixuan Wu5d6063e2017-09-05 16:15:07 -070039 Args:
40 scopes: the scopes of the to-be-connected API.
41 service_name: the service name of the to-be-connected API.
42 service_version: the service version of the to-be-connected API.
43 """
44 self.running_env = constants.environment()
45 self.scopes = scopes
46 self.service_name = service_name
47 self.service_version = service_version
xixuan878b1eb2017-03-20 15:58:17 -070048
Xixuan Wu5d6063e2017-09-05 16:15:07 -070049 @property
50 def service(self):
51 if not self._service:
52 raise NoServiceRestClientError('No service created for calling API')
xixuan878b1eb2017-03-20 15:58:17 -070053
Xixuan Wu5d6063e2017-09-05 16:15:07 -070054 return self._service
xixuan878b1eb2017-03-20 15:58:17 -070055
Xixuan Wu5d6063e2017-09-05 16:15:07 -070056 def create_service(self, discovery_url=None):
57 """Create the service for a google API."""
58 self._init_credentials()
59 # Explicitly specify timeout for http to avoid DeadlineExceededError.
60 # It's used for services like AndroidBuild API, which raise such error
61 # when being triggered too many calls in a short time frame.
62 # http://stackoverflow.com/questions/14698119/httpexception-deadline-exceeded-while-waiting-for-http-response-from-url-dead
63 http_auth = self._credentials.authorize(httplib2.Http(timeout=30))
64 if discovery_url is None:
65 self._service = apiclient.discovery.build(
66 self.service_name, self.service_version,
67 http=http_auth)
68 else:
69 self._service = apiclient.discovery.build(
70 self.service_name, self.service_version, http=http_auth,
71 discoveryServiceUrl=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -070072
Xixuan Wu5d6063e2017-09-05 16:15:07 -070073 def _init_credentials(self):
74 """Initialize the credentials for a google API."""
75 if (self.running_env == constants.RunningEnv.ENV_STANDALONE or
76 self.running_env == constants.RunningEnv.ENV_DEVELOPMENT_SERVER):
77 # Running locally
78 service_credentials = service_account.ServiceAccountCredentials
79 self._credentials = service_credentials.from_json_keyfile_name(
Xixuan Wu26d06e02017-09-20 14:50:28 -070080 file_getter.STAGING_CLIENT_SECRETS_FILE, self.scopes)
Xixuan Wu5d6063e2017-09-05 16:15:07 -070081 else:
82 # Running in app-engine production
83 self._credentials = appengine.AppAssertionCredentials(self.scopes)
xixuan878b1eb2017-03-20 15:58:17 -070084
85
86class AndroidBuildRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -070087 """REST client for android build API."""
xixuan878b1eb2017-03-20 15:58:17 -070088
Xixuan Wu5d6063e2017-09-05 16:15:07 -070089 def __init__(self, rest_client):
90 """Initialize a REST client for connecting to Android Build API."""
91 self._rest_client = rest_client
92 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -070093
Xixuan Wu5d6063e2017-09-05 16:15:07 -070094 def get_latest_build_id(self, branch, target):
95 """Get the latest build id for a given branch and target.
xixuan878b1eb2017-03-20 15:58:17 -070096
Xixuan Wu5d6063e2017-09-05 16:15:07 -070097 Args:
98 branch: an android build's branch
99 target: an android build's target
xixuan878b1eb2017-03-20 15:58:17 -0700100
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700101 Returns:
102 A string representing latest build id.
103 """
104 request = self._rest_client.service.build().list(
105 buildType='submitted',
106 branch=branch,
107 target=target,
108 successful=True,
109 maxResults=1)
110 builds = request.execute(num_retries=10)
111 if not builds or not builds['builds']:
112 return None
xixuan878b1eb2017-03-20 15:58:17 -0700113
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700114 return builds['builds'][0]['buildId']
xixuan878b1eb2017-03-20 15:58:17 -0700115
116
117class StorageRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700118 """REST client for google storage API."""
xixuan878b1eb2017-03-20 15:58:17 -0700119
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700120 def __init__(self, rest_client):
121 """Initialize a REST client for connecting to Google storage API."""
122 self._rest_client = rest_client
123 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700124
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700125 def read_object(self, bucket, object_path):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700126 """Read the contents of input_object in input_bucket.
xixuan878b1eb2017-03-20 15:58:17 -0700127
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700128 Args:
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700129 bucket: A string to indicate the bucket for fetching the object.
130 e.g. constants.StorageBucket.PROD_SUITE_SCHEDULER
131 object_path: A string to indicate the path of the object to read the
132 contents.
xixuan878b1eb2017-03-20 15:58:17 -0700133
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700134 Returns:
135 the stripped string contents of the input object.
xixuan878b1eb2017-03-20 15:58:17 -0700136
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700137 Raises:
138 apiclient.errors.HttpError
139 """
140 req = self._rest_client.service.objects().get_media(
Xixuan Wud55ac6e2019-03-14 10:56:39 -0700141 bucket=bucket,
142 object=object_path)
143 return req.execute()
144
145 def upload_object(self, bucket, src_object_path, dest_object_path):
146 """Upload object_path to input_bucket.
147
148 Args:
149 bucket: A string to indicate the bucket for the object to be uploaded to.
150 src_object_path: A string the full path of the object to upload.
151 dest_object_path: A string path inside bucket to upload to.
152
153 Returns:
154 A dict of uploaded object info.
155
156 Raises:
157 apiclient.errors.HttpError
158 """
159 req = self._rest_client.service.objects().insert(
160 bucket=bucket,
161 name=dest_object_path,
162 media_body=src_object_path,
163 media_mime_type='text/plain',
164 )
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700165 return req.execute()
xixuan878b1eb2017-03-20 15:58:17 -0700166
167
168class CalendarRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700169 """Class of REST client for google calendar API."""
xixuan878b1eb2017-03-20 15:58:17 -0700170
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700171 def __init__(self, rest_client):
172 """Initialize a REST client for connecting to Google calendar API."""
173 self._rest_client = rest_client
174 self._rest_client.create_service()
xixuan878b1eb2017-03-20 15:58:17 -0700175
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700176 def add_event(self, calendar_id, input_event):
177 """Add events of a given calendar.
xixuan878b1eb2017-03-20 15:58:17 -0700178
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700179 Args:
180 calendar_id: the ID of the given calendar.
181 input_event: the event to be added.
182 """
183 self._rest_client.service.events().insert(
184 calendarId=calendar_id,
185 body=input_event).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700186
187
Xixuan Wu6f117e92017-10-27 10:51:58 -0700188class StackdriverRestClient(object):
189 """REST client for google storage API."""
190
191 def __init__(self, rest_client):
192 """Initialize a REST client for connecting to Google storage API."""
193 self._rest_client = rest_client
194 self._rest_client.create_service()
195
196 def read_logs(self, request):
Xinan Lin318cf752019-07-19 14:50:23 -0700197 # project_id, page_size, order_by, query_filter=''):
Xixuan Wu6f117e92017-10-27 10:51:58 -0700198 """Read the logs of the project_id based on all filters.
199
200 Args:
201 request: a request dict generated by
202 stackdriver_lib.form_logging_client_request.
203
204 Returns:
205 A json object, can be parsed by
206 stackdriver_lib.parse_logging_client_response.
207
208 Raises:
209 apiclient.errors.HttpError
210 """
211 req = self._rest_client.service.entries().list(
212 fields='entries/protoPayload', body=request)
213 return req.execute()
214
215
xixuan878b1eb2017-03-20 15:58:17 -0700216class SwarmingRestClient(object):
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700217 """REST client for swarming proxy API."""
xixuan878b1eb2017-03-20 15:58:17 -0700218
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700219 DISCOVERY_URL_PATTERN = '%s/discovery/v1/apis/%s/%s/rest'
xixuan878b1eb2017-03-20 15:58:17 -0700220
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700221 def __init__(self, rest_client, service_url):
222 self._rest_client = rest_client
223 discovery_url = self.DISCOVERY_URL_PATTERN % (
224 service_url, rest_client.service_name, rest_client.service_version)
225 self._rest_client.create_service(discovery_url=discovery_url)
xixuan878b1eb2017-03-20 15:58:17 -0700226
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700227 def create_task(self, request):
228 """Create new task.
xixuan878b1eb2017-03-20 15:58:17 -0700229
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700230 Args:
231 request: a json-compatible dict expected by swarming server.
232 See _to_raw_request's output in swarming_lib.py for details.
xixuan878b1eb2017-03-20 15:58:17 -0700233
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700234 Returns:
235 A json dict returned by API task.new.
236 """
237 return self._rest_client.service.tasks().new(
238 fields='request,task_id', body=request).execute()
xixuan878b1eb2017-03-20 15:58:17 -0700239
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700240 def get_task_result(self, task_id):
241 """Get task results by a given task_id.
xixuan878b1eb2017-03-20 15:58:17 -0700242
Xixuan Wu5d6063e2017-09-05 16:15:07 -0700243 Args:
244 task_id: A string, represents task id.
245
246 Returns:
247 A json dict returned by API task.result.
248 """
249 return self._rest_client.service.task().result(
250 task_id=task_id).execute()
Xixuan Wu7d142a92019-04-26 12:03:02 -0700251
252
253class BigqueryRestClient(object):
254 """Class of REST client for Bigquery API."""
255
Xixuan Wu55d38c52019-05-21 14:26:23 -0700256 PROJECT_TO_RUN_BIGQUERY_JOB = 'google.com:suite-scheduler'
257
Xixuan Wu7d142a92019-04-26 12:03:02 -0700258 def __init__(self, rest_client):
259 """Initialize a REST client for connecting to Bigquery API."""
260 self._rest_client = rest_client
261 self._rest_client.create_service()
262
Xixuan Wu55d38c52019-05-21 14:26:23 -0700263 def query(self, query_str):
264 """Query bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700265
266 Args:
Xixuan Wu55d38c52019-05-21 14:26:23 -0700267 query_str: A string used to query Bigquery.
Xixuan Wu7d142a92019-04-26 12:03:02 -0700268
269 Returns:
270 A json dict returned by API bigquery.jobs.query, e.g.
271 # {...,
272 # "rows": [
273 # {
274 # "f": [ # field
275 # {
276 # "v": # value
Xixuan Wu55d38c52019-05-21 14:26:23 -0700277 # },
278 # {
279 # "v": # value
280 # },
281 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700282 # ]
283 # }
Xixuan Wuf856ff12019-05-21 14:09:38 -0700284 # ...
Xixuan Wu7d142a92019-04-26 12:03:02 -0700285 # ]
286 # }
287 """
288 query_data = {
Xixuan Wu55d38c52019-05-21 14:26:23 -0700289 'query': query_str,
290 'useLegacySql': False,
Xixuan Wu7d142a92019-04-26 12:03:02 -0700291 }
292 return self._rest_client.service.jobs().query(
Xixuan Wu55d38c52019-05-21 14:26:23 -0700293 projectId=self.PROJECT_TO_RUN_BIGQUERY_JOB,
294 fields='rows',
Xixuan Wu7d142a92019-04-26 12:03:02 -0700295 body=query_data).execute()
Xixuan Wu55d38c52019-05-21 14:26:23 -0700296
297
Xinan Lin80a9d932019-10-17 09:24:43 -0700298class CrOSTestPlatformBigqueryClient(BigqueryRestClient):
299 """REST client for cros_test_platform builder Bigquery API."""
Xixuan Wu55d38c52019-05-21 14:26:23 -0700300
Xinan Lin80a9d932019-10-17 09:24:43 -0700301 def get_past_job_nums(self, hours):
302 """Query the count of the jobs kicked off to cros_test_platform.
Xixuan Wu55d38c52019-05-21 14:26:23 -0700303
304 Args:
305 hours: An integer.
306
307 Returns:
308 An integer.
309 """
310 query_str = """
311 SELECT
312 COUNT(*)
313 FROM
Xinan Lin80a9d932019-10-17 09:24:43 -0700314 `cr-buildbucket.chromeos.builds`
Xixuan Wu55d38c52019-05-21 14:26:23 -0700315 WHERE
Xinan Lin80a9d932019-10-17 09:24:43 -0700316 created_by = 'user:suite-scheduler.google.com@appspot.gserviceaccount.com'
317 and create_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL %d HOUR);
Xixuan Wu55d38c52019-05-21 14:26:23 -0700318 """
319 res = self.query(query_str % hours)
320 try:
321 return int(_parse_bq_job_query(res)[0][0])
322 except (ValueError, KeyError) as e:
323 logging.debug('The returned json: \n%r', res)
324 logging.exception(str(e))
325 raise
326
327
Xixuan Wuf856ff12019-05-21 14:09:38 -0700328class BuildBucketBigqueryClient(BigqueryRestClient):
329 """Rest client for buildbucket Bigquery API."""
330
Xinan Lin028f9582019-12-11 10:55:33 -0800331 def get_latest_passed_firmware_builds(self):
332 """Get artifact link of the latest passed firmware builds for board.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700333
Xinan Lin028f9582019-12-11 10:55:33 -0800334 The query returns the latest firmware build for the combination of
335 board and build spec, which is cros or firmware. No restriction set
336 in the query, so it should return all available builds.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700337
338 Returns:
Xinan Lin028f9582019-12-11 10:55:33 -0800339 A list of (spec, board, firmware_artifact_link).
Xixuan Wuf856ff12019-05-21 14:09:38 -0700340 """
341 query_str = """
342 SELECT
Xinan Lin028f9582019-12-11 10:55:33 -0800343 spec,
344 board,
345 /*
346 * Firmware builds may contain artifacts for multiple boards in a
347 * single build - each in a separate directory.
348 */
349 IF(spec = 'firmware', CONCAT(artifact, '/', board), artifact) as artifact
Xixuan Wuf856ff12019-05-21 14:09:38 -0700350 FROM
Xinan Lin028f9582019-12-11 10:55:33 -0800351 (
352 SELECT
353 spec,
354 board,
355 artifact,
356 RANK() OVER (PARTITION BY spec, board ORDER BY end_time DESC) AS rank
357 FROM
358 (
359 SELECT
360 /*
361 * build_config is a string contains the board and build type.
362 * For Cros build, it has the form of "BoardName-release", while
363 * the firmware config shows like "firmware-BoardName-[1]-firmwarebranch".
364 * [1] is the firmware ver.
365 */
366 IF(prefix = 'firmware', 'firmware', 'cros') AS spec,
367 IF(prefix = 'firmware', firmware_post_prefix, cros_prefix) AS board,
368 artifact,
369 end_time
370 FROM
371 (
372 SELECT
373 SPLIT(build_config, '-') [OFFSET(0)] AS prefix,
374 SPLIT(build_config, '-') [OFFSET(1)] AS firmware_post_prefix,
375 REGEXP_EXTRACT(
376 build_config, r"(^[a-zA-Z0-9_.+-]+)-release"
377 ) as cros_prefix,
378 end_time,
379 artifact
380 FROM
381 (
382 SELECT
383 JSON_EXTRACT_SCALAR(
384 output.properties, '$.artifact_link'
385 ) as artifact,
386 JSON_EXTRACT_SCALAR(
387 output.properties, '$.cbb_config'
388 ) as build_config,
389 end_time
390 FROM `cr-buildbucket.chromeos.completed_builds_BETA`
391 WHERE
392 status = 'SUCCESS'
393 AND JSON_EXTRACT_SCALAR(
394 output.properties, '$.suite_scheduling'
395 ) = 'True'
396 )
397 )
398 )
399 )
400 WHERE rank = 1
Xixuan Wuf856ff12019-05-21 14:09:38 -0700401 """
Xinan Lin028f9582019-12-11 10:55:33 -0800402 res = self.query(query_str)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700403 res = _parse_bq_job_query(res)
404 if res is None:
405 return None
Xinan Lin028f9582019-12-11 10:55:33 -0800406 logging.info('Fetched the latest artifact links: %s',
407 [row[2] for row in res])
408 return res
Xinan Lin318cf752019-07-19 14:50:23 -0700409
Xinan Linea1efcb2019-12-30 23:46:42 -0800410 def get_passed_builds(self, earliest_end_time, latest_end_time):
411 """Get passed builds inside a given time span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700412
413 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800414 earliest_end_time: a datetime.datetime object in UTC.
415 latest_end_time: a datetime.datetime object in UTC.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700416
417 Returns:
418 A list of build_lib.BuildInfo objects.
419 """
420 query_str = """
421 SELECT
422 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
423 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
424 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
425 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config')
426 FROM
427 `cr-buildbucket.chromeos.completed_builds_BETA`
428 WHERE
429 end_time > '%s' and
Xinan Linea1efcb2019-12-30 23:46:42 -0800430 end_time < '%s' and
Xixuan Wuf856ff12019-05-21 14:09:38 -0700431 status = 'SUCCESS' and
432 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
433 """
434 if global_config.GAE_TESTING:
Xinan Linea1efcb2019-12-30 23:46:42 -0800435 earliest_end_time_str = '2019-05-19 20:00:00'
436 latest_end_time_str = '2019-05-20 20:00:00'
Xixuan Wuf856ff12019-05-21 14:09:38 -0700437 query_str += 'limit 10'
438 else:
Xinan Linea1efcb2019-12-30 23:46:42 -0800439 earliest_end_time_str = earliest_end_time.strftime(
440 time_converter.TIME_FORMAT)
441 latest_end_time_str = latest_end_time.strftime(
442 time_converter.TIME_FORMAT)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700443
Xinan Linea1efcb2019-12-30 23:46:42 -0800444 logging.info('Getting passed builds finished between %s and %s',
445 earliest_end_time_str, latest_end_time_str)
446 res = self.query(query_str % (earliest_end_time_str, latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700447 res = _parse_bq_job_query(res)
448 if res is None:
449 return []
450
451 build_infos = []
452 for board, milestone, platform, build_config in res:
453 board = _parse_board(build_config, board)
454 build_infos.append(
455 build_lib.BuildInfo(board, None, milestone, platform, build_config))
456
457 return build_infos
458
Xinan Linea1efcb2019-12-30 23:46:42 -0800459 def get_relaxed_passed_builds(self, earliest_end_time, latest_end_time):
460 """Get builds with successful "HWTest [sanity]" stages between a given span.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700461
462 Args:
Xinan Linea1efcb2019-12-30 23:46:42 -0800463 earliest_end_time: a datetime.datetime object in UTC.
464 latest_end_time: a datetime.datetime object in UTC.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700465
466 Returns:
467 A list of build_lib.BuildInfo objects.
468 """
469 query_str = """
470 SELECT
471 JSON_EXTRACT_SCALAR(output.properties, '$.unibuild'),
472 s.name,
473 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
474 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
475 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
476 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config')
477 FROM
478 `cr-buildbucket.chromeos.completed_builds_BETA` as c,
479 UNNEST(c.steps) AS s
480 WHERE
481 c.end_time > '%s' and
Xinan Linea1efcb2019-12-30 23:46:42 -0800482 c.end_time < '%s' and
Xixuan Wuf856ff12019-05-21 14:09:38 -0700483 c.status != 'SUCCESS' and
Xinan Lina773d082020-01-10 11:33:34 -0800484 s.name like '%%SkylabHWTest%%' and
Xixuan Wuf856ff12019-05-21 14:09:38 -0700485 s.status = 'SUCCESS' and
486 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
487 """
488 if global_config.GAE_TESTING:
Xinan Linea1efcb2019-12-30 23:46:42 -0800489 earliest_end_time_str = '2019-05-19 20:00:00'
490 latest_end_time_str = '2019-05-20 20:00:00'
Xixuan Wuf856ff12019-05-21 14:09:38 -0700491 query_str += 'limit 10'
492 else:
Xinan Linea1efcb2019-12-30 23:46:42 -0800493 earliest_end_time_str = earliest_end_time.strftime(
494 time_converter.TIME_FORMAT)
495 latest_end_time_str = latest_end_time.strftime(
496 time_converter.TIME_FORMAT)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700497
Xinan Linea1efcb2019-12-30 23:46:42 -0800498 logging.info('Getting relaxed passed builds finished between %s and %s',
499 earliest_end_time_str, latest_end_time_str)
500 res = self.query(query_str % (earliest_end_time_str, latest_end_time_str))
Xixuan Wuf856ff12019-05-21 14:09:38 -0700501 res = _parse_bq_job_query(res)
502 if res is None:
503 return []
504
505 build_infos = []
506 for unibuild, stage_name, board, milestone, platform, build_config in res:
507 board = _parse_board(build_config, board)
508 if ast.literal_eval(unibuild):
509 model = _parse_model(stage_name)
510 else:
511 model = None
512
513 build_infos.append(
514 build_lib.BuildInfo(board, model, milestone, platform, build_config))
515
516 return build_infos
517
518
Xixuan Wu55d38c52019-05-21 14:26:23 -0700519def _parse_bq_job_query(json_input):
520 """Parse response from API bigquery.jobs.query.
521
522 Args:
523 json_input: a dict, representing jsons returned by query API.
524
525 Returns:
526 A 2D string matrix: [rows[columns]], or None if no result.
527 E.g. Input:
528 "rows": [
529 {
530 "f": [ # field
531 {
532 "v": 'foo1',
533 },
534 {
535 "v": 'foo2',
536 }
537 ]
538 }
539 {
540 "f": [ # field
541 {
542 "v": 'bar1',
543 },
544 {
545 "v": 'bar2',
546 }
547 ]
548 }
549 ]
550 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
551 """
552 if 'rows' not in json_input:
553 return None
554
555 res = []
556 for r in json_input['rows']:
557 rc = []
558 for c in r['f']:
559 rc.append(c['v'])
560
561 res.append(rc)
562
563 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700564
565
566def _parse_model(build_stage_name):
567 """Parse model name from the build stage name.
568
569 It's only used for HWTest Sanity stage. An example build_stage_name will
570 be 'HWTest [sanity] [whitetip]'.
571 Args:
572 build_stage_name: The stage name of a HWTest sanity stage, e.g.
573 "HWTest [sanity] [whitetip]".
574
575 Returns:
576 A model name, e.g. "whitetip" or None.
577 """
578 if 'HWTest [sanity]' not in build_stage_name:
579 return None
580
581 try:
582 model = build_stage_name.strip().split()[-1][1:-1]
583 if not model:
584 logging.warning('Cannot parse build stage name: %s', build_stage_name)
585 return None
586
587 return model
588 except IndexError:
589 logging.error('Cannot parse build stage name: %s', build_stage_name)
590 return None
591
592
593def _parse_board(build_config, board):
594 """Parse board from build_config if needed.
595
596 Board could be None for old release. See crbug.com/944981#c16.
597 This function can be removed once all release are newer than R74.
598
599 Args:
600 build_config: A string build config, e.g. reef-release.
601 board: A string board, e.g. reef.
602
603 Returns:
604 A string board if exist, or None.
605 """
606 if board is None:
607 match = re.match(r'(.+)-release', build_config)
608 if not match:
609 logging.debug('Cannot parse board from %s', build_config)
610 return None
611
612 return match.groups()[0]
613
614 return board