• 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	"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