// Copyright 2021 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package vta import ( "testing" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/buildssa" "golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" ) func TestVTACallGraph(t *testing.T) { for _, file := range []string{ "testdata/src/callgraph_static.go", "testdata/src/callgraph_ho.go", "testdata/src/callgraph_interfaces.go", "testdata/src/callgraph_pointers.go", "testdata/src/callgraph_collections.go", "testdata/src/callgraph_fields.go", "testdata/src/callgraph_field_funcs.go", "testdata/src/callgraph_recursive_types.go", "testdata/src/callgraph_issue_57756.go", "testdata/src/callgraph_comma_maps.go", "testdata/src/callgraph_type_aliases.go", } { t.Run(file, func(t *testing.T) { prog, want, err := testProg(file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } if len(want) == 0 { t.Fatalf("couldn't find want in `%s`", file) } g := CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) got := callGraphStr(g) if diff := setdiff(want, got); len(diff) > 0 { t.Errorf("computed callgraph %v\nshould contain\n%v\n(diff: %v)", got, want, diff) } }) } } // TestVTAProgVsFuncSet exemplifies and tests different possibilities // enabled by having an arbitrary function set as input to CallGraph // instead of the whole program (i.e., ssautil.AllFunctions(prog)). func TestVTAProgVsFuncSet(t *testing.T) { prog, want, err := testProg("testdata/src/callgraph_nested_ptr.go", ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test `testdata/src/callgraph_nested_ptr.go`: %s", err) } if len(want) == 0 { t.Fatal("couldn't find want in `testdata/src/callgraph_nested_ptr.go`") } allFuncs := ssautil.AllFunctions(prog) g := CallGraph(allFuncs, cha.CallGraph(prog)) // VTA over the whole program will produce a call graph that // includes Baz:(**i).Foo -> A.Foo, B.Foo. got := callGraphStr(g) if diff := setdiff(want, got); len(diff) > 0 { t.Errorf("computed callgraph %v should contain %v (diff: %v)", got, want, diff) } // Prune the set of program functions to exclude Bar(). This should // yield a call graph that includes different set of callees for Baz // Baz:(**i).Foo -> A.Foo // // Note that the exclusion of Bar can happen, for instance, if Baz is // considered an entry point of some data flow analysis and Bar is // provably (e.g., using CHA forward reachability) unreachable from Baz. noBarFuncs := make(map[*ssa.Function]bool) for f, in := range allFuncs { noBarFuncs[f] = in && (funcName(f) != "Bar") } want = []string{"Baz: Do(i) -> Do; invoke t2.Foo() -> A.Foo"} g = CallGraph(noBarFuncs, cha.CallGraph(prog)) got = callGraphStr(g) if diff := setdiff(want, got); len(diff) > 0 { t.Errorf("pruned callgraph %v should contain %v (diff: %v)", got, want, diff) } } // TestVTAPanicMissingDefinitions tests if VTA gracefully handles the case // where VTA panics when a definition of a function or method is not // available, which can happen when using analysis package. A successful // test simply does not panic. func TestVTAPanicMissingDefinitions(t *testing.T) { run := func(pass *analysis.Pass) (interface{}, error) { s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) CallGraph(ssautil.AllFunctions(s.Pkg.Prog), cha.CallGraph(s.Pkg.Prog)) return nil, nil } analyzer := &analysis.Analyzer{ Name: "test", Doc: "test", Run: run, Requires: []*analysis.Analyzer{ buildssa.Analyzer, }, } testdata := analysistest.TestData() res := analysistest.Run(t, testdata, analyzer, "t", "d") if len(res) != 2 { t.Errorf("want analysis results for 2 packages; got %v", len(res)) } for _, r := range res { if r.Err != nil { t.Errorf("want no error for package %v; got %v", r.Pass.Pkg.Path(), r.Err) } } } func TestVTACallGraphGenerics(t *testing.T) { // TODO(zpavlinovic): add more tests files := []string{ "testdata/src/arrays_generics.go", "testdata/src/callgraph_generics.go", "testdata/src/issue63146.go", } for _, file := range files { t.Run(file, func(t *testing.T) { prog, want, err := testProg(file, ssa.InstantiateGenerics) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } if len(want) == 0 { t.Fatalf("couldn't find want in `%s`", file) } g := CallGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) got := callGraphStr(g) if diff := setdiff(want, got); len(diff) != 0 { t.Errorf("computed callgraph %v should contain %v (diff: %v)", got, want, diff) logFns(t, prog) } }) } } func TestVTACallGraphGo117(t *testing.T) { file := "testdata/src/go117.go" prog, want, err := testProg(file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } if len(want) == 0 { t.Fatalf("couldn't find want in `%s`", file) } g, _ := typePropGraph(ssautil.AllFunctions(prog), cha.CallGraph(prog)) got := vtaGraphStr(g) if diff := setdiff(want, got); len(diff) != 0 { t.Errorf("`%s`: want superset of %v;\n got %v", file, want, got) } }