github.com/april1989/origin-go-tools@v0.0.32/cmd/guru/guru_test.go (about) 1 // Copyright 2013 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main_test 6 7 // This file defines a test framework for guru queries. 8 // 9 // The files beneath testdata/src contain Go programs containing 10 // query annotations of the form: 11 // 12 // @verb id "select" 13 // 14 // where verb is the query mode (e.g. "callers"), id is a unique name 15 // for this query, and "select" is a regular expression matching the 16 // substring of the current line that is the query's input selection. 17 // 18 // The expected output for each query is provided in the accompanying 19 // .golden file. 20 // 21 // (Location information is not included because it's too fragile to 22 // display as text. TODO(adonovan): think about how we can test its 23 // correctness, since it is critical information.) 24 // 25 // Run this test with: 26 // % go test github.com/april1989/origin-go-tools/cmd/guru -update 27 // to update the golden files. 28 29 import ( 30 "bytes" 31 "flag" 32 "fmt" 33 "go/build" 34 "go/parser" 35 "go/token" 36 "io" 37 "io/ioutil" 38 "log" 39 "os" 40 "os/exec" 41 "path/filepath" 42 "regexp" 43 "runtime" 44 "sort" 45 "strconv" 46 "strings" 47 "sync" 48 "testing" 49 50 guru "github.com/april1989/origin-go-tools/cmd/guru" 51 "github.com/april1989/origin-go-tools/internal/testenv" 52 ) 53 54 func init() { 55 // This test currently requires GOPATH mode. 56 // Explicitly disabling module mode should suffix, but 57 // we'll also turn off GOPROXY just for good measure. 58 if err := os.Setenv("GO111MODULE", "off"); err != nil { 59 log.Fatal(err) 60 } 61 if err := os.Setenv("GOPROXY", "off"); err != nil { 62 log.Fatal(err) 63 } 64 } 65 66 var updateFlag = flag.Bool("update", false, "Update the golden files.") 67 68 type query struct { 69 id string // unique id 70 verb string // query mode, e.g. "callees" 71 posn token.Position // query position 72 filename string 73 queryPos string // query position in command-line syntax 74 } 75 76 func parseRegexp(text string) (*regexp.Regexp, error) { 77 pattern, err := strconv.Unquote(text) 78 if err != nil { 79 return nil, fmt.Errorf("can't unquote %s", text) 80 } 81 return regexp.Compile(pattern) 82 } 83 84 // parseQueries parses and returns the queries in the named file. 85 func parseQueries(t *testing.T, filename string) []*query { 86 filedata, err := ioutil.ReadFile(filename) 87 if err != nil { 88 t.Fatal(err) 89 } 90 91 // Parse the file once to discover the test queries. 92 fset := token.NewFileSet() 93 f, err := parser.ParseFile(fset, filename, filedata, parser.ParseComments) 94 if err != nil { 95 t.Fatal(err) 96 } 97 98 lines := bytes.Split(filedata, []byte("\n")) 99 100 var queries []*query 101 queriesById := make(map[string]*query) 102 103 // Find all annotations of these forms: 104 expectRe := regexp.MustCompile(`@([a-z]+)\s+(\S+)\s+(\".*)$`) // @verb id "regexp" 105 for _, c := range f.Comments { 106 text := strings.TrimSpace(c.Text()) 107 if text == "" || text[0] != '@' { 108 continue 109 } 110 posn := fset.Position(c.Pos()) 111 112 // @verb id "regexp" 113 match := expectRe.FindStringSubmatch(text) 114 if match == nil { 115 t.Errorf("%s: ill-formed query: %s", posn, text) 116 continue 117 } 118 119 id := match[2] 120 if prev, ok := queriesById[id]; ok { 121 t.Errorf("%s: duplicate id %s", posn, id) 122 t.Errorf("%s: previously used here", prev.posn) 123 continue 124 } 125 126 q := &query{ 127 id: id, 128 verb: match[1], 129 filename: filename, 130 posn: posn, 131 } 132 133 if match[3] != `"nopos"` { 134 selectRe, err := parseRegexp(match[3]) 135 if err != nil { 136 t.Errorf("%s: %s", posn, err) 137 continue 138 } 139 140 // Find text of the current line, sans query. 141 // (Queries must be // not /**/ comments.) 142 line := lines[posn.Line-1][:posn.Column-1] 143 144 // Apply regexp to current line to find input selection. 145 loc := selectRe.FindIndex(line) 146 if loc == nil { 147 t.Errorf("%s: selection pattern %s doesn't match line %q", 148 posn, match[3], string(line)) 149 continue 150 } 151 152 // Assumes ASCII. TODO(adonovan): test on UTF-8. 153 linestart := posn.Offset - (posn.Column - 1) 154 155 // Compute the file offsets. 156 q.queryPos = fmt.Sprintf("%s:#%d,#%d", 157 filename, linestart+loc[0], linestart+loc[1]) 158 } 159 160 queries = append(queries, q) 161 queriesById[id] = q 162 } 163 164 // Return the slice, not map, for deterministic iteration. 165 return queries 166 } 167 168 // doQuery poses query q to the guru and writes its response and 169 // error (if any) to out. 170 func doQuery(out io.Writer, q *query, json bool) { 171 fmt.Fprintf(out, "-------- @%s %s --------\n", q.verb, q.id) 172 173 var buildContext = build.Default 174 buildContext.GOPATH = "testdata" 175 pkg := filepath.Dir(strings.TrimPrefix(q.filename, "testdata/src/")) 176 177 gopathAbs, _ := filepath.Abs(buildContext.GOPATH) 178 179 var outputMu sync.Mutex // guards outputs 180 var outputs []string // JSON objects or lines of text 181 outputFn := func(fset *token.FileSet, qr guru.QueryResult) { 182 outputMu.Lock() 183 defer outputMu.Unlock() 184 if json { 185 jsonstr := string(qr.JSON(fset)) 186 // Sanitize any absolute filenames that creep in. 187 jsonstr = strings.Replace(jsonstr, gopathAbs, "$GOPATH", -1) 188 outputs = append(outputs, jsonstr) 189 } else { 190 // suppress position information 191 qr.PrintPlain(func(_ interface{}, format string, args ...interface{}) { 192 outputs = append(outputs, fmt.Sprintf(format, args...)) 193 }) 194 } 195 } 196 197 query := guru.Query{ 198 Pos: q.queryPos, 199 Build: &buildContext, 200 Scope: []string{pkg}, 201 Reflection: true, 202 Output: outputFn, 203 } 204 205 if err := guru.Run(q.verb, &query); err != nil { 206 fmt.Fprintf(out, "\nError: %s\n", err) 207 return 208 } 209 210 // In a "referrers" query, references are sorted within each 211 // package but packages are visited in arbitrary order, 212 // so for determinism we sort them. Line 0 is a caption. 213 if q.verb == "referrers" { 214 sort.Strings(outputs[1:]) 215 } 216 217 for _, output := range outputs { 218 fmt.Fprintf(out, "%s\n", output) 219 } 220 221 if !json { 222 io.WriteString(out, "\n") 223 } 224 } 225 226 func TestGuru(t *testing.T) { 227 if testing.Short() { 228 // These tests are super slow. 229 // TODO: make a lighter version of the tests for short mode? 230 t.Skipf("skipping in short mode") 231 } 232 switch runtime.GOOS { 233 case "android": 234 t.Skipf("skipping test on %q (no testdata dir)", runtime.GOOS) 235 case "windows": 236 t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS) 237 } 238 239 for _, filename := range []string{ 240 "testdata/src/alias/alias.go", 241 "testdata/src/calls/main.go", 242 "testdata/src/describe/main.go", 243 "testdata/src/freevars/main.go", 244 "testdata/src/implements/main.go", 245 "testdata/src/implements-methods/main.go", 246 "testdata/src/imports/main.go", 247 "testdata/src/peers/main.go", 248 "testdata/src/pointsto/main.go", 249 "testdata/src/referrers/main.go", 250 "testdata/src/reflection/main.go", 251 "testdata/src/what/main.go", 252 "testdata/src/whicherrs/main.go", 253 "testdata/src/softerrs/main.go", 254 // JSON: 255 // TODO(adonovan): most of these are very similar; combine them. 256 "testdata/src/calls-json/main.go", 257 "testdata/src/peers-json/main.go", 258 "testdata/src/definition-json/main.go", 259 "testdata/src/describe-json/main.go", 260 "testdata/src/implements-json/main.go", 261 "testdata/src/implements-methods-json/main.go", 262 "testdata/src/pointsto-json/main.go", 263 "testdata/src/referrers-json/main.go", 264 "testdata/src/what-json/main.go", 265 } { 266 filename := filename 267 name := strings.Split(filename, "/")[2] 268 t.Run(name, func(t *testing.T) { 269 t.Parallel() 270 if filename == "testdata/src/referrers/main.go" && runtime.GOOS == "plan9" { 271 // Disable this test on plan9 since it expects a particular 272 // wording for a "no such file or directory" error. 273 t.Skip() 274 } 275 json := strings.Contains(filename, "-json/") 276 queries := parseQueries(t, filename) 277 golden := filename + "lden" 278 gotfh, err := ioutil.TempFile("", filepath.Base(filename)+"t") 279 if err != nil { 280 t.Fatal(err) 281 } 282 got := gotfh.Name() 283 defer func() { 284 gotfh.Close() 285 os.Remove(got) 286 }() 287 288 // Run the guru on each query, redirecting its output 289 // and error (if any) to the foo.got file. 290 for _, q := range queries { 291 doQuery(gotfh, q, json) 292 } 293 294 // Compare foo.got with foo.golden. 295 var cmd *exec.Cmd 296 switch runtime.GOOS { 297 case "plan9": 298 cmd = exec.Command("/bin/diff", "-c", golden, got) 299 default: 300 cmd = exec.Command("/usr/bin/diff", "-u", golden, got) 301 } 302 testenv.NeedsTool(t, cmd.Path) 303 buf := new(bytes.Buffer) 304 cmd.Stdout = buf 305 cmd.Stderr = os.Stderr 306 if err := cmd.Run(); err != nil { 307 t.Errorf("Guru tests for %s failed: %s.\n%s\n", 308 filename, err, buf) 309 310 if *updateFlag { 311 t.Logf("Updating %s...", golden) 312 if err := exec.Command("/bin/cp", got, golden).Run(); err != nil { 313 t.Errorf("Update failed: %s", err) 314 } 315 } 316 } 317 }) 318 } 319 } 320 321 func TestIssue14684(t *testing.T) { 322 var buildContext = build.Default 323 buildContext.GOPATH = "testdata" 324 query := guru.Query{ 325 Pos: "testdata/src/README.txt:#1", 326 Build: &buildContext, 327 } 328 err := guru.Run("freevars", &query) 329 if err == nil { 330 t.Fatal("guru query succeeded unexpectedly") 331 } 332 if got, want := err.Error(), "testdata/src/README.txt is not a Go source file"; got != want { 333 t.Errorf("query error was %q, want %q", got, want) 334 } 335 }