• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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