1// Copyright 2019 The SwiftShader Authors. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// export_to_sheets updates a Google sheets document with the latest test 16// results 17package main 18 19import ( 20 "bufio" 21 "bytes" 22 "context" 23 "encoding/json" 24 "flag" 25 "fmt" 26 "io/ioutil" 27 "log" 28 "net/http" 29 "os" 30 "path/filepath" 31 "strings" 32 33 "../../cause" 34 "../../consts" 35 "../../git" 36 "../../testlist" 37 38 "golang.org/x/oauth2" 39 "golang.org/x/oauth2/google" 40 "google.golang.org/api/sheets/v4" 41) 42 43var ( 44 authdir = flag.String("authdir", "~/.regres-auth", "directory to hold credentials.json and generated token") 45 projectPath = flag.String("projpath", ".", "project path") 46 testListPath = flag.String("testlist", "tests/regres/full-tests.json", "project relative path to the test list .json file") 47 spreadsheetID = flag.String("spreadsheet", "1RCxbqtKNDG9rVMe_xHMapMBgzOCp24mumab73SbHtfw", "identifier of the spreadsheet to update") 48) 49 50const ( 51 columnGitHash = "GIT_HASH" 52 columnGitDate = "GIT_DATE" 53) 54 55func main() { 56 flag.Parse() 57 58 if err := run(); err != nil { 59 log.Fatalln(err) 60 } 61} 62 63func run() error { 64 // Load the full test list. We use this to find the test file names. 65 lists, err := testlist.Load(".", *testListPath) 66 if err != nil { 67 return cause.Wrap(err, "Unable to load test list") 68 } 69 70 // Load the creditials used for editing the Google Sheets spreadsheet. 71 srv, err := createSheetsService(*authdir) 72 if err != nil { 73 return cause.Wrap(err, "Unable to authenticate") 74 } 75 76 // Ensure that there is a sheet for each of the test lists. 77 if err := createTestListSheets(srv, lists); err != nil { 78 return cause.Wrap(err, "Unable to create sheets") 79 } 80 81 spreadsheet, err := srv.Spreadsheets.Get(*spreadsheetID).Do() 82 if err != nil { 83 return cause.Wrap(err, "Unable to get spreadsheet") 84 } 85 86 req := sheets.BatchUpdateValuesRequest{ 87 ValueInputOption: "RAW", 88 } 89 90 testListDir := filepath.Dir(filepath.Join(*projectPath, *testListPath)) 91 changes, err := git.Log(testListDir, 100) 92 if err != nil { 93 return cause.Wrap(err, "Couldn't get git changes for '%v'", testListDir) 94 } 95 96 for _, group := range lists { 97 sheetName := group.Name 98 fmt.Println("Processing sheet", sheetName) 99 sheet := getSheet(spreadsheet, sheetName) 100 if sheet == nil { 101 return cause.Wrap(err, "Sheet '%v' not found", sheetName) 102 } 103 104 columnHeaders, err := fetchRow(srv, spreadsheet, sheet, 0) 105 if err != nil { 106 return cause.Wrap(err, "Couldn't get sheet '%v' column headers", sheetName) 107 } 108 109 columnIndices := listToMap(columnHeaders) 110 111 hashColumnIndex, found := columnIndices[columnGitHash] 112 if !found { 113 return cause.Wrap(err, "Couldn't find sheet '%v' column header '%v'", sheetName, columnGitHash) 114 } 115 116 hashValues, err := fetchColumn(srv, spreadsheet, sheet, hashColumnIndex) 117 if err != nil { 118 return cause.Wrap(err, "Couldn't get sheet '%v' column headers", sheetName) 119 } 120 hashValues = hashValues[1:] // Skip header 121 122 hashIndices := listToMap(hashValues) 123 rowValues := map[string]interface{}{} 124 125 rowInsertionPoint := 1 + len(hashValues) 126 127 for i := len(changes) - 1; i >= 0; i-- { 128 change := changes[i] 129 if !strings.HasPrefix(change.Subject, consts.TestListUpdateCommitSubjectPrefix) { 130 continue 131 } 132 133 hash := change.Hash.String() 134 if _, found := hashIndices[hash]; found { 135 continue // Already in the sheet 136 } 137 138 rowValues[columnGitHash] = change.Hash.String() 139 rowValues[columnGitDate] = change.Date.Format("2006-01-02") 140 141 path := filepath.Join(*projectPath, group.File) 142 hasData := false 143 for _, status := range testlist.Statuses { 144 path := testlist.FilePathWithStatus(path, status) 145 data, err := git.Show(path, hash) 146 if err != nil { 147 continue 148 } 149 lines, err := countLines(data) 150 if err != nil { 151 return cause.Wrap(err, "Couldn't count lines in file '%s'", path) 152 } 153 154 rowValues[string(status)] = lines 155 hasData = true 156 } 157 158 if !hasData { 159 continue 160 } 161 162 data, err := mapToList(columnIndices, rowValues) 163 if err != nil { 164 return cause.Wrap(err, "Couldn't map row values to column for sheet %v. Column headers: [%+v]", sheetName, columnHeaders) 165 } 166 167 req.Data = append(req.Data, &sheets.ValueRange{ 168 Range: rowRange(rowInsertionPoint, sheet), 169 Values: [][]interface{}{data}, 170 }) 171 rowInsertionPoint++ 172 173 fmt.Printf("Adding test data at %v to %v\n", hash[:8], sheetName) 174 } 175 } 176 177 if _, err := srv.Spreadsheets.Values.BatchUpdate(*spreadsheetID, &req).Do(); err != nil { 178 return cause.Wrap(err, "Values BatchUpdate failed") 179 } 180 181 return nil 182} 183 184// listToMap returns the list l as a map where the key is the stringification 185// of the element, and the value is the element index. 186func listToMap(l []interface{}) map[string]int { 187 out := map[string]int{} 188 for i, v := range l { 189 out[fmt.Sprint(v)] = i 190 } 191 return out 192} 193 194// mapToList transforms the two maps into a single slice of values. 195// indices is a map of identifier to output slice element index. 196// values is a map of identifier to value. 197func mapToList(indices map[string]int, values map[string]interface{}) ([]interface{}, error) { 198 out := []interface{}{} 199 for name, value := range values { 200 index, ok := indices[name] 201 if !ok { 202 return nil, fmt.Errorf("No index for '%v'", name) 203 } 204 for len(out) <= index { 205 out = append(out, nil) 206 } 207 out[index] = value 208 } 209 return out, nil 210} 211 212// countLines returns the number of new lines in the byte slice data. 213func countLines(data []byte) (int, error) { 214 scanner := bufio.NewScanner(bytes.NewReader(data)) 215 lines := 0 216 for scanner.Scan() { 217 lines++ 218 } 219 return lines, nil 220} 221 222// getSheet returns the sheet with the given title name, or nil if the sheet 223// cannot be found. 224func getSheet(spreadsheet *sheets.Spreadsheet, name string) *sheets.Sheet { 225 for _, sheet := range spreadsheet.Sheets { 226 if sheet.Properties.Title == name { 227 return sheet 228 } 229 } 230 return nil 231} 232 233// rowRange returns a sheets range ("name!Ai:i") for the entire row with the 234// given index. 235func rowRange(index int, sheet *sheets.Sheet) string { 236 return fmt.Sprintf("%v!A%v:%v", sheet.Properties.Title, index+1, index+1) 237} 238 239// columnRange returns a sheets range ("name!i1:i") for the entire column with 240// the given index. 241func columnRange(index int, sheet *sheets.Sheet) string { 242 col := 'A' + index 243 if index > 25 { 244 panic("UNIMPLEMENTED") 245 } 246 return fmt.Sprintf("%v!%c1:%c", sheet.Properties.Title, col, col) 247} 248 249// fetchRow returns all the values in the given sheet's row. 250func fetchRow(srv *sheets.Service, spreadsheet *sheets.Spreadsheet, sheet *sheets.Sheet, row int) ([]interface{}, error) { 251 rng := rowRange(row, sheet) 252 data, err := srv.Spreadsheets.Values.Get(spreadsheet.SpreadsheetId, rng).Do() 253 if err != nil { 254 return nil, cause.Wrap(err, "Couldn't fetch %v", rng) 255 } 256 return data.Values[0], nil 257} 258 259// fetchColumn returns all the values in the given sheet's column. 260func fetchColumn(srv *sheets.Service, spreadsheet *sheets.Spreadsheet, sheet *sheets.Sheet, row int) ([]interface{}, error) { 261 rng := columnRange(row, sheet) 262 data, err := srv.Spreadsheets.Values.Get(spreadsheet.SpreadsheetId, rng).Do() 263 if err != nil { 264 return nil, cause.Wrap(err, "Couldn't fetch %v", rng) 265 } 266 out := make([]interface{}, len(data.Values)) 267 for i, l := range data.Values { 268 if len(l) > 0 { 269 out[i] = l[0] 270 } 271 } 272 return out, nil 273} 274 275// insertRows inserts blank rows into the given sheet. 276func insertRows(srv *sheets.Service, spreadsheet *sheets.Spreadsheet, sheet *sheets.Sheet, aboveRow, count int) error { 277 req := sheets.BatchUpdateSpreadsheetRequest{ 278 Requests: []*sheets.Request{{ 279 InsertRange: &sheets.InsertRangeRequest{ 280 Range: &sheets.GridRange{ 281 SheetId: sheet.Properties.SheetId, 282 StartRowIndex: int64(aboveRow), 283 EndRowIndex: int64(aboveRow + count), 284 }, 285 ShiftDimension: "ROWS", 286 }}, 287 }, 288 } 289 if _, err := srv.Spreadsheets.BatchUpdate(*spreadsheetID, &req).Do(); err != nil { 290 return cause.Wrap(err, "Values BatchUpdate failed") 291 } 292 return nil 293} 294 295// createTestListSheets adds a new sheet for each of the test lists, if they 296// do not already exist. These new sheets are populated with column headers. 297func createTestListSheets(srv *sheets.Service, testlists testlist.Lists) error { 298 spreadsheet, err := srv.Spreadsheets.Get(*spreadsheetID).Do() 299 if err != nil { 300 return cause.Wrap(err, "Unable to get spreadsheet") 301 } 302 303 spreadsheetReq := sheets.BatchUpdateSpreadsheetRequest{} 304 updateReq := sheets.BatchUpdateValuesRequest{ValueInputOption: "RAW"} 305 headers := []interface{}{columnGitHash, columnGitDate} 306 for _, s := range testlist.Statuses { 307 headers = append(headers, string(s)) 308 } 309 310 for _, group := range testlists { 311 name := group.Name 312 if getSheet(spreadsheet, name) == nil { 313 spreadsheetReq.Requests = append(spreadsheetReq.Requests, &sheets.Request{ 314 AddSheet: &sheets.AddSheetRequest{ 315 Properties: &sheets.SheetProperties{ 316 Title: name, 317 }, 318 }, 319 }) 320 updateReq.Data = append(updateReq.Data, 321 &sheets.ValueRange{ 322 Range: name + "!A1:Z", 323 Values: [][]interface{}{headers}, 324 }, 325 ) 326 } 327 } 328 329 if len(spreadsheetReq.Requests) > 0 { 330 if _, err := srv.Spreadsheets.BatchUpdate(*spreadsheetID, &spreadsheetReq).Do(); err != nil { 331 return cause.Wrap(err, "Spreadsheets BatchUpdate failed") 332 } 333 } 334 if len(updateReq.Data) > 0 { 335 if _, err := srv.Spreadsheets.Values.BatchUpdate(*spreadsheetID, &updateReq).Do(); err != nil { 336 return cause.Wrap(err, "Values BatchUpdate failed") 337 } 338 } 339 340 return nil 341} 342 343// createSheetsService creates a new Google Sheets service using the credentials 344// in the credentials.json file. 345func createSheetsService(authdir string) (*sheets.Service, error) { 346 authdir = os.ExpandEnv(authdir) 347 if home, err := os.UserHomeDir(); err == nil { 348 authdir = strings.ReplaceAll(authdir, "~", home) 349 } 350 351 os.MkdirAll(authdir, 0777) 352 353 credentialsPath := filepath.Join(authdir, "credentials.json") 354 b, err := ioutil.ReadFile(credentialsPath) 355 if err != nil { 356 return nil, cause.Wrap(err, "Unable to read client secret file '%v'\n"+ 357 "Obtain this file from: https://console.developers.google.com/apis/credentials", credentialsPath) 358 } 359 360 config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets") 361 if err != nil { 362 return nil, cause.Wrap(err, "Unable to parse client secret file to config") 363 } 364 365 client, err := getClient(authdir, config) 366 if err != nil { 367 return nil, cause.Wrap(err, "Unable obtain client") 368 } 369 370 srv, err := sheets.New(client) 371 if err != nil { 372 return nil, cause.Wrap(err, "Unable to retrieve Sheets client") 373 } 374 return srv, nil 375} 376 377// Retrieve a token, saves the token, then returns the generated client. 378func getClient(authdir string, config *oauth2.Config) (*http.Client, error) { 379 // The file token.json stores the user's access and refresh tokens, and is 380 // created automatically when the authorization flow completes for the first 381 // time. 382 tokFile := filepath.Join(authdir, "token.json") 383 tok, err := tokenFromFile(tokFile) 384 if err != nil { 385 tok, err = getTokenFromWeb(config) 386 if err != nil { 387 return nil, cause.Wrap(err, "Unable to get token from web") 388 } 389 if err := saveToken(tokFile, tok); err != nil { 390 log.Println("Warning: failed to write token: %v", err) 391 } 392 } 393 return config.Client(context.Background(), tok), nil 394} 395 396// Request a token from the web, then returns the retrieved token. 397func getTokenFromWeb(config *oauth2.Config) (*oauth2.Token, error) { 398 authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) 399 fmt.Printf("Go to the following link in your browser then type the "+ 400 "authorization code: \n%v\n", authURL) 401 402 var authCode string 403 if _, err := fmt.Scan(&authCode); err != nil { 404 return nil, cause.Wrap(err, "Unable to read authorization code") 405 } 406 407 tok, err := config.Exchange(context.TODO(), authCode) 408 if err != nil { 409 return nil, cause.Wrap(err, "Unable to retrieve token from web") 410 } 411 return tok, nil 412} 413 414// Retrieves a token from a local file. 415func tokenFromFile(path string) (*oauth2.Token, error) { 416 f, err := os.Open(path) 417 if err != nil { 418 return nil, err 419 } 420 defer f.Close() 421 tok := &oauth2.Token{} 422 err = json.NewDecoder(f).Decode(tok) 423 return tok, err 424} 425 426// Saves a token to a file path. 427func saveToken(path string, token *oauth2.Token) error { 428 fmt.Printf("Saving credential file to: %s\n", path) 429 f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) 430 if err != nil { 431 return cause.Wrap(err, "Unable to cache oauth token") 432 } 433 defer f.Close() 434 json.NewEncoder(f).Encode(token) 435 return nil 436} 437