1// Copyright 2017 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
5package gps
6
7import (
8	"bytes"
9	"context"
10	"fmt"
11	"log"
12	"reflect"
13	"sort"
14	"testing"
15
16	"github.com/golang/dep/internal/test"
17)
18
19// overrideMkBridge overrides the base bridge with the depspecBridge that skips
20// verifyRootDir calls
21func overrideMkBridge(s *solver, sm SourceManager, down bool) sourceBridge {
22	return &depspecBridge{mkBridge(s, sm, down)}
23}
24
25func fixSolve(params SolveParameters, sm SourceManager, t *testing.T) (Solution, error) {
26	// Trace unconditionally; by passing the trace through t.Log(), the testing
27	// system will decide whether or not to actually show the output (based on
28	// -v, or selectively on test failure).
29	params.TraceLogger = log.New(test.Writer{TB: t}, "", 0)
30	// always return false, otherwise it would identify pretty much all of
31	// our fixtures as being stdlib and skip everything
32	params.stdLibFn = func(string) bool { return false }
33	params.mkBridgeFn = overrideMkBridge
34	s, err := Prepare(params, sm)
35	if err != nil {
36		return nil, err
37	}
38
39	return s.Solve(context.Background())
40}
41
42// Test all the basic table fixtures.
43//
44// Or, just the one named in the fix arg.
45func TestBasicSolves(t *testing.T) {
46	// sort them by their keys so we get stable output
47	names := make([]string, 0, len(basicFixtures))
48	for n := range basicFixtures {
49		names = append(names, n)
50	}
51
52	sort.Strings(names)
53	for _, n := range names {
54		n := n
55		t.Run(n, func(t *testing.T) {
56			t.Parallel()
57			solveBasicsAndCheck(basicFixtures[n], t)
58		})
59	}
60}
61
62func solveBasicsAndCheck(fix basicFixture, t *testing.T) (res Solution, err error) {
63	sm := newdepspecSM(fix.ds, nil)
64	if fix.broken != "" {
65		t.Skip(fix.broken)
66	}
67
68	params := SolveParameters{
69		RootDir:         string(fix.ds[0].n),
70		RootPackageTree: fix.rootTree(),
71		Manifest:        fix.rootmanifest(),
72		Lock:            dummyLock{},
73		Downgrade:       fix.downgrade,
74		ChangeAll:       fix.changeall,
75		ToChange:        fix.changelist,
76		ProjectAnalyzer: naiveAnalyzer{},
77	}
78
79	if fix.l != nil {
80		params.Lock = fix.l
81	}
82
83	res, err = fixSolve(params, sm, t)
84
85	return fixtureSolveSimpleChecks(fix, res, err, t)
86}
87
88// Test all the bimodal table fixtures.
89//
90// Or, just the one named in the fix arg.
91func TestBimodalSolves(t *testing.T) {
92	// sort them by their keys so we get stable output
93	names := make([]string, 0, len(bimodalFixtures))
94	for n := range bimodalFixtures {
95		names = append(names, n)
96	}
97
98	sort.Strings(names)
99	for _, n := range names {
100		n := n
101		t.Run(n, func(t *testing.T) {
102			t.Parallel()
103			solveBimodalAndCheck(bimodalFixtures[n], t)
104		})
105	}
106}
107
108func solveBimodalAndCheck(fix bimodalFixture, t *testing.T) (res Solution, err error) {
109	sm := newbmSM(fix)
110	if fix.broken != "" {
111		t.Skip(fix.broken)
112	}
113
114	params := SolveParameters{
115		RootDir:         string(fix.ds[0].n),
116		RootPackageTree: fix.rootTree(),
117		Manifest:        fix.rootmanifest(),
118		Lock:            dummyLock{},
119		Downgrade:       fix.downgrade,
120		ChangeAll:       fix.changeall,
121		ProjectAnalyzer: naiveAnalyzer{},
122	}
123
124	if fix.l != nil {
125		params.Lock = fix.l
126	}
127
128	res, err = fixSolve(params, sm, t)
129
130	return fixtureSolveSimpleChecks(fix, res, err, t)
131}
132
133func fixtureSolveSimpleChecks(fix specfix, soln Solution, err error, t *testing.T) (Solution, error) {
134	ppi := func(id ProjectIdentifier) string {
135		// need this so we can clearly tell if there's a Source or not
136		if id.Source == "" {
137			return string(id.ProjectRoot)
138		}
139		return fmt.Sprintf("%s (from %s)", id.ProjectRoot, id.Source)
140	}
141
142	pv := func(v Version) string {
143		if pv, ok := v.(PairedVersion); ok {
144			return fmt.Sprintf("%s (%s)", pv.Unpair(), pv.Revision())
145		}
146		return v.String()
147	}
148
149	fixfail := fix.failure()
150	if err != nil {
151		if fixfail == nil {
152			t.Errorf("Solve failed unexpectedly:\n%s", err)
153		} else if fixfail.Error() != err.Error() {
154			// TODO(sdboyer) reflect.DeepEqual works for now, but once we start
155			// modeling more complex cases, this should probably become more robust
156			t.Errorf("Failure mismatch:\n\t(GOT): %s\n\t(WNT): %s", err, fixfail)
157		}
158	} else if fixfail != nil {
159		var buf bytes.Buffer
160		fmt.Fprintf(&buf, "Solver succeeded, but expecting failure:\n%s\nProjects in solution:", fixfail)
161		for _, p := range soln.Projects() {
162			fmt.Fprintf(&buf, "\n\t- %s at %s", ppi(p.Ident()), p.Version())
163		}
164		t.Error(buf.String())
165	} else {
166		r := soln.(solution)
167		if fix.maxTries() > 0 && r.Attempts() > fix.maxTries() {
168			t.Errorf("Solver completed in %v attempts, but expected %v or fewer", r.att, fix.maxTries())
169		}
170
171		// Dump result projects into a map for easier interrogation
172		rp := make(map[ProjectIdentifier]LockedProject)
173		for _, lp := range r.p {
174			rp[lp.Ident()] = lp
175		}
176
177		fixlen, rlen := len(fix.solution()), len(rp)
178		if fixlen != rlen {
179			// Different length, so they definitely disagree
180			t.Errorf("Solver reported %v package results, result expected %v", rlen, fixlen)
181		}
182
183		// Whether or not len is same, still have to verify that results agree
184		// Walk through fixture/expected results first
185		for id, flp := range fix.solution() {
186			if lp, exists := rp[id]; !exists {
187				t.Errorf("Project %q expected but missing from results", ppi(id))
188			} else {
189				// delete result from map so we skip it on the reverse pass
190				delete(rp, id)
191				if flp.Version() != lp.Version() {
192					t.Errorf("Expected version %q of project %q, but actual version was %q", pv(flp.Version()), ppi(id), pv(lp.Version()))
193				}
194
195				if !reflect.DeepEqual(lp.Packages(), flp.Packages()) {
196					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.Packages(), flp.Packages())
197				}
198			}
199		}
200
201		// Now walk through remaining actual results
202		for id, lp := range rp {
203			if _, exists := fix.solution()[id]; !exists {
204				t.Errorf("Unexpected project %s@%s present in results, with pkgs:\n\t%s", ppi(id), pv(lp.Version()), lp.Packages())
205			}
206		}
207	}
208
209	return soln, err
210}
211
212// This tests that, when a root lock is underspecified (has only a version) we
213// don't allow a match on that version from a rev in the manifest. We may allow
214// this in the future, but disallow it for now because going from an immutable
215// requirement to a mutable lock automagically is a bad direction that could
216// produce weird side effects.
217func TestRootLockNoVersionPairMatching(t *testing.T) {
218	fix := basicFixture{
219		n: "does not match unpaired lock versions with paired real versions",
220		ds: []depspec{
221			mkDepspec("root 0.0.0", "foo *"), // foo's constraint rewritten below to foorev
222			mkDepspec("foo 1.0.0", "bar 1.0.0"),
223			mkDepspec("foo 1.0.1 foorev", "bar 1.0.1"),
224			mkDepspec("foo 1.0.2 foorev", "bar 1.0.2"),
225			mkDepspec("bar 1.0.0"),
226			mkDepspec("bar 1.0.1"),
227			mkDepspec("bar 1.0.2"),
228		},
229		l: mklock(
230			"foo 1.0.1",
231		),
232		r: mksolution(
233			"foo 1.0.2 foorev",
234			"bar 1.0.2",
235		),
236	}
237
238	pd := fix.ds[0].deps[0]
239	pd.Constraint = Revision("foorev")
240	fix.ds[0].deps[0] = pd
241
242	sm := newdepspecSM(fix.ds, nil)
243
244	l2 := make(fixLock, 1)
245	copy(l2, fix.l)
246
247	l2lp := l2[0].(lockedProject)
248	l2lp.v = nil
249	l2[0] = l2lp
250
251	params := SolveParameters{
252		RootDir:         string(fix.ds[0].n),
253		RootPackageTree: fix.rootTree(),
254		Manifest:        fix.rootmanifest(),
255		Lock:            l2,
256		ProjectAnalyzer: naiveAnalyzer{},
257	}
258
259	res, err := fixSolve(params, sm, t)
260
261	fixtureSolveSimpleChecks(fix, res, err, t)
262}
263