Skip to content

Commit

Permalink
Merge pull request sdboyer/gps#199 from spenczar/fix_symlink_vendor_dir
Browse files Browse the repository at this point in the history
Follow symlinks when stripping vendor directories
  • Loading branch information
sdboyer authored Apr 13, 2017
2 parents b440b3c + 1d15e2a commit 2d3077a
Show file tree
Hide file tree
Showing 7 changed files with 584 additions and 12 deletions.
154 changes: 154 additions & 0 deletions filesystem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package gps

import (
"os"
"path/filepath"
"testing"
)

// This file contains utilities for running tests around file system state.

// fspath represents a file system path in an OS-agnostic way.
type fsPath []string

func (f fsPath) String() string { return filepath.Join(f...) }

func (f fsPath) prepend(prefix string) fsPath {
p := fsPath{filepath.FromSlash(prefix)}
return append(p, f...)
}

type fsTestCase struct {
before, after filesystemState
}

// filesystemState represents the state of a file system. It has a setup method
// which inflates its state to the actual host file system, and an assert
// method which checks that the actual file system matches the described state.
type filesystemState struct {
root string
dirs []fsPath
files []fsPath
links []fsLink
}

// assert makes sure that the fs state matches the state of the actual host
// file system
func (fs filesystemState) assert(t *testing.T) {
dirMap := make(map[string]bool)
fileMap := make(map[string]bool)
linkMap := make(map[string]bool)

for _, d := range fs.dirs {
dirMap[d.prepend(fs.root).String()] = true
}
for _, f := range fs.files {
fileMap[f.prepend(fs.root).String()] = true
}
for _, l := range fs.links {
linkMap[l.path.prepend(fs.root).String()] = true
}

err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error {
if err != nil {
t.Errorf("filepath.Walk path=%q err=%q", path, err)
return err
}

if path == fs.root {
return nil
}

// Careful! Have to check whether the path is a symlink first because, on
// windows, a symlink to a directory will return 'true' for info.IsDir().
if (info.Mode() & os.ModeSymlink) != 0 {
if linkMap[path] {
delete(linkMap, path)
} else {
t.Errorf("unexpected symlink exists %q", path)
}
return nil
}

if info.IsDir() {
if dirMap[path] {
delete(dirMap, path)
} else {
t.Errorf("unexpected directory exists %q", path)
}
return nil
}

if fileMap[path] {
delete(fileMap, path)
} else {
t.Errorf("unexpected file exists %q", path)
}
return nil
})

if err != nil {
t.Errorf("filesystem.Walk err=%q", err)
}

for d := range dirMap {
t.Errorf("could not find expected directory %q", d)
}
for f := range fileMap {
t.Errorf("could not find expected file %q", f)
}
for l := range linkMap {
t.Errorf("could not find expected symlink %q", l)
}
}

// fsLink represents a symbolic link.
type fsLink struct {
path fsPath
to string
}

// setup inflates fs onto the actual host file system
func (fs filesystemState) setup(t *testing.T) {
fs.setupDirs(t)
fs.setupFiles(t)
fs.setupLinks(t)
}

func (fs filesystemState) setupDirs(t *testing.T) {
for _, dir := range fs.dirs {
p := dir.prepend(fs.root)
if err := os.MkdirAll(p.String(), 0777); err != nil {
t.Fatalf("os.MkdirAll(%q, 0777) err=%q", p, err)
}
}
}

func (fs filesystemState) setupFiles(t *testing.T) {
for _, file := range fs.files {
p := file.prepend(fs.root)
f, err := os.Create(p.String())
if err != nil {
t.Fatalf("os.Create(%q) err=%q", p, err)
}
if err := f.Close(); err != nil {
t.Fatalf("file %q Close() err=%q", p, err)
}
}
}

func (fs filesystemState) setupLinks(t *testing.T) {
for _, link := range fs.links {
p := link.path.prepend(fs.root)

// On Windows, relative symlinks confuse filepath.Walk. This is golang/go
// issue 17540. So, we'll just sigh and do absolute links, assuming they are
// relative to the directory of link.path.
dir := filepath.Dir(p.String())
to := filepath.Join(dir, link.to)

if err := os.Symlink(to, p.String()); err != nil {
t.Fatalf("os.Symlink(%q, %q) err=%q", to, p, err)
}
}
}
26 changes: 26 additions & 0 deletions strip_vendor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//+build !windows

package gps

import "os"

