github.com/sdboyer/gps@v0.16.3/solve_test.go (about) 1 package gps 2 3 import ( 4 "bytes" 5 "flag" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "math/rand" 10 "reflect" 11 "sort" 12 "strconv" 13 "strings" 14 "testing" 15 "unicode" 16 17 "github.com/sdboyer/gps/internal" 18 "github.com/sdboyer/gps/pkgtree" 19 ) 20 21 var fixtorun string 22 23 // TODO(sdboyer) regression test ensuring that locks with only revs for projects don't cause errors 24 func init() { 25 flag.StringVar(&fixtorun, "gps.fix", "", "A single fixture to run in TestBasicSolves or TestBimodalSolves") 26 mkBridge(nil, nil, false) 27 overrideMkBridge() 28 overrideIsStdLib() 29 } 30 31 // sets the mkBridge global func to one that allows virtualized RootDirs 32 func overrideMkBridge() { 33 // For all tests, override the base bridge with the depspecBridge that skips 34 // verifyRootDir calls 35 mkBridge = func(s *solver, sm SourceManager, down bool) sourceBridge { 36 return &depspecBridge{ 37 &bridge{ 38 sm: sm, 39 s: s, 40 down: down, 41 vlists: make(map[ProjectIdentifier][]Version), 42 }, 43 } 44 } 45 } 46 47 // sets the isStdLib func to always return false, otherwise it would identify 48 // pretty much all of our fixtures as being stdlib and skip everything 49 func overrideIsStdLib() { 50 internal.IsStdLib = func(path string) bool { 51 return false 52 } 53 } 54 55 type testlogger struct { 56 *testing.T 57 } 58 59 func (t testlogger) Write(b []byte) (n int, err error) { 60 str := string(b) 61 if len(str) == 0 { 62 return 0, nil 63 } 64 65 for _, part := range strings.Split(str, "\n") { 66 str := strings.TrimRightFunc(part, unicode.IsSpace) 67 if len(str) != 0 { 68 t.T.Log(str) 69 } 70 } 71 return len(b), err 72 } 73 74 func fixSolve(params SolveParameters, sm SourceManager, t *testing.T) (Solution, error) { 75 // Trace unconditionally; by passing the trace through t.Log(), the testing 76 // system will decide whether or not to actually show the output (based on 77 // -v, or selectively on test failure). 78 params.Trace = true 79 params.TraceLogger = log.New(testlogger{T: t}, "", 0) 80 81 s, err := Prepare(params, sm) 82 if err != nil { 83 return nil, err 84 } 85 86 return s.Solve() 87 } 88 89 // Test all the basic table fixtures. 90 // 91 // Or, just the one named in the fix arg. 92 func TestBasicSolves(t *testing.T) { 93 if fixtorun != "" { 94 if fix, exists := basicFixtures[fixtorun]; exists { 95 solveBasicsAndCheck(fix, t) 96 } 97 } else { 98 // sort them by their keys so we get stable output 99 var names []string 100 for n := range basicFixtures { 101 names = append(names, n) 102 } 103 104 sort.Strings(names) 105 for _, n := range names { 106 t.Run(n, func(t *testing.T) { 107 //t.Parallel() // until trace output is fixed in parallel 108 solveBasicsAndCheck(basicFixtures[n], t) 109 }) 110 } 111 } 112 } 113 114 func solveBasicsAndCheck(fix basicFixture, t *testing.T) (res Solution, err error) { 115 sm := newdepspecSM(fix.ds, nil) 116 117 params := SolveParameters{ 118 RootDir: string(fix.ds[0].n), 119 RootPackageTree: fix.rootTree(), 120 Manifest: fix.rootmanifest(), 121 Lock: dummyLock{}, 122 Downgrade: fix.downgrade, 123 ChangeAll: fix.changeall, 124 ToChange: fix.changelist, 125 ProjectAnalyzer: naiveAnalyzer{}, 126 } 127 128 if fix.l != nil { 129 params.Lock = fix.l 130 } 131 132 res, err = fixSolve(params, sm, t) 133 134 return fixtureSolveSimpleChecks(fix, res, err, t) 135 } 136 137 // Test all the bimodal table fixtures. 138 // 139 // Or, just the one named in the fix arg. 140 func TestBimodalSolves(t *testing.T) { 141 if fixtorun != "" { 142 if fix, exists := bimodalFixtures[fixtorun]; exists { 143 solveBimodalAndCheck(fix, t) 144 } 145 } else { 146 // sort them by their keys so we get stable output 147 var names []string 148 for n := range bimodalFixtures { 149 names = append(names, n) 150 } 151 152 sort.Strings(names) 153 for _, n := range names { 154 t.Run(n, func(t *testing.T) { 155 //t.Parallel() // until trace output is fixed in parallel 156 solveBimodalAndCheck(bimodalFixtures[n], t) 157 }) 158 } 159 } 160 } 161 162 func solveBimodalAndCheck(fix bimodalFixture, t *testing.T) (res Solution, err error) { 163 sm := newbmSM(fix) 164 165 params := SolveParameters{ 166 RootDir: string(fix.ds[0].n), 167 RootPackageTree: fix.rootTree(), 168 Manifest: fix.rootmanifest(), 169 Lock: dummyLock{}, 170 Downgrade: fix.downgrade, 171 ChangeAll: fix.changeall, 172 ProjectAnalyzer: naiveAnalyzer{}, 173 } 174 175 if fix.l != nil { 176 params.Lock = fix.l 177 } 178 179 res, err = fixSolve(params, sm, t) 180 181 return fixtureSolveSimpleChecks(fix, res, err, t) 182 } 183 184 func fixtureSolveSimpleChecks(fix specfix, soln Solution, err error, t *testing.T) (Solution, error) { 185 ppi := func(id ProjectIdentifier) string { 186 // need this so we can clearly tell if there's a Source or not 187 if id.Source == "" { 188 return string(id.ProjectRoot) 189 } 190 return fmt.Sprintf("%s (from %s)", id.ProjectRoot, id.Source) 191 } 192 193 pv := func(v Version) string { 194 if pv, ok := v.(PairedVersion); ok { 195 return fmt.Sprintf("%s (%s)", pv.Unpair(), pv.Underlying()) 196 } 197 return v.String() 198 } 199 200 fixfail := fix.failure() 201 if err != nil { 202 if fixfail == nil { 203 t.Errorf("Solve failed unexpectedly:\n%s", err) 204 } else if !reflect.DeepEqual(fixfail, err) { 205 // TODO(sdboyer) reflect.DeepEqual works for now, but once we start 206 // modeling more complex cases, this should probably become more robust 207 t.Errorf("Failure mismatch:\n\t(GOT): %s\n\t(WNT): %s", err, fixfail) 208 } 209 } else if fixfail != nil { 210 var buf bytes.Buffer 211 fmt.Fprintf(&buf, "Solver succeeded, but expecting failure:\n%s\nProjects in solution:", fixfail) 212 for _, p := range soln.Projects() { 213 fmt.Fprintf(&buf, "\n\t- %s at %s", ppi(p.Ident()), p.Version()) 214 } 215 t.Error(buf.String()) 216 } else { 217 r := soln.(solution) 218 if fix.maxTries() > 0 && r.Attempts() > fix.maxTries() { 219 t.Errorf("Solver completed in %v attempts, but expected %v or fewer", r.att, fix.maxTries()) 220 } 221 222 // Dump result projects into a map for easier interrogation 223 rp := make(map[ProjectIdentifier]LockedProject) 224 for _, lp := range r.p { 225 rp[lp.pi] = lp 226 } 227 228 fixlen, rlen := len(fix.solution()), len(rp) 229 if fixlen != rlen { 230 // Different length, so they definitely disagree 231 t.Errorf("Solver reported %v package results, result expected %v", rlen, fixlen) 232 } 233 234 // Whether or not len is same, still have to verify that results agree 235 // Walk through fixture/expected results first 236 for id, flp := range fix.solution() { 237 if lp, exists := rp[id]; !exists { 238 t.Errorf("Project %q expected but missing from results", ppi(id)) 239 } else { 240 // delete result from map so we skip it on the reverse pass 241 delete(rp, id) 242 if flp.Version() != lp.Version() { 243 t.Errorf("Expected version %q of project %q, but actual version was %q", pv(flp.Version()), ppi(id), pv(lp.Version())) 244 } 245 246 if !reflect.DeepEqual(lp.pkgs, flp.pkgs) { 247 t.Errorf("Package list was not not as expected for project %s@%s:\n\t(GOT) %s\n\t(WNT) %s", ppi(id), pv(lp.Version()), lp.pkgs, flp.pkgs) 248 } 249 } 250 } 251 252 // Now walk through remaining actual results 253 for id, lp := range rp { 254 if _, exists := fix.solution()[id]; !exists { 255 t.Errorf("Unexpected project %s@%s present in results, with pkgs:\n\t%s", ppi(id), pv(lp.Version()), lp.pkgs) 256 } 257 } 258 } 259 260 return soln, err 261 } 262 263 // This tests that, when a root lock is underspecified (has only a version) we 264 // don't allow a match on that version from a rev in the manifest. We may allow 265 // this in the future, but disallow it for now because going from an immutable 266 // requirement to a mutable lock automagically is a bad direction that could 267 // produce weird side effects. 268 func TestRootLockNoVersionPairMatching(t *testing.T) { 269 fix := basicFixture{ 270 n: "does not match unpaired lock versions with paired real versions", 271 ds: []depspec{ 272 mkDepspec("root 0.0.0", "foo *"), // foo's constraint rewritten below to foorev 273 mkDepspec("foo 1.0.0", "bar 1.0.0"), 274 mkDepspec("foo 1.0.1 foorev", "bar 1.0.1"), 275 mkDepspec("foo 1.0.2 foorev", "bar 1.0.2"), 276 mkDepspec("bar 1.0.0"), 277 mkDepspec("bar 1.0.1"), 278 mkDepspec("bar 1.0.2"), 279 }, 280 l: mklock( 281 "foo 1.0.1", 282 ), 283 r: mksolution( 284 "foo 1.0.2 foorev", 285 "bar 1.0.2", 286 ), 287 } 288 289 pd := fix.ds[0].deps[0] 290 pd.Constraint = Revision("foorev") 291 fix.ds[0].deps[0] = pd 292 293 sm := newdepspecSM(fix.ds, nil) 294 295 l2 := make(fixLock, 1) 296 copy(l2, fix.l) 297 l2[0].v = nil 298 299 params := SolveParameters{ 300 RootDir: string(fix.ds[0].n), 301 RootPackageTree: fix.rootTree(), 302 Manifest: fix.rootmanifest(), 303 Lock: l2, 304 ProjectAnalyzer: naiveAnalyzer{}, 305 } 306 307 res, err := fixSolve(params, sm, t) 308 309 fixtureSolveSimpleChecks(fix, res, err, t) 310 } 311 312 // TestBadSolveOpts exercises the different possible inputs to a solver that can 313 // be determined as invalid in Prepare(), without any further work 314 func TestBadSolveOpts(t *testing.T) { 315 pn := strconv.FormatInt(rand.Int63(), 36) 316 fix := basicFixtures["no dependencies"] 317 fix.ds[0].n = ProjectRoot(pn) 318 319 sm := newdepspecSM(fix.ds, nil) 320 params := SolveParameters{} 321 322 _, err := Prepare(params, nil) 323 if err == nil { 324 t.Errorf("Prepare should have errored on nil SourceManager") 325 } else if !strings.Contains(err.Error(), "non-nil SourceManager") { 326 t.Error("Prepare should have given error on nil SourceManager, but gave:", err) 327 } 328 329 _, err = Prepare(params, sm) 330 if err == nil { 331 t.Errorf("Prepare should have errored without ProjectAnalyzer") 332 } else if !strings.Contains(err.Error(), "must provide a ProjectAnalyzer") { 333 t.Error("Prepare should have given error without ProjectAnalyzer, but gave:", err) 334 } 335 336 params.ProjectAnalyzer = naiveAnalyzer{} 337 _, err = Prepare(params, sm) 338 if err == nil { 339 t.Errorf("Prepare should have errored on empty root") 340 } else if !strings.Contains(err.Error(), "non-empty root directory") { 341 t.Error("Prepare should have given error on empty root, but gave:", err) 342 } 343 344 params.RootDir = pn 345 _, err = Prepare(params, sm) 346 if err == nil { 347 t.Errorf("Prepare should have errored on empty name") 348 } else if !strings.Contains(err.Error(), "non-empty import root") { 349 t.Error("Prepare should have given error on empty import root, but gave:", err) 350 } 351 352 params.RootPackageTree = pkgtree.PackageTree{ 353 ImportRoot: pn, 354 } 355 _, err = Prepare(params, sm) 356 if err == nil { 357 t.Errorf("Prepare should have errored on empty name") 358 } else if !strings.Contains(err.Error(), "at least one package") { 359 t.Error("Prepare should have given error on empty import root, but gave:", err) 360 } 361 362 params.RootPackageTree = pkgtree.PackageTree{ 363 ImportRoot: pn, 364 Packages: map[string]pkgtree.PackageOrErr{ 365 pn: { 366 P: pkgtree.Package{ 367 ImportPath: pn, 368 Name: pn, 369 }, 370 }, 371 }, 372 } 373 params.Trace = true 374 _, err = Prepare(params, sm) 375 if err == nil { 376 t.Errorf("Should have errored on trace with no logger") 377 } else if !strings.Contains(err.Error(), "no logger provided") { 378 t.Error("Prepare should have given error on missing trace logger, but gave:", err) 379 } 380 params.TraceLogger = log.New(ioutil.Discard, "", 0) 381 382 params.Manifest = simpleRootManifest{ 383 ovr: ProjectConstraints{ 384 ProjectRoot("foo"): ProjectProperties{}, 385 }, 386 } 387 _, err = Prepare(params, sm) 388 if err == nil { 389 t.Errorf("Should have errored on override with empty ProjectProperties") 390 } else if !strings.Contains(err.Error(), "foo, but without any non-zero properties") { 391 t.Error("Prepare should have given error override with empty ProjectProperties, but gave:", err) 392 } 393 394 params.Manifest = simpleRootManifest{ 395 ig: map[string]bool{"foo": true}, 396 req: map[string]bool{"foo": true}, 397 } 398 _, err = Prepare(params, sm) 399 if err == nil { 400 t.Errorf("Should have errored on pkg both ignored and required") 401 } else if !strings.Contains(err.Error(), "was given as both a required and ignored package") { 402 t.Error("Prepare should have given error with single ignore/require conflict error, but gave:", err) 403 } 404 405 params.Manifest = simpleRootManifest{ 406 ig: map[string]bool{"foo": true, "bar": true}, 407 req: map[string]bool{"foo": true, "bar": true}, 408 } 409 _, err = Prepare(params, sm) 410 if err == nil { 411 t.Errorf("Should have errored on pkg both ignored and required") 412 } else if !strings.Contains(err.Error(), "multiple packages given as both required and ignored:") { 413 t.Error("Prepare should have given error with multiple ignore/require conflict error, but gave:", err) 414 } 415 params.Manifest = nil 416 417 params.ToChange = []ProjectRoot{"foo"} 418 _, err = Prepare(params, sm) 419 if err == nil { 420 t.Errorf("Should have errored on non-empty ToChange without a lock provided") 421 } else if !strings.Contains(err.Error(), "update specifically requested for") { 422 t.Error("Prepare should have given error on ToChange without Lock, but gave:", err) 423 } 424 425 params.Lock = safeLock{ 426 p: []LockedProject{ 427 NewLockedProject(mkPI("bar"), Revision("makebelieve"), nil), 428 }, 429 } 430 _, err = Prepare(params, sm) 431 if err == nil { 432 t.Errorf("Should have errored on ToChange containing project not in lock") 433 } else if !strings.Contains(err.Error(), "cannot update foo as it is not in the lock") { 434 t.Error("Prepare should have given error on ToChange with item not present in Lock, but gave:", err) 435 } 436 437 params.Lock, params.ToChange = nil, nil 438 _, err = Prepare(params, sm) 439 if err != nil { 440 t.Error("Basic conditions satisfied, prepare should have completed successfully, err as:", err) 441 } 442 443 // swap out the test mkBridge override temporarily, just to make sure we get 444 // the right error 445 mkBridge = func(s *solver, sm SourceManager, down bool) sourceBridge { 446 return &bridge{ 447 sm: sm, 448 s: s, 449 down: down, 450 vlists: make(map[ProjectIdentifier][]Version), 451 } 452 } 453 454 _, err = Prepare(params, sm) 455 if err == nil { 456 t.Errorf("Should have errored on nonexistent root") 457 } else if !strings.Contains(err.Error(), "could not read project root") { 458 t.Error("Prepare should have given error nonexistent project root dir, but gave:", err) 459 } 460 461 // Pointing it at a file should also be an err 462 params.RootDir = "solve_test.go" 463 _, err = Prepare(params, sm) 464 if err == nil { 465 t.Errorf("Should have errored on file for RootDir") 466 } else if !strings.Contains(err.Error(), "is a file, not a directory") { 467 t.Error("Prepare should have given error on file as RootDir, but gave:", err) 468 } 469 470 // swap them back...not sure if this matters, but just in case 471 overrideMkBridge() 472 }