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  }