cuelang.org/go@v0.13.0/internal/core/toposort/graph_test.go (about)

     1  // Copyright 2024 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package toposort_test
    16  
    17  import (
    18  	"fmt"
    19  	"math/rand"
    20  	"os"
    21  	"slices"
    22  	"strconv"
    23  	"strings"
    24  	"testing"
    25  
    26  	"cuelang.org/go/internal/core/adt"
    27  	"cuelang.org/go/internal/core/runtime"
    28  	"cuelang.org/go/internal/core/toposort"
    29  )
    30  
    31  func TestSort(t *testing.T) {
    32  	type MergeTestCase struct {
    33  		name     string
    34  		inputs   [][]string
    35  		expected []string
    36  	}
    37  
    38  	a, b, c, d, e, f, g, h := "a", "b", "c", "d", "e", "f", "g", "h"
    39  
    40  	testCases := []MergeTestCase{
    41  		{
    42  			name:     "simple two",
    43  			inputs:   [][]string{{c, b}, {d, a}},
    44  			expected: []string{c, b, d, a},
    45  		},
    46  		{
    47  			name:     "simple three",
    48  			inputs:   [][]string{{c, b}, {d, a}, {f, e}},
    49  			expected: []string{c, b, d, a, f, e},
    50  		},
    51  		{
    52  			name:     "linked linear two",
    53  			inputs:   [][]string{{b, c}, {c, a}},
    54  			expected: []string{b, c, a},
    55  		},
    56  		{
    57  			name:     "linked linear two multiple",
    58  			inputs:   [][]string{{b, c, f, d, g}, {c, a, e, d}},
    59  			expected: []string{b, c, a, e, f, d, g},
    60  		},
    61  		{
    62  			name:     "linked linear three",
    63  			inputs:   [][]string{{b, c}, {c, d, a, f}, {a, f, e}},
    64  			expected: []string{b, c, d, a, f, e},
    65  		},
    66  		{
    67  			name:   "simple cycle",
    68  			inputs: [][]string{{h, b, a}, {a, b}, {h, c, d}, {d, c}},
    69  			// 3 SCCs: {h}, {a, b}, and {c,d}.
    70  			expected: []string{h, a, b, c, d},
    71  		},
    72  		{
    73  			name:   "nested cycles",
    74  			inputs: [][]string{{g, b, c}, {e, c, b, d}, {d, f, a, e}, {a, h, f}},
    75  			// 2 SCCs: {g}, and {a,b,c,d,e,f,h}.
    76  			expected: []string{g, a, b, c, d, e, f, h},
    77  		},
    78  		{
    79  			name: "fully connected 4",
    80  			inputs: [][]string{
    81  				{a, b, c, d}, {d, c, b, a}, {b, d, a, c}, {c, a, d, b},
    82  			},
    83  			expected: []string{a, b, c, d},
    84  		},
    85  	}
    86  
    87  	index := runtime.New()
    88  
    89  	for _, tc := range testCases {
    90  		t.Run(tc.name, func(t *testing.T) {
    91  			testAllPermutations(t, index, tc.inputs,
    92  				func(t *testing.T, perm [][]adt.Feature, graph *toposort.Graph) {
    93  					sortedNames := featuresNames(index, graph.Sort(index))
    94  					if !slices.Equal(sortedNames, tc.expected) {
    95  						t.Fatalf(`
    96  For permutation: %v
    97         Expected: %v
    98              Got: %v`,
    99  							permutationNames(index, perm), tc.expected, sortedNames)
   100  					}
   101  				})
   102  		})
   103  	}
   104  }
   105  
   106  func TestSortFullyConnected(t *testing.T) {
   107  	// In a fully connected graph of 12 nodes, there are 119,481,284
   108  	// cycles. The number of cycles grows with the factorial of the
   109  	// number of nodes. Any attempt to calculate and analyse the cycles
   110  	// here takes ~100 seconds at the time of writing.
   111  	//
   112  	// This test is not part of TestSort, because TestSort would
   113  	// attempt to test every permutation of the inputs, which is itself
   114  	// a factorial. So here we just test a single permutation, to
   115  	// ensure that any performance issues we see are those of the graph
   116  	// sorting, and not the test calculation permutations.
   117  	names := strings.Split("abcdefghijkl", "")
   118  	var inputs [][]string
   119  	for _, left := range names {
   120  		for _, right := range names {
   121  			if left == right {
   122  				continue
   123  			}
   124  			inputs = append(inputs, []string{left, right})
   125  		}
   126  	}
   127  
   128  	index := runtime.New()
   129  	features := makeFeatures(index, inputs)
   130  	graph := buildGraphFromPermutation(features)
   131  	sortedNames := featuresNames(index, graph.Sort(index))
   132  	if !slices.Equal(sortedNames, names) {
   133  		t.Fatalf(`
   134         Expected: %v
   135              Got: %v`,
   136  			names, sortedNames)
   137  	}
   138  }
   139  
   140  func TestSortRandom(t *testing.T) {
   141  	seed := rand.Int63()
   142  	if str := os.Getenv("SEED"); str != "" {
   143  		num, err := strconv.ParseInt(str, 10, 64)
   144  		if err != nil {
   145  			t.Fatalf("Could not parse SEED env var %q: %v", str, err)
   146  			return
   147  		}
   148  		seed = num
   149  	}
   150  	t.Log("Seed", seed)
   151  	rng := rand.New(rand.NewSource(seed))
   152  
   153  	names := strings.Split("abcdefghijklm", "")
   154  	index := runtime.New()
   155  
   156  	for n := 0; n < 100; n++ {
   157  		inputs := make([][]string, 2+rng.Intn(4))
   158  		for i := range inputs {
   159  			names := slices.Clone(names)
   160  			rng.Shuffle(len(names),
   161  				func(i, j int) { names[i], names[j] = names[j], names[i] })
   162  			inputs[i] = names[:2+rng.Intn(4)]
   163  		}
   164  
   165  		t.Run(fmt.Sprint(n), func(t *testing.T) {
   166  			t.Log("inputs:", inputs)
   167  
   168  			var expected []string
   169  			testAllPermutations(t, index, inputs,
   170  				func(t *testing.T, perm [][]adt.Feature, graph *toposort.Graph) {
   171  					sortedNames := featuresNames(index, graph.Sort(index))
   172  					if expected == nil {
   173  						expected = sortedNames
   174  						t.Log("First result:", expected)
   175  						usedNames := make(map[string]struct{}, len(expected))
   176  						for _, name := range expected {
   177  							usedNames[name] = struct{}{}
   178  						}
   179  						for _, input := range inputs {
   180  							for _, name := range input {
   181  								if _, found := usedNames[name]; !found {
   182  									t.Fatalf(`
   183  Input %v contains name %q, but that does not appear in the output: %v`,
   184  										input, name, expected)
   185  								}
   186  							}
   187  						}
   188  					} else if !slices.Equal(sortedNames, expected) {
   189  						t.Fatalf(`
   190  For permutation: %v
   191         Expected: %v
   192              Got: %v`,
   193  							permutationNames(index, perm), expected, sortedNames)
   194  					}
   195  				})
   196  		})
   197  	}
   198  }
   199  
   200  func makeFeatures(index adt.StringIndexer, inputs [][]string) [][]adt.Feature {
   201  	result := make([][]adt.Feature, len(inputs))
   202  	for i, names := range inputs {
   203  		features := make([]adt.Feature, len(names))
   204  		for j, name := range names {
   205  			features[j] = adt.MakeStringLabel(index, name)
   206  		}
   207  		result[i] = features
   208  	}
   209  	return result
   210  }
   211  
   212  // Consider that names are nodes in a cycle, we want to rotate the
   213  // slice so that it starts at the given node name. This modifies the
   214  // names slice in-place.
   215  func rotateToStartAt(names []string, start string) {
   216  	if start == names[0] {
   217  		return
   218  	}
   219  	for i, node := range names {
   220  		if start == node {
   221  			prefix := slices.Clone(names[:i])
   222  			copy(names, names[i:])
   223  			copy(names[len(names)-i:], prefix)
   224  			break
   225  		}
   226  	}
   227  }
   228  
   229  func allPermutations(featureses [][]adt.Feature) [][][]adt.Feature {
   230  	nonNilIdx := -1
   231  	var results [][][]adt.Feature
   232  	for i, features := range featureses {
   233  		if features == nil {
   234  			continue
   235  		}
   236  		nonNilIdx = i
   237  		featureses[i] = nil
   238  		for _, result := range allPermutations(featureses) {
   239  			results = append(results, append(result, features))
   240  		}
   241  		featureses[i] = features
   242  	}
   243  	if len(results) == 0 && nonNilIdx != -1 {
   244  		return [][][]adt.Feature{{featureses[nonNilIdx]}}
   245  	}
   246  	return results
   247  }
   248  
   249  func permutationNames(index adt.StringIndexer, permutation [][]adt.Feature) [][]string {
   250  	permNames := make([][]string, len(permutation))
   251  	for i, features := range permutation {
   252  		permNames[i] = featuresNames(index, features)
   253  	}
   254  	return permNames
   255  }
   256  
   257  func featuresNames(index adt.StringIndexer, features []adt.Feature) []string {
   258  	names := make([]string, len(features))
   259  	for i, feature := range features {
   260  		names[i] = feature.StringValue(index)
   261  	}
   262  	return names
   263  }
   264  
   265  func buildGraphFromPermutation(permutation [][]adt.Feature) *toposort.Graph {
   266  	builder := toposort.NewGraphBuilder(true)
   267  
   268  	for _, chain := range permutation {
   269  		if len(chain) == 0 {
   270  			continue
   271  		}
   272  
   273  		prev := chain[0]
   274  		builder.EnsureNode(prev)
   275  		for _, cur := range chain[1:] {
   276  			builder.AddEdge(prev, cur)
   277  			prev = cur
   278  		}
   279  	}
   280  	return builder.Build()
   281  }
   282  
   283  func testAllPermutations(t *testing.T, index adt.StringIndexer, inputs [][]string, fun func(*testing.T, [][]adt.Feature, *toposort.Graph)) {
   284  	features := makeFeatures(index, inputs)
   285  	for i, permutation := range allPermutations(features) {
   286  		t.Run(fmt.Sprint(i), func(t *testing.T) {
   287  			graph := buildGraphFromPermutation(permutation)
   288  			fun(t, permutation, graph)
   289  		})
   290  	}
   291  }
   292  
   293  func TestAllPermutations(t *testing.T) {
   294  	a, b, c, d := []string{"a"}, []string{"b"}, []string{"c"}, []string{"d"}
   295  
   296  	type PermutationTestCase struct {
   297  		name     string
   298  		inputs   [][]string
   299  		expected [][][]string
   300  	}
   301  
   302  	testCases := []PermutationTestCase{
   303  		{
   304  			name: "empty",
   305  		},
   306  		{
   307  			name:     "one",
   308  			inputs:   [][]string{a},
   309  			expected: [][][]string{{a}},
   310  		},
   311  		{
   312  			name:     "two",
   313  			inputs:   [][]string{a, b},
   314  			expected: [][][]string{{b, a}, {a, b}},
   315  		},
   316  		{
   317  			name:   "three",
   318  			inputs: [][]string{a, b, c},
   319  			expected: [][][]string{
   320  				{c, b, a}, {b, c, a}, {c, a, b}, {a, c, b}, {b, a, c}, {a, b, c},
   321  			},
   322  		},
   323  		{
   324  			name:   "four",
   325  			inputs: [][]string{a, b, c, d},
   326  			expected: [][][]string{
   327  				{d, c, b, a}, {c, d, b, a}, {d, b, c, a}, {b, d, c, a}, {c, b, d, a}, {b, c, d, a},
   328  				{d, c, a, b}, {c, d, a, b}, {d, a, c, b}, {a, d, c, b}, {c, a, d, b}, {a, c, d, b},
   329  				{d, b, a, c}, {b, d, a, c}, {d, a, b, c}, {a, d, b, c}, {b, a, d, c}, {a, b, d, c},
   330  				{c, b, a, d}, {b, c, a, d}, {c, a, b, d}, {a, c, b, d}, {b, a, c, d}, {a, b, c, d},
   331  			},
   332  		},
   333  	}
   334  
   335  	index := runtime.New()
   336  
   337  	for _, tc := range testCases {
   338  		t.Run(tc.name, func(t *testing.T) {
   339  			fs := makeFeatures(index, tc.inputs)
   340  			permutations := allPermutations(fs)
   341  			permutationsNames := make([][][]string, len(permutations))
   342  			for i, permutation := range permutations {
   343  				permutationsNames[i] = permutationNames(index, permutation)
   344  			}
   345  
   346  			if !slices.EqualFunc(permutationsNames, tc.expected,
   347  				func(gotPerm, expectedPerm [][]string) bool {
   348  					return slices.EqualFunc(gotPerm, expectedPerm, slices.Equal)
   349  				}) {
   350  				t.Fatalf(`
   351  For inputs: %v
   352    Expected: %v
   353         Got: %v`,
   354  					tc.inputs, tc.expected, permutations)
   355  			}
   356  		})
   357  	}
   358  }