Skip to content

Commit b6a7f25

Browse files
authored
script to determine which tests to run on a pr (#2785)
* script to determine which tests to run on a pr * don't hardcode path from script to base; fix build * add check for '/'
1 parent 3293a0f commit b6a7f25

File tree

1 file changed

+263
-0
lines changed

1 file changed

+263
-0
lines changed
+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
// affectedtests determines, for a given GitHub PR, which acceptance tests it affects.
2+
//
3+
// Example usage: git diff HEAD~ > tmp.diff && go run affectedtests.go -diff tmp.diff
4+
//
5+
// It is also possible to get the diff from a PR: go run affectedtests.go -pr 2771
6+
// However, this mode only reads the changed files from the PR and does not (currently)
7+
// take into account new resources/tests that might have been added in this PR.
8+
//
9+
// This script currently only works for changes to resources.
10+
// It is a TODO to make it work for changes to tests, data sources, and common utilities.
11+
// It also currently does not pick up tests that use configs from other files.
12+
13+
package main
14+
15+
import (
16+
"flag"
17+
"fmt"
18+
"go/ast"
19+
"go/parser"
20+
"go/token"
21+
"io/ioutil"
22+
"log"
23+
"net/http"
24+
"os"
25+
"path/filepath"
26+
"regexp"
27+
"runtime"
28+
"sort"
29+
"strings"
30+
)
31+
32+
func main() {
33+
diff := flag.String("diff", "", "file containing git diff to use when determining changed files")
34+
pr := flag.Uint("pr", 0, "PR # to use to determine changed files")
35+
flag.Parse()
36+
if (*pr == 0 && *diff == "") || (*pr != 0 && *diff != "") {
37+
fmt.Println("Exactly one of -pr and -diff must be set")
38+
flag.Usage()
39+
os.Exit(1)
40+
}
41+
42+
_, scriptPath, _, ok := runtime.Caller(0)
43+
if !ok {
44+
log.Fatal("Could not get current working directory")
45+
}
46+
tpgDir := scriptPath
47+
for !strings.HasPrefix(filepath.Base(tpgDir), "terraform-provider-") && tpgDir != "/" {
48+
tpgDir = filepath.Clean(tpgDir + "/..")
49+
}
50+
if tpgDir == "/" {
51+
log.Fatal("Script was run outside of google provider directory")
52+
}
53+
repo := strings.TrimPrefix(filepath.Base(tpgDir), "terraform-provider-")
54+
googleDir := tpgDir + "/" + repo
55+
56+
providerFiles, err := readProviderFiles(googleDir)
57+
if err != nil {
58+
log.Fatal(err)
59+
}
60+
61+
var diffVal string
62+
if *diff == "" {
63+
diffVal, err = getDiffFromPR(*pr, repo)
64+
if err != nil {
65+
log.Fatal(err)
66+
}
67+
} else {
68+
d, err := ioutil.ReadFile(*diff)
69+
if err != nil {
70+
log.Fatal(err)
71+
}
72+
diffVal = string(d)
73+
}
74+
75+
tests := map[string]struct{}{}
76+
for _, r := range getChangedResourcesFromDiff(diffVal, repo) {
77+
rn, err := getResourceName(r, googleDir, providerFiles)
78+
if err != nil {
79+
log.Fatal(err)
80+
}
81+
if rn == "" {
82+
log.Fatalf("Could not find resource represented by %s", r)
83+
}
84+
log.Printf("File %s matches resource %s", r, rn)
85+
ts, err := getTestsAffectedBy(rn, googleDir)
86+
if err != nil {
87+
log.Fatal(err)
88+
}
89+
for _, t := range ts {
90+
tests[t] = struct{}{}
91+
}
92+
}
93+
testnames := []string{}
94+
for tn, _ := range tests {
95+
testnames = append(testnames, tn)
96+
}
97+
sort.Strings(testnames)
98+
for _, tn := range testnames {
99+
fmt.Println(tn)
100+
}
101+
}
102+
103+
func readProviderFiles(googleDir string) ([]string, error) {
104+
pfs := []string{}
105+
dir, err := ioutil.ReadDir(googleDir)
106+
if err != nil {
107+
return nil, err
108+
}
109+
for _, f := range dir {
110+
if strings.HasPrefix(f.Name(), "provider") {
111+
p, err := ioutil.ReadFile(googleDir + "/" + f.Name())
112+
if err != nil {
113+
return nil, err
114+
}
115+
pfs = append(pfs, string(p))
116+
}
117+
}
118+
return pfs, nil
119+
}
120+
121+
func getDiffFromPR(pr uint, repo string) (string, error) {
122+
resp, err := http.Get(fmt.Sprintf("https://github.com/terraform-providers/terraform-provider-%s/pull/%d.diff", repo, pr))
123+
if err != nil {
124+
return "", err
125+
}
126+
defer resp.Body.Close()
127+
body, err := ioutil.ReadAll(resp.Body)
128+
if err != nil {
129+
return "", err
130+
}
131+
return string(body), nil
132+
}
133+
134+
func getChangedResourcesFromDiff(diff, repo string) []string {
135+
results := []string{}
136+
for _, l := range strings.Split(diff, "\n") {
137+
if strings.HasPrefix(l, "+++ b/") {
138+
log.Println("Found addition: " + l)
139+
fName := strings.TrimPrefix(l, "+++ b/"+repo+"/")
140+
if strings.HasPrefix(fName, "resource_") && !strings.HasSuffix(fName, "_test.go") {
141+
results = append(results, fName)
142+
}
143+
}
144+
}
145+
log.Printf("PR contains resource files %v", results)
146+
return results
147+
}
148+
149+
func getResourceName(fName, googleDir string, providerFiles []string) (string, error) {
150+
resourceFile, err := parser.ParseFile(token.NewFileSet(), googleDir+"/"+fName, nil, parser.AllErrors)
151+
if err != nil {
152+
return "", err
153+
}
154+
// Loop through all the top-level objects in the resource file.
155+
// One of them is the resource definition: something like resourceComputeInstance()
156+
for k, _ := range resourceFile.Scope.Objects {
157+
// Matches the line in the provider file where the resource is defined,
158+
// e.g. "google_compute_instance": resourceComputeInstance()
159+
re := regexp.MustCompile(`"(.*)":\s*` + k + `\(\)`)
160+
161+
// Check all the provider files to see if they have a line that matches
162+
// that regexp. If so, return the resource name.
163+
for _, pf := range providerFiles {
164+
sm := re.FindStringSubmatch(pf)
165+
if len(sm) > 1 {
166+
log.Println("Full match is " + sm[0])
167+
return sm[1], nil
168+
}
169+
}
170+
}
171+
172+
return "", nil
173+
}
174+
175+
func getTestsAffectedBy(rn, googleDir string) ([]string, error) {
176+
lines, err := getLinesContainingResourceName(rn, googleDir)
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
results := []string{}
182+
for _, line := range lines {
183+
fset := token.NewFileSet()
184+
p, err := parser.ParseFile(fset, line.file, nil, parser.AllErrors)
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
// Find the top-level func containing this offset
190+
def := findFuncContainingOffset(line.offset, fset, p)
191+
if def == "" {
192+
// We couldn't find the place in the file that contains this offset, just skip and move on
193+
continue
194+
}
195+
196+
// Go back through and find the test that calls the definition we just found
197+
results = append(results, findTestsCallingFunc(p, def)...)
198+
}
199+
return results, nil
200+
}
201+
202+
func findFuncContainingOffset(offset int, fset *token.FileSet, p *ast.File) string {
203+
for k, sc := range p.Scope.Objects {
204+
d := sc.Decl.(ast.Node)
205+
if fset.Position(d.Pos()).Offset < offset && offset < fset.Position(d.End()).Offset {
206+
return k
207+
}
208+
}
209+
return ""
210+
}
211+
212+
func findTestsCallingFunc(p *ast.File, funcName string) []string {
213+
results := []string{}
214+
for objName, sc := range p.Scope.Objects {
215+
if !strings.HasPrefix(objName, "Test") {
216+
continue
217+
}
218+
d, ok := sc.Decl.(*ast.FuncDecl)
219+
if !ok {
220+
continue
221+
}
222+
// Starting at each Test, see if there's a path to the func we just found.
223+
ast.Inspect(d, func(n ast.Node) bool {
224+
if n, ok := n.(*ast.Ident); ok {
225+
if n.Name == funcName {
226+
results = append(results, objName)
227+
}
228+
}
229+
return true
230+
})
231+
}
232+
return results
233+
}
234+
235+
type location struct {
236+
file string
237+
offset int
238+
}
239+
240+
func getLinesContainingResourceName(rn, googleDir string) ([]location, error) {
241+
results := []location{}
242+
resDef := regexp.MustCompile(fmt.Sprintf(`resource "%s"`, rn))
243+
dir, err := ioutil.ReadDir(googleDir)
244+
if err != nil {
245+
return nil, err
246+
}
247+
for _, f := range dir {
248+
if f.IsDir() {
249+
continue
250+
}
251+
fPath := googleDir + "/" + f.Name()
252+
contents, err := ioutil.ReadFile(fPath)
253+
if err != nil {
254+
return nil, err
255+
}
256+
matches := resDef.FindAllIndex(contents, -1)
257+
for _, loc := range matches {
258+
// the full match is at contents[loc[0]:loc[1]], but we only need one value
259+
results = append(results, location{fPath, loc[0]})
260+
}
261+
}
262+
return results, nil
263+
}

0 commit comments

Comments
 (0)