Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit 5aa4ffe

Browse files
authored
Merge pull request #1079 from sdboyer/warn-case-mismatch
Add satisfiability check for case variants
2 parents d62440d + cbf0318 commit 5aa4ffe

11 files changed

+677
-45
lines changed

internal/gps/identifier.go

+3
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,9 @@ type completeDep struct {
217217
pl []string
218218
}
219219

220+
// dependency represents an incomplete edge in the depgraph. It has a
221+
// fully-realized atom as the depender (the tail/source of the edge), and a set
222+
// of requirements that any atom to be attached at the head/target must satisfy.
220223
type dependency struct {
221224
depender atom
222225
dep completeDep

internal/gps/manager_test.go

+102-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package gps
66

77
import (
8+
"bytes"
89
"context"
910
"fmt"
1011
"io/ioutil"
@@ -13,9 +14,11 @@ import (
1314
"path"
1415
"path/filepath"
1516
"runtime"
17+
"sort"
1618
"sync"
1719
"sync/atomic"
1820
"testing"
21+
"text/tabwriter"
1922
"time"
2023

2124
"github.com/golang/dep/internal/test"
@@ -349,8 +352,8 @@ func TestMgrMethodsFailWithBadPath(t *testing.T) {
349352
}
350353

351354
type sourceCreationTestFixture struct {
352-
roots []ProjectIdentifier
353-
urlcount, srccount int
355+
roots []ProjectIdentifier
356+
namecount, srccount int
354357
}
355358

356359
func (f sourceCreationTestFixture) run(t *testing.T) {
@@ -365,13 +368,33 @@ func (f sourceCreationTestFixture) run(t *testing.T) {
365368
}
366369
}
367370

368-
if len(sm.srcCoord.nameToURL) != f.urlcount {
369-
t.Errorf("want %v names in the name->url map, but got %v. contents: \n%v", f.urlcount, len(sm.srcCoord.nameToURL), sm.srcCoord.nameToURL)
371+
if len(sm.srcCoord.nameToURL) != f.namecount {
372+
t.Errorf("want %v names in the name->url map, but got %v. contents: \n%v", f.namecount, len(sm.srcCoord.nameToURL), sm.srcCoord.nameToURL)
370373
}
371374

372375
if len(sm.srcCoord.srcs) != f.srccount {
373376
t.Errorf("want %v gateways in the sources map, but got %v", f.srccount, len(sm.srcCoord.srcs))
374377
}
378+
379+
var keys []string
380+
for k := range sm.srcCoord.nameToURL {
381+
keys = append(keys, k)
382+
}
383+
sort.Strings(keys)
384+
385+
var buf bytes.Buffer
386+
w := tabwriter.NewWriter(&buf, 0, 4, 2, ' ', 0)
387+
fmt.Fprint(w, "NAME\tMAPPED URL\n")
388+
for _, r := range keys {
389+
fmt.Fprintf(w, "%s\t%s\n", r, sm.srcCoord.nameToURL[r])
390+
}
391+
w.Flush()
392+
t.Log("\n", buf.String())
393+
394+
t.Log("SRC KEYS")
395+
for k := range sm.srcCoord.srcs {
396+
t.Log(k)
397+
}
375398
}
376399

377400
// This test is primarily about making sure that the logic around folding
@@ -390,16 +413,27 @@ func TestSourceCreationCounts(t *testing.T) {
390413
mkPI("gopkg.in/sdboyer/gpkt.v2"),
391414
mkPI("gopkg.in/sdboyer/gpkt.v3"),
392415
},
393-
urlcount: 6,
394-
srccount: 3,
416+
namecount: 6,
417+
srccount: 3,
395418
},
396419
"gopkgin separation from github": {
397420
roots: []ProjectIdentifier{
398421
mkPI("gopkg.in/sdboyer/gpkt.v1"),
399422
mkPI("github.com/sdboyer/gpkt"),
400423
},
401-
urlcount: 4,
402-
srccount: 2,
424+
namecount: 4,
425+
srccount: 2,
426+
},
427+
"case variance across path and URL-based access": {
428+
roots: []ProjectIdentifier{
429+
ProjectIdentifier{ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"), Source: "https://github.com/Sdboyer/gpkt"},
430+
ProjectIdentifier{ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"), Source: "https://github.com/SdbOyer/gpkt"},
431+
mkPI("github.com/sdboyer/gpkt"),
432+
ProjectIdentifier{ProjectRoot: ProjectRoot("github.com/sdboyer/gpkt"), Source: "https://github.com/sdboyeR/gpkt"},
433+
mkPI("github.com/sdboyeR/gpkt"),
434+
},
435+
namecount: 6,
436+
srccount: 1,
403437
},
404438
}
405439

@@ -467,13 +501,70 @@ func TestGetSources(t *testing.T) {
467501
})
468502

469503
// nine entries (of which three are dupes): for each vcs, raw import path,
470-
// the https url, and the http url
471-
if len(sm.srcCoord.nameToURL) != 9 {
472-
t.Errorf("Should have nine discrete entries in the nameToURL map, got %v", len(sm.srcCoord.nameToURL))
504+
// the https url, and the http url. also three more from case folding of
505+
// github.com/Masterminds/VCSTestRepo -> github.com/masterminds/vcstestrepo
506+
if len(sm.srcCoord.nameToURL) != 12 {
507+
t.Errorf("Should have twelve discrete entries in the nameToURL map, got %v", len(sm.srcCoord.nameToURL))
473508
}
474509
clean()
475510
}
476511

512+
func TestFSCaseSensitivityConvergesSources(t *testing.T) {
513+
if testing.Short() {
514+
t.Skip("Skipping slow test in short mode")
515+
}
516+
517+
f := func(name string, pi1, pi2 ProjectIdentifier) {
518+
t.Run(name, func(t *testing.T) {
519+
t.Parallel()
520+
sm, clean := mkNaiveSM(t)
521+
defer clean()
522+
523+
sm.SyncSourceFor(pi1)
524+
sg1, err := sm.srcCoord.getSourceGatewayFor(context.Background(), pi1)
525+
if err != nil {
526+
t.Fatal(err)
527+
}
528+
529+
sm.SyncSourceFor(pi2)
530+
sg2, err := sm.srcCoord.getSourceGatewayFor(context.Background(), pi2)
531+
if err != nil {
532+
t.Fatal(err)
533+
}
534+
535+
path1 := sg1.src.(*gitSource).repo.LocalPath()
536+
t.Log("path1:", path1)
537+
stat1, err := os.Stat(path1)
538+
if err != nil {
539+
t.Fatal(err)
540+
}
541+
path2 := sg2.src.(*gitSource).repo.LocalPath()
542+
t.Log("path2:", path2)
543+
stat2, err := os.Stat(path2)
544+
if err != nil {
545+
t.Fatal(err)
546+
}
547+
548+
same, count := os.SameFile(stat1, stat2), len(sm.srcCoord.srcs)
549+
if same && count != 1 {
550+
t.Log("are same, count", count)
551+
t.Fatal("on case-insensitive filesystem, case-varying sources should have been folded together but were not")
552+
}
553+
if !same && count != 2 {
554+
t.Log("not same, count", count)
555+
t.Fatal("on case-sensitive filesystem, case-varying sources should not have been folded together, but were")
556+
}
557+
})
558+
}
559+
560+
folded := mkPI("github.com/sdboyer/deptest").normalize()
561+
casevar1 := mkPI("github.com/Sdboyer/deptest").normalize()
562+
casevar2 := mkPI("github.com/SdboyeR/deptest").normalize()
563+
f("folded first", folded, casevar1)
564+
f("folded second", casevar1, folded)
565+
f("both unfolded", casevar1, casevar2)
566+
}
567+
477568
// Regression test for #32
478569
func TestGetInfoListVersionsOrdering(t *testing.T) {
479570
// This test is quite slow, skip it on -short

internal/gps/satisfy.go

+54
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ func (s *solver) check(a atomWithPackages, pkgonly bool) error {
5454
if err = s.checkIdentMatches(a, dep); err != nil {
5555
return err
5656
}
57+
if err = s.checkRootCaseConflicts(a, dep); err != nil {
58+
return err
59+
}
5760
if err = s.checkDepsConstraintsAllowable(a, dep); err != nil {
5861
return err
5962
}
@@ -218,6 +221,57 @@ func (s *solver) checkIdentMatches(a atomWithPackages, cdep completeDep) error {
218221
return nil
219222
}
220223

224+
// checkRootCaseConflicts ensures that the ProjectRoot specified in the completeDep
225+
// does not have case conflicts with any existing dependencies.
226+
//
227+
// We only need to check the ProjectRoot, rather than any packages therein, as
228+
// the later check for package existence is case-sensitive.
229+
func (s *solver) checkRootCaseConflicts(a atomWithPackages, cdep completeDep) error {
230+
pr := cdep.workingConstraint.Ident.ProjectRoot
231+
hasConflict, current := s.sel.findCaseConflicts(pr)
232+
if !hasConflict {
233+
return nil
234+
}
235+
236+
curid, _ := s.sel.getIdentFor(current)
237+
deps := s.sel.getDependenciesOn(curid)
238+
for _, d := range deps {
239+
s.fail(d.depender.id)
240+
}
241+
242+
// If a project has multiple packages that import each other, we treat that
243+
// as establishing a canonical case variant for the ProjectRoot. It's possible,
244+
// however, that that canonical variant is not the same one that others
245+
// imported it under. If that's the situation, then we'll have arrived here
246+
// when visiting the project, not its dependers, having misclassified its
247+
// internal imports as external. That means the atomWithPackages will
248+
// be the wrong case variant induced by the importers, and the cdep will be
249+
// a link pointing back at the canonical case variant.
250+
//
251+
// If this is the case, use a special failure, wrongCaseFailure, that
252+
// makes a stronger statement as to the correctness of case variants.
253+
//
254+
// TODO(sdboyer) This approach to marking failure is less than great, as
255+
// this will mark the current atom as failed, as well, causing the
256+
// backtracker to work through it. While that could prove fruitful, it's
257+
// quite likely just to be wasted effort. Addressing this - if that's a good
258+
// idea - would entail creating another path back out of checking to enable
259+
// backjumping directly to the incorrect importers.
260+
if current == a.a.id.ProjectRoot {
261+
return &wrongCaseFailure{
262+
correct: pr,
263+
goal: dependency{depender: a.a, dep: cdep},
264+
badcase: deps,
265+
}
266+
}
267+
268+
return &caseMismatchFailure{
269+
goal: dependency{depender: a.a, dep: cdep},
270+
current: current,
271+
failsib: deps,
272+
}
273+
}
274+
221275
// checkPackageImportsFromDepExist ensures that, if the dep is already selected,
222276
// the newly-required set of packages being placed on it exist and are valid.
223277
func (s *solver) checkPackageImportsFromDepExist(a atomWithPackages, cdep completeDep) error {

internal/gps/selection.go

+36-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,19 @@
55
package gps
66

77
type selection struct {
8+
// projects is a stack of the atoms that have currently been selected by the
9+
// solver. It can also be thought of as the vertex set of the current
10+
// selection graph.
811
projects []selected
9-
deps map[ProjectRoot][]dependency
10-
vu *versionUnifier
12+
// deps records the set of dependers on a given ProjectRoot. It is
13+
// essentially an adjacency list of *inbound* edges.
14+
deps map[ProjectRoot][]dependency
15+
// foldRoots records a mapping from a canonical, case-folded form of
16+
// ProjectRoots to the particular case variant that has currently been
17+
// selected.
18+
foldRoots map[string]ProjectRoot
19+
// The versoinUnifier in use for this solve run.
20+
vu *versionUnifier
1121
}
1222

1323
type selected struct {
@@ -59,13 +69,35 @@ func (s *selection) popSelection() (atomWithPackages, bool) {
5969
return sel.a, sel.first
6070
}
6171

72+
// findCaseConflicts checks to see if the given ProjectRoot has a
73+
// case-insensitive overlap with another, different ProjectRoot that's already
74+
// been picked.
75+
func (s *selection) findCaseConflicts(pr ProjectRoot) (bool, ProjectRoot) {
76+
if current, has := s.foldRoots[toFold(string(pr))]; has && pr != current {
77+
return true, current
78+
}
79+
80+
return false, ""
81+
}
82+
6283
func (s *selection) pushDep(dep dependency) {
63-
s.deps[dep.dep.Ident.ProjectRoot] = append(s.deps[dep.dep.Ident.ProjectRoot], dep)
84+
pr := dep.dep.Ident.ProjectRoot
85+
deps := s.deps[pr]
86+
if len(deps) == 0 {
87+
s.foldRoots[toFold(string(pr))] = pr
88+
}
89+
90+
s.deps[pr] = append(deps, dep)
6491
}
6592

6693
func (s *selection) popDep(id ProjectIdentifier) (dep dependency) {
6794
deps := s.deps[id.ProjectRoot]
68-
dep, s.deps[id.ProjectRoot] = deps[len(deps)-1], deps[:len(deps)-1]
95+
dlen := len(deps)
96+
if dlen == 1 {
97+
delete(s.foldRoots, toFold(string(id.ProjectRoot)))
98+
}
99+
100+
dep, s.deps[id.ProjectRoot] = deps[dlen-1], deps[:dlen-1]
69101
return dep
70102
}
71103

0 commit comments

Comments
 (0)