From 2372bffc5de5b045af5a8d6786562d1968b01944 Mon Sep 17 00:00:00 2001 From: Tom Wilkie Date: Wed, 9 Dec 2015 11:50:30 +0000 Subject: [PATCH] Extend testing of procspy. --- common/fs/fs.go | 11 + probe/endpoint/procspy/example_test.go | 20 -- .../{fixture.go => fixture_internal_test.go} | 0 probe/endpoint/procspy/proc.go | 48 ++--- probe/endpoint/procspy/proc_internal_test.go | 57 +++++ test/fs/fs.go | 199 ++++++++++++++++++ 6 files changed, 291 insertions(+), 44 deletions(-) create mode 100644 common/fs/fs.go delete mode 100644 probe/endpoint/procspy/example_test.go rename probe/endpoint/procspy/{fixture.go => fixture_internal_test.go} (100%) create mode 100644 probe/endpoint/procspy/proc_internal_test.go create mode 100644 test/fs/fs.go diff --git a/common/fs/fs.go b/common/fs/fs.go new file mode 100644 index 0000000000..50e41a3ab8 --- /dev/null +++ b/common/fs/fs.go @@ -0,0 +1,11 @@ +package fs + +import ( + "io" + "os" +) + +// Open is a mockable version of os.Open +var Open = func(path string) (io.ReadWriteCloser, error) { + return os.Open(path) +} diff --git a/probe/endpoint/procspy/example_test.go b/probe/endpoint/procspy/example_test.go deleted file mode 100644 index 26bbd9d02b..0000000000 --- a/probe/endpoint/procspy/example_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package procspy_test - -import ( - "fmt" - - "github.com/weaveworks/procspy" -) - -func Example() { - lookupProcesses := true - cs, err := procspy.Connections(lookupProcesses) - if err != nil { - panic(err) - } - - fmt.Printf("TCP Connections:\n") - for c := cs.Next(); c != nil; c = cs.Next() { - fmt.Printf(" - %v\n", c) - } -} diff --git a/probe/endpoint/procspy/fixture.go b/probe/endpoint/procspy/fixture_internal_test.go similarity index 100% rename from probe/endpoint/procspy/fixture.go rename to probe/endpoint/procspy/fixture_internal_test.go diff --git a/probe/endpoint/procspy/proc.go b/probe/endpoint/procspy/proc.go index ac0e379572..8f00dc5c62 100644 --- a/probe/endpoint/procspy/proc.go +++ b/probe/endpoint/procspy/proc.go @@ -4,10 +4,13 @@ package procspy import ( "bytes" + "io/ioutil" "os" "path/filepath" "strconv" "syscall" + + "github.com/weaveworks/scope/common/fs" ) var ( @@ -19,17 +22,19 @@ func SetProcRoot(root string) { procRoot = root } +// made variables for mocking +var ( + readDir = ioutil.ReadDir + lstat = syscall.Lstat + stat = syscall.Stat + open = fs.Open +) + // walkProcPid walks over all numerical (PID) /proc entries, and sees if their // ./fd/* files are symlink to sockets. Returns a map from socket ID (inode) // to PID. Will return an error if /proc isn't there. func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) { - fh, err := os.Open(procRoot) - if err != nil { - return nil, err - } - - dirNames, err := fh.Readdirnames(-1) - fh.Close() + dirNames, err := readDir(procRoot) if err != nil { return nil, err } @@ -37,9 +42,10 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) { var ( res = map[uint64]Proc{} namespaces = map[uint64]struct{}{} - stat syscall.Stat_t + statT syscall.Stat_t ) - for _, dirName := range dirNames { + for _, entry := range dirNames { + dirName := entry.Name() pid, err := strconv.ParseUint(dirName, 10, 0) if err != nil { // Not a number, so not a PID subdir. @@ -47,41 +53,35 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) { } fdBase := filepath.Join(procRoot, dirName, "fd") - dfh, err := os.Open(fdBase) + fds, err := readDir(fdBase) if err != nil { // Process is be gone by now, or we don't have access. continue } - fdNames, err := dfh.Readdirnames(-1) - dfh.Close() - if err != nil { - continue - } - // Read network namespace, and if we haven't seen it before, // read /proc//net/tcp - err = syscall.Lstat(filepath.Join(procRoot, dirName, "/ns/net"), &stat) + err = lstat(filepath.Join(procRoot, dirName, "/ns/net"), &statT) if err != nil { continue } - if _, ok := namespaces[stat.Ino]; !ok { - namespaces[stat.Ino] = struct{}{} + if _, ok := namespaces[statT.Ino]; !ok { + namespaces[statT.Ino] = struct{}{} readFile(filepath.Join(procRoot, dirName, "/net/tcp"), buf) readFile(filepath.Join(procRoot, dirName, "/net/tcp6"), buf) } var name string - for _, fdName := range fdNames { + for _, fd := range fds { // Direct use of syscall.Stat() to save garbage. - err = syscall.Stat(filepath.Join(fdBase, fdName), &stat) + err = stat(filepath.Join(fdBase, fd.Name()), &statT) if err != nil { continue } // We want sockets only. - if stat.Mode&syscall.S_IFMT != syscall.S_IFSOCK { + if statT.Mode&syscall.S_IFMT != syscall.S_IFSOCK { continue } @@ -92,7 +92,7 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) { } } - res[stat.Ino] = Proc{ + res[statT.Ino] = Proc{ PID: uint(pid), Name: name, } @@ -104,7 +104,7 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) { // procName does a pid->name lookup. func procName(base string) string { - fh, err := os.Open(filepath.Join(base, "/comm")) + fh, err := open(filepath.Join(base, "/comm")) if err != nil { return "" } diff --git a/probe/endpoint/procspy/proc_internal_test.go b/probe/endpoint/procspy/proc_internal_test.go new file mode 100644 index 0000000000..f072e3596c --- /dev/null +++ b/probe/endpoint/procspy/proc_internal_test.go @@ -0,0 +1,57 @@ +package procspy + +import ( + "bytes" + "reflect" + "syscall" + "testing" + + "github.com/weaveworks/scope/test/fs" +) + +var mockFS = fs.Dir("", + fs.Dir("proc", + fs.Dir("1", + fs.Dir("fd", + fs.File{ + FName: "16", + FStat: syscall.Stat_t{ + Ino: 45, + Mode: syscall.S_IFSOCK, + }, + }, + ), + fs.File{ + FName: "comm", + FContents: "foo\n", + }, + fs.Dir("ns", + fs.File{ + FName: "net", + FStat: syscall.Stat_t{}, + }, + ), + ), + ), +) + +func TestWalkProcPid(t *testing.T) { + oldReadDir, oldLstat, oldStat, oldOpen := readDir, lstat, stat, open + defer func() { readDir, lstat, stat, open = oldReadDir, oldLstat, oldStat, oldOpen }() + readDir, lstat, stat, open = mockFS.ReadDir, mockFS.Lstat, mockFS.Stat, mockFS.Open + + buf := bytes.Buffer{} + have, err := walkProcPid(&buf) + if err != nil { + t.Fatal(err) + } + want := map[uint64]Proc{ + 16: { + PID: 1, + Name: "foo", + }, + } + if !reflect.DeepEqual(want, have) { + t.Fatalf("%s", have) + } +} diff --git a/test/fs/fs.go b/test/fs/fs.go new file mode 100644 index 0000000000..2a47a602e9 --- /dev/null +++ b/test/fs/fs.go @@ -0,0 +1,199 @@ +package fs + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "syscall" + "time" +) + +type mockInode struct{} + +type dir struct { + mockInode + name string + entries map[string]FS + stat syscall.Stat_t +} + +// File is a mock file +type File struct { + mockInode + FName string + FContents string + FStat syscall.Stat_t +} + +// FS is a mock filesystem +type FS interface { + os.FileInfo + ReadDir(string) ([]os.FileInfo, error) + ReadFile(string) ([]byte, error) + Lstat(string, *syscall.Stat_t) error + Stat(string, *syscall.Stat_t) error + Open(string) (io.ReadWriteCloser, error) +} + +// Dir creates a new directory with the given entries. +func Dir(name string, entries ...FS) FS { + result := dir{ + name: name, + entries: map[string]FS{}, + } + + for _, entry := range entries { + result.entries[entry.Name()] = entry + } + + return result +} + +func split(path string) (string, string) { + if !strings.HasPrefix(path, "/") { + panic(path) + } + + comps := strings.SplitN(path, "/", 3) + if len(comps) == 2 { + return comps[1], "/" + } + + return comps[1], "/" + comps[2] +} + +func (mockInode) Size() int64 { return 0 } +func (mockInode) Mode() os.FileMode { return 0 } +func (mockInode) ModTime() time.Time { return time.Now() } +func (mockInode) Sys() interface{} { return nil } + +func (p dir) Name() string { return p.name } +func (p dir) IsDir() bool { return true } + +func (p dir) ReadDir(path string) ([]os.FileInfo, error) { + if path == "/" { + result := []os.FileInfo{} + for _, v := range p.entries { + result = append(result, v) + } + return result, nil + } + + head, tail := split(path) + fs, ok := p.entries[head] + if !ok { + return nil, fmt.Errorf("Not found: %s", path) + } + + return fs.ReadDir(tail) +} + +func (p dir) ReadFile(path string) ([]byte, error) { + if path == "/" { + return nil, fmt.Errorf("I'm a directory!") + } + + head, tail := split(path) + fs, ok := p.entries[head] + if !ok { + return nil, fmt.Errorf("Not found: %s", path) + } + + return fs.ReadFile(tail) +} + +func (p dir) Lstat(path string, stat *syscall.Stat_t) error { + if path == "/" { + return nil + } + + head, tail := split(path) + fs, ok := p.entries[head] + if !ok { + return fmt.Errorf("Not found: %s", path) + } + + return fs.Lstat(tail, stat) +} + +func (p dir) Stat(path string, stat *syscall.Stat_t) error { + if path == "/" { + return nil + } + + head, tail := split(path) + fs, ok := p.entries[head] + if !ok { + return fmt.Errorf("Not found: %s", path) + } + + return fs.Stat(tail, stat) +} + +func (p dir) Open(path string) (io.ReadWriteCloser, error) { + if path == "/" { + return nil, fmt.Errorf("I'm a directory!") + } + + head, tail := split(path) + fs, ok := p.entries[head] + if !ok { + return nil, fmt.Errorf("Not found: %s", path) + } + + return fs.Open(tail) +} + +// Name implements os.FileInfo +func (p File) Name() string { return p.FName } + +// IsDir implements os.FileInfo +func (p File) IsDir() bool { return false } + +// ReadDir implements FS +func (p File) ReadDir(path string) ([]os.FileInfo, error) { + return nil, fmt.Errorf("I'm a file!") +} + +// ReadFile implements FS +func (p File) ReadFile(path string) ([]byte, error) { + if path != "/" { + return nil, fmt.Errorf("I'm a file!") + } + return []byte(p.FContents), nil +} + +// Lstat implements FS +func (p File) Lstat(path string, stat *syscall.Stat_t) error { + if path != "/" { + return fmt.Errorf("I'm a file!") + } + *stat = p.FStat + return nil +} + +// Stat implements FS +func (p File) Stat(path string, stat *syscall.Stat_t) error { + if path != "/" { + return fmt.Errorf("I'm a file!") + } + *stat = p.FStat + return nil +} + +// Open implements FS +func (p File) Open(path string) (io.ReadWriteCloser, error) { + if path != "/" { + return nil, fmt.Errorf("I'm a file!") + } + return struct { + io.ReadWriter + io.Closer + }{ + bytes.NewBuffer([]byte(p.FContents)), + ioutil.NopCloser(nil), + }, nil +}