blob: be56e2a4f4c61d59731acfb737ba550f591fd00d [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 Lin39dcca82019-07-26 18:55:51 -0700331 def get_latest_passed_builds_artifact_link(self, build_config):
332 """Get artifact link of the latest passed builds by build_config.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700333
334 Args:
335 build_config: a string like '{board}-{build_type}', eg. link-release
336
337 Returns:
Xinan Lin39dcca82019-07-26 18:55:51 -0700338 A string: artifact_link of this firmware build.
Xixuan Wuf856ff12019-05-21 14:09:38 -0700339 """
340 query_str = """
341 SELECT
Xinan Lin39dcca82019-07-26 18:55:51 -0700342 JSON_EXTRACT_SCALAR(output.properties, '$.artifact_link')
Xixuan Wuf856ff12019-05-21 14:09:38 -0700343 FROM
344 `cr-buildbucket.chromeos.completed_builds_BETA`
345 WHERE
346 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config') = '%s' and
347 status = 'SUCCESS' and
348 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
349 ORDER BY
350 end_time desc
351 LIMIT 1
352 """
Xinan Lin80a9d932019-10-17 09:24:43 -0700353 logging.info('Getting artifact link of the latest build for %s',
354 build_config)
Xixuan Wuf856ff12019-05-21 14:09:38 -0700355 res = self.query(query_str % build_config)
356 res = _parse_bq_job_query(res)
357 if res is None:
358 return None
359
Xinan Lin39dcca82019-07-26 18:55:51 -0700360 return res[0][0]
Xixuan Wuf856ff12019-05-21 14:09:38 -0700361
Xinan Lin39dcca82019-07-26 18:55:51 -0700362 def get_latest_passed_builds_artifact_link_firmware(self, board):
363 """Get artifact link of the latest passed firmware builds by board.
Xinan Lin318cf752019-07-19 14:50:23 -0700364
365 Args:
366 board: the board against which this task will run suite job.
367
368 Returns:
Xinan Lin39dcca82019-07-26 18:55:51 -0700369 A string: artifact_link of this firmware build.
Xinan Lin318cf752019-07-19 14:50:23 -0700370 """
371 query_str = """
372 SELECT
Xinan Lin39dcca82019-07-26 18:55:51 -0700373 JSON_EXTRACT_SCALAR(output.properties, '$.artifact_link')
Xinan Lin318cf752019-07-19 14:50:23 -0700374 FROM
375 `cr-buildbucket.chromeos.completed_builds_BETA`
376 WHERE
377 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'), '-')[OFFSET(0)] = 'firmware' and
378 SPLIT(JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config'), '-')[OFFSET(1)] = '%s' and
379 status = 'SUCCESS' and
380 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
381 ORDER BY
382 end_time desc
383 LIMIT 1
384 """
Xinan Lin39dcca82019-07-26 18:55:51 -0700385 logging.info('Getting artifact link of the latest build for %s-firmware',
386 board)
Xinan Lin318cf752019-07-19 14:50:23 -0700387 res = self.query(query_str % board)
388 res = _parse_bq_job_query(res)
389 if res is None:
390 return None
Xinan Lin39dcca82019-07-26 18:55:51 -0700391
392 return res[0][0]
Xinan Lin318cf752019-07-19 14:50:23 -0700393
Xixuan Wuf856ff12019-05-21 14:09:38 -0700394 def get_passed_builds_since_date(self, since_date):
395 """Get passed builds since a given date.
396
397 Args:
398 since_date: a datetime.datetime object in UTC.
399
400 Returns:
401 A list of build_lib.BuildInfo objects.
402 """
403 query_str = """
404 SELECT
405 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
406 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
407 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
408 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config')
409 FROM
410 `cr-buildbucket.chromeos.completed_builds_BETA`
411 WHERE
412 end_time > '%s' and
413 status = 'SUCCESS' and
414 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
415 """
416 if global_config.GAE_TESTING:
417 since_date_str = '2019-05-19 20:00:00'
418 query_str += 'limit 10'
419 else:
420 since_date_str = since_date.strftime(time_converter.TIME_FORMAT)
421
422 logging.info('Getting passed builds since %s', since_date_str)
423 res = self.query(query_str % since_date_str)
424 res = _parse_bq_job_query(res)
425 if res is None:
426 return []
427
428 build_infos = []
429 for board, milestone, platform, build_config in res:
430 board = _parse_board(build_config, board)
431 build_infos.append(
432 build_lib.BuildInfo(board, None, milestone, platform, build_config))
433
434 return build_infos
435
436 def get_relaxed_passed_builds_since_date(self, since_date):
437 """Get builds with successful "HWTest [sanity]" stages since a given date.
438
439 Args:
440 since_date: a datetime.datetime object in UTC.
441
442 Returns:
443 A list of build_lib.BuildInfo objects.
444 """
445 query_str = """
446 SELECT
447 JSON_EXTRACT_SCALAR(output.properties, '$.unibuild'),
448 s.name,
449 JSON_EXTRACT_SCALAR(output.properties, '$.board'),
450 JSON_EXTRACT_SCALAR(output.properties, '$.milestone_version'),
451 JSON_EXTRACT_SCALAR(output.properties, '$.platform_version'),
452 JSON_EXTRACT_SCALAR(output.properties, '$.cbb_config')
453 FROM
454 `cr-buildbucket.chromeos.completed_builds_BETA` as c,
455 UNNEST(c.steps) AS s
456 WHERE
457 c.end_time > '%s' and
458 c.status != 'SUCCESS' and
459 s.name like '%%HWTest [sanity]%%' and
460 s.status = 'SUCCESS' and
461 JSON_EXTRACT_SCALAR(output.properties, '$.suite_scheduling') = 'True'
462 """
463 if global_config.GAE_TESTING:
464 since_date_str = '2019-05-19 20:00:00'
465 query_str += 'limit 10'
466 else:
467 since_date_str = since_date.strftime(time_converter.TIME_FORMAT)
468
469 logging.info('Getting passed builds since %s', since_date_str)
470 res = self.query(query_str % since_date_str)
471 res = _parse_bq_job_query(res)
472 if res is None:
473 return []
474
475 build_infos = []
476 for unibuild, stage_name, board, milestone, platform, build_config in res:
477 board = _parse_board(build_config, board)
478 if ast.literal_eval(unibuild):
479 model = _parse_model(stage_name)
480 else:
481 model = None
482
483 build_infos.append(
484 build_lib.BuildInfo(board, model, milestone, platform, build_config))
485
486 return build_infos
487
488
Xixuan Wu55d38c52019-05-21 14:26:23 -0700489def _parse_bq_job_query(json_input):
490 """Parse response from API bigquery.jobs.query.
491
492 Args:
493 json_input: a dict, representing jsons returned by query API.
494
495 Returns:
496 A 2D string matrix: [rows[columns]], or None if no result.
497 E.g. Input:
498 "rows": [
499 {
500 "f": [ # field
501 {
502 "v": 'foo1',
503 },
504 {
505 "v": 'foo2',
506 }
507 ]
508 }
509 {
510 "f": [ # field
511 {
512 "v": 'bar1',
513 },
514 {
515 "v": 'bar2',
516 }
517 ]
518 }
519 ]
520 => Output: [['foo1', 'foo2'], ['bar1', 'bar2']]
521 """
522 if 'rows' not in json_input:
523 return None
524
525 res = []
526 for r in json_input['rows']:
527 rc = []
528 for c in r['f']:
529 rc.append(c['v'])
530
531 res.append(rc)
532
533 return res
Xixuan Wuf856ff12019-05-21 14:09:38 -0700534
535
536def _parse_model(build_stage_name):
537 """Parse model name from the build stage name.
538
539 It's only used for HWTest Sanity stage. An example build_stage_name will
540 be 'HWTest [sanity] [whitetip]'.
541 Args:
542 build_stage_name: The stage name of a HWTest sanity stage, e.g.
543 "HWTest [sanity] [whitetip]".
544
545 Returns:
546 A model name, e.g. "whitetip" or None.
547 """
548 if 'HWTest [sanity]' not in build_stage_name:
549 return None
550
551 try:
552 model = build_stage_name.strip().split()[-1][1:-1]
553 if not model:
554 logging.warning('Cannot parse build stage name: %s', build_stage_name)
555 return None
556
557 return model
558 except IndexError:
559 logging.error('Cannot parse build stage name: %s', build_stage_name)
560 return None
561
562
563def _parse_board(build_config, board):
564 """Parse board from build_config if needed.
565
566 Board could be None for old release. See crbug.com/944981#c16.
567 This function can be removed once all release are newer than R74.
568
569 Args:
570 build_config: A string build config, e.g. reef-release.
571 board: A string board, e.g. reef.
572
573 Returns:
574 A string board if exist, or None.
575 """
576 if board is None:
577 match = re.match(r'(.+)-release', build_config)
578 if not match:
579 logging.debug('Cannot parse board from %s', build_config)
580 return None
581
582 return match.groups()[0]
583
584 return board