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 }