func stripVendor(path string, info os.FileInfo, err error) error {
if info.Name() == "vendor" {
if _, err := os.Lstat(path); err == nil {
if (info.Mode() & os.ModeSymlink) != 0 {
realInfo, err := os.Stat(path)
if err != nil {
return err
}
if realInfo.IsDir() {
return os.Remove(path)
}
}
if info.IsDir() {
return removeAll(path)
}
}
}

return nil
}
142 changes: 142 additions & 0 deletions strip_vendor_nonwindows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// +build !windows

package gps

import "testing"

func TestStripVendorSymlinks(t *testing.T) {
t.Run("vendor symlink", stripVendorTestCase(fsTestCase{
before: filesystemState{
dirs: []fsPath{
fsPath{"package"},
fsPath{"package", "_vendor"},
},
links: []fsLink{
fsLink{
path: fsPath{"package", "vendor"},
to: "_vendor",
},
},
},
after: filesystemState{
dirs: []fsPath{
fsPath{"package"},
fsPath{"package", "_vendor"},
},
},
}))

t.Run("nonvendor symlink", stripVendorTestCase(fsTestCase{
before: filesystemState{
dirs: []fsPath{
fsPath{"package"},
fsPath{"package", "_vendor"},
},
links: []fsLink{
fsLink{
path: fsPath{"package", "link"},
to: "_vendor",
},
},
},
after: filesystemState{
dirs: []fsPath{
fsPath{"package"},
fsPath{"package", "_vendor"},
},
links: []fsLink{
fsLink{
path: fsPath{"package", "link"},
to: "_vendor",
},
},
},
}))

t.Run("vendor symlink to file", stripVendorTestCase(fsTestCase{
before: filesystemState{
files: []fsPath{
fsPath{"file"},
},
links: []fsLink{
fsLink{
path: fsPath{"vendor"},
to: "file",
},
},
},
after: filesystemState{
files: []fsPath{
fsPath{"file"},
},
links: []fsLink{
fsLink{
path: fsPath{"vendor"},
to: "file",
},
},
},
}))

t.Run("chained symlinks", stripVendorTestCase(fsTestCase{
before: filesystemState{
dirs: []fsPath{
fsPath{"_vendor"},
},
links: []fsLink{
fsLink{
path: fsPath{"vendor"},
to: "vendor2",
},
fsLink{
path: fsPath{"vendor2"},
to: "_vendor",
},
},
},
after: filesystemState{
dirs: []fsPath{
fsPath{"_vendor"},
},
links: []fsLink{
fsLink{
path: fsPath{"vendor2"},
to: "_vendor",
},
},
},
}))

t.Run("circular symlinks", stripVendorTestCase(fsTestCase{
before: filesystemState{
dirs: []fsPath{
fsPath{"package"},
},
links: []fsLink{
fsLink{
path: fsPath{"package", "link1"},
to: "link2",
},
fsLink{
path: fsPath{"package", "link2"},
to: "link1",
},
},
},
after: filesystemState{
dirs: []fsPath{
fsPath{"package"},
},
links: []fsLink{
fsLink{
path: fsPath{"package", "link1"},
to: "link2",
},
fsLink{
path: fsPath{"package", "link2"},
to: "link1",
},
},
},
}))
}
67 changes: 67 additions & 0 deletions strip_vendor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package gps

import (
"io/ioutil"
"os"
"path/filepath"
"testing"
)

func stripVendorTestCase(tc fsTestCase) func(*testing.T) {
return func(t *testing.T) {
tempDir, err := ioutil.TempDir("", "TestStripVendor")
if err != nil {
t.Fatalf("ioutil.TempDir err=%q", err)
}
defer func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Errorf("os.RemoveAll(%q) err=%q", tempDir, err)
}
}()
tc.before.root = tempDir
tc.after.root = tempDir

tc.before.setup(t)

if err := filepath.Walk(tempDir, stripVendor); err != nil {
t.Errorf("filepath.Walk err=%q", err)
}

tc.after.assert(t)
}
}

func TestStripVendorDirectory(t *testing.T) {
t.Run("vendor directory", stripVendorTestCase(fsTestCase{
before: filesystemState{
dirs: []fsPath{
fsPath{"package"},
fsPath{"package", "vendor"},
},
},
after: filesystemState{
dirs: []fsPath{
fsPath{"package"},
},
},
}))

t.Run("vendor file", stripVendorTestCase(fsTestCase{
before: filesystemState{
dirs: []fsPath{
fsPath{"package"},
},
files: []fsPath{
fsPath{"package", "vendor"},
},
},
after: filesystemState{
dirs: []fsPath{
fsPath{"package"},
},
files: []fsPath{
fsPath{"package", "vendor"},
},
},
}))
}
Loading

0 comments on commit 2d3077a

Please sign in to comment.