xref: /aosp_15_r20/external/angle/scripts/generate_stats.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1*8975f5c5SAndroid Build Coastguard Worker#!/usr/bin/env vpython
2*8975f5c5SAndroid Build Coastguard Worker#
3*8975f5c5SAndroid Build Coastguard Worker# [VPYTHON:BEGIN]
4*8975f5c5SAndroid Build Coastguard Worker# wheel: <
5*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/google-auth-py2_py3"
6*8975f5c5SAndroid Build Coastguard Worker#   version: "version:1.2.1"
7*8975f5c5SAndroid Build Coastguard Worker# >
8*8975f5c5SAndroid Build Coastguard Worker#
9*8975f5c5SAndroid Build Coastguard Worker# wheel: <
10*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/pyasn1-py2_py3"
11*8975f5c5SAndroid Build Coastguard Worker#   version: "version:0.4.5"
12*8975f5c5SAndroid Build Coastguard Worker# >
13*8975f5c5SAndroid Build Coastguard Worker#
14*8975f5c5SAndroid Build Coastguard Worker# wheel: <
15*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/pyasn1_modules-py2_py3"
16*8975f5c5SAndroid Build Coastguard Worker#   version: "version:0.2.4"
17*8975f5c5SAndroid Build Coastguard Worker# >
18*8975f5c5SAndroid Build Coastguard Worker#
19*8975f5c5SAndroid Build Coastguard Worker# wheel: <
20*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/six"
21*8975f5c5SAndroid Build Coastguard Worker#   version: "version:1.10.0"
22*8975f5c5SAndroid Build Coastguard Worker# >
23*8975f5c5SAndroid Build Coastguard Worker#
24*8975f5c5SAndroid Build Coastguard Worker# wheel: <
25*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/cachetools-py2_py3"
26*8975f5c5SAndroid Build Coastguard Worker#   version: "version:2.0.1"
27*8975f5c5SAndroid Build Coastguard Worker# >
28*8975f5c5SAndroid Build Coastguard Worker# wheel: <
29*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/rsa-py2_py3"
30*8975f5c5SAndroid Build Coastguard Worker#   version: "version:4.0"
31*8975f5c5SAndroid Build Coastguard Worker# >
32*8975f5c5SAndroid Build Coastguard Worker#
33*8975f5c5SAndroid Build Coastguard Worker# wheel: <
34*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/requests"
35*8975f5c5SAndroid Build Coastguard Worker#   version: "version:2.13.0"
36*8975f5c5SAndroid Build Coastguard Worker# >
37*8975f5c5SAndroid Build Coastguard Worker#
38*8975f5c5SAndroid Build Coastguard Worker# wheel: <
39*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/google-api-python-client-py2_py3"
40*8975f5c5SAndroid Build Coastguard Worker#   version: "version:1.6.2"
41*8975f5c5SAndroid Build Coastguard Worker# >
42*8975f5c5SAndroid Build Coastguard Worker#
43*8975f5c5SAndroid Build Coastguard Worker# wheel: <
44*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/httplib2-py2_py3"
45*8975f5c5SAndroid Build Coastguard Worker#   version: "version:0.12.1"
46*8975f5c5SAndroid Build Coastguard Worker# >
47*8975f5c5SAndroid Build Coastguard Worker#
48*8975f5c5SAndroid Build Coastguard Worker# wheel: <
49*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/oauth2client-py2_py3"
50*8975f5c5SAndroid Build Coastguard Worker#   version: "version:3.0.0"
51*8975f5c5SAndroid Build Coastguard Worker# >
52*8975f5c5SAndroid Build Coastguard Worker#
53*8975f5c5SAndroid Build Coastguard Worker# wheel: <
54*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/uritemplate-py2_py3"
55*8975f5c5SAndroid Build Coastguard Worker#   version: "version:3.0.0"
56*8975f5c5SAndroid Build Coastguard Worker# >
57*8975f5c5SAndroid Build Coastguard Worker#
58*8975f5c5SAndroid Build Coastguard Worker# wheel: <
59*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/google-auth-oauthlib-py2_py3"
60*8975f5c5SAndroid Build Coastguard Worker#   version: "version:0.3.0"
61*8975f5c5SAndroid Build Coastguard Worker# >
62*8975f5c5SAndroid Build Coastguard Worker#
63*8975f5c5SAndroid Build Coastguard Worker# wheel: <
64*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/requests-oauthlib-py2_py3"
65*8975f5c5SAndroid Build Coastguard Worker#   version: "version:1.2.0"
66*8975f5c5SAndroid Build Coastguard Worker# >
67*8975f5c5SAndroid Build Coastguard Worker#
68*8975f5c5SAndroid Build Coastguard Worker# wheel: <
69*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/oauthlib-py2_py3"
70*8975f5c5SAndroid Build Coastguard Worker#   version: "version:3.0.1"
71*8975f5c5SAndroid Build Coastguard Worker# >
72*8975f5c5SAndroid Build Coastguard Worker#
73*8975f5c5SAndroid Build Coastguard Worker# wheel: <
74*8975f5c5SAndroid Build Coastguard Worker#   name: "infra/python/wheels/google-auth-httplib2-py2_py3"
75*8975f5c5SAndroid Build Coastguard Worker#   version: "version:0.0.3"
76*8975f5c5SAndroid Build Coastguard Worker# >
77*8975f5c5SAndroid Build Coastguard Worker# [VPYTHON:END]
78*8975f5c5SAndroid Build Coastguard Worker#
79*8975f5c5SAndroid Build Coastguard Worker# Copyright 2019 The ANGLE Project Authors. All rights reserved.
80*8975f5c5SAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
81*8975f5c5SAndroid Build Coastguard Worker# found in the LICENSE file.
82*8975f5c5SAndroid Build Coastguard Worker#
83*8975f5c5SAndroid Build Coastguard Worker# generate_deqp_stats.py:
84*8975f5c5SAndroid Build Coastguard Worker#   Checks output of deqp testers and generates stats using the GDocs API
85*8975f5c5SAndroid Build Coastguard Worker#
86*8975f5c5SAndroid Build Coastguard Worker# prerequirements:
87*8975f5c5SAndroid Build Coastguard Worker#   https://devsite.googleplex.com/sheets/api/quickstart/python
88*8975f5c5SAndroid Build Coastguard Worker#   Follow the quickstart guide.
89*8975f5c5SAndroid Build Coastguard Worker#
90*8975f5c5SAndroid Build Coastguard Worker# usage: generate_deqp_stats.py [-h] [--auth_path [AUTH_PATH]] [--spreadsheet [SPREADSHEET]]
91*8975f5c5SAndroid Build Coastguard Worker#                               [--verbosity [VERBOSITY]]
92*8975f5c5SAndroid Build Coastguard Worker#
93*8975f5c5SAndroid Build Coastguard Worker# optional arguments:
94*8975f5c5SAndroid Build Coastguard Worker#   -h, --help            show this help message and exit
95*8975f5c5SAndroid Build Coastguard Worker#   --auth_path [AUTH_PATH]
96*8975f5c5SAndroid Build Coastguard Worker#                         path to directory containing authorization data (credentials.json and
97*8975f5c5SAndroid Build Coastguard Worker#                         token.pickle). [default=<home>/.auth]
98*8975f5c5SAndroid Build Coastguard Worker#   --spreadsheet [SPREADSHEET]
99*8975f5c5SAndroid Build Coastguard Worker#                         ID of the spreadsheet to write stats to. [default
100*8975f5c5SAndroid Build Coastguard Worker#                         ='1D6Yh7dAPP-aYLbX3HHQD8WubJV9XPuxvkKowmn2qhIw']
101*8975f5c5SAndroid Build Coastguard Worker#   --verbosity [VERBOSITY]
102*8975f5c5SAndroid Build Coastguard Worker#                         Verbosity of output. Valid options are [DEBUG, INFO, WARNING, ERROR].
103*8975f5c5SAndroid Build Coastguard Worker#                         [default=INFO]
104*8975f5c5SAndroid Build Coastguard Worker
105*8975f5c5SAndroid Build Coastguard Workerimport argparse
106*8975f5c5SAndroid Build Coastguard Workerimport datetime
107*8975f5c5SAndroid Build Coastguard Workerimport logging
108*8975f5c5SAndroid Build Coastguard Workerimport os
109*8975f5c5SAndroid Build Coastguard Workerimport pickle
110*8975f5c5SAndroid Build Coastguard Workerimport re
111*8975f5c5SAndroid Build Coastguard Workerimport subprocess
112*8975f5c5SAndroid Build Coastguard Workerimport sys
113*8975f5c5SAndroid Build Coastguard Workerimport urllib
114*8975f5c5SAndroid Build Coastguard Workerfrom google.auth.transport.requests import Request
115*8975f5c5SAndroid Build Coastguard Workerfrom googleapiclient.discovery import build
116*8975f5c5SAndroid Build Coastguard Workerfrom google_auth_oauthlib.flow import InstalledAppFlow
117*8975f5c5SAndroid Build Coastguard Worker
118*8975f5c5SAndroid Build Coastguard Worker####################
119*8975f5c5SAndroid Build Coastguard Worker# Global Constants #
120*8975f5c5SAndroid Build Coastguard Worker####################
121*8975f5c5SAndroid Build Coastguard Worker
122*8975f5c5SAndroid Build Coastguard WorkerHOME_DIR = os.path.expanduser('~')
123*8975f5c5SAndroid Build Coastguard WorkerSCRIPT_DIR = sys.path[0]
124*8975f5c5SAndroid Build Coastguard WorkerROOT_DIR = os.path.abspath(os.path.join(SCRIPT_DIR, '..'))
125*8975f5c5SAndroid Build Coastguard Worker
126*8975f5c5SAndroid Build Coastguard WorkerLOGGER = logging.getLogger('generate_stats')
127*8975f5c5SAndroid Build Coastguard Worker
128*8975f5c5SAndroid Build Coastguard WorkerSCOPES = ['https://www.googleapis.com/auth/spreadsheets']
129*8975f5c5SAndroid Build Coastguard Worker
130*8975f5c5SAndroid Build Coastguard WorkerBOT_NAMES = [
131*8975f5c5SAndroid Build Coastguard Worker    'mac-angle-amd',
132*8975f5c5SAndroid Build Coastguard Worker    'mac-angle-intel',
133*8975f5c5SAndroid Build Coastguard Worker    'win10-angle-x64-nvidia',
134*8975f5c5SAndroid Build Coastguard Worker    'win10-angle-x64-intel',
135*8975f5c5SAndroid Build Coastguard Worker    'win7-angle-x64-nvidia',
136*8975f5c5SAndroid Build Coastguard Worker    'win7-angle-x86-amd',
137*8975f5c5SAndroid Build Coastguard Worker    'Linux FYI dEQP Release (Intel HD 630)',
138*8975f5c5SAndroid Build Coastguard Worker    'Linux FYI dEQP Release (NVIDIA)',
139*8975f5c5SAndroid Build Coastguard Worker    'Android FYI dEQP Release (Nexus 5X)',
140*8975f5c5SAndroid Build Coastguard Worker    'Android FYI 32 dEQP Vk Release (Pixel 2)',
141*8975f5c5SAndroid Build Coastguard Worker    'Android FYI 64 dEQP Vk Release (Pixel 2)',
142*8975f5c5SAndroid Build Coastguard Worker]
143*8975f5c5SAndroid Build Coastguard WorkerBOT_NAME_PREFIX = 'chromium/ci/'
144*8975f5c5SAndroid Build Coastguard WorkerBUILD_LINK_PREFIX = 'https://ci.chromium.org/p/chromium/builders/ci/'
145*8975f5c5SAndroid Build Coastguard Worker
146*8975f5c5SAndroid Build Coastguard WorkerREQUIRED_COLUMNS = ['build_link', 'time', 'date', 'revision', 'angle_revision', 'duplicate']
147*8975f5c5SAndroid Build Coastguard WorkerMAIN_RESULT_COLUMNS = ['Passed', 'Failed', 'Skipped', 'Not Supported', 'Exception', 'Crashed']
148*8975f5c5SAndroid Build Coastguard Worker
149*8975f5c5SAndroid Build Coastguard WorkerINFO_TAG = '*RESULT'
150*8975f5c5SAndroid Build Coastguard Worker
151*8975f5c5SAndroid Build Coastguard WorkerWORKAROUND_FORMATTING_ERROR_STRING = "Still waiting for the following processes to finish:"
152*8975f5c5SAndroid Build Coastguard Worker
153*8975f5c5SAndroid Build Coastguard Worker######################
154*8975f5c5SAndroid Build Coastguard Worker# Build Info Parsing #
155*8975f5c5SAndroid Build Coastguard Worker######################
156*8975f5c5SAndroid Build Coastguard Worker
157*8975f5c5SAndroid Build Coastguard Worker
158*8975f5c5SAndroid Build Coastguard Worker# Returns a struct with info about the latest successful build given a bot name. Info contains the
159*8975f5c5SAndroid Build Coastguard Worker# build_name, time, date, angle_revision, and chrome revision.
160*8975f5c5SAndroid Build Coastguard Worker# Uses: bb ls '<botname>' -n 1 -status success -p
161*8975f5c5SAndroid Build Coastguard Workerdef get_latest_success_build_info(bot_name):
162*8975f5c5SAndroid Build Coastguard Worker    bb = subprocess.Popen(['bb', 'ls', bot_name, '-n', '1', '-status', 'success', '-p'],
163*8975f5c5SAndroid Build Coastguard Worker                          stdout=subprocess.PIPE,
164*8975f5c5SAndroid Build Coastguard Worker                          stderr=subprocess.PIPE)
165*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Ran [bb ls '" + bot_name + "' -n 1 -status success -p]")
166*8975f5c5SAndroid Build Coastguard Worker    out, err = bb.communicate()
167*8975f5c5SAndroid Build Coastguard Worker    if err:
168*8975f5c5SAndroid Build Coastguard Worker        raise ValueError("Unexpected error from bb ls: '" + err + "'")
169*8975f5c5SAndroid Build Coastguard Worker    if not out:
170*8975f5c5SAndroid Build Coastguard Worker        raise ValueError("Unexpected empty result from bb ls of bot '" + bot_name + "'")
171*8975f5c5SAndroid Build Coastguard Worker    # Example output (line 1):
172*8975f5c5SAndroid Build Coastguard Worker    # ci.chromium.org/b/8915280275579996928 SUCCESS   'chromium/ci/Win10 FYI dEQP Release (NVIDIA)/26877'
173*8975f5c5SAndroid Build Coastguard Worker    # ...
174*8975f5c5SAndroid Build Coastguard Worker    if 'SUCCESS' not in out:
175*8975f5c5SAndroid Build Coastguard Worker        raise ValueError("Unexpected result from bb ls: '" + out + "'")
176*8975f5c5SAndroid Build Coastguard Worker    info = {}
177*8975f5c5SAndroid Build Coastguard Worker    for line in out.splitlines():
178*8975f5c5SAndroid Build Coastguard Worker        # The first line holds the build name
179*8975f5c5SAndroid Build Coastguard Worker        if 'build_name' not in info:
180*8975f5c5SAndroid Build Coastguard Worker            info['build_name'] = line.strip().split("'")[1]
181*8975f5c5SAndroid Build Coastguard Worker            # Remove the bot name and prepend the build link
182*8975f5c5SAndroid Build Coastguard Worker            info['build_link'] = BUILD_LINK_PREFIX + urllib.quote(
183*8975f5c5SAndroid Build Coastguard Worker                info['build_name'].split(BOT_NAME_PREFIX)[1])
184*8975f5c5SAndroid Build Coastguard Worker        if 'Created' in line:
185*8975f5c5SAndroid Build Coastguard Worker            # Example output of line with 'Created':
186*8975f5c5SAndroid Build Coastguard Worker            # ...
187*8975f5c5SAndroid Build Coastguard Worker            # Created today at 12:26:39, waited 2.056319s, started at 12:26:41, ran for 1h16m48.14963s, ended at 13:43:30
188*8975f5c5SAndroid Build Coastguard Worker            # ...
189*8975f5c5SAndroid Build Coastguard Worker            info['time'] = re.findall(r'[0-9]{1,2}:[0-9]{2}:[0-9]{2}', line.split(',', 1)[0])[0]
190*8975f5c5SAndroid Build Coastguard Worker            # Format today's date in US format so Sheets can read it properly
191*8975f5c5SAndroid Build Coastguard Worker            info['date'] = datetime.datetime.now().strftime('%m/%d/%y')
192*8975f5c5SAndroid Build Coastguard Worker        if 'got_angle_revision' in line:
193*8975f5c5SAndroid Build Coastguard Worker            # Example output of line with angle revision:
194*8975f5c5SAndroid Build Coastguard Worker            # ...
195*8975f5c5SAndroid Build Coastguard Worker            #   "parent_got_angle_revision": "8cbd321cafa92ffbf0495e6d0aeb9e1a97940fee",
196*8975f5c5SAndroid Build Coastguard Worker            # ...
197*8975f5c5SAndroid Build Coastguard Worker            info['angle_revision'] = filter(str.isalnum, line.split(':')[1])
198*8975f5c5SAndroid Build Coastguard Worker        if '"revision"' in line:
199*8975f5c5SAndroid Build Coastguard Worker            # Example output of line with chromium revision:
200*8975f5c5SAndroid Build Coastguard Worker            # ...
201*8975f5c5SAndroid Build Coastguard Worker            #   "revision": "3b68405a27f1f9590f83ae07757589dba862f141",
202*8975f5c5SAndroid Build Coastguard Worker            # ...
203*8975f5c5SAndroid Build Coastguard Worker            info['revision'] = filter(str.isalnum, line.split(':')[1])
204*8975f5c5SAndroid Build Coastguard Worker    if 'build_name' not in info:
205*8975f5c5SAndroid Build Coastguard Worker        raise ValueError("Could not find build_name from bot '" + bot_name + "'")
206*8975f5c5SAndroid Build Coastguard Worker    return info
207*8975f5c5SAndroid Build Coastguard Worker
208*8975f5c5SAndroid Build Coastguard Worker
209*8975f5c5SAndroid Build Coastguard Worker# Returns a list of step names that we're interested in given a build name. We are interested in
210*8975f5c5SAndroid Build Coastguard Worker# step names starting with 'angle_'. May raise an exception.
211*8975f5c5SAndroid Build Coastguard Worker# Uses: bb get '<build_name>' -steps
212*8975f5c5SAndroid Build Coastguard Workerdef get_step_names(build_name):
213*8975f5c5SAndroid Build Coastguard Worker    bb = subprocess.Popen(['bb', 'get', build_name, '-steps'],
214*8975f5c5SAndroid Build Coastguard Worker                          stdout=subprocess.PIPE,
215*8975f5c5SAndroid Build Coastguard Worker                          stderr=subprocess.PIPE)
216*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Ran [bb get '" + build_name + "' -steps]")
217*8975f5c5SAndroid Build Coastguard Worker    out, err = bb.communicate()
218*8975f5c5SAndroid Build Coastguard Worker    if err:
219*8975f5c5SAndroid Build Coastguard Worker        raise ValueError("Unexpected error from bb get: '" + err + "'")
220*8975f5c5SAndroid Build Coastguard Worker    step_names = []
221*8975f5c5SAndroid Build Coastguard Worker    # Example output (relevant lines to a single step):
222*8975f5c5SAndroid Build Coastguard Worker    # ...
223*8975f5c5SAndroid Build Coastguard Worker    # Step "angle_deqp_egl_vulkan_tests on (nvidia-quadro-p400-win10-stable) GPU on Windows on Windows-10"                                      SUCCESS   4m12s     Logs: "stdout", "chromium_swarming.summary", "Merge script log", "Flaky failure: dEQP.EGL&#x2f;info_version (status CRASH,SUCCESS)", "step_metadata"
224*8975f5c5SAndroid Build Coastguard Worker    # Run on OS: 'Windows-10'<br>Max shard duration: 0:04:07.309848 (shard \#1)<br>Min shard duration: 0:02:26.402128 (shard \#0)<br/>flaky failures [ignored]:<br/>dEQP.EGL/info\_version<br/>
225*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #0 isolated out](https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=9a5999a59d332e55f54f495948d0c9f959e60ed2)
226*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #0 (128.3 sec)](https://chromium-swarm.appspot.com/user/task/446903ae365b8110)
227*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #1 isolated out](https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=d71e1bdd91dee61b536b4057a9222e642bd3809f)
228*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #1 (229.3 sec)](https://chromium-swarm.appspot.com/user/task/446903b7b0d90210)
229*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #2 isolated out](https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=ac9ba85b1cca77774061b87335c077980e1eef85)
230*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #2 (144.5 sec)](https://chromium-swarm.appspot.com/user/task/446903c18e15a010)
231*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #3 isolated out](https://isolateserver.appspot.com/browse?namespace=default-gzip&hash=976d586386864abecf53915fbac3e085f672e30f)
232*8975f5c5SAndroid Build Coastguard Worker    #  * [shard #3 (138.4 sec)](https://chromium-swarm.appspot.com/user/task/446903cc8da0ad10)
233*8975f5c5SAndroid Build Coastguard Worker    # ...
234*8975f5c5SAndroid Build Coastguard Worker    for line in out.splitlines():
235*8975f5c5SAndroid Build Coastguard Worker        if 'Step "angle_' not in line:
236*8975f5c5SAndroid Build Coastguard Worker            continue
237*8975f5c5SAndroid Build Coastguard Worker        step_names.append(line.split('"')[1])
238*8975f5c5SAndroid Build Coastguard Worker    return step_names
239*8975f5c5SAndroid Build Coastguard Worker
240*8975f5c5SAndroid Build Coastguard Worker
241*8975f5c5SAndroid Build Coastguard Worker# Performs some heuristic validation of the step_info struct returned from a single step log.
242*8975f5c5SAndroid Build Coastguard Worker# Returns True if valid, False if invalid. May write to stderr
243*8975f5c5SAndroid Build Coastguard Workerdef validate_step_info(step_info, build_name, step_name):
244*8975f5c5SAndroid Build Coastguard Worker    print_name = "'" + build_name + "': '" + step_name + "'"
245*8975f5c5SAndroid Build Coastguard Worker    if not step_info:
246*8975f5c5SAndroid Build Coastguard Worker        LOGGER.warning('Step info empty for ' + print_name + '\n')
247*8975f5c5SAndroid Build Coastguard Worker        return False
248*8975f5c5SAndroid Build Coastguard Worker
249*8975f5c5SAndroid Build Coastguard Worker    if 'Total' in step_info:
250*8975f5c5SAndroid Build Coastguard Worker        partial_sum_keys = MAIN_RESULT_COLUMNS
251*8975f5c5SAndroid Build Coastguard Worker        partial_sum_values = [int(step_info[key]) for key in partial_sum_keys if key in step_info]
252*8975f5c5SAndroid Build Coastguard Worker        computed_total = sum(partial_sum_values)
253*8975f5c5SAndroid Build Coastguard Worker        if step_info['Total'] != computed_total:
254*8975f5c5SAndroid Build Coastguard Worker            LOGGER.warning('Step info does not sum to total for ' + print_name + ' | Total: ' +
255*8975f5c5SAndroid Build Coastguard Worker                           str(step_info['Total']) + ' - Computed total: ' + str(computed_total) +
256*8975f5c5SAndroid Build Coastguard Worker                           '\n')
257*8975f5c5SAndroid Build Coastguard Worker    return True
258*8975f5c5SAndroid Build Coastguard Worker
259*8975f5c5SAndroid Build Coastguard Worker
260*8975f5c5SAndroid Build Coastguard Worker# Returns a struct containing parsed info from a given step log. The info is parsed by looking for
261*8975f5c5SAndroid Build Coastguard Worker# lines with the following format in stdout:
262*8975f5c5SAndroid Build Coastguard Worker# '[TESTSTATS]: <key>: <value>''
263*8975f5c5SAndroid Build Coastguard Worker# May write to stderr
264*8975f5c5SAndroid Build Coastguard Worker# Uses: bb log '<build_name>' '<step_name>'
265*8975f5c5SAndroid Build Coastguard Workerdef get_step_info(build_name, step_name):
266*8975f5c5SAndroid Build Coastguard Worker    bb = subprocess.Popen(['bb', 'log', build_name, step_name],
267*8975f5c5SAndroid Build Coastguard Worker                          stdout=subprocess.PIPE,
268*8975f5c5SAndroid Build Coastguard Worker                          stderr=subprocess.PIPE)
269*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Ran [bb log '" + build_name + "' '" + step_name + "']")
270*8975f5c5SAndroid Build Coastguard Worker    out, err = bb.communicate()
271*8975f5c5SAndroid Build Coastguard Worker    if err:
272*8975f5c5SAndroid Build Coastguard Worker        LOGGER.warning("Unexpected error from bb log '" + build_name + "' '" + step_name + "': '" +
273*8975f5c5SAndroid Build Coastguard Worker                       err + "'")
274*8975f5c5SAndroid Build Coastguard Worker        return None
275*8975f5c5SAndroid Build Coastguard Worker    step_info = {}
276*8975f5c5SAndroid Build Coastguard Worker    # Example output (relevant lines of stdout):
277*8975f5c5SAndroid Build Coastguard Worker    # ...
278*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Total: 155
279*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Passed: 11
280*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Failed: 0
281*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Skipped: 12
282*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Not Supported: 132
283*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Exception: 0
284*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Crashed: 0
285*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: Unexpected Passed: 12
286*8975f5c5SAndroid Build Coastguard Worker    # ...
287*8975f5c5SAndroid Build Coastguard Worker    append_errors = []
288*8975f5c5SAndroid Build Coastguard Worker    # Hacky workaround to fix issue where messages are dropped into the middle of lines by another
289*8975f5c5SAndroid Build Coastguard Worker    # process:
290*8975f5c5SAndroid Build Coastguard Worker    # eg.
291*8975f5c5SAndroid Build Coastguard Worker    # *RESULT: <start_of_result>Still waiting for the following processes to finish:
292*8975f5c5SAndroid Build Coastguard Worker    # "c:\b\s\w\ir\out\Release\angle_deqp_gles3_tests.exe" --deqp-egl-display-type=angle-vulkan --gtest_flagfile="c:\b\s\w\itlcgdrz\scoped_dir7104_364984996\8ad93729-f679-406d-973b-06b9d1bf32de.tmp" --single-process-tests --test-launcher-batch-limit=400 --test-launcher-output="c:\b\s\w\itlcgdrz\7104_437216092\test_results.xml" --test-launcher-summary-output="c:\b\s\w\iosuk8ai\output.json"
293*8975f5c5SAndroid Build Coastguard Worker    # <end_of_result>
294*8975f5c5SAndroid Build Coastguard Worker    #
295*8975f5c5SAndroid Build Coastguard Worker    # Removes the message and skips the line following it, and then appends the <start_of_result>
296*8975f5c5SAndroid Build Coastguard Worker    # and <end_of_result> back together
297*8975f5c5SAndroid Build Coastguard Worker    workaround_prev_line = ""
298*8975f5c5SAndroid Build Coastguard Worker    workaround_prev_line_count = 0
299*8975f5c5SAndroid Build Coastguard Worker    for line in out.splitlines():
300*8975f5c5SAndroid Build Coastguard Worker        # Skip lines if the workaround still has lines to skip
301*8975f5c5SAndroid Build Coastguard Worker        if workaround_prev_line_count > 0:
302*8975f5c5SAndroid Build Coastguard Worker            workaround_prev_line_count -= 1
303*8975f5c5SAndroid Build Coastguard Worker            continue
304*8975f5c5SAndroid Build Coastguard Worker        # If there are no more lines to skip and there is a previous <start_of_result> to append,
305*8975f5c5SAndroid Build Coastguard Worker        # append it and finish the workaround
306*8975f5c5SAndroid Build Coastguard Worker        elif workaround_prev_line != "":
307*8975f5c5SAndroid Build Coastguard Worker            line = workaround_prev_line + line
308*8975f5c5SAndroid Build Coastguard Worker            workaround_prev_line = ""
309*8975f5c5SAndroid Build Coastguard Worker            workaround_prev_line_count = 0
310*8975f5c5SAndroid Build Coastguard Worker            LOGGER.debug("Formatting error workaround rebuilt line as: '" + line + "'\n")
311*8975f5c5SAndroid Build Coastguard Worker
312*8975f5c5SAndroid Build Coastguard Worker        if INFO_TAG not in line:
313*8975f5c5SAndroid Build Coastguard Worker            continue
314*8975f5c5SAndroid Build Coastguard Worker
315*8975f5c5SAndroid Build Coastguard Worker        # When the workaround string is detected, start the workaround with 1 line to skip and save
316*8975f5c5SAndroid Build Coastguard Worker        # the <start_of_result>, but continue the loop until the workaround is finished
317*8975f5c5SAndroid Build Coastguard Worker        if WORKAROUND_FORMATTING_ERROR_STRING in line:
318*8975f5c5SAndroid Build Coastguard Worker            workaround_prev_line = line.split(WORKAROUND_FORMATTING_ERROR_STRING)[0]
319*8975f5c5SAndroid Build Coastguard Worker            workaround_prev_line_count = 1
320*8975f5c5SAndroid Build Coastguard Worker            continue
321*8975f5c5SAndroid Build Coastguard Worker
322*8975f5c5SAndroid Build Coastguard Worker        found_stat = True
323*8975f5c5SAndroid Build Coastguard Worker        line_columns = line.split(INFO_TAG, 1)[1].split(':')
324*8975f5c5SAndroid Build Coastguard Worker        if len(line_columns) is not 3:
325*8975f5c5SAndroid Build Coastguard Worker            LOGGER.warning("Line improperly formatted: '" + line + "'\n")
326*8975f5c5SAndroid Build Coastguard Worker            continue
327*8975f5c5SAndroid Build Coastguard Worker        key = line_columns[1].strip()
328*8975f5c5SAndroid Build Coastguard Worker        # If the value is clearly an int, sum it. Otherwise, concatenate it as a string
329*8975f5c5SAndroid Build Coastguard Worker        isInt = False
330*8975f5c5SAndroid Build Coastguard Worker        intVal = 0
331*8975f5c5SAndroid Build Coastguard Worker        try:
332*8975f5c5SAndroid Build Coastguard Worker            intVal = int(line_columns[2])
333*8975f5c5SAndroid Build Coastguard Worker            if intVal is not None:
334*8975f5c5SAndroid Build Coastguard Worker                isInt = True
335*8975f5c5SAndroid Build Coastguard Worker        except Exception as error:
336*8975f5c5SAndroid Build Coastguard Worker            isInt = False
337*8975f5c5SAndroid Build Coastguard Worker
338*8975f5c5SAndroid Build Coastguard Worker        if isInt:
339*8975f5c5SAndroid Build Coastguard Worker            if key not in step_info:
340*8975f5c5SAndroid Build Coastguard Worker                step_info[key] = 0
341*8975f5c5SAndroid Build Coastguard Worker            step_info[key] += intVal
342*8975f5c5SAndroid Build Coastguard Worker        else:
343*8975f5c5SAndroid Build Coastguard Worker            if key not in step_info:
344*8975f5c5SAndroid Build Coastguard Worker                step_info[key] = line_columns[2].strip()
345*8975f5c5SAndroid Build Coastguard Worker            else:
346*8975f5c5SAndroid Build Coastguard Worker                append_string = '\n' + line_columns[2].strip()
347*8975f5c5SAndroid Build Coastguard Worker                # Sheets has a limit of 50000 characters per cell, so make sure to stop appending
348*8975f5c5SAndroid Build Coastguard Worker                # below this limit
349*8975f5c5SAndroid Build Coastguard Worker                if len(step_info[key]) + len(append_string) < 50000:
350*8975f5c5SAndroid Build Coastguard Worker                    step_info[key] += append_string
351*8975f5c5SAndroid Build Coastguard Worker                else:
352*8975f5c5SAndroid Build Coastguard Worker                    if key not in append_errors:
353*8975f5c5SAndroid Build Coastguard Worker                        append_errors.append(key)
354*8975f5c5SAndroid Build Coastguard Worker                        LOGGER.warning("Too many characters in column '" + key +
355*8975f5c5SAndroid Build Coastguard Worker                                       "'. Output capped.")
356*8975f5c5SAndroid Build Coastguard Worker    return step_info
357*8975f5c5SAndroid Build Coastguard Worker
358*8975f5c5SAndroid Build Coastguard Worker
359*8975f5c5SAndroid Build Coastguard Worker# Returns the info for each step run on a given bot_name.
360*8975f5c5SAndroid Build Coastguard Workerdef get_bot_info(bot_name):
361*8975f5c5SAndroid Build Coastguard Worker    info = get_latest_success_build_info(bot_name)
362*8975f5c5SAndroid Build Coastguard Worker    info['step_names'] = get_step_names(info['build_name'])
363*8975f5c5SAndroid Build Coastguard Worker    broken_step_names = []
364*8975f5c5SAndroid Build Coastguard Worker    for step_name in info['step_names']:
365*8975f5c5SAndroid Build Coastguard Worker        LOGGER.info("Parsing step '" + step_name + "'...")
366*8975f5c5SAndroid Build Coastguard Worker        step_info = get_step_info(info['build_name'], step_name)
367*8975f5c5SAndroid Build Coastguard Worker        if validate_step_info(step_info, info['build_name'], step_name):
368*8975f5c5SAndroid Build Coastguard Worker            info[step_name] = step_info
369*8975f5c5SAndroid Build Coastguard Worker        else:
370*8975f5c5SAndroid Build Coastguard Worker            broken_step_names += step_name
371*8975f5c5SAndroid Build Coastguard Worker    for step_name in broken_step_names:
372*8975f5c5SAndroid Build Coastguard Worker        info['step_names'].remove(step_name)
373*8975f5c5SAndroid Build Coastguard Worker    return info
374*8975f5c5SAndroid Build Coastguard Worker
375*8975f5c5SAndroid Build Coastguard Worker
376*8975f5c5SAndroid Build Coastguard Worker#####################
377*8975f5c5SAndroid Build Coastguard Worker# Sheets Formatting #
378*8975f5c5SAndroid Build Coastguard Worker#####################
379*8975f5c5SAndroid Build Coastguard Worker
380*8975f5c5SAndroid Build Coastguard Worker
381*8975f5c5SAndroid Build Coastguard Worker# Get an individual spreadsheet based on the spreadsheet id. Returns the result of
382*8975f5c5SAndroid Build Coastguard Worker# spreadsheets.get(), or throws an exception if the sheet could not open.
383*8975f5c5SAndroid Build Coastguard Workerdef get_spreadsheet(service, spreadsheet_id):
384*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Called [spreadsheets.get(spreadsheetId='" + spreadsheet_id + "')]")
385*8975f5c5SAndroid Build Coastguard Worker    request = service.get(spreadsheetId=spreadsheet_id)
386*8975f5c5SAndroid Build Coastguard Worker    spreadsheet = request.execute()
387*8975f5c5SAndroid Build Coastguard Worker    if not spreadsheet:
388*8975f5c5SAndroid Build Coastguard Worker        raise Exception("Did not open spreadsheet '" + spreadsheet_id + "'")
389*8975f5c5SAndroid Build Coastguard Worker    return spreadsheet
390*8975f5c5SAndroid Build Coastguard Worker
391*8975f5c5SAndroid Build Coastguard Worker
392*8975f5c5SAndroid Build Coastguard Worker# Returns a nicely formatted string based on the bot_name and step_name
393*8975f5c5SAndroid Build Coastguard Workerdef format_sheet_name(bot_name, step_name):
394*8975f5c5SAndroid Build Coastguard Worker    # Some tokens should be ignored for readability in the name
395*8975f5c5SAndroid Build Coastguard Worker    unneccesary_tokens = ['FYI', 'Release', 'Vk', 'dEQP', '(', ')']
396*8975f5c5SAndroid Build Coastguard Worker    for token in unneccesary_tokens:
397*8975f5c5SAndroid Build Coastguard Worker        bot_name = bot_name.replace(token, '')
398*8975f5c5SAndroid Build Coastguard Worker    bot_name = ' '.join(bot_name.strip().split())  # Remove extra spaces
399*8975f5c5SAndroid Build Coastguard Worker    step_name = re.findall(r'angle\w*', step_name)[0]  # Separate test name
400*8975f5c5SAndroid Build Coastguard Worker    # Test names are formatted as 'angle_deqp_<frontend>_<backend>_tests'
401*8975f5c5SAndroid Build Coastguard Worker    new_step_name = ''
402*8975f5c5SAndroid Build Coastguard Worker    # Put the frontend first
403*8975f5c5SAndroid Build Coastguard Worker    if '_egl_' in step_name:
404*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_egl_', '_')
405*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' EGL'
406*8975f5c5SAndroid Build Coastguard Worker    if '_gles2_' in step_name:
407*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_gles2_', '_')
408*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' GLES 2.0 '
409*8975f5c5SAndroid Build Coastguard Worker    if '_gles3_' in step_name:
410*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_gles3_', '_')
411*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' GLES 3.0 '
412*8975f5c5SAndroid Build Coastguard Worker    if '_gles31_' in step_name:
413*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_gles31_', '_')
414*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' GLES 3.1 '
415*8975f5c5SAndroid Build Coastguard Worker    # Put the backend second
416*8975f5c5SAndroid Build Coastguard Worker    if '_d3d9_' in step_name:
417*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_d3d9_', '_')
418*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' D3D9 '
419*8975f5c5SAndroid Build Coastguard Worker    if '_d3d11' in step_name:
420*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_d3d11_', '_')
421*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' D3D11 '
422*8975f5c5SAndroid Build Coastguard Worker    if '_gl_' in step_name:
423*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_gl_', '_')
424*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' Desktop OpenGL '
425*8975f5c5SAndroid Build Coastguard Worker    if '_gles_' in step_name:
426*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_gles_', '_')
427*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' OpenGLES '
428*8975f5c5SAndroid Build Coastguard Worker    if '_vulkan_' in step_name:
429*8975f5c5SAndroid Build Coastguard Worker        step_name = step_name.replace('_vulkan_', '_')
430*8975f5c5SAndroid Build Coastguard Worker        new_step_name += ' Vulkan '
431*8975f5c5SAndroid Build Coastguard Worker    # Add any remaining keywords from the step name into the formatted name (formatted nicely)
432*8975f5c5SAndroid Build Coastguard Worker    step_name = step_name.replace('angle_', '_')
433*8975f5c5SAndroid Build Coastguard Worker    step_name = step_name.replace('_deqp_', '_')
434*8975f5c5SAndroid Build Coastguard Worker    step_name = step_name.replace('_tests', '_')
435*8975f5c5SAndroid Build Coastguard Worker    step_name = step_name.replace('_', ' ').strip()
436*8975f5c5SAndroid Build Coastguard Worker    new_step_name += ' ' + step_name
437*8975f5c5SAndroid Build Coastguard Worker    new_step_name = ' '.join(new_step_name.strip().split())  # Remove extra spaces
438*8975f5c5SAndroid Build Coastguard Worker    return new_step_name + ' ' + bot_name
439*8975f5c5SAndroid Build Coastguard Worker
440*8975f5c5SAndroid Build Coastguard Worker
441*8975f5c5SAndroid Build Coastguard Worker# Returns the full list of sheet names that should be populated based on the info struct
442*8975f5c5SAndroid Build Coastguard Workerdef get_sheet_names(info):
443*8975f5c5SAndroid Build Coastguard Worker    sheet_names = []
444*8975f5c5SAndroid Build Coastguard Worker    for bot_name in info:
445*8975f5c5SAndroid Build Coastguard Worker        for step_name in info[bot_name]['step_names']:
446*8975f5c5SAndroid Build Coastguard Worker            sheet_name = format_sheet_name(bot_name, step_name)
447*8975f5c5SAndroid Build Coastguard Worker            sheet_names.append(sheet_name)
448*8975f5c5SAndroid Build Coastguard Worker    return sheet_names
449*8975f5c5SAndroid Build Coastguard Worker
450*8975f5c5SAndroid Build Coastguard Worker
451*8975f5c5SAndroid Build Coastguard Worker# Returns True if the sheet is found in the spreadsheets object
452*8975f5c5SAndroid Build Coastguard Workerdef sheet_exists(spreadsheet, step_name):
453*8975f5c5SAndroid Build Coastguard Worker    for sheet in spreadsheet['sheets']:
454*8975f5c5SAndroid Build Coastguard Worker        if sheet['properties']['title'] == step_name:
455*8975f5c5SAndroid Build Coastguard Worker            return True
456*8975f5c5SAndroid Build Coastguard Worker    return False
457*8975f5c5SAndroid Build Coastguard Worker
458*8975f5c5SAndroid Build Coastguard Worker
459*8975f5c5SAndroid Build Coastguard Worker# Validates the spreadsheets object against the list of sheet names which should appear. Returns a
460*8975f5c5SAndroid Build Coastguard Worker# list of sheets that need creation.
461*8975f5c5SAndroid Build Coastguard Workerdef validate_sheets(spreadsheet, sheet_names):
462*8975f5c5SAndroid Build Coastguard Worker    create_sheets = []
463*8975f5c5SAndroid Build Coastguard Worker    for sheet_name in sheet_names:
464*8975f5c5SAndroid Build Coastguard Worker        if not sheet_exists(spreadsheet, sheet_name):
465*8975f5c5SAndroid Build Coastguard Worker            create_sheets.append(sheet_name)
466*8975f5c5SAndroid Build Coastguard Worker    return create_sheets
467*8975f5c5SAndroid Build Coastguard Worker
468*8975f5c5SAndroid Build Coastguard Worker
469*8975f5c5SAndroid Build Coastguard Worker# Performs a batch update with a given service, spreadsheet id, and list <object(Request)> of
470*8975f5c5SAndroid Build Coastguard Worker# updates to do.
471*8975f5c5SAndroid Build Coastguard Workerdef batch_update(service, spreadsheet_id, updates):
472*8975f5c5SAndroid Build Coastguard Worker    batch_update_request_body = {
473*8975f5c5SAndroid Build Coastguard Worker        'requests': updates,
474*8975f5c5SAndroid Build Coastguard Worker    }
475*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Called [spreadsheets.batchUpdate(spreadsheetId='" + spreadsheet_id + "', body=" +
476*8975f5c5SAndroid Build Coastguard Worker                 str(batch_update_request_body) + ')]')
477*8975f5c5SAndroid Build Coastguard Worker    request = service.batchUpdate(spreadsheetId=spreadsheet_id, body=batch_update_request_body)
478*8975f5c5SAndroid Build Coastguard Worker    request.execute()
479*8975f5c5SAndroid Build Coastguard Worker
480*8975f5c5SAndroid Build Coastguard Worker
481*8975f5c5SAndroid Build Coastguard Worker# Creates sheets given a service and spreadsheed id based on a list of sheet names input
482*8975f5c5SAndroid Build Coastguard Workerdef create_sheets(service, spreadsheet_id, sheet_names):
483*8975f5c5SAndroid Build Coastguard Worker    updates = [{'addSheet': {'properties': {'title': sheet_name,}}} for sheet_name in sheet_names]
484*8975f5c5SAndroid Build Coastguard Worker    batch_update(service, spreadsheet_id, updates)
485*8975f5c5SAndroid Build Coastguard Worker
486*8975f5c5SAndroid Build Coastguard Worker
487*8975f5c5SAndroid Build Coastguard Worker# Calls a values().batchGet() on the service to find the list of column names from each sheet in
488*8975f5c5SAndroid Build Coastguard Worker# sheet_names. Returns a dictionary with one list per sheet_name.
489*8975f5c5SAndroid Build Coastguard Workerdef get_headers(service, spreadsheet_id, sheet_names):
490*8975f5c5SAndroid Build Coastguard Worker    header_ranges = [sheet_name + '!A1:Z' for sheet_name in sheet_names]
491*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Called [spreadsheets.values().batchGet(spreadsheetId='" + spreadsheet_id +
492*8975f5c5SAndroid Build Coastguard Worker                 ', ranges=' + str(header_ranges) + "')]")
493*8975f5c5SAndroid Build Coastguard Worker    request = service.values().batchGet(spreadsheetId=spreadsheet_id, ranges=header_ranges)
494*8975f5c5SAndroid Build Coastguard Worker    response = request.execute()
495*8975f5c5SAndroid Build Coastguard Worker    headers = {}
496*8975f5c5SAndroid Build Coastguard Worker    for k, sheet_name in enumerate(sheet_names):
497*8975f5c5SAndroid Build Coastguard Worker        if 'values' in response['valueRanges'][k]:
498*8975f5c5SAndroid Build Coastguard Worker            # Headers are in the first row of values
499*8975f5c5SAndroid Build Coastguard Worker            headers[sheet_name] = response['valueRanges'][k]['values'][0]
500*8975f5c5SAndroid Build Coastguard Worker        else:
501*8975f5c5SAndroid Build Coastguard Worker            headers[sheet_name] = []
502*8975f5c5SAndroid Build Coastguard Worker    return headers
503*8975f5c5SAndroid Build Coastguard Worker
504*8975f5c5SAndroid Build Coastguard Worker
505*8975f5c5SAndroid Build Coastguard Worker# Calls values().batchUpdate() with supplied list of data <object(ValueRange)> to update on the
506*8975f5c5SAndroid Build Coastguard Worker# service.
507*8975f5c5SAndroid Build Coastguard Workerdef batch_update_values(service, spreadsheet_id, data):
508*8975f5c5SAndroid Build Coastguard Worker    batch_update_values_request_body = {
509*8975f5c5SAndroid Build Coastguard Worker        'valueInputOption': 'USER_ENTERED',  # Helps with formatting of dates
510*8975f5c5SAndroid Build Coastguard Worker        'data': data,
511*8975f5c5SAndroid Build Coastguard Worker    }
512*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Called [spreadsheets.values().batchUpdate(spreadsheetId='" + spreadsheet_id +
513*8975f5c5SAndroid Build Coastguard Worker                 "', body=" + str(batch_update_values_request_body) + ')]')
514*8975f5c5SAndroid Build Coastguard Worker    request = service.values().batchUpdate(
515*8975f5c5SAndroid Build Coastguard Worker        spreadsheetId=spreadsheet_id, body=batch_update_values_request_body)
516*8975f5c5SAndroid Build Coastguard Worker    request.execute()
517*8975f5c5SAndroid Build Coastguard Worker
518*8975f5c5SAndroid Build Coastguard Worker
519*8975f5c5SAndroid Build Coastguard Worker# Get the sheetId of a sheet based on its name
520*8975f5c5SAndroid Build Coastguard Workerdef get_sheet_id(spreadsheet, sheet_name):
521*8975f5c5SAndroid Build Coastguard Worker    for sheet in spreadsheet['sheets']:
522*8975f5c5SAndroid Build Coastguard Worker        if sheet['properties']['title'] == sheet_name:
523*8975f5c5SAndroid Build Coastguard Worker            return sheet['properties']['sheetId']
524*8975f5c5SAndroid Build Coastguard Worker    return -1
525*8975f5c5SAndroid Build Coastguard Worker
526*8975f5c5SAndroid Build Coastguard Worker
527*8975f5c5SAndroid Build Coastguard Worker# Update the filters on sheets with a 'duplicate' column. Filter out any duplicate rows
528*8975f5c5SAndroid Build Coastguard Workerdef update_filters(service, spreadsheet_id, headers, info, spreadsheet):
529*8975f5c5SAndroid Build Coastguard Worker    updates = []
530*8975f5c5SAndroid Build Coastguard Worker    for bot_name in info:
531*8975f5c5SAndroid Build Coastguard Worker        for step_name in info[bot_name]['step_names']:
532*8975f5c5SAndroid Build Coastguard Worker            sheet_name = format_sheet_name(bot_name, step_name)
533*8975f5c5SAndroid Build Coastguard Worker            duplicate_found = 'duplicate' in headers[sheet_name]
534*8975f5c5SAndroid Build Coastguard Worker            if duplicate_found:
535*8975f5c5SAndroid Build Coastguard Worker                sheet_id = get_sheet_id(spreadsheet, sheet_name)
536*8975f5c5SAndroid Build Coastguard Worker                if sheet_id > -1:
537*8975f5c5SAndroid Build Coastguard Worker                    updates.append({
538*8975f5c5SAndroid Build Coastguard Worker                        "setBasicFilter": {
539*8975f5c5SAndroid Build Coastguard Worker                            "filter": {
540*8975f5c5SAndroid Build Coastguard Worker                                "range": {
541*8975f5c5SAndroid Build Coastguard Worker                                    "sheetId": sheet_id,
542*8975f5c5SAndroid Build Coastguard Worker                                    "startColumnIndex": 0,
543*8975f5c5SAndroid Build Coastguard Worker                                    "endColumnIndex": len(headers[sheet_name])
544*8975f5c5SAndroid Build Coastguard Worker                                },
545*8975f5c5SAndroid Build Coastguard Worker                                "sortSpecs": [{
546*8975f5c5SAndroid Build Coastguard Worker                                    "dimensionIndex": headers[sheet_name].index('date'),
547*8975f5c5SAndroid Build Coastguard Worker                                    "sortOrder": "ASCENDING"
548*8975f5c5SAndroid Build Coastguard Worker                                }],
549*8975f5c5SAndroid Build Coastguard Worker                                "criteria": {
550*8975f5c5SAndroid Build Coastguard Worker                                    str(headers[sheet_name].index('duplicate')): {
551*8975f5c5SAndroid Build Coastguard Worker                                        "hiddenValues":
552*8975f5c5SAndroid Build Coastguard Worker                                            ["1"]  # Hide rows when duplicate is 1 (true)
553*8975f5c5SAndroid Build Coastguard Worker                                    }
554*8975f5c5SAndroid Build Coastguard Worker                                }
555*8975f5c5SAndroid Build Coastguard Worker                            }
556*8975f5c5SAndroid Build Coastguard Worker                        }
557*8975f5c5SAndroid Build Coastguard Worker                    })
558*8975f5c5SAndroid Build Coastguard Worker    if updates:
559*8975f5c5SAndroid Build Coastguard Worker        LOGGER.info('Updating sheet filters...')
560*8975f5c5SAndroid Build Coastguard Worker        batch_update(service, spreadsheet_id, updates)
561*8975f5c5SAndroid Build Coastguard Worker
562*8975f5c5SAndroid Build Coastguard Worker# Populates the headers with any missing/desired rows based on the info struct, and calls
563*8975f5c5SAndroid Build Coastguard Worker# batch update to update the corresponding sheets if necessary.
564*8975f5c5SAndroid Build Coastguard Workerdef update_headers(service, spreadsheet_id, headers, info):
565*8975f5c5SAndroid Build Coastguard Worker    data = []
566*8975f5c5SAndroid Build Coastguard Worker    sheet_names = []
567*8975f5c5SAndroid Build Coastguard Worker    for bot_name in info:
568*8975f5c5SAndroid Build Coastguard Worker        for step_name in info[bot_name]['step_names']:
569*8975f5c5SAndroid Build Coastguard Worker            if not step_name in info[bot_name]:
570*8975f5c5SAndroid Build Coastguard Worker                LOGGER.error("Missing info for step name: '" + step_name + "'")
571*8975f5c5SAndroid Build Coastguard Worker            sheet_name = format_sheet_name(bot_name, step_name)
572*8975f5c5SAndroid Build Coastguard Worker            headers_stale = False
573*8975f5c5SAndroid Build Coastguard Worker            # Headers should always contain the following columns
574*8975f5c5SAndroid Build Coastguard Worker            for req in REQUIRED_COLUMNS:
575*8975f5c5SAndroid Build Coastguard Worker                if req not in headers[sheet_name]:
576*8975f5c5SAndroid Build Coastguard Worker                    headers_stale = True
577*8975f5c5SAndroid Build Coastguard Worker                    headers[sheet_name].append(req)
578*8975f5c5SAndroid Build Coastguard Worker            # Headers also must contain all the keys seen in this step
579*8975f5c5SAndroid Build Coastguard Worker            for key in info[bot_name][step_name].keys():
580*8975f5c5SAndroid Build Coastguard Worker                if key not in headers[sheet_name]:
581*8975f5c5SAndroid Build Coastguard Worker                    headers_stale = True
582*8975f5c5SAndroid Build Coastguard Worker                    headers[sheet_name].append(key)
583*8975f5c5SAndroid Build Coastguard Worker            # Update the Gdoc headers if necessary
584*8975f5c5SAndroid Build Coastguard Worker            if headers_stale:
585*8975f5c5SAndroid Build Coastguard Worker                sheet_names.append(sheet_name)
586*8975f5c5SAndroid Build Coastguard Worker                header_range = sheet_name + '!A1:Z'
587*8975f5c5SAndroid Build Coastguard Worker                data.append({
588*8975f5c5SAndroid Build Coastguard Worker                    'range': header_range,
589*8975f5c5SAndroid Build Coastguard Worker                    'majorDimension': 'ROWS',
590*8975f5c5SAndroid Build Coastguard Worker                    'values': [headers[sheet_name]]
591*8975f5c5SAndroid Build Coastguard Worker                })
592*8975f5c5SAndroid Build Coastguard Worker    if data:
593*8975f5c5SAndroid Build Coastguard Worker        LOGGER.info('Updating sheet headers...')
594*8975f5c5SAndroid Build Coastguard Worker        batch_update_values(service, spreadsheet_id, data)
595*8975f5c5SAndroid Build Coastguard Worker
596*8975f5c5SAndroid Build Coastguard Worker# Calls values().append() to append a list of values to a given sheet.
597*8975f5c5SAndroid Build Coastguard Workerdef append_values(service, spreadsheet_id, sheet_name, values):
598*8975f5c5SAndroid Build Coastguard Worker    header_range = sheet_name + '!A1:Z'
599*8975f5c5SAndroid Build Coastguard Worker    insert_data_option = 'INSERT_ROWS'
600*8975f5c5SAndroid Build Coastguard Worker    value_input_option = 'USER_ENTERED'  # Helps with formatting of dates
601*8975f5c5SAndroid Build Coastguard Worker    append_values_request_body = {
602*8975f5c5SAndroid Build Coastguard Worker        'range': header_range,
603*8975f5c5SAndroid Build Coastguard Worker        'majorDimension': 'ROWS',
604*8975f5c5SAndroid Build Coastguard Worker        'values': [values],
605*8975f5c5SAndroid Build Coastguard Worker    }
606*8975f5c5SAndroid Build Coastguard Worker    LOGGER.debug("Called [spreadsheets.values().append(spreadsheetId='" + spreadsheet_id +
607*8975f5c5SAndroid Build Coastguard Worker                 "', body=" + str(append_values_request_body) + ", range='" + header_range +
608*8975f5c5SAndroid Build Coastguard Worker                 "', insertDataOption='" + insert_data_option + "', valueInputOption='" +
609*8975f5c5SAndroid Build Coastguard Worker                 value_input_option + "')]")
610*8975f5c5SAndroid Build Coastguard Worker    request = service.values().append(
611*8975f5c5SAndroid Build Coastguard Worker        spreadsheetId=spreadsheet_id,
612*8975f5c5SAndroid Build Coastguard Worker        body=append_values_request_body,
613*8975f5c5SAndroid Build Coastguard Worker        range=header_range,
614*8975f5c5SAndroid Build Coastguard Worker        insertDataOption=insert_data_option,
615*8975f5c5SAndroid Build Coastguard Worker        valueInputOption=value_input_option)
616*8975f5c5SAndroid Build Coastguard Worker    request.execute()
617*8975f5c5SAndroid Build Coastguard Worker
618*8975f5c5SAndroid Build Coastguard Worker
619*8975f5c5SAndroid Build Coastguard Worker# Formula to determine whether a row is a duplicate of the previous row based on checking the
620*8975f5c5SAndroid Build Coastguard Worker# columns listed in filter_columns.
621*8975f5c5SAndroid Build Coastguard Worker# Eg.
622*8975f5c5SAndroid Build Coastguard Worker# date | pass | fail
623*8975f5c5SAndroid Build Coastguard Worker# Jan 1  100    50
624*8975f5c5SAndroid Build Coastguard Worker# Jan 2  100    50
625*8975f5c5SAndroid Build Coastguard Worker# Jan 3  99     51
626*8975f5c5SAndroid Build Coastguard Worker#
627*8975f5c5SAndroid Build Coastguard Worker# If we want to filter based on only the "pass" and "fail" columns, we generate the following
628*8975f5c5SAndroid Build Coastguard Worker# formula in the 'duplicate' column: 'IF(B1=B0, IF(C1=C0,1,0) ,0);
629*8975f5c5SAndroid Build Coastguard Worker# This formula is recursively generated for each column in filter_columns, using the column
630*8975f5c5SAndroid Build Coastguard Worker# position as determined by headers. The formula uses a more generalized form with
631*8975f5c5SAndroid Build Coastguard Worker# 'INDIRECT(ADDRESS(<row>, <col>))'' instead of 'B1', where <row> is Row() and Row()-1, and col is
632*8975f5c5SAndroid Build Coastguard Worker# determined by the column's position in headers
633*8975f5c5SAndroid Build Coastguard Workerdef generate_duplicate_formula(headers, filter_columns):
634*8975f5c5SAndroid Build Coastguard Worker    # No more columns, put a 1 in the IF statement true branch
635*8975f5c5SAndroid Build Coastguard Worker    if len(filter_columns) == 0:
636*8975f5c5SAndroid Build Coastguard Worker        return '1'
637*8975f5c5SAndroid Build Coastguard Worker    # Next column is found, generate the formula for duplicate checking, and remove from the list
638*8975f5c5SAndroid Build Coastguard Worker    # for recursion
639*8975f5c5SAndroid Build Coastguard Worker    for i in range(len(headers)):
640*8975f5c5SAndroid Build Coastguard Worker        if headers[i] == filter_columns[0]:
641*8975f5c5SAndroid Build Coastguard Worker            col = str(i + 1)
642*8975f5c5SAndroid Build Coastguard Worker            formula = "IF(INDIRECT(ADDRESS(ROW(), " + col + "))=INDIRECT(ADDRESS(ROW() - 1, " + \
643*8975f5c5SAndroid Build Coastguard Worker                col + "))," + generate_duplicate_formula(headers, filter_columns[1:]) + ",0)"
644*8975f5c5SAndroid Build Coastguard Worker            return formula
645*8975f5c5SAndroid Build Coastguard Worker    # Next column not found, remove from recursion but just return whatever the next one is
646*8975f5c5SAndroid Build Coastguard Worker    return generate_duplicate_formula(headers, filter_columns[1:])
647*8975f5c5SAndroid Build Coastguard Worker
648*8975f5c5SAndroid Build Coastguard Worker
649*8975f5c5SAndroid Build Coastguard Worker# Helper function to start the recursive call to generate_duplicate_formula
650*8975f5c5SAndroid Build Coastguard Workerdef generate_duplicate_formula_helper(headers):
651*8975f5c5SAndroid Build Coastguard Worker    filter_columns = MAIN_RESULT_COLUMNS
652*8975f5c5SAndroid Build Coastguard Worker    formula = generate_duplicate_formula(headers, filter_columns)
653*8975f5c5SAndroid Build Coastguard Worker    if (formula == "1"):
654*8975f5c5SAndroid Build Coastguard Worker        return ""
655*8975f5c5SAndroid Build Coastguard Worker    else:
656*8975f5c5SAndroid Build Coastguard Worker        # Final result needs to be prepended with =
657*8975f5c5SAndroid Build Coastguard Worker        return "=" + formula
658*8975f5c5SAndroid Build Coastguard Worker
659*8975f5c5SAndroid Build Coastguard Worker# Uses the list of headers and the info struct to come up with a list of values for each step
660*8975f5c5SAndroid Build Coastguard Worker# from the latest builds.
661*8975f5c5SAndroid Build Coastguard Workerdef update_values(service, spreadsheet_id, headers, info):
662*8975f5c5SAndroid Build Coastguard Worker    data = []
663*8975f5c5SAndroid Build Coastguard Worker    for bot_name in info:
664*8975f5c5SAndroid Build Coastguard Worker        for step_name in info[bot_name]['step_names']:
665*8975f5c5SAndroid Build Coastguard Worker            sheet_name = format_sheet_name(bot_name, step_name)
666*8975f5c5SAndroid Build Coastguard Worker            values = []
667*8975f5c5SAndroid Build Coastguard Worker            # For each key in the list of headers, either add the corresponding value or add a blank
668*8975f5c5SAndroid Build Coastguard Worker            # value. It's necessary for the values to match the order of the headers
669*8975f5c5SAndroid Build Coastguard Worker            for key in headers[sheet_name]:
670*8975f5c5SAndroid Build Coastguard Worker                if key in info[bot_name] and key in REQUIRED_COLUMNS:
671*8975f5c5SAndroid Build Coastguard Worker                    values.append(info[bot_name][key])
672*8975f5c5SAndroid Build Coastguard Worker                elif key in info[bot_name][step_name]:
673*8975f5c5SAndroid Build Coastguard Worker                    values.append(info[bot_name][step_name][key])
674*8975f5c5SAndroid Build Coastguard Worker                elif key == "duplicate" and key in REQUIRED_COLUMNS:
675*8975f5c5SAndroid Build Coastguard Worker                    values.append(generate_duplicate_formula_helper(headers[sheet_name]))
676*8975f5c5SAndroid Build Coastguard Worker                else:
677*8975f5c5SAndroid Build Coastguard Worker                    values.append('')
678*8975f5c5SAndroid Build Coastguard Worker            LOGGER.info("Appending new rows to sheet '" + sheet_name + "'...")
679*8975f5c5SAndroid Build Coastguard Worker            try:
680*8975f5c5SAndroid Build Coastguard Worker                append_values(service, spreadsheet_id, sheet_name, values)
681*8975f5c5SAndroid Build Coastguard Worker            except Exception as error:
682*8975f5c5SAndroid Build Coastguard Worker                LOGGER.warning('%s\n' % str(error))
683*8975f5c5SAndroid Build Coastguard Worker
684*8975f5c5SAndroid Build Coastguard Worker
685*8975f5c5SAndroid Build Coastguard Worker# Updates the given spreadsheed_id with the info struct passed in.
686*8975f5c5SAndroid Build Coastguard Workerdef update_spreadsheet(service, spreadsheet_id, info):
687*8975f5c5SAndroid Build Coastguard Worker    LOGGER.info('Opening spreadsheet...')
688*8975f5c5SAndroid Build Coastguard Worker    spreadsheet = get_spreadsheet(service, spreadsheet_id)
689*8975f5c5SAndroid Build Coastguard Worker    LOGGER.info('Parsing sheet names...')
690*8975f5c5SAndroid Build Coastguard Worker    sheet_names = get_sheet_names(info)
691*8975f5c5SAndroid Build Coastguard Worker    new_sheets = validate_sheets(spreadsheet, sheet_names)
692*8975f5c5SAndroid Build Coastguard Worker    if new_sheets:
693*8975f5c5SAndroid Build Coastguard Worker        LOGGER.info('Creating new sheets...')
694*8975f5c5SAndroid Build Coastguard Worker        create_sheets(service, spreadsheet_id, new_sheets)
695*8975f5c5SAndroid Build Coastguard Worker    LOGGER.info('Parsing sheet headers...')
696*8975f5c5SAndroid Build Coastguard Worker    headers = get_headers(service, spreadsheet_id, sheet_names)
697*8975f5c5SAndroid Build Coastguard Worker    update_headers(service, spreadsheet_id, headers, info)
698*8975f5c5SAndroid Build Coastguard Worker    update_filters(service, spreadsheet_id, headers, info, spreadsheet)
699*8975f5c5SAndroid Build Coastguard Worker    update_values(service, spreadsheet_id, headers, info)
700*8975f5c5SAndroid Build Coastguard Worker
701*8975f5c5SAndroid Build Coastguard Worker
702*8975f5c5SAndroid Build Coastguard Worker#####################
703*8975f5c5SAndroid Build Coastguard Worker# Main/helpers      #
704*8975f5c5SAndroid Build Coastguard Worker#####################
705*8975f5c5SAndroid Build Coastguard Worker
706*8975f5c5SAndroid Build Coastguard Worker
707*8975f5c5SAndroid Build Coastguard Worker# Loads or creates credentials and connects to the Sheets API. Returns a Spreadsheets object with
708*8975f5c5SAndroid Build Coastguard Worker# an open connection.
709*8975f5c5SAndroid Build Coastguard Workerdef get_sheets_service(auth_path):
710*8975f5c5SAndroid Build Coastguard Worker    credentials_path = auth_path + '/credentials.json'
711*8975f5c5SAndroid Build Coastguard Worker    token_path = auth_path + '/token.pickle'
712*8975f5c5SAndroid Build Coastguard Worker    creds = None
713*8975f5c5SAndroid Build Coastguard Worker    if not os.path.exists(auth_path):
714*8975f5c5SAndroid Build Coastguard Worker        LOGGER.info("Creating auth dir '" + auth_path + "'")
715*8975f5c5SAndroid Build Coastguard Worker        os.makedirs(auth_path)
716*8975f5c5SAndroid Build Coastguard Worker    if not os.path.exists(credentials_path):
717*8975f5c5SAndroid Build Coastguard Worker        raise Exception('Missing credentials.json.\n'
718*8975f5c5SAndroid Build Coastguard Worker                        'Go to: https://developers.google.com/sheets/api/quickstart/python\n'
719*8975f5c5SAndroid Build Coastguard Worker                        "Under Step 1, click 'ENABLE THE GOOGLE SHEETS API'\n"
720*8975f5c5SAndroid Build Coastguard Worker                        "Click 'DOWNLOAD CLIENT CONFIGURATION'\n"
721*8975f5c5SAndroid Build Coastguard Worker                        'Save to your auth_path (' + auth_path + ') as credentials.json')
722*8975f5c5SAndroid Build Coastguard Worker    if os.path.exists(token_path):
723*8975f5c5SAndroid Build Coastguard Worker        with open(token_path, 'rb') as token:
724*8975f5c5SAndroid Build Coastguard Worker            creds = pickle.load(token)
725*8975f5c5SAndroid Build Coastguard Worker            LOGGER.info('Loaded credentials from ' + token_path)
726*8975f5c5SAndroid Build Coastguard Worker    if not creds or not creds.valid:
727*8975f5c5SAndroid Build Coastguard Worker        if creds and creds.expired and creds.refresh_token:
728*8975f5c5SAndroid Build Coastguard Worker            LOGGER.info('Refreshing credentials...')
729*8975f5c5SAndroid Build Coastguard Worker            creds.refresh(Request())
730*8975f5c5SAndroid Build Coastguard Worker        else:
731*8975f5c5SAndroid Build Coastguard Worker            LOGGER.info('Could not find credentials. Requesting new credentials.')
732*8975f5c5SAndroid Build Coastguard Worker            flow = InstalledAppFlow.from_client_secrets_file(credentials_path, SCOPES)
733*8975f5c5SAndroid Build Coastguard Worker            creds = flow.run_local_server()
734*8975f5c5SAndroid Build Coastguard Worker        with open(token_path, 'wb') as token:
735*8975f5c5SAndroid Build Coastguard Worker            pickle.dump(creds, token)
736*8975f5c5SAndroid Build Coastguard Worker    service = build('sheets', 'v4', credentials=creds)
737*8975f5c5SAndroid Build Coastguard Worker    sheets = service.spreadsheets()
738*8975f5c5SAndroid Build Coastguard Worker    return sheets
739*8975f5c5SAndroid Build Coastguard Worker
740*8975f5c5SAndroid Build Coastguard Worker
741*8975f5c5SAndroid Build Coastguard Worker# Parse the input to the script
742*8975f5c5SAndroid Build Coastguard Workerdef parse_args():
743*8975f5c5SAndroid Build Coastguard Worker    parser = argparse.ArgumentParser(os.path.basename(sys.argv[0]))
744*8975f5c5SAndroid Build Coastguard Worker    parser.add_argument(
745*8975f5c5SAndroid Build Coastguard Worker        '--auth_path',
746*8975f5c5SAndroid Build Coastguard Worker        default=HOME_DIR + '/.auth',
747*8975f5c5SAndroid Build Coastguard Worker        nargs='?',
748*8975f5c5SAndroid Build Coastguard Worker        help='path to directory containing authorization data '
749*8975f5c5SAndroid Build Coastguard Worker        '(credentials.json and token.pickle). '
750*8975f5c5SAndroid Build Coastguard Worker        '[default=<home>/.auth]')
751*8975f5c5SAndroid Build Coastguard Worker    parser.add_argument(
752*8975f5c5SAndroid Build Coastguard Worker        '--spreadsheet',
753*8975f5c5SAndroid Build Coastguard Worker        default='1uttk1z8lJ4ZsUY7wMdFauMzUxb048nh5l52zdrAznek',
754*8975f5c5SAndroid Build Coastguard Worker        nargs='?',
755*8975f5c5SAndroid Build Coastguard Worker        help='ID of the spreadsheet to write stats to. '
756*8975f5c5SAndroid Build Coastguard Worker        "[default='1uttk1z8lJ4ZsUY7wMdFauMzUxb048nh5l52zdrAznek']")
757*8975f5c5SAndroid Build Coastguard Worker    parser.add_argument(
758*8975f5c5SAndroid Build Coastguard Worker        '--verbosity',
759*8975f5c5SAndroid Build Coastguard Worker        default='INFO',
760*8975f5c5SAndroid Build Coastguard Worker        nargs='?',
761*8975f5c5SAndroid Build Coastguard Worker        help='Verbosity of output. Valid options are '
762*8975f5c5SAndroid Build Coastguard Worker        '[DEBUG, INFO, WARNING, ERROR]. '
763*8975f5c5SAndroid Build Coastguard Worker        '[default=INFO]')
764*8975f5c5SAndroid Build Coastguard Worker    return parser.parse_args()
765*8975f5c5SAndroid Build Coastguard Worker
766*8975f5c5SAndroid Build Coastguard Worker
767*8975f5c5SAndroid Build Coastguard Worker# Set up the logging with the right verbosity and output.
768*8975f5c5SAndroid Build Coastguard Workerdef initialize_logging(verbosity):
769*8975f5c5SAndroid Build Coastguard Worker    handler = logging.StreamHandler()
770*8975f5c5SAndroid Build Coastguard Worker    formatter = logging.Formatter(fmt='%(levelname)s: %(message)s')
771*8975f5c5SAndroid Build Coastguard Worker    handler.setFormatter(formatter)
772*8975f5c5SAndroid Build Coastguard Worker    LOGGER.addHandler(handler)
773*8975f5c5SAndroid Build Coastguard Worker    if 'DEBUG' in verbosity:
774*8975f5c5SAndroid Build Coastguard Worker        LOGGER.setLevel(level=logging.DEBUG)
775*8975f5c5SAndroid Build Coastguard Worker    elif 'INFO' in verbosity:
776*8975f5c5SAndroid Build Coastguard Worker        LOGGER.setLevel(level=logging.INFO)
777*8975f5c5SAndroid Build Coastguard Worker    elif 'WARNING' in verbosity:
778*8975f5c5SAndroid Build Coastguard Worker        LOGGER.setLevel(level=logging.WARNING)
779*8975f5c5SAndroid Build Coastguard Worker    elif 'ERROR' in verbosity:
780*8975f5c5SAndroid Build Coastguard Worker        LOGGER.setLevel(level=logging.ERROR)
781*8975f5c5SAndroid Build Coastguard Worker    else:
782*8975f5c5SAndroid Build Coastguard Worker        LOGGER.setLevel(level=logging.INFO)
783*8975f5c5SAndroid Build Coastguard Worker
784*8975f5c5SAndroid Build Coastguard Worker
785*8975f5c5SAndroid Build Coastguard Workerdef main():
786*8975f5c5SAndroid Build Coastguard Worker    os.chdir(ROOT_DIR)
787*8975f5c5SAndroid Build Coastguard Worker    args = parse_args()
788*8975f5c5SAndroid Build Coastguard Worker    verbosity = args.verbosity.strip().upper()
789*8975f5c5SAndroid Build Coastguard Worker    initialize_logging(verbosity)
790*8975f5c5SAndroid Build Coastguard Worker    auth_path = args.auth_path.replace('\\', '/')
791*8975f5c5SAndroid Build Coastguard Worker    try:
792*8975f5c5SAndroid Build Coastguard Worker        service = get_sheets_service(auth_path)
793*8975f5c5SAndroid Build Coastguard Worker    except Exception as error:
794*8975f5c5SAndroid Build Coastguard Worker        LOGGER.error('%s\n' % str(error))
795*8975f5c5SAndroid Build Coastguard Worker        exit(1)
796*8975f5c5SAndroid Build Coastguard Worker
797*8975f5c5SAndroid Build Coastguard Worker    info = {}
798*8975f5c5SAndroid Build Coastguard Worker    LOGGER.info('Building info struct...')
799*8975f5c5SAndroid Build Coastguard Worker    for bot_name in BOT_NAMES:
800*8975f5c5SAndroid Build Coastguard Worker        LOGGER.info("Parsing bot '" + bot_name + "'...")
801*8975f5c5SAndroid Build Coastguard Worker        try:
802*8975f5c5SAndroid Build Coastguard Worker            info[bot_name] = get_bot_info(BOT_NAME_PREFIX + bot_name)
803*8975f5c5SAndroid Build Coastguard Worker        except Exception as error:
804*8975f5c5SAndroid Build Coastguard Worker            LOGGER.error('%s\n' % str(error))
805*8975f5c5SAndroid Build Coastguard Worker
806*8975f5c5SAndroid Build Coastguard Worker    LOGGER.info('Updating sheets...')
807*8975f5c5SAndroid Build Coastguard Worker    try:
808*8975f5c5SAndroid Build Coastguard Worker        update_spreadsheet(service, args.spreadsheet, info)
809*8975f5c5SAndroid Build Coastguard Worker    except Exception as error:
810*8975f5c5SAndroid Build Coastguard Worker        LOGGER.error('%s\n' % str(error))
811*8975f5c5SAndroid Build Coastguard Worker        quit(1)
812*8975f5c5SAndroid Build Coastguard Worker
813*8975f5c5SAndroid Build Coastguard Worker    LOGGER.info('Info was successfully parsed to sheet: https://docs.google.com/spreadsheets/d/' +
814*8975f5c5SAndroid Build Coastguard Worker                args.spreadsheet)
815*8975f5c5SAndroid Build Coastguard Worker
816*8975f5c5SAndroid Build Coastguard Worker
817*8975f5c5SAndroid Build Coastguard Workerif __name__ == '__main__':
818*8975f5c5SAndroid Build Coastguard Worker    sys.exit(main())
819