123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- #!/usr/bin/env python
- # Copyright 2017 gRPC authors.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- """Uploads RBE results to BigQuery"""
- import argparse
- import json
- import os
- import ssl
- import sys
- import urllib.error
- import urllib.parse
- import urllib.request
- import uuid
- gcp_utils_dir = os.path.abspath(
- os.path.join(os.path.dirname(__file__), '../../gcp/utils'))
- sys.path.append(gcp_utils_dir)
- import big_query_utils
- _DATASET_ID = 'jenkins_test_results'
- _DESCRIPTION = 'Test results from master RBE builds on Kokoro'
- # 365 days in milliseconds
- _EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000
- _PARTITION_TYPE = 'DAY'
- _PROJECT_ID = 'grpc-testing'
- _RESULTS_SCHEMA = [
- ('job_name', 'STRING', 'Name of Kokoro job'),
- ('build_id', 'INTEGER', 'Build ID of Kokoro job'),
- ('build_url', 'STRING', 'URL of Kokoro build'),
- ('test_target', 'STRING', 'Bazel target path'),
- ('test_class_name', 'STRING', 'Name of test class'),
- ('test_case', 'STRING', 'Name of test case'),
- ('result', 'STRING', 'Test or build result'),
- ('timestamp', 'TIMESTAMP', 'Timestamp of test run'),
- ('duration', 'FLOAT', 'Duration of the test run'),
- ]
- _TABLE_ID = 'rbe_test_results'
- def _get_api_key():
- """Returns string with API key to access ResultStore.
- Intended to be used in Kokoro environment."""
- api_key_directory = os.getenv('KOKORO_GFILE_DIR')
- api_key_file = os.path.join(api_key_directory, 'resultstore_api_key')
- assert os.path.isfile(api_key_file), 'Must add --api_key arg if not on ' \
- 'Kokoro or Kokoro environment is not set up properly.'
- with open(api_key_file, 'r') as f:
- return f.read().replace('\n', '')
- def _get_invocation_id():
- """Returns String of Bazel invocation ID. Intended to be used in
- Kokoro environment."""
- bazel_id_directory = os.getenv('KOKORO_ARTIFACTS_DIR')
- bazel_id_file = os.path.join(bazel_id_directory, 'bazel_invocation_ids')
- assert os.path.isfile(bazel_id_file), 'bazel_invocation_ids file, written ' \
- 'by RBE initialization script, expected but not found.'
- with open(bazel_id_file, 'r') as f:
- return f.read().replace('\n', '')
- def _parse_test_duration(duration_str):
- """Parse test duration string in '123.567s' format"""
- try:
- if duration_str.endswith('s'):
- duration_str = duration_str[:-1]
- return float(duration_str)
- except:
- return None
- def _upload_results_to_bq(rows):
- """Upload test results to a BQ table.
- Args:
- rows: A list of dictionaries containing data for each row to insert
- """
- bq = big_query_utils.create_big_query()
- big_query_utils.create_partitioned_table(bq,
- _PROJECT_ID,
- _DATASET_ID,
- _TABLE_ID,
- _RESULTS_SCHEMA,
- _DESCRIPTION,
- partition_type=_PARTITION_TYPE,
- expiration_ms=_EXPIRATION_MS)
- max_retries = 3
- for attempt in range(max_retries):
- if big_query_utils.insert_rows(bq, _PROJECT_ID, _DATASET_ID, _TABLE_ID,
- rows):
- break
- else:
- if attempt < max_retries - 1:
- print('Error uploading result to bigquery, will retry.')
- else:
- print(
- 'Error uploading result to bigquery, all attempts failed.')
- sys.exit(1)
- def _get_resultstore_data(api_key, invocation_id):
- """Returns dictionary of test results by querying ResultStore API.
- Args:
- api_key: String of ResultStore API key
- invocation_id: String of ResultStore invocation ID to results from
- """
- all_actions = []
- page_token = ''
- # ResultStore's API returns data on a limited number of tests. When we exceed
- # that limit, the 'nextPageToken' field is included in the request to get
- # subsequent data, so keep requesting until 'nextPageToken' field is omitted.
- while True:
- req = urllib.request.Request(
- url=
- 'https://resultstore.googleapis.com/v2/invocations/%s/targets/-/configuredTargets/-/actions?key=%s&pageToken=%s&fields=next_page_token,actions.id,actions.status_attributes,actions.timing,actions.test_action'
- % (invocation_id, api_key, page_token),
- headers={'Content-Type': 'application/json'})
- ctx_dict = {}
- if os.getenv("PYTHONHTTPSVERIFY") == "0":
- ctx = ssl.create_default_context()
- ctx.check_hostname = False
- ctx.verify_mode = ssl.CERT_NONE
- ctx_dict = {"context": ctx}
- raw_resp = urllib.request.urlopen(req, **ctx_dict).read()
- decoded_resp = raw_resp if isinstance(
- raw_resp, str) else raw_resp.decode('utf-8', 'ignore')
- results = json.loads(decoded_resp)
- all_actions.extend(results['actions'])
- if 'nextPageToken' not in results:
- break
- page_token = results['nextPageToken']
- return all_actions
- if __name__ == "__main__":
- # Arguments are necessary if running in a non-Kokoro environment.
- argp = argparse.ArgumentParser(
- description=
- 'Fetches results for given RBE invocation and uploads them to BigQuery table.'
- )
- argp.add_argument('--api_key',
- default='',
- type=str,
- help='The API key to read from ResultStore API')
- argp.add_argument('--invocation_id',
- default='',
- type=str,
- help='UUID of bazel invocation to fetch.')
- argp.add_argument('--bq_dump_file',
- default=None,
- type=str,
- help='Dump JSON data to file just before uploading')
- argp.add_argument('--resultstore_dump_file',
- default=None,
- type=str,
- help='Dump JSON data as received from ResultStore API')
- argp.add_argument('--skip_upload',
- default=False,
- action='store_const',
- const=True,
- help='Skip uploading to bigquery')
- args = argp.parse_args()
- api_key = args.api_key or _get_api_key()
- invocation_id = args.invocation_id or _get_invocation_id()
- resultstore_actions = _get_resultstore_data(api_key, invocation_id)
- if args.resultstore_dump_file:
- with open(args.resultstore_dump_file, 'w') as f:
- json.dump(resultstore_actions, f, indent=4, sort_keys=True)
- print(
- ('Dumped resultstore data to file %s' % args.resultstore_dump_file))
- # google.devtools.resultstore.v2.Action schema:
- # https://github.com/googleapis/googleapis/blob/master/google/devtools/resultstore/v2/action.proto
- bq_rows = []
- for index, action in enumerate(resultstore_actions):
- # Filter out non-test related data, such as build results.
- if 'testAction' not in action:
- continue
- # Some test results contain the fileProcessingErrors field, which indicates
- # an issue with parsing results individual test cases.
- if 'fileProcessingErrors' in action:
- test_cases = [{
- 'testCase': {
- 'caseName': str(action['id']['actionId']),
- }
- }]
- # Test timeouts have a different dictionary structure compared to pass and
- # fail results.
- elif action['statusAttributes']['status'] == 'TIMED_OUT':
- test_cases = [{
- 'testCase': {
- 'caseName': str(action['id']['actionId']),
- 'timedOut': True
- }
- }]
- # When RBE believes its infrastructure is failing, it will abort and
- # mark running tests as UNKNOWN. These infrastructure failures may be
- # related to our tests, so we should investigate if specific tests are
- # repeatedly being marked as UNKNOWN.
- elif action['statusAttributes']['status'] == 'UNKNOWN':
- test_cases = [{
- 'testCase': {
- 'caseName': str(action['id']['actionId']),
- 'unknown': True
- }
- }]
- # Take the timestamp from the previous action, which should be
- # a close approximation.
- action['timing'] = {
- 'startTime':
- resultstore_actions[index - 1]['timing']['startTime']
- }
- elif 'testSuite' not in action['testAction']:
- continue
- elif 'tests' not in action['testAction']['testSuite']:
- continue
- else:
- test_cases = []
- for tests_item in action['testAction']['testSuite']['tests']:
- test_cases += tests_item['testSuite']['tests']
- for test_case in test_cases:
- if any(s in test_case['testCase'] for s in ['errors', 'failures']):
- result = 'FAILED'
- elif 'timedOut' in test_case['testCase']:
- result = 'TIMEOUT'
- elif 'unknown' in test_case['testCase']:
- result = 'UNKNOWN'
- else:
- result = 'PASSED'
- try:
- bq_rows.append({
- 'insertId': str(uuid.uuid4()),
- 'json': {
- 'job_name':
- os.getenv('KOKORO_JOB_NAME'),
- 'build_id':
- os.getenv('KOKORO_BUILD_NUMBER'),
- 'build_url':
- 'https://source.cloud.google.com/results/invocations/%s'
- % invocation_id,
- 'test_target':
- action['id']['targetId'],
- 'test_class_name':
- test_case['testCase'].get('className', ''),
- 'test_case':
- test_case['testCase']['caseName'],
- 'result':
- result,
- 'timestamp':
- action['timing']['startTime'],
- 'duration':
- _parse_test_duration(action['timing']['duration']),
- }
- })
- except Exception as e:
- print(('Failed to parse test result. Error: %s' % str(e)))
- print((json.dumps(test_case, indent=4)))
- bq_rows.append({
- 'insertId': str(uuid.uuid4()),
- 'json': {
- 'job_name':
- os.getenv('KOKORO_JOB_NAME'),
- 'build_id':
- os.getenv('KOKORO_BUILD_NUMBER'),
- 'build_url':
- 'https://source.cloud.google.com/results/invocations/%s'
- % invocation_id,
- 'test_target':
- action['id']['targetId'],
- 'test_class_name':
- 'N/A',
- 'test_case':
- 'N/A',
- 'result':
- 'UNPARSEABLE',
- 'timestamp':
- 'N/A',
- }
- })
- if args.bq_dump_file:
- with open(args.bq_dump_file, 'w') as f:
- json.dump(bq_rows, f, indent=4, sort_keys=True)
- print(('Dumped BQ data to file %s' % args.bq_dump_file))
- if not args.skip_upload:
- # BigQuery sometimes fails with large uploads, so batch 1,000 rows at a time.
- MAX_ROWS = 1000
- for i in range(0, len(bq_rows), MAX_ROWS):
- _upload_results_to_bq(bq_rows[i:i + MAX_ROWS])
- else:
- print('Skipped upload to bigquery.')
|