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  }