// Copyright 2019 The SwiftShader Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // export_to_sheets updates a Google sheets document with the latest test // results package main import ( "bufio" "bytes" "context" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "strings" "../../cause" "../../consts" "../../git" "../../testlist" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/sheets/v4" ) var ( authdir = flag.String("authdir", "~/.regres-auth", "directory to hold credentials.json and generated token") projectPath = flag.String("projpath", ".", "project path") testListPath = flag.String("testlist", "tests/regres/full-tests.json", "project relative path to the test list .json file") spreadsheetID = flag.String("spreadsheet", "1RCxbqtKNDG9rVMe_xHMapMBgzOCp24mumab73SbHtfw", "identifier of the spreadsheet to update") ) const ( columnGitHash = "GIT_HASH" columnGitDate = "GIT_DATE" ) func main() { flag.Parse() if err := run(); err != nil { log.Fatalln(err) } } func run() error { // Load the full test list. We use this to find the test file names. lists, err := testlist.Load(".", *testListPath) if err != nil { return cause.Wrap(err, "Unable to load test list") } // Load the creditials used for editing the Google Sheets spreadsheet. srv, err := createSheetsService(*authdir) if err != nil { return cause.Wrap(err, "Unable to authenticate") } // Ensure that there is a sheet for each of the test lists. if err := createTestListSheets(srv, lists); err != nil { return cause.Wrap(err, "Unable to create sheets") } spreadsheet, err := srv.Spreadsheets.Get(*spreadsheetID).Do() if err != nil { return cause.Wrap(err, "Unable to get spreadsheet") } req := sheets.BatchUpdateValuesRequest{ ValueInputOption: "RAW", } testListDir := filepath.Dir(filepath.Join(*projectPath, *testListPath)) changes, err := git.Log(testListDir, 100) if err != nil { return cause.Wrap(err, "Couldn't get git changes for '%v'", testListDir) } for _, group := range lists { sheetName := group.Name fmt.Println("Processing sheet", sheetName) sheet := getSheet(spreadsheet, sheetName) if sheet == nil { return cause.Wrap(err, "Sheet '%v' not found", sheetName) } columnHeaders, err := fetchRow(srv, spreadsheet, sheet, 0) if err != nil { return cause.Wrap(err, "Couldn't get sheet '%v' column headers", sheetName) } columnIndices := listToMap(columnHeaders) hashColumnIndex, found := columnIndices[columnGitHash] if !found { return cause.Wrap(err, "Couldn't find sheet '%v' column header '%v'", sheetName, columnGitHash) } hashValues, err := fetchColumn(srv, spreadsheet, sheet, hashColumnIndex) if err != nil { return cause.Wrap(err, "Couldn't get sheet '%v' column headers", sheetName) } hashValues = hashValues[1:] // Skip header hashIndices := listToMap(hashValues) rowValues := map[string]interface{}{} rowInsertionPoint := 1 + len(hashValues) for i := len(changes) - 1; i >= 0; i-- { change := changes[i] if !strings.HasPrefix(change.Subject, consts.TestListUpdateCommitSubjectPrefix) { continue } hash := change.Hash.String() if _, found := hashIndices[hash]; found { continue // Already in the sheet } rowValues[columnGitHash] = change.Hash.String() rowValues[columnGitDate] = change.Date.Format("2006-01-02") path := filepath.Join(*projectPath, group.File) hasData := false for _, status := range testlist.Statuses { path := testlist.FilePathWithStatus(path, status) data, err := git.Show(path, hash) if err != nil { continue } lines, err := countLines(data) if err != nil { return cause.Wrap(err, "Couldn't count lines in file '%s'", path) } rowValues[string(status)] = lines hasData = true } if !hasData { continue } data, err := mapToList(columnIndices, rowValues) if err != nil { return cause.Wrap(err, "Couldn't map row values to column for sheet %v. Column headers: [%+v]", sheetName, columnHeaders) } req.Data = append(req.Data, &sheets.ValueRange{ Range: rowRange(rowInsertionPoint, sheet), Values: [][]interface{}{data}, }) rowInsertionPoint++ fmt.Printf("Adding test data at %v to %v\n", hash[:8], sheetName) } } if _, err := srv.Spreadsheets.Values.BatchUpdate(*spreadsheetID, &req).Do(); err != nil { return cause.Wrap(err, "Values BatchUpdate failed") } return nil } // listToMap returns the list l as a map where the key is the stringification // of the element, and the value is the element index. func listToMap(l []interface{}) map[string]int { out := map[string]int{} for i, v := range l { out[fmt.Sprint(v)] = i } return out } // mapToList transforms the two maps into a single slice of values. // indices is a map of identifier to output slice element index. // values is a map of identifier to value. func mapToList(indices map[string]int, values map[string]interface{}) ([]interface{}, error) { out := []interface{}{} for name, value := range values { index, ok := indices[name] if !ok { return nil, fmt.Errorf("No index for '%v'", name) } for len(out) <= index { out = append(out, nil) } out[index] = value } return out, nil } // countLines returns the number of new lines in the byte slice data. func countLines(data []byte) (int, error) { scanner := bufio.NewScanner(bytes.NewReader(data)) lines := 0 for scanner.Scan() { lines++ } return lines, nil } // getSheet returns the sheet with the given title name, or nil if the sheet // cannot be found. func getSheet(spreadsheet *sheets.Spreadsheet, name string) *sheets.Sheet { for _, sheet := range spreadsheet.Sheets { if sheet.Properties.Title == name { return sheet } } return nil } // rowRange returns a sheets range ("name!Ai:i") for the entire row with the // given index. func rowRange(index int, sheet *sheets.Sheet) string { return fmt.Sprintf("%v!A%v:%v", sheet.Properties.Title, index+1, index+1) } // columnRange returns a sheets range ("name!i1:i") for the entire column with // the given index. func columnRange(index int, sheet *sheets.Sheet) string { col := 'A' + index if index > 25 { panic("UNIMPLEMENTED") } return fmt.Sprintf("%v!%c1:%c", sheet.Properties.Title, col, col) } // fetchRow returns all the values in the given sheet's row. func fetchRow(srv *sheets.Service, spreadsheet *sheets.Spreadsheet, sheet *sheets.Sheet, row int) ([]interface{}, error) { rng := rowRange(row, sheet) data, err := srv.Spreadsheets.Values.Get(spreadsheet.SpreadsheetId, rng).Do() if err != nil { return nil, cause.Wrap(err, "Couldn't fetch %v", rng) } return data.Values[0], nil } // fetchColumn returns all the values in the given sheet's column. func fetchColumn(srv *sheets.Service, spreadsheet *sheets.Spreadsheet, sheet *sheets.Sheet, row int) ([]interface{}, error) { rng := columnRange(row, sheet) data, err := srv.Spreadsheets.Values.Get(spreadsheet.SpreadsheetId, rng).Do() if err != nil { return nil, cause.Wrap(err, "Couldn't fetch %v", rng) } out := make([]interface{}, len(data.Values)) for i, l := range data.Values { if len(l) > 0 { out[i] = l[0] } } return out, nil } // insertRows inserts blank rows into the given sheet. func insertRows(srv *sheets.Service, spreadsheet *sheets.Spreadsheet, sheet *sheets.Sheet, aboveRow, count int) error { req := sheets.BatchUpdateSpreadsheetRequest{ Requests: []*sheets.Request{{ InsertRange: &sheets.InsertRangeRequest{ Range: &sheets.GridRange{ SheetId: sheet.Properties.SheetId, StartRowIndex: int64(aboveRow), EndRowIndex: int64(aboveRow + count), }, ShiftDimension: "ROWS", }}, }, } if _, err := srv.Spreadsheets.BatchUpdate(*spreadsheetID, &req).Do(); err != nil { return cause.Wrap(err, "Values BatchUpdate failed") } return nil } // createTestListSheets adds a new sheet for each of the test lists, if they // do not already exist. These new sheets are populated with column headers. func createTestListSheets(srv *sheets.Service, testlists testlist.Lists) error { spreadsheet, err := srv.Spreadsheets.Get(*spreadsheetID).Do() if err != nil { return cause.Wrap(err, "Unable to get spreadsheet") } spreadsheetReq := sheets.BatchUpdateSpreadsheetRequest{} updateReq := sheets.BatchUpdateValuesRequest{ValueInputOption: "RAW"} headers := []interface{}{columnGitHash, columnGitDate} for _, s := range testlist.Statuses { headers = append(headers, string(s)) } for _, group := range testlists { name := group.Name if getSheet(spreadsheet, name) == nil { spreadsheetReq.Requests = append(spreadsheetReq.Requests, &sheets.Request{ AddSheet: &sheets.AddSheetRequest{ Properties: &sheets.SheetProperties{ Title: name, }, }, }) updateReq.Data = append(updateReq.Data, &sheets.ValueRange{ Range: name + "!A1:Z", Values: [][]interface{}{headers}, }, ) } } if len(spreadsheetReq.Requests) > 0 { if _, err := srv.Spreadsheets.BatchUpdate(*spreadsheetID, &spreadsheetReq).Do(); err != nil { return cause.Wrap(err, "Spreadsheets BatchUpdate failed") } } if len(updateReq.Data) > 0 { if _, err := srv.Spreadsheets.Values.BatchUpdate(*spreadsheetID, &updateReq).Do(); err != nil { return cause.Wrap(err, "Values BatchUpdate failed") } } return nil } // createSheetsService creates a new Google Sheets service using the credentials // in the credentials.json file. func createSheetsService(authdir string) (*sheets.Service, error) { authdir = os.ExpandEnv(authdir) if home, err := os.UserHomeDir(); err == nil { authdir = strings.ReplaceAll(authdir, "~", home) } os.MkdirAll(authdir, 0777) credentialsPath := filepath.Join(authdir, "credentials.json") b, err := ioutil.ReadFile(credentialsPath) if err != nil { return nil, cause.Wrap(err, "Unable to read client secret file '%v'\n"+ "Obtain this file from: https://console.developers.google.com/apis/credentials", credentialsPath) } config, err := google.ConfigFromJSON(b, "https://www.googleapis.com/auth/spreadsheets") if err != nil { return nil, cause.Wrap(err, "Unable to parse client secret file to config") } client, err := getClient(authdir, config) if err != nil { return nil, cause.Wrap(err, "Unable obtain client") } srv, err := sheets.New(client) if err != nil { return nil, cause.Wrap(err, "Unable to retrieve Sheets client") } return srv, nil } // Retrieve a token, saves the token, then returns the generated client. func getClient(authdir string, config *oauth2.Config) (*http.Client, error) { // The file token.json stores the user's access and refresh tokens, and is // created automatically when the authorization flow completes for the first // time. tokFile := filepath.Join(authdir, "token.json") tok, err := tokenFromFile(tokFile) if err != nil { tok, err = getTokenFromWeb(config) if err != nil { return nil, cause.Wrap(err, "Unable to get token from web") } if err := saveToken(tokFile, tok); err != nil { log.Println("Warning: failed to write token: %v", err) } } return config.Client(context.Background(), tok), nil } // Request a token from the web, then returns the retrieved token. func getTokenFromWeb(config *oauth2.Config) (*oauth2.Token, error) { authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline) fmt.Printf("Go to the following link in your browser then type the "+ "authorization code: \n%v\n", authURL) var authCode string if _, err := fmt.Scan(&authCode); err != nil { return nil, cause.Wrap(err, "Unable to read authorization code") } tok, err := config.Exchange(context.TODO(), authCode) if err != nil { return nil, cause.Wrap(err, "Unable to retrieve token from web") } return tok, nil } // Retrieves a token from a local file. func tokenFromFile(path string) (*oauth2.Token, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() tok := &oauth2.Token{} err = json.NewDecoder(f).Decode(tok) return tok, err } // Saves a token to a file path. func saveToken(path string, token *oauth2.Token) error { fmt.Printf("Saving credential file to: %s\n", path) f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return cause.Wrap(err, "Unable to cache oauth token") } defer f.Close() json.NewEncoder(f).Encode(token) return nil }