golang.org/x/tools/gopls@v0.15.3/internal/cache/metadata/cycle_test.go (about) 1 // Copyright 2023 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 metadata 6 7 import ( 8 "sort" 9 "strings" 10 "testing" 11 12 "golang.org/x/tools/gopls/internal/util/bug" 13 ) 14 15 func init() { 16 bug.PanicOnBugs = true 17 } 18 19 // This is an internal test of the breakImportCycles logic. 20 func TestBreakImportCycles(t *testing.T) { 21 22 // parse parses an import dependency graph. 23 // The input is a semicolon-separated list of node descriptions. 24 // Each node description is a package ID, optionally followed by 25 // "->" and a comma-separated list of successor IDs. 26 // Thus "a->b;b->c,d;e" represents the set of nodes {a,b,e} 27 // and the set of edges {a->b, b->c, b->d}. 28 parse := func(s string) map[PackageID]*Package { 29 m := make(map[PackageID]*Package) 30 makeNode := func(name string) *Package { 31 id := PackageID(name) 32 n, ok := m[id] 33 if !ok { 34 n = &Package{ 35 ID: id, 36 DepsByPkgPath: make(map[PackagePath]PackageID), 37 } 38 m[id] = n 39 } 40 return n 41 } 42 if s != "" { 43 for _, item := range strings.Split(s, ";") { 44 nodeID, succIDs, ok := strings.Cut(item, "->") 45 node := makeNode(nodeID) 46 if ok { 47 for _, succID := range strings.Split(succIDs, ",") { 48 node.DepsByPkgPath[PackagePath(succID)] = PackageID(succID) 49 } 50 } 51 } 52 } 53 return m 54 } 55 56 // Sanity check of cycle detector. 57 { 58 got := cyclic(parse("a->b;b->c;c->a,d")) 59 has := func(s string) bool { return strings.Contains(got, s) } 60 if !(has("a->b") && has("b->c") && has("c->a") && !has("d")) { 61 t.Fatalf("cyclic: got %q, want a->b->c->a or equivalent", got) 62 } 63 } 64 65 // format formats an import graph, in lexicographic order, 66 // in the notation of parse, but with a "!" after the name 67 // of each node that has errors. 68 format := func(graph map[PackageID]*Package) string { 69 var items []string 70 for _, mp := range graph { 71 item := string(mp.ID) 72 if len(mp.Errors) > 0 { 73 item += "!" 74 } 75 var succs []string 76 for _, depID := range mp.DepsByPkgPath { 77 succs = append(succs, string(depID)) 78 } 79 if succs != nil { 80 sort.Strings(succs) 81 item += "->" + strings.Join(succs, ",") 82 } 83 items = append(items, item) 84 } 85 sort.Strings(items) 86 return strings.Join(items, ";") 87 } 88 89 // We needn't test self-cycles as they are eliminated at Metadata construction. 90 for _, test := range []struct { 91 metadata, updates, want string 92 }{ 93 // Simple 2-cycle. 94 {"a->b", "b->a", 95 "a->b;b!"}, // broke b->a 96 97 {"a->b;b->c;c", "b->a,c", 98 "a->b;b!->c;c"}, // broke b->a 99 100 // Reversing direction of p->s edge creates pqrs cycle. 101 {"a->p,q,r,s;p->q,s,z;q->r,z;r->s,z;s->z", "p->q,z;s->p,z", 102 "a->p,q,r,s;p!->z;q->r,z;r->s,z;s!->z"}, // broke p->q, s->p 103 104 // We break all intra-SCC edges from updated nodes, 105 // which may be more than necessary (e.g. a->b). 106 {"a->b;b->c;c;d->a", "a->b,e;c->d", 107 "a!->e;b->c;c!;d->a"}, // broke a->b, c->d 108 } { 109 metadata := parse(test.metadata) 110 updates := parse(test.updates) 111 112 if cycle := cyclic(metadata); cycle != "" { 113 t.Errorf("initial metadata %s has cycle %s: ", format(metadata), cycle) 114 continue 115 } 116 117 t.Log("initial", format(metadata)) 118 119 // Apply updates. 120 // (parse doesn't have a way to express node deletions, 121 // but they aren't very interesting.) 122 for id, mp := range updates { 123 metadata[id] = mp 124 } 125 126 t.Log("updated", format(metadata)) 127 128 // breakImportCycles accesses only these fields of Metadata: 129 // DepsByImpPath, ID - read 130 // DepsByPkgPath - read, updated 131 // Errors - updated 132 breakImportCycles(metadata, updates) 133 134 t.Log("acyclic", format(metadata)) 135 136 if cycle := cyclic(metadata); cycle != "" { 137 t.Errorf("resulting metadata %s has cycle %s: ", format(metadata), cycle) 138 } 139 140 got := format(metadata) 141 if got != test.want { 142 t.Errorf("test.metadata=%s test.updates=%s: got=%s want=%s", 143 test.metadata, test.updates, got, test.want) 144 } 145 } 146 }