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